mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
Compare commits
1281 Commits
beta
...
get-cached
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc9f0b961d | ||
|
|
7969e047c7 | ||
|
|
f0d6d9d177 | ||
|
|
0b793d82fe | ||
|
|
f6d51462eb | ||
|
|
5bdacbab61 | ||
|
|
e239cc962b | ||
|
|
6ebd4fcf5b | ||
|
|
ef427fa966 | ||
|
|
f4383a11d7 | ||
|
|
77b6377473 | ||
|
|
7bf3cf999f | ||
|
|
4ab611de0c | ||
|
|
d7745a418f | ||
|
|
058a5a43ba | ||
|
|
878dbd81b1 | ||
|
|
3c64ed1eb2 | ||
|
|
ee50f1238c | ||
|
|
1af2513fc0 | ||
|
|
9c0d26bc84 | ||
|
|
4d9053a83b | ||
|
|
3f7e98c277 | ||
|
|
aebc877e7b | ||
|
|
eef5f3fec2 | ||
|
|
16a1677fde | ||
|
|
f199816fcd | ||
|
|
5e74e17b41 | ||
|
|
98b041e84a | ||
|
|
bba9c8f652 | ||
|
|
1a05fe6ae1 | ||
|
|
16fcbf66ee | ||
|
|
b7fd4e90e2 | ||
|
|
b6341c10cc | ||
|
|
08487b0fcc | ||
|
|
b084dde22a | ||
|
|
5229a7c997 | ||
|
|
e56c56e2fe | ||
|
|
7374f956cf | ||
|
|
287df42994 | ||
|
|
06e514cc2e | ||
|
|
dffd8b5fec | ||
|
|
2a87337875 | ||
|
|
a74f79118f | ||
|
|
a13ed0bec3 | ||
|
|
f8ca45f0f2 | ||
|
|
4bf92a34f6 | ||
|
|
4f1c84004a | ||
|
|
1bd430598d | ||
|
|
3bc654bf97 | ||
|
|
3906acb83d | ||
|
|
cfd62ac137 | ||
|
|
cd540dfae9 | ||
|
|
74ad9ec8bf | ||
|
|
4f8a3fe5b9 | ||
|
|
09ca0e6ef0 | ||
|
|
fae2b5acfa | ||
|
|
d35a3eab6c | ||
|
|
7f7f47497a | ||
|
|
eb14ac3741 | ||
|
|
22334faba3 | ||
|
|
d08fd297e8 | ||
|
|
0dd664bfbf | ||
|
|
1602932d72 | ||
|
|
98c8b7d2b0 | ||
|
|
b9ae24c42d | ||
|
|
b387fd2bd4 | ||
|
|
818f4540fd | ||
|
|
49a97dbb66 | ||
|
|
a8b72c1d5f | ||
|
|
765b8dc97b | ||
|
|
5123697afe | ||
|
|
2a2a9d7941 | ||
|
|
2873aa5f81 | ||
|
|
795c925ba1 | ||
|
|
d6ace3f695 | ||
|
|
dd04759de7 | ||
|
|
10fbde84ba | ||
|
|
2b5652e1e4 | ||
|
|
18796ae44e | ||
|
|
a67692dc29 | ||
|
|
1efd756a55 | ||
|
|
29671acdb6 | ||
|
|
e82240a60e | ||
|
|
72083c8614 | ||
|
|
8c2c1e534c | ||
|
|
bfc01d957b | ||
|
|
4a12d045e4 | ||
|
|
2d78b2c219 | ||
|
|
3049bb0b9f | ||
|
|
34ab8152fb | ||
|
|
fb58c50fb7 | ||
|
|
955f917015 | ||
|
|
12c7df98e4 | ||
|
|
889c29a163 | ||
|
|
886c1370e7 | ||
|
|
febcc0a673 | ||
|
|
98cad6bf8d | ||
|
|
7e5daedc8c | ||
|
|
da3fe6f7ea | ||
|
|
f612ce262f | ||
|
|
24ccfca279 | ||
|
|
34b3c3982b | ||
|
|
7f732c94da | ||
|
|
bdc49a65aa | ||
|
|
73d82dd0ba | ||
|
|
dfa4403c8a | ||
|
|
b8f3b19499 | ||
|
|
448718d112 | ||
|
|
6de55df4bc | ||
|
|
189fe26667 | ||
|
|
7230884116 | ||
|
|
d7fba81f8f | ||
|
|
29ac13185c | ||
|
|
3a49ee83ce | ||
|
|
95cbbc3b45 | ||
|
|
2a5c7d139f | ||
|
|
b74863873b | ||
|
|
7b46fe9cc8 | ||
|
|
afc8c69a82 | ||
|
|
38bbad6e88 | ||
|
|
1df47fd415 | ||
|
|
faf21c5fff | ||
|
|
2aee580795 | ||
|
|
404c027546 | ||
|
|
04e59c6df2 | ||
|
|
835042b794 | ||
|
|
907490e266 | ||
|
|
80fe167646 | ||
|
|
d30631f991 | ||
|
|
8956ab85f9 | ||
|
|
2cdc9e9f5f | ||
|
|
13c623755c | ||
|
|
bdfceec520 | ||
|
|
941dace7f9 | ||
|
|
07693e54af | ||
|
|
b6132f2497 | ||
|
|
b3fe3d02c9 | ||
|
|
e880b18bb1 | ||
|
|
74a299eef7 | ||
|
|
300428ddfb | ||
|
|
1c27f8251e | ||
|
|
92badd3722 | ||
|
|
8a80f0b3dd | ||
|
|
fcc74b63d3 | ||
|
|
d7155e6662 | ||
|
|
42c3841639 | ||
|
|
c331713401 | ||
|
|
002d9c1747 | ||
|
|
2885ceceb1 | ||
|
|
22a644ba01 | ||
|
|
bab120a75d | ||
|
|
7a07c82f06 | ||
|
|
e881d2f6cf | ||
|
|
c8d003a08f | ||
|
|
e2cc404571 | ||
|
|
be71eaae47 | ||
|
|
ed31a452b2 | ||
|
|
f51ee7f3a0 | ||
|
|
9d1dc97766 | ||
|
|
b78729f685 | ||
|
|
44a76e59f9 | ||
|
|
1504e36a68 | ||
|
|
80348ef190 | ||
|
|
a3c14748d3 | ||
|
|
3c0143af92 | ||
|
|
22a93a9c39 | ||
|
|
e8866a6431 | ||
|
|
455ed79872 | ||
|
|
3d17c531d7 | ||
|
|
dfe90243d6 | ||
|
|
bf1db50667 | ||
|
|
a2565a7c83 | ||
|
|
947d01a3c0 | ||
|
|
be11d82c9c | ||
|
|
7a0e7fff13 | ||
|
|
81fb71b7f7 | ||
|
|
b10f5ec99f | ||
|
|
5abe7bdeef | ||
|
|
54be651415 | ||
|
|
cdbf6d7ae7 | ||
|
|
50349edf4d | ||
|
|
da307c1b40 | ||
|
|
b50b96bd1d | ||
|
|
92654fc5aa | ||
|
|
36b2de216b | ||
|
|
8745c1016e | ||
|
|
f5a58c1ff0 | ||
|
|
d9e72049ae | ||
|
|
927ca01161 | ||
|
|
3ea8d0b01c | ||
|
|
c52d33e331 | ||
|
|
fd36606acc | ||
|
|
1c6f4a79e0 | ||
|
|
7896d274a3 | ||
|
|
6937c8ecb4 | ||
|
|
f02b9566c5 | ||
|
|
c9936c2b7e | ||
|
|
bbd9e5e07c | ||
|
|
476fb7ec4e | ||
|
|
7435274be2 | ||
|
|
08d2ea6a10 | ||
|
|
41b7ed6938 | ||
|
|
7a311a181b | ||
|
|
ddcb597710 | ||
|
|
9c75f29875 | ||
|
|
343f3885f7 | ||
|
|
ed7dfeab84 | ||
|
|
8de27b3674 | ||
|
|
f56b0a5f6d | ||
|
|
0a27e1254f | ||
|
|
3f9b256fcb | ||
|
|
9ea9859150 | ||
|
|
03e3f95d2e | ||
|
|
e721b0af92 | ||
|
|
e18c589de3 | ||
|
|
aea34264a9 | ||
|
|
8d3a04235d | ||
|
|
9c4088b24c | ||
|
|
1e7ee4e0a1 | ||
|
|
ec92f110b3 | ||
|
|
2aa5eb85ad | ||
|
|
2815f02382 | ||
|
|
8bd7c8dd41 | ||
|
|
269dcf071f | ||
|
|
997ec7f0bc | ||
|
|
d9c26bb77f | ||
|
|
c0fc3a19c8 | ||
|
|
ce638c39e3 | ||
|
|
6b651cd5e4 | ||
|
|
4560f31010 | ||
|
|
c97a32e24b | ||
|
|
8a005bc5a1 | ||
|
|
20aabee72e | ||
|
|
a00c2345ee | ||
|
|
cb35b3624a | ||
|
|
c6f59a7aa6 | ||
|
|
bf296ad797 | ||
|
|
256540934b | ||
|
|
3c07c0818d | ||
|
|
a01d18ace1 | ||
|
|
55e02f01dc | ||
|
|
fe6ccad485 | ||
|
|
11fe79312d | ||
|
|
bdb2338b5b | ||
|
|
bbafb048d0 | ||
|
|
9fc2fa51bd | ||
|
|
d8ec50345a | ||
|
|
9f1cc09ca8 | ||
|
|
5dcc3db36b | ||
|
|
898b73ffc8 | ||
|
|
c5d49a9d34 | ||
|
|
ef9f828d35 | ||
|
|
c691764205 | ||
|
|
2c940d4fd6 | ||
|
|
54bd55d45d | ||
|
|
0b846b15b1 | ||
|
|
269eb7e154 | ||
|
|
97bc19e4ae | ||
|
|
2656cc7842 | ||
|
|
ba94818415 | ||
|
|
ac759a6eed | ||
|
|
1839b346a6 | ||
|
|
c1ffe7f8e6 | ||
|
|
833b4d10bd | ||
|
|
ce98c336c9 | ||
|
|
d05619990a | ||
|
|
8033e41d4a | ||
|
|
60f4eab759 | ||
|
|
d7656ea985 | ||
|
|
e402998577 | ||
|
|
073f75efa3 | ||
|
|
da414f7eb3 | ||
|
|
270b89830a | ||
|
|
74ce7ca416 | ||
|
|
3f4338cb51 | ||
|
|
30ee41fd0e | ||
|
|
4965fec55c | ||
|
|
18dff8455c | ||
|
|
fe16f06aee | ||
|
|
48c1c05a93 | ||
|
|
38dee1166d | ||
|
|
0c6fc68eae | ||
|
|
223611d89e | ||
|
|
6f5141d5fb | ||
|
|
a6ac7d9c4e | ||
|
|
9b35736be3 | ||
|
|
9f54cb35f4 | ||
|
|
329bffb127 | ||
|
|
e2542f41b5 | ||
|
|
efc7b9d4a5 | ||
|
|
72915760c4 | ||
|
|
e9d7a946c5 | ||
|
|
714e5e0456 | ||
|
|
26e8642aca | ||
|
|
68dfb4ee86 | ||
|
|
f1ff789334 | ||
|
|
1f45d5b8e4 | ||
|
|
c20052f314 | ||
|
|
c28d87d59c | ||
|
|
237ddcba9a | ||
|
|
eadb5b6461 | ||
|
|
faebabe3c7 | ||
|
|
02c510b07f | ||
|
|
63541970eb | ||
|
|
a8a5605fe1 | ||
|
|
0c0ddc10ee | ||
|
|
9bd5ff69ef | ||
|
|
eadf351e82 | ||
|
|
e3afa294af | ||
|
|
582894cdc3 | ||
|
|
2788c36ca6 | ||
|
|
872a9d393d | ||
|
|
b1ca242d89 | ||
|
|
97c769e805 | ||
|
|
0de33b36f8 | ||
|
|
cf39bdc7f7 | ||
|
|
34b49498c9 | ||
|
|
3a4bd00020 | ||
|
|
effd07d8c0 | ||
|
|
d9ce89ab31 | ||
|
|
5483c52227 | ||
|
|
f12e9b6a49 | ||
|
|
2b48902f1b | ||
|
|
305460dedb | ||
|
|
bacef41a3b | ||
|
|
f789c84816 | ||
|
|
09466a2dff | ||
|
|
e77d888aab | ||
|
|
478d91928c | ||
|
|
fdd1a778f3 | ||
|
|
a5d87ab948 | ||
|
|
f1672dd6d2 | ||
|
|
48c25c380d | ||
|
|
3a5aa87853 | ||
|
|
f436744dd4 | ||
|
|
6df5e55807 | ||
|
|
c758054250 | ||
|
|
fff0a8a522 | ||
|
|
4ff978f318 | ||
|
|
b29e07faba | ||
|
|
b35107a966 | ||
|
|
1090ff0175 | ||
|
|
8de57ec0e0 | ||
|
|
4165f47a64 | ||
|
|
f931026216 | ||
|
|
19df73729a | ||
|
|
9efc1a1c09 | ||
|
|
234e7afb12 | ||
|
|
8904afaa74 | ||
|
|
d95a18b6eb | ||
|
|
bcd4bdb4e0 | ||
|
|
73df41b5b2 | ||
|
|
d32fbfd634 | ||
|
|
6b0c532f48 | ||
|
|
9f4ee7d6a8 | ||
|
|
7da83d2259 | ||
|
|
ceb9453006 | ||
|
|
7091b37f3a | ||
|
|
18e6f9be71 | ||
|
|
19d40845a4 | ||
|
|
211ce20132 | ||
|
|
275b97948b | ||
|
|
13d602a9e0 | ||
|
|
69215e7d27 | ||
|
|
7e8df34681 | ||
|
|
6451065c77 | ||
|
|
bde8c54e7e | ||
|
|
97b17af056 | ||
|
|
9c2e3e2c76 | ||
|
|
3c637872f2 | ||
|
|
4c8e2a1258 | ||
|
|
e5a76d737c | ||
|
|
a482d5998d | ||
|
|
12bc540ec9 | ||
|
|
b6a37f6fb8 | ||
|
|
bbdb25420a | ||
|
|
e3099a16d4 | ||
|
|
167fe5f758 | ||
|
|
36f59da7cc | ||
|
|
1ac23ce191 | ||
|
|
a000dfe676 | ||
|
|
9e834e0db5 | ||
|
|
021fc8fb59 | ||
|
|
625fa03c22 | ||
|
|
6e80b03faa | ||
|
|
c3f3eea7fb | ||
|
|
47da5e0338 | ||
|
|
2ef7ea6512 | ||
|
|
6b1f2c0ed2 | ||
|
|
bb465ed1ed | ||
|
|
ac75f9bf57 | ||
|
|
c80deeb5ec | ||
|
|
1b87f9690c | ||
|
|
e799fcd48a | ||
|
|
4644e55883 | ||
|
|
747a8ad09c | ||
|
|
32dc19cb1c | ||
|
|
527579aef4 | ||
|
|
1869ef0c38 | ||
|
|
e7007b4231 | ||
|
|
6ca57c1f8c | ||
|
|
f2f7a349ce | ||
|
|
f696aa3748 | ||
|
|
f35e3ec78a | ||
|
|
e339ee3f0c | ||
|
|
c30b424f36 | ||
|
|
0b0b405974 | ||
|
|
ef64fa3794 | ||
|
|
2531aed50b | ||
|
|
6adb46abd5 | ||
|
|
3ef1d8b0b9 | ||
|
|
71b5dc2f81 | ||
|
|
5909ab7641 | ||
|
|
b7beb73a92 | ||
|
|
0acbb20c00 | ||
|
|
9a2c0067f1 | ||
|
|
ab45b42382 | ||
|
|
4a6cee0611 | ||
|
|
d39cada0c6 | ||
|
|
b7b67681c7 | ||
|
|
8551e05808 | ||
|
|
cfdbd418c1 | ||
|
|
2a4feb7bee | ||
|
|
7202d758a2 | ||
|
|
dab59aded3 | ||
|
|
20d0b4ad16 | ||
|
|
eed4fc7844 | ||
|
|
0ccd9e0579 | ||
|
|
74b36d6d32 | ||
|
|
58215a470b | ||
|
|
608e0a0122 | ||
|
|
bddb3f0542 | ||
|
|
83da81839b | ||
|
|
73d63293d9 | ||
|
|
f49710f361 | ||
|
|
dffbce1934 | ||
|
|
06a33b0c8b | ||
|
|
a1f140acf7 | ||
|
|
fed37bcc48 | ||
|
|
88df9f0134 | ||
|
|
79d1425530 | ||
|
|
f9144378ae | ||
|
|
d13d28e6f4 | ||
|
|
c438bb2fbe | ||
|
|
5f4dd43124 | ||
|
|
e7f16f371c | ||
|
|
30ff17df28 | ||
|
|
d7a3e2f450 | ||
|
|
9ce3fc9f8e | ||
|
|
f0017c3e92 | ||
|
|
99b7508c7a | ||
|
|
cff8857a36 | ||
|
|
60395852d5 | ||
|
|
edf125b4ba | ||
|
|
b731fa4b78 | ||
|
|
676e6ecec1 | ||
|
|
7d9951aa3c | ||
|
|
1d0876af4d | ||
|
|
c6f23eee77 | ||
|
|
8d3cf04324 | ||
|
|
fe9344ce57 | ||
|
|
d7c4824633 | ||
|
|
2feba3182a | ||
|
|
e9920caa69 | ||
|
|
9bcaaab9d7 | ||
|
|
d47db317fb | ||
|
|
287d0fad85 | ||
|
|
7c19de3d61 | ||
|
|
a76cdf7514 | ||
|
|
9abead7c49 | ||
|
|
5ff3f71f83 | ||
|
|
e2f9ca66b6 | ||
|
|
e90048e5a8 | ||
|
|
eb1795aff9 | ||
|
|
3a92f93e6f | ||
|
|
d1bd358785 | ||
|
|
f63ea62f2d | ||
|
|
3fd5ed4feb | ||
|
|
ba7df8b9cf | ||
|
|
18b97df619 | ||
|
|
087d23269b | ||
|
|
c77fb98b1f | ||
|
|
8c1f38f74d | ||
|
|
13091e0de4 | ||
|
|
1a72bf5962 | ||
|
|
b8cd0c1a77 | ||
|
|
ecd593fb53 | ||
|
|
b17f20e2c5 | ||
|
|
eae9f9ceee | ||
|
|
d2c13ed32b | ||
|
|
6fb78a99bf | ||
|
|
bcc4980189 | ||
|
|
bed394db80 | ||
|
|
1fe2bf5dd5 | ||
|
|
7cc332a96e | ||
|
|
6ce24b3443 | ||
|
|
1dc6e91ec4 | ||
|
|
f59e3cd4da | ||
|
|
94a30b2167 | ||
|
|
bd0fa1487f | ||
|
|
d262f017c5 | ||
|
|
a98c08c06c | ||
|
|
a2e0fd28e0 | ||
|
|
5dbdf8321a | ||
|
|
9d122bd181 | ||
|
|
09727101c1 | ||
|
|
5fc9cd7d48 | ||
|
|
7adaa53f42 | ||
|
|
cc82b1ae25 | ||
|
|
0df531a646 | ||
|
|
b1d0368479 | ||
|
|
46c6a0b4ff | ||
|
|
97d414aa00 | ||
|
|
ab8da3965b | ||
|
|
589fa4c9de | ||
|
|
f4a27af37e | ||
|
|
ca0f407b7b | ||
|
|
4810a5643e | ||
|
|
72a983f6d8 | ||
|
|
a720333c0f | ||
|
|
38c6fa9c76 | ||
|
|
eed3d27665 | ||
|
|
450e345b28 | ||
|
|
913568aba2 | ||
|
|
3c3de9d325 | ||
|
|
fada732b33 | ||
|
|
152d0fdda7 | ||
|
|
6506fa792d | ||
|
|
867c72ba90 | ||
|
|
3f6b095da4 | ||
|
|
f1d6d386c5 | ||
|
|
72944a4e5e | ||
|
|
193e012aa6 | ||
|
|
3ee17e01e1 | ||
|
|
7421fa0a33 | ||
|
|
2cdfc3f4c3 | ||
|
|
4322d8e494 | ||
|
|
73a59dcd7d | ||
|
|
3a15790847 | ||
|
|
3f31573bcb | ||
|
|
967ab18d53 | ||
|
|
0929bd217d | ||
|
|
ce832a8063 | ||
|
|
fc0281b563 | ||
|
|
f42bd02cfc | ||
|
|
52634ddeb3 | ||
|
|
ed79b4ebd8 | ||
|
|
36ca7839d6 | ||
|
|
fa5d583657 | ||
|
|
5e67f09583 | ||
|
|
8b74d96f12 | ||
|
|
769d99e7bd | ||
|
|
812f4d2699 | ||
|
|
f95defe82f | ||
|
|
226dafa9e3 | ||
|
|
6c92d50c68 | ||
|
|
384e74fe7e | ||
|
|
216f6cc8e8 | ||
|
|
333c377bc7 | ||
|
|
59b33faf61 | ||
|
|
b87003427c | ||
|
|
cb48000df7 | ||
|
|
58cc5d8d1a | ||
|
|
39799d3006 | ||
|
|
73bf4479b5 | ||
|
|
9f0f84bbee | ||
|
|
1ff422a29c | ||
|
|
8daa525cc1 | ||
|
|
76f1fcb634 | ||
|
|
2b6cf95752 | ||
|
|
a99d193b12 | ||
|
|
a3b576abd8 | ||
|
|
2261eac288 | ||
|
|
9366729d7a | ||
|
|
ad1a4fe450 | ||
|
|
9f97725894 | ||
|
|
bff3d27518 | ||
|
|
2bc1192ad3 | ||
|
|
f165131da8 | ||
|
|
afd29fef81 | ||
|
|
071a4f97e5 | ||
|
|
04c990de89 | ||
|
|
b08ffcc437 | ||
|
|
7156df8d9a | ||
|
|
1a83e69669 | ||
|
|
210d4f6aa1 | ||
|
|
b559506d4e | ||
|
|
3fec6ff5bc | ||
|
|
ce74307172 | ||
|
|
e44e68f8fc | ||
|
|
eff1341088 | ||
|
|
ddd35e3d80 | ||
|
|
265272b9d3 | ||
|
|
206e34ac80 | ||
|
|
ea556ff201 | ||
|
|
110dc751a4 | ||
|
|
46546def28 | ||
|
|
48de14ade3 | ||
|
|
f74647ccfc | ||
|
|
b92a85f0a9 | ||
|
|
853965e7a9 | ||
|
|
6f9dd8d7cd | ||
|
|
905eb1a93f | ||
|
|
7862fc7cb7 | ||
|
|
903168b3a6 | ||
|
|
5e8fcb579f | ||
|
|
fae018b4ea | ||
|
|
dc0e278a24 | ||
|
|
aaa34ab860 | ||
|
|
66638cab33 | ||
|
|
a729a61100 | ||
|
|
23b39c6a63 | ||
|
|
37467d3753 | ||
|
|
8d3a378761 | ||
|
|
3993f9c2bb | ||
|
|
b542762dce | ||
|
|
35b2ea870d | ||
|
|
b2605dd30c | ||
|
|
18b04e2999 | ||
|
|
54c2dedac0 | ||
|
|
0efa6661b8 | ||
|
|
42d0532580 | ||
|
|
8d5f7c8d3e | ||
|
|
04214200b8 | ||
|
|
99229513ba | ||
|
|
c3a992e6d4 | ||
|
|
e15c80927b | ||
|
|
e918a0bf26 | ||
|
|
35bff8cc67 | ||
|
|
0998ae753c | ||
|
|
7bb6506709 | ||
|
|
64f80312de | ||
|
|
ce2eed28c1 | ||
|
|
505fa91d7d | ||
|
|
dd7e6d3831 | ||
|
|
b086337dbe | ||
|
|
49562f50f2 | ||
|
|
884ec05a1e | ||
|
|
212d7f1865 | ||
|
|
9ab8a2cbd2 | ||
|
|
f633eddd73 | ||
|
|
f5761ee69d | ||
|
|
b8cdc0f145 | ||
|
|
b5eea2136b | ||
|
|
deded47da2 | ||
|
|
fdc0e2597d | ||
|
|
da5b0260f2 | ||
|
|
beb960b753 | ||
|
|
5cc338dedc | ||
|
|
15be42340d | ||
|
|
f10bee8cb0 | ||
|
|
eadf18821f | ||
|
|
56b1c7b11a | ||
|
|
e4513976f7 | ||
|
|
b71ea3852e | ||
|
|
ae6c29ccff | ||
|
|
1820e79617 | ||
|
|
2a95b7a37c | ||
|
|
fb95df66c9 | ||
|
|
3c76284d89 | ||
|
|
29967fde50 | ||
|
|
bd65e4084c | ||
|
|
a2a9977af6 | ||
|
|
0369b490b8 | ||
|
|
d9e5821d31 | ||
|
|
54a7df8d40 | ||
|
|
17ed502123 | ||
|
|
56eef2ec94 | ||
|
|
200036efc9 | ||
|
|
7fa7f4ed8a | ||
|
|
3466325d4d | ||
|
|
1613345dec | ||
|
|
759accef07 | ||
|
|
6d02669fc3 | ||
|
|
6d8d688063 | ||
|
|
5207bdfd85 | ||
|
|
690d4238e8 | ||
|
|
95ee78b1bd | ||
|
|
25eadc2263 | ||
|
|
28e4065890 | ||
|
|
e44388b506 | ||
|
|
540dea9fc3 | ||
|
|
c31290b794 | ||
|
|
f1fe4c0c70 | ||
|
|
921ac18876 | ||
|
|
505ad0380e | ||
|
|
2b7a7c0054 | ||
|
|
0dea4c51b7 | ||
|
|
3095f2110e | ||
|
|
e32d35b156 | ||
|
|
db28336e5d | ||
|
|
c5c5accaa8 | ||
|
|
78bfdd4515 | ||
|
|
01aa826a24 | ||
|
|
7f2506d8a6 | ||
|
|
7c2f7b6338 | ||
|
|
5f05de30a6 | ||
|
|
7741de7ae0 | ||
|
|
d4c8e8c50e | ||
|
|
bf36ff9cb9 | ||
|
|
8eadccdee2 | ||
|
|
b32839292c | ||
|
|
2402443dcc | ||
|
|
9f72c98967 | ||
|
|
f6f744aea1 | ||
|
|
cddc55694a | ||
|
|
8930e2f06e | ||
|
|
b8e5e130b9 | ||
|
|
a8c5087a38 | ||
|
|
d9f21e0475 | ||
|
|
ca3fa3dc40 | ||
|
|
ddd0a42b26 | ||
|
|
f884627927 | ||
|
|
9373cf9cf6 | ||
|
|
f04030904e | ||
|
|
271b2a0417 | ||
|
|
a4f7393fc8 | ||
|
|
8f851beda1 | ||
|
|
4489efa8d9 | ||
|
|
8b9084cb73 | ||
|
|
1146453dc2 | ||
|
|
bd54395948 | ||
|
|
89ac27ba97 | ||
|
|
74eaee53a4 | ||
|
|
20e4261aa7 | ||
|
|
312189fbde | ||
|
|
d05063ec61 | ||
|
|
47c14db54c | ||
|
|
f0e0650244 | ||
|
|
d2a68e62e9 | ||
|
|
09fbbc1e17 | ||
|
|
8971822247 | ||
|
|
1f0d1920bf | ||
|
|
cb7c8502b0 | ||
|
|
27d1f79839 | ||
|
|
83ef21e699 | ||
|
|
6c592669da | ||
|
|
88f7687646 | ||
|
|
f12a527ae3 | ||
|
|
7dde0be043 | ||
|
|
2910f4f527 | ||
|
|
93c0df33c2 | ||
|
|
7d9f6eef27 | ||
|
|
7d742d62b8 | ||
|
|
4db80cb9e7 | ||
|
|
addfbcb68f | ||
|
|
fac46d9d0b | ||
|
|
e38ff08de2 | ||
|
|
c31e2d91dd | ||
|
|
7309fec51d | ||
|
|
2e01fa738a | ||
|
|
9044925f32 | ||
|
|
2d5ff8252c | ||
|
|
072110481f | ||
|
|
0fb0532875 | ||
|
|
d8dd94c679 | ||
|
|
f3d7736acf | ||
|
|
8fbf5590f8 | ||
|
|
b8ac93045e | ||
|
|
89fea9b4df | ||
|
|
a3323dc8a7 | ||
|
|
ba0505c13c | ||
|
|
dd8432e8fd | ||
|
|
11c7f57745 | ||
|
|
89a3fac316 | ||
|
|
b0b3e92600 | ||
|
|
1fca035cfe | ||
|
|
4c89bb0e0a | ||
|
|
332508f563 | ||
|
|
158d11e93c | ||
|
|
18a49601a0 | ||
|
|
b971b4754f | ||
|
|
cfef22257e | ||
|
|
3153d8ee8c | ||
|
|
b2a975fe4a | ||
|
|
b2ba505954 | ||
|
|
a1b673175a | ||
|
|
d666de07a7 | ||
|
|
64508cec61 | ||
|
|
e0bcb625c2 | ||
|
|
9534e765e5 | ||
|
|
39124d2878 | ||
|
|
9ae4d66194 | ||
|
|
09850d7500 | ||
|
|
8897d9179c | ||
|
|
2d1b9d64bd | ||
|
|
e603a1707c | ||
|
|
6b1e7a1c5d | ||
|
|
5acd4b5fd4 | ||
|
|
9e88adb0da | ||
|
|
69eaf0d338 | ||
|
|
680de2dca1 | ||
|
|
837188f8d1 | ||
|
|
4a696b4053 | ||
|
|
0b2c4679fd | ||
|
|
5a08c92d02 | ||
|
|
faf93441f6 | ||
|
|
8aa3608a3c | ||
|
|
9727a9d000 | ||
|
|
1b74289c43 | ||
|
|
a698ff8309 | ||
|
|
5026c48805 | ||
|
|
2ac63b6985 | ||
|
|
114e11f52a | ||
|
|
3277d1baac | ||
|
|
f3d8ec040c | ||
|
|
0a29e9b3cf | ||
|
|
4b7c17ac03 | ||
|
|
1849f4c11d | ||
|
|
b9f61466ba | ||
|
|
d8fa9b8c4f | ||
|
|
42bc80e5b5 | ||
|
|
9f7446ba56 | ||
|
|
7bdea1befa | ||
|
|
66ec087416 | ||
|
|
9b4d1d442e | ||
|
|
16a30fa3b7 | ||
|
|
1cd3ebfc3f | ||
|
|
fd170df98f | ||
|
|
a2291b0713 | ||
|
|
3134ff81f4 | ||
|
|
072bc514f4 | ||
|
|
581a79f3fc | ||
|
|
cccb8e9645 | ||
|
|
646fcafa62 | ||
|
|
615453a687 | ||
|
|
361a1a21ac | ||
|
|
e3e3311dd0 | ||
|
|
74fa9a6b2b | ||
|
|
b62faef520 | ||
|
|
74391d59a5 | ||
|
|
1c08b3e5e4 | ||
|
|
8c489c2131 | ||
|
|
19976939b7 | ||
|
|
4e1659b98d | ||
|
|
26ef8deca5 | ||
|
|
4e5fe5ae1a | ||
|
|
7f308f59b4 | ||
|
|
f4e8bb6c66 | ||
|
|
e3638053d0 | ||
|
|
d688d8812d | ||
|
|
4a6bf38666 | ||
|
|
f532b62913 | ||
|
|
0080c8457f | ||
|
|
613904e3a4 | ||
|
|
753a093689 | ||
|
|
ea6f8ce4d9 | ||
|
|
180359e148 | ||
|
|
5816443ad3 | ||
|
|
e9fce9223e | ||
|
|
f6c43eaf9c | ||
|
|
8af71be551 | ||
|
|
9e36702eb2 | ||
|
|
cda6f89dba | ||
|
|
b8d7744563 | ||
|
|
25dcae7648 | ||
|
|
ee6382ef03 | ||
|
|
0310192660 | ||
|
|
c88bc65379 | ||
|
|
37340dc549 | ||
|
|
9b6764a852 | ||
|
|
b176857b8d | ||
|
|
f034065247 | ||
|
|
64bd4dee38 | ||
|
|
22307239ae | ||
|
|
3fc7ffadbf | ||
|
|
b87a80a32d | ||
|
|
c775de260a | ||
|
|
30fd358286 | ||
|
|
71c3d484a9 | ||
|
|
66bac32e33 | ||
|
|
4f0ea888ef | ||
|
|
bc1a83d04a | ||
|
|
32d9fc0d32 | ||
|
|
41bd3704ef | ||
|
|
be75b5b237 | ||
|
|
3a7da6665f | ||
|
|
2f47e04de7 | ||
|
|
7dc3add5fd | ||
|
|
8b296534a4 | ||
|
|
f9c4cefe59 | ||
|
|
d772eaf4a2 | ||
|
|
27ec1a13da | ||
|
|
07e8dfa257 | ||
|
|
0fbf48ab4d | ||
|
|
f38a0d2d67 | ||
|
|
b76875bf5d | ||
|
|
0253de80de | ||
|
|
647575261e | ||
|
|
3c2b348ce5 | ||
|
|
8aef6ca372 | ||
|
|
0139437c3d | ||
|
|
a7b91ee57d | ||
|
|
ad0117e060 | ||
|
|
309d70c142 | ||
|
|
c9ff59a433 | ||
|
|
ec9a1416a1 | ||
|
|
dac622fc46 | ||
|
|
92e2daf056 | ||
|
|
08e68a1cff | ||
|
|
8f4be9b76f | ||
|
|
fab6ec94fa | ||
|
|
5cbcb901f1 | ||
|
|
4d075818f6 | ||
|
|
4302be5619 | ||
|
|
68d1be3b94 | ||
|
|
af68b10c5d | ||
|
|
8b16d0e7ed | ||
|
|
2d5c24d8b5 | ||
|
|
0110ac62bf | ||
|
|
5bfa44b1b4 | ||
|
|
d21821a0fb | ||
|
|
84dfde2e51 | ||
|
|
22d33fa286 | ||
|
|
f6f83e2114 | ||
|
|
c6ad734de0 | ||
|
|
cf015b2ce7 | ||
|
|
fbe8086c98 | ||
|
|
95cae6e7de | ||
|
|
d12fd78ef0 | ||
|
|
b2d9f835bf | ||
|
|
735772f43a | ||
|
|
75f66a6cb2 | ||
|
|
24d5dfe3c6 | ||
|
|
be9e953971 | ||
|
|
82e67b7550 | ||
|
|
791549fda8 | ||
|
|
c763783d53 | ||
|
|
e347e7e5fb | ||
|
|
3f1d0df7f9 | ||
|
|
c6cb6d5eeb | ||
|
|
57025f8173 | ||
|
|
3e7f07374c | ||
|
|
fba9cb071d | ||
|
|
c6538e1038 | ||
|
|
3a1a582013 | ||
|
|
531a484cb0 | ||
|
|
16c477229a | ||
|
|
f2565049b8 | ||
|
|
afdb5d7233 | ||
|
|
18be1202db | ||
|
|
14cc87e1a5 | ||
|
|
2a0d1b0a48 | ||
|
|
22aa126b29 | ||
|
|
feb2046549 | ||
|
|
2f362f2aa2 | ||
|
|
de160d9170 | ||
|
|
226c18cb56 | ||
|
|
314aea4e1e | ||
|
|
807d3a600c | ||
|
|
fa8ea1ef43 | ||
|
|
2017d4785b | ||
|
|
fd35724aa8 | ||
|
|
e1a85d97e3 | ||
|
|
b972c9fe30 | ||
|
|
4c68150dec | ||
|
|
3d6dd06b99 | ||
|
|
81759fa57a | ||
|
|
20160cb071 | ||
|
|
8931506657 | ||
|
|
2aee346299 | ||
|
|
f89efd84d3 | ||
|
|
7607ab2c84 | ||
|
|
fe7f6bee1c | ||
|
|
b43658eb3f | ||
|
|
85caa09e63 | ||
|
|
c32853bfd6 | ||
|
|
e79cd58c8f | ||
|
|
0d291f1a36 | ||
|
|
24aa8e2a07 | ||
|
|
0a0c155292 | ||
|
|
55a942aa22 | ||
|
|
b51499e87b | ||
|
|
936048d478 | ||
|
|
bd6497743c | ||
|
|
6873d8d445 | ||
|
|
21c9dde858 | ||
|
|
17d3d620ff | ||
|
|
705603a088 | ||
|
|
ba8a0179d5 | ||
|
|
9fe10747ce | ||
|
|
4a4d9a9377 | ||
|
|
2e7342a59c | ||
|
|
c9bc5be42b | ||
|
|
b75b36dc61 | ||
|
|
1e6a1bd3af | ||
|
|
b0a2087015 | ||
|
|
a5ee34a2db | ||
|
|
a6a8130234 | ||
|
|
288761632f | ||
|
|
25bf4fa738 | ||
|
|
3b4de6a405 | ||
|
|
75512602c3 | ||
|
|
cd33a089d1 | ||
|
|
6b83281539 | ||
|
|
2609671982 | ||
|
|
accf2c0e5e | ||
|
|
53f6e66c23 | ||
|
|
56ddcc8e29 | ||
|
|
430779979e | ||
|
|
671dbcfd55 | ||
|
|
087a7b5f3c | ||
|
|
229844d399 | ||
|
|
36081653b0 | ||
|
|
9811c5d577 | ||
|
|
4394186dc3 | ||
|
|
725b48d8aa | ||
|
|
3fd8347943 | ||
|
|
5e7c26c34b | ||
|
|
d7019264a2 | ||
|
|
ade9fa5d0e | ||
|
|
f84c4393b9 | ||
|
|
48d01c0ab5 | ||
|
|
aca01d81d6 | ||
|
|
6a0b154d67 | ||
|
|
7ce69987d5 | ||
|
|
3fe28d5441 | ||
|
|
43f42f9ca0 | ||
|
|
3e288f1fcf | ||
|
|
8ccd75fdfb | ||
|
|
fd6aa6e54e | ||
|
|
4802a2ce82 | ||
|
|
e3409a27e7 | ||
|
|
5182edce6f | ||
|
|
763d8d025e | ||
|
|
a3045c9808 | ||
|
|
6b78b011b7 | ||
|
|
bd7b84e136 | ||
|
|
2a9bab3f13 | ||
|
|
6ca1e6c6dd | ||
|
|
f3a1a6a191 | ||
|
|
675932c65b | ||
|
|
708abb0e30 | ||
|
|
9de84aee2e | ||
|
|
adb8779d00 | ||
|
|
fbb0e675f5 | ||
|
|
a3e2b5246e | ||
|
|
ccacac0597 | ||
|
|
ca230aa230 | ||
|
|
7b775d2ad7 | ||
|
|
c5397bfbe2 | ||
|
|
9fec6ebc66 | ||
|
|
6bc38c5782 | ||
|
|
7f9d585d7f | ||
|
|
0b14d36c95 | ||
|
|
e22ca2d082 | ||
|
|
52a70cb7f5 | ||
|
|
a00d1d068a | ||
|
|
6ae4ed9fc3 | ||
|
|
6f5028612a | ||
|
|
c31c12d31a | ||
|
|
28008d835e | ||
|
|
08e99a32cb | ||
|
|
68fc87bc01 | ||
|
|
d0ba06c44b | ||
|
|
d501cbf765 | ||
|
|
488c7e6c27 | ||
|
|
155559c2c4 | ||
|
|
a22e1bc5e5 | ||
|
|
9519d3f7ce | ||
|
|
3f23e07c02 | ||
|
|
6c75177edc | ||
|
|
85df280447 | ||
|
|
734cf243f6 | ||
|
|
d8f7817eeb | ||
|
|
94b6b2636a | ||
|
|
1036f7580f | ||
|
|
908febb363 | ||
|
|
aefd091b44 | ||
|
|
99fb82e244 | ||
|
|
756d6620cc | ||
|
|
09505dba09 | ||
|
|
9401eff297 | ||
|
|
adbec3d272 | ||
|
|
e301ba0cdb | ||
|
|
b12eef218a | ||
|
|
bc4560877a | ||
|
|
521a740d3a | ||
|
|
be12b724cc | ||
|
|
073873a3e9 | ||
|
|
fcdcb50b8b | ||
|
|
61a7848fd9 | ||
|
|
6d6b840cf6 | ||
|
|
4dbba103d4 | ||
|
|
a2932f05f4 | ||
|
|
5d4efb7692 | ||
|
|
39a9efb73b | ||
|
|
5037bd07d5 | ||
|
|
73a2fa3f9c | ||
|
|
79387f469b | ||
|
|
f986cfecff | ||
|
|
4d51a9123b | ||
|
|
7602f15544 | ||
|
|
3180ba7de9 | ||
|
|
3e01cf19b0 | ||
|
|
14eebfe39e | ||
|
|
9176599b29 | ||
|
|
d6575faa9f | ||
|
|
24c5bf9ff4 | ||
|
|
cdcc5e106f | ||
|
|
1a8cc2d019 | ||
|
|
27e907491b | ||
|
|
0a1e6623c8 | ||
|
|
689dddd11a | ||
|
|
f8d01e1596 | ||
|
|
cd429f5935 | ||
|
|
03355f6a4a | ||
|
|
dc1d593019 | ||
|
|
9894cceeaa | ||
|
|
bcedbc845e | ||
|
|
f508288ce3 | ||
|
|
18080cef9f | ||
|
|
c4eeef2a86 | ||
|
|
b60a91f53c | ||
|
|
b1c3de6518 | ||
|
|
a43a6a299c | ||
|
|
d8fae5bc41 | ||
|
|
fa9b6f58e5 | ||
|
|
89ff1411e9 | ||
|
|
701e8277d6 | ||
|
|
4a11f80c45 | ||
|
|
f1b275d5d0 | ||
|
|
68e0ffc95c | ||
|
|
0753eb7691 | ||
|
|
92afcd174d | ||
|
|
94be7a0e79 | ||
|
|
0814daf99d | ||
|
|
b2e3419bff | ||
|
|
1846d0bc21 | ||
|
|
d282055e10 | ||
|
|
6ab64d155b | ||
|
|
6ba3e57f5f | ||
|
|
14fe4f65e1 | ||
|
|
bdb70444d6 | ||
|
|
4d9cc55a87 | ||
|
|
f41c1cbfd0 | ||
|
|
72eaab68be | ||
|
|
733c6b4c17 | ||
|
|
c0c0694fcc | ||
|
|
055530c8c6 | ||
|
|
fb3b38aec7 | ||
|
|
4e4a8f1bab | ||
|
|
39b3786776 | ||
|
|
8b22313ca1 | ||
|
|
402f72cfa8 | ||
|
|
e7dcb8a605 | ||
|
|
8f8a1fda85 | ||
|
|
26be25c3d5 | ||
|
|
50b53b00e0 | ||
|
|
94531cb3d0 | ||
|
|
842760255b | ||
|
|
c78b582d71 | ||
|
|
4ab02fab1c | ||
|
|
6863f3227f | ||
|
|
d01d43eccb | ||
|
|
2aa5f4fc82 | ||
|
|
3af0531111 | ||
|
|
6e58b98b3d | ||
|
|
62805cdf1d | ||
|
|
4229b1d2a4 | ||
|
|
2c4661a250 | ||
|
|
0c1a486ed9 | ||
|
|
688cb55c2b | ||
|
|
1594f148f8 | ||
|
|
fafd8c4af1 | ||
|
|
3d66758507 | ||
|
|
fc0ec860b0 | ||
|
|
00d332cd16 | ||
|
|
4c8c0f8738 | ||
|
|
54978132bb | ||
|
|
018abe0188 | ||
|
|
b186497fb0 | ||
|
|
27f9963ccb | ||
|
|
a4e3f03bf5 | ||
|
|
27a6be4ce0 | ||
|
|
76a2520e56 | ||
|
|
0a472681af | ||
|
|
6d530691f3 | ||
|
|
a74c9e8481 | ||
|
|
8aac26a331 | ||
|
|
fc59a0f6ab | ||
|
|
3fb16774b7 | ||
|
|
7b35bb4c0f | ||
|
|
318e2bd1c6 | ||
|
|
09ba4bcf43 | ||
|
|
0c89fa7b1e | ||
|
|
7eedb3320d | ||
|
|
cfac75ea49 | ||
|
|
f00a6c396f | ||
|
|
e74a9711ca | ||
|
|
636d3cdf90 | ||
|
|
71966affa1 | ||
|
|
bf4dc195ec | ||
|
|
dccca17e09 | ||
|
|
5381a4354c | ||
|
|
c70425fbf7 | ||
|
|
341f5725a4 | ||
|
|
d7069df80d | ||
|
|
579714a60b | ||
|
|
bbdf63635a | ||
|
|
fd7db18221 | ||
|
|
482ed8d958 | ||
|
|
673e16878d | ||
|
|
e11ceab029 | ||
|
|
7fe719f43c | ||
|
|
3fd3ac1de1 | ||
|
|
0e90a675af | ||
|
|
ee861c1f91 | ||
|
|
40c9355088 | ||
|
|
8f1557254a | ||
|
|
11d28b0bc3 | ||
|
|
974cf780c0 | ||
|
|
73bb14e4a9 | ||
|
|
daf4236023 | ||
|
|
4c9a24c64e | ||
|
|
c149f65158 | ||
|
|
c5688c1bd3 | ||
|
|
b276a15786 | ||
|
|
2fed239ece | ||
|
|
8e2cb36597 | ||
|
|
bcaace1c91 | ||
|
|
d664d07141 | ||
|
|
cb8b80c856 | ||
|
|
d777d77b06 | ||
|
|
43678f8dc0 | ||
|
|
5811577824 | ||
|
|
1587122efa | ||
|
|
48e7c8ad0f | ||
|
|
766f9798f6 | ||
|
|
680d634725 | ||
|
|
7ac945bf88 | ||
|
|
188d7a8558 | ||
|
|
ee561b0d1e | ||
|
|
82bbe78e95 | ||
|
|
c761cd059b | ||
|
|
03e87155ca | ||
|
|
ea39cc52b1 | ||
|
|
90fb90b186 | ||
|
|
5f8327eaf7 | ||
|
|
07869f3c48 | ||
|
|
8377eb02a5 | ||
|
|
3738e8eb44 | ||
|
|
4b000e44b3 | ||
|
|
d6021d1702 | ||
|
|
b391eafc38 | ||
|
|
2af4545e10 | ||
|
|
b96644d893 | ||
|
|
3cb77c0a32 | ||
|
|
b3f7fb7be3 | ||
|
|
9fb51a1f29 | ||
|
|
d78e8a725d | ||
|
|
7829bdbc95 | ||
|
|
90ba6deba2 | ||
|
|
5fc763a738 | ||
|
|
84614e903c | ||
|
|
c95d739347 | ||
|
|
c55849cef5 | ||
|
|
88adb09417 | ||
|
|
cb356ffca9 | ||
|
|
2ba3f252ee | ||
|
|
ebe2c8e3dd | ||
|
|
ca2d63edcf | ||
|
|
489a1a1c37 | ||
|
|
5cad13eea3 | ||
|
|
a3e09e015c | ||
|
|
22a6accac1 | ||
|
|
277c97a959 | ||
|
|
c08bc3239a | ||
|
|
f798c7e0fb | ||
|
|
193780a88f | ||
|
|
ab4973ab6c | ||
|
|
f9da815e8f | ||
|
|
59e187d59a | ||
|
|
3b018e2a6d | ||
|
|
d4939c8260 | ||
|
|
485352594c | ||
|
|
bcdcfee467 | ||
|
|
0217e3fcae | ||
|
|
6e7d6421d5 | ||
|
|
913d3af938 | ||
|
|
53dd0a5e4c |
20
.github/actions/install/action.yml
vendored
20
.github/actions/install/action.yml
vendored
@@ -5,7 +5,7 @@ inputs:
|
||||
zig:
|
||||
description: 'Zig version to install'
|
||||
required: false
|
||||
default: '0.13.0'
|
||||
default: '0.14.1'
|
||||
arch:
|
||||
description: 'CPU arch used to select the v8 lib'
|
||||
required: false
|
||||
@@ -17,11 +17,11 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.1.9'
|
||||
default: 'v0.1.28'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
default: '11.1.134'
|
||||
default: '13.6.233.8'
|
||||
cache-dir:
|
||||
description: 'cache dir to use'
|
||||
required: false
|
||||
@@ -34,9 +34,11 @@ runs:
|
||||
- name: Install apt deps
|
||||
if: ${{ inputs.os == 'linux' }}
|
||||
shell: bash
|
||||
run: sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
|
||||
run: |
|
||||
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
|
||||
|
||||
- uses: mlugg/setup-zig@v1
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: ${{ inputs.zig }}
|
||||
|
||||
@@ -59,11 +61,11 @@ runs:
|
||||
- name: install v8
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug/libc_v8.a
|
||||
mkdir -p v8/out/${{ inputs.os }}/debug/obj/zig/
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/debug/obj/zig/libc_v8.a
|
||||
|
||||
mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release/libc_v8.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: libiconv
|
||||
shell: bash
|
||||
|
||||
164
.github/workflows/build.yml
vendored
164
.github/workflows/build.yml
vendored
@@ -1,5 +1,11 @@
|
||||
name: nightly build
|
||||
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ vars.NIGHTLY_BUILD_AWS_ACCESS_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
|
||||
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "2 2 * * *"
|
||||
@@ -17,35 +23,7 @@ jobs:
|
||||
OS: linux
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
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
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dcpu=x86_64
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: nightly
|
||||
|
||||
build-macos-aarch64:
|
||||
env:
|
||||
ARCH: aarch64
|
||||
OS: macos
|
||||
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -60,11 +38,137 @@ jobs:
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: zig build
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: upload on s3
|
||||
run: |
|
||||
export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`
|
||||
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: nightly
|
||||
|
||||
build-linux-aarch64:
|
||||
env:
|
||||
ARCH: aarch64
|
||||
OS: linux
|
||||
|
||||
runs-on: ubuntu-22.04-arm
|
||||
timeout-minutes: 15
|
||||
|
||||
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
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: zig build
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: upload on s3
|
||||
run: |
|
||||
export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`
|
||||
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: nightly
|
||||
|
||||
build-macos-aarch64:
|
||||
env:
|
||||
ARCH: aarch64
|
||||
OS: macos
|
||||
|
||||
# macos-14 runs on arm CPU. see
|
||||
# https://github.com/actions/runner-images?tab=readme-ov-file
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 15
|
||||
|
||||
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
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: zig build
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: upload on s3
|
||||
run: |
|
||||
export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`
|
||||
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: nightly
|
||||
|
||||
build-macos-x86_64:
|
||||
env:
|
||||
ARCH: x86_64
|
||||
OS: macos
|
||||
|
||||
# macos-13 runs on x86 CPU. see
|
||||
# https://github.com/actions/runner-images?tab=readme-ov-file
|
||||
# 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:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: zig build
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: upload on s3
|
||||
run: |
|
||||
export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`
|
||||
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
|
||||
4
.github/workflows/cla.yml
vendored
4
.github/workflows/cla.yml
vendored
@@ -14,6 +14,8 @@ permissions:
|
||||
jobs:
|
||||
CLAAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
@@ -26,7 +28,7 @@ jobs:
|
||||
path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'
|
||||
# branch should not be protected
|
||||
branch: 'main'
|
||||
allowlist: krichprollsch,francisbouvier
|
||||
allowlist: krichprollsch,francisbouvier,katie-lpd,sjorsdonkers,bornlex
|
||||
|
||||
remote-organization-name: lightpanda-io
|
||||
remote-repository-name: cla
|
||||
|
||||
211
.github/workflows/e2e-test.yml
vendored
Normal file
211
.github/workflows/e2e-test.yml
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
name: e2e-test
|
||||
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
||||
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "build.zig"
|
||||
- "src/**/*.zig"
|
||||
- "src/*.zig"
|
||||
- "vendor/zig-js-runtime"
|
||||
- ".github/**"
|
||||
- "vendor/**"
|
||||
pull_request:
|
||||
|
||||
# By default GH trigger on types opened, synchronize and reopened.
|
||||
# see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
# Since we skip the job when the PR is in draft state, we want to force CI
|
||||
# running when the PR is marked ready_for_review w/o other change.
|
||||
# see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
paths:
|
||||
- ".github/**"
|
||||
- "build.zig"
|
||||
- "src/**/*.zig"
|
||||
- "src/*.zig"
|
||||
- "vendor/**"
|
||||
- ".github/**"
|
||||
- "vendor/**"
|
||||
# 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 -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-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 tests
|
||||
run: |
|
||||
./lightpanda serve & echo $! > LPD.pid
|
||||
go run runner/main.go --verbose
|
||||
kill `cat LPD.pid`
|
||||
|
||||
cdp-and-hyperfine-bench:
|
||||
name: cdp-and-hyperfine-bench
|
||||
needs: zig-build-release
|
||||
|
||||
env:
|
||||
MAX_MEMORY: 27000
|
||||
MAX_AVG_DURATION: 23
|
||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||
|
||||
# use a self host runner.
|
||||
runs-on: lpd-bench-hetzner
|
||||
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: start http
|
||||
run: |
|
||||
go run ws/main.go & echo $! > WS.pid
|
||||
sleep 2
|
||||
|
||||
- name: run puppeteer
|
||||
run: |
|
||||
./lightpanda serve & echo $! > LPD.pid
|
||||
sleep 2
|
||||
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
|
||||
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
|
||||
kill `cat LPD.pid`
|
||||
|
||||
- name: puppeteer result
|
||||
run: cat puppeteer.out
|
||||
|
||||
- name: memory regression
|
||||
run: |
|
||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||
echo "Peak resident set size: $LPD_VmHWM"
|
||||
test "$LPD_VmHWM" -le "$MAX_MEMORY"
|
||||
|
||||
- name: duration regression
|
||||
run: |
|
||||
export PUPPETEER_AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
||||
echo "puppeteer avg duration: $PUPPETEER_AVG_DURATION"
|
||||
test "$PUPPETEER_AVG_DURATION" -le "$MAX_AVG_DURATION"
|
||||
|
||||
- name: json output
|
||||
run: |
|
||||
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 LPD_VmHWM=`cat LPD.VmHWM`
|
||||
echo "{\"duration_total\":${TOTAL_DURATION},\"duration_avg\":${AVG_DURATION},\"mem_peak\":${LPD_VmHWM}}" > bench.json
|
||||
cat bench.json
|
||||
|
||||
- name: run hyperfine
|
||||
run: |
|
||||
hyperfine --export-json=hyperfine.json --warmup 3 --runs 20 --shell=none "./lightpanda --dump http://127.0.0.1:1234/campfire-commerce/"
|
||||
|
||||
- name: stop http
|
||||
run: kill `cat WS.pid`
|
||||
|
||||
- name: write commit
|
||||
run: |
|
||||
echo "${{github.sha}}" > commit.txt
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bench-results
|
||||
path: |
|
||||
bench.json
|
||||
hyperfine.json
|
||||
commit.txt
|
||||
retention-days: 10
|
||||
|
||||
|
||||
perf-fmt:
|
||||
name: perf-fmt
|
||||
needs: cdp-and-hyperfine-bench
|
||||
|
||||
# Don't execute on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
container:
|
||||
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bench-results
|
||||
|
||||
- name: format and send json result
|
||||
run: /perf-fmt cdp ${{ github.sha }} bench.json
|
||||
|
||||
- name: format and send json result
|
||||
run: /perf-fmt hyperfine ${{ github.sha }} hyperfine.json
|
||||
48
.github/workflows/wpt.yml
vendored
48
.github/workflows/wpt.yml
vendored
@@ -7,44 +7,18 @@ env:
|
||||
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "build.zig"
|
||||
- "src/**/*.zig"
|
||||
- "src/*.zig"
|
||||
- "tests/wpt/**"
|
||||
- "vendor/**"
|
||||
- ".github/**"
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "23 2 * * *"
|
||||
|
||||
# By default GH trigger on types opened, synchronize and reopened.
|
||||
# see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
# Since we skip the job when the PR is in draft state, we want to force CI
|
||||
# running when the PR is marked ready_for_review w/o other change.
|
||||
# see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
paths:
|
||||
- ".github/**"
|
||||
- "build.zig"
|
||||
- "src/**/*.zig"
|
||||
- "src/*.zig"
|
||||
- "tests/wpt/**"
|
||||
- "vendor/**"
|
||||
- ".github/**"
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
wpt:
|
||||
name: web platform tests
|
||||
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
name: web platform tests json output
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -55,15 +29,8 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- run: zig build wpt -Dengine=v8 -- --safe --summary
|
||||
|
||||
# For now WPT tests doesn't pass at all.
|
||||
# We accept then to continue the job on failure.
|
||||
# TODO remove the continue-on-error when tests will pass.
|
||||
continue-on-error: true
|
||||
|
||||
- name: json output
|
||||
run: zig build wpt -Dengine=v8 -- --safe --json > wpt.json
|
||||
run: zig build wpt -- --json > wpt.json
|
||||
|
||||
- name: write commit
|
||||
run: |
|
||||
@@ -82,10 +49,9 @@ jobs:
|
||||
name: perf-fmt
|
||||
needs: wpt
|
||||
|
||||
# Don't execute on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
container:
|
||||
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
||||
credentials:
|
||||
|
||||
5
.github/workflows/zig-fmt.yml
vendored
5
.github/workflows/zig-fmt.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: zig-fmt
|
||||
|
||||
env:
|
||||
ZIG_VERSION: 0.13.0
|
||||
ZIG_VERSION: 0.14.1
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -29,9 +29,10 @@ jobs:
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: mlugg/setup-zig@v1
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: ${{ env.ZIG_VERSION }}
|
||||
|
||||
|
||||
38
.github/workflows/zig-test.yml
vendored
38
.github/workflows/zig-test.yml
vendored
@@ -16,6 +16,7 @@ on:
|
||||
- "src/*.zig"
|
||||
- "vendor/zig-js-runtime"
|
||||
- ".github/**"
|
||||
- "vendor/**"
|
||||
pull_request:
|
||||
|
||||
# By default GH trigger on types opened, synchronize and reopened.
|
||||
@@ -32,6 +33,7 @@ on:
|
||||
- "src/*.zig"
|
||||
- "vendor/**"
|
||||
- ".github/**"
|
||||
- "vendor/**"
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -54,30 +56,38 @@ jobs:
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- name: zig build debug
|
||||
run: zig build -Dengine=v8
|
||||
run: zig build
|
||||
|
||||
zig-build-release:
|
||||
name: zig build release
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lightpanda-build-dev
|
||||
path: |
|
||||
zig-out/bin/lightpanda
|
||||
retention-days: 1
|
||||
|
||||
# Don't run the CI on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
browser-fetch:
|
||||
name: browser fetch
|
||||
needs: zig-build-dev
|
||||
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
name: lightpanda-build-dev
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
- run: chmod a+x ./lightpanda
|
||||
|
||||
- name: zig build release
|
||||
run: zig build -Doptimize=ReleaseSafe -Dengine=v8
|
||||
- run: ./lightpanda fetch https://httpbin.io/xhr/get
|
||||
|
||||
zig-test:
|
||||
name: zig test
|
||||
timeout-minutes: 15
|
||||
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
@@ -94,7 +104,7 @@ jobs:
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- name: zig build test
|
||||
run: zig build test -Dengine=v8 -- --json > bench.json
|
||||
run: zig build test -- --json > bench.json
|
||||
|
||||
- name: write commit
|
||||
run: |
|
||||
@@ -117,6 +127,8 @@ jobs:
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
container:
|
||||
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
||||
credentials:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@ zig-cache
|
||||
zig-out
|
||||
/vendor/netsurf/out
|
||||
/vendor/libiconv/
|
||||
lightpanda.id
|
||||
/v8/
|
||||
|
||||
23
.gitmodules
vendored
23
.gitmodules
vendored
@@ -1,34 +1,21 @@
|
||||
[submodule "vendor/zig-js-runtime"]
|
||||
path = vendor/zig-js-runtime
|
||||
url = git@github.com:lightpanda-io/zig-js-runtime.git
|
||||
[submodule "vendor/netsurf/libwapcaplet"]
|
||||
path = vendor/netsurf/libwapcaplet
|
||||
url = git@github.com:lightpanda-io/libwapcaplet.git
|
||||
url = https://github.com/lightpanda-io/libwapcaplet.git/
|
||||
[submodule "vendor/netsurf/libparserutils"]
|
||||
path = vendor/netsurf/libparserutils
|
||||
url = git@github.com:lightpanda-io/libparserutils.git
|
||||
url = https://github.com/lightpanda-io/libparserutils.git/
|
||||
[submodule "vendor/netsurf/libdom"]
|
||||
path = vendor/netsurf/libdom
|
||||
url = git@github.com:lightpanda-io/libdom.git
|
||||
url = https://github.com/lightpanda-io/libdom.git/
|
||||
[submodule "vendor/netsurf/share/netsurf-buildsystem"]
|
||||
path = vendor/netsurf/share/netsurf-buildsystem
|
||||
url = https://source.netsurf-browser.org/buildsystem.git
|
||||
[submodule "vendor/netsurf/libhubbub"]
|
||||
path = vendor/netsurf/libhubbub
|
||||
url = git@github.com:lightpanda-io/libhubbub.git
|
||||
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 = git@github.com:microsoft/mimalloc.git
|
||||
[submodule "vendor/tls.zig"]
|
||||
path = vendor/tls.zig
|
||||
url = git@github.com:ianic/tls.zig.git
|
||||
[submodule "vendor/zig-async-io"]
|
||||
path = vendor/zig-async-io
|
||||
url = git@github.com:lightpanda-io/zig-async-io.git
|
||||
[submodule "vendor/websocket.zig"]
|
||||
path = vendor/websocket.zig
|
||||
url = git@github.com:lightpanda-io/websocket.zig.git
|
||||
branch = lightpanda
|
||||
url = https://github.com/microsoft/mimalloc.git/
|
||||
|
||||
10
CONTRIBUTING.md
Normal file
10
CONTRIBUTING.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Contributing
|
||||
|
||||
Lightpanda accepts pull requests through GitHub.
|
||||
|
||||
You have to sign our [CLA](CLA.md) during your first pull request process
|
||||
otherwise we're not able to accept your contributions.
|
||||
|
||||
The process signature uses the [CLA assistant
|
||||
lite](https://github.com/marketplace/actions/cla-assistant-lite). You can see
|
||||
an example of the process in [#303](https://github.com/lightpanda-io/browser/pull/303).
|
||||
69
Dockerfile
69
Dockerfile
@@ -1,11 +1,11 @@
|
||||
FROM ubuntu:22.04
|
||||
FROM debian:stable
|
||||
|
||||
ARG ZIG=0.13.0
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG=0.14.1
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG OS=linux
|
||||
ARG ARCH=x86_64
|
||||
ARG V8=11.1.134
|
||||
ARG ZIG_V8=v0.1.9
|
||||
ARG V8=13.6.233.8
|
||||
ARG ZIG_V8=v0.1.28
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
apt-get install -yq xz-utils \
|
||||
@@ -16,34 +16,23 @@ RUN apt-get update -yq && \
|
||||
curl git
|
||||
|
||||
# install minisig
|
||||
RUN curl -L -O https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-linux.tar.gz && \
|
||||
tar xvzf minisign-0.11-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
|
||||
|
||||
# install zig
|
||||
RUN curl -O https://ziglang.org/download/${ZIG}/zig-linux-x86_64-${ZIG}.tar.xz && \
|
||||
curl -O https://ziglang.org/download/${ZIG}/zig-linux-x86_64-${ZIG}.tar.xz.minisig
|
||||
|
||||
RUN minisign-linux/x86_64/minisign -Vm zig-linux-x86_64-${ZIG}.tar.xz -P ${ZIG_MINISIG}
|
||||
|
||||
# clean minisg
|
||||
RUN rm -fr minisign-0.11-linux.tar.gz minisign-linux
|
||||
|
||||
# install zig
|
||||
RUN tar xvf zig-linux-x86_64-${ZIG}.tar.xz && \
|
||||
mv zig-linux-x86_64-${ZIG} /usr/local/lib && \
|
||||
ln -s /usr/local/lib/zig-linux-x86_64-${ZIG}/zig /usr/local/bin/zig
|
||||
|
||||
# clean up zig install
|
||||
RUN rm -fr zig-linux-x86_64-${ZIG}.tar.xz zig-linux-x86_64-${ZIG}.tar.xz.minisig
|
||||
|
||||
# force use of http instead of ssh with github
|
||||
RUN cat <<EOF > /root/.gitconfig
|
||||
[url "https://github.com/"]
|
||||
insteadOf="git@github.com:"
|
||||
EOF
|
||||
RUN case $TARGETPLATFORM in \
|
||||
"linux/arm64") ARCH="aarch64" ;; \
|
||||
*) ARCH="x86_64" ;; \
|
||||
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.minisig && \
|
||||
minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG} && \
|
||||
tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
||||
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
|
||||
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
|
||||
|
||||
# clone lightpanda
|
||||
RUN git clone git@github.com:lightpanda-io/browser.git
|
||||
RUN git clone https://github.com/lightpanda-io/browser.git
|
||||
|
||||
WORKDIR /browser
|
||||
|
||||
@@ -51,27 +40,29 @@ WORKDIR /browser
|
||||
RUN git submodule init && \
|
||||
git submodule update --recursive
|
||||
|
||||
RUN cd vendor/zig-js-runtime && \
|
||||
git submodule init && \
|
||||
git submodule update --recursive
|
||||
|
||||
RUN make install-libiconv && \
|
||||
make install-netsurf && \
|
||||
make install-mimalloc
|
||||
|
||||
# download and install v8
|
||||
RUN curl -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_${OS}_${ARCH}.a && \
|
||||
mkdir -p vendor/zig-js-runtime/vendor/v8/${ARCH}-${OS}/release && \
|
||||
mv libc_v8.a vendor/zig-js-runtime/vendor/v8/${ARCH}-${OS}/release/libc_v8.a
|
||||
RUN case $TARGETPLATFORM in \
|
||||
"linux/arm64") ARCH="aarch64" ;; \
|
||||
*) ARCH="x86_64" ;; \
|
||||
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 && \
|
||||
mkdir -p v8/out/linux/release/obj/zig/ && \
|
||||
mv libc_v8.a v8/out/linux/release/obj/zig/libc_v8.a
|
||||
|
||||
# build release
|
||||
RUN make build
|
||||
|
||||
FROM ubuntu:22.04
|
||||
FROM debian:stable-slim
|
||||
|
||||
# copy ca certificates
|
||||
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
|
||||
|
||||
CMD ["/bin/lightpanda", "--host", "0.0.0.0", "--port", "3245"]
|
||||
EXPOSE 9222/tcp
|
||||
|
||||
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222"]
|
||||
|
||||
@@ -10,7 +10,6 @@ The default license for this project is [AGPL-3.0-only](LICENSE).
|
||||
The following files are licensed under MIT:
|
||||
|
||||
```
|
||||
src/http/Client.zig
|
||||
src/polyfill/fetch.js
|
||||
```
|
||||
|
||||
|
||||
64
Makefile
64
Makefile
@@ -3,12 +3,17 @@
|
||||
|
||||
ZIG := zig
|
||||
BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
# option test filter make test F="server"
|
||||
F=
|
||||
|
||||
# OS and ARCH
|
||||
kernel = $(shell uname -ms)
|
||||
ifeq ($(kernel), Darwin arm64)
|
||||
OS := macos
|
||||
ARCH := aarch64
|
||||
else ifeq ($(kernel), Darwin x86_64)
|
||||
OS := macos
|
||||
ARCH := x86_64
|
||||
else ifeq ($(kernel), Linux aarch64)
|
||||
OS := linux
|
||||
ARCH := aarch64
|
||||
@@ -42,7 +47,8 @@ help:
|
||||
|
||||
# $(ZIG) commands
|
||||
# ------------
|
||||
.PHONY: build build-dev run run-release shell test bench download-zig wpt
|
||||
.PHONY: build build-dev run run-release shell test bench download-zig wpt data get-v8 build-v8 build-v8-dev
|
||||
.PHONY: end2end
|
||||
|
||||
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
|
||||
|
||||
@@ -57,53 +63,74 @@ download-zig:
|
||||
## Build in release-safe mode
|
||||
build:
|
||||
@printf "\e[36mBuilding (release safe)...\e[0m\n"
|
||||
@$(ZIG) build -Doptimize=ReleaseSafe -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
$(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-dev:
|
||||
@printf "\e[36mBuilding (debug)...\e[0m\n"
|
||||
@$(ZIG) build -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@printf "\e[33mBuild OK\e[0m\n"
|
||||
|
||||
## Run the server in debug mode
|
||||
## Run the server in release mode
|
||||
run: build
|
||||
@printf "\e[36mRunning...\e[0m\n"
|
||||
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
|
||||
|
||||
## Run the server in debug mode
|
||||
run-debug: build-dev
|
||||
@printf "\e[36mRunning...\e[0m\n"
|
||||
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
|
||||
|
||||
## Run a JS shell in debug mode
|
||||
shell:
|
||||
@printf "\e[36mBuilding shell...\e[0m\n"
|
||||
@$(ZIG) build shell -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@$(ZIG) build shell || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
|
||||
## Run WPT tests
|
||||
wpt:
|
||||
@printf "\e[36mBuilding wpt...\e[0m\n"
|
||||
@$(ZIG) build wpt -Dengine=v8 -- --safe $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@$(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 -Dengine=v8 -- --safe --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
|
||||
## Test
|
||||
test:
|
||||
@printf "\e[36mTesting...\e[0m\n"
|
||||
@$(ZIG) build test -Dengine=v8 || (printf "\e[33mTest ERROR\e[0m\n"; exit 1;)
|
||||
@printf "\e[33mTest OK\e[0m\n"
|
||||
@TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
|
||||
|
||||
## Run demo/runner end to end tests
|
||||
end2end:
|
||||
@test -d ../demo
|
||||
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
|
||||
# ------------
|
||||
.PHONY: install-submodule
|
||||
.PHONY: install-zig-js-runtime install-zig-js-runtime-dev install-libiconv
|
||||
.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: install-submodule install-zig-js-runtime install-libiconv install-netsurf install-mimalloc
|
||||
install: install-submodule install-libiconv install-netsurf install-mimalloc
|
||||
|
||||
## Install and build dependencies for dev
|
||||
install-dev: install-submodule install-zig-js-runtime-dev install-libiconv install-netsurf-dev install-mimalloc-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
|
||||
@@ -139,7 +166,7 @@ _install-netsurf: clean-netsurf
|
||||
BUILDDIR=$(BC_NS)/build/libdom make install && \
|
||||
printf "\e[33mRunning libdom example...\e[0m\n" && \
|
||||
cd examples && \
|
||||
zig cc \
|
||||
$(ZIG) cc \
|
||||
-I$(ICONV)/include \
|
||||
-I$(BC_NS)/include \
|
||||
-L$(ICONV)/lib \
|
||||
@@ -186,13 +213,8 @@ ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
|
||||
make clean
|
||||
endif
|
||||
|
||||
install-zig-js-runtime-dev:
|
||||
@cd vendor/zig-js-runtime && \
|
||||
make install-dev
|
||||
|
||||
install-zig-js-runtime:
|
||||
@cd vendor/zig-js-runtime && \
|
||||
make install
|
||||
data:
|
||||
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
||||
|
||||
.PHONY: _build_mimalloc
|
||||
|
||||
|
||||
228
README.md
228
README.md
@@ -7,78 +7,166 @@
|
||||
<p align="center"><a href="https://lightpanda.io/">lightpanda.io</a></p>
|
||||
|
||||
<div align="center">
|
||||
<br />
|
||||
|
||||
[](https://github.com/lightpanda-io/browser/blob/main/LICENSE)
|
||||
[](https://twitter.com/lightpanda_io)
|
||||
[](https://github.com/lightpanda-io/browser)
|
||||
|
||||
</div>
|
||||
|
||||
Lightpanda is the open-source browser made for headless usage:
|
||||
|
||||
- Javascript execution
|
||||
- Support of the Web APIs (partial, WIP)
|
||||
- Compatible with Playwright, Puppeteer through CDP (WIP)
|
||||
- Support of Web APIs (partial, WIP)
|
||||
- Compatible with Playwright[^1], Puppeteer, chromedp through CDP
|
||||
|
||||
Fast scraping and web automation with minimal memory footprint:
|
||||
Fast web automation for AI agents, LLM training, scraping and testing:
|
||||
|
||||
- Ultra-low memory footprint (9x less than Chrome)
|
||||
- Blazingly fast execution (11x faster than Chrome) & instant startup
|
||||
- Exceptionally fast execution (11x faster than Chrome)
|
||||
- Instant startup
|
||||
|
||||
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark_2024-12-04.png">
|
||||
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/execution-time.svg">
|
||||
](https://github.com/lightpanda-io/demo)
|
||||
 
|
||||
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/memory-frame.svg">
|
||||
](https://github.com/lightpanda-io/demo)
|
||||
</div>
|
||||
|
||||
See [benchmark details](https://github.com/lightpanda-io/demo).
|
||||
_Puppeteer requesting 100 pages from a local website on a AWS EC2 m5.large instance.
|
||||
See [benchmark details](https://github.com/lightpanda-io/demo)._
|
||||
|
||||
## Why?
|
||||
[^1]: **Playwright support disclaimer:**
|
||||
Due to the nature of Playwright, a script that works with the current version of the browser may not function correctly with a future version. Playwright uses an intermediate JavaScript layer that selects an execution strategy based on the browser's available features. If Lightpanda adds a new [Web API](https://developer.mozilla.org/en-US/docs/Web/API), Playwright may choose to execute different code for the same script. This new code path could attempt to use features that are not yet implemented. Lightpanda makes an effort to add compatibility tests, but we can't cover all scenarios. If you encounter an issue, please create a [GitHub issue](https://github.com/lightpanda-io/browser/issues) and include the last known working version of the script.
|
||||
|
||||
### Javascript execution is mandatory for the modern web
|
||||
## Quick start
|
||||
|
||||
Back in the good old times, grabbing a webpage was as easy as making an HTTP request, cURL-like. It’s not possible anymore, because Javascript is everywhere, like it or not:
|
||||
### Install
|
||||
**Install from the nightly builds**
|
||||
|
||||
- Ajax, Single Page App, Infinite loading, “click to display”, instant search, etc.
|
||||
- JS web frameworks: React, Vue, Angular & others
|
||||
You can download the last binary from the [nightly
|
||||
builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for
|
||||
Linux x86_64 and MacOS aarch64.
|
||||
|
||||
### Chrome is not the right tool
|
||||
*For Linux*
|
||||
```console
|
||||
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux && \
|
||||
chmod a+x ./lightpanda
|
||||
```
|
||||
|
||||
So if we need Javascript, why not use a real web browser. Let’s take a huge desktop application, hack it, and run it on the server, right? Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure it’s such a good idea?
|
||||
*For MacOS*
|
||||
```console
|
||||
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-aarch64-macos && \
|
||||
chmod a+x ./lightpanda
|
||||
```
|
||||
|
||||
- Heavy on RAM and CPU, expensive to run
|
||||
- Hard to package, deploy and maintain at scale
|
||||
- Bloated, lots of features are not useful in headless usage
|
||||
*For Windows + WSL2*
|
||||
|
||||
### Lightpanda is built for performance
|
||||
The Lightpanda browser is compatible to run on windows inside WSL. Follow the Linux instruction for installation from a WSL terminal.
|
||||
It is recommended to install clients like Puppeteer on the Windows host.
|
||||
|
||||
If we want both Javascript and performance, for a real headless browser, we need to start from scratch. Not yet another iteration of Chromium, really from a blank page. Crazy right? But that’s we did:
|
||||
**Install from Docker**
|
||||
|
||||
- Not based on Chromium, Blink or WebKit
|
||||
- Low-level system programming language (Zig) with optimisations in mind
|
||||
- Opinionated, without graphical rendering
|
||||
Lightpanda provides [official Docker
|
||||
images](https://hub.docker.com/r/lightpanda/browser) for both Linux amd64 and
|
||||
arm64 architectures.
|
||||
The following command fetches the Docker image and starts a new container exposing Lightpanda's CDP server on port `9222`.
|
||||
The `--privileged` option is required because the browser requires `io_uring` syscalls which are blocked by default by Docker.
|
||||
```console
|
||||
docker run -d --name lightpanda -p 9222:9222 --privileged lightpanda/browser:nightly
|
||||
```
|
||||
|
||||
### Dump a URL
|
||||
|
||||
```console
|
||||
./lightpanda fetch --dump https://lightpanda.io
|
||||
```
|
||||
```console
|
||||
info(browser): GET https://lightpanda.io/ http.Status.ok
|
||||
info(browser): fetch script https://api.website.lightpanda.io/js/script.js: http.Status.ok
|
||||
info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeError: Cannot read properties of undefined (reading 'pushState')
|
||||
<!DOCTYPE html>
|
||||
```
|
||||
|
||||
### Start a CDP server
|
||||
|
||||
```console
|
||||
./lightpanda serve --host 127.0.0.1 --port 9222
|
||||
```
|
||||
```console
|
||||
info(websocket): starting blocking worker to listen on 127.0.0.1:9222
|
||||
info(server): accepting new conn...
|
||||
```
|
||||
|
||||
Once the CDP server started, you can run a Puppeteer script by configuring the
|
||||
`browserWSEndpoint`.
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
|
||||
import puppeteer from 'puppeteer-core';
|
||||
|
||||
// use browserWSEndpoint to pass the Lightpanda's CDP server address.
|
||||
const browser = await puppeteer.connect({
|
||||
browserWSEndpoint: "ws://127.0.0.1:9222",
|
||||
});
|
||||
|
||||
// The rest of your script remains the same.
|
||||
const context = await browser.createBrowserContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
// Dump all the links from the page.
|
||||
await page.goto('https://wikipedia.com/');
|
||||
|
||||
const links = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll('a')).map(row => {
|
||||
return row.getAttribute('href');
|
||||
});
|
||||
});
|
||||
|
||||
console.log(links);
|
||||
|
||||
await page.close();
|
||||
await context.close();
|
||||
await browser.disconnect();
|
||||
```
|
||||
|
||||
### Telemetry
|
||||
By default, Lightpanda collects and sends usage telemetry. This can be disabled by setting an environment variable `LIGHTPANDA_DISABLE_TELEMETRY=true`. You can read Lightpanda's privacy policy at: [https://lightpanda.io/privacy-policy](https://lightpanda.io/privacy-policy).
|
||||
|
||||
## Status
|
||||
|
||||
Lightpanda is still a work in progress and is currently at a Beta stage.
|
||||
Lightpanda is in Beta and currently a work in progress. Stability and coverage are improving and many websites now work.
|
||||
You may still encounter errors or crashes. Please open an issue with specifics if so.
|
||||
|
||||
Here are the key features we have implemented:
|
||||
|
||||
- [x] HTTP loader
|
||||
- [x] HTML parser and DOM tree
|
||||
- [x] HTTP loader
|
||||
- [x] HTML parser and DOM tree (based on Netsurf libs)
|
||||
- [x] Javascript support (v8)
|
||||
- [x] Basic DOM APIs
|
||||
- [x] DOM APIs
|
||||
- [x] Ajax
|
||||
- [x] XHR API
|
||||
- [x] Fetch API
|
||||
- [x] Fetch API (polyfill)
|
||||
- [x] DOM dump
|
||||
- [x] Basic CDP/websockets server
|
||||
- [x] CDP/websockets server
|
||||
- [x] Click
|
||||
- [x] Input form
|
||||
- [x] Cookies
|
||||
- [x] Custom HTTP headers
|
||||
- [ ] Proxy support
|
||||
- [ ] Network interception
|
||||
|
||||
NOTE: There are hundreds of Web APIs. Developing a browser, even just for headless mode, is a huge task. It's more about coverage than a _working/not working_ binary situation.
|
||||
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||
|
||||
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
|
||||
|
||||
## Install
|
||||
|
||||
We do provide [nighly builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for Linux x86_64 and MacOS aarch64.
|
||||
|
||||
## Build from sources
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.13.0`. You have to
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.14.1`. You have to
|
||||
install it with the right version in order to build the project.
|
||||
|
||||
Lightpanda also depends on
|
||||
@@ -94,10 +182,15 @@ For Debian/Ubuntu based Linux:
|
||||
sudo apt install xz-utils \
|
||||
python3 ca-certificates git \
|
||||
pkg-config libglib2.0-dev \
|
||||
gperf libexpat1-dev \
|
||||
gperf libexpat1-dev unzip rsync \
|
||||
cmake clang
|
||||
```
|
||||
|
||||
For systems with [Nix](https://nixos.org/download/), you can use the devShell:
|
||||
```
|
||||
nix develop
|
||||
```
|
||||
|
||||
For MacOS, you only need cmake:
|
||||
|
||||
```
|
||||
@@ -110,9 +203,9 @@ brew install cmake
|
||||
|
||||
You can run `make install` to install deps all in one (or `make install-dev` if you need the development versions).
|
||||
|
||||
Be aware that the build task is very long and cpu consuming, as you will build from sources all dependancies, including the v8 Javascript engine.
|
||||
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.
|
||||
|
||||
#### Step by step build dependancy
|
||||
#### Step by step build dependency
|
||||
|
||||
The project uses git submodules for dependencies.
|
||||
|
||||
@@ -122,6 +215,14 @@ To init or update the submodules in the `vendor/` directory:
|
||||
make install-submodule
|
||||
```
|
||||
|
||||
**iconv**
|
||||
|
||||
libiconv is an internationalization library used by Netsurf.
|
||||
|
||||
```
|
||||
make install-libiconv
|
||||
```
|
||||
|
||||
**Netsurf libs**
|
||||
|
||||
Netsurf libs are used for HTML parsing and DOM tree generation.
|
||||
@@ -146,17 +247,21 @@ 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).
|
||||
|
||||
**zig-js-runtime**
|
||||
**v8**
|
||||
|
||||
Our own Zig/Javascript runtime, which includes the v8 Javascript engine.
|
||||
|
||||
This build task is very long and cpu consuming, as you will build v8 from sources.
|
||||
First, get the tools necessary for building V8, as well as the V8 source code:
|
||||
|
||||
```
|
||||
make install-zig-js-runtime
|
||||
make get-v8
|
||||
```
|
||||
|
||||
For dev env, use `make iinstall-zig-js-runtime-dev`.
|
||||
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
|
||||
|
||||
@@ -164,6 +269,20 @@ For dev env, use `make iinstall-zig-js-runtime-dev`.
|
||||
|
||||
You can test Lightpanda by running `make test`.
|
||||
|
||||
### End to end tests
|
||||
|
||||
To run end to end tests, you need to clone the [demo
|
||||
repository](https://github.com/lightpanda-io/demo) into `../demo` dir.
|
||||
|
||||
You have to install the [demo's node
|
||||
requirements](https://github.com/lightpanda-io/demo?tab=readme-ov-file#dependencies-1)
|
||||
|
||||
You also need to install [Go](https://go.dev) > v1.24.
|
||||
|
||||
```
|
||||
make end2end
|
||||
```
|
||||
|
||||
### Web Platform Tests
|
||||
|
||||
Lightpanda is tested against the standardized [Web Platform
|
||||
@@ -205,3 +324,28 @@ Lightpanda accepts pull requests through GitHub.
|
||||
|
||||
You have to sign our [CLA](CLA.md) during the pull request process otherwise
|
||||
we're not able to accept your contributions.
|
||||
|
||||
## Why?
|
||||
|
||||
### Javascript execution is mandatory for the modern web
|
||||
|
||||
In the good old days, scraping a webpage was as easy as making an HTTP request, cURL-like. It’s not possible anymore, because Javascript is everywhere, like it or not:
|
||||
|
||||
- Ajax, Single Page App, infinite loading, “click to display”, instant search, etc.
|
||||
- JS web frameworks: React, Vue, Angular & others
|
||||
|
||||
### Chrome is not the right tool
|
||||
|
||||
If we need Javascript, why not use a real web browser? Take a huge desktop application, hack it, and run it on the server. Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure it’s such a good idea?
|
||||
|
||||
- Heavy on RAM and CPU, expensive to run
|
||||
- Hard to package, deploy and maintain at scale
|
||||
- Bloated, lots of features are not useful in headless usage
|
||||
|
||||
### Lightpanda is built for performance
|
||||
|
||||
If we want both Javascript and performance in a true headless browser, we need to start from scratch. Not another iteration of Chromium, really from a blank page. Crazy right? But that’s what we did:
|
||||
|
||||
- Not based on Chromium, Blink or WebKit
|
||||
- Low-level system programming language (Zig) with optimisations in mind
|
||||
- Opinionated: without graphical rendering
|
||||
|
||||
343
build.zig
343
build.zig
@@ -17,16 +17,11 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const jsruntime_path = "vendor/zig-js-runtime/";
|
||||
const jsruntime = @import("vendor/zig-js-runtime/build.zig");
|
||||
const jsruntime_pkgs = jsruntime.packages(jsruntime_path);
|
||||
|
||||
/// Do not rename this constant. It is scanned by some scripts to determine
|
||||
/// which zig version to install.
|
||||
const recommended_zig_version = jsruntime.recommended_zig_version;
|
||||
const recommended_zig_version = "0.14.1";
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
||||
@@ -42,173 +37,202 @@ pub fn build(b: *std.Build) !void {
|
||||
},
|
||||
}
|
||||
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const mode = b.standardOptimizeOption(.{});
|
||||
|
||||
const options = jsruntime.buildOptions(b);
|
||||
|
||||
// browser
|
||||
// -------
|
||||
|
||||
// compile and install
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "lightpanda",
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(b, exe, options);
|
||||
b.installArtifact(exe);
|
||||
|
||||
// run
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
// shell
|
||||
// -----
|
||||
|
||||
// compile and install
|
||||
const shell = b.addExecutable(.{
|
||||
.name = "lightpanda-shell",
|
||||
.root_source_file = b.path("src/main_shell.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(b, shell, options);
|
||||
try jsruntime_pkgs.add_shell(shell);
|
||||
|
||||
// run
|
||||
const shell_cmd = b.addRunArtifact(shell);
|
||||
if (b.args) |args| {
|
||||
shell_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const shell_step = b.step("shell", "Run JS shell");
|
||||
shell_step.dependOn(&shell_cmd.step);
|
||||
|
||||
// test
|
||||
// ----
|
||||
|
||||
// compile
|
||||
const tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/run_tests.zig"),
|
||||
.test_runner = b.path("src/test_runner.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(b, tests, options);
|
||||
|
||||
// add jsruntime pretty deps
|
||||
tests.root_module.addAnonymousImport("pretty", .{
|
||||
.root_source_file = b.path("vendor/zig-js-runtime/src/pretty.zig"),
|
||||
});
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
if (b.args) |args| {
|
||||
run_tests.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_tests.step);
|
||||
|
||||
// wpt
|
||||
// -----
|
||||
|
||||
// compile and install
|
||||
const wpt = b.addExecutable(.{
|
||||
.name = "lightpanda-wpt",
|
||||
.root_source_file = b.path("src/main_wpt.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(b, wpt, options);
|
||||
|
||||
// run
|
||||
const wpt_cmd = b.addRunArtifact(wpt);
|
||||
if (b.args) |args| {
|
||||
wpt_cmd.addArgs(args);
|
||||
}
|
||||
// step
|
||||
const wpt_step = b.step("wpt", "WPT tests");
|
||||
wpt_step.dependOn(&wpt_cmd.step);
|
||||
}
|
||||
|
||||
fn common(
|
||||
b: *std.Build,
|
||||
step: *std.Build.Step.Compile,
|
||||
options: jsruntime.Options,
|
||||
) !void {
|
||||
const target = step.root_module.resolved_target.?;
|
||||
const jsruntimemod = try jsruntime_pkgs.module(
|
||||
b,
|
||||
options,
|
||||
step.root_module.optimize.?,
|
||||
target,
|
||||
var opts = b.addOptions();
|
||||
opts.addOption(
|
||||
[]const u8,
|
||||
"git_commit",
|
||||
b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
|
||||
);
|
||||
step.root_module.addImport("jsruntime", jsruntimemod);
|
||||
|
||||
const netsurf = try moduleNetSurf(b, target);
|
||||
netsurf.addImport("jsruntime", jsruntimemod);
|
||||
step.root_module.addImport("netsurf", netsurf);
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const asyncio = b.addModule("asyncio", .{
|
||||
.root_source_file = b.path("vendor/zig-async-io/src/lib.zig"),
|
||||
});
|
||||
step.root_module.addImport("asyncio", asyncio);
|
||||
{
|
||||
// browser
|
||||
// -------
|
||||
|
||||
const tlsmod = b.addModule("tls", .{
|
||||
.root_source_file = b.path("vendor/tls.zig/src/main.zig"),
|
||||
});
|
||||
step.root_module.addImport("tls", tlsmod);
|
||||
// compile and install
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "lightpanda",
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
});
|
||||
|
||||
const wsmod = b.addModule("websocket", .{
|
||||
.root_source_file = b.path("vendor/websocket.zig/src/websocket.zig"),
|
||||
});
|
||||
step.root_module.addImport("websocket", wsmod);
|
||||
try common(b, opts, exe);
|
||||
b.installArtifact(exe);
|
||||
|
||||
// run
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}
|
||||
|
||||
{
|
||||
// get v8
|
||||
// -------
|
||||
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
|
||||
const get_v8 = b.addRunArtifact(v8.artifact("get-v8"));
|
||||
const get_step = b.step("get-v8", "Get v8");
|
||||
get_step.dependOn(&get_v8.step);
|
||||
}
|
||||
|
||||
{
|
||||
// build v8
|
||||
// -------
|
||||
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
|
||||
const build_v8 = b.addRunArtifact(v8.artifact("build-v8"));
|
||||
const build_step = b.step("build-v8", "Build v8");
|
||||
build_step.dependOn(&build_v8.step);
|
||||
}
|
||||
|
||||
{
|
||||
// tests
|
||||
// ----
|
||||
|
||||
// compile
|
||||
const tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
try common(b, opts, tests);
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
if (b.args) |args| {
|
||||
run_tests.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const tests_step = b.step("test", "Run unit tests");
|
||||
tests_step.dependOn(&run_tests.step);
|
||||
}
|
||||
|
||||
{
|
||||
// wpt
|
||||
// -----
|
||||
|
||||
// compile and install
|
||||
const wpt = b.addExecutable(.{
|
||||
.name = "lightpanda-wpt",
|
||||
.root_source_file = b.path("src/main_wpt.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
try common(b, opts, wpt);
|
||||
|
||||
// run
|
||||
const wpt_cmd = b.addRunArtifact(wpt);
|
||||
if (b.args) |args| {
|
||||
wpt_cmd.addArgs(args);
|
||||
}
|
||||
// step
|
||||
const wpt_step = b.step("wpt", "WPT tests");
|
||||
wpt_step.dependOn(&wpt_cmd.step);
|
||||
}
|
||||
}
|
||||
|
||||
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
|
||||
const mod = b.addModule("netsurf", .{
|
||||
.root_source_file = b.path("src/netsurf/netsurf.zig"),
|
||||
.target = target,
|
||||
});
|
||||
fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Compile) !void {
|
||||
const mod = step.root_module;
|
||||
const target = mod.resolved_target.?;
|
||||
const optimize = mod.optimize.?;
|
||||
const dep_opts = .{ .target = target, .optimize = optimize };
|
||||
|
||||
try moduleNetSurf(b, step, target);
|
||||
mod.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
|
||||
mod.addImport("tigerbeetle-io", b.dependency("tigerbeetle_io", .{}).module("tigerbeetle_io"));
|
||||
|
||||
{
|
||||
// v8
|
||||
const v8_opts = b.addOptions();
|
||||
v8_opts.addOption(bool, "inspector_subtype", false);
|
||||
|
||||
const v8_mod = b.dependency("v8", dep_opts).module("v8");
|
||||
v8_mod.addOptions("default_exports", v8_opts);
|
||||
mod.addImport("v8", v8_mod);
|
||||
}
|
||||
|
||||
mod.link_libcpp = true;
|
||||
|
||||
{
|
||||
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
|
||||
const os = switch (target.result.os.tag) {
|
||||
.linux => "linux",
|
||||
.macos => "macos",
|
||||
else => return error.UnsupportedPlatform,
|
||||
};
|
||||
var lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
"v8/out/{s}/{s}/obj/zig/libc_v8.a",
|
||||
.{ os, release_dir },
|
||||
);
|
||||
std.fs.cwd().access(lib_path, .{}) catch {
|
||||
// legacy path
|
||||
lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
"v8/out/{s}/obj/zig/libc_v8.a",
|
||||
.{release_dir},
|
||||
);
|
||||
};
|
||||
mod.addObjectFile(mod.owner.path(lib_path));
|
||||
}
|
||||
|
||||
switch (target.result.os.tag) {
|
||||
.macos => {
|
||||
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
|
||||
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
|
||||
mod.linkFramework("CoreFoundation", .{});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
mod.addImport("build_config", opts.createModule());
|
||||
}
|
||||
|
||||
fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build.ResolvedTarget) !void {
|
||||
const os = target.result.os.tag;
|
||||
const arch = target.result.cpu.arch;
|
||||
|
||||
// iconv
|
||||
const libiconv_lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
b.allocator,
|
||||
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
const libiconv_include_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
b.allocator,
|
||||
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addObjectFile(b.path(libiconv_lib_path));
|
||||
mod.addIncludePath(b.path(libiconv_include_path));
|
||||
step.addObjectFile(b.path(libiconv_lib_path));
|
||||
step.addIncludePath(b.path(libiconv_include_path));
|
||||
|
||||
// mimalloc
|
||||
mod.addImport("mimalloc", (try moduleMimalloc(b, target)));
|
||||
{
|
||||
// mimalloc
|
||||
const mimalloc = "vendor/mimalloc";
|
||||
const lib_path = try std.fmt.allocPrint(
|
||||
b.allocator,
|
||||
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
step.addObjectFile(b.path(lib_path));
|
||||
step.addIncludePath(b.path(mimalloc ++ "/include"));
|
||||
}
|
||||
|
||||
// netsurf libs
|
||||
const ns = "vendor/netsurf";
|
||||
const ns_include_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
b.allocator,
|
||||
ns ++ "/out/{s}-{s}/include",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addIncludePath(b.path(ns_include_path));
|
||||
step.addIncludePath(b.path(ns_include_path));
|
||||
|
||||
const libs: [4][]const u8 = .{
|
||||
"libdom",
|
||||
@@ -218,34 +242,11 @@ fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Mo
|
||||
};
|
||||
inline for (libs) |lib| {
|
||||
const ns_lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
b.allocator,
|
||||
ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addObjectFile(b.path(ns_lib_path));
|
||||
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
|
||||
step.addObjectFile(b.path(ns_lib_path));
|
||||
step.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
|
||||
}
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
fn moduleMimalloc(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
|
||||
const mod = b.addModule("mimalloc", .{
|
||||
.root_source_file = b.path("src/mimalloc/mimalloc.zig"),
|
||||
.target = target,
|
||||
});
|
||||
|
||||
const os = target.result.os.tag;
|
||||
const arch = target.result.cpu.arch;
|
||||
|
||||
const mimalloc = "vendor/mimalloc";
|
||||
const lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addObjectFile(b.path(lib_path));
|
||||
mod.addIncludePath(b.path(mimalloc ++ "/include"));
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
22
build.zig.zon
Normal file
22
build.zig.zon
Normal file
@@ -0,0 +1,22 @@
|
||||
.{
|
||||
.name = .browser,
|
||||
.paths = .{""},
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xda130f3af836cea0,
|
||||
.dependencies = .{
|
||||
.tls = .{
|
||||
.url = "https://github.com/ianic/tls.zig/archive/55845f755d9e2e821458ea55693f85c737cd0c7a.tar.gz",
|
||||
.hash = "tls-0.1.0-ER2e0m43BQAshi8ixj1qf3w2u2lqKtXtkrxUJ4AGZDcl",
|
||||
},
|
||||
.tigerbeetle_io = .{
|
||||
.url = "https://github.com/lightpanda-io/tigerbeetle-io/archive/61d9652f1a957b7f4db723ea6aa0ce9635e840ce.tar.gz",
|
||||
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
|
||||
},
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/b22911e02e4884a76acf52aa9aff2ba169d05b40.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH69zCAwAzm1u5cQVa-uG5ib2y6PpENXCl8yEYdUYk",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
|
||||
},
|
||||
}
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1748964450,
|
||||
"narHash": "sha256-ZouDiXkUk8mkMnah10QcoQ9Nu6UW6AFAHLScS3En6aI=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9ff500cd9e123f46c55855eca64beccead29b152",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "release-25.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
59
flake.nix
Normal file
59
flake.nix
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
description = "headless browser designed for AI and automation";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/release-25.05";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
|
||||
# We need crtbeginS.o for building.
|
||||
crtFiles = pkgs.runCommand "crt-files" { } ''
|
||||
mkdir -p $out/lib
|
||||
cp -r ${pkgs.gcc.cc}/lib/gcc $out/lib/gcc
|
||||
'';
|
||||
|
||||
# This build pipeline is very unhappy without an FHS-compliant env.
|
||||
fhs = pkgs.buildFHSEnv {
|
||||
name = "fhs-shell";
|
||||
multiArch = true;
|
||||
targetPkgs =
|
||||
pkgs: with pkgs; [
|
||||
# Build Tools
|
||||
zig
|
||||
zls
|
||||
python3
|
||||
pkg-config
|
||||
cmake
|
||||
gperf
|
||||
|
||||
# GCC
|
||||
gcc
|
||||
gcc.cc.lib
|
||||
crtFiles
|
||||
|
||||
# Libaries
|
||||
expat.dev
|
||||
glib.dev
|
||||
glibc.dev
|
||||
zlib
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = fhs.env;
|
||||
}
|
||||
);
|
||||
}
|
||||
109
src/app.zig
Normal file
109
src/app.zig
Normal file
@@ -0,0 +1,109 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("log.zig");
|
||||
const Loop = @import("runtime/loop.zig").Loop;
|
||||
const http = @import("http/client.zig");
|
||||
const Platform = @import("runtime/js.zig").Platform;
|
||||
|
||||
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 {
|
||||
loop: *Loop,
|
||||
config: Config,
|
||||
platform: ?*const Platform,
|
||||
allocator: Allocator,
|
||||
telemetry: Telemetry,
|
||||
http_client: http.Client,
|
||||
app_dir_path: ?[]const u8,
|
||||
notification: *Notification,
|
||||
|
||||
pub const RunMode = enum {
|
||||
help,
|
||||
fetch,
|
||||
serve,
|
||||
version,
|
||||
};
|
||||
|
||||
pub const Config = struct {
|
||||
run_mode: RunMode,
|
||||
platform: ?*const Platform = null,
|
||||
tls_verify_host: bool = true,
|
||||
http_proxy: ?std.Uri = null,
|
||||
proxy_type: ?http.ProxyType = null,
|
||||
proxy_auth: ?http.ProxyAuth = null,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator, config: Config) !*App {
|
||||
const app = try allocator.create(App);
|
||||
errdefer allocator.destroy(app);
|
||||
|
||||
const loop = try allocator.create(Loop);
|
||||
errdefer allocator.destroy(loop);
|
||||
|
||||
loop.* = try Loop.init(allocator);
|
||||
errdefer loop.deinit();
|
||||
|
||||
const notification = try Notification.init(allocator, null);
|
||||
errdefer notification.deinit();
|
||||
|
||||
const app_dir_path = getAndMakeAppDir(allocator);
|
||||
|
||||
app.* = .{
|
||||
.loop = loop,
|
||||
.allocator = allocator,
|
||||
.telemetry = undefined,
|
||||
.platform = config.platform,
|
||||
.app_dir_path = app_dir_path,
|
||||
.notification = notification,
|
||||
.http_client = try http.Client.init(allocator, loop, .{
|
||||
.max_concurrent = 3,
|
||||
.http_proxy = config.http_proxy,
|
||||
.proxy_type = config.proxy_type,
|
||||
.proxy_auth = config.proxy_auth,
|
||||
.tls_verify_host = config.tls_verify_host,
|
||||
}),
|
||||
.config = config,
|
||||
};
|
||||
app.telemetry = Telemetry.init(app, config.run_mode);
|
||||
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.loop.deinit();
|
||||
allocator.destroy(self.loop);
|
||||
self.http_client.deinit();
|
||||
self.notification.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;
|
||||
}
|
||||
76
src/browser/State.zig
Normal file
76
src/browser/State.zig
Normal file
@@ -0,0 +1,76 @@
|
||||
// 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 Env = @import("env.zig").Env;
|
||||
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").StyleSheet;
|
||||
const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
|
||||
// for HTMLScript (but probably needs to be added to more)
|
||||
onload: ?Env.Function = null,
|
||||
onerror: ?Env.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,
|
||||
|
||||
// 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,
|
||||
};
|
||||
@@ -17,597 +17,104 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Types = @import("root").Types;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const Loader = @import("loader.zig").Loader;
|
||||
const Dump = @import("dump.zig");
|
||||
const Mime = @import("mime.zig");
|
||||
const State = @import("State.zig");
|
||||
const Env = @import("env.zig").Env;
|
||||
const App = @import("../app.zig").App;
|
||||
const Session = @import("session.zig").Session;
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Loop = jsruntime.Loop;
|
||||
const Env = jsruntime.Env;
|
||||
const log = @import("../log.zig");
|
||||
|
||||
const apiweb = @import("../apiweb.zig");
|
||||
|
||||
const Window = @import("../html/window.zig").Window;
|
||||
const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
|
||||
const FetchResult = @import("../http/Client.zig").Client.FetchResult;
|
||||
|
||||
const UserContext = @import("../user_context.zig").UserContext;
|
||||
const HttpClient = @import("asyncio").Client;
|
||||
|
||||
const polyfill = @import("../polyfill/polyfill.zig");
|
||||
|
||||
const log = std.log.scoped(.browser);
|
||||
const http = @import("../http/client.zig");
|
||||
|
||||
// Browser is an instance of the browser.
|
||||
// You can create multiple browser instances.
|
||||
// A browser contains only one session.
|
||||
// TODO allow multiple sessions per browser.
|
||||
pub const Browser = struct {
|
||||
session: Session = undefined,
|
||||
env: *Env,
|
||||
app: *App,
|
||||
session: ?Session,
|
||||
allocator: Allocator,
|
||||
http_client: *http.Client,
|
||||
page_arena: ArenaAllocator,
|
||||
session_arena: ArenaAllocator,
|
||||
transfer_arena: ArenaAllocator,
|
||||
notification: *Notification,
|
||||
state_pool: std.heap.MemoryPool(State),
|
||||
|
||||
const uri = "about:blank";
|
||||
pub fn init(app: *App) !Browser {
|
||||
const allocator = app.allocator;
|
||||
|
||||
pub fn init(self: *Browser, alloc: std.mem.Allocator, loop: *Loop, vm: jsruntime.VM) !void {
|
||||
// We want to ensure the caller initialised a VM, but the browser
|
||||
// doesn't use it directly...
|
||||
_ = vm;
|
||||
const env = try Env.init(allocator, app.platform, .{});
|
||||
errdefer env.deinit();
|
||||
|
||||
try Session.init(&self.session, alloc, loop, uri);
|
||||
const notification = try Notification.init(allocator, app.notification);
|
||||
errdefer notification.deinit();
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.env = env,
|
||||
.session = null,
|
||||
.allocator = allocator,
|
||||
.notification = notification,
|
||||
.http_client = &app.http_client,
|
||||
.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.session.deinit();
|
||||
}
|
||||
|
||||
pub fn newSession(
|
||||
self: *Browser,
|
||||
alloc: std.mem.Allocator,
|
||||
loop: *jsruntime.Loop,
|
||||
) !void {
|
||||
self.session.deinit();
|
||||
try Session.init(&self.session, alloc, loop, uri);
|
||||
}
|
||||
};
|
||||
|
||||
// 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.
|
||||
pub const Session = struct {
|
||||
// allocator used to init the arena.
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
// The arena is used only to bound the js env init b/c it leaks memory.
|
||||
// see https://github.com/lightpanda-io/jsruntime-lib/issues/181
|
||||
//
|
||||
// The arena is initialised with self.alloc allocator.
|
||||
// all others Session deps use directly self.alloc and not the arena.
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
uri: []const u8,
|
||||
|
||||
// TODO handle proxy
|
||||
loader: Loader,
|
||||
env: Env = undefined,
|
||||
inspector: ?jsruntime.Inspector = null,
|
||||
window: Window,
|
||||
// TODO move the shed to the browser?
|
||||
storageShed: storage.Shed,
|
||||
page: ?Page = null,
|
||||
httpClient: HttpClient,
|
||||
|
||||
jstypes: [Types.len]usize = undefined,
|
||||
|
||||
fn init(self: *Session, alloc: std.mem.Allocator, loop: *Loop, uri: []const u8) !void {
|
||||
self.* = Session{
|
||||
.uri = uri,
|
||||
.alloc = alloc,
|
||||
.arena = std.heap.ArenaAllocator.init(alloc),
|
||||
.window = Window.create(null),
|
||||
.loader = Loader.init(alloc),
|
||||
.storageShed = storage.Shed.init(alloc),
|
||||
.httpClient = undefined,
|
||||
};
|
||||
|
||||
Env.init(&self.env, self.arena.allocator(), loop, null);
|
||||
self.httpClient = .{ .allocator = alloc };
|
||||
try self.env.load(&self.jstypes);
|
||||
}
|
||||
|
||||
fn deinit(self: *Session) void {
|
||||
if (self.page) |*p| p.end();
|
||||
|
||||
if (self.inspector) |inspector| {
|
||||
inspector.deinit(self.alloc);
|
||||
}
|
||||
|
||||
self.closeSession();
|
||||
self.env.deinit();
|
||||
self.arena.deinit();
|
||||
|
||||
self.httpClient.deinit();
|
||||
self.loader.deinit();
|
||||
self.storageShed.deinit();
|
||||
self.page_arena.deinit();
|
||||
self.session_arena.deinit();
|
||||
self.transfer_arena.deinit();
|
||||
self.notification.deinit();
|
||||
self.state_pool.deinit();
|
||||
}
|
||||
|
||||
pub fn initInspector(
|
||||
self: *Session,
|
||||
ctx: anytype,
|
||||
onResp: jsruntime.InspectorOnResponseFn,
|
||||
onEvent: jsruntime.InspectorOnEventFn,
|
||||
) !void {
|
||||
const ctx_opaque = @as(*anyopaque, @ptrCast(ctx));
|
||||
self.inspector = try jsruntime.Inspector.init(self.alloc, self.env, ctx_opaque, onResp, onEvent);
|
||||
self.env.setInspector(self.inspector.?);
|
||||
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 callInspector(self: *Session, msg: []const u8) void {
|
||||
if (self.inspector) |inspector| {
|
||||
inspector.send(msg, self.env);
|
||||
} else {
|
||||
@panic("No Inspector");
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if (self.page != null) return error.SessionPageExists;
|
||||
const p: Page = undefined;
|
||||
self.page = p;
|
||||
Page.init(&self.page.?, self.alloc, self);
|
||||
return &self.page.?;
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
// Page navigates to an url.
|
||||
// You can navigates multiple urls with the same page, but you have to call
|
||||
// end() to stop the previous navigation before starting a new one.
|
||||
// The page handle all its memory in an arena allocator. The arena is reseted
|
||||
// when end() is called.
|
||||
pub const Page = struct {
|
||||
arena: std.heap.ArenaAllocator,
|
||||
session: *Session,
|
||||
doc: ?*parser.Document = null,
|
||||
|
||||
// handle url
|
||||
rawuri: ?[]const u8 = null,
|
||||
uri: std.Uri = undefined,
|
||||
origin: ?[]const u8 = null,
|
||||
|
||||
raw_data: ?[]const u8 = null,
|
||||
|
||||
fn init(
|
||||
self: *Page,
|
||||
alloc: std.mem.Allocator,
|
||||
session: *Session,
|
||||
) void {
|
||||
self.* = .{
|
||||
.arena = std.heap.ArenaAllocator.init(alloc),
|
||||
.session = session,
|
||||
};
|
||||
}
|
||||
|
||||
// reset js env and mem arena.
|
||||
pub fn end(self: *Page) void {
|
||||
self.session.env.stop();
|
||||
// TODO unload document: https://html.spec.whatwg.org/#unloading-documents
|
||||
|
||||
// clear netsurf memory arena.
|
||||
parser.deinit();
|
||||
|
||||
_ = self.arena.reset(.free_all);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Page) void {
|
||||
self.arena.deinit();
|
||||
self.session.page = null;
|
||||
}
|
||||
|
||||
// dump writes the page content into the given file.
|
||||
pub fn dump(self: *Page, out: std.fs.File) !void {
|
||||
|
||||
// if no HTML document pointer available, dump the data content only.
|
||||
if (self.doc == null) {
|
||||
// no data loaded, nothing to do.
|
||||
if (self.raw_data == null) return;
|
||||
return try out.writeAll(self.raw_data.?);
|
||||
}
|
||||
|
||||
// if the page has a pointer to a document, dumps the HTML.
|
||||
try Dump.writeHTML(self.doc.?, out);
|
||||
}
|
||||
|
||||
pub fn wait(self: *Page) !void {
|
||||
|
||||
// try catch
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
try_catch.init(self.session.env);
|
||||
defer try_catch.deinit();
|
||||
|
||||
self.session.env.wait() catch |err| {
|
||||
// the js env could not be started if the document wasn't an HTML.
|
||||
if (err == error.EnvNotStarted) return;
|
||||
|
||||
const alloc = self.arena.allocator();
|
||||
if (try try_catch.err(alloc, self.session.env)) |msg| {
|
||||
defer alloc.free(msg);
|
||||
log.info("wait error: {s}", .{msg});
|
||||
return;
|
||||
}
|
||||
};
|
||||
log.debug("wait: OK", .{});
|
||||
}
|
||||
|
||||
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
|
||||
// - auxData: extra data forwarded to the Inspector
|
||||
// see Inspector.contextCreated
|
||||
pub fn navigate(self: *Page, uri: []const u8, auxData: ?[]const u8) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
|
||||
log.debug("starting GET {s}", .{uri});
|
||||
|
||||
// own the url
|
||||
if (self.rawuri) |prev| alloc.free(prev);
|
||||
self.rawuri = try alloc.dupe(u8, uri);
|
||||
self.uri = std.Uri.parse(self.rawuri.?) catch try std.Uri.parseAfterScheme("", self.rawuri.?);
|
||||
|
||||
// prepare origin value.
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
try self.uri.writeToStream(.{
|
||||
.scheme = true,
|
||||
.authority = true,
|
||||
}, buf.writer());
|
||||
self.origin = try buf.toOwnedSlice();
|
||||
|
||||
// TODO handle fragment in url.
|
||||
|
||||
// load the data
|
||||
var resp = try self.session.loader.get(alloc, self.uri);
|
||||
defer resp.deinit();
|
||||
|
||||
const req = resp.req;
|
||||
|
||||
log.info("GET {any} {d}", .{ self.uri, req.response.status });
|
||||
|
||||
// TODO handle redirection
|
||||
if (req.response.status != .ok) {
|
||||
log.debug("{?} {d} {s}", .{
|
||||
req.response.version,
|
||||
req.response.status,
|
||||
req.response.reason,
|
||||
// TODO log headers
|
||||
});
|
||||
return error.BadStatusCode;
|
||||
}
|
||||
|
||||
// TODO handle charset
|
||||
// https://html.spec.whatwg.org/#content-type
|
||||
var it = req.response.iterateHeaders();
|
||||
var ct: ?[]const u8 = null;
|
||||
while (true) {
|
||||
const h = it.next() orelse break;
|
||||
if (std.ascii.eqlIgnoreCase(h.name, "Content-Type")) {
|
||||
ct = try alloc.dupe(u8, h.value);
|
||||
}
|
||||
}
|
||||
if (ct == null) {
|
||||
// no content type in HTTP headers.
|
||||
// TODO try to sniff mime type from the body.
|
||||
log.info("no content-type HTTP header", .{});
|
||||
return;
|
||||
}
|
||||
defer alloc.free(ct.?);
|
||||
|
||||
log.debug("header content-type: {s}", .{ct.?});
|
||||
const mime = try Mime.parse(ct.?);
|
||||
if (mime.eql(Mime.HTML)) {
|
||||
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8", auxData);
|
||||
} else {
|
||||
log.info("non-HTML document: {s}", .{ct.?});
|
||||
|
||||
// save the body into the page.
|
||||
self.raw_data = try req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
|
||||
}
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, auxData: ?[]const u8) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
|
||||
// start netsurf memory arena.
|
||||
try parser.init();
|
||||
|
||||
log.debug("parse html with charset {s}", .{charset});
|
||||
|
||||
const ccharset = try alloc.dupeZ(u8, charset);
|
||||
defer alloc.free(ccharset);
|
||||
|
||||
const html_doc = try parser.documentHTMLParse(reader, ccharset);
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
|
||||
// save a document's pointer in the page.
|
||||
self.doc = doc;
|
||||
|
||||
// TODO set document.readyState to interactive
|
||||
// https://html.spec.whatwg.org/#reporting-document-loading-status
|
||||
|
||||
// inject the URL to the document including the fragment.
|
||||
try parser.documentSetDocumentURI(doc, self.rawuri orelse "about:blank");
|
||||
|
||||
// TODO set the referrer to the document.
|
||||
|
||||
self.session.window.replaceDocument(html_doc);
|
||||
self.session.window.setStorageShelf(
|
||||
try self.session.storageShed.getOrPut(self.origin orelse "null"),
|
||||
);
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
|
||||
// start JS env
|
||||
// TODO load the js env concurrently with the HTML parsing.
|
||||
log.debug("start js env", .{});
|
||||
try self.session.env.start();
|
||||
|
||||
// load polyfills
|
||||
try polyfill.load(alloc, self.session.env);
|
||||
|
||||
// inspector
|
||||
if (self.session.inspector) |inspector| {
|
||||
inspector.contextCreated(self.session.env, "", self.origin.?, auxData);
|
||||
}
|
||||
|
||||
// replace the user context document with the new one.
|
||||
try self.session.env.setUserContext(.{
|
||||
.document = html_doc,
|
||||
.httpClient = &self.session.httpClient,
|
||||
});
|
||||
|
||||
// add global objects
|
||||
log.debug("setup global env", .{});
|
||||
try self.session.env.bindGlobal(&self.session.window);
|
||||
|
||||
// browse the DOM tree to retrieve scripts
|
||||
// TODO execute the synchronous scripts during the HTL parsing.
|
||||
// TODO fetch the script resources concurrently but execute them in the
|
||||
// declaration order for synchronous ones.
|
||||
|
||||
// sasync stores scripts which can be run asynchronously.
|
||||
// for now they are just run after the non-async one in order to
|
||||
// dispatch DOMContentLoaded the sooner as possible.
|
||||
var sasync = std.ArrayList(*parser.Element).init(alloc);
|
||||
defer sasync.deinit();
|
||||
|
||||
const root = parser.documentToNode(doc);
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try walker.get_next(root, next) orelse break;
|
||||
|
||||
// ignore non-elements nodes.
|
||||
if (try parser.nodeType(next.?) != .element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const e = parser.nodeToElement(next.?);
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
|
||||
|
||||
// ignore non-script tags
|
||||
if (tag != .script) continue;
|
||||
|
||||
// ignore non-js script.
|
||||
// > type
|
||||
// > Attribute is not set (default), an empty string, or a JavaScript MIME
|
||||
// > type indicates that the script is a "classic script", containing
|
||||
// > JavaScript code.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
|
||||
const stype = try parser.elementGetAttribute(e, "type");
|
||||
if (!isJS(stype)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore the defer attribute b/c we analyze all script
|
||||
// after the document has been parsed.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer
|
||||
|
||||
// TODO use fetchpriority
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#fetchpriority
|
||||
|
||||
// > async
|
||||
// > For classic scripts, if the async attribute is present,
|
||||
// > then the classic script will be fetched in parallel to
|
||||
// > parsing and evaluated as soon as it is available.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
|
||||
if (try parser.elementGetAttribute(e, "async") != null) {
|
||||
try sasync.append(e);
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO handle for attribute
|
||||
// TODO handle event attribute
|
||||
|
||||
// TODO defer
|
||||
// > This Boolean attribute is set to indicate to a browser
|
||||
// > that the script is meant to be executed after the
|
||||
// > document has been parsed, but before firing
|
||||
// > DOMContentLoaded.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer
|
||||
// defer allow us to load a script w/o blocking the rest of
|
||||
// evaluations.
|
||||
|
||||
// > Scripts without async, defer or type="module"
|
||||
// > attributes, as well as inline scripts without the
|
||||
// > type="module" attribute, are fetched and executed
|
||||
// > immediately before the browser continues to parse the
|
||||
// > page.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
|
||||
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||
}
|
||||
|
||||
// TODO wait for deferred scripts
|
||||
|
||||
// dispatch DOMContentLoaded before the transition to "complete",
|
||||
// at the point where all subresources apart from async script elements
|
||||
// have loaded.
|
||||
// https://html.spec.whatwg.org/#reporting-document-loading-status
|
||||
const evt = try parser.eventCreate();
|
||||
defer parser.eventDestroy(evt);
|
||||
|
||||
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
|
||||
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
|
||||
|
||||
// eval async scripts.
|
||||
for (sasync.items) |e| {
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
|
||||
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, null);
|
||||
}
|
||||
|
||||
// TODO wait for async scripts
|
||||
|
||||
// TODO set document.readyState to complete
|
||||
|
||||
// dispatch window.load event
|
||||
const loadevt = try parser.eventCreate();
|
||||
defer parser.eventDestroy(loadevt);
|
||||
|
||||
try parser.eventInit(loadevt, "load", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(Window, &self.session.window),
|
||||
loadevt,
|
||||
);
|
||||
}
|
||||
|
||||
// evalScript evaluates the src in priority.
|
||||
// if no src is present, we evaluate the text source.
|
||||
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
|
||||
fn evalScript(self: *Page, e: *parser.Element) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
||||
const opt_src = try parser.elementGetAttribute(e, "src");
|
||||
if (opt_src) |src| {
|
||||
log.debug("starting GET {s}", .{src});
|
||||
|
||||
self.fetchScript(src) catch |err| {
|
||||
switch (err) {
|
||||
FetchError.BadStatusCode => return err,
|
||||
|
||||
// TODO If el's result is null, then fire an event named error at
|
||||
// el, and return.
|
||||
FetchError.NoBody => return,
|
||||
|
||||
FetchError.JsErr => {}, // nothing to do here.
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
// TODO If el's from an external file is true, then fire an event
|
||||
// named load at el.
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
try_catch.init(self.session.env);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const opt_text = try parser.nodeTextContent(parser.elementToNode(e));
|
||||
if (opt_text) |text| {
|
||||
// TODO handle charset attribute
|
||||
const res = self.session.env.exec(text, "") catch {
|
||||
if (try try_catch.err(alloc, self.session.env)) |msg| {
|
||||
defer alloc.free(msg);
|
||||
log.info("eval inline {s}: {s}", .{ text, msg });
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(alloc, self.session.env);
|
||||
defer alloc.free(msg);
|
||||
log.debug("eval inline {s}", .{msg});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// nothing has been loaded.
|
||||
// TODO If el's result is null, then fire an event named error at
|
||||
// el, and return.
|
||||
}
|
||||
|
||||
const FetchError = error{
|
||||
BadStatusCode,
|
||||
NoBody,
|
||||
JsErr,
|
||||
};
|
||||
|
||||
// fetchScript senf a GET request to the src and execute the script
|
||||
// received.
|
||||
fn fetchScript(self: *Page, src: []const u8) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
|
||||
log.debug("starting fetch script {s}", .{src});
|
||||
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var b: []u8 = buffer[0..];
|
||||
const u = try std.Uri.resolve_inplace(self.uri, src, &b);
|
||||
|
||||
var fetchres = try self.session.loader.get(alloc, u);
|
||||
defer fetchres.deinit();
|
||||
|
||||
const resp = fetchres.req.response;
|
||||
|
||||
log.info("fetch script {any}: {d}", .{ u, resp.status });
|
||||
|
||||
if (resp.status != .ok) return FetchError.BadStatusCode;
|
||||
|
||||
// TODO check content-type
|
||||
const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
|
||||
defer alloc.free(body);
|
||||
|
||||
// check no body
|
||||
if (body.len == 0) return FetchError.NoBody;
|
||||
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
try_catch.init(self.session.env);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const res = self.session.env.exec(body, src) catch {
|
||||
if (try try_catch.err(alloc, self.session.env)) |msg| {
|
||||
defer alloc.free(msg);
|
||||
log.info("eval remote {s}: {s}", .{ src, msg });
|
||||
}
|
||||
return FetchError.JsErr;
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(alloc, self.session.env);
|
||||
defer alloc.free(msg);
|
||||
log.debug("eval remote {s}: {s}", .{ src, msg });
|
||||
}
|
||||
}
|
||||
|
||||
// > type
|
||||
// > Attribute is not set (default), an empty string, or a JavaScript MIME
|
||||
// > type indicates that the script is a "classic script", containing
|
||||
// > JavaScript code.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
|
||||
fn isJS(stype: ?[]const u8) bool {
|
||||
if (stype == null or stype.?.len == 0) return true;
|
||||
if (std.mem.eql(u8, stype.?, "application/javascript")) return true;
|
||||
if (!std.mem.eql(u8, stype.?, "module")) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const testing = @import("../testing.zig");
|
||||
test "Browser" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
// this will crash if ICU isn't properly configured / ininitialized
|
||||
try runner.testCases(&.{
|
||||
.{ "new Intl.DateTimeFormat()", "[object Intl.DateTimeFormat]" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
329
src/browser/console/console.zig
Normal file
329
src/browser/console/console.zig
Normal file
@@ -0,0 +1,329 @@
|
||||
// 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 Allocator = std.mem.Allocator;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const JsObject = @import("../env.zig").Env.JsObject;
|
||||
|
||||
const log = if (builtin.is_test) &test_capture else @import("../../log.zig");
|
||||
|
||||
pub const Console = struct {
|
||||
// TODO: configurable writer
|
||||
timers: std.StringHashMapUnmanaged(u32) = .{},
|
||||
counts: std.StringHashMapUnmanaged(u32) = .{},
|
||||
|
||||
pub fn _lp(values: []JsObject, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn _log(values: []JsObject, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn _info(values: []JsObject, page: *Page) !void {
|
||||
return _log(values, page);
|
||||
}
|
||||
|
||||
pub fn _debug(values: []JsObject, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn _warn(values: []JsObject, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn _error(values: []JsObject, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(.console, "error", .{
|
||||
.args = try serializeValues(values, page),
|
||||
.stack = page.stackTrace() catch "???",
|
||||
});
|
||||
}
|
||||
|
||||
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: JsObject, values: []JsObject, 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: []JsObject, 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 {
|
||||
const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch unreachable;
|
||||
return @intCast(ts.sec);
|
||||
}
|
||||
|
||||
var test_capture = TestCapture{};
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Console" {
|
||||
defer testing.reset();
|
||||
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
{
|
||||
try runner.testCases(&.{
|
||||
.{ "console.log('a')", "undefined" },
|
||||
.{ "console.warn('hello world', 23, true, new Object())", "undefined" },
|
||||
}, .{});
|
||||
|
||||
const captured = test_capture.captured.items;
|
||||
try testing.expectEqual("[info] args= 1: a", captured[0]);
|
||||
try testing.expectEqual("[warn] args= 1: hello world 2: 23 3: true 4: #<Object>", captured[1]);
|
||||
}
|
||||
|
||||
{
|
||||
test_capture.reset();
|
||||
try runner.testCases(&.{
|
||||
.{ "console.countReset()", "undefined" },
|
||||
.{ "console.count()", "undefined" },
|
||||
.{ "console.count('teg')", "undefined" },
|
||||
.{ "console.count('teg')", "undefined" },
|
||||
.{ "console.count('teg')", "undefined" },
|
||||
.{ "console.count()", "undefined" },
|
||||
.{ "console.countReset('teg')", "undefined" },
|
||||
.{ "console.countReset()", "undefined" },
|
||||
.{ "console.count()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
const captured = test_capture.captured.items;
|
||||
try testing.expectEqual("[invalid counter] label=default", captured[0]);
|
||||
try testing.expectEqual("[count] label=default count=1", captured[1]);
|
||||
try testing.expectEqual("[count] label=teg count=1", captured[2]);
|
||||
try testing.expectEqual("[count] label=teg count=2", captured[3]);
|
||||
try testing.expectEqual("[count] label=teg count=3", captured[4]);
|
||||
try testing.expectEqual("[count] label=default count=2", captured[5]);
|
||||
try testing.expectEqual("[count reset] label=teg count=3", captured[6]);
|
||||
try testing.expectEqual("[count reset] label=default count=2", captured[7]);
|
||||
try testing.expectEqual("[count] label=default count=1", captured[8]);
|
||||
}
|
||||
|
||||
{
|
||||
test_capture.reset();
|
||||
try runner.testCases(&.{
|
||||
.{ "console.assert(true)", "undefined" },
|
||||
.{ "console.assert('a', 2, 3, 4)", "undefined" },
|
||||
.{ "console.assert('')", "undefined" },
|
||||
.{ "console.assert('', 'x', true)", "undefined" },
|
||||
.{ "console.assert(false, 'x')", "undefined" },
|
||||
}, .{});
|
||||
|
||||
const captured = test_capture.captured.items;
|
||||
try testing.expectEqual("[assertion failed] values=", captured[0]);
|
||||
try testing.expectEqual("[assertion failed] values= 1: x 2: true", captured[1]);
|
||||
try testing.expectEqual("[assertion failed] values= 1: x", captured[2]);
|
||||
}
|
||||
|
||||
{
|
||||
test_capture.reset();
|
||||
try runner.testCases(&.{
|
||||
.{ "[1].forEach(console.log)", null },
|
||||
}, .{});
|
||||
|
||||
const captured = test_capture.captured.items;
|
||||
try testing.expectEqual("[info] args= 1: 1 2: 0 3: [1]", captured[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const TestCapture = struct {
|
||||
captured: std.ArrayListUnmanaged([]const u8) = .{},
|
||||
|
||||
fn separator(_: *const TestCapture) []const u8 {
|
||||
return " ";
|
||||
}
|
||||
|
||||
fn reset(self: *TestCapture) void {
|
||||
self.captured = .{};
|
||||
}
|
||||
|
||||
fn debug(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self.capture(scope, msg, args);
|
||||
}
|
||||
|
||||
fn info(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self.capture(scope, msg, args);
|
||||
}
|
||||
|
||||
fn warn(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self.capture(scope, msg, args);
|
||||
}
|
||||
|
||||
fn err(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self.capture(scope, msg, args);
|
||||
}
|
||||
|
||||
fn fatal(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self.capture(scope, msg, args);
|
||||
}
|
||||
|
||||
fn capture(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self._capture(scope, msg, args) catch unreachable;
|
||||
}
|
||||
|
||||
fn _capture(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) !void {
|
||||
std.debug.assert(scope == .console);
|
||||
|
||||
const allocator = testing.arena_allocator;
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
try buf.appendSlice(allocator, "[" ++ msg ++ "] ");
|
||||
|
||||
inline for (@typeInfo(@TypeOf(args)).@"struct".fields) |f| {
|
||||
try buf.appendSlice(allocator, f.name);
|
||||
try buf.append(allocator, '=');
|
||||
try @import("../../log.zig").writeValue(.pretty, @field(args, f.name), buf.writer(allocator));
|
||||
try buf.append(allocator, ' ');
|
||||
}
|
||||
self.captured.append(testing.arena_allocator, std.mem.trimRight(u8, buf.items, " ")) catch unreachable;
|
||||
}
|
||||
};
|
||||
97
src/browser/crypto/crypto.zig
Normal file
97
src/browser/crypto/crypto.zig
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
const uuidv4 = @import("../../id.zig").uuidv4;
|
||||
|
||||
// https://w3c.github.io/webcrypto/#crypto-interface
|
||||
pub const Crypto = struct {
|
||||
_not_empty: bool = true,
|
||||
|
||||
pub fn _getRandomValues(_: *const Crypto, js_obj: Env.JsObject) !Env.JsObject {
|
||||
var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues);
|
||||
const buf = into.asBuffer();
|
||||
if (buf.len > 65_536) {
|
||||
return error.QuotaExceededError;
|
||||
}
|
||||
std.crypto.random.bytes(buf);
|
||||
return js_obj;
|
||||
}
|
||||
|
||||
pub fn _randomUUID(_: *const Crypto) [36]u8 {
|
||||
var hex: [36]u8 = undefined;
|
||||
uuidv4(&hex);
|
||||
return hex;
|
||||
}
|
||||
};
|
||||
|
||||
const RandomValues = union(enum) {
|
||||
int8: []i8,
|
||||
uint8: []u8,
|
||||
int16: []i16,
|
||||
uint16: []u16,
|
||||
int32: []i32,
|
||||
uint32: []u32,
|
||||
int64: []i64,
|
||||
uint64: []u64,
|
||||
|
||||
fn asBuffer(self: RandomValues) []u8 {
|
||||
return switch (self) {
|
||||
.int8 => |b| (@as([]u8, @ptrCast(b)))[0..b.len],
|
||||
.uint8 => |b| (@as([]u8, @ptrCast(b)))[0..b.len],
|
||||
.int16 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
||||
.uint16 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
||||
.int32 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
||||
.uint32 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
||||
.int64 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
||||
.uint64 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Crypto" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const a = crypto.randomUUID();", "undefined" },
|
||||
.{ "const b = crypto.randomUUID();", "undefined" },
|
||||
.{ "a.length;", "36" },
|
||||
.{ "b.length;", "36" },
|
||||
.{ "a == b;", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "try { crypto.getRandomValues(new BigUint64Array(8193)) } catch(e) { e.message == 'QuotaExceededError' }", "true" },
|
||||
.{ "let r1 = new Int32Array(5)", "undefined" },
|
||||
.{ "let r2 = crypto.getRandomValues(r1)", "undefined" },
|
||||
.{ "new Set(r1).size", "5" },
|
||||
.{ "new Set(r2).size", "5" },
|
||||
.{ "r1.every((v, i) => v === r2[i])", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var r3 = new Uint8Array(16)", null },
|
||||
.{ "let r4 = crypto.getRandomValues(r3)", "undefined" },
|
||||
.{ "r4[6] = 10", null },
|
||||
.{ "r4[6]", "10" },
|
||||
.{ "r3[6]", "10" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -23,6 +23,20 @@ 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 };
|
||||
@@ -174,3 +188,14 @@ test "parse" {
|
||||
defer s.deinit(alloc);
|
||||
}
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.CSS" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "CSS.supports('display: flex')", "true" },
|
||||
.{ "CSS.supports('text-decoration-style', 'blink')", "true" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
// Node implementation with Netsurf Libdom C lib.
|
||||
pub const Node = struct {
|
||||
@@ -19,7 +19,7 @@
|
||||
const std = @import("std");
|
||||
const css = @import("css.zig");
|
||||
const Node = @import("libdom.zig").Node;
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Matcher = struct {
|
||||
const Nodes = std.ArrayList(Node);
|
||||
@@ -29,6 +29,8 @@ const PseudoClass = selector.PseudoClass;
|
||||
const AttributeOP = selector.AttributeOP;
|
||||
const Combinator = selector.Combinator;
|
||||
|
||||
const REPLACEMENT_CHARACTER = &.{ 239, 191, 189 };
|
||||
|
||||
pub const ParseError = error{
|
||||
ExpectedSelector,
|
||||
ExpectedIdentifier,
|
||||
@@ -202,7 +204,7 @@ pub const Parser = struct {
|
||||
}
|
||||
|
||||
const c = p.s[p.i];
|
||||
if (!nameStart(c) or c == '\\') {
|
||||
if (!(nameStart(c) or c == '\\')) {
|
||||
return ParseError.ExpectedSelector;
|
||||
}
|
||||
|
||||
@@ -217,22 +219,31 @@ pub const Parser = struct {
|
||||
// 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 < p.s.len) {
|
||||
const c = p.s[i];
|
||||
while (i < sel_len) {
|
||||
const c = sel[i];
|
||||
|
||||
if (nameChar(c)) {
|
||||
const start = i;
|
||||
while (i < p.s.len and nameChar(p.s[i])) i += 1;
|
||||
w.writeAll(p.s[start..i]) catch return ParseError.WriteError;
|
||||
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;
|
||||
@@ -246,33 +257,52 @@ pub const Parser = struct {
|
||||
// parseEscape parses a backslash escape.
|
||||
// The returned string is owned by the caller.
|
||||
fn parseEscape(p: *Parser, w: anytype) ParseError!void {
|
||||
if (p.s.len < p.i + 2 or p.s[p.i] != '\\') {
|
||||
return ParseError.InvalidEscape;
|
||||
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 = p.s[start];
|
||||
if (ascii.isWhitespace(c)) return ParseError.EscapeLineEndingOutsideString;
|
||||
const c = sel[start];
|
||||
|
||||
// unicode escape (hex)
|
||||
if (ascii.isHex(c)) {
|
||||
var i: usize = start;
|
||||
while (i < start + 6 and i < p.s.len and ascii.isHex(p.s[i])) {
|
||||
while (i < start + 6 and i < sel_len and ascii.isHex(sel[i])) {
|
||||
i += 1;
|
||||
}
|
||||
const v = std.fmt.parseUnsigned(u21, p.s[start..i], 16) catch return ParseError.InvalidUnicode;
|
||||
if (p.s.len > i) {
|
||||
switch (p.s[i]) {
|
||||
'\r' => {
|
||||
i += 1;
|
||||
if (p.s.len > i and p.s[i] == '\n') i += 1;
|
||||
},
|
||||
' ', '\t', '\n', std.ascii.control_code.ff => i += 1,
|
||||
else => {},
|
||||
|
||||
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 return ParseError.InvalidUnicode;
|
||||
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;
|
||||
}
|
||||
@@ -280,7 +310,7 @@ pub const Parser = struct {
|
||||
|
||||
// Return the literal character after the backslash.
|
||||
p.i += 2;
|
||||
w.writeAll(p.s[start .. start + 1]) catch return ParseError.WriteError;
|
||||
w.writeByte(sel[start]) catch return ParseError.WriteError;
|
||||
}
|
||||
|
||||
// parseIDSelector parses a selector that matches by id attribute.
|
||||
@@ -383,20 +413,23 @@ pub const Parser = struct {
|
||||
|
||||
// parseString parses a single- or double-quoted string.
|
||||
fn parseString(p: *Parser, writer: anytype) ParseError!void {
|
||||
var i = p.i;
|
||||
if (p.s.len < i + 2) return ParseError.ExpectedString;
|
||||
const sel = p.s;
|
||||
const sel_len = sel.len;
|
||||
|
||||
const quote = p.s[i];
|
||||
var i = p.i;
|
||||
if (sel_len < i + 2) return ParseError.ExpectedString;
|
||||
|
||||
const quote = sel[i];
|
||||
i += 1;
|
||||
|
||||
loop: while (i < p.s.len) {
|
||||
switch (p.s[i]) {
|
||||
loop: while (i < sel_len) {
|
||||
switch (sel[i]) {
|
||||
'\\' => {
|
||||
if (p.s.len > i + 1) {
|
||||
const c = p.s[i + 1];
|
||||
if (sel_len > i + 1) {
|
||||
const c = sel[i + 1];
|
||||
switch (c) {
|
||||
'\r' => {
|
||||
if (p.s.len > i + 2 and p.s[i + 2] == '\n') {
|
||||
if (sel_len > i + 2 and sel[i + 2] == '\n') {
|
||||
i += 3;
|
||||
continue :loop;
|
||||
}
|
||||
@@ -418,17 +451,17 @@ pub const Parser = struct {
|
||||
else => |c| {
|
||||
if (c == quote) break :loop;
|
||||
const start = i;
|
||||
while (i < p.s.len) {
|
||||
const cc = p.s[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(p.s[start..i]) catch return ParseError.WriteError;
|
||||
writer.writeAll(sel[start..i]) catch return ParseError.WriteError;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (i >= p.s.len) return ParseError.InvalidString;
|
||||
if (i >= sel_len) return ParseError.InvalidString;
|
||||
|
||||
// Consume the final quote.
|
||||
i += 1;
|
||||
@@ -549,6 +582,7 @@ pub const Parser = struct {
|
||||
.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;
|
||||
@@ -572,6 +606,7 @@ pub const Parser = struct {
|
||||
.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 },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,3 +950,36 @@ test "parser.parseString" {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "parser.parse" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = 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" } },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
const sel = p.parse(alloc) 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,9 @@ pub const PseudoClass = enum {
|
||||
placeholder,
|
||||
selection,
|
||||
spelling_error,
|
||||
modal,
|
||||
popover_open,
|
||||
visible,
|
||||
|
||||
pub const Error = error{
|
||||
InvalidPseudoClass,
|
||||
@@ -113,51 +116,108 @@ pub const PseudoClass = enum {
|
||||
}
|
||||
|
||||
pub fn parse(s: []const u8) Error!PseudoClass {
|
||||
if (std.ascii.eqlIgnoreCase(s, "not")) return .not;
|
||||
if (std.ascii.eqlIgnoreCase(s, "has")) return .has;
|
||||
if (std.ascii.eqlIgnoreCase(s, "haschild")) return .haschild;
|
||||
if (std.ascii.eqlIgnoreCase(s, "contains")) return .contains;
|
||||
if (std.ascii.eqlIgnoreCase(s, "containsown")) return .containsown;
|
||||
if (std.ascii.eqlIgnoreCase(s, "matches")) return .matches;
|
||||
if (std.ascii.eqlIgnoreCase(s, "matchesown")) return .matchesown;
|
||||
if (std.ascii.eqlIgnoreCase(s, "nth-child")) return .nth_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "nth-last-child")) return .nth_last_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "nth-of-type")) return .nth_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "nth-last-of-type")) return .nth_last_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "first-child")) return .first_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "last-child")) return .last_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "first-of-type")) return .first_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "last-of-type")) return .last_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "only-child")) return .only_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "only-of-type")) return .only_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "input")) return .input;
|
||||
if (std.ascii.eqlIgnoreCase(s, "empty")) return .empty;
|
||||
if (std.ascii.eqlIgnoreCase(s, "root")) return .root;
|
||||
if (std.ascii.eqlIgnoreCase(s, "link")) return .link;
|
||||
if (std.ascii.eqlIgnoreCase(s, "lang")) return .lang;
|
||||
if (std.ascii.eqlIgnoreCase(s, "enabled")) return .enabled;
|
||||
if (std.ascii.eqlIgnoreCase(s, "disabled")) return .disabled;
|
||||
if (std.ascii.eqlIgnoreCase(s, "checked")) return .checked;
|
||||
if (std.ascii.eqlIgnoreCase(s, "visited")) return .visited;
|
||||
if (std.ascii.eqlIgnoreCase(s, "hover")) return .hover;
|
||||
if (std.ascii.eqlIgnoreCase(s, "active")) return .active;
|
||||
if (std.ascii.eqlIgnoreCase(s, "focus")) return .focus;
|
||||
if (std.ascii.eqlIgnoreCase(s, "target")) return .target;
|
||||
if (std.ascii.eqlIgnoreCase(s, "after")) return .after;
|
||||
if (std.ascii.eqlIgnoreCase(s, "backdrop")) return .backdrop;
|
||||
if (std.ascii.eqlIgnoreCase(s, "before")) return .before;
|
||||
if (std.ascii.eqlIgnoreCase(s, "cue")) return .cue;
|
||||
if (std.ascii.eqlIgnoreCase(s, "first-letter")) return .first_letter;
|
||||
if (std.ascii.eqlIgnoreCase(s, "first-line")) return .first_line;
|
||||
if (std.ascii.eqlIgnoreCase(s, "grammar-error")) return .grammar_error;
|
||||
if (std.ascii.eqlIgnoreCase(s, "marker")) return .marker;
|
||||
if (std.ascii.eqlIgnoreCase(s, "placeholder")) return .placeholder;
|
||||
if (std.ascii.eqlIgnoreCase(s, "selection")) return .selection;
|
||||
if (std.ascii.eqlIgnoreCase(s, "spelling-error")) return .spelling_error;
|
||||
const longest_selector = "nth-last-of-type";
|
||||
if (s.len > longest_selector.len) {
|
||||
return Error.InvalidPseudoClass;
|
||||
}
|
||||
|
||||
var buf: [longest_selector.len]u8 = undefined;
|
||||
const selector = std.ascii.lowerString(&buf, s);
|
||||
|
||||
switch (selector.len) {
|
||||
3 => switch (@as(u24, @bitCast(selector[0..3].*))) {
|
||||
asUint(u24, "cue") => return .cue,
|
||||
asUint(u24, "has") => return .has,
|
||||
asUint(u24, "not") => return .not,
|
||||
else => {},
|
||||
},
|
||||
4 => switch (@as(u32, @bitCast(selector[0..4].*))) {
|
||||
asUint(u32, "lang") => return .lang,
|
||||
asUint(u32, "link") => return .link,
|
||||
asUint(u32, "root") => return .root,
|
||||
else => {},
|
||||
},
|
||||
5 => switch (@as(u40, @bitCast(selector[0..5].*))) {
|
||||
asUint(u40, "after") => return .after,
|
||||
asUint(u40, "empty") => return .empty,
|
||||
asUint(u40, "focus") => return .focus,
|
||||
asUint(u40, "hover") => return .hover,
|
||||
asUint(u40, "input") => return .input,
|
||||
asUint(u40, "modal") => return .modal,
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(selector[0..6].*))) {
|
||||
asUint(u48, "active") => return .active,
|
||||
asUint(u48, "before") => return .before,
|
||||
asUint(u48, "marker") => return .marker,
|
||||
asUint(u48, "target") => return .target,
|
||||
else => {},
|
||||
},
|
||||
7 => switch (@as(u56, @bitCast(selector[0..7].*))) {
|
||||
asUint(u56, "checked") => return .checked,
|
||||
asUint(u56, "enabled") => return .enabled,
|
||||
asUint(u56, "matches") => return .matches,
|
||||
asUint(u56, "visited") => return .visited,
|
||||
asUint(u56, "visible") => return .visible,
|
||||
else => {},
|
||||
},
|
||||
8 => switch (@as(u64, @bitCast(selector[0..8].*))) {
|
||||
asUint(u64, "backdrop") => return .backdrop,
|
||||
asUint(u64, "contains") => return .contains,
|
||||
asUint(u64, "disabled") => return .disabled,
|
||||
asUint(u64, "haschild") => return .haschild,
|
||||
else => {},
|
||||
},
|
||||
9 => switch (@as(u72, @bitCast(selector[0..9].*))) {
|
||||
asUint(u72, "nth-child") => return .nth_child,
|
||||
asUint(u72, "selection") => return .selection,
|
||||
else => {},
|
||||
},
|
||||
10 => switch (@as(u80, @bitCast(selector[0..10].*))) {
|
||||
asUint(u80, "first-line") => return .first_line,
|
||||
asUint(u80, "last-child") => return .last_child,
|
||||
asUint(u80, "matchesown") => return .matchesown,
|
||||
asUint(u80, "only-child") => return .only_child,
|
||||
else => {},
|
||||
},
|
||||
11 => switch (@as(u88, @bitCast(selector[0..11].*))) {
|
||||
asUint(u88, "containsown") => return .containsown,
|
||||
asUint(u88, "first-child") => return .first_child,
|
||||
asUint(u88, "nth-of-type") => return .nth_of_type,
|
||||
asUint(u88, "placeholder") => return .placeholder,
|
||||
else => {},
|
||||
},
|
||||
12 => switch (@as(u96, @bitCast(selector[0..12].*))) {
|
||||
asUint(u96, "first-letter") => return .first_letter,
|
||||
asUint(u96, "last-of-type") => return .last_of_type,
|
||||
asUint(u96, "only-of-type") => return .only_of_type,
|
||||
asUint(u96, "popover-open") => return .popover_open,
|
||||
else => {},
|
||||
},
|
||||
13 => switch (@as(u104, @bitCast(selector[0..13].*))) {
|
||||
asUint(u104, "first-of-type") => return .first_of_type,
|
||||
asUint(u104, "grammar-error") => return .grammar_error,
|
||||
else => {},
|
||||
},
|
||||
14 => switch (@as(u112, @bitCast(selector[0..14].*))) {
|
||||
asUint(u112, "nth-last-child") => return .nth_last_child,
|
||||
asUint(u112, "spelling-error") => return .spelling_error,
|
||||
else => {},
|
||||
},
|
||||
16 => switch (@as(u128, @bitCast(selector[0..16].*))) {
|
||||
asUint(u128, "nth-last-of-type") => return .nth_last_of_type,
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return Error.InvalidPseudoClass;
|
||||
}
|
||||
};
|
||||
|
||||
fn asUint(comptime T: type, comptime string: []const u8) T {
|
||||
return @bitCast(string[0..string.len].*);
|
||||
}
|
||||
|
||||
pub const Selector = union(enum) {
|
||||
pub const Error = error{
|
||||
UnknownCombinedCombinator,
|
||||
@@ -509,6 +569,8 @@ pub const Selector = union(enum) {
|
||||
// TODO implement using the url fragment.
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/CSS/:target
|
||||
.target => return false,
|
||||
// visible always returns true.
|
||||
.visible => return true,
|
||||
|
||||
// all others pseudo class are handled by specialized
|
||||
// pseudo_class_X selectors.
|
||||
291
src/browser/cssom/css_parser.zig
Normal file
291
src/browser/cssom/css_parser.zig
Normal file
@@ -0,0 +1,291 @@
|
||||
// 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(";
|
||||
};
|
||||
|
||||
pub const CSSParserState = enum {
|
||||
seek_name,
|
||||
in_name,
|
||||
seek_colon,
|
||||
seek_value,
|
||||
in_value,
|
||||
in_quoted_value,
|
||||
in_single_quoted_value,
|
||||
in_url,
|
||||
in_important,
|
||||
};
|
||||
|
||||
pub const CSSDeclaration = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
is_important: bool,
|
||||
};
|
||||
|
||||
pub const CSSParser = struct {
|
||||
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 "CSSParser - 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 "CSSParser - 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 "CSSParser - 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 "CSSParser - 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 "CSSParser - 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 "CSSParser - 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);
|
||||
}
|
||||
42
src/browser/cssom/css_rule.zig
Normal file
42
src/browser/cssom/css_rule.zig
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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 CSSStyleSheet = @import("css_stylesheet.zig").CSSStyleSheet;
|
||||
|
||||
pub const Interfaces = .{
|
||||
CSSRule,
|
||||
CSSImportRule,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
|
||||
pub const CSSRule = struct {
|
||||
css_text: []const u8,
|
||||
parent_rule: ?*CSSRule = null,
|
||||
parent_stylesheet: ?*CSSStyleSheet = null,
|
||||
};
|
||||
|
||||
pub const CSSImportRule = struct {
|
||||
pub const prototype = *CSSRule;
|
||||
href: []const u8,
|
||||
layer_name: ?[]const u8,
|
||||
media: void,
|
||||
style_sheet: CSSStyleSheet,
|
||||
supports_text: ?[]const u8,
|
||||
};
|
||||
60
src/browser/cssom/css_rule_list.zig
Normal file
60
src/browser/cssom/css_rule_list.zig
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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 StyleSheet = @import("stylesheet.zig").StyleSheet;
|
||||
const CSSRule = @import("css_rule.zig").CSSRule;
|
||||
const CSSImportRule = @import("css_rule.zig").CSSImportRule;
|
||||
|
||||
pub const CSSRuleList = struct {
|
||||
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" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let list = new CSSRuleList()", "undefined" },
|
||||
.{ "list instanceof CSSRuleList", "true" },
|
||||
.{ "list.length", "0" },
|
||||
.{ "list.item(0)", "null" },
|
||||
}, .{});
|
||||
}
|
||||
241
src/browser/cssom/css_style_declaration.zig
Normal file
241
src/browser/cssom/css_style_declaration.zig
Normal file
@@ -0,0 +1,241 @@
|
||||
// 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 CSSParser = @import("./css_parser.zig").CSSParser;
|
||||
const CSSValueAnalyzer = @import("./css_value_analyzer.zig").CSSValueAnalyzer;
|
||||
const CSSRule = @import("css_rule.zig").CSSRule;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
pub const CSSStyleDeclaration = struct {
|
||||
store: std.StringHashMapUnmanaged(Property),
|
||||
order: std.ArrayListUnmanaged([]const u8),
|
||||
|
||||
pub const empty: CSSStyleDeclaration = .{
|
||||
.store = .empty,
|
||||
.order = .empty,
|
||||
};
|
||||
|
||||
const Property = struct {
|
||||
value: []const u8,
|
||||
priority: bool,
|
||||
};
|
||||
|
||||
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);
|
||||
for (self.order.items) |name| {
|
||||
const prop = self.store.get(name).?;
|
||||
const escaped = try CSSValueAnalyzer.escapeCSSValue(page.call_arena, prop.value);
|
||||
try writer.print("{s}: {s}", .{ name, escaped });
|
||||
if (prop.priority) try writer.writeAll(" !important");
|
||||
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.store.clearRetainingCapacity();
|
||||
self.order.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 (!CSSValueAnalyzer.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.order.items.len;
|
||||
}
|
||||
|
||||
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _getPropertyPriority(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
||||
return if (self.store.get(name)) |prop| (if (prop.priority) "important" else "") else "";
|
||||
}
|
||||
|
||||
// TODO should handle properly shorthand properties and canonical forms
|
||||
pub fn _getPropertyValue(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
||||
if (self.store.get(name)) |prop| {
|
||||
return prop.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 {
|
||||
return if (index < self.order.items.len) self.order.items[index] else "";
|
||||
}
|
||||
|
||||
pub fn _removeProperty(self: *CSSStyleDeclaration, name: []const u8) ![]const u8 {
|
||||
const prop = self.store.fetchRemove(name) orelse return "";
|
||||
for (self.order.items, 0..) |item, i| {
|
||||
if (std.mem.eql(u8, item, name)) {
|
||||
_ = self.order.orderedRemove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// safe to return, since it's in our page.arena
|
||||
return prop.value.value;
|
||||
}
|
||||
|
||||
pub fn _setProperty(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, priority: ?[]const u8, page: *Page) !void {
|
||||
const owned_value = try page.arena.dupe(u8, value);
|
||||
const is_important = priority != null and std.ascii.eqlIgnoreCase(priority.?, "important");
|
||||
|
||||
const gop = try self.store.getOrPut(page.arena, name);
|
||||
if (!gop.found_existing) {
|
||||
const owned_name = try page.arena.dupe(u8, name);
|
||||
gop.key_ptr.* = owned_name;
|
||||
try self.order.append(page.arena, owned_name);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
test "CSSOM.CSSStyleDeclaration" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let style = document.getElementById('content').style", "undefined" },
|
||||
.{ "style.cssText = 'color: red; font-size: 12px; margin: 5px !important;'", "color: red; font-size: 12px; margin: 5px !important;" },
|
||||
.{ "style.length", "3" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.getPropertyValue('color')", "red" },
|
||||
.{ "style.getPropertyValue('font-size')", "12px" },
|
||||
.{ "style.getPropertyValue('unknown-property')", "" },
|
||||
|
||||
.{ "style.getPropertyPriority('margin')", "important" },
|
||||
.{ "style.getPropertyPriority('color')", "" },
|
||||
.{ "style.getPropertyPriority('unknown-property')", "" },
|
||||
|
||||
.{ "style.item(0)", "color" },
|
||||
.{ "style.item(1)", "font-size" },
|
||||
.{ "style.item(2)", "margin" },
|
||||
.{ "style.item(3)", "" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.setProperty('background-color', 'blue')", "undefined" },
|
||||
.{ "style.getPropertyValue('background-color')", "blue" },
|
||||
.{ "style.length", "4" },
|
||||
|
||||
.{ "style.setProperty('color', 'green')", "undefined" },
|
||||
.{ "style.getPropertyValue('color')", "green" },
|
||||
.{ "style.length", "4" },
|
||||
.{ "style.color", "green" },
|
||||
|
||||
.{ "style.setProperty('padding', '10px', 'important')", "undefined" },
|
||||
.{ "style.getPropertyValue('padding')", "10px" },
|
||||
.{ "style.getPropertyPriority('padding')", "important" },
|
||||
|
||||
.{ "style.setProperty('border', '1px solid black', 'IMPORTANT')", "undefined" },
|
||||
.{ "style.getPropertyPriority('border')", "important" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.removeProperty('color')", "green" },
|
||||
.{ "style.getPropertyValue('color')", "" },
|
||||
.{ "style.length", "5" },
|
||||
|
||||
.{ "style.removeProperty('unknown-property')", "" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.cssText.includes('font-size: 12px;')", "true" },
|
||||
.{ "style.cssText.includes('margin: 5px !important;')", "true" },
|
||||
.{ "style.cssText.includes('padding: 10px !important;')", "true" },
|
||||
.{ "style.cssText.includes('border: 1px solid black !important;')", "true" },
|
||||
|
||||
.{ "style.cssText = 'color: purple; text-align: center;'", "color: purple; text-align: center;" },
|
||||
.{ "style.length", "2" },
|
||||
.{ "style.getPropertyValue('color')", "purple" },
|
||||
.{ "style.getPropertyValue('text-align')", "center" },
|
||||
.{ "style.getPropertyValue('font-size')", "" },
|
||||
|
||||
.{ "style.setProperty('cont', 'Hello; world!')", "undefined" },
|
||||
.{ "style.getPropertyValue('cont')", "Hello; world!" },
|
||||
|
||||
.{ "style.cssText = 'content: \"Hello; world!\"; background-image: url(\"test.png\");'", "content: \"Hello; world!\"; background-image: url(\"test.png\");" },
|
||||
.{ "style.getPropertyValue('content')", "\"Hello; world!\"" },
|
||||
.{ "style.getPropertyValue('background-image')", "url(\"test.png\")" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.cssFloat", "" },
|
||||
.{ "style.cssFloat = 'left'", "left" },
|
||||
.{ "style.cssFloat", "left" },
|
||||
.{ "style.getPropertyValue('float')", "left" },
|
||||
|
||||
.{ "style.cssFloat = 'right'", "right" },
|
||||
.{ "style.cssFloat", "right" },
|
||||
|
||||
.{ "style.cssFloat = null", "null" },
|
||||
.{ "style.cssFloat", "" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.setProperty('display', '')", "undefined" },
|
||||
.{ "style.getPropertyValue('display')", "" },
|
||||
|
||||
.{ "style.cssText = ' color : purple ; margin : 10px ; '", " color : purple ; margin : 10px ; " },
|
||||
.{ "style.getPropertyValue('color')", "purple" },
|
||||
.{ "style.getPropertyValue('margin')", "10px" },
|
||||
|
||||
.{ "style.setProperty('border-bottom-left-radius', '5px')", "undefined" },
|
||||
.{ "style.getPropertyValue('border-bottom-left-radius')", "5px" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.visibility", "visible" },
|
||||
.{ "style.getPropertyValue('visibility')", "visible" },
|
||||
}, .{});
|
||||
}
|
||||
91
src/browser/cssom/css_stylesheet.zig
Normal file
91
src/browser/cssom/css_stylesheet.zig
Normal file
@@ -0,0 +1,91 @@
|
||||
// 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 StyleSheet = @import("stylesheet.zig").StyleSheet;
|
||||
|
||||
const CSSRuleList = @import("css_rule_list.zig").CSSRuleList;
|
||||
const CSSImportRule = @import("css_rule.zig").CSSImportRule;
|
||||
|
||||
pub const CSSStyleSheet = struct {
|
||||
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 = StyleSheet{ .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);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.CSS.StyleSheet" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let css = new CSSStyleSheet()", "undefined" },
|
||||
.{ "css instanceof CSSStyleSheet", "true" },
|
||||
.{ "css.cssRules.length", "0" },
|
||||
.{ "css.ownerRule", "null" },
|
||||
.{ "let index1 = css.insertRule('body { color: red; }', 0)", "undefined" },
|
||||
.{ "index1", "0" },
|
||||
.{ "css.cssRules.length", "1" },
|
||||
}, .{});
|
||||
}
|
||||
811
src/browser/cssom/css_value_analyzer.zig
Normal file
811
src/browser/cssom/css_value_analyzer.zig
Normal file
@@ -0,0 +1,811 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
pub const CSSValueAnalyzer = struct {
|
||||
pub fn isNumericWithUnit(value: []const u8) bool {
|
||||
if (value.len == 0) return false;
|
||||
|
||||
if (!std.ascii.isDigit(value[0]) and
|
||||
value[0] != '+' and value[0] != '-' and value[0] != '.')
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
pub fn isHexColor(value: []const u8) bool {
|
||||
if (!std.mem.startsWith(u8, value, "#")) 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;
|
||||
}
|
||||
|
||||
pub 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;
|
||||
|
||||
const is_numeric = isNumericWithUnit(part);
|
||||
const is_hex_color = isHexColor(part);
|
||||
const is_known_keyword = CSSKeywords.isKnownKeyword(part);
|
||||
const is_function = CSSKeywords.startsWithFunction(part);
|
||||
|
||||
if (!is_numeric and !is_hex_color and !is_known_keyword and !is_function) {
|
||||
all_parts_valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return multi_value_parts >= 2 and all_parts_valid;
|
||||
}
|
||||
|
||||
pub 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] == '\''));
|
||||
}
|
||||
|
||||
pub 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;
|
||||
}
|
||||
|
||||
pub 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 };
|
||||
}
|
||||
|
||||
pub 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;
|
||||
}
|
||||
|
||||
pub 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;
|
||||
}
|
||||
|
||||
pub fn isKnownKeyword(value: []const u8) bool {
|
||||
return CSSKeywords.isKnownKeyword(value);
|
||||
}
|
||||
|
||||
pub 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 length_units = [_][]const u8{
|
||||
"px", "em", "rem", "vw", "vh", "vmin", "vmax", "%", "pt", "pc", "in", "cm", "mm",
|
||||
"ex", "ch", "fr",
|
||||
};
|
||||
|
||||
const angle_units = [_][]const u8{
|
||||
"deg", "rad", "grad", "turn",
|
||||
};
|
||||
|
||||
const time_units = [_][]const u8{
|
||||
"s", "ms",
|
||||
};
|
||||
|
||||
const frequency_units = [_][]const u8{
|
||||
"Hz", "kHz",
|
||||
};
|
||||
|
||||
const resolution_units = [_][]const u8{
|
||||
"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(",
|
||||
};
|
||||
|
||||
pub fn isKnownKeyword(value: []const u8) bool {
|
||||
const all_categories = [_][]const []const u8{
|
||||
&border_styles, &color_names, &position_keywords, &background_repeat,
|
||||
&font_styles, &font_sizes, &font_families, &css_global,
|
||||
&display_values,
|
||||
};
|
||||
|
||||
for (all_categories) |category| {
|
||||
for (category) |keyword| {
|
||||
if (std.ascii.eqlIgnoreCase(value, keyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn containsSpecialChar(value: []const u8) bool {
|
||||
for (value) |c| {
|
||||
for (special_chars) |special| {
|
||||
if (c == special) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isValidUnit(unit: []const u8) bool {
|
||||
const all_units = [_][]const []const u8{
|
||||
&length_units, &angle_units, &time_units, &frequency_units, &resolution_units,
|
||||
};
|
||||
|
||||
for (all_units) |category| {
|
||||
for (category) |valid_unit| {
|
||||
if (std.ascii.eqlIgnoreCase(unit, valid_unit)) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
test "CSSValueAnalyzer: isNumericWithUnit - valid numbers with units" {
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("10px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("3.14em"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-5rem"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("+12.5%"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0vh"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit(".5vw"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isNumericWithUnit - scientific notation" {
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e5px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("2.5E-3em"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e+2rem"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-3.14e10px"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isNumericWithUnit - edge cases and invalid inputs" {
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(""));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("px"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("--px"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(".px"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1epx"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e+"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e+px"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1.2.3px"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10xyz"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("5invalid"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("10"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("3.14"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-5"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isHexColor - valid hex colors" {
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#000"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#fff"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#123456"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#abcdef"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#ABCDEF"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#12345678"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isHexColor - invalid hex colors" {
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("000"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#00"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#0000"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#00000"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#0000000"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#000000000"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#gggggg"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#123xyz"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isMultiValueProperty - valid multi-value properties" {
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px"));
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("solid red"));
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("#fff black"));
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("1em 2em 3em 4em"));
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("rgb(255,0,0) solid"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isMultiValueProperty - invalid multi-value properties" {
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px"));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("invalid unknown"));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px invalid"));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(" "));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isAlreadyQuoted - various quoting scenarios" {
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"hello\""));
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'world'"));
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"\""));
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("''"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\""));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello'"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'hello\""));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello\""));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isValidPropertyName - valid property names" {
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("color"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("background-color"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-webkit-transform"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("font-size"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("margin-top"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("z-index"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("line-height"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isValidPropertyName - invalid property names" {
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("123color"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color!"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color space"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("@color"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color.test"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color_test"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: extractImportant - with and without !important" {
|
||||
var result = CSSValueAnalyzer.extractImportant("red !important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("blue");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("blue", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant(" green !important ");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("green", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("!important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("important", result.value);
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: needsQuotes - various scenarios" {
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes(""));
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes("hello world"));
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes("test;"));
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes("a{b}"));
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes("test\"quote"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("\"already quoted\""));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("'already quoted'"));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("url(image.png)"));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0)"));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("10px 20px"));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("simple"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: escapeCSSValue - escaping various characters" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
var result = try CSSValueAnalyzer.escapeCSSValue(allocator, "simple");
|
||||
try testing.expectEqual("simple", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "\"already quoted\"");
|
||||
try testing.expectEqual("\"already quoted\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\"quote");
|
||||
try testing.expectEqual("\"test\\\"quote\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\nline");
|
||||
try testing.expectEqual("\"test\\A line\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\\back");
|
||||
try testing.expectEqual("\"test\\\\back\"", result);
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: 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 "CSSValueAnalyzer: 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 "CSSValueAnalyzer: 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 "CSSValueAnalyzer: 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 "CSSValueAnalyzer: isNumericWithUnit - whitespace handling" {
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(" 10px"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10 px"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10px "));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(" 10 px "));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: extractImportant - whitespace edge cases" {
|
||||
var result = CSSValueAnalyzer.extractImportant(" ");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("\t\n\r !important\t\n");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("red\t!important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red", result.value);
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isHexColor - mixed case handling" {
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#AbC"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#123aBc"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#FFffFF"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#000FFF"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: edge case - very long inputs" {
|
||||
const long_valid = "a" ** 1000 ++ "px";
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(long_valid)); // not numeric
|
||||
|
||||
const long_property = "a-" ** 100 ++ "property";
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName(long_property));
|
||||
|
||||
const long_hex = "#" ++ "a" ** 20;
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor(long_hex));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: boundary conditions - numeric parsing" {
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.0px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit(".0px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.px"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("999999999px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1.7976931348623157e+308px"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.000000001px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e-100px"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: extractImportant - malformed important declarations" {
|
||||
var result = CSSValueAnalyzer.extractImportant("red ! important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red ! important", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("red !Important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red !Important", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("red !IMPORTANT");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red !IMPORTANT", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("!importantred");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("!importantred", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("red !important !important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red !important", result.value);
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isMultiValueProperty - complex spacing scenarios" {
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px"));
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("solid red"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty(" 10px 20px "));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px\t20px"));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px\n20px"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px 30px"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isAlreadyQuoted - edge cases with quotes" {
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"'hello'\""));
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'\"hello\"'"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"hello\\\"world\""));
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'hello\\'world'"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello\""));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'hello"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello'"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"a\""));
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'b'"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: needsQuotes - function and URL edge cases" {
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0)"));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("calc(100% - 20px)"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("url(path with spaces.jpg)"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("linear-gradient(to right, red, blue)"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: escapeCSSValue - control characters and Unicode" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
var result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\ttab");
|
||||
try testing.expectEqual("\"test\\9 tab\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\rreturn");
|
||||
try testing.expectEqual("\"test\\D return\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\x00null");
|
||||
try testing.expectEqual("\"test\\0null\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\x7Fdel");
|
||||
try testing.expectEqual("\"test\\7f del\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\"quote\nline\\back");
|
||||
try testing.expectEqual("\"test\\\"quote\\A line\\\\back\"", result);
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isValidPropertyName - CSS custom properties and vendor prefixes" {
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("--custom-color"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("--my-variable"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("--123"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-webkit-transform"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-moz-border-radius"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-ms-filter"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-o-transition"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("-123invalid"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("--"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("-"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: 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 "CSSValueAnalyzer: isHexColor - Unicode and invalid characters" {
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#ghijkl"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#12345g"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#xyz"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#АВС"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#1234567g"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#g2345678"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: complex integration scenarios" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("rgb(255,0,0) url(bg.jpg)"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("calc(100% - 20px)"));
|
||||
|
||||
const result = try CSSValueAnalyzer.escapeCSSValue(allocator, "fake(function with spaces");
|
||||
try testing.expectEqual("\"fake(function with spaces\"", result);
|
||||
|
||||
const important_result = CSSValueAnalyzer.extractImportant("rgb(255,0,0) !important");
|
||||
try testing.expect(important_result.is_important);
|
||||
try testing.expectEqual("rgb(255,0,0)", important_result.value);
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: performance edge cases - empty and minimal inputs" {
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName(""));
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes(""));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("a"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("a"));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("a"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("a"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("a"));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("a"));
|
||||
}
|
||||
30
src/browser/cssom/cssom.zig
Normal file
30
src/browser/cssom/cssom.zig
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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 Stylesheet = @import("stylesheet.zig").StyleSheet;
|
||||
pub const CSSStylesheet = @import("css_stylesheet.zig").CSSStyleSheet;
|
||||
pub const CSSStyleDeclaration = @import("css_style_declaration.zig").CSSStyleDeclaration;
|
||||
pub const CSSRuleList = @import("css_rule_list.zig").CSSRuleList;
|
||||
|
||||
pub const Interfaces = .{
|
||||
Stylesheet,
|
||||
CSSStylesheet,
|
||||
CSSStyleDeclaration,
|
||||
CSSRuleList,
|
||||
@import("css_rule.zig").Interfaces,
|
||||
};
|
||||
55
src/browser/cssom/stylesheet.zig
Normal file
55
src/browser/cssom/stylesheet.zig
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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
|
||||
pub const StyleSheet = struct {
|
||||
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;
|
||||
}
|
||||
};
|
||||
79
src/browser/datauri.zig
Normal file
79
src/browser/datauri.zig
Normal file
@@ -0,0 +1,79 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// Represents https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data
|
||||
pub const DataURI = struct {
|
||||
was_base64_encoded: bool,
|
||||
// The contents in the uri. It will be base64 decoded but not prepared in
|
||||
// any way for mime.charset.
|
||||
data: []const u8,
|
||||
|
||||
// Parses data:[<media-type>][;base64],<data>
|
||||
pub fn parse(allocator: Allocator, src: []const u8) !?DataURI {
|
||||
if (!std.mem.startsWith(u8, src, "data:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uri = src[5..];
|
||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
||||
|
||||
// Extract the encoding.
|
||||
var metadata = uri[0..data_starts];
|
||||
var base64_encoded = false;
|
||||
if (std.mem.endsWith(u8, metadata, ";base64")) {
|
||||
base64_encoded = true;
|
||||
metadata = metadata[0 .. metadata.len - 7];
|
||||
}
|
||||
|
||||
// TODO: Extract mime type. This not trivial because Mime.parse requires
|
||||
// a []u8 and might mutate the src. And, the DataURI.parse references atm
|
||||
// do not have deinit calls.
|
||||
|
||||
// Prepare the data.
|
||||
var data = uri[data_starts + 1 ..];
|
||||
if (base64_encoded) {
|
||||
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 .{
|
||||
.was_base64_encoded = base64_encoded,
|
||||
.data = data,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const DataURI, allocator: Allocator) void {
|
||||
if (self.was_base64_encoded) {
|
||||
allocator.free(self.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = std.testing;
|
||||
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 {
|
||||
const data_uri = try DataURI.parse(std.testing.allocator, uri) orelse return error.TestFailed;
|
||||
defer data_uri.deinit(testing.allocator);
|
||||
try testing.expectEqualStrings(expected, data_uri.data);
|
||||
}
|
||||
|
||||
fn test_cannot_parse(uri: []const u8) !void {
|
||||
try testing.expectEqual(null, DataURI.parse(std.testing.allocator, uri));
|
||||
}
|
||||
126
src/browser/dom/Animation.zig
Normal file
126
src/browser/dom/Animation.zig
Normal file
@@ -0,0 +1,126 @@
|
||||
// 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 Page = @import("../page.zig").Page;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
const Promise = @import("../env.zig").Promise;
|
||||
const PromiseResolver = @import("../env.zig").PromiseResolver;
|
||||
|
||||
const Animation = @This();
|
||||
|
||||
effect: ?JsObject,
|
||||
timeline: ?JsObject,
|
||||
ready_resolver: ?PromiseResolver,
|
||||
finished_resolver: ?PromiseResolver,
|
||||
|
||||
pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !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) !Promise {
|
||||
if (self.finished_resolver == null) {
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
try resolver.resolve(self);
|
||||
self.finished_resolver = resolver;
|
||||
}
|
||||
return self.finished_resolver.?.promise();
|
||||
}
|
||||
|
||||
pub fn get_ready(self: *Animation, page: *Page) !Promise {
|
||||
// never resolved, because we're always "finished"
|
||||
if (self.ready_resolver == null) {
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
self.ready_resolver = resolver;
|
||||
}
|
||||
return self.ready_resolver.?.promise();
|
||||
}
|
||||
|
||||
pub fn get_effect(self: *const Animation) ?JsObject {
|
||||
return self.effect;
|
||||
}
|
||||
|
||||
pub fn set_effect(self: *Animation, effect: JsObject) !void {
|
||||
self.effect = try effect.persist();
|
||||
}
|
||||
|
||||
pub fn get_timeline(self: *const Animation) ?JsObject {
|
||||
return self.timeline;
|
||||
}
|
||||
|
||||
pub fn set_timeline(self: *Animation, timeline: JsObject) !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.Animation" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a1 = document.createElement('div').animate(null, null)", null },
|
||||
.{ "a1.playState", "finished" },
|
||||
.{ "let cb = [];", null },
|
||||
.{ "a1.ready.then(() => { cb.push('ready') })", null },
|
||||
.{
|
||||
\\ a1.finished.then((x) => {
|
||||
\\ cb.push('finished');
|
||||
\\ cb.push(x == a1);
|
||||
\\ })
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "cb", "finished,true" },
|
||||
}, .{});
|
||||
}
|
||||
361
src/browser/dom/MessageChannel.zig
Normal file
361
src/browser/dom/MessageChannel.zig
Normal file
@@ -0,0 +1,361 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const JsObject = Env.JsObject;
|
||||
const Function = Env.Function;
|
||||
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: ?Function = null,
|
||||
onmessageerror_cbk: ?Function = null,
|
||||
// This is the queue of messages to dispatch to THIS MessagePort when the
|
||||
// MessagePort is started.
|
||||
queue: std.ArrayListUnmanaged(JsObject) = .empty,
|
||||
|
||||
pub const PostMessageOption = union(enum) {
|
||||
transfer: JsObject,
|
||||
options: Opts,
|
||||
|
||||
pub const Opts = struct {
|
||||
transfer: JsObject,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn _postMessage(self: *MessagePort, obj: JsObject, opts_: ?PostMessageOption, page: *Page) !void {
|
||||
if (self.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts_ != null) {
|
||||
log.warn(.web_api, "not implemented", .{ .feature = "MessagePort postMessage options" });
|
||||
return error.NotImplemented;
|
||||
}
|
||||
|
||||
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) ?Function {
|
||||
return self.onmessage_cbk;
|
||||
}
|
||||
pub fn get_onmessageerror(self: *MessagePort) ?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: JsObject, 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: JsObject) !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,
|
||||
) !?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: ?JsObject,
|
||||
|
||||
// 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: ?JsObject,
|
||||
|
||||
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: ?JsObject = null,
|
||||
source: ?JsObject = 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 JsObjects 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", .{});
|
||||
try 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) !?JsObject {
|
||||
return self.data;
|
||||
}
|
||||
|
||||
pub fn get_origin(self: *const MessageEvent) []const u8 {
|
||||
return self.origin;
|
||||
}
|
||||
|
||||
pub fn get_source(self: *const MessageEvent) ?JsObject {
|
||||
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.MessageChannel" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{
|
||||
.html = "",
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const mc1 = new MessageChannel()", null },
|
||||
.{ "mc1.port1 == mc1.port1", "true" },
|
||||
.{ "mc1.port2 == mc1.port2", "true" },
|
||||
.{ "mc1.port1 != mc1.port2", "true" },
|
||||
.{ "mc1.port1.postMessage('msg1');", "undefined" },
|
||||
.{
|
||||
\\ let message = null;
|
||||
\\ let target = null;
|
||||
\\ let currentTarget = null;
|
||||
\\ mc1.port2.onmessage = (e) => {
|
||||
\\ message = e.data;
|
||||
\\ target = e.target;
|
||||
\\ currentTarget = e.currentTarget;
|
||||
\\ };
|
||||
,
|
||||
null,
|
||||
},
|
||||
// as soon as onmessage is called, queued messages are delivered
|
||||
.{ "message", "msg1" },
|
||||
.{ "target == mc1.port2", "true" },
|
||||
.{ "currentTarget == mc1.port2", "true" },
|
||||
|
||||
.{ "mc1.port1.postMessage('msg2');", "undefined" },
|
||||
.{ "message", "msg2" },
|
||||
.{ "target == mc1.port2", "true" },
|
||||
.{ "currentTarget == mc1.port2", "true" },
|
||||
|
||||
.{ "message = null", null },
|
||||
.{ "mc1.port1.close();", null },
|
||||
.{ "mc1.port1.postMessage('msg3');", "undefined" },
|
||||
.{ "message", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const mc2 = new MessageChannel()", null },
|
||||
.{ "mc2.port2.postMessage('msg1');", "undefined" },
|
||||
.{ "mc2.port1.postMessage('msg2');", "undefined" },
|
||||
.{
|
||||
\\ let message1 = null;
|
||||
\\ mc2.port1.addEventListener('message', (e) => {
|
||||
\\ message1 = e.data;
|
||||
\\ });
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{
|
||||
\\ let message2 = null;
|
||||
\\ mc2.port2.addEventListener('message', (e) => {
|
||||
\\ message2 = e.data;
|
||||
\\ });
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "message1", "null" },
|
||||
.{ "message2", "null" },
|
||||
.{ "mc2.port2.start()", null },
|
||||
|
||||
.{ "message1", "null" },
|
||||
.{ "message2", "msg2" },
|
||||
.{ "message2 = null", null },
|
||||
|
||||
.{ "mc2.port1.start()", null },
|
||||
.{ "message1", "msg1" },
|
||||
.{ "message2", "null" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -15,23 +15,15 @@
|
||||
//
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#attr
|
||||
pub const Attr = struct {
|
||||
pub const Self = parser.Attribute;
|
||||
pub const prototype = *Node;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_namespaceURI(self: *parser.Attribute) !?[]const u8 {
|
||||
return try parser.nodeGetNamespace(parser.attributeToNode(self));
|
||||
@@ -54,7 +46,14 @@ pub const Attr = struct {
|
||||
}
|
||||
|
||||
pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 {
|
||||
try parser.attributeSetValue(self, v);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -70,34 +69,33 @@ pub const Attr = struct {
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var getters = [_]Case{
|
||||
.{ .src = "let a = document.createAttributeNS('foo', 'bar')", .ex = "undefined" },
|
||||
.{ .src = "a.namespaceURI", .ex = "foo" },
|
||||
.{ .src = "a.prefix", .ex = "null" },
|
||||
.{ .src = "a.localName", .ex = "bar" },
|
||||
.{ .src = "a.name", .ex = "bar" },
|
||||
.{ .src = "a.value", .ex = "" },
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Attribute" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.createAttributeNS('foo', 'bar')", "undefined" },
|
||||
.{ "a.namespaceURI", "foo" },
|
||||
.{ "a.prefix", "null" },
|
||||
.{ "a.localName", "bar" },
|
||||
.{ "a.name", "bar" },
|
||||
.{ "a.value", "" },
|
||||
// TODO: libdom has a bug here: the created attr has no parent, it
|
||||
// causes a panic w/ libdom when setting the value.
|
||||
//.{ .src = "a.value = 'nok'", .ex = "nok" },
|
||||
.{ .src = "a.ownerElement", .ex = "null" },
|
||||
};
|
||||
try checkCases(js_env, &getters);
|
||||
//.{ "a.value = 'nok'", "nok" },
|
||||
.{ "a.ownerElement", "null" },
|
||||
}, .{});
|
||||
|
||||
var attr = [_]Case{
|
||||
.{ .src = "let b = document.getElementById('link').getAttributeNode('class')", .ex = "undefined" },
|
||||
.{ .src = "b.name", .ex = "class" },
|
||||
.{ .src = "b.value", .ex = "ok" },
|
||||
.{ .src = "b.value = 'nok'", .ex = "nok" },
|
||||
.{ .src = "b.value", .ex = "nok" },
|
||||
.{ .src = "b.value = null", .ex = "null" },
|
||||
.{ .src = "b.value", .ex = "null" },
|
||||
.{ .src = "b.value = 'ok'", .ex = "ok" },
|
||||
.{ .src = "b.ownerElement.id", .ex = "link" },
|
||||
};
|
||||
try checkCases(js_env, &attr);
|
||||
try runner.testCases(&.{
|
||||
.{ "let b = document.getElementById('link').getAttributeNode('class')", "undefined" },
|
||||
.{ "b.name", "class" },
|
||||
.{ "b.value", "ok" },
|
||||
.{ "b.value = 'nok'", "nok" },
|
||||
.{ "b.value", "nok" },
|
||||
.{ "b.value = null", "null" },
|
||||
.{ "b.value", "null" },
|
||||
.{ "b.value = 'ok'", "ok" },
|
||||
.{ "b.ownerElement.id", "link" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -16,9 +16,7 @@
|
||||
// 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");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Text = @import("text.zig").Text;
|
||||
|
||||
@@ -26,5 +24,5 @@ const Text = @import("text.zig").Text;
|
||||
pub const CDATASection = struct {
|
||||
pub const Self = parser.CDATASection;
|
||||
pub const prototype = *Text;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
@@ -18,12 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
const Comment = @import("comment.zig").Comment;
|
||||
@@ -32,18 +27,18 @@ const ProcessingInstruction = @import("processing_instruction.zig").ProcessingIn
|
||||
const HTMLElem = @import("../html/elements.zig");
|
||||
|
||||
// CharacterData interfaces
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
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 mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
@@ -102,79 +97,100 @@ pub const CharacterData = struct {
|
||||
pub fn _substringData(self: *parser.CharacterData, offset: u32, count: u32) ![]const u8 {
|
||||
return try 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 (try parser.nodeType(@alignCast(@ptrCast(self))) != try parser.nodeType(other_node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const other: *parser.CharacterData = @ptrCast(other_node);
|
||||
if (std.mem.eql(u8, try get_data(self), try 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
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var get_data = [_]Case{
|
||||
.{ .src = "let link = document.getElementById('link')", .ex = "undefined" },
|
||||
.{ .src = "let cdata = link.firstChild", .ex = "undefined" },
|
||||
.{ .src = "cdata.data", .ex = "OK" },
|
||||
};
|
||||
try checkCases(js_env, &get_data);
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.CharacterData" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
var set_data = [_]Case{
|
||||
.{ .src = "cdata.data = 'OK modified'", .ex = "OK modified" },
|
||||
.{ .src = "cdata.data === 'OK modified'", .ex = "true" },
|
||||
.{ .src = "cdata.data = 'OK'", .ex = "OK" },
|
||||
};
|
||||
try checkCases(js_env, &set_data);
|
||||
try runner.testCases(&.{
|
||||
.{ "let link = document.getElementById('link')", "undefined" },
|
||||
.{ "let cdata = link.firstChild", "undefined" },
|
||||
.{ "cdata.data", "OK" },
|
||||
}, .{});
|
||||
|
||||
var get_length = [_]Case{
|
||||
.{ .src = "cdata.length === 2", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &get_length);
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.data = 'OK modified'", "OK modified" },
|
||||
.{ "cdata.data === 'OK modified'", "true" },
|
||||
.{ "cdata.data = 'OK'", "OK" },
|
||||
}, .{});
|
||||
|
||||
var get_next_elem_sibling = [_]Case{
|
||||
.{ .src = "cdata.nextElementSibling === null", .ex = "true" },
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.length === 2", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.nextElementSibling === null", "true" },
|
||||
// create a next element
|
||||
.{ .src = "let next = document.createElement('a')", .ex = "undefined" },
|
||||
.{ .src = "link.appendChild(next, cdata) !== undefined", .ex = "true" },
|
||||
.{ .src = "cdata.nextElementSibling.localName === 'a' ", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &get_next_elem_sibling);
|
||||
.{ "let next = document.createElement('a')", "undefined" },
|
||||
.{ "link.appendChild(next, cdata) !== undefined", "true" },
|
||||
.{ "cdata.nextElementSibling.localName === 'a' ", "true" },
|
||||
}, .{});
|
||||
|
||||
var get_prev_elem_sibling = [_]Case{
|
||||
.{ .src = "cdata.previousElementSibling === null", .ex = "true" },
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.previousElementSibling === null", "true" },
|
||||
// create a prev element
|
||||
.{ .src = "let prev = document.createElement('div')", .ex = "undefined" },
|
||||
.{ .src = "link.insertBefore(prev, cdata) !== undefined", .ex = "true" },
|
||||
.{ .src = "cdata.previousElementSibling.localName === 'div' ", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &get_prev_elem_sibling);
|
||||
.{ "let prev = document.createElement('div')", "undefined" },
|
||||
.{ "link.insertBefore(prev, cdata) !== undefined", "true" },
|
||||
.{ "cdata.previousElementSibling.localName === 'div' ", "true" },
|
||||
}, .{});
|
||||
|
||||
var append_data = [_]Case{
|
||||
.{ .src = "cdata.appendData(' modified')", .ex = "undefined" },
|
||||
.{ .src = "cdata.data === 'OK modified' ", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &append_data);
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.appendData(' modified')", "undefined" },
|
||||
.{ "cdata.data === 'OK modified' ", "true" },
|
||||
}, .{});
|
||||
|
||||
var delete_data = [_]Case{
|
||||
.{ .src = "cdata.deleteData('OK'.length, ' modified'.length)", .ex = "undefined" },
|
||||
.{ .src = "cdata.data == 'OK'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &delete_data);
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.deleteData('OK'.length, ' modified'.length)", "undefined" },
|
||||
.{ "cdata.data == 'OK'", "true" },
|
||||
}, .{});
|
||||
|
||||
var insert_data = [_]Case{
|
||||
.{ .src = "cdata.insertData('OK'.length-1, 'modified')", .ex = "undefined" },
|
||||
.{ .src = "cdata.data == 'OmodifiedK'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &insert_data);
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.insertData('OK'.length-1, 'modified')", "undefined" },
|
||||
.{ "cdata.data == 'OmodifiedK'", "true" },
|
||||
}, .{});
|
||||
|
||||
var replace_data = [_]Case{
|
||||
.{ .src = "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", .ex = "undefined" },
|
||||
.{ .src = "cdata.data == 'OreplacedK'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &replace_data);
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", "undefined" },
|
||||
.{ "cdata.data == 'OreplacedK'", "true" },
|
||||
}, .{});
|
||||
|
||||
var substring_data = [_]Case{
|
||||
.{ .src = "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", .ex = "true" },
|
||||
.{ .src = "cdata.substringData('OK'.length-1, 0) == ''", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &substring_data);
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
|
||||
.{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
|
||||
.{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -15,27 +15,22 @@
|
||||
//
|
||||
// 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");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const CharacterData = @import("character_data.zig").CharacterData;
|
||||
|
||||
const UserContext = @import("../user_context.zig").UserContext;
|
||||
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 mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Comment {
|
||||
pub fn constructor(data: ?[]const u8, page: *const Page) !*parser.Comment {
|
||||
return parser.documentCreateComment(
|
||||
parser.documentHTMLToDocument(userctx.document),
|
||||
parser.documentHTMLToDocument(page.window.document),
|
||||
data orelse "",
|
||||
);
|
||||
}
|
||||
@@ -44,16 +39,16 @@ pub const Comment = struct {
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var constructor = [_]Case{
|
||||
.{ .src = "let comment = new Comment('foo')", .ex = "undefined" },
|
||||
.{ .src = "comment.data", .ex = "foo" },
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Comment" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
.{ .src = "let emptycomment = new Comment()", .ex = "undefined" },
|
||||
.{ .src = "emptycomment.data", .ex = "" },
|
||||
};
|
||||
try checkCases(js_env, &constructor);
|
||||
try runner.testCases(&.{
|
||||
.{ "let comment = new Comment('foo')", "undefined" },
|
||||
.{ "comment.data", "foo" },
|
||||
|
||||
.{ "let emptycomment = new Comment()", "undefined" },
|
||||
.{ "emptycomment.data", "" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const css = @import("../css/css.zig");
|
||||
const Node = @import("../css/libdom.zig").Node;
|
||||
@@ -49,7 +49,7 @@ const MatchAll = struct {
|
||||
fn init(alloc: std.mem.Allocator) MatchAll {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.nl = NodeList.init(),
|
||||
.nl = .{},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ const MatchAll = struct {
|
||||
}
|
||||
|
||||
fn toOwnedList(m: *MatchAll) NodeList {
|
||||
defer m.nl = NodeList.init();
|
||||
// reset it.
|
||||
defer m.nl = .{};
|
||||
return m.nl;
|
||||
}
|
||||
};
|
||||
498
src/browser/dom/document.zig
Normal file
498
src/browser/dom/document.zig
Normal file
@@ -0,0 +1,498 @@
|
||||
// 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 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/css_stylesheet.zig").CSSStyleSheet;
|
||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
||||
const Range = @import("range.zig").Range;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
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) !*parser.Event {
|
||||
// TODO: for now only "Event" constructor is supported
|
||||
// see table on https://dom.spec.whatwg.org/#dom-document-createevent $2
|
||||
if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) {
|
||||
return try parser.eventCreate();
|
||||
}
|
||||
return parser.DOMError.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: []const u8,
|
||||
page: *Page,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, true);
|
||||
}
|
||||
|
||||
pub fn _getElementsByClassName(
|
||||
self: *parser.Document,
|
||||
classNames: []const u8,
|
||||
page: *Page,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, 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 try collection.HTMLCollectionChildren(parser.documentToNode(self), 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: ?u32, filter: ?TreeWalker.TreeWalkerOpts) !TreeWalker {
|
||||
return try TreeWalker.init(root, what_to_show, filter);
|
||||
}
|
||||
|
||||
pub fn _createNodeIterator(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?NodeIterator.NodeIteratorOpts) !NodeIterator {
|
||||
return try NodeIterator.init(root, what_to_show, filter);
|
||||
}
|
||||
|
||||
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
|
||||
if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| {
|
||||
if (state.active_element) |ae| {
|
||||
return ae;
|
||||
}
|
||||
}
|
||||
|
||||
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
||||
return @alignCast(@ptrCast(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(@alignCast(@ptrCast(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 &.{};
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Document" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{
|
||||
.url = "about:blank",
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.__proto__.__proto__.constructor.name", "Document" },
|
||||
.{ "document.__proto__.__proto__.__proto__.constructor.name", "Node" },
|
||||
.{ "document.__proto__.__proto__.__proto__.__proto__.constructor.name", "EventTarget" },
|
||||
|
||||
.{ "let newdoc = new Document()", "undefined" },
|
||||
.{ "newdoc.documentElement", "null" },
|
||||
.{ "newdoc.children.length", "0" },
|
||||
.{ "newdoc.getElementsByTagName('*').length", "0" },
|
||||
.{ "newdoc.getElementsByTagName('*').item(0)", "null" },
|
||||
.{ "newdoc.inputEncoding === document.inputEncoding", "true" },
|
||||
.{ "newdoc.documentURI === document.documentURI", "true" },
|
||||
.{ "newdoc.URL === document.URL", "true" },
|
||||
.{ "newdoc.compatMode === document.compatMode", "true" },
|
||||
.{ "newdoc.characterSet === document.characterSet", "true" },
|
||||
.{ "newdoc.charset === document.charset", "true" },
|
||||
.{ "newdoc.contentType === document.contentType", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let getElementById = document.getElementById('content')", "undefined" },
|
||||
.{ "getElementById.constructor.name", "HTMLDivElement" },
|
||||
.{ "getElementById.localName", "div" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
|
||||
.{ "getElementsByTagName.length", "2" },
|
||||
.{ "getElementsByTagName.item(0).localName", "p" },
|
||||
.{ "getElementsByTagName.item(1).localName", "p" },
|
||||
.{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
|
||||
.{ "getElementsByTagNameAll.length", "8" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(7).localName", "p" },
|
||||
.{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let ok = document.getElementsByClassName('ok')", "undefined" },
|
||||
.{ "ok.length", "2" },
|
||||
.{ "let empty = document.getElementsByClassName('empty')", "undefined" },
|
||||
.{ "empty.length", "1" },
|
||||
.{ "let emptyok = document.getElementsByClassName('empty ok')", "undefined" },
|
||||
.{ "emptyok.length", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let e = document.documentElement", "undefined" },
|
||||
.{ "e.localName", "html" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.characterSet", "UTF-8" },
|
||||
.{ "document.charset", "UTF-8" },
|
||||
.{ "document.inputEncoding", "UTF-8" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.compatMode", "CSS1Compat" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.contentType", "text/html" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.documentURI", "about:blank" },
|
||||
.{ "document.URL", "about:blank" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let impl = document.implementation", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let d = new Document()", "undefined" },
|
||||
.{ "d.characterSet", "UTF-8" },
|
||||
.{ "d.URL", "about:blank" },
|
||||
.{ "d.documentURI", "about:blank" },
|
||||
.{ "d.compatMode", "CSS1Compat" },
|
||||
.{ "d.contentType", "text/html" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createDocumentFragment()", "undefined" },
|
||||
.{ "v.nodeName", "#document-fragment" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createTextNode('foo')", "undefined" },
|
||||
.{ "v.nodeName", "#text" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createCDATASection('foo')", "undefined" },
|
||||
.{ "v.nodeName", "#cdata-section" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createComment('foo')", "undefined" },
|
||||
.{ "v.nodeName", "#comment" },
|
||||
.{ "let v2 = v.cloneNode()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" },
|
||||
.{ "pi.target", "foo" },
|
||||
.{ "let pi2 = pi.cloneNode()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let nimp = document.getElementById('content')", "undefined" },
|
||||
.{ "var v = document.importNode(nimp)", "undefined" },
|
||||
.{ "v.nodeName", "DIV" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createAttribute('foo')", "undefined" },
|
||||
.{ "v.nodeName", "foo" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.children.length", "1" },
|
||||
.{ "document.children.item(0).nodeName", "HTML" },
|
||||
.{ "document.firstElementChild.nodeName", "HTML" },
|
||||
.{ "document.lastElementChild.nodeName", "HTML" },
|
||||
.{ "document.childElementCount", "1" },
|
||||
|
||||
.{ "let nd = new Document()", "undefined" },
|
||||
.{ "nd.children.length", "0" },
|
||||
.{ "nd.children.item(0)", "null" },
|
||||
.{ "nd.firstElementChild", "null" },
|
||||
.{ "nd.lastElementChild", "null" },
|
||||
.{ "nd.childElementCount", "0" },
|
||||
|
||||
.{ "let emptydoc = document.createElement('html')", "undefined" },
|
||||
.{ "emptydoc.prepend(document.createElement('html'))", "undefined" },
|
||||
|
||||
.{ "let emptydoc2 = document.createElement('html')", "undefined" },
|
||||
.{ "emptydoc2.append(document.createElement('html'))", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.querySelector('')", "null" },
|
||||
.{ "document.querySelector('*').nodeName", "HTML" },
|
||||
.{ "document.querySelector('#content').id", "content" },
|
||||
.{ "document.querySelector('#para').id", "para" },
|
||||
.{ "document.querySelector('.ok').id", "link" },
|
||||
.{ "document.querySelector('a ~ p').id", "para-empty" },
|
||||
.{ "document.querySelector(':root').nodeName", "HTML" },
|
||||
|
||||
.{ "document.querySelectorAll('p').length", "2" },
|
||||
.{
|
||||
\\ Array.from(document.querySelectorAll('#content > p#para-empty'))
|
||||
\\ .map(row => row.querySelector('span').textContent)
|
||||
\\ .length;
|
||||
,
|
||||
"1",
|
||||
},
|
||||
|
||||
.{ "document.querySelectorAll('.\\\\:popover-open').length", "0" },
|
||||
.{ "document.querySelectorAll('.foo\\\\:bar').length", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.activeElement === document.body", "true" },
|
||||
.{ "document.getElementById('link').focus()", "undefined" },
|
||||
.{ "document.activeElement === document.getElementById('link')", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.styleSheets.length", "0" },
|
||||
}, .{});
|
||||
|
||||
// this test breaks the doc structure, keep it at the end of the test
|
||||
// suite.
|
||||
try runner.testCases(&.{
|
||||
.{ "let nadop = document.getElementById('content')", "undefined" },
|
||||
.{ "var v = document.adoptNode(nadop)", "undefined" },
|
||||
.{ "v.nodeName", "DIV" },
|
||||
}, .{});
|
||||
|
||||
const Case = testing.JsRunner.Case;
|
||||
const tags = comptime parser.Tag.all();
|
||||
var createElements: [(tags.len) * 2]Case = undefined;
|
||||
inline for (tags, 0..) |tag, i| {
|
||||
const tag_name = @tagName(tag);
|
||||
createElements[i * 2] = Case{
|
||||
"var " ++ tag_name ++ "Elem = document.createElement('" ++ tag_name ++ "')",
|
||||
"undefined",
|
||||
};
|
||||
createElements[(i * 2) + 1] = Case{
|
||||
tag_name ++ "Elem.localName",
|
||||
tag_name,
|
||||
};
|
||||
}
|
||||
try runner.testCases(&createElements, .{});
|
||||
}
|
||||
124
src/browser/dom/document_fragment.zig
Normal file
124
src/browser/dom/document_fragment.zig
Normal file
@@ -0,0 +1,124 @@
|
||||
// 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 = try 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), false);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.DocumentFragment" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const dc = new DocumentFragment()", "undefined" },
|
||||
.{ "dc.constructor.name", "DocumentFragment" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const dc1 = new DocumentFragment()", "undefined" },
|
||||
.{ "const dc2 = new DocumentFragment()", "undefined" },
|
||||
.{ "dc1.isEqualNode(dc1)", "true" },
|
||||
.{ "dc1.isEqualNode(dc2)", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let f = document.createDocumentFragment()", null },
|
||||
.{ "let d = document.createElement('div');", null },
|
||||
.{ "d.childElementCount", "0" },
|
||||
|
||||
.{ "d.id = 'x';", null },
|
||||
.{ "document.getElementById('x') == null;", "true" },
|
||||
.{ "f.append(d);", null },
|
||||
.{ "f.childElementCount", "1" },
|
||||
.{ "f.children[0].id", "x" },
|
||||
.{ "document.getElementById('x') == null;", "true" },
|
||||
|
||||
.{ "document.getElementsByTagName('body')[0].append(f.cloneNode(true));", null },
|
||||
.{ "document.getElementById('x') != null;", "true" },
|
||||
|
||||
.{ "document.querySelector('.hello')", "null" },
|
||||
.{ "document.querySelectorAll('.hello').length", "0" },
|
||||
|
||||
.{ "document.querySelector('#x').id", "x" },
|
||||
.{ "document.querySelectorAll('#x')[0].id", "x" },
|
||||
}, .{});
|
||||
}
|
||||
80
src/browser/dom/document_type.zig
Normal file
80
src/browser/dom/document_type.zig
Normal file
@@ -0,0 +1,80 @@
|
||||
// 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 try parser.documentTypeGetName(self);
|
||||
}
|
||||
|
||||
pub fn get_publicId(self: *parser.DocumentType) ![]const u8 {
|
||||
return try parser.documentTypeGetPublicId(self);
|
||||
}
|
||||
|
||||
pub fn get_systemId(self: *parser.DocumentType) ![]const u8 {
|
||||
return try 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 (try 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, try get_publicId(self), try get_publicId(other)) == false) {
|
||||
return false;
|
||||
}
|
||||
if (std.mem.eql(u8, try get_systemId(self), try get_systemId(other)) == false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.DocumentType" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let dt1 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');", "undefined" },
|
||||
.{ "let dt2 = document.implementation.createDocumentType('qname2', 'pid2', 'sys2');", "undefined" },
|
||||
.{ "let dt3 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');", "undefined" },
|
||||
.{ "dt1.isEqualNode(dt1)", "true" },
|
||||
.{ "dt1.isEqualNode(dt3)", "true" },
|
||||
.{ "dt1.isEqualNode(dt2)", "false" },
|
||||
.{ "dt2.isEqualNode(dt3)", "false" },
|
||||
.{ "dt1.isEqualNode(document)", "false" },
|
||||
.{ "document.isEqualNode(dt1)", "false" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -16,25 +16,42 @@
|
||||
// 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 generate = @import("../generate.zig");
|
||||
|
||||
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").DOMTokenList;
|
||||
const DOMTokenList = @import("token_list.zig");
|
||||
const NodeList = @import("nodelist.zig");
|
||||
const Nod = @import("node.zig");
|
||||
const Node = @import("node.zig");
|
||||
const ResizeObserver = @import("resize_observer.zig");
|
||||
const MutationObserver = @import("mutation_observer.zig");
|
||||
const IntersectionObserver = @import("intersection_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 = generate.Tuple(.{
|
||||
pub const Interfaces = .{
|
||||
DOMException,
|
||||
EventTarget,
|
||||
DOMImplementation,
|
||||
NamedNodeMap,
|
||||
DOMTokenList,
|
||||
NamedNodeMap.Iterator,
|
||||
DOMTokenList.Interfaces,
|
||||
NodeList.Interfaces,
|
||||
Nod.Node,
|
||||
Nod.Interfaces,
|
||||
Node.Node,
|
||||
Node.Interfaces,
|
||||
ResizeObserver.Interfaces,
|
||||
MutationObserver.Interfaces,
|
||||
});
|
||||
IntersectionObserver.Interfaces,
|
||||
DOMParser,
|
||||
TreeWalker,
|
||||
NodeIterator,
|
||||
NodeFilter,
|
||||
@import("performance.zig").Interfaces,
|
||||
PerformanceObserver,
|
||||
@import("range.zig").Interfaces,
|
||||
@import("Animation.zig"),
|
||||
@import("MessageChannel.zig").Interfaces,
|
||||
};
|
||||
47
src/browser/dom/dom_parser.zig
Normal file
47
src/browser/dom/dom_parser.zig
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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");
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
|
||||
pub const DOMParser = struct {
|
||||
pub fn constructor() !DOMParser {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn _parseFromString(_: *DOMParser, string: []const u8, mime_type: []const u8) !*parser.DocumentHTML {
|
||||
if (!std.mem.eql(u8, mime_type, "text/html")) {
|
||||
// TODO: Support XML
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
return try parser.documentHTMLParseFromStr(string);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.DOMParser" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const dp = new DOMParser()", "undefined" },
|
||||
.{ "dp.parseFromString('<div>abc</div>', 'text/html')", "[object HTMLDocument]" },
|
||||
}, .{});
|
||||
}
|
||||
789
src/browser/dom/element.zig
Normal file
789
src/browser/dom/element.zig
Normal file
@@ -0,0 +1,789 @@
|
||||
// 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 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");
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
|
||||
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 try HTMLElem.toInterface(Union, e);
|
||||
// SVGElement and MathML are not supported yet.
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
pub fn get_namespaceURI(self: *parser.Element) !?[]const u8 {
|
||||
return try parser.nodeGetNamespace(parser.elementToNode(self));
|
||||
}
|
||||
|
||||
pub fn get_prefix(self: *parser.Element) !?[]const u8 {
|
||||
return try 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_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_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 buf = std.ArrayList(u8).init(page.arena);
|
||||
try dump.writeChildren(parser.elementToNode(self), .{}, buf.writer());
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn get_outerHTML(self: *parser.Element, page: *Page) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(page.arena);
|
||||
try dump.writeNode(parser.elementToNode(self), .{}, buf.writer());
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn set_innerHTML(self: *parser.Element, str: []const u8) !void {
|
||||
const node = parser.elementToNode(self);
|
||||
const doc = try 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);
|
||||
|
||||
// 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 fragment_node = parser.documentFragmentToNode(fragment);
|
||||
const html = try parser.nodeFirstChild(fragment_node) orelse return;
|
||||
const head = try parser.nodeFirstChild(html) orelse return;
|
||||
{
|
||||
// First, copy some of the head element
|
||||
const children = try parser.nodeGetChildNodes(head);
|
||||
const ln = try 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 = try parser.nodeListItem(children, 0) orelse continue;
|
||||
_ = try parser.nodeAppendChild(node, child);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const body = try parser.nodeNextSibling(head) orelse return;
|
||||
const children = try parser.nodeGetChildNodes(body);
|
||||
const ln = try 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 = try parser.nodeListItem(children, 0) orelse continue;
|
||||
_ = try parser.nodeAppendChild(node, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = try 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 _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: []const u8,
|
||||
page: *Page,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(
|
||||
page.arena,
|
||||
parser.elementToNode(self),
|
||||
tag_name,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _getElementsByClassName(
|
||||
self: *parser.Element,
|
||||
classNames: []const u8,
|
||||
page: *Page,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByClassName(
|
||||
page.arena,
|
||||
parser.elementToNode(self),
|
||||
classNames,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
// ParentNode
|
||||
// https://dom.spec.whatwg.org/#parentnode
|
||||
pub fn get_children(self: *parser.Element) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionChildren(parser.elementToNode(self), 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 = try parser.nodePreviousElementSibling(parser.elementToNode(self));
|
||||
if (res == null) return null;
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
|
||||
}
|
||||
|
||||
pub fn get_nextElementSibling(self: *parser.Element) !?Union {
|
||||
const res = try parser.nodeNextElementSibling(parser.elementToNode(self));
|
||||
if (res == null) return null;
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, 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 (try 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 (!try 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 (!try 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,
|
||||
};
|
||||
|
||||
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(@alignCast(@ptrCast(self)));
|
||||
if (state.shadow_root) |sr| {
|
||||
if (mode != sr.mode) {
|
||||
// this is the behavior per the spec
|
||||
return error.NotSupportedError;
|
||||
}
|
||||
|
||||
try Node.removeChildren(@alignCast(@ptrCast(sr.proto)));
|
||||
return sr;
|
||||
}
|
||||
|
||||
// Not sure what to do if there is no owner document
|
||||
const doc = try 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;
|
||||
return sr;
|
||||
}
|
||||
|
||||
pub fn get_shadowRoot(self: *parser.Element, page: *Page) ?*ShadowRoot {
|
||||
const state = page.getNodeState(@alignCast(@ptrCast(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: JsObject, opts: JsObject) !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 = try parser.nodeParentNode(as_node) orelse return;
|
||||
_ = try Node._removeChild(parent, as_node);
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Element" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let g = document.getElementById('content')", "undefined" },
|
||||
.{ "g.namespaceURI", "http://www.w3.org/1999/xhtml" },
|
||||
.{ "g.prefix", "null" },
|
||||
.{ "g.localName", "div" },
|
||||
.{ "g.tagName", "DIV" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let gs = document.getElementById('content')", "undefined" },
|
||||
.{ "gs.id", "content" },
|
||||
.{ "gs.id = 'foo'", "foo" },
|
||||
.{ "gs.id", "foo" },
|
||||
.{ "gs.id = 'content'", "content" },
|
||||
.{ "gs.className", "" },
|
||||
.{ "let gs2 = document.getElementById('para-empty')", "undefined" },
|
||||
.{ "gs2.className", "ok empty" },
|
||||
.{ "gs2.className = 'foo bar baz'", "foo bar baz" },
|
||||
.{ "gs2.className", "foo bar baz" },
|
||||
.{ "gs2.className = 'ok empty'", "ok empty" },
|
||||
.{ "let cl = gs2.classList", "undefined" },
|
||||
.{ "cl.length", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const el2 = document.createElement('div');", "undefined" },
|
||||
.{ "el2.id = 'closest'; el2.className = 'ok';", "ok" },
|
||||
.{ "el2.closest('#closest')", "[object HTMLDivElement]" },
|
||||
.{ "el2.closest('.ok')", "[object HTMLDivElement]" },
|
||||
.{ "el2.closest('#9000')", "null" },
|
||||
.{ "el2.closest('.notok')", "null" },
|
||||
|
||||
.{ "const sp = document.createElement('span');", "undefined" },
|
||||
.{ "el2.appendChild(sp);", "[object HTMLSpanElement]" },
|
||||
.{ "sp.closest('#closest')", "[object HTMLDivElement]" },
|
||||
.{ "sp.closest('#9000')", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.getElementById('content')", "undefined" },
|
||||
.{ "a.hasAttributes()", "true" },
|
||||
.{ "a.attributes.length", "1" },
|
||||
.{ "a.getAttribute('id')", "content" },
|
||||
.{ "a.attributes['id'].value", "content" },
|
||||
.{
|
||||
\\ let x = '';
|
||||
\\ for (const attr of a.attributes) {
|
||||
\\ x += attr.name + '=' + attr.value;
|
||||
\\ }
|
||||
\\ x;
|
||||
,
|
||||
"id=content",
|
||||
},
|
||||
|
||||
.{ "a.hasAttribute('foo')", "false" },
|
||||
.{ "a.getAttribute('foo')", "null" },
|
||||
|
||||
.{ "a.setAttribute('foo', 'bar')", "undefined" },
|
||||
.{ "a.hasAttribute('foo')", "true" },
|
||||
.{ "a.getAttribute('foo')", "bar" },
|
||||
|
||||
.{ "a.setAttribute('foo', 'baz')", "undefined" },
|
||||
.{ "a.hasAttribute('foo')", "true" },
|
||||
.{ "a.getAttribute('foo')", "baz" },
|
||||
|
||||
.{ "a.removeAttribute('foo')", "undefined" },
|
||||
.{ "a.hasAttribute('foo')", "false" },
|
||||
.{ "a.getAttribute('foo')", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let b = document.getElementById('content')", "undefined" },
|
||||
.{ "b.toggleAttribute('foo')", "true" },
|
||||
.{ "b.hasAttribute('foo')", "true" },
|
||||
.{ "b.getAttribute('foo')", "" },
|
||||
|
||||
.{ "b.toggleAttribute('foo')", "false" },
|
||||
.{ "b.hasAttribute('foo')", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let c = document.getElementById('content')", "undefined" },
|
||||
.{ "c.children.length", "3" },
|
||||
.{ "c.firstElementChild.nodeName", "A" },
|
||||
.{ "c.lastElementChild.nodeName", "P" },
|
||||
.{ "c.childElementCount", "3" },
|
||||
|
||||
.{ "c.prepend(document.createTextNode('foo'))", "undefined" },
|
||||
.{ "c.append(document.createTextNode('bar'))", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let d = document.getElementById('para')", "undefined" },
|
||||
.{ "d.previousElementSibling.nodeName", "P" },
|
||||
.{ "d.nextElementSibling", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let e = document.getElementById('content')", "undefined" },
|
||||
.{ "e.querySelector('foo')", "null" },
|
||||
.{ "e.querySelector('#foo')", "null" },
|
||||
.{ "e.querySelector('#link').id", "link" },
|
||||
.{ "e.querySelector('#para').id", "para" },
|
||||
.{ "e.querySelector('*').id", "link" },
|
||||
.{ "e.querySelector('')", "null" },
|
||||
.{ "e.querySelector('*').id", "link" },
|
||||
.{ "e.querySelector('#content')", "null" },
|
||||
.{ "e.querySelector('#para').id", "para" },
|
||||
.{ "e.querySelector('.ok').id", "link" },
|
||||
.{ "e.querySelector('a ~ p').id", "para-empty" },
|
||||
|
||||
.{ "e.querySelectorAll('foo').length", "0" },
|
||||
.{ "e.querySelectorAll('#foo').length", "0" },
|
||||
.{ "e.querySelectorAll('#link').length", "1" },
|
||||
.{ "e.querySelectorAll('#link').item(0).id", "link" },
|
||||
.{ "e.querySelectorAll('#para').length", "1" },
|
||||
.{ "e.querySelectorAll('#para').item(0).id", "para" },
|
||||
.{ "e.querySelectorAll('*').length", "4" },
|
||||
.{ "e.querySelectorAll('p').length", "2" },
|
||||
.{ "e.querySelectorAll('.ok').item(0).id", "link" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let f = document.getElementById('content')", "undefined" },
|
||||
.{ "let ff = document.createAttribute('foo')", "undefined" },
|
||||
.{ "f.setAttributeNode(ff)", "null" },
|
||||
.{ "f.getAttributeNode('foo').name", "foo" },
|
||||
.{ "f.removeAttributeNode(ff).name", "foo" },
|
||||
.{ "f.getAttributeNode('bar')", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('para').innerHTML", " And" },
|
||||
.{ "document.getElementById('para-empty').innerHTML.trim()", "<span id=\"para-empty-child\"></span>" },
|
||||
|
||||
.{ "let h = document.getElementById('para-empty')", "undefined" },
|
||||
.{ "const prev = h.innerHTML", "undefined" },
|
||||
.{ "h.innerHTML = '<p id=\"hello\">hello world</p>'", "<p id=\"hello\">hello world</p>" },
|
||||
.{ "h.innerHTML", "<p id=\"hello\">hello world</p>" },
|
||||
.{ "h.firstChild.nodeName", "P" },
|
||||
.{ "h.firstChild.id", "hello" },
|
||||
.{ "h.firstChild.textContent", "hello world" },
|
||||
.{ "h.innerHTML = prev; true", "true" },
|
||||
.{ "document.getElementById('para-empty').innerHTML.trim()", "<span id=\"para-empty-child\"></span>" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('para').outerHTML", "<p id=\"para\"> And</p>" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('para').clientWidth", "1" },
|
||||
.{ "document.getElementById('para').clientHeight", "1" },
|
||||
|
||||
.{ "let r1 = document.getElementById('para').getBoundingClientRect()", "undefined" },
|
||||
.{ "r1.x", "0" },
|
||||
.{ "r1.y", "0" },
|
||||
.{ "r1.width", "1" },
|
||||
.{ "r1.height", "1" },
|
||||
|
||||
.{ "let r2 = document.getElementById('content').getBoundingClientRect()", "undefined" },
|
||||
.{ "r2.x", "1" },
|
||||
.{ "r2.y", "0" },
|
||||
.{ "r2.width", "1" },
|
||||
.{ "r2.height", "1" },
|
||||
|
||||
.{ "let r3 = document.getElementById('para').getBoundingClientRect()", "undefined" },
|
||||
.{ "r3.x", "0" },
|
||||
.{ "r3.y", "0" },
|
||||
.{ "r3.width", "1" },
|
||||
.{ "r3.height", "1" },
|
||||
|
||||
.{ "document.getElementById('para').clientWidth", "2" },
|
||||
.{ "document.getElementById('para').clientHeight", "1" },
|
||||
|
||||
.{ "let r4 = document.createElement('div').getBoundingClientRect()", null },
|
||||
.{ "r4.x", "0" },
|
||||
.{ "r4.y", "0" },
|
||||
.{ "r4.width", "0" },
|
||||
.{ "r4.height", "0" },
|
||||
|
||||
// Test setup causes WrongDocument or HierarchyRequest error unlike in chrome/firefox
|
||||
// .{ // An element of another document, even if created from the main document, is not rendered.
|
||||
// \\ let div5 = document.createElement('div');
|
||||
// \\ const newDoc = document.implementation.createHTMLDocument("New Document");
|
||||
// \\ newDoc.body.appendChild(div5);
|
||||
// \\ let r5 = div5.getBoundingClientRect();
|
||||
// ,
|
||||
// null,
|
||||
// },
|
||||
// .{ "r5.x", "0" },
|
||||
// .{ "r5.y", "0" },
|
||||
// .{ "r5.width", "0" },
|
||||
// .{ "r5.height", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const el = document.createElement('div');", "undefined" },
|
||||
.{ "el.id = 'matches'; el.className = 'ok';", "ok" },
|
||||
.{ "el.matches('#matches')", "true" },
|
||||
.{ "el.matches('.ok')", "true" },
|
||||
.{ "el.matches('#9000')", "false" },
|
||||
.{ "el.matches('.notok')", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const el3 = document.createElement('div');", "undefined" },
|
||||
.{ "el3.scrollIntoViewIfNeeded();", "undefined" },
|
||||
.{ "el3.scrollIntoViewIfNeeded(false);", "undefined" },
|
||||
}, .{});
|
||||
|
||||
// before
|
||||
try runner.testCases(&.{
|
||||
.{ "const before_container = document.createElement('div');", "undefined" },
|
||||
.{ "document.append(before_container);", "undefined" },
|
||||
.{ "const b1 = document.createElement('div');", "undefined" },
|
||||
.{ "before_container.append(b1);", "undefined" },
|
||||
|
||||
.{ "const b1_a = document.createElement('p');", "undefined" },
|
||||
.{ "b1.before(b1_a, 'over 9000');", "undefined" },
|
||||
.{ "before_container.innerHTML", "<p></p>over 9000<div></div>" },
|
||||
}, .{});
|
||||
|
||||
// after
|
||||
try runner.testCases(&.{
|
||||
.{ "const after_container = document.createElement('div');", "undefined" },
|
||||
.{ "document.append(after_container);", "undefined" },
|
||||
.{ "const a1 = document.createElement('div');", "undefined" },
|
||||
.{ "after_container.append(a1);", "undefined" },
|
||||
|
||||
.{ "const a1_a = document.createElement('p');", "undefined" },
|
||||
.{ "a1.after('over 9000', a1_a);", "undefined" },
|
||||
.{ "after_container.innerHTML", "<div></div>over 9000<p></p>" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var div1 = document.createElement('div');", null },
|
||||
.{ "div1.innerHTML = \" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>\"", null },
|
||||
.{ "div1.getElementsByTagName('a').length", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.createElement('a').hasAttributes()", "false" },
|
||||
.{ "var fc; (fc = document.createElement('div')).innerHTML = '<script><\\/script>'", null },
|
||||
.{ "fc.outerHTML", "<div><script></script></div>" },
|
||||
|
||||
.{ "fc; (fc = document.createElement('div')).innerHTML = '<script><\\/script><p>hello</p>'", null },
|
||||
.{ "fc.outerHTML", "<div><script></script><p>hello</p></div>" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const rm = document.createElement('div')", null },
|
||||
.{ "rm.id = 'to-remove'", null },
|
||||
.{ "document.getElementsByTagName('body')[0].appendChild(rm)", null },
|
||||
.{ "document.getElementById('to-remove') != null", "true" },
|
||||
.{ "rm.remove()", "undefined" },
|
||||
.{ "document.getElementById('to-remove') != null", "false" },
|
||||
}, .{});
|
||||
}
|
||||
272
src/browser/dom/event_target.zig
Normal file
272
src/browser/dom/event_target.zig
Normal file
@@ -0,0 +1,272 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
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,
|
||||
};
|
||||
|
||||
// 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 (try 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))) };
|
||||
},
|
||||
else => return error.MissingEventTargetType,
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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) !bool {
|
||||
return try parser.eventTargetDispatchEvent(self, event);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.EventTarget" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "new EventTarget()", "[object EventTarget]" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let para = document.getElementById('para')", "undefined" },
|
||||
// NOTE: as some event properties will change during the event dispatching phases
|
||||
// we need to copy thoses values in order to check them afterwards
|
||||
.{
|
||||
\\ var nb = 0; var evt; var phase; var cur;
|
||||
\\ function cbk(event) {
|
||||
\\ evt = event;
|
||||
\\ phase = event.eventPhase;
|
||||
\\ cur = event.currentTarget;
|
||||
\\ nb ++;
|
||||
\\ }
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "content.addEventListener('basic', cbk)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "basic" },
|
||||
.{ "phase", "2" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "para.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "0" }, // handler is not called, no capture, not the target, no bubbling
|
||||
.{ "evt === undefined", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{ "content.addEventListener('basic', cbk)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{ "content.addEventListener('basic', cbk, true)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{ "content.removeEventListener('basic', cbk)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{ "content.removeEventListener('basic', cbk, {capture: true})", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "content.addEventListener('capture', cbk, true)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('capture'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "capture" },
|
||||
.{ "phase", "2" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "para.dispatchEvent(new Event('capture'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "capture" },
|
||||
.{ "phase", "1" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "content.addEventListener('bubbles', cbk)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "bubbles" },
|
||||
.{ "evt.bubbles", "true" },
|
||||
.{ "phase", "2" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "para.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "bubbles" },
|
||||
.{ "phase", "3" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const obj1 = {calls: 0, handleEvent: function() { this.calls += 1; } };", null },
|
||||
.{ "content.addEventListener('he', obj1);", null },
|
||||
.{ "content.dispatchEvent(new Event('he'));", null },
|
||||
.{ "obj1.calls", "1" },
|
||||
|
||||
.{ "content.removeEventListener('he', obj1);", null },
|
||||
.{ "content.dispatchEvent(new Event('he'));", null },
|
||||
.{ "obj1.calls", "1" },
|
||||
}, .{});
|
||||
|
||||
// doesn't crash on null receiver
|
||||
try runner.testCases(&.{
|
||||
.{ "content.addEventListener('he2', null);", null },
|
||||
.{ "content.dispatchEvent(new Event('he2'));", null },
|
||||
}, .{});
|
||||
}
|
||||
@@ -19,19 +19,14 @@
|
||||
const std = @import("std");
|
||||
const allocPrint = std.fmt.allocPrint;
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
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,
|
||||
err: ?parser.DOMError,
|
||||
str: []const u8,
|
||||
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub const ErrorSet = parser.DOMError;
|
||||
|
||||
// static attributes
|
||||
@@ -61,8 +56,19 @@ pub const DOMException = struct {
|
||||
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, callerName: []const u8) anyerror!DOMException {
|
||||
pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) !DOMException {
|
||||
const errCast = @as(parser.DOMError, @errorCast(err));
|
||||
const errName = DOMException.name(errCast);
|
||||
const str = switch (errCast) {
|
||||
@@ -81,14 +87,52 @@ pub const DOMException = struct {
|
||||
return .{ .err = errCast, .str = str };
|
||||
}
|
||||
|
||||
fn name(err: parser.DOMError) []const u8 {
|
||||
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",
|
||||
error.StringSize => "StringSizeError", // Legacy: DOMSTRING_SIZE_ERR
|
||||
error.HierarchyRequest => "HierarchyRequestError",
|
||||
error.WrongDocument => "WrongDocumentError",
|
||||
error.InvalidCharacter => "InvalidCharacterError",
|
||||
error.NoDataAllowed => "NoDataAllowedError",
|
||||
error.NoDataAllowed => "NoDataAllowedError", // Legacy: NO_DATA_ALLOWED_ERR
|
||||
error.NoModificationAllowed => "NoModificationAllowedError",
|
||||
error.NotFound => "NotFoundError",
|
||||
error.NotSupported => "NotSupportedError",
|
||||
@@ -98,7 +142,7 @@ pub const DOMException = struct {
|
||||
error.InvalidModification => "InvalidModificationError",
|
||||
error.Namespace => "NamespaceError",
|
||||
error.InvalidAccess => "InvalidAccessError",
|
||||
error.Validation => "ValidationError",
|
||||
error.Validation => "ValidationError", // Legacy: VALIDATION_ERR
|
||||
error.TypeMismatch => "TypeMismatchError",
|
||||
error.Security => "SecurityError",
|
||||
error.Network => "NetworkError",
|
||||
@@ -120,8 +164,9 @@ pub const DOMException = struct {
|
||||
|
||||
// JS properties and methods
|
||||
|
||||
pub fn get_code(self: DOMException) u8 {
|
||||
return switch (self.err) {
|
||||
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,
|
||||
@@ -157,38 +202,63 @@ pub const DOMException = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_name(self: DOMException) []const u8 {
|
||||
pub fn get_name(self: *const DOMException) []const u8 {
|
||||
return DOMException.name(self.err);
|
||||
}
|
||||
|
||||
pub fn get_message(self: DOMException) []const u8 {
|
||||
pub fn get_message(self: *const DOMException) []const u8 {
|
||||
const errName = DOMException.name(self.err);
|
||||
return self.str[errName.len + 2 ..];
|
||||
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: DOMException) []const u8 {
|
||||
pub fn _toString(self: *const DOMException) []const u8 {
|
||||
return self.str;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Exception" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
const err = "Failed to execute 'appendChild' on 'Node': The new child element contains the parent.";
|
||||
var cases = [_]Case{
|
||||
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "let link = document.getElementById('link')", .ex = "undefined" },
|
||||
try runner.testCases(&.{
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let link = document.getElementById('link')", "undefined" },
|
||||
// HierarchyRequestError
|
||||
.{ .src = "var HierarchyRequestError; try {link.appendChild(content)} catch (error) {HierarchyRequestError = error} HierarchyRequestError.name", .ex = "HierarchyRequestError" },
|
||||
.{ .src = "HierarchyRequestError.code", .ex = "3" },
|
||||
.{ .src = "HierarchyRequestError.message", .ex = err },
|
||||
.{ .src = "HierarchyRequestError.toString()", .ex = "HierarchyRequestError: " ++ err },
|
||||
.{ .src = "HierarchyRequestError instanceof DOMException", .ex = "true" },
|
||||
.{ .src = "HierarchyRequestError instanceof Error", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &cases);
|
||||
.{
|
||||
\\ var he;
|
||||
\\ try { link.appendChild(content) } catch (error) { he = error}
|
||||
\\ he.name
|
||||
,
|
||||
"HierarchyRequestError",
|
||||
},
|
||||
.{ "he.code", "3" },
|
||||
.{ "he.message", err },
|
||||
.{ "he.toString()", "HierarchyRequestError: " ++ err },
|
||||
.{ "he instanceof DOMException", "true" },
|
||||
.{ "he instanceof Error", "true" },
|
||||
}, .{});
|
||||
|
||||
// Test DOMException constructor
|
||||
try runner.testCases(&.{
|
||||
.{ "let exc0 = new DOMException()", "undefined" },
|
||||
.{ "exc0.name", "Error" },
|
||||
.{ "exc0.code", "0" },
|
||||
.{ "exc0.message", "" },
|
||||
.{ "exc0.toString()", "Error" },
|
||||
|
||||
.{ "let exc1 = new DOMException('Sandwich malfunction')", "undefined" },
|
||||
.{ "exc1.name", "Error" },
|
||||
.{ "exc1.code", "0" },
|
||||
.{ "exc1.message", "Sandwich malfunction" },
|
||||
.{ "exc1.toString()", "Error: Sandwich malfunction" },
|
||||
|
||||
.{ "let exc2 = new DOMException('Caterpillar turned into a butterfly', 'NoModificationAllowedError')", "undefined" },
|
||||
.{ "exc2.name", "NoModificationAllowedError" },
|
||||
.{ "exc2.code", "7" },
|
||||
.{ "exc2.message", "Caterpillar turned into a butterfly" },
|
||||
.{ "exc2.toString()", "NoModificationAllowedError: Caterpillar turned into a butterfly" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -17,22 +17,14 @@
|
||||
// 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");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const utils = @import("utils.z");
|
||||
const Element = @import("element.zig").Element;
|
||||
const Union = @import("element.zig").Union;
|
||||
|
||||
const JsThis = @import("../env.zig").JsThis;
|
||||
const Walker = @import("walker.zig").Walker;
|
||||
const WalkerDepthFirst = @import("walker.zig").WalkerDepthFirst;
|
||||
const WalkerChildren = @import("walker.zig").WalkerChildren;
|
||||
const WalkerNone = @import("walker.zig").WalkerNone;
|
||||
|
||||
const Matcher = union(enum) {
|
||||
matchByName: MatchByName,
|
||||
@@ -45,25 +37,11 @@ const Matcher = union(enum) {
|
||||
|
||||
pub fn match(self: Matcher, node: *parser.Node) !bool {
|
||||
switch (self) {
|
||||
inline .matchTrue => return true,
|
||||
inline .matchFalse => return false,
|
||||
inline .matchByTagName => |case| return case.match(node),
|
||||
inline .matchByClassName => |case| return case.match(node),
|
||||
inline .matchByName => |case| return case.match(node),
|
||||
inline .matchByLinks => return MatchByLinks.match(node),
|
||||
inline .matchByAnchors => return MatchByAnchors.match(node),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: Matcher, alloc: std.mem.Allocator) void {
|
||||
switch (self) {
|
||||
inline .matchTrue => return,
|
||||
inline .matchFalse => return,
|
||||
inline .matchByTagName => |case| return case.deinit(alloc),
|
||||
inline .matchByClassName => |case| return case.deinit(alloc),
|
||||
inline .matchByName => |case| return case.deinit(alloc),
|
||||
inline .matchByLinks => return,
|
||||
inline .matchByAnchors => return,
|
||||
.matchTrue => return true,
|
||||
.matchFalse => return false,
|
||||
.matchByLinks => return MatchByLinks.match(node),
|
||||
.matchByAnchors => return MatchByAnchors.match(node),
|
||||
inline else => |m| return m.match(node),
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -74,54 +52,49 @@ pub const MatchByTagName = struct {
|
||||
tag: []const u8,
|
||||
is_wildcard: bool,
|
||||
|
||||
fn init(alloc: std.mem.Allocator, tag_name: []const u8) !MatchByTagName {
|
||||
const tag_name_alloc = try alloc.alloc(u8, tag_name.len);
|
||||
@memcpy(tag_name_alloc, tag_name);
|
||||
return MatchByTagName{
|
||||
.tag = tag_name_alloc,
|
||||
.is_wildcard = std.mem.eql(u8, tag_name, "*"),
|
||||
fn init(arena: Allocator, tag_name: []const u8) !MatchByTagName {
|
||||
if (std.mem.eql(u8, tag_name, "*")) {
|
||||
return .{ .tag = "*", .is_wildcard = true };
|
||||
}
|
||||
|
||||
return .{
|
||||
.tag = try arena.dupe(u8, 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));
|
||||
}
|
||||
|
||||
fn deinit(self: MatchByTagName, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.tag);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn HTMLCollectionByTagName(
|
||||
alloc: std.mem.Allocator,
|
||||
arena: Allocator,
|
||||
root: ?*parser.Node,
|
||||
tag_name: []const u8,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByTagName = try MatchByTagName.init(alloc, tag_name),
|
||||
},
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
|
||||
pub const MatchByClassName = struct {
|
||||
classNames: []const u8,
|
||||
class_names: []const u8,
|
||||
|
||||
fn init(alloc: std.mem.Allocator, classNames: []const u8) !MatchByClassName {
|
||||
const class_names_alloc = try alloc.alloc(u8, classNames.len);
|
||||
@memcpy(class_names_alloc, classNames);
|
||||
return MatchByClassName{
|
||||
.classNames = class_names_alloc,
|
||||
fn init(arena: Allocator, class_names: []const u8) !MatchByClassName {
|
||||
return .{
|
||||
.class_names = try arena.dupe(u8, class_names),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn match(self: MatchByClassName, node: *parser.Node) !bool {
|
||||
var it = std.mem.splitAny(u8, self.classNames, " ");
|
||||
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;
|
||||
@@ -130,24 +103,18 @@ pub const MatchByClassName = struct {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn deinit(self: MatchByClassName, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.classNames);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn HTMLCollectionByClassName(
|
||||
alloc: std.mem.Allocator,
|
||||
arena: Allocator,
|
||||
root: ?*parser.Node,
|
||||
classNames: []const u8,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByClassName = try MatchByClassName.init(alloc, classNames),
|
||||
},
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
@@ -155,11 +122,9 @@ pub fn HTMLCollectionByClassName(
|
||||
pub const MatchByName = struct {
|
||||
name: []const u8,
|
||||
|
||||
fn init(alloc: std.mem.Allocator, name: []const u8) !MatchByName {
|
||||
const names_alloc = try alloc.alloc(u8, name.len);
|
||||
@memcpy(names_alloc, name);
|
||||
return MatchByName{
|
||||
.name = names_alloc,
|
||||
fn init(arena: Allocator, name: []const u8) !MatchByName {
|
||||
return .{
|
||||
.name = try arena.dupe(u8, name),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -168,39 +133,59 @@ pub const MatchByName = struct {
|
||||
const nname = try parser.elementGetAttribute(e, "name") orelse return false;
|
||||
return std.mem.eql(u8, self.name, nname);
|
||||
}
|
||||
|
||||
fn deinit(self: MatchByName, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.name);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn HTMLCollectionByName(
|
||||
alloc: std.mem.Allocator,
|
||||
arena: Allocator,
|
||||
root: ?*parser.Node,
|
||||
name: []const u8,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByName = try MatchByName.init(alloc, name),
|
||||
},
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByName = try MatchByName.init(arena, name) },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn HTMLCollectionAll(
|
||||
root: ?*parser.Node,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{ .matchTrue = .{} },
|
||||
.include_root = 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,
|
||||
@@ -208,8 +193,8 @@ pub fn HTMLCollectionChildren(
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerChildren = .{} },
|
||||
.matcher = Matcher{ .matchTrue = .{} },
|
||||
.walker = .{ .walkerChildren = .{} },
|
||||
.matcher = .{ .matchTrue = .{} },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
@@ -217,8 +202,8 @@ pub fn HTMLCollectionChildren(
|
||||
pub fn HTMLCollectionEmpty() !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = null,
|
||||
.walker = Walker{ .walkerNone = .{} },
|
||||
.matcher = Matcher{ .matchFalse = .{} },
|
||||
.walker = .{ .walkerNone = .{} },
|
||||
.matcher = .{ .matchFalse = .{} },
|
||||
.include_root = false,
|
||||
};
|
||||
}
|
||||
@@ -243,10 +228,8 @@ pub fn HTMLCollectionByLinks(
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByLinks = MatchByLinks{},
|
||||
},
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByLinks = MatchByLinks{} },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
@@ -270,17 +253,13 @@ pub fn HTMLCollectionByAnchors(
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByAnchors = MatchByAnchors{},
|
||||
},
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByAnchors = MatchByAnchors{} },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
|
||||
pub const HTMLCollectionIterator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
coll: *HTMLCollection,
|
||||
index: u32 = 0,
|
||||
|
||||
@@ -311,8 +290,6 @@ pub const HTMLCollectionIterator = struct {
|
||||
// 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 {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
matcher: Matcher,
|
||||
walker: Walker,
|
||||
|
||||
@@ -324,15 +301,11 @@ pub const HTMLCollection = struct {
|
||||
include_root: bool = false,
|
||||
|
||||
// save a state for the collection to improve the _item speed.
|
||||
cur_idx: ?u32 = undefined,
|
||||
cur_node: ?*parser.Node = undefined,
|
||||
|
||||
// array_like_keys is used to keep reference to array like interface implementation.
|
||||
// the collection generates keys string which must be free on deinit.
|
||||
array_like_keys: std.ArrayListUnmanaged([]u8) = .{},
|
||||
cur_idx: ?u32 = null,
|
||||
cur_node: ?*parser.Node = null,
|
||||
|
||||
// start returns the first node to walk on.
|
||||
fn start(self: HTMLCollection) !?*parser.Node {
|
||||
fn start(self: *const HTMLCollection) !?*parser.Node {
|
||||
if (self.root == null) return null;
|
||||
|
||||
if (self.include_root) {
|
||||
@@ -412,7 +385,7 @@ pub const HTMLCollection = struct {
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
|
||||
pub fn _namedItem(self: *HTMLCollection, name: []const u8) !?Union {
|
||||
pub fn _namedItem(self: *const HTMLCollection, name: []const u8) !?Union {
|
||||
if (self.root == null) return null;
|
||||
if (name.len == 0) return null;
|
||||
|
||||
@@ -454,81 +427,74 @@ pub const HTMLCollection = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn postAttach(self: *HTMLCollection, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void {
|
||||
const ln = try self.get_length();
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
defer i += 1;
|
||||
const k = try std.fmt.allocPrint(alloc, "{d}", .{i});
|
||||
try self.array_like_keys.append(alloc, k);
|
||||
|
||||
const node = try self.item(i) orelse unreachable;
|
||||
pub fn postAttach(self: *HTMLCollection, js_this: JsThis) !void {
|
||||
const len = try self.get_length();
|
||||
for (0..len) |i| {
|
||||
const node = try self.item(@intCast(i)) orelse unreachable;
|
||||
const e = @as(*parser.Element, @ptrCast(node));
|
||||
try js_obj.set(k, e);
|
||||
const as_interface = try Element.toInterface(e);
|
||||
try js_this.setIndex(@intCast(i), as_interface, .{});
|
||||
|
||||
if (try item_name(e)) |name| {
|
||||
try js_obj.set(name, e);
|
||||
// Even though an entry might have an empty id, the spec says
|
||||
// that namedItem("") should always return null
|
||||
if (name.len > 0) {
|
||||
// Named fields should not be enumerable (it is defined with
|
||||
// the LegacyUnenumerableNamedProperties flag.)
|
||||
try js_this.set(name, as_interface, .{ .DONT_ENUM = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: *HTMLCollection, alloc: std.mem.Allocator) void {
|
||||
for (self.array_like_keys_) |k| alloc.free(k);
|
||||
self.array_like_keys.deinit(alloc);
|
||||
self.matcher.deinit(alloc);
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.HTMLCollection" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var getElementsByTagName = [_]Case{
|
||||
.{ .src = "let getElementsByTagName = document.getElementsByTagName('p')", .ex = "undefined" },
|
||||
.{ .src = "getElementsByTagName.length", .ex = "2" },
|
||||
.{ .src = "let getElementsByTagNameCI = document.getElementsByTagName('P')", .ex = "undefined" },
|
||||
.{ .src = "getElementsByTagNameCI.length", .ex = "2" },
|
||||
.{ .src = "getElementsByTagName.item(0).localName", .ex = "p" },
|
||||
.{ .src = "getElementsByTagName.item(1).localName", .ex = "p" },
|
||||
.{ .src = "let getElementsByTagNameAll = document.getElementsByTagName('*')", .ex = "undefined" },
|
||||
.{ .src = "getElementsByTagNameAll.length", .ex = "8" },
|
||||
.{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" },
|
||||
.{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" },
|
||||
.{ .src = "getElementsByTagNameAll.item(1).localName", .ex = "head" },
|
||||
.{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" },
|
||||
.{ .src = "getElementsByTagNameAll.item(2).localName", .ex = "body" },
|
||||
.{ .src = "getElementsByTagNameAll.item(3).localName", .ex = "div" },
|
||||
.{ .src = "getElementsByTagNameAll.item(7).localName", .ex = "p" },
|
||||
.{ .src = "getElementsByTagNameAll.namedItem('para-empty-child').localName", .ex = "span" },
|
||||
try runner.testCases(&.{
|
||||
.{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
|
||||
.{ "getElementsByTagName.length", "2" },
|
||||
.{ "let getElementsByTagNameCI = document.getElementsByTagName('P')", "undefined" },
|
||||
.{ "getElementsByTagNameCI.length", "2" },
|
||||
.{ "getElementsByTagName.item(0).localName", "p" },
|
||||
.{ "getElementsByTagName.item(1).localName", "p" },
|
||||
.{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
|
||||
.{ "getElementsByTagNameAll.length", "8" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(1).localName", "head" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(2).localName", "body" },
|
||||
.{ "getElementsByTagNameAll.item(3).localName", "div" },
|
||||
.{ "getElementsByTagNameAll.item(7).localName", "p" },
|
||||
.{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
|
||||
|
||||
// array like
|
||||
.{ .src = "getElementsByTagNameAll[0].localName", .ex = "html" },
|
||||
.{ .src = "getElementsByTagNameAll[7].localName", .ex = "p" },
|
||||
.{ .src = "getElementsByTagNameAll[8]", .ex = "undefined" },
|
||||
.{ .src = "getElementsByTagNameAll['para-empty-child'].localName", .ex = "span" },
|
||||
.{ .src = "getElementsByTagNameAll['foo']", .ex = "undefined" },
|
||||
.{ "getElementsByTagNameAll[0].localName", "html" },
|
||||
.{ "getElementsByTagNameAll[7].localName", "p" },
|
||||
.{ "getElementsByTagNameAll[8]", "undefined" },
|
||||
.{ "getElementsByTagNameAll['para-empty-child'].localName", "span" },
|
||||
.{ "getElementsByTagNameAll['foo']", "undefined" },
|
||||
|
||||
.{ .src = "document.getElementById('content').getElementsByTagName('*').length", .ex = "4" },
|
||||
.{ .src = "document.getElementById('content').getElementsByTagName('p').length", .ex = "2" },
|
||||
.{ .src = "document.getElementById('content').getElementsByTagName('div').length", .ex = "0" },
|
||||
.{ "document.getElementById('content').getElementsByTagName('*').length", "4" },
|
||||
.{ "document.getElementById('content').getElementsByTagName('p').length", "2" },
|
||||
.{ "document.getElementById('content').getElementsByTagName('div').length", "0" },
|
||||
|
||||
.{ .src = "document.children.length", .ex = "1" },
|
||||
.{ .src = "document.getElementById('content').children.length", .ex = "3" },
|
||||
.{ "document.children.length", "1" },
|
||||
.{ "document.getElementById('content').children.length", "3" },
|
||||
|
||||
// check liveness
|
||||
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "let pe = document.getElementById('para-empty')", .ex = "undefined" },
|
||||
.{ .src = "let p = document.createElement('p')", .ex = "undefined" },
|
||||
.{ .src = "p.textContent = 'OK live'", .ex = "OK live" },
|
||||
.{ .src = "getElementsByTagName.item(1).textContent", .ex = " And" },
|
||||
.{ .src = "content.appendChild(p) != undefined", .ex = "true" },
|
||||
.{ .src = "getElementsByTagName.length", .ex = "3" },
|
||||
.{ .src = "getElementsByTagName.item(2).textContent", .ex = "OK live" },
|
||||
.{ .src = "content.insertBefore(p, pe) != undefined", .ex = "true" },
|
||||
.{ .src = "getElementsByTagName.item(0).textContent", .ex = "OK live" },
|
||||
};
|
||||
try checkCases(js_env, &getElementsByTagName);
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let pe = document.getElementById('para-empty')", "undefined" },
|
||||
.{ "let p = document.createElement('p')", "undefined" },
|
||||
.{ "p.textContent = 'OK live'", "OK live" },
|
||||
.{ "getElementsByTagName.item(1).textContent", " And" },
|
||||
.{ "content.appendChild(p) != undefined", "true" },
|
||||
.{ "getElementsByTagName.length", "3" },
|
||||
.{ "getElementsByTagName.item(2).textContent", "OK live" },
|
||||
.{ "content.insertBefore(p, pe) != undefined", "true" },
|
||||
.{ "getElementsByTagName.item(0).textContent", "OK live" },
|
||||
}, .{});
|
||||
}
|
||||
72
src/browser/dom/implementation.zig
Normal file
72
src/browser/dom/implementation.zig
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Implementation" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let impl = document.implementation", "undefined" },
|
||||
.{ "impl.createHTMLDocument();", "[object HTMLDocument]" },
|
||||
.{ "const doc = impl.createHTMLDocument('foo');", "undefined" },
|
||||
.{ "doc", "[object HTMLDocument]" },
|
||||
.{ "doc.title", "foo" },
|
||||
.{ "doc.body", "[object HTMLBodyElement]" },
|
||||
.{ "impl.createDocument(null, 'foo');", "[object Document]" },
|
||||
.{ "impl.createDocumentType('foo', 'bar', 'baz')", "[object DocumentType]" },
|
||||
.{ "impl.hasFeature()", "true" },
|
||||
}, .{});
|
||||
}
|
||||
290
src/browser/dom/intersection_observer.zig
Normal file
290
src/browser/dom/intersection_observer.zig
Normal file
@@ -0,0 +1,290 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Element = @import("element.zig").Element;
|
||||
|
||||
pub const Interfaces = .{
|
||||
IntersectionObserver,
|
||||
IntersectionObserverEntry,
|
||||
};
|
||||
|
||||
// This is supposed to listen to change between the root and observation targets.
|
||||
// However, our rendered stores everything as 1 pixel sized boxes in a long row that never changes.
|
||||
// As such, there are no changes to intersections between the root and any target.
|
||||
// Instead we keep a list of all entries that are being observed.
|
||||
// The callback is called with all entries everytime a new entry is added(observed).
|
||||
// Potentially we should also call the callback at a regular interval.
|
||||
// The returned Entries are phony, they always indicate full intersection.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
|
||||
pub const IntersectionObserver = struct {
|
||||
page: *Page,
|
||||
callback: Env.Function,
|
||||
options: IntersectionObserverOptions,
|
||||
|
||||
observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry),
|
||||
|
||||
// new IntersectionObserver(callback)
|
||||
// new IntersectionObserver(callback, options) [not supported yet]
|
||||
pub fn constructor(callback: Env.Function, options_: ?IntersectionObserverOptions, page: *Page) !IntersectionObserver {
|
||||
var options = IntersectionObserverOptions{
|
||||
.root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
|
||||
.rootMargin = "0px 0px 0px 0px",
|
||||
.threshold = .{ .single = 0.0 },
|
||||
};
|
||||
if (options_) |*o| {
|
||||
if (o.root) |root| {
|
||||
options.root = root;
|
||||
} // Other properties are not used due to the way we render
|
||||
}
|
||||
|
||||
return .{
|
||||
.page = page,
|
||||
.callback = callback,
|
||||
.options = options,
|
||||
.observed_entries = .{},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _disconnect(self: *IntersectionObserver) !void {
|
||||
self.observed_entries = .{}; // We don't free as it is on an arena
|
||||
}
|
||||
|
||||
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element) !void {
|
||||
for (self.observed_entries.items) |*observer| {
|
||||
if (observer.target == target_element) {
|
||||
return; // Already observed
|
||||
}
|
||||
}
|
||||
|
||||
try self.observed_entries.append(self.page.arena, .{
|
||||
.page = self.page,
|
||||
.target = target_element,
|
||||
.options = &self.options,
|
||||
});
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.source = "intersection observer",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
||||
for (self.observed_entries.items, 0..) |*observer, index| {
|
||||
if (observer.target == target) {
|
||||
_ = self.observed_entries.swapRemove(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _takeRecords(self: *IntersectionObserver) []IntersectionObserverEntry {
|
||||
return self.observed_entries.items;
|
||||
}
|
||||
};
|
||||
|
||||
const IntersectionObserverOptions = struct {
|
||||
root: ?*parser.Node, // Element or Document
|
||||
rootMargin: ?[]const u8,
|
||||
threshold: ?Threshold,
|
||||
|
||||
const Threshold = union(enum) {
|
||||
single: f32,
|
||||
list: []const f32,
|
||||
};
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
|
||||
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
||||
pub const IntersectionObserverEntry = struct {
|
||||
page: *Page,
|
||||
target: *parser.Element,
|
||||
options: *IntersectionObserverOptions,
|
||||
|
||||
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
|
||||
pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
||||
return Element._getBoundingClientRect(self.target, self.page);
|
||||
}
|
||||
|
||||
// Returns the ratio of the intersectionRect to the boundingClientRect.
|
||||
pub fn get_intersectionRatio(_: *const IntersectionObserverEntry) f32 {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Returns a DOMRectReadOnly representing the target's visible area.
|
||||
pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
||||
return Element._getBoundingClientRect(self.target, self.page);
|
||||
}
|
||||
|
||||
// A Boolean value which is true if the target element intersects with the
|
||||
// intersection observer's root. If this is true, then, the
|
||||
// IntersectionObserverEntry describes a transition into a state of
|
||||
// intersection; if it's false, then you know the transition is from
|
||||
// intersecting to not-intersecting.
|
||||
pub fn get_isIntersecting(_: *const IntersectionObserverEntry) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns a DOMRectReadOnly for the intersection observer's root.
|
||||
pub fn get_rootBounds(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
||||
const root = self.options.root.?;
|
||||
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
|
||||
return self.page.renderer.boundingRect();
|
||||
}
|
||||
|
||||
const root_type = try parser.nodeType(root);
|
||||
|
||||
var element: *parser.Element = undefined;
|
||||
switch (root_type) {
|
||||
.element => element = parser.nodeToElement(root),
|
||||
.document => {
|
||||
const doc = parser.nodeToDocument(root);
|
||||
element = (try parser.documentGetDocumentElement(doc)).?;
|
||||
},
|
||||
else => return error.InvalidState,
|
||||
}
|
||||
|
||||
return Element._getBoundingClientRect(element, self.page);
|
||||
}
|
||||
|
||||
// The Element whose intersection with the root changed.
|
||||
pub fn get_target(self: *const IntersectionObserverEntry) *parser.Element {
|
||||
return self.target;
|
||||
}
|
||||
|
||||
// TODO: pub fn get_time(self: *const IntersectionObserverEntry)
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.IntersectionObserver" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "new IntersectionObserver(() => {}).observe(document.documentElement);", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let count_a = 0;", "undefined" },
|
||||
.{ "const a1 = document.createElement('div');", "undefined" },
|
||||
.{ "new IntersectionObserver(entries => {count_a += 1;}).observe(a1);", "undefined" },
|
||||
.{ "count_a;", "1" },
|
||||
}, .{});
|
||||
|
||||
// This test is documenting current behavior, not correct behavior.
|
||||
// Currently every time observe is called, the callback is called with all entries.
|
||||
try runner.testCases(&.{
|
||||
.{ "let count_b = 0;", "undefined" },
|
||||
.{ "let observer_b = new IntersectionObserver(entries => {count_b = entries.length;});", "undefined" },
|
||||
.{ "const b1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_b.observe(b1);", "undefined" },
|
||||
.{ "count_b;", "1" },
|
||||
.{ "const b2 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_b.observe(b2);", "undefined" },
|
||||
.{ "count_b;", "2" },
|
||||
}, .{});
|
||||
|
||||
// Re-observing is a no-op
|
||||
try runner.testCases(&.{
|
||||
.{ "let count_bb = 0;", "undefined" },
|
||||
.{ "let observer_bb = new IntersectionObserver(entries => {count_bb = entries.length;});", "undefined" },
|
||||
.{ "const bb1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_bb.observe(bb1);", "undefined" },
|
||||
.{ "count_bb;", "1" },
|
||||
.{ "observer_bb.observe(bb1);", "undefined" },
|
||||
.{ "count_bb;", "1" }, // Still 1, not 2
|
||||
}, .{});
|
||||
|
||||
// Unobserve
|
||||
try runner.testCases(&.{
|
||||
.{ "let count_c = 0;", "undefined" },
|
||||
.{ "let observer_c = new IntersectionObserver(entries => { count_c = entries.length;});", "undefined" },
|
||||
.{ "const c1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_c.observe(c1);", "undefined" },
|
||||
.{ "count_c;", "1" },
|
||||
.{ "observer_c.unobserve(c1);", "undefined" },
|
||||
.{ "const c2 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_c.observe(c2);", "undefined" },
|
||||
.{ "count_c;", "1" },
|
||||
}, .{});
|
||||
|
||||
// Disconnect
|
||||
try runner.testCases(&.{
|
||||
.{ "let observer_d = new IntersectionObserver(entries => {});", "undefined" },
|
||||
.{ "let d1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_d.observe(d1);", "undefined" },
|
||||
.{ "observer_d.disconnect();", "undefined" },
|
||||
.{ "observer_d.takeRecords().length;", "0" },
|
||||
}, .{});
|
||||
|
||||
// takeRecords
|
||||
try runner.testCases(&.{
|
||||
.{ "let observer_e = new IntersectionObserver(entries => {});", "undefined" },
|
||||
.{ "let e1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_e.observe(e1);", "undefined" },
|
||||
.{ "const e2 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_e.observe(e2);", "undefined" },
|
||||
.{ "observer_e.takeRecords().length;", "2" },
|
||||
}, .{});
|
||||
|
||||
// Entry
|
||||
try runner.testCases(&.{
|
||||
.{ "let entry;", "undefined" },
|
||||
.{ "let div1 = document.createElement('div')", null },
|
||||
.{ "document.body.appendChild(div1);", null },
|
||||
.{ "new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);", null },
|
||||
.{ "entry.boundingClientRect.x;", "0" },
|
||||
.{ "entry.intersectionRatio;", "1" },
|
||||
.{ "entry.intersectionRect.x;", "0" },
|
||||
.{ "entry.intersectionRect.y;", "0" },
|
||||
.{ "entry.intersectionRect.width;", "1" },
|
||||
.{ "entry.intersectionRect.height;", "1" },
|
||||
.{ "entry.isIntersecting;", "true" },
|
||||
.{ "entry.rootBounds.x;", "0" },
|
||||
.{ "entry.rootBounds.y;", "0" },
|
||||
.{ "entry.rootBounds.width;", "1" },
|
||||
.{ "entry.rootBounds.height;", "1" },
|
||||
.{ "entry.target;", "[object HTMLDivElement]" },
|
||||
}, .{});
|
||||
|
||||
// Options
|
||||
try runner.testCases(&.{
|
||||
.{ "const new_root = document.createElement('span');", null },
|
||||
.{ "document.body.appendChild(new_root);", null },
|
||||
.{ "let new_entry;", "undefined" },
|
||||
.{
|
||||
\\ const new_observer = new IntersectionObserver(
|
||||
\\ entries => { new_entry = entries[0]; },
|
||||
\\ {root: new_root, rootMargin: '0px 0px 0px 0px', threshold: [0]});
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "new_observer.observe(document.createElement('div'));", "undefined" },
|
||||
.{ "new_entry.rootBounds.x;", "1" },
|
||||
}, .{});
|
||||
}
|
||||
436
src/browser/dom/mutation_observer.zig
Normal file
436
src/browser/dom/mutation_observer.zig
Normal file
@@ -0,0 +1,436 @@
|
||||
// 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 Page = @import("../page.zig").Page;
|
||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
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 {
|
||||
loop: *Loop,
|
||||
cbk: Env.Function,
|
||||
arena: Allocator,
|
||||
connected: bool,
|
||||
scheduled: bool,
|
||||
loop_node: Loop.CallbackNode,
|
||||
|
||||
// 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: Env.Function, page: *Page) !MutationObserver {
|
||||
return .{
|
||||
.cbk = cbk,
|
||||
.loop = page.loop,
|
||||
.observed = .{},
|
||||
.connected = true,
|
||||
.scheduled = false,
|
||||
.arena = page.arena,
|
||||
.loop_node = .{ .func = callback },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?Options) !void {
|
||||
const arena = self.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 },
|
||||
};
|
||||
|
||||
// register node's events
|
||||
if (options.childList or options.subtree) {
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMNodeInserted",
|
||||
&observer.event_node,
|
||||
false,
|
||||
);
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMNodeRemoved",
|
||||
&observer.event_node,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (options.attr()) {
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMAttrModified",
|
||||
&observer.event_node,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (options.cdata()) {
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMCharacterDataModified",
|
||||
&observer.event_node,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (options.subtree) {
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMSubtreeModified",
|
||||
&observer.event_node,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn callback(node: *Loop.CallbackNode, _: *?u63) void {
|
||||
const self: *MutationObserver = @fieldParentPtr("loop_node", node);
|
||||
if (self.connected == false) {
|
||||
self.scheduled = true;
|
||||
return;
|
||||
}
|
||||
self.scheduled = false;
|
||||
|
||||
const records = self.observed.items;
|
||||
if (records.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
defer self.observed.clearRetainingCapacity();
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
self.cbk.tryCall(void, .{records}, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.source = "mutation observer",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _disconnect(self: *MutationObserver) !void {
|
||||
self.connected = false;
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
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 = try parser.eventTarget(event) orelse return;
|
||||
break :blk parser.eventTargetToNode(event_target);
|
||||
};
|
||||
|
||||
const mutation_event = parser.eventToMutationEvent(event);
|
||||
const event_type = blk: {
|
||||
const t = try 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.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) catch null;
|
||||
}
|
||||
},
|
||||
.DOMCharacterDataModified => {
|
||||
if (self.options.characterDataOldValue) {
|
||||
record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
|
||||
}
|
||||
},
|
||||
.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.loop.timeout(0, &mutation_observer.loop_node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "new MutationObserver(() => {}).observe(document, { childList: true });", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var nb = 0;
|
||||
\\ var mrs;
|
||||
\\ new MutationObserver((mu) => {
|
||||
\\ mrs = mu;
|
||||
\\ nb++;
|
||||
\\ }).observe(document.firstElementChild, { attributes: true, attributeOldValue: true });
|
||||
\\ document.firstElementChild.setAttribute("foo", "bar");
|
||||
\\ // ignored b/c it's about another target.
|
||||
\\ document.firstElementChild.firstChild.setAttribute("foo", "bar");
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "nb", "1" },
|
||||
.{ "mrs[0].type", "attributes" },
|
||||
.{ "mrs[0].target == document.firstElementChild", "true" },
|
||||
.{ "mrs[0].target.getAttribute('foo')", "bar" },
|
||||
.{ "mrs[0].attributeName", "foo" },
|
||||
.{ "mrs[0].oldValue", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var node = document.getElementById("para").firstChild;
|
||||
\\ var nb2 = 0;
|
||||
\\ var mrs2;
|
||||
\\ new MutationObserver((mu) => {
|
||||
\\ mrs2 = mu;
|
||||
\\ nb2++;
|
||||
\\ }).observe(node, { characterData: true, characterDataOldValue: true });
|
||||
\\ node.data = "foo";
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "nb2", "1" },
|
||||
.{ "mrs2[0].type", "characterData" },
|
||||
.{ "mrs2[0].target == node", "true" },
|
||||
.{ "mrs2[0].target.data", "foo" },
|
||||
.{ "mrs2[0].oldValue", " And" },
|
||||
}, .{});
|
||||
|
||||
// tests that mutation observers that have a callback which trigger the
|
||||
// mutation observer don't crash.
|
||||
// https://github.com/lightpanda-io/browser/issues/550
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var node = document.getElementById("para");
|
||||
\\ new MutationObserver(() => {
|
||||
\\ node.innerText = 'a';
|
||||
\\ }).observe(document, { subtree:true,childList:true });
|
||||
\\ node.innerText = "2";
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "node.innerText", "a" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var node = document.getElementById("para");
|
||||
\\ var attrWatch = 0;
|
||||
\\ new MutationObserver(() => {
|
||||
\\ attrWatch++;
|
||||
\\ }).observe(document, { attributeFilter: ["name"], subtree: true });
|
||||
\\ node.setAttribute("id", "1");
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "attrWatch", "0" },
|
||||
.{ "node.setAttribute('name', 'other');", null },
|
||||
.{ "attrWatch", "1" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -16,22 +16,16 @@
|
||||
// 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");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
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 mem_guarantied = true;
|
||||
|
||||
pub const Exception = DOMException;
|
||||
pub const Iterator = NamedNodeMapIterator;
|
||||
|
||||
// TODO implement LegacyUnenumerableNamedProperties.
|
||||
// https://webidl.spec.whatwg.org/#LegacyUnenumerableNamedProperties
|
||||
@@ -75,23 +69,72 @@ pub const NamedNodeMap = struct {
|
||||
) !*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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var setItem = [_]Case{
|
||||
.{ .src = "let a = document.getElementById('content').attributes", .ex = "undefined" },
|
||||
.{ .src = "a.length", .ex = "1" },
|
||||
.{ .src = "a.item(0)", .ex = "[object Attr]" },
|
||||
.{ .src = "a.item(1)", .ex = "null" },
|
||||
.{ .src = "a.getNamedItem('id')", .ex = "[object Attr]" },
|
||||
.{ .src = "a.getNamedItem('foo')", .ex = "null" },
|
||||
.{ .src = "a.setNamedItem(a.getNamedItem('id'))", .ex = "[object Attr]" },
|
||||
};
|
||||
try checkCases(js_env, &setItem);
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NamedNodeMap" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.getElementById('content').attributes", "undefined" },
|
||||
.{ "a.length", "1" },
|
||||
.{ "a.item(0)", "[object Attr]" },
|
||||
.{ "a.item(1)", "null" },
|
||||
.{ "a.getNamedItem('id')", "[object Attr]" },
|
||||
.{ "a.getNamedItem('foo')", "null" },
|
||||
.{ "a.setNamedItem(a.getNamedItem('id'))", "[object Attr]" },
|
||||
.{ "a['id'].name", "id" },
|
||||
.{ "a['id'].value", "content" },
|
||||
.{ "a['other']", "undefined" },
|
||||
.{ "a[0].value = 'abc123'", null },
|
||||
.{ "a[0].value", "abc123" },
|
||||
}, .{});
|
||||
}
|
||||
722
src/browser/dom/node.zig
Normal file
722
src/browser/dom/node.zig
Normal file
@@ -0,0 +1,722 @@
|
||||
// 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("../../runtime/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 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 Walker = @import("walker.zig").WalkerDepthFirst;
|
||||
|
||||
// HTML
|
||||
const HTML = @import("../html/html.zig");
|
||||
const HTMLElem = @import("../html/elements.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 (try parser.nodeType(node)) {
|
||||
.element => try HTMLElem.toInterface(
|
||||
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 => .{ .HTMLDocument = @as(*parser.DocumentHTML, @ptrCast(node)) },
|
||||
.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);
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
// Read-only attributes
|
||||
|
||||
pub fn get_firstChild(self: *parser.Node) !?Union {
|
||||
const res = try parser.nodeFirstChild(self);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try Node.toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_lastChild(self: *parser.Node) !?Union {
|
||||
const res = try parser.nodeLastChild(self);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try Node.toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_nextSibling(self: *parser.Node) !?Union {
|
||||
const res = try parser.nodeNextSibling(self);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try Node.toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_previousSibling(self: *parser.Node) !?Union {
|
||||
const res = try parser.nodePreviousSibling(self);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try Node.toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_parentNode(self: *parser.Node) !?Union {
|
||||
const res = try parser.nodeParentNode(self);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try Node.toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_parentElement(self: *parser.Node) !?HTMLElem.Union {
|
||||
const res = try parser.nodeParentElement(self);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, @as(*parser.Element, @ptrCast(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(try parser.nodeType(self));
|
||||
}
|
||||
|
||||
pub fn get_ownerDocument(self: *parser.Node) !?*parser.DocumentHTML {
|
||||
const res = try parser.nodeOwnerDocument(self);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return @as(*parser.DocumentHTML, @ptrCast(res.?));
|
||||
}
|
||||
|
||||
pub fn get_isConnected(self: *parser.Node) !bool {
|
||||
// TODO: handle Shadow DOM
|
||||
if (try parser.nodeType(self) == .document) {
|
||||
return true;
|
||||
}
|
||||
return try Node.get_parentNode(self) != null;
|
||||
}
|
||||
|
||||
// Read/Write attributes
|
||||
|
||||
pub fn get_nodeValue(self: *parser.Node) !?[]const u8 {
|
||||
return try 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 try 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 {
|
||||
// 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 = try parser.nodeOwnerDocument(self);
|
||||
const docother = try parser.nodeOwnerDocument(other);
|
||||
|
||||
// Both are in different document.
|
||||
if (docself == null or docother == null or docother.? != docself.?) {
|
||||
return @intFromEnum(parser.DocumentPosition.disconnected);
|
||||
}
|
||||
|
||||
// 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 try 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
|
||||
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }) !Union {
|
||||
if (options) |options_| if (options_.composed) {
|
||||
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
|
||||
};
|
||||
return try Node.toInterface(try parser.nodeGetRootNode(self));
|
||||
}
|
||||
|
||||
pub fn _hasChildNodes(self: *parser.Node) !bool {
|
||||
return try parser.nodeHasChildNodes(self);
|
||||
}
|
||||
|
||||
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
|
||||
const allocator = page.arena;
|
||||
var list: NodeList = .{};
|
||||
|
||||
var n = try parser.nodeFirstChild(self) orelse return list;
|
||||
while (true) {
|
||||
try list.append(allocator, n);
|
||||
n = try parser.nodeNextSibling(n) orelse return list;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _insertBefore(self: *parser.Node, new_node: *parser.Node, ref_node_: ?*parser.Node) !Union {
|
||||
if (ref_node_) |ref_node| {
|
||||
return Node.toInterface(try parser.nodeInsertBefore(self, new_node, ref_node));
|
||||
}
|
||||
return _appendChild(self, new_node);
|
||||
}
|
||||
|
||||
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
|
||||
return try 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 try 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 = (try parser.nodeOwnerDocument(self)) orelse return;
|
||||
|
||||
if (try 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 = (try 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 = (try 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 (!try parser.nodeHasChildNodes(self)) return;
|
||||
|
||||
const children = try parser.nodeGetChildNodes(self);
|
||||
const ln = try 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 = try parser.nodeListItem(children, 0) orelse continue;
|
||||
_ = try parser.nodeRemoveChild(self, child);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn before(self: *parser.Node, nodes: []const NodeOrText) !void {
|
||||
const parent = try parser.nodeParentNode(self) orelse return;
|
||||
const doc = (try 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 = try parser.nodePreviousSibling(s);
|
||||
continue :CHECK;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (sibling == null) {
|
||||
sibling = try 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 = try parser.nodeParentNode(self) orelse return;
|
||||
const doc = (try parser.nodeOwnerDocument(parent)) orelse return;
|
||||
|
||||
// have to find the first sibling that isn't in nodes
|
||||
var sibling = try parser.nodeNextSibling(self);
|
||||
CHECK: while (sibling) |s| {
|
||||
for (nodes) |n| {
|
||||
if (n.is(s)) {
|
||||
sibling = try 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| @alignCast(@ptrCast(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" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
{
|
||||
var err_out: ?[]const u8 = null;
|
||||
try runner.exec(
|
||||
\\ function trimAndReplace(str) {
|
||||
\\ str = str.replace(/(\r\n|\n|\r)/gm,'');
|
||||
\\ str = str.replace(/\s+/g, ' ');
|
||||
\\ str = str.trim();
|
||||
\\ return str;
|
||||
\\ }
|
||||
, "trimAndReplace", &err_out);
|
||||
}
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.body.compareDocumentPosition(document.firstChild); ", "10" },
|
||||
.{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", "10" },
|
||||
.{ "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "20" },
|
||||
.{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", "0" },
|
||||
.{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", "2" },
|
||||
.{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "4" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('content').getRootNode().__proto__.constructor.name", "HTMLDocument" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
// for next test cases
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let link = document.getElementById('link')", "undefined" },
|
||||
.{ "let first_child = content.firstChild.nextSibling", "undefined" }, // nextSibling because of line return \n
|
||||
|
||||
.{ "let body_first_child = document.body.firstChild", "undefined" },
|
||||
.{ "body_first_child.localName", "div" },
|
||||
.{ "body_first_child.__proto__.constructor.name", "HTMLDivElement" },
|
||||
.{ "document.getElementById('para-empty').firstChild.firstChild", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let last_child = content.lastChild.previousSibling", "undefined" }, // previousSibling because of line return \n
|
||||
.{ "last_child.__proto__.constructor.name", "Comment" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let next_sibling = link.nextSibling.nextSibling", "undefined" },
|
||||
.{ "next_sibling.localName", "p" },
|
||||
.{ "next_sibling.__proto__.constructor.name", "HTMLParagraphElement" },
|
||||
.{ "content.nextSibling.nextSibling", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let prev_sibling = document.getElementById('para-empty').previousSibling.previousSibling", "undefined" },
|
||||
.{ "prev_sibling.localName", "a" },
|
||||
.{ "prev_sibling.__proto__.constructor.name", "HTMLAnchorElement" },
|
||||
.{ "content.previousSibling", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let parent = document.getElementById('para').parentElement", "undefined" },
|
||||
.{ "parent.localName", "div" },
|
||||
.{ "parent.__proto__.constructor.name", "HTMLDivElement" },
|
||||
.{ "let h = content.parentElement.parentElement", "undefined" },
|
||||
.{ "h.parentElement", "null" },
|
||||
.{ "h.parentNode.__proto__.constructor.name", "HTMLDocument" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "first_child.nodeName === 'A'", "true" },
|
||||
.{ "link.firstChild.nodeName === '#text'", "true" },
|
||||
.{ "last_child.nodeName === '#comment'", "true" },
|
||||
.{ "document.nodeName === '#document'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "first_child.nodeType === 1", "true" },
|
||||
.{ "link.firstChild.nodeType === 3", "true" },
|
||||
.{ "last_child.nodeType === 8", "true" },
|
||||
.{ "document.nodeType === 9", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let owner = content.ownerDocument", "undefined" },
|
||||
.{ "owner.__proto__.constructor.name", "HTMLDocument" },
|
||||
.{ "document.ownerDocument", "null" },
|
||||
.{ "let owner2 = document.createElement('div').ownerDocument", "undefined" },
|
||||
.{ "owner2.__proto__.constructor.name", "HTMLDocument" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "content.isConnected", "true" },
|
||||
.{ "document.isConnected", "true" },
|
||||
.{ "document.createElement('div').isConnected", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "last_child.nodeValue === 'comment'", "true" },
|
||||
.{ "link.nodeValue === null", "true" },
|
||||
.{ "let text = link.firstChild", "undefined" },
|
||||
.{ "text.nodeValue === 'OK'", "true" },
|
||||
.{ "text.nodeValue = 'OK modified'", "OK modified" },
|
||||
.{ "text.nodeValue === 'OK modified'", "true" },
|
||||
.{ "link.nodeValue = 'nothing'", "nothing" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "text.textContent === 'OK modified'", "true" },
|
||||
.{ "trimAndReplace(content.textContent) === 'OK modified And'", "true" },
|
||||
.{ "text.textContent = 'OK'", "OK" },
|
||||
.{ "text.textContent", "OK" },
|
||||
.{ "trimAndReplace(document.getElementById('para-empty').textContent)", "" },
|
||||
.{ "document.getElementById('para-empty').textContent = 'OK'", "OK" },
|
||||
.{ "document.getElementById('para-empty').firstChild.nodeName === '#text'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let append = document.createElement('h1')", "undefined" },
|
||||
.{ "content.appendChild(append).toString()", "[object HTMLHeadingElement]" },
|
||||
.{ "content.lastChild.__proto__.constructor.name", "HTMLHeadingElement" },
|
||||
.{ "content.appendChild(link).toString()", "[object HTMLAnchorElement]" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let clone = link.cloneNode()", "undefined" },
|
||||
.{ "clone.toString()", "[object HTMLAnchorElement]" },
|
||||
.{ "clone.parentNode === null", "true" },
|
||||
.{ "clone.firstChild === null", "true" },
|
||||
.{ "let clone_deep = link.cloneNode(true)", "undefined" },
|
||||
.{ "clone_deep.firstChild.nodeName === '#text'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "link.contains(text)", "true" },
|
||||
.{ "text.contains(link)", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "link.hasChildNodes()", "true" },
|
||||
.{ "text.hasChildNodes()", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "link.childNodes.length", "1" },
|
||||
.{ "text.childNodes.length", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let insertBefore = document.createElement('a')", "undefined" },
|
||||
.{ "link.insertBefore(insertBefore, text) !== undefined", "true" },
|
||||
.{ "link.firstChild.localName === 'a'", "true" },
|
||||
|
||||
.{ "let insertBefore2 = document.createElement('b')", null },
|
||||
.{ "link.insertBefore(insertBefore2, null).localName", "b" },
|
||||
.{ "link.childNodes[link.childNodes.length - 1].localName", "b" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
// TODO: does not seems to work
|
||||
// .{ "link.isDefaultNamespace('')", "true" },
|
||||
.{ "link.isDefaultNamespace('false')", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let equal1 = document.createElement('a')", "undefined" },
|
||||
.{ "let equal2 = document.createElement('a')", "undefined" },
|
||||
.{ "equal1.textContent = 'is equal'", "is equal" },
|
||||
.{ "equal2.textContent = 'is equal'", "is equal" },
|
||||
// TODO: does not seems to work
|
||||
// .{ "equal1.isEqualNode(equal2)", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.body.isSameNode(document.body)", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
// TODO: no test
|
||||
.{ "link.normalize()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "content.removeChild(append) !== undefined", "true" },
|
||||
.{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let replace = document.createElement('div')", "undefined" },
|
||||
.{ "link.replaceChild(replace, insertBefore) !== undefined", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "Node.ELEMENT_NODE", "1" },
|
||||
.{ "Node.ATTRIBUTE_NODE", "2" },
|
||||
.{ "Node.TEXT_NODE", "3" },
|
||||
.{ "Node.CDATA_SECTION_NODE", "4" },
|
||||
.{ "Node.PROCESSING_INSTRUCTION_NODE", "7" },
|
||||
.{ "Node.COMMENT_NODE", "8" },
|
||||
.{ "Node.DOCUMENT_NODE", "9" },
|
||||
.{ "Node.DOCUMENT_TYPE_NODE", "10" },
|
||||
.{ "Node.DOCUMENT_FRAGMENT_NODE", "11" },
|
||||
.{ "Node.ENTITY_REFERENCE_NODE", "5" },
|
||||
.{ "Node.ENTITY_NODE", "6" },
|
||||
.{ "Node.NOTATION_NODE", "12" },
|
||||
}, .{});
|
||||
}
|
||||
89
src/browser/dom/node_filter.zig
Normal file
89
src/browser/dom/node_filter.zig
Normal file
@@ -0,0 +1,89 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
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: ?Env.Function, node: *parser.Node) !VerifyResult {
|
||||
const node_type = try parser.nodeType(node);
|
||||
|
||||
// Verify that we can show this node type.
|
||||
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 .reject;
|
||||
|
||||
// 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" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "NodeFilter.FILTER_ACCEPT", "1" },
|
||||
.{ "NodeFilter.FILTER_REJECT", "2" },
|
||||
.{ "NodeFilter.FILTER_SKIP", "3" },
|
||||
.{ "NodeFilter.SHOW_ALL", "4294967295" },
|
||||
.{ "NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT", "129" },
|
||||
}, .{});
|
||||
}
|
||||
290
src/browser/dom/node_iterator.zig
Normal file
290
src/browser/dom/node_iterator.zig
Normal file
@@ -0,0 +1,290 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
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/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 {
|
||||
root: *parser.Node,
|
||||
reference_node: *parser.Node,
|
||||
what_to_show: u32,
|
||||
filter: ?NodeIteratorOpts,
|
||||
filter_func: ?Env.Function,
|
||||
|
||||
pointer_before_current: bool = true,
|
||||
|
||||
pub const NodeIteratorOpts = union(enum) {
|
||||
function: Env.Function,
|
||||
object: struct { acceptNode: Env.Function },
|
||||
};
|
||||
|
||||
pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?NodeIteratorOpts) !NodeIterator {
|
||||
var filter_func: ?Env.Function = null;
|
||||
if (filter) |f| {
|
||||
filter_func = switch (f) {
|
||||
.function => |func| func,
|
||||
.object => |o| o.acceptNode,
|
||||
};
|
||||
}
|
||||
|
||||
return .{
|
||||
.root = node,
|
||||
.reference_node = node,
|
||||
.what_to_show = what_to_show orelse NodeFilter.NodeFilter._SHOW_ALL,
|
||||
.filter = filter,
|
||||
.filter_func = filter_func,
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
if (self.pointer_before_current) { // Unlike TreeWalker, NodeIterator starts at the first node
|
||||
self.pointer_before_current = false;
|
||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
||||
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) {
|
||||
if (try self.nextSibling(current)) |sibling| {
|
||||
self.reference_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
}
|
||||
|
||||
current = (try parser.nodeParentNode(current)) orelse break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _previousNode(self: *NodeIterator) !?NodeUnion {
|
||||
if (!self.pointer_before_current) {
|
||||
self.pointer_before_current = true;
|
||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
||||
return try Node.toInterface(self.reference_node); // Still need to verify as last may be first as well
|
||||
}
|
||||
}
|
||||
if (self.reference_node == self.root) return null;
|
||||
|
||||
var current = self.reference_node;
|
||||
while (try 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;
|
||||
}
|
||||
|
||||
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
const child_count = try parser.nodeListLength(children);
|
||||
|
||||
for (0..child_count) |i| {
|
||||
const index: u32 = @intCast(i);
|
||||
const child = (try 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 = try parser.nodeListLength(children);
|
||||
|
||||
var index: u32 = child_count;
|
||||
while (index > 0) {
|
||||
index -= 1;
|
||||
const child = (try 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 = (try 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 = (try 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;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NodeFilter" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ const nodeIterator = document.createNodeIterator(
|
||||
\\ document.body,
|
||||
\\ NodeFilter.SHOW_ELEMENT,
|
||||
\\ {
|
||||
\\ acceptNode(node) {
|
||||
\\ return NodeFilter.FILTER_ACCEPT;
|
||||
\\ },
|
||||
\\ },
|
||||
\\ );
|
||||
\\ nodeIterator.nextNode().nodeName;
|
||||
,
|
||||
"BODY",
|
||||
},
|
||||
.{ "nodeIterator.nextNode().nodeName", "DIV" },
|
||||
.{ "nodeIterator.nextNode().nodeName", "A" },
|
||||
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
|
||||
.{ "nodeIterator.nextNode().nodeName", "A" }, // pointer_before_current flips
|
||||
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
|
||||
.{ "nodeIterator.previousNode().nodeName", "DIV" },
|
||||
.{ "nodeIterator.previousNode().nodeName", "BODY" },
|
||||
.{ "nodeIterator.previousNode()", "null" }, // Not HEAD since body is root
|
||||
.{ "nodeIterator.previousNode()", "null" }, // Keeps returning null
|
||||
.{ "nodeIterator.nextNode().nodeName", "BODY" },
|
||||
|
||||
.{ "nodeIterator.nextNode().nodeName", null },
|
||||
.{ "nodeIterator.nextNode().nodeName", null },
|
||||
.{ "nodeIterator.nextNode().nodeName", null },
|
||||
.{ "nodeIterator.nextNode().nodeName", "SPAN" },
|
||||
.{ "nodeIterator.nextNode().nodeName", "P" },
|
||||
.{ "nodeIterator.nextNode()", "null" }, // Just the last one
|
||||
.{ "nodeIterator.nextNode()", "null" }, // Keeps returning null
|
||||
.{ "nodeIterator.previousNode().nodeName", "P" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ const notationIterator = document.createNodeIterator(
|
||||
\\ document.body,
|
||||
\\ NodeFilter.SHOW_NOTATION,
|
||||
\\ );
|
||||
\\ notationIterator.nextNode();
|
||||
,
|
||||
"null",
|
||||
},
|
||||
.{ "notationIterator.previousNode()", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nodeIterator.filter.acceptNode(document.body)", "1" },
|
||||
.{ "notationIterator.filter", "null" },
|
||||
.{
|
||||
\\ const rejectIterator = document.createNodeIterator(
|
||||
\\ document.body,
|
||||
\\ NodeFilter.SHOW_ALL,
|
||||
\\ (e => { return NodeFilter.FILTER_REJECT}),
|
||||
\\ );
|
||||
\\ rejectIterator.filter(document.body);
|
||||
,
|
||||
"2",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,32 +18,25 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Callback = jsruntime.Callback;
|
||||
const CallbackResult = jsruntime.CallbackResult;
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
const JsThis = @import("../env.zig").JsThis;
|
||||
const Function = @import("../env.zig").Function;
|
||||
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
const U32Iterator = @import("../iterator/iterator.zig").U32Iterator;
|
||||
|
||||
const log = std.log.scoped(.nodelist);
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
pub const Interfaces = .{
|
||||
NodeListIterator,
|
||||
NodeList,
|
||||
});
|
||||
};
|
||||
|
||||
pub const NodeListIterator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
coll: *NodeList,
|
||||
index: u32 = 0,
|
||||
|
||||
@@ -70,8 +63,6 @@ pub const NodeListIterator = struct {
|
||||
};
|
||||
|
||||
pub const NodeListEntriesIterator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
coll: *NodeList,
|
||||
index: u32 = 0,
|
||||
|
||||
@@ -105,18 +96,10 @@ pub const NodeListEntriesIterator = struct {
|
||||
// implementation allows only static nodelist.
|
||||
// see https://dom.spec.whatwg.org/#old-style-collections
|
||||
pub const NodeList = struct {
|
||||
pub const mem_guarantied = true;
|
||||
pub const Exception = DOMException;
|
||||
|
||||
const NodesArrayList = std.ArrayListUnmanaged(*parser.Node);
|
||||
|
||||
nodes: NodesArrayList,
|
||||
|
||||
pub fn init() NodeList {
|
||||
return NodeList{
|
||||
.nodes = NodesArrayList{},
|
||||
};
|
||||
}
|
||||
nodes: NodesArrayList = .{},
|
||||
|
||||
pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void {
|
||||
// TODO unref all nodes
|
||||
@@ -131,7 +114,7 @@ pub const NodeList = struct {
|
||||
return @intCast(self.nodes.items.len);
|
||||
}
|
||||
|
||||
pub fn _item(self: *NodeList, index: u32) !?NodeUnion {
|
||||
pub fn _item(self: *const NodeList, index: u32) !?NodeUnion {
|
||||
if (index >= self.nodes.items.len) {
|
||||
return null;
|
||||
}
|
||||
@@ -140,17 +123,29 @@ pub const NodeList = struct {
|
||||
return try Node.toInterface(n);
|
||||
}
|
||||
|
||||
pub fn _forEach(self: *NodeList, alloc: std.mem.Allocator, cbk: Callback) !void { // TODO handle thisArg
|
||||
var res = CallbackResult.init(alloc);
|
||||
defer res.deinit();
|
||||
// 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: Function) !void { // TODO handle thisArg
|
||||
for (self.nodes.items, 0..) |n, i| {
|
||||
const ii: u32 = @intCast(i);
|
||||
cbk.trycall(.{ n, ii, self }, &res) catch |e| {
|
||||
log.err("callback error: {s}", .{res.result orelse "unknown"});
|
||||
log.debug("{s}", .{res.stack orelse "no stack trace"});
|
||||
|
||||
return e;
|
||||
var result: Function.Result = undefined;
|
||||
cbk.tryCall(void, .{ n, ii, self }, &result) catch {
|
||||
log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack });
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -172,38 +167,32 @@ pub const NodeList = struct {
|
||||
}
|
||||
|
||||
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
|
||||
|
||||
pub fn postAttach(self: *NodeList, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void {
|
||||
const ln = self.get_length();
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
defer i += 1;
|
||||
const k = try std.fmt.allocPrint(alloc, "{d}", .{i});
|
||||
|
||||
const node = try self._item(i) orelse unreachable;
|
||||
try js_obj.set(k, node);
|
||||
pub fn postAttach(self: *NodeList, js_this: JsThis) !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, .{});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NodeList" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var childnodes = [_]Case{
|
||||
.{ .src = "let list = document.getElementById('content').childNodes", .ex = "undefined" },
|
||||
.{ .src = "list.length", .ex = "9" },
|
||||
.{ .src = "list[0].__proto__.constructor.name", .ex = "Text" },
|
||||
.{ .src =
|
||||
\\let i = 0;
|
||||
\\list.forEach(function (n, idx) {
|
||||
\\ i += idx;
|
||||
\\});
|
||||
\\i;
|
||||
, .ex = "36" },
|
||||
};
|
||||
try checkCases(js_env, &childnodes);
|
||||
try runner.testCases(&.{
|
||||
.{ "let list = document.getElementById('content').childNodes", "undefined" },
|
||||
.{ "list.length", "9" },
|
||||
.{ "list[0].__proto__.constructor.name", "Text" },
|
||||
.{
|
||||
\\ let i = 0;
|
||||
\\ list.forEach(function (n, idx) {
|
||||
\\ i += idx;
|
||||
\\ });
|
||||
\\ i;
|
||||
,
|
||||
"36",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
214
src/browser/dom/performance.zig
Normal file
214
src/browser/dom/performance.zig
Normal file
@@ -0,0 +1,214 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
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: std.time.Timer,
|
||||
// if (Window.crossOriginIsolated) -> Resolution in isolated contexts: 5 microseconds
|
||||
// else -> Resolution in non-isolated contexts: 100 microseconds
|
||||
const ms_resolution = 100;
|
||||
|
||||
fn limitedResolutionMs(nanoseconds: u64) f64 {
|
||||
const elapsed_at_resolution = ((nanoseconds / std.time.ns_per_us) + ms_resolution / 2) / ms_resolution * ms_resolution;
|
||||
const elapsed = @as(f64, @floatFromInt(elapsed_at_resolution));
|
||||
return elapsed / @as(f64, std.time.us_per_ms);
|
||||
}
|
||||
|
||||
pub fn get_timeOrigin(self: *const Performance) f64 {
|
||||
const is_posix = switch (@import("builtin").os.tag) { // From std.time.zig L125
|
||||
.windows, .uefi, .wasi => false,
|
||||
else => true,
|
||||
};
|
||||
const zero = std.time.Instant{ .timestamp = if (!is_posix) 0 else .{ .sec = 0, .nsec = 0 } };
|
||||
const started = self.time_origin.started.since(zero);
|
||||
return limitedResolutionMs(started);
|
||||
}
|
||||
|
||||
pub fn _now(self: *Performance) f64 {
|
||||
return limitedResolutionMs(self.time_origin.read());
|
||||
}
|
||||
|
||||
pub fn _mark(_: *Performance, name: []const u8, _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;
|
||||
}
|
||||
};
|
||||
|
||||
// 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: ?Env.JsObject,
|
||||
|
||||
const Options = struct {
|
||||
detail: ?Env.JsObject = null,
|
||||
start_time: ?f64 = null,
|
||||
};
|
||||
|
||||
pub fn constructor(name: []const u8, _options: ?Options, page: *Page) !PerformanceMark {
|
||||
const perf = &page.window.performance;
|
||||
|
||||
const options = _options orelse Options{};
|
||||
const start_time = options.start_time orelse perf._now();
|
||||
|
||||
if (start_time < 0.0) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const detail = if (options.detail) |d| try d.persist() else null;
|
||||
|
||||
const duped_name = try page.arena.dupe(u8, name);
|
||||
const proto = PerformanceEntry{ .name = duped_name, .entry_type = .mark, .start_time = start_time };
|
||||
|
||||
return .{ .proto = proto, .detail = detail };
|
||||
}
|
||||
|
||||
pub fn get_detail(self: *const PerformanceMark) ?Env.JsObject {
|
||||
return self.detail;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("./../../testing.zig");
|
||||
|
||||
test "Performance: get_timeOrigin" {
|
||||
var perf = Performance{ .time_origin = try std.time.Timer.start() };
|
||||
const time_origin = perf.get_timeOrigin();
|
||||
try testing.expect(time_origin >= 0);
|
||||
|
||||
// Check resolution
|
||||
try testing.expectDelta(@rem(time_origin * std.time.us_per_ms, 100.0), 0.0, 0.2);
|
||||
}
|
||||
|
||||
test "Performance: now" {
|
||||
var perf = Performance{ .time_origin = try std.time.Timer.start() };
|
||||
|
||||
// Monotonically increasing
|
||||
var now = perf._now();
|
||||
while (now <= 0) { // Loop for now to not be 0
|
||||
try testing.expectEqual(now, 0);
|
||||
now = perf._now();
|
||||
}
|
||||
// Check resolution
|
||||
try testing.expectDelta(@rem(now * std.time.us_per_ms, 100.0), 0.0, 0.1);
|
||||
|
||||
var after = perf._now();
|
||||
while (after <= now) { // Loop untill after > now
|
||||
try testing.expectEqual(after, now);
|
||||
after = perf._now();
|
||||
}
|
||||
// Check resolution
|
||||
try testing.expectDelta(@rem(after * std.time.us_per_ms, 100.0), 0.0, 0.1);
|
||||
}
|
||||
|
||||
test "Browser.Performance.Mark" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let performance = window.performance", "undefined" },
|
||||
.{ "performance instanceof Performance", "true" },
|
||||
.{ "let mark = performance.mark(\"start\")", "undefined" },
|
||||
.{ "mark instanceof PerformanceMark", "true" },
|
||||
.{ "mark.name", "start" },
|
||||
.{ "mark.entryType", "mark" },
|
||||
.{ "mark.duration", "0" },
|
||||
.{ "mark.detail", "null" },
|
||||
}, .{});
|
||||
}
|
||||
63
src/browser/dom/performance_observer.zig
Normal file
63
src/browser/dom/performance_observer.zig
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
|
||||
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: Env.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" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "PerformanceObserver.supportedEntryTypes.length", "0" },
|
||||
}, .{});
|
||||
}
|
||||
116
src/browser/dom/processing_instruction.zig
Normal file
116
src/browser/dom/processing_instruction.zig
Normal file
@@ -0,0 +1,116 @@
|
||||
// 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 try 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 (try 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" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" },
|
||||
.{ "pi.target", "foo" },
|
||||
.{ "pi.data", "bar" },
|
||||
.{ "pi.data = 'foo'", "foo" },
|
||||
.{ "pi.data", "foo" },
|
||||
|
||||
.{ "let pi2 = pi.cloneNode()", "undefined" },
|
||||
.{ "pi2.nodeType", "7" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let pi11 = document.createProcessingInstruction('target1', 'data1');", "undefined" },
|
||||
.{ "let pi12 = document.createProcessingInstruction('target2', 'data2');", "undefined" },
|
||||
.{ "let pi13 = document.createProcessingInstruction('target1', 'data1');", "undefined" },
|
||||
.{ "pi11.isEqualNode(pi11)", "true" },
|
||||
.{ "pi11.isEqualNode(pi13)", "true" },
|
||||
.{ "pi11.isEqualNode(pi12)", "false" },
|
||||
.{ "pi12.isEqualNode(pi13)", "false" },
|
||||
.{ "pi11.isEqualNode(document)", "false" },
|
||||
.{ "document.isEqualNode(pi11)", "false" },
|
||||
}, .{});
|
||||
}
|
||||
178
src/browser/dom/range.zig
Normal file
178
src/browser/dom/range.zig
Normal file
@@ -0,0 +1,178 @@
|
||||
// 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 NodeUnion = @import("node.zig").Union;
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
pub const Interfaces = .{
|
||||
AbstractRange,
|
||||
Range,
|
||||
};
|
||||
|
||||
pub const AbstractRange = struct {
|
||||
collapsed: bool,
|
||||
end_container: *parser.Node,
|
||||
end_offset: i32,
|
||||
start_container: *parser.Node,
|
||||
start_offset: i32,
|
||||
|
||||
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_container);
|
||||
}
|
||||
|
||||
pub fn get_endOffset(self: *const AbstractRange) i32 {
|
||||
return self.end_offset;
|
||||
}
|
||||
|
||||
pub fn get_startContainer(self: *const AbstractRange) !NodeUnion {
|
||||
return Node.toInterface(self.start_container);
|
||||
}
|
||||
|
||||
pub fn get_startOffset(self: *const AbstractRange) i32 {
|
||||
return self.start_offset;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Range = struct {
|
||||
pub const prototype = *AbstractRange;
|
||||
|
||||
proto: AbstractRange,
|
||||
|
||||
// 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_container = parser.documentHTMLToNode(page.window.document),
|
||||
.end_offset = 0,
|
||||
.start_container = parser.documentHTMLToNode(page.window.document),
|
||||
.start_offset = 0,
|
||||
};
|
||||
|
||||
return .{ .proto = proto };
|
||||
}
|
||||
|
||||
pub fn _setStart(self: *Range, node: *parser.Node, offset: i32) void {
|
||||
self.proto.start_container = node;
|
||||
self.proto.start_offset = offset;
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
pub fn _setEnd(self: *Range, node: *parser.Node, offset: i32) void {
|
||||
self.proto.end_container = node;
|
||||
self.proto.end_offset = offset;
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
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_container = node;
|
||||
self.proto.start_offset = 0;
|
||||
self.proto.end_container = node;
|
||||
|
||||
// Set end_offset
|
||||
switch (try parser.nodeType(node)) {
|
||||
.text, .cdata_section, .comment, .processing_instruction => {
|
||||
// For text-like nodes, end_offset should be the length of the text data
|
||||
if (try 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 = try parser.nodeListLength(child_nodes);
|
||||
self.proto.end_offset = @intCast(child_count);
|
||||
},
|
||||
}
|
||||
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
// 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 {}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Range" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
// Test Range constructor
|
||||
.{ "let range = new Range()", "undefined" },
|
||||
.{ "range instanceof Range", "true" },
|
||||
.{ "range instanceof AbstractRange", "true" },
|
||||
|
||||
// Test initial state - collapsed range
|
||||
.{ "range.collapsed", "true" },
|
||||
.{ "range.startOffset", "0" },
|
||||
.{ "range.endOffset", "0" },
|
||||
.{ "range.startContainer instanceof HTMLDocument", "true" },
|
||||
.{ "range.endContainer instanceof HTMLDocument", "true" },
|
||||
|
||||
// Test document.createRange()
|
||||
.{ "let docRange = document.createRange()", "undefined" },
|
||||
.{ "docRange instanceof Range", "true" },
|
||||
.{ "docRange.collapsed", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const container = document.getElementById('content');", null },
|
||||
|
||||
// Test text range
|
||||
.{ "const commentNode = container.childNodes[7];", null },
|
||||
.{ "commentNode.nodeValue", "comment" },
|
||||
.{ "const textRange = document.createRange();", null },
|
||||
.{ "textRange.selectNodeContents(commentNode)", "undefined" },
|
||||
.{ "textRange.startOffset", "0" },
|
||||
.{ "textRange.endOffset", "7" }, // length of `comment`
|
||||
|
||||
// Test Node range
|
||||
.{ "const nodeRange = document.createRange();", null },
|
||||
.{ "nodeRange.selectNodeContents(container)", "undefined" },
|
||||
.{ "nodeRange.startOffset", "0" },
|
||||
.{ "nodeRange.endOffset", "9" }, // length of container.childNodes
|
||||
}, .{});
|
||||
}
|
||||
54
src/browser/dom/resize_observer.zig
Normal file
54
src/browser/dom/resize_observer.zig
Normal file
@@ -0,0 +1,54 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
ResizeObserver,
|
||||
};
|
||||
|
||||
// WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface
|
||||
pub const ResizeObserver = struct {
|
||||
pub fn constructor(cbk: Env.Function) ResizeObserver {
|
||||
_ = cbk;
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *const ResizeObserver, element: *parser.Element, options_: ?Options) void {
|
||||
_ = self;
|
||||
_ = element;
|
||||
_ = options_;
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn _unobserve(self: *const ResizeObserver, element: *parser.Element) void {
|
||||
_ = self;
|
||||
_ = element;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _disconnect(self: *ResizeObserver) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
const Options = struct {
|
||||
box: []const u8,
|
||||
};
|
||||
73
src/browser/dom/shadow_root.zig
Normal file
73
src/browser/dom/shadow_root.zig
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const parser = @import("../netsurf.zig");
|
||||
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,
|
||||
|
||||
pub const Mode = enum {
|
||||
open,
|
||||
closed,
|
||||
};
|
||||
|
||||
pub fn get_host(self: *const ShadowRoot) !ElementUnion {
|
||||
return Element.toInterface(self.host);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.ShadowRoot" {
|
||||
defer testing.reset();
|
||||
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const div1 = document.createElement('div');", null },
|
||||
.{ "let sr1 = div1.attachShadow({mode: 'open'})", null },
|
||||
.{ "sr1.host == div1", "true" },
|
||||
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
|
||||
.{ "div1.shadowRoot == sr1", "true" },
|
||||
|
||||
.{ "try { div1.attachShadow({mode: 'closed'}) } catch (e) { e }", "Error: NotSupportedError" },
|
||||
|
||||
.{ " sr1.append(document.createElement('div'))", null },
|
||||
.{ " sr1.append(document.createElement('span'))", null },
|
||||
.{ "sr1.childElementCount", "2" },
|
||||
// re-attaching clears it
|
||||
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
|
||||
.{ "sr1.childElementCount", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const div2 = document.createElement('di2');", null },
|
||||
.{ "let sr2 = div2.attachShadow({mode: 'closed'})", null },
|
||||
.{ "sr2.host == div2", "true" },
|
||||
.{ "div2.shadowRoot", "null" }, // null when attached with 'closed'
|
||||
}, .{});
|
||||
}
|
||||
@@ -16,33 +16,25 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
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;
|
||||
|
||||
const UserContext = @import("../user_context.zig").UserContext;
|
||||
|
||||
// Text interfaces
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
pub const Interfaces = .{
|
||||
CDATASection,
|
||||
});
|
||||
};
|
||||
|
||||
pub const Text = struct {
|
||||
pub const Self = parser.Text;
|
||||
pub const prototype = *CharacterData;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Text {
|
||||
pub fn constructor(data: ?[]const u8, page: *const Page) !*parser.Text {
|
||||
return parser.documentCreateTextNode(
|
||||
parser.documentHTMLToDocument(userctx.document),
|
||||
parser.documentHTMLToDocument(page.window.document),
|
||||
data orelse "",
|
||||
);
|
||||
}
|
||||
@@ -67,30 +59,28 @@ pub const Text = struct {
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var constructor = [_]Case{
|
||||
.{ .src = "let t = new Text('foo')", .ex = "undefined" },
|
||||
.{ .src = "t.data", .ex = "foo" },
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Text" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
.{ .src = "let emptyt = new Text()", .ex = "undefined" },
|
||||
.{ .src = "emptyt.data", .ex = "" },
|
||||
};
|
||||
try checkCases(js_env, &constructor);
|
||||
try runner.testCases(&.{
|
||||
.{ "let t = new Text('foo')", "undefined" },
|
||||
.{ "t.data", "foo" },
|
||||
|
||||
var get_whole_text = [_]Case{
|
||||
.{ .src = "let text = document.getElementById('link').firstChild", .ex = "undefined" },
|
||||
.{ .src = "text.wholeText === 'OK'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &get_whole_text);
|
||||
.{ "let emptyt = new Text()", "undefined" },
|
||||
.{ "emptyt.data", "" },
|
||||
}, .{});
|
||||
|
||||
var split_text = [_]Case{
|
||||
.{ .src = "text.data = 'OK modified'", .ex = "OK modified" },
|
||||
.{ .src = "let split = text.splitText('OK'.length)", .ex = "undefined" },
|
||||
.{ .src = "split.data === ' modified'", .ex = "true" },
|
||||
.{ .src = "text.data === 'OK'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &split_text);
|
||||
try runner.testCases(&.{
|
||||
.{ "let text = document.getElementById('link').firstChild", "undefined" },
|
||||
.{ "text.wholeText === 'OK'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "text.data = 'OK modified'", "OK modified" },
|
||||
.{ "let split = text.splitText('OK'.length)", "undefined" },
|
||||
.{ "split.data === ' modified'", "true" },
|
||||
.{ "text.data === 'OK'", "true" },
|
||||
}, .{});
|
||||
}
|
||||
245
src/browser/dom/token_list.zig
Normal file
245
src/browser/dom/token_list.zig
Normal file
@@ -0,0 +1,245 @@
|
||||
// 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 iterator = @import("../iterator/iterator.zig");
|
||||
|
||||
const Function = @import("../env.zig").Function;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
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: Function, this_arg: JsObject) !void {
|
||||
var entries = _entries(self);
|
||||
while (try entries._next()) |entry| {
|
||||
var result: 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);
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.TokenList" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let gs = document.getElementById('para-empty')", "undefined" },
|
||||
.{ "let cl = gs.classList", "undefined" },
|
||||
.{ "gs.className", "ok empty" },
|
||||
.{ "cl.value", "ok empty" },
|
||||
.{ "cl.length", "2" },
|
||||
.{ "gs.className = 'foo bar baz'", "foo bar baz" },
|
||||
.{ "gs.className", "foo bar baz" },
|
||||
.{ "cl.length", "3" },
|
||||
.{ "gs.className = 'ok empty'", "ok empty" },
|
||||
.{ "cl.length", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let cl2 = gs.classList", "undefined" },
|
||||
.{ "cl2.length", "2" },
|
||||
.{ "cl2.item(0)", "ok" },
|
||||
.{ "cl2.item(1)", "empty" },
|
||||
.{ "cl2.contains('ok')", "true" },
|
||||
.{ "cl2.contains('nok')", "false" },
|
||||
.{ "cl2.add('foo', 'bar', 'baz')", "undefined" },
|
||||
.{ "cl2.length", "5" },
|
||||
.{ "cl2.remove('foo', 'bar', 'baz')", "undefined" },
|
||||
.{ "cl2.length", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let cl3 = gs.classList", "undefined" },
|
||||
.{ "cl3.toggle('ok')", "false" },
|
||||
.{ "cl3.toggle('ok')", "true" },
|
||||
.{ "cl3.length", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let cl4 = gs.classList", "undefined" },
|
||||
.{ "cl4.replace('ok', 'nok')", "true" },
|
||||
.{ "cl4.value", "empty nok" },
|
||||
.{ "cl4.replace('nok', 'ok')", "true" },
|
||||
.{ "cl4.value", "empty ok" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let cl5 = gs.classList", "undefined" },
|
||||
.{ "let keys = [...cl5.keys()]", "undefined" },
|
||||
.{ "keys.length", "2" },
|
||||
.{ "keys[0]", "0" },
|
||||
.{ "keys[1]", "1" },
|
||||
|
||||
.{ "let values = [...cl5.values()]", "undefined" },
|
||||
.{ "values.length", "2" },
|
||||
.{ "values[0]", "empty" },
|
||||
.{ "values[1]", "ok" },
|
||||
|
||||
.{ "let entries = [...cl5.entries()]", "undefined" },
|
||||
.{ "entries.length", "2" },
|
||||
.{ "entries[0]", "0,empty" },
|
||||
.{ "entries[1]", "1,ok" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let cl6 = gs.classList", "undefined" },
|
||||
.{ "cl6.value = 'a b ccc'", "a b ccc" },
|
||||
.{ "cl6.value", "a b ccc" },
|
||||
.{ "cl6.toString()", "a b ccc" },
|
||||
}, .{});
|
||||
}
|
||||
265
src/browser/dom/tree_walker.zig
Normal file
265
src/browser/dom/tree_walker.zig
Normal file
@@ -0,0 +1,265 @@
|
||||
// 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 NodeFilter = @import("node_filter.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
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: ?Env.Function,
|
||||
|
||||
pub const TreeWalkerOpts = union(enum) {
|
||||
function: Env.Function,
|
||||
object: struct { acceptNode: Env.Function },
|
||||
};
|
||||
|
||||
pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?TreeWalkerOpts) !TreeWalker {
|
||||
var filter_func: ?Env.Function = null;
|
||||
|
||||
if (filter) |f| {
|
||||
filter_func = switch (f) {
|
||||
.function => |func| func,
|
||||
.object => |o| o.acceptNode,
|
||||
};
|
||||
}
|
||||
|
||||
return .{
|
||||
.root = node,
|
||||
.current_node = node,
|
||||
.what_to_show = what_to_show orelse NodeFilter.NodeFilter._SHOW_ALL,
|
||||
.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 = try parser.nodeListLength(children);
|
||||
|
||||
for (0..child_count) |i| {
|
||||
const index: u32 = @intCast(i);
|
||||
const child = (try 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 = try parser.nodeListLength(children);
|
||||
|
||||
var index: u32 = child_count;
|
||||
while (index > 0) {
|
||||
index -= 1;
|
||||
const child = (try 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 = (try 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;
|
||||
}
|
||||
|
||||
fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
||||
var current = node;
|
||||
|
||||
while (true) {
|
||||
current = (try 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 = (try 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 {
|
||||
if (try self.firstChild(self.current_node)) |child| {
|
||||
self.current_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
var current = self.current_node;
|
||||
while (current != self.root) {
|
||||
if (try self.nextSibling(current)) |sibling| {
|
||||
self.current_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
}
|
||||
|
||||
current = (try parser.nodeParentNode(current)) orelse break;
|
||||
}
|
||||
|
||||
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 (try 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;
|
||||
}
|
||||
};
|
||||
@@ -16,9 +16,7 @@
|
||||
// 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");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
pub const Walker = union(enum) {
|
||||
walkerDepthFirst: WalkerDepthFirst,
|
||||
@@ -17,90 +17,133 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const File = std.fs.File;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const Walker = @import("../dom/walker.zig").WalkerChildren;
|
||||
const parser = @import("netsurf.zig");
|
||||
const Walker = @import("dom/walker.zig").WalkerChildren;
|
||||
|
||||
pub const Opts = struct {
|
||||
exclude_scripts: bool = false,
|
||||
};
|
||||
|
||||
// writer must be a std.io.Writer
|
||||
pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {
|
||||
pub fn writeHTML(doc: *parser.Document, opts: Opts, writer: anytype) !void {
|
||||
try writer.writeAll("<!DOCTYPE html>\n");
|
||||
try writeNode(parser.documentToNode(doc), writer);
|
||||
try writeChildren(parser.documentToNode(doc), opts, writer);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
// Spec: https://www.w3.org/TR/xml/#sec-prolog-dtd
|
||||
pub fn writeDocType(doc_type: *parser.DocumentType, writer: anytype) !void {
|
||||
try writer.writeAll("<!DOCTYPE ");
|
||||
try writer.writeAll(try parser.documentTypeGetName(doc_type));
|
||||
|
||||
const public_id = try parser.documentTypeGetPublicId(doc_type);
|
||||
const system_id = try 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: anytype) anyerror!void {
|
||||
switch (try parser.nodeType(node)) {
|
||||
.element => {
|
||||
// open the tag
|
||||
const tag_type = try parser.elementHTMLGetTagType(@ptrCast(node));
|
||||
if (tag_type == .script and opts.exclude_scripts) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = try parser.nodeLocalName(node);
|
||||
try writer.writeAll("<");
|
||||
try writer.writeAll(tag);
|
||||
|
||||
// write the attributes
|
||||
const _map = try parser.nodeGetAttributes(node);
|
||||
if (_map) |map| {
|
||||
const ln = try parser.namedNodeMapGetLength(map);
|
||||
for (0..ln) |i| {
|
||||
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(">");
|
||||
|
||||
// void elements can't have any content.
|
||||
if (try isVoid(parser.nodeToElement(node))) return;
|
||||
|
||||
if (tag_type == .script) {
|
||||
try writer.writeAll(try parser.nodeTextContent(node) orelse "");
|
||||
} else {
|
||||
// write the children
|
||||
// TODO avoid recursion
|
||||
try writeChildren(node, opts, writer);
|
||||
}
|
||||
|
||||
// close the tag
|
||||
try writer.writeAll("</");
|
||||
try writer.writeAll(tag);
|
||||
try writer.writeAll(">");
|
||||
},
|
||||
.text => {
|
||||
const v = try parser.nodeValue(node) orelse return;
|
||||
try writeEscapedTextNode(writer, v);
|
||||
},
|
||||
.cdata_section => {
|
||||
const v = try parser.nodeValue(node) orelse return;
|
||||
try writer.writeAll("<![CDATA[");
|
||||
try writer.writeAll(v);
|
||||
try writer.writeAll("]]>");
|
||||
},
|
||||
.comment => {
|
||||
const v = try parser.nodeValue(node) orelse return;
|
||||
try writer.writeAll("<!--");
|
||||
try writer.writeAll(v);
|
||||
try writer.writeAll("-->");
|
||||
},
|
||||
// TODO handle processing instruction dump
|
||||
.processing_instruction => return,
|
||||
// document fragment is outside of the main document DOM, so we
|
||||
// don't output it.
|
||||
.document_fragment => return,
|
||||
// document will never be called, but required for completeness.
|
||||
.document => return,
|
||||
// done globally instead, but required for completeness. Only the outer DOCTYPE should be written
|
||||
.document_type => return,
|
||||
// deprecated
|
||||
.attribute => return,
|
||||
.entity_reference => return,
|
||||
.entity => return,
|
||||
.notation => return,
|
||||
}
|
||||
}
|
||||
|
||||
// writer must be a std.io.Writer
|
||||
pub fn writeNode(root: *parser.Node, writer: anytype) !void {
|
||||
pub fn writeChildren(root: *parser.Node, opts: Opts, writer: anytype) !void {
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try walker.get_next(root, next) orelse break;
|
||||
switch (try parser.nodeType(next.?)) {
|
||||
.element => {
|
||||
// open the tag
|
||||
const tag = try parser.nodeLocalName(next.?);
|
||||
try writer.writeAll("<");
|
||||
try writer.writeAll(tag);
|
||||
|
||||
// write the attributes
|
||||
const map = try parser.nodeGetAttributes(next.?);
|
||||
const ln = try parser.namedNodeMapGetLength(map);
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
const attr = try parser.namedNodeMapItem(map, i) orelse break;
|
||||
try writer.writeAll(" ");
|
||||
try writer.writeAll(try parser.attributeGetName(attr));
|
||||
try writer.writeAll("=\"");
|
||||
try writer.writeAll(try parser.attributeGetValue(attr) orelse "");
|
||||
try writer.writeAll("\"");
|
||||
i += 1;
|
||||
}
|
||||
|
||||
try writer.writeAll(">");
|
||||
|
||||
// void elements can't have any content.
|
||||
if (try isVoid(parser.nodeToElement(next.?))) continue;
|
||||
|
||||
// write the children
|
||||
// TODO avoid recursion
|
||||
try writeNode(next.?, writer);
|
||||
|
||||
// close the tag
|
||||
try writer.writeAll("</");
|
||||
try writer.writeAll(tag);
|
||||
try writer.writeAll(">");
|
||||
},
|
||||
.text => {
|
||||
const v = try parser.nodeValue(next.?) orelse continue;
|
||||
try writer.writeAll(v);
|
||||
},
|
||||
.cdata_section => {
|
||||
const v = try parser.nodeValue(next.?) orelse continue;
|
||||
try writer.writeAll("<![CDATA[");
|
||||
try writer.writeAll(v);
|
||||
try writer.writeAll("]]>");
|
||||
},
|
||||
.comment => {
|
||||
const v = try parser.nodeValue(next.?) orelse continue;
|
||||
try writer.writeAll("<!--");
|
||||
try writer.writeAll(v);
|
||||
try writer.writeAll("-->");
|
||||
},
|
||||
// TODO handle processing instruction dump
|
||||
.processing_instruction => continue,
|
||||
// document fragment is outside of the main document DOM, so we
|
||||
// don't output it.
|
||||
.document_fragment => continue,
|
||||
// document will never be called, but required for completeness.
|
||||
.document => continue,
|
||||
// done globally instead, but required for completeness.
|
||||
.document_type => continue,
|
||||
// deprecated
|
||||
.attribute => continue,
|
||||
.entity_reference => continue,
|
||||
.entity => continue,
|
||||
.notation => continue,
|
||||
}
|
||||
try writeNode(next.?, opts, writer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,18 +158,95 @@ fn isVoid(elem: *parser.Element) !bool {
|
||||
};
|
||||
}
|
||||
|
||||
fn writeEscapedTextNode(writer: anytype, 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(">"),
|
||||
else => unreachable,
|
||||
}
|
||||
v = v[index + 1 ..];
|
||||
}
|
||||
}
|
||||
|
||||
fn writeEscapedAttributeValue(writer: anytype, 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" {
|
||||
const out = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only });
|
||||
defer out.close();
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
|
||||
const file = try std.fs.cwd().openFile("test.html", .{});
|
||||
defer file.close();
|
||||
try testWriteHTML(
|
||||
"<div id=\"content\">Over 9000!</div>",
|
||||
"<div id=\"content\">Over 9000!</div>",
|
||||
);
|
||||
|
||||
const doc_html = try parser.documentHTMLParse(file.reader(), "UTF-8");
|
||||
// ignore close error
|
||||
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 buf = std.ArrayListUnmanaged(u8){};
|
||||
defer buf.deinit(testing.allocator);
|
||||
|
||||
const doc_html = try parser.documentHTMLParseFromStr(src);
|
||||
defer parser.documentHTMLClose(doc_html) catch {};
|
||||
|
||||
const doc = parser.documentHTMLToDocument(doc_html);
|
||||
|
||||
try writeHTML(doc, out);
|
||||
try writeHTML(doc, .{}, buf.writer(testing.allocator));
|
||||
try testing.expectEqualStrings(expected, buf.items);
|
||||
}
|
||||
|
||||
107
src/browser/encoding/TextDecoder.zig
Normal file
107
src/browser/encoding/TextDecoder.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/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
// 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,
|
||||
|
||||
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 .{
|
||||
.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;
|
||||
}
|
||||
|
||||
// TODO: Should accept an ArrayBuffer, TypedArray or DataView
|
||||
// js.zig will currently only map a TypedArray to our []const u8.
|
||||
pub fn _decode(self: *const TextDecoder, v: []const u8) ![]const u8 {
|
||||
if (self.fatal and !std.unicode.utf8ValidateSlice(v)) {
|
||||
return error.InvalidUtf8;
|
||||
}
|
||||
|
||||
if (self.ignore_bom == false and std.mem.startsWith(u8, v, &.{ 0xEF, 0xBB, 0xBF })) {
|
||||
return v[3..];
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Encoding.TextDecoder" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{
|
||||
.html = "",
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let d1 = new TextDecoder();", null },
|
||||
.{ "d1.encoding;", "utf-8" },
|
||||
.{ "d1.fatal", "false" },
|
||||
.{ "d1.ignoreBOM", "false" },
|
||||
.{ "d1.decode(new Uint8Array([240, 160, 174, 183]))", "𠮷" },
|
||||
.{ "d1.decode(new Uint8Array([0xEF, 0xBB, 0xBF, 240, 160, 174, 183]))", "𠮷" },
|
||||
.{ "d1.decode(new Uint8Array([49, 50]).buffer)", "12" },
|
||||
|
||||
.{ "let d2 = new TextDecoder('utf8', {fatal: true})", null },
|
||||
.{
|
||||
\\ try {
|
||||
\\ let data = new Uint8Array([240, 240, 160, 174, 183]);
|
||||
\\ d2.decode(data);
|
||||
\\ } catch (e) {e}
|
||||
,
|
||||
"Error: InvalidUtf8",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
62
src/browser/encoding/TextEncoder.zig
Normal file
62
src/browser/encoding/TextEncoder.zig
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
|
||||
// https://encoding.spec.whatwg.org/#interface-textencoder
|
||||
const TextEncoder = @This();
|
||||
|
||||
pub fn constructor() !TextEncoder {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn get_encoding(_: *const TextEncoder) []const u8 {
|
||||
return "utf-8";
|
||||
}
|
||||
|
||||
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
|
||||
// Ensure the input is a valid utf-8
|
||||
// It seems chrome accepts invalid utf-8 sequence.
|
||||
//
|
||||
if (!std.unicode.utf8ValidateSlice(v)) {
|
||||
return error.InvalidUtf8;
|
||||
}
|
||||
|
||||
return .{ .values = v };
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Encoding.TextEncoder" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{
|
||||
.html = "",
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var encoder = new TextEncoder();", null },
|
||||
.{ "encoder.encoding;", "utf-8" },
|
||||
.{ "encoder.encode('€');", "226,130,172" },
|
||||
|
||||
// Invalid utf-8 sequence.
|
||||
// Result with chrome:
|
||||
// .{ "encoder.encode(new Uint8Array([0xE2,0x28,0xA1]))", "50,50,54,44,52,48,44,49,54,49" },
|
||||
.{ "try {encoder.encode(new Uint8Array([0xE2,0x28,0xA1])) } catch (e) { e };", "Error: InvalidUtf8" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -16,9 +16,7 @@
|
||||
// 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 html: []const u8 =
|
||||
\\<main id='content'>
|
||||
\\<a href='foo'>OK</a>
|
||||
\\<p>blah-blah-blah</p>
|
||||
\\</main>
|
||||
;
|
||||
pub const Interfaces = .{
|
||||
@import("TextDecoder.zig"),
|
||||
@import("TextEncoder.zig"),
|
||||
};
|
||||
48
src/browser/env.zig
Normal file
48
src/browser/env.zig
Normal file
@@ -0,0 +1,48 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Page = @import("page.zig").Page;
|
||||
const js = @import("../runtime/js.zig");
|
||||
const generate = @import("../runtime/generate.zig");
|
||||
|
||||
const WebApis = struct {
|
||||
// Wrapped like this for debug ergonomics.
|
||||
// When we create our Env, a few lines down, we define it as:
|
||||
// pub const Env = js.Env(*Page, WebApis);
|
||||
//
|
||||
// If there's a compile time error witht he Env, it's type will be readable,
|
||||
// i.e.: runtime.js.Env(*browser.env.Page, browser.env.WebApis)
|
||||
//
|
||||
// But if we didn't wrap it in the struct, like we once didn't, and defined
|
||||
// env as:
|
||||
// pub const Env = js.Env(*Page, Interfaces);
|
||||
//
|
||||
// Because Interfaces is an anynoumous type, it doesn't have a friendly name
|
||||
// and errors would be something like:
|
||||
// runtime.js.Env(*browser.Page, .{...A HUNDRED TYPES...})
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
@import("crypto/crypto.zig").Crypto,
|
||||
@import("console/console.zig").Console,
|
||||
@import("css/css.zig").Interfaces,
|
||||
@import("cssom/cssom.zig").Interfaces,
|
||||
@import("dom/dom.zig").Interfaces,
|
||||
@import("dom/shadow_root.zig").ShadowRoot,
|
||||
@import("encoding/encoding.zig").Interfaces,
|
||||
@import("events/event.zig").Interfaces,
|
||||
@import("html/html.zig").Interfaces,
|
||||
@import("iterator/iterator.zig").Interfaces,
|
||||
@import("storage/storage.zig").Interfaces,
|
||||
@import("url/url.zig").Interfaces,
|
||||
@import("xhr/xhr.zig").Interfaces,
|
||||
@import("xhr/form_data.zig").Interfaces,
|
||||
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
||||
});
|
||||
};
|
||||
|
||||
pub const JsThis = Env.JsThis;
|
||||
pub const JsObject = Env.JsObject;
|
||||
pub const Function = Env.Function;
|
||||
pub const Promise = Env.Promise;
|
||||
pub const PromiseResolver = Env.PromiseResolver;
|
||||
|
||||
pub const Env = js.Env(*Page, WebApis);
|
||||
pub const Global = @import("html/window.zig").Window;
|
||||
80
src/browser/events/custom_event.zig
Normal file
80
src/browser/events/custom_event.zig
Normal file
@@ -0,0 +1,80 @@
|
||||
// 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 Event = @import("event.zig").Event;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
|
||||
// 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: ?JsObject,
|
||||
|
||||
const CustomEventInit = struct {
|
||||
bubbles: bool = false,
|
||||
cancelable: bool = false,
|
||||
composed: bool = false,
|
||||
detail: ?JsObject = 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) ?JsObject {
|
||||
return self.detail;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.CustomEvent" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let capture = null", "undefined" },
|
||||
.{ "const el = document.createElement('div');", "undefined" },
|
||||
.{ "el.addEventListener('c1', (e) => { capture = 'c1-' + new String(e.detail)})", "undefined" },
|
||||
.{ "el.addEventListener('c2', (e) => { capture = 'c2-' + new String(e.detail.over)})", "undefined" },
|
||||
|
||||
.{ "el.dispatchEvent(new CustomEvent('c1'));", "true" },
|
||||
.{ "capture", "c1-null" },
|
||||
|
||||
.{ "el.dispatchEvent(new CustomEvent('c1', {detail: '123'}));", "true" },
|
||||
.{ "capture", "c1-123" },
|
||||
|
||||
.{ "el.dispatchEvent(new CustomEvent('c2', {detail: {over: 9000}}));", "true" },
|
||||
.{ "capture", "c2-9000" },
|
||||
}, .{});
|
||||
}
|
||||
449
src/browser/events/event.zig
Normal file
449
src/browser/events/event.zig
Normal file
@@ -0,0 +1,449 @@
|
||||
// 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("../../runtime/generate.zig");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
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 ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
||||
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent };
|
||||
|
||||
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 (try 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)).* },
|
||||
};
|
||||
}
|
||||
|
||||
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 try parser.eventType(self);
|
||||
}
|
||||
|
||||
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
|
||||
const et = try 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 = try parser.eventCurrentTarget(self);
|
||||
if (et == null) return null;
|
||||
return try EventTarget.toInterface(et.?, page);
|
||||
}
|
||||
|
||||
pub fn get_eventPhase(self: *parser.Event) !u8 {
|
||||
return try parser.eventPhase(self);
|
||||
}
|
||||
|
||||
pub fn get_bubbles(self: *parser.Event) !bool {
|
||||
return try parser.eventBubbles(self);
|
||||
}
|
||||
|
||||
pub fn get_cancelable(self: *parser.Event) !bool {
|
||||
return try parser.eventCancelable(self);
|
||||
}
|
||||
|
||||
pub fn get_defaultPrevented(self: *parser.Event) !bool {
|
||||
return try parser.eventDefaultPrevented(self);
|
||||
}
|
||||
|
||||
pub fn get_isTrusted(self: *parser.Event) !bool {
|
||||
return try parser.eventIsTrusted(self);
|
||||
}
|
||||
|
||||
pub fn get_timestamp(self: *parser.Event) !u32 {
|
||||
return try 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 try parser.eventStopPropagation(self);
|
||||
}
|
||||
|
||||
pub fn _stopImmediatePropagation(self: *parser.Event) !void {
|
||||
return try parser.eventStopImmediatePropagation(self);
|
||||
}
|
||||
|
||||
pub fn _preventDefault(self: *parser.Event) !void {
|
||||
return try parser.eventPreventDefault(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const EventHandler = struct {
|
||||
once: bool,
|
||||
capture: bool,
|
||||
callback: Function,
|
||||
node: parser.EventNode,
|
||||
listener: *parser.EventListener,
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Function = Env.Function;
|
||||
|
||||
pub const Listener = union(enum) {
|
||||
function: Function,
|
||||
object: Env.JsObject,
|
||||
|
||||
pub fn callback(self: Listener, target: *parser.EventTarget) !?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) catch |err| {
|
||||
log.err(.app, "toInterface error", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
|
||||
const self: *EventHandler = @fieldParentPtr("node", node);
|
||||
var result: 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) catch return).?;
|
||||
const typ = parser.eventType(event) catch return;
|
||||
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" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let para = document.getElementById('para')", "undefined" },
|
||||
.{ "var nb = 0; var evt", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ content.addEventListener('target', function(e) {
|
||||
\\ evt = e; nb = nb + 1;
|
||||
\\ e.preventDefault();
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", "false" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt.target === content", "true" },
|
||||
.{ "evt.bubbles", "true" },
|
||||
.{ "evt.cancelable", "true" },
|
||||
.{ "evt.defaultPrevented", "true" },
|
||||
.{ "evt.isTrusted", "true" },
|
||||
.{ "evt.timestamp > 1704063600", "true" }, // 2024/01/01 00:00
|
||||
// event.type, event.currentTarget, event.phase checked in EventTarget
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{
|
||||
\\ content.addEventListener('stop',function(e) {
|
||||
\\ e.stopPropagation();
|
||||
\\ nb = nb + 1;
|
||||
\\ }, true)
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
// the following event listener will not be invoked
|
||||
.{
|
||||
\\ para.addEventListener('stop',function(e) {
|
||||
\\ nb = nb + 1;
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "para.dispatchEvent(new Event('stop'))", "true" },
|
||||
.{ "nb", "1" }, // will be 2 if event was not stopped at content event listener
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{
|
||||
\\ content.addEventListener('immediate', function(e) {
|
||||
\\ e.stopImmediatePropagation();
|
||||
\\ nb = nb + 1;
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
// the following event listener will not be invoked
|
||||
.{
|
||||
\\ content.addEventListener('immediate', function(e) {
|
||||
\\ nb = nb + 1;
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "content.dispatchEvent(new Event('immediate'))", "true" },
|
||||
.{ "nb", "1" }, // will be 2 if event was not stopped at first content event listener
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{
|
||||
\\ content.addEventListener('legacy', function(e) {
|
||||
\\ evt = e; nb = nb + 1;
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "let evtLegacy = document.createEvent('Event')", "undefined" },
|
||||
.{ "evtLegacy.initEvent('legacy')", "undefined" },
|
||||
.{ "content.dispatchEvent(evtLegacy)", "true" },
|
||||
.{ "nb", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", "undefined" },
|
||||
.{ "document.addEventListener('count', cbk)", "undefined" },
|
||||
.{ "document.removeEventListener('count', cbk)", "undefined" },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "nb", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; function cbk(event) { nb ++; }", null },
|
||||
.{ "document.addEventListener('count', cbk, {once: true})", null },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "document.removeEventListener('count', cbk)", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; function cbk(event) { nb ++; }", null },
|
||||
.{ "let ac = new AbortController()", null },
|
||||
.{ "document.addEventListener('count', cbk, {signal: ac.signal})", null },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "ac.abort()", null },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "nb", "2" },
|
||||
.{ "document.removeEventListener('count', cbk)", "undefined" },
|
||||
}, .{});
|
||||
}
|
||||
140
src/browser/events/mouse_event.zig
Normal file
140
src/browser/events/mouse_event.zig
Normal file
@@ -0,0 +1,140 @@
|
||||
// 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 = std.log.scoped(.mouse_event);
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Event = @import("event.zig").Event;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
|
||||
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
|
||||
// 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{};
|
||||
|
||||
var mouse_event = try parser.mouseEventCreate();
|
||||
try 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("MouseEvent currently only supports listeners for 'click' events!", .{});
|
||||
}
|
||||
|
||||
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.MouseEvent" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
// Default MouseEvent
|
||||
.{ "let event = new MouseEvent('click')", "undefined" },
|
||||
.{ "event.type", "click" },
|
||||
.{ "event instanceof MouseEvent", "true" },
|
||||
.{ "event instanceof Event", "true" },
|
||||
.{ "event.clientX", "0" },
|
||||
.{ "event.clientY", "0" },
|
||||
.{ "event.screenX", "0" },
|
||||
.{ "event.screenY", "0" },
|
||||
// MouseEvent with parameters
|
||||
.{ "let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20 })", "undefined" },
|
||||
.{ "new_event.button", "0" },
|
||||
.{ "new_event.x", "10" },
|
||||
.{ "new_event.y", "20" },
|
||||
.{ "new_event.screenX", "10" },
|
||||
.{ "new_event.screenY", "20" },
|
||||
// MouseEvent Listener
|
||||
.{ "let me = new MouseEvent('click')", "undefined" },
|
||||
.{ "me instanceof Event", "true" },
|
||||
.{ "var eevt = null; function ccbk(event) { eevt = event; }", "undefined" },
|
||||
.{ "document.addEventListener('click', ccbk)", "undefined" },
|
||||
.{ "document.dispatchEvent(me)", "true" },
|
||||
.{ "eevt.type", "click" },
|
||||
.{ "eevt instanceof MouseEvent", "true" },
|
||||
}, .{});
|
||||
}
|
||||
187
src/browser/html/AbortController.zig
Normal file
187
src/browser/html/AbortController.zig
Normal file
@@ -0,0 +1,187 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
||||
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,
|
||||
.node = .{ .func = TimeoutCallback.run },
|
||||
};
|
||||
|
||||
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
|
||||
_ = try page.loop.timeout(delay_ms, &callback.node);
|
||||
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();
|
||||
try 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: Env.Exception,
|
||||
undefined: void,
|
||||
};
|
||||
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
|
||||
if (self.aborted) {
|
||||
const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON);
|
||||
return .{ .exception = ex };
|
||||
}
|
||||
return .{ .undefined = {} };
|
||||
}
|
||||
};
|
||||
|
||||
const TimeoutCallback = struct {
|
||||
signal: AbortSignal,
|
||||
|
||||
// This is the internal data that the event loop tracks. We'll get this
|
||||
// back in run and, from it, can get our TimeoutCallback instance
|
||||
node: Loop.CallbackNode = undefined,
|
||||
|
||||
fn run(node: *Loop.CallbackNode, _: *?u63) void {
|
||||
const self: *TimeoutCallback = @fieldParentPtr("node", node);
|
||||
self.signal.abort("TimeoutError") catch |err| {
|
||||
log.warn(.app, "abort signal timeout", .{ .err = err });
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.AbortController" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var called = 0", null },
|
||||
.{ "var a1 = new AbortController()", null },
|
||||
.{ "var s1 = a1.signal", null },
|
||||
.{ "s1.throwIfAborted()", "undefined" },
|
||||
.{ "s1.reason", "undefined" },
|
||||
.{ "var target;", null },
|
||||
.{
|
||||
\\ s1.addEventListener('abort', (e) => {
|
||||
\\ called += 1;
|
||||
\\ target = e.target;
|
||||
\\
|
||||
\\ });
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "a1.abort()", null },
|
||||
.{ "s1.aborted", "true" },
|
||||
.{ "target == s1", "true" },
|
||||
.{ "s1.reason", "AbortError" },
|
||||
.{ "called", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var s2 = AbortSignal.abort('over 9000')", null },
|
||||
.{ "s2.aborted", "true" },
|
||||
.{ "s2.reason", "over 9000" },
|
||||
.{ "AbortSignal.abort().reason", "AbortError" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var s3 = AbortSignal.timeout(10)", null },
|
||||
.{ "s3.aborted", "true" },
|
||||
.{ "s3.reason", "TimeoutError" },
|
||||
.{ "try { s3.throwIfAborted() } catch (e) { e }", "Error: TimeoutError" },
|
||||
}, .{});
|
||||
}
|
||||
97
src/browser/html/DataSet.zig
Normal file
97
src/browser/html/DataSet.zig
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
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) !Env.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" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let el1 = document.createElement('div')", null },
|
||||
.{ "el1.dataset.x", "undefined" },
|
||||
.{ "el1.dataset.x = '123'", "123" },
|
||||
.{ "delete el1.dataset.x", "true" },
|
||||
.{ "el1.dataset.x", "undefined" },
|
||||
.{ "delete el1.dataset.other", "true" }, // yes, this is right
|
||||
|
||||
.{ "let ds1 = el1.dataset", null },
|
||||
.{ "ds1.helloWorld = 'yes'", null },
|
||||
.{ "el1.getAttribute('data-hello-world')", "yes" },
|
||||
.{ "el1.setAttribute('data-this-will-work', 'positive')", null },
|
||||
.{ "ds1.thisWillWork", "positive" },
|
||||
}, .{});
|
||||
}
|
||||
412
src/browser/html/document.zig
Normal file
412
src/browser/html/document.zig
Normal file
@@ -0,0 +1,412 @@
|
||||
// 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 Page = @import("../page.zig").Page;
|
||||
|
||||
const Window = @import("window.zig").Window;
|
||||
const Element = @import("../dom/element.zig").Element;
|
||||
const ElementUnion = @import("../dom/element.zig").Union;
|
||||
const Document = @import("../dom/document.zig").Document;
|
||||
const NodeList = @import("../dom/nodelist.zig").NodeList;
|
||||
const Location = @import("location.zig").Location;
|
||||
|
||||
const collection = @import("../dom/html_collection.zig");
|
||||
const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
|
||||
const Cookie = @import("../storage/cookie.zig").Cookie;
|
||||
|
||||
// WEB IDL https://html.spec.whatwg.org/#the-document-object
|
||||
pub const HTMLDocument = struct {
|
||||
pub const Self = parser.DocumentHTML;
|
||||
pub const prototype = *Document;
|
||||
pub const subtype = .node;
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
// libdom's document_html get_domain always returns null, this is
|
||||
// the way MDN recommends getting the domain anyways, since document.domain
|
||||
// is deprecated.
|
||||
const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
|
||||
return location.get_host(page);
|
||||
}
|
||||
|
||||
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
|
||||
return error.NotImplemented;
|
||||
}
|
||||
|
||||
pub fn get_referrer(self: *parser.DocumentHTML) ![]const u8 {
|
||||
return try parser.documentHTMLGetReferrer(self);
|
||||
}
|
||||
|
||||
pub fn set_referrer(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
|
||||
return error.NotImplemented;
|
||||
}
|
||||
|
||||
pub fn get_body(self: *parser.DocumentHTML) !?*parser.Body {
|
||||
return try parser.documentHTMLBody(self);
|
||||
}
|
||||
|
||||
pub fn set_body(self: *parser.DocumentHTML, elt: ?*parser.ElementHTML) !?*parser.Body {
|
||||
try parser.documentHTMLSetBody(self, elt);
|
||||
return try get_body(self);
|
||||
}
|
||||
|
||||
pub fn get_head(self: *parser.DocumentHTML) !?*parser.Head {
|
||||
const root = parser.documentHTMLToNode(self);
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try walker.get_next(root, next) orelse return null;
|
||||
if (std.ascii.eqlIgnoreCase("head", try parser.nodeName(next.?))) {
|
||||
return @as(*parser.Head, @ptrCast(next.?));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true, .is_http = false });
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, page: *Page) ![]const u8 {
|
||||
// we use the cookie jar's allocator to parse the cookie because it
|
||||
// outlives the page's arena.
|
||||
const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str);
|
||||
errdefer c.deinit();
|
||||
if (c.http_only) {
|
||||
c.deinit();
|
||||
return ""; // HttpOnly cookies cannot be set from JS
|
||||
}
|
||||
try page.cookie_jar.add(c, std.time.timestamp());
|
||||
return cookie_str;
|
||||
}
|
||||
|
||||
pub fn get_title(self: *parser.DocumentHTML) ![]const u8 {
|
||||
return try parser.documentHTMLGetTitle(self);
|
||||
}
|
||||
|
||||
pub fn set_title(self: *parser.DocumentHTML, v: []const u8) ![]const u8 {
|
||||
try parser.documentHTMLSetTitle(self, v);
|
||||
return v;
|
||||
}
|
||||
|
||||
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList {
|
||||
const arena = page.arena;
|
||||
var list: NodeList = .{};
|
||||
|
||||
if (name.len == 0) return list;
|
||||
|
||||
const root = parser.documentHTMLToNode(self);
|
||||
var c = try collection.HTMLCollectionByName(arena, root, name, false);
|
||||
|
||||
const ln = try c.get_length();
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
const n = try c.item(i) orelse break;
|
||||
try list.append(arena, n);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", false);
|
||||
}
|
||||
|
||||
pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", false);
|
||||
}
|
||||
|
||||
pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return get_embeds(self, page);
|
||||
}
|
||||
|
||||
pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", false);
|
||||
}
|
||||
|
||||
pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", false);
|
||||
}
|
||||
|
||||
pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionEmpty();
|
||||
}
|
||||
|
||||
pub fn get_links(self: *parser.DocumentHTML) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), false);
|
||||
}
|
||||
|
||||
pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), false);
|
||||
}
|
||||
|
||||
pub fn get_all(self: *parser.DocumentHTML) collection.HTMLAllCollection {
|
||||
return collection.HTMLAllCollection.init(parser.documentHTMLToNode(self));
|
||||
}
|
||||
|
||||
pub fn get_currentScript(self: *parser.DocumentHTML) !?*parser.Script {
|
||||
return try parser.documentHTMLGetCurrentScript(self);
|
||||
}
|
||||
|
||||
pub fn get_location(self: *parser.DocumentHTML) !?*Location {
|
||||
return try parser.documentHTMLGetLocation(Location, self);
|
||||
}
|
||||
|
||||
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
|
||||
return "off";
|
||||
}
|
||||
|
||||
pub fn set_designMode(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
||||
return "off";
|
||||
}
|
||||
|
||||
pub fn get_defaultView(_: *parser.DocumentHTML, page: *Page) *Window {
|
||||
return &page.window;
|
||||
}
|
||||
|
||||
pub fn get_readyState(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
return @tagName(state.ready_state);
|
||||
}
|
||||
|
||||
// noop legacy functions
|
||||
// https://html.spec.whatwg.org/#Document-partial
|
||||
pub fn _clear(_: *parser.DocumentHTML) void {}
|
||||
pub fn _captureEvents(_: *parser.DocumentHTML) void {}
|
||||
pub fn _releaseEvents(_: *parser.DocumentHTML) void {}
|
||||
|
||||
pub fn get_fgColor(_: *parser.DocumentHTML) []const u8 {
|
||||
return "";
|
||||
}
|
||||
pub fn set_fgColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
||||
return "";
|
||||
}
|
||||
pub fn get_linkColor(_: *parser.DocumentHTML) []const u8 {
|
||||
return "";
|
||||
}
|
||||
pub fn set_linkColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
||||
return "";
|
||||
}
|
||||
pub fn get_vlinkColor(_: *parser.DocumentHTML) []const u8 {
|
||||
return "";
|
||||
}
|
||||
pub fn set_vlinkColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
||||
return "";
|
||||
}
|
||||
pub fn get_alinkColor(_: *parser.DocumentHTML) []const u8 {
|
||||
return "";
|
||||
}
|
||||
pub fn set_alinkColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
||||
return "";
|
||||
}
|
||||
pub fn get_bgColor(_: *parser.DocumentHTML) []const u8 {
|
||||
return "";
|
||||
}
|
||||
pub fn set_bgColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Returns the topmost Element at the specified coordinates (relative to the viewport).
|
||||
// Since LightPanda requires the client to know what they are clicking on we do not return the underlying element at this moment
|
||||
// This can currenty only happen if the first pixel is clicked without having rendered any element. This will change when css properties are supported.
|
||||
// This returns an ElementUnion instead of a *Parser.Element in case the element somehow hasn't passed through the js runtime yet.
|
||||
// While x and y should be f32, here we take i32 since that's what our
|
||||
// "renderer" uses. By specifying i32 here, rather than f32 and doing the
|
||||
// conversion ourself, we rely on v8's type conversion which is both more
|
||||
// flexible (e.g. handles NaN) and will be more consistent with a browser.
|
||||
pub fn _elementFromPoint(_: *parser.DocumentHTML, x: i32, y: i32, page: *Page) !?ElementUnion {
|
||||
const element = page.renderer.getElementAtPosition(x, y) orelse return null;
|
||||
// TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?)
|
||||
return try Element.toInterface(element);
|
||||
}
|
||||
|
||||
// Returns an array of all elements at the specified coordinates (relative to the viewport). The elements are ordered from the topmost to the bottommost box of the viewport.
|
||||
// While x and y should be f32, here we take i32 since that's what our
|
||||
// "renderer" uses. By specifying i32 here, rather than f32 and doing the
|
||||
// conversion ourself, we rely on v8's type conversion which is both more
|
||||
// flexible (e.g. handles NaN) and will be more consistent with a browser.
|
||||
pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: i32, y: i32, page: *Page) ![]ElementUnion {
|
||||
const element = page.renderer.getElementAtPosition(x, y) orelse return &.{};
|
||||
// TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?)
|
||||
|
||||
var list: std.ArrayListUnmanaged(ElementUnion) = .empty;
|
||||
try list.ensureTotalCapacity(page.call_arena, 3);
|
||||
list.appendAssumeCapacity(try Element.toInterface(element));
|
||||
|
||||
// Since we are using a flat renderer there is no hierarchy of elements. What we do know is that the element is part of the main document.
|
||||
// Thus we can add the HtmlHtmlElement and it's child HTMLBodyElement to the returned list.
|
||||
// TBD Should we instead return every parent that is an element? Note that a child does not physically need to be overlapping the parent.
|
||||
// Should we do a render pass on demand?
|
||||
const doc_elem = try parser.documentGetDocumentElement(parser.documentHTMLToDocument(page.window.document)) orelse {
|
||||
return list.items;
|
||||
};
|
||||
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
||||
list.appendAssumeCapacity(try Element.toInterface(parser.bodyToElement(body)));
|
||||
}
|
||||
list.appendAssumeCapacity(try Element.toInterface(doc_elem));
|
||||
return list.items;
|
||||
}
|
||||
|
||||
pub fn documentIsLoaded(self: *parser.DocumentHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
state.ready_state = .interactive;
|
||||
|
||||
log.debug(.script_event, "dispatch event", .{
|
||||
.type = "DOMContentLoaded",
|
||||
.source = "document",
|
||||
});
|
||||
|
||||
const evt = try parser.eventCreate();
|
||||
defer parser.eventDestroy(evt);
|
||||
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
|
||||
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, self), evt);
|
||||
|
||||
try page.window.dispatchForDocumentTarget(evt);
|
||||
}
|
||||
|
||||
pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
state.ready_state = .complete;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
test "Browser.HTML.Document" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.__proto__.constructor.name", "HTMLDocument" },
|
||||
.{ "document.__proto__.__proto__.constructor.name", "Document" },
|
||||
.{ "document.body.localName == 'body'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.domain", "lightpanda.io" },
|
||||
.{ "document.referrer", "" },
|
||||
.{ "document.title", "" },
|
||||
.{ "document.body.localName", "body" },
|
||||
.{ "document.head.localName", "head" },
|
||||
.{ "document.images.length", "0" },
|
||||
.{ "document.embeds.length", "0" },
|
||||
.{ "document.plugins.length", "0" },
|
||||
.{ "document.scripts.length", "0" },
|
||||
.{ "document.forms.length", "0" },
|
||||
.{ "document.links.length", "1" },
|
||||
.{ "document.applets.length", "0" },
|
||||
.{ "document.anchors.length", "0" },
|
||||
.{ "document.all.length", "8" },
|
||||
.{ "document.currentScript", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.title = 'foo'", "foo" },
|
||||
.{ "document.title", "foo" },
|
||||
.{ "document.title = ''", "" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('link').setAttribute('name', 'foo')", "undefined" },
|
||||
.{ "let list = document.getElementsByName('foo')", "undefined" },
|
||||
.{ "list.length", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.cookie", "" },
|
||||
.{ "document.cookie = 'name=Oeschger; SameSite=None; Secure'", "name=Oeschger; SameSite=None; Secure" },
|
||||
.{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" },
|
||||
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
|
||||
.{ "document.cookie = 'IgnoreMy=Ghost; HttpOnly'", null }, // "" should be returned, but the framework overrules it atm
|
||||
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.elementFromPoint(0.5, 0.5)", "null" }, // Return null since we only return element s when they have previously been localized
|
||||
.{ "document.elementsFromPoint(0.5, 0.5)", "" },
|
||||
.{
|
||||
\\ let div1 = document.createElement('div');
|
||||
\\ document.body.appendChild(div1);
|
||||
\\ div1.getClientRects();
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "document.elementFromPoint(0.5, 0.5)", "[object HTMLDivElement]" },
|
||||
.{ "let elems = document.elementsFromPoint(0.5, 0.5)", null },
|
||||
.{ "elems.length", "3" },
|
||||
.{ "elems[0]", "[object HTMLDivElement]" },
|
||||
.{ "elems[1]", "[object HTMLBodyElement]" },
|
||||
.{ "elems[2]", "[object HTMLHtmlElement]" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let a = document.createElement('a');
|
||||
\\ a.href = "https://lightpanda.io";
|
||||
\\ document.body.appendChild(a);
|
||||
\\ a.getClientRects();
|
||||
, // Note this will be placed after the div of previous test
|
||||
null,
|
||||
},
|
||||
.{ "let a_again = document.elementFromPoint(1.5, 0.5)", null },
|
||||
.{ "a_again", "[object HTMLAnchorElement]" },
|
||||
.{ "a_again.href", "https://lightpanda.io" },
|
||||
.{ "let a_agains = document.elementsFromPoint(1.5, 0.5)", null },
|
||||
.{ "a_agains[0].href", "https://lightpanda.io" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "!document.all", "true" },
|
||||
.{ "!!document.all", "false" },
|
||||
.{ "document.all(5)", "[object HTMLParagraphElement]" },
|
||||
.{ "document.all('content')", "[object HTMLDivElement]" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.defaultView.document == document", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.readyState", "loading" },
|
||||
}, .{});
|
||||
|
||||
try HTMLDocument.documentIsLoaded(runner.page.window.document, runner.page);
|
||||
try runner.testCases(&.{
|
||||
.{ "document.readyState", "interactive" },
|
||||
}, .{});
|
||||
|
||||
try HTMLDocument.documentIsComplete(runner.page.window.document, runner.page);
|
||||
try runner.testCases(&.{
|
||||
.{ "document.readyState", "complete" },
|
||||
}, .{});
|
||||
}
|
||||
1512
src/browser/html/elements.zig
Normal file
1512
src/browser/html/elements.zig
Normal file
File diff suppressed because it is too large
Load Diff
114
src/browser/html/error_event.zig
Normal file
114
src/browser/html/error_event.zig
Normal file
@@ -0,0 +1,114 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
|
||||
pub const ErrorEvent = struct {
|
||||
pub const prototype = *parser.Event;
|
||||
pub const union_make_copy = true;
|
||||
|
||||
proto: parser.Event,
|
||||
message: []const u8,
|
||||
filename: []const u8,
|
||||
lineno: i32,
|
||||
colno: i32,
|
||||
@"error": ?Env.JsObject,
|
||||
|
||||
const ErrorEventInit = struct {
|
||||
message: []const u8 = "",
|
||||
filename: []const u8 = "",
|
||||
lineno: i32 = 0,
|
||||
colno: i32 = 0,
|
||||
@"error": ?Env.JsObject = null,
|
||||
};
|
||||
|
||||
pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, event_type, .{});
|
||||
try parser.eventSetInternalType(event, .event);
|
||||
|
||||
const o = opts orelse ErrorEventInit{};
|
||||
|
||||
return .{
|
||||
.proto = event.*,
|
||||
.message = o.message,
|
||||
.filename = o.filename,
|
||||
.lineno = o.lineno,
|
||||
.colno = o.colno,
|
||||
.@"error" = if (o.@"error") |e| try e.persist() else null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_message(self: *const ErrorEvent) []const u8 {
|
||||
return self.message;
|
||||
}
|
||||
|
||||
pub fn get_filename(self: *const ErrorEvent) []const u8 {
|
||||
return self.filename;
|
||||
}
|
||||
|
||||
pub fn get_lineno(self: *const ErrorEvent) i32 {
|
||||
return self.lineno;
|
||||
}
|
||||
|
||||
pub fn get_colno(self: *const ErrorEvent) i32 {
|
||||
return self.colno;
|
||||
}
|
||||
|
||||
pub fn get_error(self: *const ErrorEvent) Env.UndefinedOr(Env.JsObject) {
|
||||
if (self.@"error") |e| {
|
||||
return .{ .value = e };
|
||||
}
|
||||
return .undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.ErrorEvent" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=c></div>" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let e1 = new ErrorEvent('err1')", null },
|
||||
.{ "e1.message", "" },
|
||||
.{ "e1.filename", "" },
|
||||
.{ "e1.lineno", "0" },
|
||||
.{ "e1.colno", "0" },
|
||||
.{ "e1.error", "undefined" },
|
||||
|
||||
.{
|
||||
\\ let e2 = new ErrorEvent('err1', {
|
||||
\\ message: 'm1',
|
||||
\\ filename: 'fx19',
|
||||
\\ lineno: 443,
|
||||
\\ colno: 8999,
|
||||
\\ error: 'under 9000!',
|
||||
\\
|
||||
\\})
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "e2.message", "m1" },
|
||||
.{ "e2.filename", "fx19" },
|
||||
.{ "e2.lineno", "443" },
|
||||
.{ "e2.colno", "8999" },
|
||||
.{ "e2.error", "under 9000!" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -15,28 +15,24 @@
|
||||
//
|
||||
// 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");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||
const FormData = @import("../xhr/form_data.zig").FormData;
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
pub const HTMLFormElement = struct {
|
||||
pub const Self = parser.Form;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#documenttype
|
||||
pub const DocumentType = struct {
|
||||
pub const Self = parser.DocumentType;
|
||||
pub const prototype = *Node;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn get_name(self: *parser.DocumentType) ![]const u8 {
|
||||
return try parser.documentTypeGetName(self);
|
||||
pub fn _submit(self: *parser.Form, page: *Page) !void {
|
||||
return page.submitForm(self, null);
|
||||
}
|
||||
|
||||
pub fn get_publicId(self: *parser.DocumentType) ![]const u8 {
|
||||
return try parser.documentTypeGetPublicId(self);
|
||||
}
|
||||
|
||||
pub fn get_systemId(self: *parser.DocumentType) ![]const u8 {
|
||||
return try parser.documentTypeGetSystemId(self);
|
||||
pub fn _reset(self: *parser.Form) !void {
|
||||
try parser.formElementReset(self);
|
||||
}
|
||||
};
|
||||
120
src/browser/html/history.zig
Normal file
120
src/browser/html/history.zig
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
||||
pub const History = struct {
|
||||
const ScrollRestorationMode = enum {
|
||||
auto,
|
||||
manual,
|
||||
};
|
||||
|
||||
scrollRestoration: ScrollRestorationMode = .auto,
|
||||
state: std.json.Value = .null,
|
||||
|
||||
// count tracks the history length until we implement correctly pushstate.
|
||||
count: u32 = 0,
|
||||
|
||||
pub fn get_length(self: *History) u32 {
|
||||
// TODO return the real history length value.
|
||||
return self.count;
|
||||
}
|
||||
|
||||
pub fn get_scrollRestoration(self: *History) []const u8 {
|
||||
return switch (self.scrollRestoration) {
|
||||
.auto => "auto",
|
||||
.manual => "manual",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
|
||||
if (std.mem.eql(u8, "manual", mode)) self.scrollRestoration = .manual;
|
||||
if (std.mem.eql(u8, "auto", mode)) self.scrollRestoration = .auto;
|
||||
}
|
||||
|
||||
pub fn get_state(self: *History) std.json.Value {
|
||||
return self.state;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
// data must handle any argument. We could expect a std.json.Value but
|
||||
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
|
||||
pub fn _pushState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
|
||||
self.count += 1;
|
||||
_ = url;
|
||||
_ = data;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
// data must handle any argument. We could expect a std.json.Value but
|
||||
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
|
||||
pub fn _replaceState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
|
||||
_ = self;
|
||||
_ = url;
|
||||
_ = data;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
pub fn _go(self: *History, delta: ?i32) void {
|
||||
_ = self;
|
||||
_ = delta;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
pub fn _back(self: *History) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
pub fn _forward(self: *History) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.History" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "history.scrollRestoration", "auto" },
|
||||
.{ "history.scrollRestoration = 'manual'", "manual" },
|
||||
.{ "history.scrollRestoration = 'foo'", "foo" },
|
||||
.{ "history.scrollRestoration", "manual" },
|
||||
.{ "history.scrollRestoration = 'auto'", "auto" },
|
||||
.{ "history.scrollRestoration", "auto" },
|
||||
|
||||
.{ "history.state", "null" },
|
||||
|
||||
.{ "history.pushState({}, null, '')", "undefined" },
|
||||
|
||||
.{ "history.replaceState({}, null, '')", "undefined" },
|
||||
|
||||
.{ "history.go()", "undefined" },
|
||||
.{ "history.go(1)", "undefined" },
|
||||
.{ "history.go(-1)", "undefined" },
|
||||
|
||||
.{ "history.forward()", "undefined" },
|
||||
|
||||
.{ "history.back()", "undefined" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -16,30 +16,28 @@
|
||||
// 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 generate = @import("generate.zig");
|
||||
const HTMLDocument = @import("document.zig").HTMLDocument;
|
||||
const HTMLElem = @import("elements.zig");
|
||||
const SVGElem = @import("svg_elements.zig");
|
||||
const Window = @import("window.zig").Window;
|
||||
const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
const Location = @import("location.zig").Location;
|
||||
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
|
||||
|
||||
const Console = @import("jsruntime").Console;
|
||||
|
||||
const DOM = @import("dom/dom.zig");
|
||||
const HTML = @import("html/html.zig");
|
||||
const Events = @import("events/event.zig");
|
||||
const XHR = @import("xhr/xhr.zig");
|
||||
const Storage = @import("storage/storage.zig");
|
||||
const URL = @import("url/url.zig");
|
||||
const Iterators = @import("iterator/iterator.zig");
|
||||
|
||||
pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
||||
|
||||
// Interfaces
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
Console,
|
||||
DOM.Interfaces,
|
||||
Events.Interfaces,
|
||||
HTML.Interfaces,
|
||||
XHR.Interfaces,
|
||||
Storage.Interfaces,
|
||||
URL.Interfaces,
|
||||
Iterators.Interfaces,
|
||||
});
|
||||
|
||||
pub const UserContext = @import("user_context.zig").UserContext;
|
||||
pub const Interfaces = .{
|
||||
HTMLDocument,
|
||||
HTMLElem.HTMLElement,
|
||||
HTMLElem.HTMLMediaElement,
|
||||
HTMLElem.Interfaces,
|
||||
SVGElem.SVGElement,
|
||||
Window,
|
||||
Navigator,
|
||||
History,
|
||||
Location,
|
||||
MediaQueryList,
|
||||
@import("DataSet.zig"),
|
||||
@import("screen.zig").Interfaces,
|
||||
@import("error_event.zig").ErrorEvent,
|
||||
@import("AbortController.zig").Interfaces,
|
||||
};
|
||||
106
src/browser/html/location.zig
Normal file
106
src/browser/html/location.zig
Normal file
@@ -0,0 +1,106 @@
|
||||
// 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 Page = @import("../page.zig").Page;
|
||||
|
||||
const URL = @import("../url/url.zig").URL;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface
|
||||
pub const Location = struct {
|
||||
url: ?URL = null,
|
||||
|
||||
pub fn get_href(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_href(page);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_protocol(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_protocol(page);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_host(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_host(page);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_hostname(self: *Location) []const u8 {
|
||||
if (self.url) |*u| return u.get_hostname();
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_port(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_port(page);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_pathname(self: *Location) []const u8 {
|
||||
if (self.url) |*u| return u.get_pathname();
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_search(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_search(page);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_hash(page);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_origin(page);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn _reload(_: *const Location, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
|
||||
return try self.get_href(page);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Location" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "location.href", "https://lightpanda.io/opensource-browser/" },
|
||||
.{ "document.location.href", "https://lightpanda.io/opensource-browser/" },
|
||||
|
||||
.{ "location.host", "lightpanda.io" },
|
||||
.{ "location.hostname", "lightpanda.io" },
|
||||
.{ "location.origin", "https://lightpanda.io" },
|
||||
.{ "location.pathname", "/opensource-browser/" },
|
||||
.{ "location.hash", "" },
|
||||
.{ "location.port", "" },
|
||||
.{ "location.search", "" },
|
||||
}, .{});
|
||||
}
|
||||
45
src/browser/html/media_query_list.zig
Normal file
45
src/browser/html/media_query_list.zig
Normal file
@@ -0,0 +1,45 @@
|
||||
// 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 parser = @import("../netsurf.zig");
|
||||
const Function = @import("../env.zig").Function;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
// https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface
|
||||
pub const MediaQueryList = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
// This is not safe as it relies on a structure layout that isn't guaranteed
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .media_query_list },
|
||||
|
||||
matches: bool,
|
||||
media: []const u8,
|
||||
|
||||
pub fn get_matches(self: *const MediaQueryList) bool {
|
||||
return self.matches;
|
||||
}
|
||||
|
||||
pub fn get_media(self: *const MediaQueryList) []const u8 {
|
||||
return self.media;
|
||||
}
|
||||
|
||||
pub fn _addListener(_: *const MediaQueryList, _: Function) void {}
|
||||
|
||||
pub fn _removeListener(_: *const MediaQueryList, _: Function) void {}
|
||||
};
|
||||
96
src/browser/html/navigator.zig
Normal file
96
src/browser/html/navigator.zig
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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");
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/system-state.html#navigator
|
||||
pub const Navigator = struct {
|
||||
agent: []const u8 = "Lightpanda/1.0",
|
||||
version: []const u8 = "1.0",
|
||||
vendor: []const u8 = "",
|
||||
platform: []const u8 = std.fmt.comptimePrint("{any} {any}", .{ builtin.os.tag, builtin.cpu.arch }),
|
||||
|
||||
language: []const u8 = "en-US",
|
||||
|
||||
pub fn get_userAgent(self: *Navigator) []const u8 {
|
||||
return self.agent;
|
||||
}
|
||||
pub fn get_appCodeName(_: *Navigator) []const u8 {
|
||||
return "Mozilla";
|
||||
}
|
||||
pub fn get_appName(_: *Navigator) []const u8 {
|
||||
return "Netscape";
|
||||
}
|
||||
pub fn get_appVersion(self: *Navigator) []const u8 {
|
||||
return self.version;
|
||||
}
|
||||
pub fn get_platform(self: *Navigator) []const u8 {
|
||||
return self.platform;
|
||||
}
|
||||
pub fn get_product(_: *Navigator) []const u8 {
|
||||
return "Gecko";
|
||||
}
|
||||
pub fn get_productSub(_: *Navigator) []const u8 {
|
||||
return "20030107";
|
||||
}
|
||||
pub fn get_vendor(self: *Navigator) []const u8 {
|
||||
return self.vendor;
|
||||
}
|
||||
pub fn get_vendorSub(_: *Navigator) []const u8 {
|
||||
return "";
|
||||
}
|
||||
pub fn get_language(self: *Navigator) []const u8 {
|
||||
return self.language;
|
||||
}
|
||||
// TODO wait for arrays.
|
||||
//pub fn get_languages(self: *Navigator) [][]const u8 {
|
||||
// return .{self.language};
|
||||
//}
|
||||
pub fn get_online(_: *Navigator) bool {
|
||||
return true;
|
||||
}
|
||||
pub fn _registerProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
|
||||
_ = scheme;
|
||||
_ = url;
|
||||
}
|
||||
pub fn _unregisterProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
|
||||
_ = scheme;
|
||||
_ = url;
|
||||
}
|
||||
|
||||
pub fn get_cookieEnabled(_: *Navigator) bool {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Navigator" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "navigator.userAgent", "Lightpanda/1.0" },
|
||||
.{ "navigator.appVersion", "1.0" },
|
||||
.{ "navigator.language", "en-US" },
|
||||
}, .{});
|
||||
}
|
||||
109
src/browser/html/screen.zig
Normal file
109
src/browser/html/screen.zig
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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 EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
pub const Interfaces = .{
|
||||
Screen,
|
||||
ScreenOrientation,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen
|
||||
pub const Screen = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
height: u32 = 1080,
|
||||
width: u32 = 1920,
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/colorDepth
|
||||
color_depth: u32 = 8,
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/pixelDepth
|
||||
pixel_depth: u32 = 8,
|
||||
orientation: ScreenOrientation = .{ .type = .landscape_primary },
|
||||
|
||||
pub fn get_availHeight(self: *const Screen) u32 {
|
||||
return self.height;
|
||||
}
|
||||
|
||||
pub fn get_availWidth(self: *const Screen) u32 {
|
||||
return self.width;
|
||||
}
|
||||
|
||||
pub fn get_height(self: *const Screen) u32 {
|
||||
return self.height;
|
||||
}
|
||||
|
||||
pub fn get_width(self: *const Screen) u32 {
|
||||
return self.width;
|
||||
}
|
||||
|
||||
pub fn get_pixelDepth(self: *const Screen) u32 {
|
||||
return self.pixel_depth;
|
||||
}
|
||||
|
||||
pub fn get_orientation(self: *const Screen) ScreenOrientation {
|
||||
return self.orientation;
|
||||
}
|
||||
};
|
||||
|
||||
const ScreenOrientationType = enum {
|
||||
portrait_primary,
|
||||
portrait_secondary,
|
||||
landscape_primary,
|
||||
landscape_secondary,
|
||||
|
||||
pub fn toString(self: ScreenOrientationType) []const u8 {
|
||||
return switch (self) {
|
||||
.portrait_primary => "portrait-primary",
|
||||
.portrait_secondary => "portrait-secondary",
|
||||
.landscape_primary => "landscape-primary",
|
||||
.landscape_secondary => "landscape-secondary",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const ScreenOrientation = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
angle: u32 = 0,
|
||||
type: ScreenOrientationType,
|
||||
|
||||
pub fn get_angle(self: *const ScreenOrientation) u32 {
|
||||
return self.angle;
|
||||
}
|
||||
|
||||
pub fn get_type(self: *const ScreenOrientation) []const u8 {
|
||||
return self.type.toString();
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Screen" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let screen = window.screen", "undefined" },
|
||||
.{ "screen.width === 1920", "true" },
|
||||
.{ "screen.height === 1080", "true" },
|
||||
.{ "let orientation = screen.orientation", "undefined" },
|
||||
.{ "orientation.angle === 0", "true" },
|
||||
.{ "orientation.type === \"landscape-primary\"", "true" },
|
||||
}, .{});
|
||||
}
|
||||
144
src/browser/html/select.zig
Normal file
144
src/browser/html/select.zig
Normal file
@@ -0,0 +1,144 @@
|
||||
// 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 HTMLElement = @import("elements.zig").HTMLElement;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
pub const HTMLSelectElement = struct {
|
||||
pub const Self = parser.Select;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_length(select: *parser.Select) !u32 {
|
||||
return parser.selectGetLength(select);
|
||||
}
|
||||
|
||||
pub fn get_form(select: *parser.Select) !?*parser.Form {
|
||||
return parser.selectGetForm(select);
|
||||
}
|
||||
|
||||
pub fn get_name(select: *parser.Select) ![]const u8 {
|
||||
return parser.selectGetName(select);
|
||||
}
|
||||
pub fn set_name(select: *parser.Select, name: []const u8) !void {
|
||||
return parser.selectSetName(select, name);
|
||||
}
|
||||
|
||||
pub fn get_disabled(select: *parser.Select) !bool {
|
||||
return parser.selectGetDisabled(select);
|
||||
}
|
||||
pub fn set_disabled(select: *parser.Select, disabled: bool) !void {
|
||||
return parser.selectSetDisabled(select, disabled);
|
||||
}
|
||||
|
||||
pub fn get_multiple(select: *parser.Select) !bool {
|
||||
return parser.selectGetMultiple(select);
|
||||
}
|
||||
pub fn set_multiple(select: *parser.Select, multiple: bool) !void {
|
||||
return parser.selectSetMultiple(select, multiple);
|
||||
}
|
||||
|
||||
pub fn get_selectedIndex(select: *parser.Select, page: *Page) !i32 {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(select)));
|
||||
const selected_index = try parser.selectGetSelectedIndex(select);
|
||||
|
||||
// See the explicit_index_set field documentation
|
||||
if (!state.explicit_index_set) {
|
||||
if (selected_index == -1) {
|
||||
if (try parser.selectGetMultiple(select) == false) {
|
||||
if (try get_length(select) > 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected_index;
|
||||
}
|
||||
|
||||
// Libdom's dom_html_select_select_set_selected_index will crash if index
|
||||
// is out of range, and it doesn't properly unset options
|
||||
pub fn set_selectedIndex(select: *parser.Select, index: i32, page: *Page) !void {
|
||||
var state = try page.getOrCreateNodeState(@alignCast(@ptrCast(select)));
|
||||
state.explicit_index_set = true;
|
||||
|
||||
const options = try parser.selectGetOptions(select);
|
||||
const len = try parser.optionCollectionGetLength(options);
|
||||
for (0..len) |i| {
|
||||
const option = try parser.optionCollectionItem(options, @intCast(i));
|
||||
try parser.optionSetSelected(option, false);
|
||||
}
|
||||
if (index >= 0 and index < try get_length(select)) {
|
||||
const option = try parser.optionCollectionItem(options, @intCast(index));
|
||||
try parser.optionSetSelected(option, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Select" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
||||
\\ <form id=f1>
|
||||
\\ <select id=s1 name=s1><option>o1<option>o2</select>
|
||||
\\ </form>
|
||||
\\ <select id=s2></select>
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const s = document.getElementById('s1');", null },
|
||||
.{ "s.form", "[object HTMLFormElement]" },
|
||||
|
||||
.{ "document.getElementById('s2').form", "null" },
|
||||
|
||||
.{ "s.disabled", "false" },
|
||||
.{ "s.disabled = true", null },
|
||||
.{ "s.disabled", "true" },
|
||||
.{ "s.disabled = false", null },
|
||||
.{ "s.disabled", "false" },
|
||||
|
||||
.{ "s.multiple", "false" },
|
||||
.{ "s.multiple = true", null },
|
||||
.{ "s.multiple", "true" },
|
||||
.{ "s.multiple = false", null },
|
||||
.{ "s.multiple", "false" },
|
||||
|
||||
.{ "s.name;", "s1" },
|
||||
.{ "s.name = 'sel1';", null },
|
||||
.{ "s.name", "sel1" },
|
||||
|
||||
.{ "s.length;", "2" },
|
||||
|
||||
.{ "s.selectedIndex", "0" },
|
||||
.{ "s.selectedIndex = 2", null }, // out of range
|
||||
.{ "s.selectedIndex", "-1" },
|
||||
|
||||
.{ "s.selectedIndex = -1", null },
|
||||
.{ "s.selectedIndex", "-1" },
|
||||
|
||||
.{ "s.selectedIndex = 0", null },
|
||||
.{ "s.selectedIndex", "0" },
|
||||
|
||||
.{ "s.selectedIndex = 1", null },
|
||||
.{ "s.selectedIndex", "1" },
|
||||
|
||||
.{ "s.selectedIndex = -323", null },
|
||||
.{ "s.selectedIndex", "-1" },
|
||||
}, .{});
|
||||
}
|
||||
41
src/browser/html/svg_elements.zig
Normal file
41
src/browser/html/svg_elements.zig
Normal file
@@ -0,0 +1,41 @@
|
||||
// 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 Element = @import("../dom/element.zig").Element;
|
||||
|
||||
// Support for SVGElements is very limited, this is a dummy implementation.
|
||||
// This is here no to be able to support `element instanceof SVGElement;` in JavaScript.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/SVGElement
|
||||
pub const SVGElement = struct {
|
||||
// Currently the prototype chain is not implemented (will not be returned by toInterface())
|
||||
// For that we need parser.SvgElement and the derived types with tags in the v-table.
|
||||
pub const prototype = *Element;
|
||||
// While this is a Node, could consider not exposing the subtype untill we have
|
||||
// a Self type to cast to.
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.SVGElement" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "'AString' instanceof SVGElement", "false" },
|
||||
}, .{});
|
||||
}
|
||||
528
src/browser/html/window.zig
Normal file
528
src/browser/html/window.zig
Normal file
@@ -0,0 +1,528 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
||||
|
||||
const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
const Location = @import("location.zig").Location;
|
||||
const Crypto = @import("../crypto/crypto.zig").Crypto;
|
||||
const Console = @import("../console/console.zig").Console;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
|
||||
const Performance = @import("../dom/performance.zig").Performance;
|
||||
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
const Screen = @import("screen.zig").Screen;
|
||||
const Css = @import("../css/css.zig").Css;
|
||||
|
||||
const Function = Env.Function;
|
||||
const JsObject = Env.JsObject;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-window-extensions
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
|
||||
pub const Window = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .window },
|
||||
|
||||
document: *parser.DocumentHTML,
|
||||
target: []const u8 = "",
|
||||
history: History = .{},
|
||||
location: Location = .{},
|
||||
storage_shelf: ?*storage.Shelf = null,
|
||||
|
||||
// counter for having unique timer ids
|
||||
timer_id: u30 = 0,
|
||||
timers: std.AutoHashMapUnmanaged(u32, *TimerCallback) = .{},
|
||||
|
||||
crypto: Crypto = .{},
|
||||
console: Console = .{},
|
||||
navigator: Navigator = .{},
|
||||
performance: Performance,
|
||||
screen: Screen = .{},
|
||||
css: Css = .{},
|
||||
|
||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
|
||||
var fbs = std.io.fixedBufferStream("");
|
||||
const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8");
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
try parser.documentSetDocumentURI(doc, "about:blank");
|
||||
|
||||
return .{
|
||||
.document = html_doc,
|
||||
.target = target orelse "",
|
||||
.navigator = navigator orelse .{},
|
||||
.performance = .{ .time_origin = try std.time.Timer.start() },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn replaceLocation(self: *Window, loc: Location) !void {
|
||||
self.location = loc;
|
||||
try parser.documentHTMLSetLocation(Location, self.document, &self.location);
|
||||
}
|
||||
|
||||
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
|
||||
self.performance.time_origin.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
|
||||
self.document = doc;
|
||||
try parser.documentHTMLSetLocation(Location, doc, &self.location);
|
||||
}
|
||||
|
||||
pub fn setStorageShelf(self: *Window, shelf: *storage.Shelf) void {
|
||||
self.storage_shelf = shelf;
|
||||
}
|
||||
|
||||
pub fn get_window(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn get_navigator(self: *Window) *Navigator {
|
||||
return &self.navigator;
|
||||
}
|
||||
|
||||
pub fn get_location(self: *Window) *Location {
|
||||
return &self.location;
|
||||
}
|
||||
|
||||
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn get_console(self: *Window) *Console {
|
||||
return &self.console;
|
||||
}
|
||||
|
||||
pub fn get_crypto(self: *Window) *Crypto {
|
||||
return &self.crypto;
|
||||
}
|
||||
|
||||
pub fn get_self(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn get_parent(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
|
||||
// TODO: frames
|
||||
pub fn get_top(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn get_document(self: *Window) ?*parser.DocumentHTML {
|
||||
return self.document;
|
||||
}
|
||||
|
||||
pub fn get_history(self: *Window) *History {
|
||||
return &self.history;
|
||||
}
|
||||
|
||||
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
|
||||
pub fn get_innerHeight(_: *Window, page: *Page) u32 {
|
||||
// We do not have scrollbars or padding so this is the same as Element.clientHeight
|
||||
return page.renderer.height();
|
||||
}
|
||||
|
||||
// The interior width of the window in pixels. That includes the width of the vertical scroll bar, if one is present.
|
||||
pub fn get_innerWidth(_: *Window, page: *Page) u32 {
|
||||
// We do not have scrollbars or padding so this is the same as Element.clientWidth
|
||||
return page.renderer.width();
|
||||
}
|
||||
|
||||
pub fn get_name(self: *Window) []const u8 {
|
||||
return self.target;
|
||||
}
|
||||
|
||||
pub fn get_localStorage(self: *Window) !*storage.Bottle {
|
||||
if (self.storage_shelf == null) return parser.DOMError.NotSupported;
|
||||
return &self.storage_shelf.?.bucket.local;
|
||||
}
|
||||
|
||||
pub fn get_sessionStorage(self: *Window) !*storage.Bottle {
|
||||
if (self.storage_shelf == null) return parser.DOMError.NotSupported;
|
||||
return &self.storage_shelf.?.bucket.session;
|
||||
}
|
||||
|
||||
pub fn get_performance(self: *Window) *Performance {
|
||||
return &self.performance;
|
||||
}
|
||||
|
||||
pub fn get_screen(self: *Window) *Screen {
|
||||
return &self.screen;
|
||||
}
|
||||
|
||||
pub fn get_CSS(self: *Window) *Css {
|
||||
return &self.css;
|
||||
}
|
||||
|
||||
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 5, page, .{ .animation_frame = true });
|
||||
}
|
||||
|
||||
pub fn _cancelAnimationFrame(self: *Window, id: u32, page: *Page) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
return page.loop.cancel(kv.value.loop_id);
|
||||
}
|
||||
|
||||
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .args = params });
|
||||
}
|
||||
|
||||
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params });
|
||||
}
|
||||
|
||||
pub fn _clearTimeout(self: *Window, id: u32, page: *Page) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
return page.loop.cancel(kv.value.loop_id);
|
||||
}
|
||||
|
||||
pub fn _clearInterval(self: *Window, id: u32, page: *Page) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
return page.loop.cancel(kv.value.loop_id);
|
||||
}
|
||||
|
||||
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 0, page, .{});
|
||||
}
|
||||
|
||||
pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
|
||||
return .{
|
||||
.matches = false, // TODO?
|
||||
.media = try page.arena.dupe(u8, media),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _btoa(_: *const Window, value: []const u8, page: *Page) ![]const u8 {
|
||||
const Encoder = std.base64.standard.Encoder;
|
||||
const out = try page.call_arena.alloc(u8, Encoder.calcSize(value.len));
|
||||
return Encoder.encode(out, value);
|
||||
}
|
||||
|
||||
pub fn _atob(_: *const Window, value: []const u8, page: *Page) ![]const u8 {
|
||||
const Decoder = std.base64.standard.Decoder;
|
||||
const size = Decoder.calcSizeForSlice(value) catch return error.InvalidCharacterError;
|
||||
|
||||
const out = try page.call_arena.alloc(u8, size);
|
||||
Decoder.decode(out, value) catch return error.InvalidCharacterError;
|
||||
return out;
|
||||
}
|
||||
|
||||
const CreateTimeoutOpts = struct {
|
||||
args: []Env.JsObject = &.{},
|
||||
repeat: bool = false,
|
||||
animation_frame: bool = false,
|
||||
};
|
||||
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
|
||||
const delay = delay_ orelse 0;
|
||||
if (delay > 5000) {
|
||||
log.warn(.user_script, "long timeout ignored", .{ .delay = delay, .interval = opts.repeat });
|
||||
// self.timer_id is u30, so the largest value we can generate is
|
||||
// 1_073_741_824. Returning 2_000_000_000 makes sure that clients
|
||||
// can call cancelTimer/cancelInterval without breaking anything.
|
||||
return 2_000_000_000;
|
||||
}
|
||||
|
||||
if (self.timers.count() > 512) {
|
||||
return error.TooManyTimeout;
|
||||
}
|
||||
const timer_id = self.timer_id +% 1;
|
||||
self.timer_id = timer_id;
|
||||
|
||||
const arena = page.arena;
|
||||
|
||||
const gop = try self.timers.getOrPut(arena, timer_id);
|
||||
if (gop.found_existing) {
|
||||
// this can only happen if we've created 2^31 timeouts.
|
||||
return error.TooManyTimeout;
|
||||
}
|
||||
errdefer _ = self.timers.remove(timer_id);
|
||||
|
||||
const args = opts.args;
|
||||
var persisted_args: []Env.JsObject = &.{};
|
||||
if (args.len > 0) {
|
||||
persisted_args = try page.arena.alloc(Env.JsObject, args.len);
|
||||
for (args, persisted_args) |a, *ca| {
|
||||
ca.* = try a.persist();
|
||||
}
|
||||
}
|
||||
|
||||
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
|
||||
const callback = try arena.create(TimerCallback);
|
||||
|
||||
callback.* = .{
|
||||
.cbk = cbk,
|
||||
.loop_id = 0, // we're going to set this to a real value shortly
|
||||
.window = self,
|
||||
.timer_id = timer_id,
|
||||
.args = persisted_args,
|
||||
.node = .{ .func = TimerCallback.run },
|
||||
.repeat = if (opts.repeat) delay_ms else null,
|
||||
.animation_frame = opts.animation_frame,
|
||||
};
|
||||
callback.loop_id = try page.loop.timeout(delay_ms, &callback.node);
|
||||
|
||||
gop.value_ptr.* = callback;
|
||||
return timer_id;
|
||||
}
|
||||
|
||||
// TODO: getComputedStyle should return a read-only CSSStyleDeclaration.
|
||||
// We currently don't have a read-only one, so we return a new instance on
|
||||
// each call.
|
||||
pub fn _getComputedStyle(_: *const Window, element: *parser.Element, pseudo_element: ?[]const u8) !CSSStyleDeclaration {
|
||||
_ = element;
|
||||
_ = pseudo_element;
|
||||
return .empty;
|
||||
}
|
||||
|
||||
const ScrollToOpts = union(enum) {
|
||||
x: i32,
|
||||
opts: Opts,
|
||||
|
||||
const Opts = struct {
|
||||
top: i32,
|
||||
left: i32,
|
||||
behavior: []const u8,
|
||||
};
|
||||
};
|
||||
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?u32) !void {
|
||||
_ = opts;
|
||||
_ = y;
|
||||
|
||||
{
|
||||
const scroll_event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(scroll_event);
|
||||
|
||||
try parser.eventInit(scroll_event, "scroll", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(Window, self),
|
||||
scroll_event,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const scroll_end = try parser.eventCreate();
|
||||
defer parser.eventDestroy(scroll_end);
|
||||
|
||||
try parser.eventInit(scroll_end, "scrollend", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(parser.DocumentHTML, self.document),
|
||||
scroll_end,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// libdom's document doesn't have a parent, which is correct, but
|
||||
// breaks the event bubbling that happens for many events from
|
||||
// document -> window.
|
||||
// We need to force dispatch this event on the window, with the
|
||||
// document target.
|
||||
// In theory, we should do this for a lot of events and might need
|
||||
// to come up with a good way to solve this more generically. But
|
||||
// this specific event, and maybe a few others in the near future,
|
||||
// are blockers.
|
||||
// Worth noting that NetSurf itself appears to do something similar:
|
||||
// https://github.com/netsurf-browser/netsurf/blob/a32e1a03e1c91ee9f0aa211937dbae7a96831149/content/handlers/html/html.c#L380
|
||||
pub fn dispatchForDocumentTarget(self: *Window, evt: *parser.Event) !void {
|
||||
// we assume that this evt has already been dispatched on the document
|
||||
// and thus the target has already been set to the document.
|
||||
return self.base.redispatchEvent(evt);
|
||||
}
|
||||
};
|
||||
|
||||
const TimerCallback = struct {
|
||||
// the internal loop id, need it when cancelling
|
||||
loop_id: usize,
|
||||
|
||||
// the id of our timer (windows.timers key)
|
||||
timer_id: u31,
|
||||
|
||||
// The JavaScript callback to execute
|
||||
cbk: Function,
|
||||
|
||||
// This is the internal data that the event loop tracks. We'll get this
|
||||
// back in run and, from it, can get our TimerCallback instance
|
||||
node: Loop.CallbackNode = undefined,
|
||||
|
||||
// if the event should be repeated
|
||||
repeat: ?u63 = null,
|
||||
|
||||
animation_frame: bool = false,
|
||||
|
||||
window: *Window,
|
||||
|
||||
args: []Env.JsObject = &.{},
|
||||
|
||||
fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
const self: *TimerCallback = @fieldParentPtr("node", node);
|
||||
|
||||
var result: Function.Result = undefined;
|
||||
|
||||
var call: anyerror!void = undefined;
|
||||
if (self.animation_frame) {
|
||||
call = self.cbk.tryCall(void, .{self.window.performance._now()}, &result);
|
||||
} else {
|
||||
call = self.cbk.tryCall(void, self.args, &result);
|
||||
}
|
||||
|
||||
call catch {
|
||||
log.warn(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.source = "window timeout",
|
||||
});
|
||||
};
|
||||
|
||||
if (self.repeat) |r| {
|
||||
// setInterval
|
||||
repeat_delay.* = r;
|
||||
return;
|
||||
}
|
||||
|
||||
// setTimeout
|
||||
_ = self.window.timers.remove(self.timer_id);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Window" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "window.parent === window", "true" },
|
||||
.{ "window.top === window", "true" },
|
||||
}, .{});
|
||||
|
||||
// requestAnimationFrame should be able to wait by recursively calling itself
|
||||
// Note however that we in this test do not wait as the request is just send to the browser
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let start = 0;
|
||||
\\ function step(timestamp) {
|
||||
\\ start = timestamp;
|
||||
\\ }
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "requestAnimationFrame(step);", null }, // returned id is checked in the next test
|
||||
.{ " start > 0", "true" },
|
||||
}, .{});
|
||||
|
||||
// cancelAnimationFrame should be able to cancel a request with the given id
|
||||
try runner.testCases(&.{
|
||||
.{ "let request_id = requestAnimationFrame(timestamp => {});", null },
|
||||
.{ "cancelAnimationFrame(request_id);", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "innerHeight", "1" },
|
||||
.{ "innerWidth", "1" }, // Width is 1 even if there are no elements
|
||||
.{
|
||||
\\ let div1 = document.createElement('div');
|
||||
\\ document.body.appendChild(div1);
|
||||
\\ div1.getClientRects();
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{
|
||||
\\ let div2 = document.createElement('div');
|
||||
\\ document.body.appendChild(div2);
|
||||
\\ div2.getClientRects();
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "innerHeight", "1" },
|
||||
.{ "innerWidth", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let longCall = false;", null },
|
||||
.{ "window.setTimeout(() => {longCall = true}, 5001);", null },
|
||||
.{ "longCall;", "false" },
|
||||
|
||||
.{ "let wst = 0;", null },
|
||||
.{ "window.setTimeout(() => {wst += 1}, 1)", null },
|
||||
.{ "wst", "1" },
|
||||
|
||||
.{ "window.setTimeout((a, b) => {wst = a + b}, 1, 2, 3)", null },
|
||||
.{ "wst", "5" },
|
||||
}, .{});
|
||||
|
||||
// window event target
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let called = false;
|
||||
\\ window.addEventListener("ready", (e) => {
|
||||
\\ called = (e.currentTarget == window);
|
||||
\\ }, {capture: false, once: false});
|
||||
\\ const evt = new Event("ready", { bubbles: true, cancelable: false });
|
||||
\\ window.dispatchEvent(evt);
|
||||
\\ called;
|
||||
,
|
||||
"true",
|
||||
},
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const b64 = btoa('https://ziglang.org/documentation/master/std/#std.base64.Base64Decoder')", "undefined" },
|
||||
.{ "b64", "aHR0cHM6Ly96aWdsYW5nLm9yZy9kb2N1bWVudGF0aW9uL21hc3Rlci9zdGQvI3N0ZC5iYXNlNjQuQmFzZTY0RGVjb2Rlcg==" },
|
||||
.{ "const str = atob(b64)", "undefined" },
|
||||
.{ "str", "https://ziglang.org/documentation/master/std/#std.base64.Base64Decoder" },
|
||||
.{ "try { atob('b') } catch (e) { e } ", "Error: InvalidCharacterError" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let scroll = false; let scrolend = false", null },
|
||||
.{ "window.addEventListener('scroll', () => {scroll = true});", null },
|
||||
.{ "document.addEventListener('scrollend', () => {scrollend = true});", null },
|
||||
.{ "window.scrollTo(0)", null },
|
||||
.{ "scroll", "true" },
|
||||
.{ "scrollend", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var qm = false; window.queueMicrotask(() => {qm = true });", null },
|
||||
.{ "qm", "true" },
|
||||
}, .{});
|
||||
|
||||
{
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let dcl = false;
|
||||
\\ window.addEventListener('DOMContentLoaded', (e) => {
|
||||
\\ dcl = e.target == document;
|
||||
\\ });
|
||||
,
|
||||
null,
|
||||
},
|
||||
}, .{});
|
||||
try runner.dispatchDOMContentLoaded();
|
||||
try runner.testCases(&.{
|
||||
.{ "dcl", "true" },
|
||||
}, .{});
|
||||
}
|
||||
}
|
||||
226
src/browser/iterator/iterator.zig
Normal file
226
src/browser/iterator/iterator.zig
Normal file
@@ -0,0 +1,226 @@
|
||||
pub const Interfaces = .{
|
||||
U32Iterator,
|
||||
};
|
||||
|
||||
pub const U32Iterator = struct {
|
||||
length: u32,
|
||||
index: u32 = 0,
|
||||
|
||||
pub const Return = struct {
|
||||
value: u32,
|
||||
done: bool,
|
||||
};
|
||||
|
||||
pub fn _next(self: *U32Iterator) Return {
|
||||
const i = self.index;
|
||||
if (i >= self.length) {
|
||||
return .{
|
||||
.value = 0,
|
||||
.done = true,
|
||||
};
|
||||
}
|
||||
|
||||
self.index = i + 1;
|
||||
return .{
|
||||
.value = i,
|
||||
.done = false,
|
||||
};
|
||||
}
|
||||
|
||||
// Iterators should be iterable. There's a [JS] example on MDN that
|
||||
// suggests this is the correct approach:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol
|
||||
pub fn _symbol_iterator(self: *U32Iterator) *U32Iterator {
|
||||
return self;
|
||||
}
|
||||
};
|
||||
|
||||
// A wrapper around an iterator that emits an Iterable result
|
||||
// An iterable has a next() which emits a {done: bool, value: T} result
|
||||
pub fn Iterable(comptime T: type, comptime JsName: []const u8) type {
|
||||
// The inner iterator's return type.
|
||||
// Maybe an error union.
|
||||
// Definitely an optional
|
||||
const RawValue = @typeInfo(@TypeOf(T._next)).@"fn".return_type.?;
|
||||
const CanError = @typeInfo(RawValue) == .error_union;
|
||||
|
||||
const Value = blk: {
|
||||
// Unwrap the RawValue
|
||||
var V = RawValue;
|
||||
if (CanError) {
|
||||
V = @typeInfo(V).error_union.payload;
|
||||
}
|
||||
break :blk @typeInfo(V).optional.child;
|
||||
};
|
||||
|
||||
const Result = struct {
|
||||
done: bool,
|
||||
// todo, technically, we should return undefined when done = true
|
||||
// or even omit the value;
|
||||
value: ?Value,
|
||||
};
|
||||
|
||||
const ReturnType = if (CanError) T.Error!Result else Result;
|
||||
|
||||
return struct {
|
||||
// the inner value iterator
|
||||
inner: T,
|
||||
|
||||
// Generics don't generate clean names. Can't just take the resulting
|
||||
// type name and use that as a the JS class name. So we always ask for
|
||||
// an explicit JS class name
|
||||
pub const js_name = JsName;
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(inner: T) Self {
|
||||
return .{ .inner = inner };
|
||||
}
|
||||
|
||||
pub fn _next(self: *Self) ReturnType {
|
||||
const value = if (comptime CanError) try self.inner._next() else self.inner._next();
|
||||
return .{ .done = value == null, .value = value };
|
||||
}
|
||||
|
||||
pub fn _symbol_iterator(self: *Self) *Self {
|
||||
return self;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// A wrapper around an iterator that emits integer/index keyed entries.
|
||||
pub fn NumericEntries(comptime T: type, comptime JsName: []const u8) type {
|
||||
// The inner iterator's return type.
|
||||
// Maybe an error union.
|
||||
// Definitely an optional
|
||||
const RawValue = @typeInfo(@TypeOf(T._next)).@"fn".return_type.?;
|
||||
const CanError = @typeInfo(RawValue) == .error_union;
|
||||
|
||||
const Value = blk: {
|
||||
// Unwrap the RawValue
|
||||
var V = RawValue;
|
||||
if (CanError) {
|
||||
V = @typeInfo(V).error_union.payload;
|
||||
}
|
||||
break :blk @typeInfo(V).optional.child;
|
||||
};
|
||||
|
||||
const ReturnType = if (CanError) T.Error!?struct { u32, Value } else ?struct { u32, Value };
|
||||
|
||||
// Avoid ambiguity. We want to expose a NumericEntries(T).Iterable, so we
|
||||
// need a declartion inside here for an "Iterable", but that will conflict
|
||||
// with the above Iterable generic function we have.
|
||||
const BaseIterable = Iterable;
|
||||
|
||||
return struct {
|
||||
// the inner value iterator
|
||||
inner: T,
|
||||
index: u32,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
// Generics don't generate clean names. Can't just take the resulting
|
||||
// type name and use that as a the JS class name. So we always ask for
|
||||
// an explicit JS class name
|
||||
pub const js_name = JsName;
|
||||
|
||||
// re-exposed for when/if we compose this type into an Iterable
|
||||
pub const Error = T.Error;
|
||||
|
||||
// This iterator as an iterable
|
||||
pub const Iterable = BaseIterable(Self, JsName ++ "Iterable");
|
||||
|
||||
pub fn init(inner: T) Self {
|
||||
return .{ .inner = inner, .index = 0 };
|
||||
}
|
||||
|
||||
pub fn _next(self: *Self) ReturnType {
|
||||
const value_ = if (comptime CanError) try self.inner._next() else self.inner._next();
|
||||
const value = value_ orelse return null;
|
||||
|
||||
const index = self.index;
|
||||
self.index = index + 1;
|
||||
return .{ index, value };
|
||||
}
|
||||
|
||||
// make the iterator, iterable
|
||||
pub fn _symbol_iterator(self: *Self) Self.Iterable {
|
||||
return Self.Iterable.init(self.*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "U32Iterator" {
|
||||
{
|
||||
var it = U32Iterator{ .length = 0 };
|
||||
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
|
||||
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
|
||||
}
|
||||
|
||||
{
|
||||
var it = U32Iterator{ .length = 3 };
|
||||
try testing.expectEqual(.{ .value = 0, .done = false }, it._next());
|
||||
try testing.expectEqual(.{ .value = 1, .done = false }, it._next());
|
||||
try testing.expectEqual(.{ .value = 2, .done = false }, it._next());
|
||||
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
|
||||
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
|
||||
}
|
||||
}
|
||||
|
||||
test "NumericEntries" {
|
||||
const it = DummyIterator{};
|
||||
var entries = NumericEntries(DummyIterator, "DummyIterator").init(it);
|
||||
|
||||
const v1 = entries._next().?;
|
||||
try testing.expectEqual(0, v1.@"0");
|
||||
try testing.expectEqual("it's", v1.@"1");
|
||||
|
||||
const v2 = entries._next().?;
|
||||
try testing.expectEqual(1, v2.@"0");
|
||||
try testing.expectEqual("over", v2.@"1");
|
||||
|
||||
const v3 = entries._next().?;
|
||||
try testing.expectEqual(2, v3.@"0");
|
||||
try testing.expectEqual("9000!!", v3.@"1");
|
||||
|
||||
try testing.expectEqual(null, entries._next());
|
||||
try testing.expectEqual(null, entries._next());
|
||||
try testing.expectEqual(null, entries._next());
|
||||
}
|
||||
|
||||
test "Iterable" {
|
||||
const it = DummyIterator{};
|
||||
var entries = Iterable(DummyIterator, "DummyIterator").init(it);
|
||||
|
||||
const v1 = entries._next();
|
||||
try testing.expectEqual(false, v1.done);
|
||||
try testing.expectEqual("it's", v1.value.?);
|
||||
|
||||
const v2 = entries._next();
|
||||
try testing.expectEqual(false, v2.done);
|
||||
try testing.expectEqual("over", v2.value.?);
|
||||
|
||||
const v3 = entries._next();
|
||||
try testing.expectEqual(false, v3.done);
|
||||
try testing.expectEqual("9000!!", v3.value.?);
|
||||
|
||||
try testing.expectEqual(true, entries._next().done);
|
||||
try testing.expectEqual(true, entries._next().done);
|
||||
try testing.expectEqual(true, entries._next().done);
|
||||
}
|
||||
|
||||
const DummyIterator = struct {
|
||||
index: u32 = 0,
|
||||
|
||||
pub fn _next(self: *DummyIterator) ?[]const u8 {
|
||||
const index = self.index;
|
||||
self.index = index + 1;
|
||||
return switch (index) {
|
||||
0 => "it's",
|
||||
1 => "over",
|
||||
2 => "9000!!",
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
284
src/browser/key_value.zig
Normal file
284
src/browser/key_value.zig
Normal file
@@ -0,0 +1,284 @@
|
||||
// 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;
|
||||
|
||||
// Used by FormDAta and URLSearchParams.
|
||||
//
|
||||
// We store the values in an ArrayList rather than a an
|
||||
// StringArrayHashMap([]const u8) because of the way the iterators (i.e., keys(),
|
||||
// values() and entries()) work. The FormData can contain duplicate keys, and
|
||||
// each iteration yields 1 key=>value pair. So, given:
|
||||
//
|
||||
// let f = new FormData();
|
||||
// f.append('a', '1');
|
||||
// f.append('a', '2');
|
||||
//
|
||||
// Then we'd expect f.keys(), f.values() and f.entries() to yield 2 results:
|
||||
// ['a', '1']
|
||||
// ['a', '2']
|
||||
//
|
||||
// This is much easier to do with an ArrayList than a HashMap, especially given
|
||||
// that the FormData could be mutated while iterating.
|
||||
// The downside is that most of the normal operations are O(N).
|
||||
pub const List = struct {
|
||||
entries: std.ArrayListUnmanaged(KeyValue) = .{},
|
||||
|
||||
pub fn init(entries: std.ArrayListUnmanaged(KeyValue)) List {
|
||||
return .{ .entries = entries };
|
||||
}
|
||||
|
||||
pub fn clone(self: *const List, arena: Allocator) !List {
|
||||
const entries = self.entries.items;
|
||||
|
||||
var c: std.ArrayListUnmanaged(KeyValue) = .{};
|
||||
try c.ensureTotalCapacity(arena, entries.len);
|
||||
for (entries) |kv| {
|
||||
c.appendAssumeCapacity(kv);
|
||||
}
|
||||
|
||||
return .{ .entries = c };
|
||||
}
|
||||
|
||||
pub fn fromOwnedSlice(entries: []KeyValue) List {
|
||||
return .{
|
||||
.entries = std.ArrayListUnmanaged(KeyValue).fromOwnedSlice(entries),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn count(self: *const List) usize {
|
||||
return self.entries.items.len;
|
||||
}
|
||||
|
||||
pub fn get(self: *const List, key: []const u8) ?[]const u8 {
|
||||
const result = self.find(key) orelse return null;
|
||||
return result.entry.value;
|
||||
}
|
||||
|
||||
pub fn getAll(self: *const List, arena: Allocator, key: []const u8) ![]const []const u8 {
|
||||
var arr: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
for (self.entries.items) |entry| {
|
||||
if (std.mem.eql(u8, key, entry.key)) {
|
||||
try arr.append(arena, entry.value);
|
||||
}
|
||||
}
|
||||
return arr.items;
|
||||
}
|
||||
|
||||
pub fn has(self: *const List, key: []const u8) bool {
|
||||
return self.find(key) != null;
|
||||
}
|
||||
|
||||
pub fn set(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void {
|
||||
self.delete(key);
|
||||
return self.append(arena, key, value);
|
||||
}
|
||||
|
||||
pub fn append(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void {
|
||||
return self.appendOwned(arena, try arena.dupe(u8, key), try arena.dupe(u8, value));
|
||||
}
|
||||
|
||||
pub fn appendOwned(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void {
|
||||
return self.entries.append(arena, .{
|
||||
.key = key,
|
||||
.value = value,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn appendOwnedAssumeCapacity(self: *List, key: []const u8, value: []const u8) void {
|
||||
self.entries.appendAssumeCapacity(.{
|
||||
.key = key,
|
||||
.value = value,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn delete(self: *List, key: []const u8) void {
|
||||
var i: usize = 0;
|
||||
while (i < self.entries.items.len) {
|
||||
const entry = self.entries.items[i];
|
||||
if (std.mem.eql(u8, key, entry.key)) {
|
||||
_ = self.entries.swapRemove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deleteKeyValue(self: *List, key: []const u8, value: []const u8) void {
|
||||
var i: usize = 0;
|
||||
while (i < self.entries.items.len) {
|
||||
const entry = self.entries.items[i];
|
||||
if (std.mem.eql(u8, key, entry.key) and std.mem.eql(u8, value, entry.value)) {
|
||||
_ = self.entries.swapRemove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keyIterator(self: *const List) KeyIterator {
|
||||
return .{ .entries = &self.entries };
|
||||
}
|
||||
|
||||
pub fn valueIterator(self: *const List) ValueIterator {
|
||||
return .{ .entries = &self.entries };
|
||||
}
|
||||
|
||||
pub fn entryIterator(self: *const List) EntryIterator {
|
||||
return .{ .entries = &self.entries };
|
||||
}
|
||||
|
||||
pub fn ensureTotalCapacity(self: *List, arena: Allocator, len: usize) !void {
|
||||
return self.entries.ensureTotalCapacity(arena, len);
|
||||
}
|
||||
|
||||
const FindResult = struct {
|
||||
index: usize,
|
||||
entry: KeyValue,
|
||||
};
|
||||
|
||||
fn find(self: *const List, key: []const u8) ?FindResult {
|
||||
for (self.entries.items, 0..) |entry, i| {
|
||||
if (std.mem.eql(u8, key, entry.key)) {
|
||||
return .{ .index = i, .entry = entry };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const KeyValue = struct {
|
||||
key: []const u8,
|
||||
value: []const u8,
|
||||
};
|
||||
|
||||
pub const KeyIterator = struct {
|
||||
index: usize = 0,
|
||||
entries: *const std.ArrayListUnmanaged(KeyValue),
|
||||
|
||||
pub fn _next(self: *KeyIterator) ?[]const u8 {
|
||||
const entries = self.entries.items;
|
||||
|
||||
const index = self.index;
|
||||
if (index == entries.len) {
|
||||
return null;
|
||||
}
|
||||
self.index += 1;
|
||||
return entries[index].key;
|
||||
}
|
||||
};
|
||||
|
||||
pub const ValueIterator = struct {
|
||||
index: usize = 0,
|
||||
entries: *const std.ArrayListUnmanaged(KeyValue),
|
||||
|
||||
pub fn _next(self: *ValueIterator) ?[]const u8 {
|
||||
const entries = self.entries.items;
|
||||
|
||||
const index = self.index;
|
||||
if (index == entries.len) {
|
||||
return null;
|
||||
}
|
||||
self.index += 1;
|
||||
return entries[index].value;
|
||||
}
|
||||
};
|
||||
|
||||
pub const EntryIterator = struct {
|
||||
index: usize = 0,
|
||||
entries: *const std.ArrayListUnmanaged(KeyValue),
|
||||
|
||||
pub fn _next(self: *EntryIterator) ?struct { []const u8, []const u8 } {
|
||||
const entries = self.entries.items;
|
||||
|
||||
const index = self.index;
|
||||
if (index == entries.len) {
|
||||
return null;
|
||||
}
|
||||
self.index += 1;
|
||||
const entry = entries[index];
|
||||
return .{ entry.key, entry.value };
|
||||
}
|
||||
};
|
||||
|
||||
const URLEncodeMode = enum {
|
||||
form,
|
||||
query,
|
||||
};
|
||||
|
||||
pub fn urlEncode(list: List, mode: URLEncodeMode, writer: anytype) !void {
|
||||
const entries = list.entries.items;
|
||||
if (entries.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try urlEncodeEntry(entries[0], mode, writer);
|
||||
for (entries[1..]) |entry| {
|
||||
try writer.writeByte('&');
|
||||
try urlEncodeEntry(entry, mode, writer);
|
||||
}
|
||||
}
|
||||
|
||||
fn urlEncodeEntry(entry: KeyValue, mode: URLEncodeMode, writer: anytype) !void {
|
||||
try urlEncodeValue(entry.key, mode, writer);
|
||||
|
||||
// for a form, for an empty value, we'll do "spice="
|
||||
// but for a query, we do "spice"
|
||||
if (mode == .query and entry.value.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try writer.writeByte('=');
|
||||
try urlEncodeValue(entry.value, mode, writer);
|
||||
}
|
||||
|
||||
fn urlEncodeValue(value: []const u8, mode: URLEncodeMode, writer: anytype) !void {
|
||||
if (!urlEncodeShouldEscape(value, mode)) {
|
||||
return writer.writeAll(value);
|
||||
}
|
||||
|
||||
for (value) |b| {
|
||||
if (urlEncodeUnreserved(b, mode)) {
|
||||
try writer.writeByte(b);
|
||||
} else if (b == ' ' and mode == .form) {
|
||||
// for form submission, space should be encoded as '+', not '%20'
|
||||
try writer.writeByte('+');
|
||||
} else {
|
||||
try writer.print("%{X:0>2}", .{b});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn urlEncodeShouldEscape(value: []const u8, mode: URLEncodeMode) bool {
|
||||
for (value) |b| {
|
||||
if (!urlEncodeUnreserved(b, mode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn urlEncodeUnreserved(b: u8, mode: URLEncodeMode) bool {
|
||||
return switch (b) {
|
||||
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_' => true,
|
||||
'~' => mode == .query,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
@@ -1,91 +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 Client = @import("../http/Client.zig");
|
||||
|
||||
const user_agent = "Lightpanda.io/1.0";
|
||||
|
||||
pub const Loader = struct {
|
||||
client: Client,
|
||||
// use 64KB for headers buffer size.
|
||||
server_header_buffer: [1024 * 64]u8 = undefined,
|
||||
|
||||
pub const Response = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
req: *Client.Request,
|
||||
|
||||
pub fn deinit(self: *Response) void {
|
||||
self.req.deinit();
|
||||
self.alloc.destroy(self.req);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) Loader {
|
||||
return Loader{
|
||||
.client = Client{
|
||||
.allocator = alloc,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Loader) void {
|
||||
self.client.deinit();
|
||||
}
|
||||
|
||||
// see
|
||||
// https://ziglang.org/documentation/master/std/#A;std:http.Client.fetch
|
||||
// for reference.
|
||||
// The caller is responsible for calling `deinit()` on the `Response`.
|
||||
pub fn get(self: *Loader, alloc: std.mem.Allocator, uri: std.Uri) !Response {
|
||||
var resp = Response{
|
||||
.alloc = alloc,
|
||||
.req = try alloc.create(Client.Request),
|
||||
};
|
||||
errdefer alloc.destroy(resp.req);
|
||||
|
||||
resp.req.* = try self.client.open(.GET, uri, .{
|
||||
.headers = .{
|
||||
.user_agent = .{ .override = user_agent },
|
||||
},
|
||||
.extra_headers = &.{
|
||||
.{ .name = "Accept", .value = "*/*" },
|
||||
.{ .name = "Accept-Language", .value = "en-US,en;q=0.5" },
|
||||
},
|
||||
.server_header_buffer = &self.server_header_buffer,
|
||||
});
|
||||
errdefer resp.req.deinit();
|
||||
|
||||
try resp.req.send();
|
||||
try resp.req.finish();
|
||||
try resp.req.wait();
|
||||
|
||||
return resp;
|
||||
}
|
||||
};
|
||||
|
||||
test "basic url get" {
|
||||
const alloc = std.testing.allocator;
|
||||
var loader = Loader.init(alloc);
|
||||
defer loader.deinit();
|
||||
|
||||
var result = try loader.get(alloc, "https://en.wikipedia.org/wiki/Main_Page");
|
||||
defer result.deinit();
|
||||
|
||||
try std.testing.expect(result.req.response.status == std.http.Status.ok);
|
||||
}
|
||||
@@ -21,7 +21,6 @@
|
||||
// We replace the libdom default usage of allocations with mimalloc heap
|
||||
// allocation to be able to free all memory used at once, like an arena usage.
|
||||
|
||||
const std = @import("std");
|
||||
const c = @cImport({
|
||||
@cInclude("mimalloc.h");
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user