mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
Compare commits
1453 Commits
selenium-c
...
nikneym/mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9cd0fad40 | ||
|
|
8f96ea457f | ||
|
|
9c83b268b9 | ||
|
|
10fc056184 | ||
|
|
7517937155 | ||
|
|
a86fa8cc37 | ||
|
|
e1c765e78a | ||
|
|
2ed8a1c0ec | ||
|
|
389dff7031 | ||
|
|
123d69e595 | ||
|
|
4ab7fe26fc | ||
|
|
7136851893 | ||
|
|
85f60cbc7b | ||
|
|
9c35f8a24e | ||
|
|
1ca8dc0ac0 | ||
|
|
85d148822e | ||
|
|
1e738dcf79 | ||
|
|
b5ffd8d046 | ||
|
|
21e354d252 | ||
|
|
15628d9b07 | ||
|
|
950182986a | ||
|
|
bc82023878 | ||
|
|
d5363e5993 | ||
|
|
80adee8558 | ||
|
|
37fe6a661b | ||
|
|
eb453f471b | ||
|
|
afd278ca4e | ||
|
|
ca8877da2d | ||
|
|
42828c64fb | ||
|
|
6600626f4f | ||
|
|
ac10d5b2a3 | ||
|
|
9f040025e7 | ||
|
|
2522e7fe9c | ||
|
|
dd22c55d23 | ||
|
|
a6efa9e9b2 | ||
|
|
5087b8004a | ||
|
|
d4c40242d0 | ||
|
|
5af55f1d5d | ||
|
|
55ef0a5e9e | ||
|
|
5dda86bf4a | ||
|
|
d81377b10d | ||
|
|
da128f5d49 | ||
|
|
6e5fe8e4a2 | ||
|
|
b3d350d41e | ||
|
|
7c6870f8eb | ||
|
|
327b4e4e37 | ||
|
|
7fdc857326 | ||
|
|
0382c2775e | ||
|
|
a0374133cd | ||
|
|
055f697340 | ||
|
|
ec8a9862c7 | ||
|
|
f393eb7b7d | ||
|
|
78285d7b1e | ||
|
|
b6137b03cd | ||
|
|
e237e709b6 | ||
|
|
2ac9b2088a | ||
|
|
a791212d89 | ||
|
|
5cc5f45ef3 | ||
|
|
a11e50c087 | ||
|
|
4dc09360a1 | ||
|
|
3a5528cc4d | ||
|
|
de533755e5 | ||
|
|
024b69ee3e | ||
|
|
d7e7832e9f | ||
|
|
8d4d72bf15 | ||
|
|
86a82d55fa | ||
|
|
5a15066da3 | ||
|
|
81766c8517 | ||
|
|
e486f28a41 | ||
|
|
8a9cbaf413 | ||
|
|
3a0a930b79 | ||
|
|
c40704d2f3 | ||
|
|
c0f0630e17 | ||
|
|
19dbb3a778 | ||
|
|
d4fc6f1b35 | ||
|
|
7c82942912 | ||
|
|
87d48b028b | ||
|
|
d6640f4d15 | ||
|
|
785a8da623 | ||
|
|
57dc303d90 | ||
|
|
ce08cc9659 | ||
|
|
866393743c | ||
|
|
ba255aa653 | ||
|
|
7d46e8fe80 | ||
|
|
6c41245c73 | ||
|
|
2a8e51c2d2 | ||
|
|
b2cf5df612 | ||
|
|
ada9ddd5b8 | ||
|
|
f66f4d9aeb | ||
|
|
d02ba777f2 | ||
|
|
aef614823b | ||
|
|
431db85ecb | ||
|
|
1ebac06f4b | ||
|
|
c7c5af4708 | ||
|
|
0b6a9d3a0b | ||
|
|
23d6362058 | ||
|
|
1443f38e5f | ||
|
|
94960cc842 | ||
|
|
efc983b009 | ||
|
|
74d90f2892 | ||
|
|
56f1b6cc19 | ||
|
|
fa2cd9dfd9 | ||
|
|
687f09d952 | ||
|
|
67b479b5c8 | ||
|
|
eac2140693 | ||
|
|
7a3f5de9c2 | ||
|
|
7005bf2481 | ||
|
|
b80ee3342c | ||
|
|
4c7b7b1e60 | ||
|
|
1a4a3608c8 | ||
|
|
6800d50339 | ||
|
|
036f808ec6 | ||
|
|
7647ce9e6d | ||
|
|
545d3f81ce | ||
|
|
455615b9c1 | ||
|
|
d0e2a03da5 | ||
|
|
fa408e644c | ||
|
|
a22416584d | ||
|
|
b8fc60df45 | ||
|
|
c6455cf02e | ||
|
|
2ac1d39367 | ||
|
|
041e014d68 | ||
|
|
5defb5c442 | ||
|
|
520a572bb4 | ||
|
|
4c602256da | ||
|
|
5a40cbd655 | ||
|
|
a75f9dd48d | ||
|
|
6b47aa2446 | ||
|
|
a847a1faae | ||
|
|
bb381e522c | ||
|
|
6962cfb91a | ||
|
|
302c50a90e | ||
|
|
e2d47e1c86 | ||
|
|
7d51da1efb | ||
|
|
c7674926c3 | ||
|
|
f0ca9728ae | ||
|
|
5fa8567801 | ||
|
|
e5b1acb6e1 | ||
|
|
8fdbaef4c7 | ||
|
|
7869159657 | ||
|
|
7046e18d7e | ||
|
|
a7516061d0 | ||
|
|
e61d787ff0 | ||
|
|
25ad420f85 | ||
|
|
fcd49c000f | ||
|
|
e2320ebe66 | ||
|
|
5e78a26e3d | ||
|
|
159bd06a56 | ||
|
|
bc7e1e07f4 | ||
|
|
ccc9618102 | ||
|
|
0ad09cca9d | ||
|
|
0959eea677 | ||
|
|
3316f2fcf4 | ||
|
|
390a21e4aa | ||
|
|
70ce54a5cd | ||
|
|
087e42a641 | ||
|
|
e26d4afce2 | ||
|
|
b9ae4c6077 | ||
|
|
11485d24f5 | ||
|
|
ce14f0b380 | ||
|
|
8bb2158a2a | ||
|
|
1a9d4af565 | ||
|
|
a6f37633a1 | ||
|
|
3182a47858 | ||
|
|
7335b1d0a4 | ||
|
|
cd33e9ad0e | ||
|
|
557f8444b2 | ||
|
|
65088b8644 | ||
|
|
7cc9521cbb | ||
|
|
4ad19fc4d8 | ||
|
|
ec71f8e2d9 | ||
|
|
ff8a847795 | ||
|
|
6b001c50a4 | ||
|
|
5759c88932 | ||
|
|
00c11d9bd4 | ||
|
|
ed99acebfe | ||
|
|
bade412d30 | ||
|
|
191e9ba073 | ||
|
|
b21688a0ac | ||
|
|
a4d4da4d96 | ||
|
|
16c85c5b8a | ||
|
|
2c7b39927a | ||
|
|
7f47692ad4 | ||
|
|
af4066da87 | ||
|
|
4de4e7504d | ||
|
|
b46c181b07 | ||
|
|
e4f89092b3 | ||
|
|
4fbedf5b20 | ||
|
|
d51a03f1b6 | ||
|
|
f7eee0d461 | ||
|
|
39178d8d2b | ||
|
|
7795916c08 | ||
|
|
0e2a3d8009 | ||
|
|
38a0b6905e | ||
|
|
8797549369 | ||
|
|
f5ec74252d | ||
|
|
211012d367 | ||
|
|
c1319d1f27 | ||
|
|
d4d8773fd1 | ||
|
|
01223601f2 | ||
|
|
d9ed4cfca8 | ||
|
|
7d0e4b6270 | ||
|
|
b2f645a5ce | ||
|
|
6a29d6711c | ||
|
|
5b2806a784 | ||
|
|
a2f15ce0b2 | ||
|
|
68400f3bcf | ||
|
|
31f3c2771a | ||
|
|
f9352e26cb | ||
|
|
4fa542bc38 | ||
|
|
a707e10af7 | ||
|
|
1e095fede5 | ||
|
|
96b10f4b85 | ||
|
|
5100e06f38 | ||
|
|
35e2fa5058 | ||
|
|
8d2d4ffdd2 | ||
|
|
7d05712f40 | ||
|
|
c0106a238b | ||
|
|
f6c68e4580 | ||
|
|
3c8065fdee | ||
|
|
9bd8b2fc43 | ||
|
|
5a3d5f5512 | ||
|
|
ca9e850ac7 | ||
|
|
2dc09c799f | ||
|
|
a49154acf4 | ||
|
|
77eee7f087 | ||
|
|
03694b54f0 | ||
|
|
bed320204d | ||
|
|
971524fa3b | ||
|
|
4758456069 | ||
|
|
3ef4ba6b8b | ||
|
|
a504f051e7 | ||
|
|
ea0bbaf332 | ||
|
|
19c908035b | ||
|
|
05192b6850 | ||
|
|
079ce5e9de | ||
|
|
ff742c0169 | ||
|
|
332e264437 | ||
|
|
3554634c1c | ||
|
|
c96fb3c2f2 | ||
|
|
1e612e4166 | ||
|
|
06984ace21 | ||
|
|
cabd4fa718 | ||
|
|
ddb549cb45 | ||
|
|
c7484c69c0 | ||
|
|
9876d79680 | ||
|
|
32566ccc80 | ||
|
|
7f9e309ae8 | ||
|
|
7831aabe5a | ||
|
|
74b40b97ec | ||
|
|
f45726d61f | ||
|
|
3c0d027306 | ||
|
|
dc83765808 | ||
|
|
4244b572d1 | ||
|
|
77475ca5e4 | ||
|
|
3555680335 | ||
|
|
f65a39a3e3 | ||
|
|
94e8964f69 | ||
|
|
254d22e2cc | ||
|
|
54ab1326e5 | ||
|
|
b0fe5d60ab | ||
|
|
4b1eb2794f | ||
|
|
6a2dd1111c | ||
|
|
f5da89b50b | ||
|
|
bede244598 | ||
|
|
4df48c9695 | ||
|
|
05ad77ffbe | ||
|
|
dc23a74e7b | ||
|
|
f463cb16da | ||
|
|
b785884cd8 | ||
|
|
f09caec09a | ||
|
|
5e30a3997e | ||
|
|
8552a5797c | ||
|
|
a0d528981e | ||
|
|
7ffdee0d7f | ||
|
|
3d0928a449 | ||
|
|
ea1bca05c7 | ||
|
|
df292a2103 | ||
|
|
7f2c360f33 | ||
|
|
fbd40a6514 | ||
|
|
9dd02ec67d | ||
|
|
8e55082d4e | ||
|
|
29378c57ea | ||
|
|
16c74cf3b4 | ||
|
|
b199925f91 | ||
|
|
28397bf9d0 | ||
|
|
1b7abf9972 | ||
|
|
b98bdeaae7 | ||
|
|
221274b473 | ||
|
|
cc6d443113 | ||
|
|
b3c81c9e55 | ||
|
|
84d07f3f18 | ||
|
|
0fee2bbf28 | ||
|
|
ea38845622 | ||
|
|
81a0e95916 | ||
|
|
2a9feee476 | ||
|
|
c38c1fa93a | ||
|
|
8d7c35d034 | ||
|
|
425f62607b | ||
|
|
c1752ae5eb | ||
|
|
d61e91b949 | ||
|
|
090c0f8857 | ||
|
|
c453dd2b3c | ||
|
|
b2b2e97edc | ||
|
|
bd9e4dbc79 | ||
|
|
0c19070800 | ||
|
|
07e37b257f | ||
|
|
0a5f060d1b | ||
|
|
6fcfcb630d | ||
|
|
7aff90aec7 | ||
|
|
f1e513443b | ||
|
|
c533b10e19 | ||
|
|
b4014e8c24 | ||
|
|
478f3a5308 | ||
|
|
b98edf3d76 | ||
|
|
02fe46de58 | ||
|
|
ab2fd0ad36 | ||
|
|
88655d877b | ||
|
|
6e94affea6 | ||
|
|
f7f382275a | ||
|
|
23f3bf43c2 | ||
|
|
8a0c4909b9 | ||
|
|
2aeaf02d05 | ||
|
|
f4a6e34713 | ||
|
|
3eb85da02c | ||
|
|
6533456472 | ||
|
|
7969e047c7 | ||
|
|
f0d6d9d177 | ||
|
|
ca574df3be | ||
|
|
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 | ||
|
|
1846d0bc21 | ||
|
|
d282055e10 | ||
|
|
6ab64d155b | ||
|
|
14fe4f65e1 | ||
|
|
bdb70444d6 | ||
|
|
4d9cc55a87 | ||
|
|
f41c1cbfd0 | ||
|
|
72eaab68be | ||
|
|
733c6b4c17 | ||
|
|
c0c0694fcc | ||
|
|
fd7db18221 |
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.15.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.11'
|
||||
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,katie-lpd
|
||||
allowlist: krichprollsch,francisbouvier,katie-lpd,sjorsdonkers,bornlex
|
||||
|
||||
remote-organization-name: lightpanda-io
|
||||
remote-repository-name: cla
|
||||
|
||||
223
.github/workflows/e2e-test.yml
vendored
Normal file
223
.github/workflows/e2e-test.yml
vendored
Normal file
@@ -0,0 +1,223 @@
|
||||
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
|
||||
kill `cat LPD.pid`
|
||||
|
||||
- name: build proxy
|
||||
run: |
|
||||
cd proxy
|
||||
go build
|
||||
|
||||
- name: run end to end tests through proxy
|
||||
run: |
|
||||
./proxy/proxy & echo $! > PROXY.id
|
||||
./lightpanda serve --http_proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid
|
||||
go run runner/main.go
|
||||
kill `cat LPD.pid` `cat PROXY.id`
|
||||
|
||||
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.15.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 }}
|
||||
|
||||
|
||||
55
.github/workflows/zig-test.yml
vendored
55
.github/workflows/zig-test.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- name: zig build debug
|
||||
run: zig build -Dengine=v8
|
||||
run: zig build
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -66,28 +66,28 @@ jobs:
|
||||
zig-out/bin/lightpanda
|
||||
retention-days: 1
|
||||
|
||||
zig-build-release:
|
||||
name: zig build release
|
||||
browser-fetch:
|
||||
name: browser fetch
|
||||
needs: zig-build-dev
|
||||
|
||||
# Don't run the CI on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
# 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
|
||||
@@ -104,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: |
|
||||
@@ -127,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:
|
||||
@@ -141,30 +143,3 @@ jobs:
|
||||
|
||||
- name: format and send json result
|
||||
run: /perf-fmt bench-browser ${{ github.sha }} bench.json
|
||||
|
||||
demo-puppeteer:
|
||||
name: demo-puppeteer
|
||||
needs: zig-build-dev
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
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-dev
|
||||
|
||||
- run: chmod a+x ./lightpanda
|
||||
|
||||
- name: run puppeteer
|
||||
run: |
|
||||
python3 -m http.server 1234 -d ./public &
|
||||
./lightpanda &
|
||||
RUNS=2 npm run bench-puppeteer-cdp
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@ zig-cache
|
||||
zig-out
|
||||
/vendor/netsurf/out
|
||||
/vendor/libiconv/
|
||||
lightpanda.id
|
||||
/v8/
|
||||
|
||||
21
.gitmodules
vendored
21
.gitmodules
vendored
@@ -1,6 +1,3 @@
|
||||
[submodule "vendor/zig-js-runtime"]
|
||||
path = vendor/zig-js-runtime
|
||||
url = https://github.com/lightpanda-io/zig-js-runtime.git/
|
||||
[submodule "vendor/netsurf/libwapcaplet"]
|
||||
path = vendor/netsurf/libwapcaplet
|
||||
url = https://github.com/lightpanda-io/libwapcaplet.git/
|
||||
@@ -22,9 +19,15 @@
|
||||
[submodule "vendor/mimalloc"]
|
||||
path = vendor/mimalloc
|
||||
url = https://github.com/microsoft/mimalloc.git/
|
||||
[submodule "vendor/tls.zig"]
|
||||
path = vendor/tls.zig
|
||||
url = https://github.com/ianic/tls.zig.git/
|
||||
[submodule "vendor/zig-async-io"]
|
||||
path = vendor/zig-async-io
|
||||
url = https://github.com/lightpanda-io/zig-async-io.git/
|
||||
[submodule "vendor/nghttp2"]
|
||||
path = vendor/nghttp2
|
||||
url = https://github.com/nghttp2/nghttp2.git
|
||||
[submodule "vendor/mbedtls"]
|
||||
path = vendor/mbedtls
|
||||
url = https://github.com/Mbed-TLS/mbedtls.git
|
||||
[submodule "vendor/zlib"]
|
||||
path = vendor/zlib
|
||||
url = https://github.com/madler/zlib.git
|
||||
[submodule "vendor/curl"]
|
||||
path = vendor/curl
|
||||
url = https://github.com/curl/curl.git
|
||||
|
||||
67
Dockerfile
67
Dockerfile
@@ -1,11 +1,11 @@
|
||||
FROM ubuntu:22.04
|
||||
FROM debian:stable
|
||||
|
||||
ARG ZIG=0.13.0
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG=0.15.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.11
|
||||
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,23 +40,23 @@ 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
|
||||
@@ -76,4 +65,4 @@ COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
|
||||
|
||||
EXPOSE 9222/tcp
|
||||
|
||||
CMD ["/bin/lightpanda", "--host", "0.0.0.0", "--port", "9222"]
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
65
Makefile
65
Makefile
@@ -3,7 +3,7 @@
|
||||
|
||||
ZIG := zig
|
||||
BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
# option test filter make unittest F="server"
|
||||
# option test filter make test F="server"
|
||||
F=
|
||||
|
||||
# OS and ARCH
|
||||
@@ -11,6 +11,9 @@ 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
|
||||
@@ -44,7 +47,8 @@ help:
|
||||
|
||||
# $(ZIG) commands
|
||||
# ------------
|
||||
.PHONY: build build-dev run run-release shell test bench download-zig wpt unittest
|
||||
.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)
|
||||
|
||||
@@ -59,56 +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
|
||||
|
||||
unittest:
|
||||
@TEST_FILTER='${F}' $(ZIG) build unittest -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
|
||||
@@ -144,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 \
|
||||
@@ -191,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
|
||||
|
||||
|
||||
190
README.md
190
README.md
@@ -8,65 +8,79 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/lightpanda-io/browser/commits/main)
|
||||
[](https://github.com/lightpanda-io/browser/blob/main/LICENSE)
|
||||
[](https://twitter.com/lightpanda_io)
|
||||
[](https://github.com/lightpanda-io/browser)
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://trendshift.io/repositories/12815" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12815" alt="lightpanda-io%2Fbrowser | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
</div>
|
||||
|
||||
Lightpanda is the open-source browser made for headless usage:
|
||||
|
||||
- Javascript execution
|
||||
- Support of Web APIs (partial, WIP)
|
||||
- Compatible with Playwright, Puppeteer through CDP (WIP)
|
||||
- Compatible with Playwright[^1], Puppeteer, chromedp through CDP
|
||||
|
||||
Fast web automation for AI agents, LLM training, scraping and testing with minimal memory footprint:
|
||||
Fast web automation for AI agents, LLM training, scraping and testing:
|
||||
|
||||
- Ultra-low memory footprint (9x less than Chrome)
|
||||
- Exceptionally 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)._
|
||||
|
||||
[^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.
|
||||
|
||||
## Quick start
|
||||
|
||||
### Install from the nightly builds
|
||||
### Install
|
||||
**Install from the nightly builds**
|
||||
|
||||
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.
|
||||
|
||||
*For Linux*
|
||||
```console
|
||||
# Download the binary
|
||||
$ wget https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux
|
||||
$ chmod a+x ./lightpanda-x86_64-linux
|
||||
$ ./lightpanda-x86_64-linux -h
|
||||
usage: ./lightpanda-x86_64-linux [options] [URL]
|
||||
|
||||
start Lightpanda browser
|
||||
|
||||
* if an url is provided the browser will fetch the page and exit
|
||||
* otherwhise the browser starts a CDP server
|
||||
|
||||
-h, --help Print this help message and exit.
|
||||
--host Host of the CDP server (default "127.0.0.1")
|
||||
--port Port of the CDP server (default "9222")
|
||||
--timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
|
||||
--dump Dump document in stdout (fetch mode only)
|
||||
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux && \
|
||||
chmod a+x ./lightpanda
|
||||
```
|
||||
|
||||
### Dump an URL
|
||||
*For MacOS*
|
||||
```console
|
||||
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-aarch64-macos && \
|
||||
chmod a+x ./lightpanda
|
||||
```
|
||||
|
||||
*For Windows + WSL2*
|
||||
|
||||
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.
|
||||
|
||||
**Install from Docker**
|
||||
|
||||
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`.
|
||||
```console
|
||||
docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
|
||||
```
|
||||
|
||||
### Dump a URL
|
||||
|
||||
```console
|
||||
$ ./lightpanda-x86_64-linux --dump https://lightpanda.io
|
||||
./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')
|
||||
@@ -76,7 +90,9 @@ info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeE
|
||||
### Start a CDP server
|
||||
|
||||
```console
|
||||
$ ./lightpanda-x86_64-linux --host 127.0.0.1 --port 9222
|
||||
./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...
|
||||
```
|
||||
@@ -98,21 +114,62 @@ const browser = await puppeteer.connect({
|
||||
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 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 (based on Libcurl)
|
||||
- [x] HTML parser and DOM tree (based on Netsurf libs)
|
||||
- [x] Javascript support (v8)
|
||||
- [x] DOM APIs
|
||||
- [x] Ajax
|
||||
- [x] XHR API
|
||||
- [x] Fetch API (polyfill)
|
||||
- [x] DOM dump
|
||||
- [x] CDP/websockets server
|
||||
- [x] Click
|
||||
- [x] Input form
|
||||
- [x] Cookies
|
||||
- [x] Custom HTTP headers
|
||||
- [x] Proxy support
|
||||
- [x] Network interception
|
||||
|
||||
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.
|
||||
|
||||
## 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.15.1`. You have to
|
||||
install it with the right version in order to build the project.
|
||||
|
||||
Lightpanda also depends on
|
||||
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
|
||||
[Libcurl](https://curl.se/libcurl/),
|
||||
[Netsurf libs](https://www.netsurf-browser.org/) and
|
||||
[Mimalloc](https://microsoft.github.io/mimalloc).
|
||||
|
||||
@@ -124,10 +181,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:
|
||||
|
||||
```
|
||||
@@ -152,6 +214,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.
|
||||
@@ -176,17 +246,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 install-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
|
||||
|
||||
@@ -194,6 +268,20 @@ For dev env, use `make install-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
|
||||
@@ -260,25 +348,3 @@ If we want both Javascript and performance in a true headless browser, we need t
|
||||
- Not based on Chromium, Blink or WebKit
|
||||
- Low-level system programming language (Zig) with optimisations in mind
|
||||
- Opinionated: without graphical rendering
|
||||
|
||||
## Status
|
||||
|
||||
Lightpanda is still a work in progress and is currently at a Beta stage.
|
||||
|
||||
:warning: You should expect most websites to fail or crash.
|
||||
|
||||
Here are the key features we have implemented:
|
||||
|
||||
- [x] HTTP loader
|
||||
- [x] HTML parser and DOM tree (based on Netsurf libs)
|
||||
- [x] Javascript support (v8)
|
||||
- [x] Basic DOM APIs
|
||||
- [x] Ajax
|
||||
- [x] XHR API
|
||||
- [x] Fetch API
|
||||
- [x] DOM dump
|
||||
- [x] Basic CDP/websockets server
|
||||
|
||||
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.
|
||||
|
||||
874
build.zig
874
build.zig
@@ -17,18 +17,15 @@
|
||||
// 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);
|
||||
const Build = std.Build;
|
||||
|
||||
/// 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.15.1";
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
pub fn build(b: *Build) !void {
|
||||
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
||||
.eq => {},
|
||||
.lt => {
|
||||
@@ -42,185 +39,397 @@ pub fn build(b: *std.Build) !void {
|
||||
},
|
||||
}
|
||||
|
||||
var opts = b.addOptions();
|
||||
opts.addOption(
|
||||
[]const u8,
|
||||
"git_commit",
|
||||
b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
|
||||
);
|
||||
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const mode = b.standardOptimizeOption(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const options = jsruntime.buildOptions(b);
|
||||
// We're still using llvm because the new x86 backend seems to crash
|
||||
// with v8. This can be reproduced in zig-v8-fork.
|
||||
|
||||
// browser
|
||||
// -------
|
||||
|
||||
// compile and install
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "lightpanda",
|
||||
const lightpanda_module = b.addModule("lightpanda", .{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
.link_libcpp = true,
|
||||
});
|
||||
try common(b, exe, options);
|
||||
b.installArtifact(exe);
|
||||
try addDependencies(b, lightpanda_module, opts);
|
||||
|
||||
// run
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
{
|
||||
// browser
|
||||
// -------
|
||||
|
||||
// compile and install
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "lightpanda",
|
||||
.use_llvm = true,
|
||||
.root_module = lightpanda_module,
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
// step
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
{
|
||||
// tests
|
||||
// ----
|
||||
|
||||
// shell
|
||||
// -----
|
||||
// compile
|
||||
const tests = b.addTest(.{
|
||||
.root_module = lightpanda_module,
|
||||
.use_llvm = true,
|
||||
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
|
||||
});
|
||||
|
||||
// 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);
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
if (b.args) |args| {
|
||||
run_tests.addArgs(args);
|
||||
}
|
||||
|
||||
// run
|
||||
const shell_cmd = b.addRunArtifact(shell);
|
||||
if (b.args) |args| {
|
||||
shell_cmd.addArgs(args);
|
||||
// step
|
||||
const tests_step = b.step("test", "Run unit tests");
|
||||
tests_step.dependOn(&run_tests.step);
|
||||
}
|
||||
|
||||
// step
|
||||
const shell_step = b.step("shell", "Run JS shell");
|
||||
shell_step.dependOn(&shell_cmd.step);
|
||||
{
|
||||
// wpt
|
||||
// -----
|
||||
const wpt_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main_wpt.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
try addDependencies(b, wpt_module, opts);
|
||||
|
||||
// test
|
||||
// ----
|
||||
// compile and install
|
||||
const wpt = b.addExecutable(.{
|
||||
.name = "lightpanda-wpt",
|
||||
.use_llvm = true,
|
||||
.root_module = wpt_module,
|
||||
});
|
||||
|
||||
// compile
|
||||
const tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/main_tests.zig"),
|
||||
.test_runner = b.path("src/main_tests.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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
// step
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_tests.step);
|
||||
|
||||
// unittest
|
||||
// ----
|
||||
|
||||
// compile
|
||||
const unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/unit_tests.zig"),
|
||||
.test_runner = b.path("src/unit_tests.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(b, unit_tests, options);
|
||||
|
||||
const run_unit_tests = b.addRunArtifact(unit_tests);
|
||||
if (b.args) |args| {
|
||||
run_unit_tests.addArgs(args);
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
// step
|
||||
const unit_test_step = b.step("unittest", "Run unit tests");
|
||||
unit_test_step.dependOn(&run_unit_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);
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
// 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,
|
||||
);
|
||||
step.root_module.addImport("jsruntime", jsruntimemod);
|
||||
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !void {
|
||||
try moduleNetSurf(b, mod);
|
||||
mod.addImport("build_config", opts.createModule());
|
||||
|
||||
const netsurf = try moduleNetSurf(b, target);
|
||||
netsurf.addImport("jsruntime", jsruntimemod);
|
||||
step.root_module.addImport("netsurf", netsurf);
|
||||
const target = mod.resolved_target.?;
|
||||
const dep_opts = .{
|
||||
.target = target,
|
||||
.optimize = mod.optimize.?,
|
||||
};
|
||||
|
||||
const asyncio = b.addModule("asyncio", .{
|
||||
.root_source_file = b.path("vendor/zig-async-io/src/lib.zig"),
|
||||
});
|
||||
step.root_module.addImport("asyncio", asyncio);
|
||||
mod.addIncludePath(b.path("vendor/lightpanda"));
|
||||
|
||||
const tlsmod = b.addModule("tls", .{
|
||||
.root_source_file = b.path("vendor/tls.zig/src/main.zig"),
|
||||
});
|
||||
step.root_module.addImport("tls", tlsmod);
|
||||
{
|
||||
// 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);
|
||||
|
||||
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 => {},
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
//curl
|
||||
{
|
||||
const is_linux = target.result.os.tag == .linux;
|
||||
if (is_linux) {
|
||||
mod.addCMacro("HAVE_LINUX_TCP_H", "1");
|
||||
mod.addCMacro("HAVE_MSG_NOSIGNAL", "1");
|
||||
mod.addCMacro("HAVE_GETHOSTBYNAME_R", "1");
|
||||
}
|
||||
mod.addCMacro("_FILE_OFFSET_BITS", "64");
|
||||
mod.addCMacro("BUILDING_LIBCURL", "1");
|
||||
mod.addCMacro("CURL_DISABLE_AWS", "1");
|
||||
mod.addCMacro("CURL_DISABLE_DICT", "1");
|
||||
mod.addCMacro("CURL_DISABLE_DOH", "1");
|
||||
mod.addCMacro("CURL_DISABLE_FILE", "1");
|
||||
mod.addCMacro("CURL_DISABLE_FTP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_GOPHER", "1");
|
||||
mod.addCMacro("CURL_DISABLE_KERBEROS", "1");
|
||||
mod.addCMacro("CURL_DISABLE_IMAP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_IPFS", "1");
|
||||
mod.addCMacro("CURL_DISABLE_LDAP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_LDAPS", "1");
|
||||
mod.addCMacro("CURL_DISABLE_MQTT", "1");
|
||||
mod.addCMacro("CURL_DISABLE_NTLM", "1");
|
||||
mod.addCMacro("CURL_DISABLE_PROGRESS_METER", "1");
|
||||
mod.addCMacro("CURL_DISABLE_POP3", "1");
|
||||
mod.addCMacro("CURL_DISABLE_RTSP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_SMB", "1");
|
||||
mod.addCMacro("CURL_DISABLE_SMTP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_TELNET", "1");
|
||||
mod.addCMacro("CURL_DISABLE_TFTP", "1");
|
||||
mod.addCMacro("CURL_EXTERN_SYMBOL", "__attribute__ ((__visibility__ (\"default\"))");
|
||||
mod.addCMacro("CURL_OS", if (is_linux) "\"Linux\"" else "\"mac\"");
|
||||
mod.addCMacro("CURL_STATICLIB", "1");
|
||||
mod.addCMacro("ENABLE_IPV6", "1");
|
||||
mod.addCMacro("HAVE_ALARM", "1");
|
||||
mod.addCMacro("HAVE_ALLOCA_H", "1");
|
||||
mod.addCMacro("HAVE_ARPA_INET_H", "1");
|
||||
mod.addCMacro("HAVE_ARPA_TFTP_H", "1");
|
||||
mod.addCMacro("HAVE_ASSERT_H", "1");
|
||||
mod.addCMacro("HAVE_BASENAME", "1");
|
||||
mod.addCMacro("HAVE_BOOL_T", "1");
|
||||
mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1");
|
||||
mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
|
||||
mod.addCMacro("HAVE_DLFCN_H", "1");
|
||||
mod.addCMacro("HAVE_ERRNO_H", "1");
|
||||
mod.addCMacro("HAVE_FCNTL", "1");
|
||||
mod.addCMacro("HAVE_FCNTL_H", "1");
|
||||
mod.addCMacro("HAVE_FCNTL_O_NONBLOCK", "1");
|
||||
mod.addCMacro("HAVE_FREEADDRINFO", "1");
|
||||
mod.addCMacro("HAVE_FSETXATTR", "1");
|
||||
mod.addCMacro("HAVE_FSETXATTR_5", "1");
|
||||
mod.addCMacro("HAVE_FTRUNCATE", "1");
|
||||
mod.addCMacro("HAVE_GETADDRINFO", "1");
|
||||
mod.addCMacro("HAVE_GETEUID", "1");
|
||||
mod.addCMacro("HAVE_GETHOSTBYNAME", "1");
|
||||
mod.addCMacro("HAVE_GETHOSTBYNAME_R_6", "1");
|
||||
mod.addCMacro("HAVE_GETHOSTNAME", "1");
|
||||
mod.addCMacro("HAVE_GETPEERNAME", "1");
|
||||
mod.addCMacro("HAVE_GETPPID", "1");
|
||||
mod.addCMacro("HAVE_GETPPID", "1");
|
||||
mod.addCMacro("HAVE_GETPROTOBYNAME", "1");
|
||||
mod.addCMacro("HAVE_GETPWUID", "1");
|
||||
mod.addCMacro("HAVE_GETPWUID_R", "1");
|
||||
mod.addCMacro("HAVE_GETRLIMIT", "1");
|
||||
mod.addCMacro("HAVE_GETSOCKNAME", "1");
|
||||
mod.addCMacro("HAVE_GETTIMEOFDAY", "1");
|
||||
mod.addCMacro("HAVE_GMTIME_R", "1");
|
||||
mod.addCMacro("HAVE_IDN2_H", "1");
|
||||
mod.addCMacro("HAVE_IF_NAMETOINDEX", "1");
|
||||
mod.addCMacro("HAVE_IFADDRS_H", "1");
|
||||
mod.addCMacro("HAVE_INET_ADDR", "1");
|
||||
mod.addCMacro("HAVE_INET_PTON", "1");
|
||||
mod.addCMacro("HAVE_INTTYPES_H", "1");
|
||||
mod.addCMacro("HAVE_IOCTL", "1");
|
||||
mod.addCMacro("HAVE_IOCTL_FIONBIO", "1");
|
||||
mod.addCMacro("HAVE_IOCTL_SIOCGIFADDR", "1");
|
||||
mod.addCMacro("HAVE_LDAP_URL_PARSE", "1");
|
||||
mod.addCMacro("HAVE_LIBGEN_H", "1");
|
||||
mod.addCMacro("HAVE_LIBZ", "1");
|
||||
mod.addCMacro("HAVE_LL", "1");
|
||||
mod.addCMacro("HAVE_LOCALE_H", "1");
|
||||
mod.addCMacro("HAVE_LOCALTIME_R", "1");
|
||||
mod.addCMacro("HAVE_LONGLONG", "1");
|
||||
mod.addCMacro("HAVE_MALLOC_H", "1");
|
||||
mod.addCMacro("HAVE_MEMORY_H", "1");
|
||||
mod.addCMacro("HAVE_NET_IF_H", "1");
|
||||
mod.addCMacro("HAVE_NETDB_H", "1");
|
||||
mod.addCMacro("HAVE_NETINET_IN_H", "1");
|
||||
mod.addCMacro("HAVE_NETINET_TCP_H", "1");
|
||||
mod.addCMacro("HAVE_PIPE", "1");
|
||||
mod.addCMacro("HAVE_POLL", "1");
|
||||
mod.addCMacro("HAVE_POLL_FINE", "1");
|
||||
mod.addCMacro("HAVE_POLL_H", "1");
|
||||
mod.addCMacro("HAVE_POSIX_STRERROR_R", "1");
|
||||
mod.addCMacro("HAVE_PTHREAD_H", "1");
|
||||
mod.addCMacro("HAVE_PWD_H", "1");
|
||||
mod.addCMacro("HAVE_RECV", "1");
|
||||
mod.addCMacro("HAVE_SA_FAMILY_T", "1");
|
||||
mod.addCMacro("HAVE_SELECT", "1");
|
||||
mod.addCMacro("HAVE_SEND", "1");
|
||||
mod.addCMacro("HAVE_SETJMP_H", "1");
|
||||
mod.addCMacro("HAVE_SETLOCALE", "1");
|
||||
mod.addCMacro("HAVE_SETRLIMIT", "1");
|
||||
mod.addCMacro("HAVE_SETSOCKOPT", "1");
|
||||
mod.addCMacro("HAVE_SIGACTION", "1");
|
||||
mod.addCMacro("HAVE_SIGINTERRUPT", "1");
|
||||
mod.addCMacro("HAVE_SIGNAL", "1");
|
||||
mod.addCMacro("HAVE_SIGNAL_H", "1");
|
||||
mod.addCMacro("HAVE_SIGSETJMP", "1");
|
||||
mod.addCMacro("HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID", "1");
|
||||
mod.addCMacro("HAVE_SOCKET", "1");
|
||||
mod.addCMacro("HAVE_STDBOOL_H", "1");
|
||||
mod.addCMacro("HAVE_STDINT_H", "1");
|
||||
mod.addCMacro("HAVE_STDIO_H", "1");
|
||||
mod.addCMacro("HAVE_STDLIB_H", "1");
|
||||
mod.addCMacro("HAVE_STRCASECMP", "1");
|
||||
mod.addCMacro("HAVE_STRDUP", "1");
|
||||
mod.addCMacro("HAVE_STRERROR_R", "1");
|
||||
mod.addCMacro("HAVE_STRING_H", "1");
|
||||
mod.addCMacro("HAVE_STRINGS_H", "1");
|
||||
mod.addCMacro("HAVE_STRSTR", "1");
|
||||
mod.addCMacro("HAVE_STRTOK_R", "1");
|
||||
mod.addCMacro("HAVE_STRTOLL", "1");
|
||||
mod.addCMacro("HAVE_STRUCT_SOCKADDR_STORAGE", "1");
|
||||
mod.addCMacro("HAVE_STRUCT_TIMEVAL", "1");
|
||||
mod.addCMacro("HAVE_SYS_IOCTL_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_PARAM_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_POLL_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_RESOURCE_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_SELECT_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_SOCKET_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_STAT_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_TIME_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_TYPES_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_UIO_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_UN_H", "1");
|
||||
mod.addCMacro("HAVE_TERMIO_H", "1");
|
||||
mod.addCMacro("HAVE_TERMIOS_H", "1");
|
||||
mod.addCMacro("HAVE_TIME_H", "1");
|
||||
mod.addCMacro("HAVE_UNAME", "1");
|
||||
mod.addCMacro("HAVE_UNISTD_H", "1");
|
||||
mod.addCMacro("HAVE_UTIME", "1");
|
||||
mod.addCMacro("HAVE_UTIME_H", "1");
|
||||
mod.addCMacro("HAVE_UTIMES", "1");
|
||||
mod.addCMacro("HAVE_VARIADIC_MACROS_C99", "1");
|
||||
mod.addCMacro("HAVE_VARIADIC_MACROS_GCC", "1");
|
||||
mod.addCMacro("HAVE_ZLIB_H", "1");
|
||||
mod.addCMacro("RANDOM_FILE", "\"/dev/urandom\"");
|
||||
mod.addCMacro("RECV_TYPE_ARG1", "int");
|
||||
mod.addCMacro("RECV_TYPE_ARG2", "void *");
|
||||
mod.addCMacro("RECV_TYPE_ARG3", "size_t");
|
||||
mod.addCMacro("RECV_TYPE_ARG4", "int");
|
||||
mod.addCMacro("RECV_TYPE_RETV", "ssize_t");
|
||||
mod.addCMacro("SEND_QUAL_ARG2", "const");
|
||||
mod.addCMacro("SEND_TYPE_ARG1", "int");
|
||||
mod.addCMacro("SEND_TYPE_ARG2", "void *");
|
||||
mod.addCMacro("SEND_TYPE_ARG3", "size_t");
|
||||
mod.addCMacro("SEND_TYPE_ARG4", "int");
|
||||
mod.addCMacro("SEND_TYPE_RETV", "ssize_t");
|
||||
mod.addCMacro("SIZEOF_CURL_OFF_T", "8");
|
||||
mod.addCMacro("SIZEOF_INT", "4");
|
||||
mod.addCMacro("SIZEOF_LONG", "8");
|
||||
mod.addCMacro("SIZEOF_OFF_T", "8");
|
||||
mod.addCMacro("SIZEOF_SHORT", "2");
|
||||
mod.addCMacro("SIZEOF_SIZE_T", "8");
|
||||
mod.addCMacro("SIZEOF_TIME_T", "8");
|
||||
mod.addCMacro("STDC_HEADERS", "1");
|
||||
mod.addCMacro("TIME_WITH_SYS_TIME", "1");
|
||||
mod.addCMacro("USE_NGHTTP2", "1");
|
||||
mod.addCMacro("USE_MBEDTLS", "1");
|
||||
mod.addCMacro("USE_THREADS_POSIX", "1");
|
||||
mod.addCMacro("USE_UNIX_SOCKETS", "1");
|
||||
}
|
||||
|
||||
try buildZlib(b, mod);
|
||||
try buildMbedtls(b, mod);
|
||||
try buildNghttp2(b, mod);
|
||||
try buildCurl(b, mod);
|
||||
|
||||
switch (target.result.os.tag) {
|
||||
.macos => {
|
||||
// needed for proxying on mac
|
||||
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
|
||||
mod.linkFramework("CoreFoundation", .{});
|
||||
mod.linkFramework("SystemConfiguration", .{});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 moduleNetSurf(b: *Build, mod: *Build.Module) !void {
|
||||
const target = mod.resolved_target.?;
|
||||
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));
|
||||
|
||||
// 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) },
|
||||
);
|
||||
mod.addObjectFile(b.path(lib_path));
|
||||
mod.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) },
|
||||
);
|
||||
@@ -234,34 +443,383 @@ 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"));
|
||||
}
|
||||
|
||||
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,
|
||||
fn buildZlib(b: *Build, m: *Build.Module) !void {
|
||||
const zlib = b.addLibrary(.{
|
||||
.name = "zlib",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
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;
|
||||
const root = "vendor/zlib/";
|
||||
zlib.installHeader(b.path(root ++ "zlib.h"), "zlib.h");
|
||||
zlib.installHeader(b.path(root ++ "zconf.h"), "zconf.h");
|
||||
zlib.addCSourceFiles(.{ .flags = &.{
|
||||
"-DHAVE_SYS_TYPES_H",
|
||||
"-DHAVE_STDINT_H",
|
||||
"-DHAVE_STDDEF_H",
|
||||
}, .files = &.{
|
||||
root ++ "adler32.c",
|
||||
root ++ "compress.c",
|
||||
root ++ "crc32.c",
|
||||
root ++ "deflate.c",
|
||||
root ++ "gzclose.c",
|
||||
root ++ "gzlib.c",
|
||||
root ++ "gzread.c",
|
||||
root ++ "gzwrite.c",
|
||||
root ++ "inflate.c",
|
||||
root ++ "infback.c",
|
||||
root ++ "inftrees.c",
|
||||
root ++ "inffast.c",
|
||||
root ++ "trees.c",
|
||||
root ++ "uncompr.c",
|
||||
root ++ "zutil.c",
|
||||
} });
|
||||
}
|
||||
|
||||
fn buildMbedtls(b: *Build, m: *Build.Module) !void {
|
||||
const mbedtls = b.addLibrary(.{
|
||||
.name = "mbedtls",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
const root = "vendor/mbedtls/";
|
||||
mbedtls.addIncludePath(b.path(root ++ "include"));
|
||||
mbedtls.addIncludePath(b.path(root ++ "library"));
|
||||
|
||||
mbedtls.addCSourceFiles(.{ .flags = &.{}, .files = &.{
|
||||
root ++ "library/aes.c",
|
||||
root ++ "library/aesni.c",
|
||||
root ++ "library/aesce.c",
|
||||
root ++ "library/aria.c",
|
||||
root ++ "library/asn1parse.c",
|
||||
root ++ "library/asn1write.c",
|
||||
root ++ "library/base64.c",
|
||||
root ++ "library/bignum.c",
|
||||
root ++ "library/bignum_core.c",
|
||||
root ++ "library/bignum_mod.c",
|
||||
root ++ "library/bignum_mod_raw.c",
|
||||
root ++ "library/camellia.c",
|
||||
root ++ "library/ccm.c",
|
||||
root ++ "library/chacha20.c",
|
||||
root ++ "library/chachapoly.c",
|
||||
root ++ "library/cipher.c",
|
||||
root ++ "library/cipher_wrap.c",
|
||||
root ++ "library/constant_time.c",
|
||||
root ++ "library/cmac.c",
|
||||
root ++ "library/ctr_drbg.c",
|
||||
root ++ "library/des.c",
|
||||
root ++ "library/dhm.c",
|
||||
root ++ "library/ecdh.c",
|
||||
root ++ "library/ecdsa.c",
|
||||
root ++ "library/ecjpake.c",
|
||||
root ++ "library/ecp.c",
|
||||
root ++ "library/ecp_curves.c",
|
||||
root ++ "library/entropy.c",
|
||||
root ++ "library/entropy_poll.c",
|
||||
root ++ "library/error.c",
|
||||
root ++ "library/gcm.c",
|
||||
root ++ "library/hkdf.c",
|
||||
root ++ "library/hmac_drbg.c",
|
||||
root ++ "library/lmots.c",
|
||||
root ++ "library/lms.c",
|
||||
root ++ "library/md.c",
|
||||
root ++ "library/md5.c",
|
||||
root ++ "library/memory_buffer_alloc.c",
|
||||
root ++ "library/nist_kw.c",
|
||||
root ++ "library/oid.c",
|
||||
root ++ "library/padlock.c",
|
||||
root ++ "library/pem.c",
|
||||
root ++ "library/pk.c",
|
||||
root ++ "library/pk_ecc.c",
|
||||
root ++ "library/pk_wrap.c",
|
||||
root ++ "library/pkcs12.c",
|
||||
root ++ "library/pkcs5.c",
|
||||
root ++ "library/pkparse.c",
|
||||
root ++ "library/pkwrite.c",
|
||||
root ++ "library/platform.c",
|
||||
root ++ "library/platform_util.c",
|
||||
root ++ "library/poly1305.c",
|
||||
root ++ "library/psa_crypto.c",
|
||||
root ++ "library/psa_crypto_aead.c",
|
||||
root ++ "library/psa_crypto_cipher.c",
|
||||
root ++ "library/psa_crypto_client.c",
|
||||
root ++ "library/psa_crypto_ffdh.c",
|
||||
root ++ "library/psa_crypto_driver_wrappers_no_static.c",
|
||||
root ++ "library/psa_crypto_ecp.c",
|
||||
root ++ "library/psa_crypto_hash.c",
|
||||
root ++ "library/psa_crypto_mac.c",
|
||||
root ++ "library/psa_crypto_pake.c",
|
||||
root ++ "library/psa_crypto_rsa.c",
|
||||
root ++ "library/psa_crypto_se.c",
|
||||
root ++ "library/psa_crypto_slot_management.c",
|
||||
root ++ "library/psa_crypto_storage.c",
|
||||
root ++ "library/psa_its_file.c",
|
||||
root ++ "library/psa_util.c",
|
||||
root ++ "library/ripemd160.c",
|
||||
root ++ "library/rsa.c",
|
||||
root ++ "library/rsa_alt_helpers.c",
|
||||
root ++ "library/sha1.c",
|
||||
root ++ "library/sha3.c",
|
||||
root ++ "library/sha256.c",
|
||||
root ++ "library/sha512.c",
|
||||
root ++ "library/threading.c",
|
||||
root ++ "library/timing.c",
|
||||
root ++ "library/version.c",
|
||||
root ++ "library/version_features.c",
|
||||
root ++ "library/pkcs7.c",
|
||||
root ++ "library/x509.c",
|
||||
root ++ "library/x509_create.c",
|
||||
root ++ "library/x509_crl.c",
|
||||
root ++ "library/x509_crt.c",
|
||||
root ++ "library/x509_csr.c",
|
||||
root ++ "library/x509write.c",
|
||||
root ++ "library/x509write_crt.c",
|
||||
root ++ "library/x509write_csr.c",
|
||||
root ++ "library/debug.c",
|
||||
root ++ "library/mps_reader.c",
|
||||
root ++ "library/mps_trace.c",
|
||||
root ++ "library/net_sockets.c",
|
||||
root ++ "library/ssl_cache.c",
|
||||
root ++ "library/ssl_ciphersuites.c",
|
||||
root ++ "library/ssl_client.c",
|
||||
root ++ "library/ssl_cookie.c",
|
||||
root ++ "library/ssl_debug_helpers_generated.c",
|
||||
root ++ "library/ssl_msg.c",
|
||||
root ++ "library/ssl_ticket.c",
|
||||
root ++ "library/ssl_tls.c",
|
||||
root ++ "library/ssl_tls12_client.c",
|
||||
root ++ "library/ssl_tls12_server.c",
|
||||
root ++ "library/ssl_tls13_keys.c",
|
||||
root ++ "library/ssl_tls13_server.c",
|
||||
root ++ "library/ssl_tls13_client.c",
|
||||
root ++ "library/ssl_tls13_generic.c",
|
||||
} });
|
||||
}
|
||||
|
||||
fn buildNghttp2(b: *Build, m: *Build.Module) !void {
|
||||
const nghttp2 = b.addLibrary(.{
|
||||
.name = "nghttp2",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
const root = "vendor/nghttp2/";
|
||||
nghttp2.addIncludePath(b.path(root ++ "lib"));
|
||||
nghttp2.addIncludePath(b.path(root ++ "lib/includes"));
|
||||
nghttp2.addCSourceFiles(.{ .flags = &.{
|
||||
"-DNGHTTP2_STATICLIB",
|
||||
"-DHAVE_NETINET_IN",
|
||||
"-DHAVE_TIME_H",
|
||||
}, .files = &.{
|
||||
root ++ "lib/sfparse.c",
|
||||
root ++ "lib/nghttp2_alpn.c",
|
||||
root ++ "lib/nghttp2_buf.c",
|
||||
root ++ "lib/nghttp2_callbacks.c",
|
||||
root ++ "lib/nghttp2_debug.c",
|
||||
root ++ "lib/nghttp2_extpri.c",
|
||||
root ++ "lib/nghttp2_frame.c",
|
||||
root ++ "lib/nghttp2_hd.c",
|
||||
root ++ "lib/nghttp2_hd_huffman.c",
|
||||
root ++ "lib/nghttp2_hd_huffman_data.c",
|
||||
root ++ "lib/nghttp2_helper.c",
|
||||
root ++ "lib/nghttp2_http.c",
|
||||
root ++ "lib/nghttp2_map.c",
|
||||
root ++ "lib/nghttp2_mem.c",
|
||||
root ++ "lib/nghttp2_option.c",
|
||||
root ++ "lib/nghttp2_outbound_item.c",
|
||||
root ++ "lib/nghttp2_pq.c",
|
||||
root ++ "lib/nghttp2_priority_spec.c",
|
||||
root ++ "lib/nghttp2_queue.c",
|
||||
root ++ "lib/nghttp2_rcbuf.c",
|
||||
root ++ "lib/nghttp2_session.c",
|
||||
root ++ "lib/nghttp2_stream.c",
|
||||
root ++ "lib/nghttp2_submit.c",
|
||||
root ++ "lib/nghttp2_version.c",
|
||||
root ++ "lib/nghttp2_ratelim.c",
|
||||
root ++ "lib/nghttp2_time.c",
|
||||
} });
|
||||
}
|
||||
|
||||
fn buildCurl(b: *Build, m: *Build.Module) !void {
|
||||
const curl = b.addLibrary(.{
|
||||
.name = "curl",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
const root = "vendor/curl/";
|
||||
|
||||
curl.addIncludePath(b.path(root ++ "lib"));
|
||||
curl.addIncludePath(b.path(root ++ "include"));
|
||||
curl.addCSourceFiles(.{
|
||||
.flags = &.{},
|
||||
.files = &.{
|
||||
root ++ "lib/altsvc.c",
|
||||
root ++ "lib/amigaos.c",
|
||||
root ++ "lib/asyn-ares.c",
|
||||
root ++ "lib/asyn-base.c",
|
||||
root ++ "lib/asyn-thrdd.c",
|
||||
root ++ "lib/bufq.c",
|
||||
root ++ "lib/bufref.c",
|
||||
root ++ "lib/cf-h1-proxy.c",
|
||||
root ++ "lib/cf-h2-proxy.c",
|
||||
root ++ "lib/cf-haproxy.c",
|
||||
root ++ "lib/cf-https-connect.c",
|
||||
root ++ "lib/cf-socket.c",
|
||||
root ++ "lib/cfilters.c",
|
||||
root ++ "lib/conncache.c",
|
||||
root ++ "lib/connect.c",
|
||||
root ++ "lib/content_encoding.c",
|
||||
root ++ "lib/cookie.c",
|
||||
root ++ "lib/cshutdn.c",
|
||||
root ++ "lib/curl_addrinfo.c",
|
||||
root ++ "lib/curl_des.c",
|
||||
root ++ "lib/curl_endian.c",
|
||||
root ++ "lib/curl_fnmatch.c",
|
||||
root ++ "lib/curl_get_line.c",
|
||||
root ++ "lib/curl_gethostname.c",
|
||||
root ++ "lib/curl_gssapi.c",
|
||||
root ++ "lib/curl_memrchr.c",
|
||||
root ++ "lib/curl_ntlm_core.c",
|
||||
root ++ "lib/curl_range.c",
|
||||
root ++ "lib/curl_rtmp.c",
|
||||
root ++ "lib/curl_sasl.c",
|
||||
root ++ "lib/curl_sha512_256.c",
|
||||
root ++ "lib/curl_sspi.c",
|
||||
root ++ "lib/curl_threads.c",
|
||||
root ++ "lib/curl_trc.c",
|
||||
root ++ "lib/cw-out.c",
|
||||
root ++ "lib/cw-pause.c",
|
||||
root ++ "lib/dict.c",
|
||||
root ++ "lib/doh.c",
|
||||
root ++ "lib/dynhds.c",
|
||||
root ++ "lib/easy.c",
|
||||
root ++ "lib/easygetopt.c",
|
||||
root ++ "lib/easyoptions.c",
|
||||
root ++ "lib/escape.c",
|
||||
root ++ "lib/fake_addrinfo.c",
|
||||
root ++ "lib/file.c",
|
||||
root ++ "lib/fileinfo.c",
|
||||
root ++ "lib/fopen.c",
|
||||
root ++ "lib/formdata.c",
|
||||
root ++ "lib/ftp.c",
|
||||
root ++ "lib/ftplistparser.c",
|
||||
root ++ "lib/getenv.c",
|
||||
root ++ "lib/getinfo.c",
|
||||
root ++ "lib/gopher.c",
|
||||
root ++ "lib/hash.c",
|
||||
root ++ "lib/headers.c",
|
||||
root ++ "lib/hmac.c",
|
||||
root ++ "lib/hostip.c",
|
||||
root ++ "lib/hostip4.c",
|
||||
root ++ "lib/hostip6.c",
|
||||
root ++ "lib/hsts.c",
|
||||
root ++ "lib/http.c",
|
||||
root ++ "lib/http1.c",
|
||||
root ++ "lib/http2.c",
|
||||
root ++ "lib/http_aws_sigv4.c",
|
||||
root ++ "lib/http_chunks.c",
|
||||
root ++ "lib/http_digest.c",
|
||||
root ++ "lib/http_negotiate.c",
|
||||
root ++ "lib/http_ntlm.c",
|
||||
root ++ "lib/http_proxy.c",
|
||||
root ++ "lib/httpsrr.c",
|
||||
root ++ "lib/idn.c",
|
||||
root ++ "lib/if2ip.c",
|
||||
root ++ "lib/imap.c",
|
||||
root ++ "lib/krb5.c",
|
||||
root ++ "lib/ldap.c",
|
||||
root ++ "lib/llist.c",
|
||||
root ++ "lib/macos.c",
|
||||
root ++ "lib/md4.c",
|
||||
root ++ "lib/md5.c",
|
||||
root ++ "lib/memdebug.c",
|
||||
root ++ "lib/mime.c",
|
||||
root ++ "lib/mprintf.c",
|
||||
root ++ "lib/mqtt.c",
|
||||
root ++ "lib/multi.c",
|
||||
root ++ "lib/multi_ev.c",
|
||||
root ++ "lib/netrc.c",
|
||||
root ++ "lib/noproxy.c",
|
||||
root ++ "lib/openldap.c",
|
||||
root ++ "lib/parsedate.c",
|
||||
root ++ "lib/pingpong.c",
|
||||
root ++ "lib/pop3.c",
|
||||
root ++ "lib/progress.c",
|
||||
root ++ "lib/psl.c",
|
||||
root ++ "lib/rand.c",
|
||||
root ++ "lib/rename.c",
|
||||
root ++ "lib/request.c",
|
||||
root ++ "lib/rtsp.c",
|
||||
root ++ "lib/select.c",
|
||||
root ++ "lib/sendf.c",
|
||||
root ++ "lib/setopt.c",
|
||||
root ++ "lib/sha256.c",
|
||||
root ++ "lib/share.c",
|
||||
root ++ "lib/slist.c",
|
||||
root ++ "lib/smb.c",
|
||||
root ++ "lib/smtp.c",
|
||||
root ++ "lib/socketpair.c",
|
||||
root ++ "lib/socks.c",
|
||||
root ++ "lib/socks_gssapi.c",
|
||||
root ++ "lib/socks_sspi.c",
|
||||
root ++ "lib/speedcheck.c",
|
||||
root ++ "lib/splay.c",
|
||||
root ++ "lib/strcase.c",
|
||||
root ++ "lib/strdup.c",
|
||||
root ++ "lib/strequal.c",
|
||||
root ++ "lib/strerror.c",
|
||||
root ++ "lib/system_win32.c",
|
||||
root ++ "lib/telnet.c",
|
||||
root ++ "lib/tftp.c",
|
||||
root ++ "lib/transfer.c",
|
||||
root ++ "lib/uint-bset.c",
|
||||
root ++ "lib/uint-hash.c",
|
||||
root ++ "lib/uint-spbset.c",
|
||||
root ++ "lib/uint-table.c",
|
||||
root ++ "lib/url.c",
|
||||
root ++ "lib/urlapi.c",
|
||||
root ++ "lib/version.c",
|
||||
root ++ "lib/ws.c",
|
||||
root ++ "lib/curlx/base64.c",
|
||||
root ++ "lib/curlx/dynbuf.c",
|
||||
root ++ "lib/curlx/inet_ntop.c",
|
||||
root ++ "lib/curlx/nonblock.c",
|
||||
root ++ "lib/curlx/strparse.c",
|
||||
root ++ "lib/curlx/timediff.c",
|
||||
root ++ "lib/curlx/timeval.c",
|
||||
root ++ "lib/curlx/wait.c",
|
||||
root ++ "lib/curlx/warnless.c",
|
||||
root ++ "lib/vquic/curl_ngtcp2.c",
|
||||
root ++ "lib/vquic/curl_osslq.c",
|
||||
root ++ "lib/vquic/curl_quiche.c",
|
||||
root ++ "lib/vquic/vquic.c",
|
||||
root ++ "lib/vquic/vquic-tls.c",
|
||||
root ++ "lib/vauth/cleartext.c",
|
||||
root ++ "lib/vauth/cram.c",
|
||||
root ++ "lib/vauth/digest.c",
|
||||
root ++ "lib/vauth/digest_sspi.c",
|
||||
root ++ "lib/vauth/gsasl.c",
|
||||
root ++ "lib/vauth/krb5_gssapi.c",
|
||||
root ++ "lib/vauth/krb5_sspi.c",
|
||||
root ++ "lib/vauth/ntlm.c",
|
||||
root ++ "lib/vauth/ntlm_sspi.c",
|
||||
root ++ "lib/vauth/oauth2.c",
|
||||
root ++ "lib/vauth/spnego_gssapi.c",
|
||||
root ++ "lib/vauth/spnego_sspi.c",
|
||||
root ++ "lib/vauth/vauth.c",
|
||||
root ++ "lib/vtls/cipher_suite.c",
|
||||
root ++ "lib/vtls/mbedtls.c",
|
||||
root ++ "lib/vtls/mbedtls_threadlock.c",
|
||||
root ++ "lib/vtls/vtls.c",
|
||||
root ++ "lib/vtls/vtls_scache.c",
|
||||
root ++ "lib/vtls/x509asn1.c",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
13
build.zig.zon
Normal file
13
build.zig.zon
Normal file
@@ -0,0 +1,13 @@
|
||||
.{
|
||||
.name = .browser,
|
||||
.paths = .{""},
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xda130f3af836cea0,
|
||||
.dependencies = .{
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d3040a953e5a37290dae20e7ddf138b7aeb5e67d.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH6w_EAwA8vK0NAxfxfI7IcbnpkUAcXKNujn7qwnmY",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" }
|
||||
},
|
||||
}
|
||||
180
flake.lock
generated
Normal file
180
flake.lock
generated
Normal file
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"zlsPkg",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1756822655,
|
||||
"narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "release-25.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"zigPkgs": "zigPkgs",
|
||||
"zlsPkg": "zlsPkg"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"zigPkgs": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756555914,
|
||||
"narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=",
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"zlsPkg": {
|
||||
"inputs": {
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"zig-overlay": [
|
||||
"zigPkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756048867,
|
||||
"narHash": "sha256-GFzSHUljcxy7sM1PaabbkQUdUnLwpherekPWJFxXtnk=",
|
||||
"owner": "zigtools",
|
||||
"repo": "zls",
|
||||
"rev": "ce6c8f02c78e622421cfc2405c67c5222819ec03",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "zigtools",
|
||||
"ref": "0.15.0",
|
||||
"repo": "zls",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
77
flake.nix
Normal file
77
flake.nix
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
description = "headless browser designed for AI and automation";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/release-25.05";
|
||||
|
||||
zigPkgs.url = "github:mitchellh/zig-overlay";
|
||||
zigPkgs.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
zlsPkg.url = "github:zigtools/zls/0.15.0";
|
||||
zlsPkg.inputs.zig-overlay.follows = "zigPkgs";
|
||||
zlsPkg.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
zigPkgs,
|
||||
zlsPkg,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
overlays = [
|
||||
(final: prev: {
|
||||
zigpkgs = zigPkgs.packages.${prev.system};
|
||||
zls = zlsPkg.packages.${prev.system}.default;
|
||||
})
|
||||
];
|
||||
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
};
|
||||
|
||||
# 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
|
||||
zigpkgs."0.15.1"
|
||||
zls
|
||||
python3
|
||||
pkg-config
|
||||
cmake
|
||||
gperf
|
||||
|
||||
# GCC
|
||||
gcc
|
||||
gcc.cc.lib
|
||||
crtFiles
|
||||
|
||||
# Libaries
|
||||
expat.dev
|
||||
glib.dev
|
||||
glibc.dev
|
||||
zlib
|
||||
zlib.dev
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = fhs.env;
|
||||
}
|
||||
);
|
||||
}
|
||||
117
src/TestHTTPServer.zig
Normal file
117
src/TestHTTPServer.zig
Normal file
@@ -0,0 +1,117 @@
|
||||
const std = @import("std");
|
||||
|
||||
const TestHTTPServer = @This();
|
||||
|
||||
shutdown: bool,
|
||||
listener: ?std.net.Server,
|
||||
handler: Handler,
|
||||
|
||||
const Handler = *const fn (req: *std.http.Server.Request) anyerror!void;
|
||||
|
||||
pub fn init(handler: Handler) TestHTTPServer {
|
||||
return .{
|
||||
.shutdown = true,
|
||||
.listener = null,
|
||||
.handler = handler,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TestHTTPServer) void {
|
||||
self.shutdown = true;
|
||||
if (self.listener) |*listener| {
|
||||
listener.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {
|
||||
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
|
||||
|
||||
self.listener = try address.listen(.{ .reuse_address = true });
|
||||
var listener = &self.listener.?;
|
||||
|
||||
wg.finish();
|
||||
|
||||
while (true) {
|
||||
const conn = listener.accept() catch |err| {
|
||||
if (self.shutdown) {
|
||||
return;
|
||||
}
|
||||
return err;
|
||||
};
|
||||
const thrd = try std.Thread.spawn(.{}, handleConnection, .{ self, conn });
|
||||
thrd.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void {
|
||||
defer conn.stream.close();
|
||||
|
||||
var req_buf: [2048]u8 = undefined;
|
||||
var conn_reader = conn.stream.reader(&req_buf);
|
||||
var conn_writer = conn.stream.writer(&req_buf);
|
||||
|
||||
var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface);
|
||||
|
||||
while (true) {
|
||||
var req = http_server.receiveHead() catch |err| switch (err) {
|
||||
error.ReadFailed => continue,
|
||||
error.HttpConnectionClosing => continue,
|
||||
else => {
|
||||
std.debug.print("Test HTTP Server error: {}\n", .{err});
|
||||
return err;
|
||||
},
|
||||
};
|
||||
self.handler(&req) catch |err| {
|
||||
std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err });
|
||||
try req.respond("server error", .{ .status = .internal_server_error });
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
|
||||
var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) {
|
||||
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
|
||||
else => return err,
|
||||
};
|
||||
|
||||
const stat = try file.stat();
|
||||
var send_buffer: [4096]u8 = undefined;
|
||||
|
||||
var res = try req.respondStreaming(&send_buffer, .{
|
||||
.content_length = stat.size,
|
||||
.respond_options = .{
|
||||
.extra_headers = &.{
|
||||
.{ .name = "content-type", .value = getContentType(file_path) },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var read_buffer: [4096]u8 = undefined;
|
||||
var reader = file.reader(&read_buffer);
|
||||
_ = try res.writer.sendFileAll(&reader, .unlimited);
|
||||
try res.writer.flush();
|
||||
try res.end();
|
||||
}
|
||||
|
||||
fn getContentType(file_path: []const u8) []const u8 {
|
||||
if (std.mem.endsWith(u8, file_path, ".js")) {
|
||||
return "application/json";
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, file_path, ".html")) {
|
||||
return "text/html";
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, file_path, ".htm")) {
|
||||
return "text/html";
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, file_path, ".xml")) {
|
||||
// some wpt tests do this
|
||||
return "text/xml";
|
||||
}
|
||||
|
||||
std.debug.print("TestHTTPServer asked to serve an unknown file type: {s}\n", .{file_path});
|
||||
return "text/html";
|
||||
}
|
||||
@@ -1,47 +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 generate = @import("generate.zig");
|
||||
|
||||
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");
|
||||
const XMLSerializer = @import("xmlserializer/xmlserializer.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,
|
||||
XMLSerializer.Interfaces,
|
||||
}){};
|
||||
|
||||
pub const UserContext = @import("user_context.zig").UserContext;
|
||||
113
src/app.zig
Normal file
113
src/app.zig
Normal file
@@ -0,0 +1,113 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("log.zig");
|
||||
const Http = @import("http/Http.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 {
|
||||
http: Http,
|
||||
config: Config,
|
||||
platform: Platform,
|
||||
allocator: Allocator,
|
||||
telemetry: Telemetry,
|
||||
app_dir_path: ?[]const u8,
|
||||
notification: *Notification,
|
||||
|
||||
pub const RunMode = enum {
|
||||
help,
|
||||
fetch,
|
||||
serve,
|
||||
version,
|
||||
};
|
||||
|
||||
pub const Config = struct {
|
||||
run_mode: RunMode,
|
||||
tls_verify_host: bool = true,
|
||||
http_proxy: ?[:0]const u8 = null,
|
||||
proxy_bearer_token: ?[:0]const u8 = null,
|
||||
http_timeout_ms: ?u31 = null,
|
||||
http_connect_timeout_ms: ?u31 = null,
|
||||
http_max_host_open: ?u8 = null,
|
||||
http_max_concurrent: ?u8 = null,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator, config: Config) !*App {
|
||||
const app = try allocator.create(App);
|
||||
errdefer allocator.destroy(app);
|
||||
|
||||
const notification = try Notification.init(allocator, null);
|
||||
errdefer notification.deinit();
|
||||
|
||||
var http = try Http.init(allocator, .{
|
||||
.max_host_open = config.http_max_host_open orelse 4,
|
||||
.max_concurrent = config.http_max_concurrent orelse 10,
|
||||
.timeout_ms = config.http_timeout_ms orelse 5000,
|
||||
.connect_timeout_ms = config.http_connect_timeout_ms orelse 0,
|
||||
.http_proxy = config.http_proxy,
|
||||
.tls_verify_host = config.tls_verify_host,
|
||||
.proxy_bearer_token = config.proxy_bearer_token,
|
||||
});
|
||||
errdefer http.deinit();
|
||||
|
||||
const platform = try Platform.init();
|
||||
errdefer platform.deinit();
|
||||
|
||||
const app_dir_path = getAndMakeAppDir(allocator);
|
||||
|
||||
app.* = .{
|
||||
.http = http,
|
||||
.allocator = allocator,
|
||||
.telemetry = undefined,
|
||||
.platform = platform,
|
||||
.app_dir_path = app_dir_path,
|
||||
.notification = notification,
|
||||
.config = config,
|
||||
};
|
||||
|
||||
app.telemetry = try Telemetry.init(app, config.run_mode);
|
||||
errdefer app.telemetry.deinit();
|
||||
|
||||
try app.telemetry.register(app.notification);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App) void {
|
||||
const allocator = self.allocator;
|
||||
if (self.app_dir_path) |app_dir_path| {
|
||||
allocator.free(app_dir_path);
|
||||
}
|
||||
self.telemetry.deinit();
|
||||
self.notification.deinit();
|
||||
self.http.deinit();
|
||||
self.platform.deinit();
|
||||
allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 {
|
||||
if (@import("builtin").is_test) {
|
||||
return allocator.dupe(u8, "/tmp") catch unreachable;
|
||||
}
|
||||
const app_dir_path = std.fs.getAppDataDir(allocator, "lightpanda") catch |err| {
|
||||
log.warn(.app, "get data dir", .{ .err = err });
|
||||
return null;
|
||||
};
|
||||
|
||||
std.fs.cwd().makePath(app_dir_path) catch |err| switch (err) {
|
||||
error.PathAlreadyExists => return app_dir_path,
|
||||
else => {
|
||||
allocator.free(app_dir_path);
|
||||
log.warn(.app, "create data dir", .{ .err = err, .path = app_dir_path });
|
||||
return null;
|
||||
},
|
||||
};
|
||||
return app_dir_path;
|
||||
}
|
||||
52
src/browser/DataURI.zig
Normal file
52
src/browser/DataURI.zig
Normal file
@@ -0,0 +1,52 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// Parses data:[<media-type>][;base64],<data>
|
||||
pub fn parse(allocator: Allocator, src: []const u8) !?[]const u8 {
|
||||
if (!std.mem.startsWith(u8, src, "data:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uri = src[5..];
|
||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
||||
|
||||
var data = uri[data_starts + 1 ..];
|
||||
|
||||
// Extract the encoding.
|
||||
const metadata = uri[0..data_starts];
|
||||
if (std.mem.endsWith(u8, metadata, ";base64")) {
|
||||
const decoder = std.base64.standard.Decoder;
|
||||
const decoded_size = try decoder.calcSizeForSlice(data);
|
||||
|
||||
const buffer = try allocator.alloc(u8, decoded_size);
|
||||
errdefer allocator.free(buffer);
|
||||
|
||||
try decoder.decode(buffer, data);
|
||||
data = buffer;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "DataURI: parse valid" {
|
||||
try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo");
|
||||
try test_valid("data:text/javascript; charset=utf-8;,foo", "foo");
|
||||
try test_valid("data:,foo", "foo");
|
||||
}
|
||||
|
||||
test "DataURI: parse invalid" {
|
||||
try test_cannot_parse("atad:,foo");
|
||||
try test_cannot_parse("data:foo");
|
||||
try test_cannot_parse("data:");
|
||||
}
|
||||
|
||||
fn test_valid(uri: []const u8, expected: []const u8) !void {
|
||||
defer testing.reset();
|
||||
const data_uri = try parse(testing.arena_allocator, uri) orelse return error.TestFailed;
|
||||
try testing.expectEqual(expected, data_uri);
|
||||
}
|
||||
|
||||
fn test_cannot_parse(uri: []const u8) !void {
|
||||
try testing.expectEqual(null, parse(undefined, uri));
|
||||
}
|
||||
174
src/browser/Scheduler.zig
Normal file
174
src/browser/Scheduler.zig
Normal file
@@ -0,0 +1,174 @@
|
||||
// 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 Allocator = std.mem.Allocator;
|
||||
|
||||
const Scheduler = @This();
|
||||
|
||||
high_priority: Queue,
|
||||
|
||||
// For repeating tasks. We only want to run these if there are other things to
|
||||
// do. We don't, for example, want a window.setInterval or the page.runMicrotasks
|
||||
// to block the page.wait.
|
||||
low_priority: Queue,
|
||||
|
||||
// we expect allocator to be the page arena, hence we never call high_priority.deinit
|
||||
pub fn init(allocator: Allocator) Scheduler {
|
||||
return .{
|
||||
.high_priority = Queue.init(allocator, {}),
|
||||
.low_priority = Queue.init(allocator, {}),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Scheduler) void {
|
||||
self.high_priority.clearRetainingCapacity();
|
||||
self.low_priority.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
const AddOpts = struct {
|
||||
name: []const u8 = "",
|
||||
low_priority: bool = false,
|
||||
};
|
||||
pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: AddOpts) !void {
|
||||
var low_priority = opts.low_priority;
|
||||
if (ms > 5_000) {
|
||||
// we don't want tasks in the far future to block page.wait from
|
||||
// completing. However, if page.wait is called multiple times (maybe
|
||||
// a CDP driver is wait for something to happen), then we do want
|
||||
// to [eventually] run these when their time is up.
|
||||
low_priority = true;
|
||||
}
|
||||
|
||||
var q = if (low_priority) &self.low_priority else &self.high_priority;
|
||||
return q.add(.{
|
||||
.ms = std.time.milliTimestamp() + ms,
|
||||
.ctx = ctx,
|
||||
.func = func,
|
||||
.name = opts.name,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn runHighPriority(self: *Scheduler) !?i32 {
|
||||
return self.runQueue(&self.high_priority);
|
||||
}
|
||||
|
||||
pub fn runLowPriority(self: *Scheduler) !?i32 {
|
||||
return self.runQueue(&self.low_priority);
|
||||
}
|
||||
|
||||
fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
|
||||
// this is O(1)
|
||||
if (queue.count() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = std.time.milliTimestamp();
|
||||
|
||||
var next = queue.peek();
|
||||
while (next) |task| {
|
||||
const time_to_next = task.ms - now;
|
||||
if (time_to_next > 0) {
|
||||
// @intCast is petty safe since we limit tasks to just 5 seconds
|
||||
// in the future
|
||||
return @intCast(time_to_next);
|
||||
}
|
||||
|
||||
if (task.func(task.ctx)) |repeat_delay| {
|
||||
// if we do (now + 0) then our WHILE loop will run endlessly.
|
||||
// no task should ever return 0
|
||||
std.debug.assert(repeat_delay != 0);
|
||||
|
||||
var copy = task;
|
||||
copy.ms = now + repeat_delay;
|
||||
try self.low_priority.add(copy);
|
||||
}
|
||||
_ = queue.remove();
|
||||
next = queue.peek();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const Task = struct {
|
||||
ms: i64,
|
||||
func: Func,
|
||||
ctx: *anyopaque,
|
||||
name: []const u8,
|
||||
|
||||
const Func = *const fn (ctx: *anyopaque) ?u32;
|
||||
};
|
||||
|
||||
const Queue = std.PriorityQueue(Task, void, struct {
|
||||
fn compare(_: void, a: Task, b: Task) std.math.Order {
|
||||
return std.math.order(a.ms, b.ms);
|
||||
}
|
||||
}.compare);
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "Scheduler" {
|
||||
defer testing.reset();
|
||||
|
||||
var task = TestTask{ .allocator = testing.arena_allocator };
|
||||
|
||||
var s = Scheduler.init(testing.arena_allocator);
|
||||
try testing.expectEqual(null, s.runHighPriority());
|
||||
try testing.expectEqual(0, task.calls.items.len);
|
||||
|
||||
try s.add(&task, TestTask.run1, 3, .{});
|
||||
|
||||
try testing.expectDelta(3, try s.runHighPriority(), 1);
|
||||
try testing.expectEqual(0, task.calls.items.len);
|
||||
|
||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
||||
try testing.expectEqual(null, s.runHighPriority());
|
||||
try testing.expectEqualSlices(u32, &.{1}, task.calls.items);
|
||||
|
||||
try s.add(&task, TestTask.run2, 3, .{});
|
||||
try s.add(&task, TestTask.run1, 2, .{});
|
||||
|
||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
||||
try testing.expectDelta(null, try s.runHighPriority(), 1);
|
||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
||||
|
||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
||||
// won't run low_priority
|
||||
try testing.expectEqual(null, try s.runHighPriority());
|
||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
||||
|
||||
//runs low_priority
|
||||
try testing.expectDelta(2, try s.runLowPriority(), 1);
|
||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2, 2 }, task.calls.items);
|
||||
}
|
||||
|
||||
const TestTask = struct {
|
||||
allocator: Allocator,
|
||||
calls: std.ArrayListUnmanaged(u32) = .{},
|
||||
|
||||
fn run1(ctx: *anyopaque) ?u32 {
|
||||
var self: *TestTask = @ptrCast(@alignCast(ctx));
|
||||
self.calls.append(self.allocator, 1) catch unreachable;
|
||||
return null;
|
||||
}
|
||||
|
||||
fn run2(ctx: *anyopaque) ?u32 {
|
||||
var self: *TestTask = @ptrCast(@alignCast(ctx));
|
||||
self.calls.append(self.allocator, 2) catch unreachable;
|
||||
return 2;
|
||||
}
|
||||
};
|
||||
835
src/browser/ScriptManager.zig
Normal file
835
src/browser/ScriptManager.zig
Normal file
@@ -0,0 +1,835 @@
|
||||
// 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 DataURI = @import("DataURI.zig");
|
||||
const Http = @import("../http/Http.zig");
|
||||
const Browser = @import("browser.zig").Browser;
|
||||
const URL = @import("../url.zig").URL;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArrayListUnmanaged = std.ArrayListUnmanaged;
|
||||
|
||||
const ScriptManager = @This();
|
||||
|
||||
page: *Page,
|
||||
|
||||
// used to prevent recursive evalutaion
|
||||
is_evaluating: bool,
|
||||
|
||||
// used to prevent executing scripts while we're doing a blocking load
|
||||
is_blocking: bool = false,
|
||||
|
||||
// Only once this is true can deferred scripts be run
|
||||
static_scripts_done: bool,
|
||||
|
||||
// List of async scripts. We don't care about the execution order of these, but
|
||||
// on shutdown/abort, we need to cleanup any pending ones.
|
||||
asyncs: OrderList,
|
||||
|
||||
// When an async script is ready to be evaluated, it's moved from asyncs to
|
||||
// this list. You might think we can evaluate an async script as soon as it's
|
||||
// done, but we can only evaluate scripts when `is_blocking == false`. So this
|
||||
// becomes a list of scripts to execute on the next evaluate().
|
||||
asyncs_ready: OrderList,
|
||||
|
||||
// Normal scripts (non-deferred & non-async). These must be executed in order
|
||||
scripts: OrderList,
|
||||
|
||||
// List of deferred scripts. These must be executed in order, but only once
|
||||
// dom_loaded == true,
|
||||
deferreds: OrderList,
|
||||
|
||||
shutdown: bool = false,
|
||||
|
||||
client: *Http.Client,
|
||||
allocator: Allocator,
|
||||
buffer_pool: BufferPool,
|
||||
script_pool: std.heap.MemoryPool(PendingScript),
|
||||
|
||||
const OrderList = std.DoublyLinkedList;
|
||||
|
||||
pub fn init(browser: *Browser, page: *Page) ScriptManager {
|
||||
// page isn't fully initialized, we can setup our reference, but that's it.
|
||||
const allocator = browser.allocator;
|
||||
return .{
|
||||
.page = page,
|
||||
.asyncs = .{},
|
||||
.scripts = .{},
|
||||
.deferreds = .{},
|
||||
.asyncs_ready = .{},
|
||||
.is_evaluating = false,
|
||||
.allocator = allocator,
|
||||
.client = browser.http_client,
|
||||
.static_scripts_done = false,
|
||||
.buffer_pool = BufferPool.init(allocator, 5),
|
||||
.script_pool = std.heap.MemoryPool(PendingScript).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ScriptManager) void {
|
||||
self.reset();
|
||||
self.buffer_pool.deinit();
|
||||
self.script_pool.deinit();
|
||||
}
|
||||
|
||||
pub fn reset(self: *ScriptManager) void {
|
||||
self.clearList(&self.asyncs);
|
||||
self.clearList(&self.scripts);
|
||||
self.clearList(&self.deferreds);
|
||||
self.clearList(&self.asyncs_ready);
|
||||
self.static_scripts_done = false;
|
||||
}
|
||||
|
||||
fn clearList(_: *const ScriptManager, list: *OrderList) void {
|
||||
while (list.first) |node| {
|
||||
const pending_script: *PendingScript = @fieldParentPtr("node", node);
|
||||
// this removes it from the list
|
||||
pending_script.deinit();
|
||||
}
|
||||
std.debug.assert(list.first == null);
|
||||
}
|
||||
|
||||
pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
||||
if (try parser.elementGetAttribute(element, "nomodule") != null) {
|
||||
// these scripts should only be loaded if we don't support modules
|
||||
// but since we do support modules, we can just skip them.
|
||||
return;
|
||||
}
|
||||
|
||||
// If a script tag gets dynamically created and added to the dom:
|
||||
// document.getElementsByTagName('head')[0].appendChild(script)
|
||||
// that script tag will immediately get executed by our scriptAddedCallback.
|
||||
// However, if the location where the script tag is inserted happens to be
|
||||
// below where processHTMLDoc currently is, then we'll re-run that same script
|
||||
// again in processHTMLDoc. This flag is used to let us know if a specific
|
||||
// <script> has already been processed.
|
||||
if (try parser.scriptGetProcessed(@ptrCast(element))) {
|
||||
return;
|
||||
}
|
||||
try parser.scriptSetProcessed(@ptrCast(element), true);
|
||||
|
||||
const kind: Script.Kind = blk: {
|
||||
const script_type = try parser.elementGetAttribute(element, "type") orelse break :blk .javascript;
|
||||
if (script_type.len == 0) {
|
||||
break :blk .javascript;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(script_type, "application/javascript")) {
|
||||
break :blk .javascript;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(script_type, "text/javascript")) {
|
||||
break :blk .javascript;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(script_type, "module")) {
|
||||
break :blk .module;
|
||||
}
|
||||
|
||||
// "type" could be anything, but only the above are ones we need to process.
|
||||
// Common other ones are application/json, application/ld+json, text/template
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
const page = self.page;
|
||||
var source: Script.Source = undefined;
|
||||
var remote_url: ?[:0]const u8 = null;
|
||||
if (try parser.elementGetAttribute(element, "src")) |src| {
|
||||
if (try DataURI.parse(page.arena, src)) |data_uri| {
|
||||
source = .{ .@"inline" = data_uri };
|
||||
}
|
||||
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true });
|
||||
source = .{ .remote = .{} };
|
||||
} else {
|
||||
const inline_source = try parser.nodeTextContent(@ptrCast(element)) orelse return;
|
||||
source = .{ .@"inline" = inline_source };
|
||||
}
|
||||
|
||||
var script = Script{
|
||||
.kind = kind,
|
||||
.element = element,
|
||||
.source = source,
|
||||
.url = remote_url orelse page.url.raw,
|
||||
.is_defer = if (remote_url == null) false else try parser.elementGetAttribute(element, "defer") != null,
|
||||
.is_async = if (remote_url == null) false else try parser.elementGetAttribute(element, "async") != null,
|
||||
};
|
||||
|
||||
if (source == .@"inline" and self.scripts.first == null) {
|
||||
// inline script with no pending scripts, execute it immediately.
|
||||
// (if there is a pending script, then we cannot execute this immediately
|
||||
// as it needs to best executed in order)
|
||||
return script.eval(page);
|
||||
}
|
||||
|
||||
const pending_script = try self.script_pool.create();
|
||||
errdefer self.script_pool.destroy(pending_script);
|
||||
pending_script.* = .{
|
||||
.script = script,
|
||||
.complete = false,
|
||||
.manager = self,
|
||||
.node = .{},
|
||||
};
|
||||
|
||||
if (source == .@"inline") {
|
||||
// if we're here, it means that we have pending scripts (i.e. self.scripts
|
||||
// is not empty). Because the script is inline, it's complete/ready, but
|
||||
// we need to process them in order
|
||||
pending_script.complete = true;
|
||||
self.scripts.append(&pending_script.node);
|
||||
return;
|
||||
} else {
|
||||
log.debug(.http, "script queue", .{ .url = remote_url.? });
|
||||
}
|
||||
|
||||
pending_script.getList().append(&pending_script.node);
|
||||
|
||||
errdefer pending_script.deinit();
|
||||
|
||||
var headers = try Http.Headers.init();
|
||||
try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers);
|
||||
|
||||
try self.client.request(.{
|
||||
.url = remote_url.?,
|
||||
.ctx = pending_script,
|
||||
.method = .GET,
|
||||
.headers = headers,
|
||||
.cookie_jar = page.cookie_jar,
|
||||
.resource_type = .script,
|
||||
.start_callback = if (log.enabled(.http, .debug)) startCallback else null,
|
||||
.header_callback = headerCallback,
|
||||
.data_callback = dataCallback,
|
||||
.done_callback = doneCallback,
|
||||
.error_callback = errorCallback,
|
||||
});
|
||||
}
|
||||
|
||||
// @TODO: Improving this would have the simplest biggest performance improvement
|
||||
// for most sites.
|
||||
//
|
||||
// For JS imports (both static and dynamic), we currently block to get the
|
||||
// result (the content of the file).
|
||||
//
|
||||
// For static imports, this is necessary, since v8 is expecting the compiled module
|
||||
// as part of the function return. (we should try to pre-load the JavaScript
|
||||
// source via module.GetModuleRequests(), but that's for a later time).
|
||||
//
|
||||
// For dynamic dynamic imports, this is not strictly necessary since the v8
|
||||
// call returns a Promise; we could make this a normal get call, associated with
|
||||
// the promise, and when done, resolve the promise.
|
||||
//
|
||||
// In both cases, for now at least, we just issue a "blocking" request. We block
|
||||
// by ticking the http client until the script is complete.
|
||||
//
|
||||
// This uses the client.blockingRequest call which has a dedicated handle for
|
||||
// these blocking requests. Because they are blocking, we're guaranteed to have
|
||||
// only 1 at a time, thus the 1 reserved handle.
|
||||
//
|
||||
// You almost don't need the http client's blocking handle. In most cases, you
|
||||
// should always have 1 free handle whenever you get here, because we always
|
||||
// release the handle before executing the doneCallback. So, if a module does:
|
||||
// import * as x from 'blah'
|
||||
// And we need to load 'blah', there should always be 1 free handle - the handle
|
||||
// of the http GET we just completed before executing the module.
|
||||
// The exception to this, and the reason we need a special blocking handle, is
|
||||
// for inline modules within the HTML page itself:
|
||||
// <script type=module>import ....</script>
|
||||
// Unlike external modules which can only ever be executed after releasing an
|
||||
// http handle, these are executed without there necessarily being a free handle.
|
||||
// Thus, Http/Client.zig maintains a dedicated handle for these calls.
|
||||
pub fn blockingGet(self: *ScriptManager, url: [:0]const u8) !BlockingResult {
|
||||
std.debug.assert(self.is_blocking == false);
|
||||
|
||||
self.is_blocking = true;
|
||||
defer {
|
||||
self.is_blocking = false;
|
||||
|
||||
// we blocked evaluation while loading this script, there could be
|
||||
// scripts ready to process.
|
||||
self.evaluate();
|
||||
}
|
||||
|
||||
var blocking = Blocking{
|
||||
.allocator = self.allocator,
|
||||
.buffer_pool = &self.buffer_pool,
|
||||
};
|
||||
|
||||
var headers = try Http.Headers.init();
|
||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||
|
||||
var client = self.client;
|
||||
try client.blockingRequest(.{
|
||||
.url = url,
|
||||
.method = .GET,
|
||||
.headers = headers,
|
||||
.cookie_jar = self.page.cookie_jar,
|
||||
.ctx = &blocking,
|
||||
.resource_type = .script,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null,
|
||||
.header_callback = Blocking.headerCallback,
|
||||
.data_callback = Blocking.dataCallback,
|
||||
.done_callback = Blocking.doneCallback,
|
||||
.error_callback = Blocking.errorCallback,
|
||||
});
|
||||
|
||||
// rely on http's timeout settings to avoid an endless/long loop.
|
||||
while (true) {
|
||||
_ = try client.tick(200);
|
||||
switch (blocking.state) {
|
||||
.running => {},
|
||||
.done => |result| return result,
|
||||
.err => |err| return err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn staticScriptsDone(self: *ScriptManager) void {
|
||||
std.debug.assert(self.static_scripts_done == false);
|
||||
self.static_scripts_done = true;
|
||||
}
|
||||
|
||||
// try to evaluate completed scripts (in order). This is called whenever a script
|
||||
// is completed.
|
||||
fn evaluate(self: *ScriptManager) void {
|
||||
if (self.is_evaluating) {
|
||||
// It's possible for a script.eval to cause evaluate to be called again.
|
||||
// This is particularly true with blockingGet, but even without this,
|
||||
// it's theoretically possible (but unlikely). We could make this work
|
||||
// but there's little reason to support the complexity.
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.is_blocking) {
|
||||
// Cannot evaluate scripts while a blocking-load is in progress. Not
|
||||
// only could that result in incorrect evaluation order, it could
|
||||
// trigger another blocking request, while we're doing a blocking request.
|
||||
return;
|
||||
}
|
||||
|
||||
const page = self.page;
|
||||
self.is_evaluating = true;
|
||||
defer self.is_evaluating = false;
|
||||
|
||||
// every script in asyncs_ready is ready to be evaluated.
|
||||
while (self.asyncs_ready.first) |n| {
|
||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
||||
defer pending_script.deinit();
|
||||
pending_script.script.eval(page);
|
||||
}
|
||||
|
||||
while (self.scripts.first) |n| {
|
||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
||||
if (pending_script.complete == false) {
|
||||
return;
|
||||
}
|
||||
defer pending_script.deinit();
|
||||
pending_script.script.eval(page);
|
||||
}
|
||||
|
||||
if (self.static_scripts_done == false) {
|
||||
// We can only execute deferred scripts if
|
||||
// 1 - all the normal scripts are done
|
||||
// 2 - we've finished parsing the HTML and at least queued all the scripts
|
||||
// The last one isn't obvious, but it's possible for self.scripts to
|
||||
// be empty not because we're done executing all the normal scripts
|
||||
// but because we're done executing some (or maybe none), but we're still
|
||||
// parsing the HTML.
|
||||
return;
|
||||
}
|
||||
|
||||
while (self.deferreds.first) |n| {
|
||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
||||
if (pending_script.complete == false) {
|
||||
return;
|
||||
}
|
||||
defer pending_script.deinit();
|
||||
pending_script.script.eval(page);
|
||||
}
|
||||
|
||||
// When all scripts (normal and deferred) are done loading, the document
|
||||
// state changes (this ultimately triggers the DOMContentLoaded event)
|
||||
page.documentIsLoaded();
|
||||
|
||||
if (self.asyncs.first == null) {
|
||||
// 1 - there are no async scripts pending
|
||||
// 2 - we checkecked static_scripts_done == true above
|
||||
// 3 - we drained self.scripts above
|
||||
// 4 - we drained self.deferred above
|
||||
page.documentIsComplete();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isDone(self: *const ScriptManager) bool {
|
||||
return self.asyncs.first == null and // there are no more async scripts
|
||||
self.static_scripts_done and // and we've finished parsing the HTML to queue all <scripts>
|
||||
self.scripts.first == null and // and there are no more <script src=> to wait for
|
||||
self.deferreds.first == null; // and there are no more <script defer src=> to wait for
|
||||
}
|
||||
|
||||
fn startCallback(transfer: *Http.Transfer) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||
script.startCallback(transfer) catch |err| {
|
||||
log.err(.http, "SM.startCallback", .{ .err = err, .transfer = transfer });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn headerCallback(transfer: *Http.Transfer) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||
try script.headerCallback(transfer);
|
||||
}
|
||||
|
||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||
script.dataCallback(transfer, data) catch |err| {
|
||||
log.err(.http, "SM.dataCallback", .{ .err = err, .transfer = transfer, .len = data.len });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn doneCallback(ctx: *anyopaque) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(ctx));
|
||||
script.doneCallback();
|
||||
}
|
||||
|
||||
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(ctx));
|
||||
script.errorCallback(err);
|
||||
}
|
||||
|
||||
// A script which is pending execution.
|
||||
// It could be pending because:
|
||||
// (a) we're still downloading its content or
|
||||
// (b) this is a non-async script that has to be executed in order
|
||||
pub const PendingScript = struct {
|
||||
script: Script,
|
||||
complete: bool,
|
||||
node: OrderList.Node,
|
||||
manager: *ScriptManager,
|
||||
|
||||
fn deinit(self: *PendingScript) void {
|
||||
const script = &self.script;
|
||||
const manager = self.manager;
|
||||
|
||||
if (script.source == .remote) {
|
||||
manager.buffer_pool.release(script.source.remote);
|
||||
}
|
||||
self.getList().remove(&self.node);
|
||||
}
|
||||
|
||||
fn remove(self: *PendingScript) void {
|
||||
if (self.node) |*node| {
|
||||
self.getList().remove(node);
|
||||
self.node = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn startCallback(self: *PendingScript, transfer: *Http.Transfer) !void {
|
||||
_ = self;
|
||||
log.debug(.http, "script fetch start", .{ .req = transfer });
|
||||
}
|
||||
|
||||
fn headerCallback(self: *PendingScript, transfer: *Http.Transfer) !void {
|
||||
const header = &transfer.response_header.?;
|
||||
if (header.status != 200) {
|
||||
log.info(.http, "script header", .{
|
||||
.req = transfer,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(.http, "script header", .{
|
||||
.req = transfer,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
|
||||
// If this isn't true, then we'll likely leak memory. If you don't
|
||||
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
|
||||
// will fail. This assertion exists to catch incorrect assumptions about
|
||||
// how libcurl works, or about how we've configured it.
|
||||
std.debug.assert(self.script.source.remote.capacity == 0);
|
||||
var buffer = self.manager.buffer_pool.get();
|
||||
if (transfer.getContentLength()) |cl| {
|
||||
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
|
||||
}
|
||||
self.script.source = .{ .remote = buffer };
|
||||
}
|
||||
|
||||
fn dataCallback(self: *PendingScript, transfer: *Http.Transfer, data: []const u8) !void {
|
||||
_ = transfer;
|
||||
// too verbose
|
||||
// log.debug(.http, "script data chunk", .{
|
||||
// .req = transfer,
|
||||
// .len = data.len,
|
||||
// });
|
||||
|
||||
try self.script.source.remote.appendSlice(self.manager.allocator, data);
|
||||
}
|
||||
|
||||
fn doneCallback(self: *PendingScript) void {
|
||||
log.debug(.http, "script fetch complete", .{ .req = self.script.url });
|
||||
|
||||
const manager = self.manager;
|
||||
self.complete = true;
|
||||
if (self.script.is_async) {
|
||||
manager.asyncs.remove(&self.node);
|
||||
manager.asyncs_ready.append(&self.node);
|
||||
}
|
||||
manager.evaluate();
|
||||
}
|
||||
|
||||
fn errorCallback(self: *PendingScript, err: anyerror) void {
|
||||
log.warn(.http, "script fetch error", .{ .req = self.script.url, .err = err });
|
||||
|
||||
const manager = self.manager;
|
||||
|
||||
self.deinit();
|
||||
|
||||
if (manager.shutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
manager.evaluate();
|
||||
}
|
||||
|
||||
fn getList(self: *const PendingScript) *OrderList {
|
||||
// When a script has both the async and defer flag set, it should be
|
||||
// treated as async. Async is newer, so some websites use both so that
|
||||
// if async isn't known, it'll fallback to defer.
|
||||
|
||||
const script = &self.script;
|
||||
if (script.is_async) {
|
||||
return if (self.complete) &self.manager.asyncs_ready else &self.manager.asyncs;
|
||||
}
|
||||
|
||||
if (script.is_defer) {
|
||||
return &self.manager.deferreds;
|
||||
}
|
||||
|
||||
return &self.manager.scripts;
|
||||
}
|
||||
};
|
||||
|
||||
const Script = struct {
|
||||
kind: Kind,
|
||||
url: []const u8,
|
||||
is_async: bool,
|
||||
is_defer: bool,
|
||||
source: Source,
|
||||
element: *parser.Element,
|
||||
|
||||
const Kind = enum {
|
||||
module,
|
||||
javascript,
|
||||
};
|
||||
|
||||
const Callback = union(enum) {
|
||||
string: []const u8,
|
||||
function: Env.Function,
|
||||
};
|
||||
|
||||
const Source = union(enum) {
|
||||
@"inline": []const u8,
|
||||
remote: std.ArrayListUnmanaged(u8),
|
||||
|
||||
fn content(self: Source) []const u8 {
|
||||
return switch (self) {
|
||||
.remote => |buf| buf.items,
|
||||
.@"inline" => |c| c,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
fn eval(self: *Script, page: *Page) void {
|
||||
page.setCurrentScript(@ptrCast(self.element)) catch |err| {
|
||||
log.err(.browser, "set document script", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
|
||||
defer page.setCurrentScript(null) catch |err| {
|
||||
log.err(.browser, "clear document script", .{ .err = err });
|
||||
};
|
||||
|
||||
// inline scripts aren't cached. remote ones are.
|
||||
const cacheable = self.source == .remote;
|
||||
|
||||
const url = self.url;
|
||||
|
||||
log.info(.browser, "executing script", .{
|
||||
.src = url,
|
||||
.kind = self.kind,
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
|
||||
const js_context = page.main_context;
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const success = blk: {
|
||||
const content = self.source.content();
|
||||
switch (self.kind) {
|
||||
.javascript => _ = js_context.eval(content, url) catch break :blk false,
|
||||
.module => {
|
||||
// We don't care about waiting for the evaluation here.
|
||||
_ = js_context.module(content, url, cacheable) catch break :blk false;
|
||||
},
|
||||
}
|
||||
break :blk true;
|
||||
};
|
||||
|
||||
if (success) {
|
||||
self.executeCallback("onload", page);
|
||||
return;
|
||||
}
|
||||
|
||||
if (page.delayed_navigation) {
|
||||
// If we're navigating to another page, an error is expected
|
||||
// since we probably terminated the script forcefully.
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = try_catch.err(page.arena) catch |err| @errorName(err) orelse "unknown";
|
||||
log.warn(.user_script, "eval script", .{
|
||||
.url = url,
|
||||
.err = msg,
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
|
||||
self.executeCallback("onerror", page);
|
||||
}
|
||||
|
||||
fn executeCallback(self: *const Script, comptime typ: []const u8, page: *Page) void {
|
||||
const callback = self.getCallback(typ, page) orelse return;
|
||||
|
||||
switch (callback) {
|
||||
.string => |str| {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(page.main_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
_ = page.main_context.exec(str, typ) catch |err| {
|
||||
const msg = try_catch.err(page.arena) catch @errorName(err) orelse "unknown";
|
||||
log.warn(.user_script, "script callback", .{
|
||||
.url = self.url,
|
||||
.err = msg,
|
||||
.type = typ,
|
||||
.@"inline" = true,
|
||||
});
|
||||
};
|
||||
},
|
||||
.function => |f| {
|
||||
const Event = @import("events/event.zig").Event;
|
||||
const loadevt = parser.eventCreate() catch |err| {
|
||||
log.err(.browser, "SM event creation", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
defer parser.eventDestroy(loadevt);
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
const iface = Event.toInterface(loadevt) catch |err| {
|
||||
log.err(.browser, "SM event interface", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
f.tryCall(void, .{iface}, &result) catch {
|
||||
log.warn(.user_script, "script callback", .{
|
||||
.url = self.url,
|
||||
.type = typ,
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.@"inline" = false,
|
||||
});
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn getCallback(self: *const Script, comptime typ: []const u8, page: *Page) ?Callback {
|
||||
const element = self.element;
|
||||
// first we check if there was an el.onload set directly on the
|
||||
// element in JavaScript (if so, it'd be stored in the node state)
|
||||
if (page.getNodeState(@ptrCast(element))) |se| {
|
||||
if (@field(se, typ)) |function| {
|
||||
return .{ .function = function };
|
||||
}
|
||||
}
|
||||
// if we have no node state, or if the node state has no onload/onerror
|
||||
// then check for the onload/onerror attribute
|
||||
if (parser.elementGetAttribute(element, typ) catch null) |string| {
|
||||
return .{ .string = string };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const BufferPool = struct {
|
||||
count: usize,
|
||||
available: List = .{},
|
||||
allocator: Allocator,
|
||||
max_concurrent_transfers: u8,
|
||||
mem_pool: std.heap.MemoryPool(Container),
|
||||
|
||||
const List = std.DoublyLinkedList;
|
||||
|
||||
const Container = struct {
|
||||
node: List.Node,
|
||||
buf: std.ArrayListUnmanaged(u8),
|
||||
};
|
||||
|
||||
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
|
||||
return .{
|
||||
.available = .{},
|
||||
.count = 0,
|
||||
.allocator = allocator,
|
||||
.max_concurrent_transfers = max_concurrent_transfers,
|
||||
.mem_pool = std.heap.MemoryPool(Container).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *BufferPool) void {
|
||||
const allocator = self.allocator;
|
||||
|
||||
var node = self.available.first;
|
||||
while (node) |n| {
|
||||
const container: *Container = @fieldParentPtr("node", n);
|
||||
container.buf.deinit(allocator);
|
||||
node = n.next;
|
||||
}
|
||||
self.mem_pool.deinit();
|
||||
}
|
||||
|
||||
fn get(self: *BufferPool) std.ArrayListUnmanaged(u8) {
|
||||
const node = self.available.popFirst() orelse {
|
||||
// return a new buffer
|
||||
return .{};
|
||||
};
|
||||
|
||||
self.count -= 1;
|
||||
const container: *Container = @fieldParentPtr("node", node);
|
||||
defer self.mem_pool.destroy(container);
|
||||
return container.buf;
|
||||
}
|
||||
|
||||
fn release(self: *BufferPool, buffer: ArrayListUnmanaged(u8)) void {
|
||||
// create mutable copy
|
||||
var b = buffer;
|
||||
|
||||
if (self.count == self.max_concurrent_transfers) {
|
||||
b.deinit(self.allocator);
|
||||
return;
|
||||
}
|
||||
|
||||
const container = self.mem_pool.create() catch |err| {
|
||||
b.deinit(self.allocator);
|
||||
log.err(.http, "SM BufferPool release", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
|
||||
b.clearRetainingCapacity();
|
||||
container.* = .{ .buf = b, .node = .{} };
|
||||
self.count += 1;
|
||||
self.available.append(&container.node);
|
||||
}
|
||||
};
|
||||
|
||||
const Blocking = struct {
|
||||
allocator: Allocator,
|
||||
buffer_pool: *BufferPool,
|
||||
state: State = .{ .running = {} },
|
||||
buffer: std.ArrayListUnmanaged(u8) = .{},
|
||||
|
||||
const State = union(enum) {
|
||||
running: void,
|
||||
err: anyerror,
|
||||
done: BlockingResult,
|
||||
};
|
||||
|
||||
fn startCallback(transfer: *Http.Transfer) !void {
|
||||
log.debug(.http, "script fetch start", .{ .req = transfer, .blocking = true });
|
||||
}
|
||||
|
||||
fn headerCallback(transfer: *Http.Transfer) !void {
|
||||
const header = &transfer.response_header.?;
|
||||
log.debug(.http, "script header", .{
|
||||
.req = transfer,
|
||||
.blocking = true,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
|
||||
if (header.status != 200) {
|
||||
return error.InvalidStatusCode;
|
||||
}
|
||||
|
||||
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
|
||||
self.buffer = self.buffer_pool.get();
|
||||
}
|
||||
|
||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
// too verbose
|
||||
// log.debug(.http, "script data chunk", .{
|
||||
// .req = transfer,
|
||||
// .blocking = true,
|
||||
// });
|
||||
|
||||
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
|
||||
self.buffer.appendSlice(self.allocator, data) catch |err| {
|
||||
log.err(.http, "SM.dataCallback", .{
|
||||
.err = err,
|
||||
.len = data.len,
|
||||
.blocking = true,
|
||||
.transfer = transfer,
|
||||
});
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn doneCallback(ctx: *anyopaque) !void {
|
||||
var self: *Blocking = @ptrCast(@alignCast(ctx));
|
||||
self.state = .{ .done = .{
|
||||
.buffer = self.buffer,
|
||||
.buffer_pool = self.buffer_pool,
|
||||
} };
|
||||
}
|
||||
|
||||
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
var self: *Blocking = @ptrCast(@alignCast(ctx));
|
||||
self.state = .{ .err = err };
|
||||
self.buffer_pool.release(self.buffer);
|
||||
}
|
||||
};
|
||||
|
||||
pub const BlockingResult = struct {
|
||||
buffer: std.ArrayListUnmanaged(u8),
|
||||
buffer_pool: *BufferPool,
|
||||
|
||||
pub fn deinit(self: *BlockingResult) void {
|
||||
self.buffer_pool.release(self.buffer);
|
||||
}
|
||||
|
||||
pub fn src(self: *const BlockingResult) []const u8 {
|
||||
return self.buffer.items;
|
||||
}
|
||||
};
|
||||
78
src/browser/State.zig
Normal file
78
src/browser/State.zig
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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");
|
||||
const CSSStyleSheet = @import("cssom/CSSStyleSheet.zig");
|
||||
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");
|
||||
|
||||
// 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,
|
||||
adopted_style_sheets: ?Env.JsObject = 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,677 +17,100 @@
|
||||
// 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").Mime;
|
||||
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 Module = jsruntime.Module;
|
||||
|
||||
const apiweb = @import("../apiweb.zig");
|
||||
|
||||
const Window = @import("../html/window.zig").Window;
|
||||
const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
|
||||
|
||||
const URL = @import("../url/url.zig").URL;
|
||||
const Location = @import("../html/location.zig").Location;
|
||||
|
||||
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);
|
||||
|
||||
pub const user_agent = "Lightpanda/1.0";
|
||||
const log = @import("../log.zig");
|
||||
const HttpClient = @import("../http/Client.zig");
|
||||
|
||||
// Browser is an instance of the browser.
|
||||
// You can create multiple browser instances.
|
||||
// A browser contains only one session.
|
||||
// TODO allow multiple sessions per browser.
|
||||
pub const Browser = struct {
|
||||
session: Session = undefined,
|
||||
agent: []const u8 = user_agent,
|
||||
env: *Env,
|
||||
app: *App,
|
||||
session: ?Session,
|
||||
allocator: Allocator,
|
||||
http_client: *HttpClient,
|
||||
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);
|
||||
app.http.client.notification = notification;
|
||||
app.http.client.next_request_id = 0; // Should we track ids in CDP only?
|
||||
errdefer notification.deinit();
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.env = env,
|
||||
.session = null,
|
||||
.allocator = allocator,
|
||||
.notification = notification,
|
||||
.http_client = app.http.client,
|
||||
.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);
|
||||
}
|
||||
|
||||
pub fn currentPage(self: *Browser) ?*Page {
|
||||
if (self.session.page == null) return null;
|
||||
|
||||
return &self.session.page.?;
|
||||
}
|
||||
};
|
||||
|
||||
// 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, .{ .agent = user_agent }),
|
||||
.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 fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module {
|
||||
_ = referrer;
|
||||
|
||||
const self: *Session = @ptrCast(@alignCast(ctx));
|
||||
|
||||
if (self.page == null) return error.NoPage;
|
||||
|
||||
log.debug("fetch module: specifier: {s}", .{specifier});
|
||||
const alloc = self.arena.allocator();
|
||||
const body = try self.page.?.fetchData(alloc, specifier);
|
||||
defer alloc.free(body);
|
||||
|
||||
return self.env.compileModule(body, specifier);
|
||||
}
|
||||
|
||||
fn deinit(self: *Session) void {
|
||||
if (self.page) |*p| p.deinit();
|
||||
|
||||
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.http_client.notification = null;
|
||||
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,
|
||||
|
||||
// html url and location
|
||||
url: ?URL = null,
|
||||
location: Location = .{},
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
// start js env.
|
||||
// - auxData: extra data forwarded to the Inspector
|
||||
// see Inspector.contextCreated
|
||||
pub fn start(self: *Page, auxData: ?[]const u8) !void {
|
||||
// start JS env
|
||||
log.debug("start js env", .{});
|
||||
try self.session.env.start();
|
||||
|
||||
// register the module loader
|
||||
try self.session.env.setModuleLoadFn(self.session, Session.fetchModule);
|
||||
|
||||
// add global objects
|
||||
log.debug("setup global env", .{});
|
||||
try self.session.env.bindGlobal(&self.session.window);
|
||||
|
||||
// load polyfills
|
||||
try polyfill.load(self.arena.allocator(), self.session.env);
|
||||
|
||||
// inspector
|
||||
if (self.session.inspector) |inspector| {
|
||||
log.debug("inspector context created", .{});
|
||||
inspector.contextCreated(self.session.env, "", self.origin orelse "://", auxData);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
if (self.url) |*u| u.deinit(self.arena.allocator());
|
||||
self.url = null;
|
||||
self.location.url = null;
|
||||
self.session.window.replaceLocation(&self.location) catch |e| {
|
||||
log.err("reset window location: {any}", .{e});
|
||||
};
|
||||
self.doc = null;
|
||||
|
||||
// clear netsurf memory arena.
|
||||
parser.deinit();
|
||||
|
||||
_ = self.arena.reset(.free_all);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Page) void {
|
||||
self.end();
|
||||
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});
|
||||
|
||||
// if the uri is about:blank, nothing to do.
|
||||
if (std.mem.eql(u8, "about:blank", uri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.?);
|
||||
|
||||
if (self.url) |*prev| prev.deinit(alloc);
|
||||
self.url = try URL.constructor(alloc, self.rawuri.?, null);
|
||||
self.location.url = &self.url.?;
|
||||
try self.session.window.replaceLocation(&self.location);
|
||||
|
||||
// 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, @intFromEnum(req.response.status) });
|
||||
|
||||
// TODO handle redirection
|
||||
log.debug("{?} {d} {s}", .{
|
||||
req.response.version,
|
||||
@intFromEnum(req.response.status),
|
||||
req.response.reason,
|
||||
// TODO log headers
|
||||
});
|
||||
|
||||
// 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.?});
|
||||
var mime = try Mime.parse(alloc, ct.?);
|
||||
defer mime.deinit();
|
||||
|
||||
if (mime.isHTML()) {
|
||||
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.
|
||||
|
||||
try 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
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
// 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(Script).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.?);
|
||||
|
||||
// ignore non-js script.
|
||||
const script = try Script.init(e) orelse continue;
|
||||
if (script.kind == .unknown) 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 (script.isasync) {
|
||||
try sasync.append(script);
|
||||
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(script) 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) |s| {
|
||||
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
|
||||
self.evalScript(s) 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, s: Script) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
||||
const opt_src = try parser.elementGetAttribute(s.element, "src");
|
||||
if (opt_src) |src| {
|
||||
log.debug("starting GET {s}", .{src});
|
||||
|
||||
self.fetchScript(s) 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;
|
||||
}
|
||||
|
||||
// TODO handle charset attribute
|
||||
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
|
||||
if (opt_text) |text| {
|
||||
try s.eval(alloc, self.session.env, text);
|
||||
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,
|
||||
};
|
||||
|
||||
// the caller owns the returned string
|
||||
fn fetchData(self: *Page, alloc: std.mem.Allocator, src: []const u8) ![]const u8 {
|
||||
log.debug("starting fetch {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 {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);
|
||||
|
||||
// check no body
|
||||
if (body.len == 0) return FetchError.NoBody;
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
// fetchScript senf a GET request to the src and execute the script
|
||||
// received.
|
||||
fn fetchScript(self: *Page, s: Script) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
const body = try self.fetchData(alloc, s.src);
|
||||
defer alloc.free(body);
|
||||
|
||||
try s.eval(alloc, self.session.env, body);
|
||||
}
|
||||
|
||||
const Script = struct {
|
||||
element: *parser.Element,
|
||||
kind: Kind,
|
||||
isasync: bool,
|
||||
|
||||
src: []const u8,
|
||||
|
||||
const Kind = enum {
|
||||
unknown,
|
||||
javascript,
|
||||
module,
|
||||
};
|
||||
|
||||
fn init(e: *parser.Element) !?Script {
|
||||
// ignore non-script tags
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
|
||||
if (tag != .script) return null;
|
||||
|
||||
return .{
|
||||
.element = e,
|
||||
.kind = kind(try parser.elementGetAttribute(e, "type")),
|
||||
.isasync = try parser.elementGetAttribute(e, "async") != null,
|
||||
|
||||
.src = try parser.elementGetAttribute(e, "src") orelse "inline",
|
||||
};
|
||||
}
|
||||
|
||||
// > 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 kind(stype: ?[]const u8) Kind {
|
||||
if (stype == null or stype.?.len == 0) return .javascript;
|
||||
if (std.mem.eql(u8, stype.?, "application/javascript")) return .javascript;
|
||||
if (std.mem.eql(u8, stype.?, "module")) return .module;
|
||||
|
||||
return .unknown;
|
||||
}
|
||||
|
||||
fn eval(self: Script, alloc: std.mem.Allocator, env: Env, body: []const u8) !void {
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
try_catch.init(env);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const res = switch (self.kind) {
|
||||
.unknown => return error.UnknownScript,
|
||||
.javascript => env.exec(body, self.src),
|
||||
.module => env.module(body, self.src),
|
||||
} catch {
|
||||
if (try try_catch.err(alloc, env)) |msg| {
|
||||
defer alloc.free(msg);
|
||||
log.info("eval script {s}: {s}", .{ self.src, msg });
|
||||
}
|
||||
return FetchError.JsErr;
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(alloc, env);
|
||||
defer alloc.free(msg);
|
||||
log.debug("eval script {s}: {s}", .{ self.src, msg });
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
const testing = @import("../testing.zig");
|
||||
test "Browser" {
|
||||
try testing.htmlRunner("browser.html");
|
||||
}
|
||||
|
||||
239
src/browser/console/console.zig
Normal file
239
src/browser/console/console.zig
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const JsObject = @import("../env.zig").Env.JsObject;
|
||||
|
||||
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.warn(.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 {
|
||||
return @import("../../datetime.zig").timestamp();
|
||||
}
|
||||
|
||||
// const testing = @import("../../testing.zig");
|
||||
// test "Browser.Console" {
|
||||
// defer testing.reset();
|
||||
|
||||
// var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
// defer runner.deinit();
|
||||
|
||||
// {
|
||||
// try runner.testCases(&.{
|
||||
// .{ "console.log('a')", "undefined" },
|
||||
// .{ "console.warn('hello world', 23, true, new Object())", "undefined" },
|
||||
// }, .{});
|
||||
|
||||
// const captured = test_capture.captured.items;
|
||||
// try testing.expectEqual("[info] args= 1: a", captured[0]);
|
||||
// try testing.expectEqual("[warn] args= 1: hello world 2: 23 3: true 4: #<Object>", captured[1]);
|
||||
// }
|
||||
|
||||
// {
|
||||
// test_capture.reset();
|
||||
// try runner.testCases(&.{
|
||||
// .{ "console.countReset()", "undefined" },
|
||||
// .{ "console.count()", "undefined" },
|
||||
// .{ "console.count('teg')", "undefined" },
|
||||
// .{ "console.count('teg')", "undefined" },
|
||||
// .{ "console.count('teg')", "undefined" },
|
||||
// .{ "console.count()", "undefined" },
|
||||
// .{ "console.countReset('teg')", "undefined" },
|
||||
// .{ "console.countReset()", "undefined" },
|
||||
// .{ "console.count()", "undefined" },
|
||||
// }, .{});
|
||||
|
||||
// const captured = test_capture.captured.items;
|
||||
// try testing.expectEqual("[invalid counter] label=default", captured[0]);
|
||||
// try testing.expectEqual("[count] label=default count=1", captured[1]);
|
||||
// try testing.expectEqual("[count] label=teg count=1", captured[2]);
|
||||
// try testing.expectEqual("[count] label=teg count=2", captured[3]);
|
||||
// try testing.expectEqual("[count] label=teg count=3", captured[4]);
|
||||
// try testing.expectEqual("[count] label=default count=2", captured[5]);
|
||||
// try testing.expectEqual("[count reset] label=teg count=3", captured[6]);
|
||||
// try testing.expectEqual("[count reset] label=default count=2", captured[7]);
|
||||
// try testing.expectEqual("[count] label=default count=1", captured[8]);
|
||||
// }
|
||||
|
||||
// {
|
||||
// test_capture.reset();
|
||||
// try runner.testCases(&.{
|
||||
// .{ "console.assert(true)", "undefined" },
|
||||
// .{ "console.assert('a', 2, 3, 4)", "undefined" },
|
||||
// .{ "console.assert('')", "undefined" },
|
||||
// .{ "console.assert('', 'x', true)", "undefined" },
|
||||
// .{ "console.assert(false, 'x')", "undefined" },
|
||||
// }, .{});
|
||||
|
||||
// const captured = test_capture.captured.items;
|
||||
// try testing.expectEqual("[assertion failed] values=", captured[0]);
|
||||
// try testing.expectEqual("[assertion failed] values= 1: x 2: true", captured[1]);
|
||||
// try testing.expectEqual("[assertion failed] values= 1: x", captured[2]);
|
||||
// }
|
||||
|
||||
// {
|
||||
// test_capture.reset();
|
||||
// try runner.testCases(&.{
|
||||
// .{ "[1].forEach(console.log)", null },
|
||||
// }, .{});
|
||||
|
||||
// const captured = test_capture.captured.items;
|
||||
// try testing.expectEqual("[info] args= 1: 1 2: 0 3: [1]", captured[0]);
|
||||
// }
|
||||
// }
|
||||
71
src/browser/crypto/crypto.zig
Normal file
71
src/browser/crypto/crypto.zig
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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" {
|
||||
try testing.htmlRunner("crypto.html");
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -31,7 +45,7 @@ pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions)
|
||||
|
||||
// matchFirst call m.match with the first node that matches the selector s, from the
|
||||
// descendants of n and returns true. If none matches, it returns false.
|
||||
pub fn matchFirst(s: Selector, node: anytype, m: anytype) !bool {
|
||||
pub fn matchFirst(s: *const Selector, node: anytype, m: anytype) !bool {
|
||||
var c = try node.firstChild();
|
||||
while (true) {
|
||||
if (c == null) break;
|
||||
@@ -49,7 +63,7 @@ pub fn matchFirst(s: Selector, node: anytype, m: anytype) !bool {
|
||||
|
||||
// matchAll call m.match with the all the nodes that matches the selector s, from the
|
||||
// descendants of n.
|
||||
pub fn matchAll(s: Selector, node: anytype, m: anytype) !void {
|
||||
pub fn matchAll(s: *const Selector, node: anytype, m: anytype) !void {
|
||||
var c = try node.firstChild();
|
||||
while (true) {
|
||||
if (c == null) break;
|
||||
@@ -174,3 +188,8 @@ test "parse" {
|
||||
defer s.deinit(alloc);
|
||||
}
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS" {
|
||||
try testing.htmlRunner("css.html");
|
||||
}
|
||||
@@ -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,19 +19,24 @@
|
||||
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 Allocator = std.mem.Allocator;
|
||||
|
||||
const Matcher = struct {
|
||||
const Nodes = std.ArrayList(Node);
|
||||
const Nodes = std.ArrayListUnmanaged(Node);
|
||||
|
||||
nodes: Nodes,
|
||||
allocator: Allocator,
|
||||
|
||||
fn init(alloc: std.mem.Allocator) Matcher {
|
||||
return .{ .nodes = Nodes.init(alloc) };
|
||||
fn init(allocator: Allocator) Matcher {
|
||||
return .{
|
||||
.nodes = .empty,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(m: *Matcher) void {
|
||||
m.nodes.deinit();
|
||||
m.nodes.deinit(m.allocator);
|
||||
}
|
||||
|
||||
fn reset(m: *Matcher) void {
|
||||
@@ -39,7 +44,7 @@ const Matcher = struct {
|
||||
}
|
||||
|
||||
pub fn match(m: *Matcher, n: Node) !void {
|
||||
try m.nodes.append(n);
|
||||
try m.nodes.append(m.allocator, n);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -84,16 +84,20 @@ pub const Node = struct {
|
||||
};
|
||||
|
||||
const Matcher = struct {
|
||||
const Nodes = std.ArrayList(*const Node);
|
||||
const Nodes = std.ArrayListUnmanaged(*const Node);
|
||||
|
||||
nodes: Nodes,
|
||||
allocator: Allocator,
|
||||
|
||||
fn init(alloc: std.mem.Allocator) Matcher {
|
||||
return .{ .nodes = Nodes.init(alloc) };
|
||||
fn init(allocator: Allocator) Matcher {
|
||||
return .{
|
||||
.nodes = .empty,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(m: *Matcher) void {
|
||||
m.nodes.deinit();
|
||||
m.nodes.deinit(self.allocator);
|
||||
}
|
||||
|
||||
fn reset(m: *Matcher) void {
|
||||
@@ -101,7 +105,7 @@ const Matcher = struct {
|
||||
}
|
||||
|
||||
pub fn match(m: *Matcher, n: *const Node) !void {
|
||||
try m.nodes.append(n);
|
||||
try m.nodes.append(self.allocator, n);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
// see https://github.com/andybalholm/cascadia/blob/master/parser.go
|
||||
const std = @import("std");
|
||||
const ascii = std.ascii;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const selector = @import("selector.zig");
|
||||
const Selector = selector.Selector;
|
||||
@@ -29,6 +30,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,
|
||||
@@ -75,8 +78,8 @@ pub const Parser = struct {
|
||||
|
||||
opts: ParseOptions,
|
||||
|
||||
pub fn parse(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
return p.parseSelectorGroup(alloc);
|
||||
pub fn parse(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
return p.parseSelectorGroup(allocator);
|
||||
}
|
||||
|
||||
// skipWhitespace consumes whitespace characters and comments.
|
||||
@@ -113,13 +116,13 @@ pub const Parser = struct {
|
||||
|
||||
// parseSimpleSelectorSequence parses a selector sequence that applies to
|
||||
// a single element.
|
||||
fn parseSimpleSelectorSequence(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
fn parseSimpleSelectorSequence(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) {
|
||||
return ParseError.ExpectedSelector;
|
||||
}
|
||||
|
||||
var buf = std.ArrayList(Selector).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(Selector) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
switch (p.s[p.i]) {
|
||||
'*' => {
|
||||
@@ -136,20 +139,20 @@ pub const Parser = struct {
|
||||
// There's no type selector. Wait to process the other till the
|
||||
// main loop.
|
||||
},
|
||||
else => try buf.append(try p.parseTypeSelector(alloc)),
|
||||
else => try buf.append(allocator, try p.parseTypeSelector(allocator)),
|
||||
}
|
||||
|
||||
var pseudo_elt: ?PseudoClass = null;
|
||||
|
||||
loop: while (p.i < p.s.len) {
|
||||
var ns: Selector = switch (p.s[p.i]) {
|
||||
'#' => try p.parseIDSelector(alloc),
|
||||
'.' => try p.parseClassSelector(alloc),
|
||||
'[' => try p.parseAttributeSelector(alloc),
|
||||
':' => try p.parsePseudoclassSelector(alloc),
|
||||
'#' => try p.parseIDSelector(allocator),
|
||||
'.' => try p.parseClassSelector(allocator),
|
||||
'[' => try p.parseAttributeSelector(allocator),
|
||||
':' => try p.parsePseudoclassSelector(allocator),
|
||||
else => break :loop,
|
||||
};
|
||||
errdefer ns.deinit(alloc);
|
||||
errdefer ns.deinit(allocator);
|
||||
|
||||
// From https://drafts.csswg.org/selectors-3/#pseudo-elements :
|
||||
// "Only one pseudo-element may appear per selector, and if present
|
||||
@@ -163,28 +166,32 @@ pub const Parser = struct {
|
||||
if (!p.opts.accept_pseudo_elts) return ParseError.PseudoElementDisabled;
|
||||
|
||||
pseudo_elt = e;
|
||||
ns.deinit(alloc);
|
||||
ns.deinit(allocator);
|
||||
},
|
||||
else => {
|
||||
if (pseudo_elt != null) return ParseError.PseudoElementNotAtSelectorEnd;
|
||||
try buf.append(ns);
|
||||
try buf.append(allocator, ns);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// no need wrap the selectors in compoundSelector
|
||||
if (buf.items.len == 1 and pseudo_elt == null) return buf.items[0];
|
||||
if (buf.items.len == 1 and pseudo_elt == null) {
|
||||
return buf.items[0];
|
||||
}
|
||||
|
||||
return .{ .compound = .{ .selectors = try buf.toOwnedSlice(), .pseudo_elt = pseudo_elt } };
|
||||
return .{
|
||||
.compound = .{ .selectors = try buf.toOwnedSlice(allocator), .pseudo_elt = pseudo_elt },
|
||||
};
|
||||
}
|
||||
|
||||
// parseTypeSelector parses a type selector (one that matches by tag name).
|
||||
fn parseTypeSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
try p.parseIdentifier(buf.writer());
|
||||
fn parseTypeSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
|
||||
return .{ .tag = try buf.toOwnedSlice() };
|
||||
return .{ .tag = try buf.toOwnedSlice(allocator) };
|
||||
}
|
||||
|
||||
// parseIdentifier parses an identifier.
|
||||
@@ -202,7 +209,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 +224,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 +262,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,51 +315,51 @@ 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.
|
||||
fn parseIDSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
fn parseIDSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedIDSelector;
|
||||
if (p.s[p.i] != '#') return ParseError.ExpectedIDSelector;
|
||||
|
||||
p.i += 1;
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
try p.parseName(buf.writer());
|
||||
return .{ .id = try buf.toOwnedSlice() };
|
||||
try p.parseName(buf.writer(allocator));
|
||||
return .{ .id = try buf.toOwnedSlice(allocator) };
|
||||
}
|
||||
|
||||
// parseClassSelector parses a selector that matches by class attribute.
|
||||
fn parseClassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
fn parseClassSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedClassSelector;
|
||||
if (p.s[p.i] != '.') return ParseError.ExpectedClassSelector;
|
||||
|
||||
p.i += 1;
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
try p.parseIdentifier(buf.writer());
|
||||
return .{ .class = try buf.toOwnedSlice() };
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
return .{ .class = try buf.toOwnedSlice(allocator) };
|
||||
}
|
||||
|
||||
// parseAttributeSelector parses a selector that matches by attribute value.
|
||||
fn parseAttributeSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
fn parseAttributeSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
||||
if (p.s[p.i] != '[') return ParseError.ExpectedAttributeSelector;
|
||||
|
||||
p.i += 1;
|
||||
_ = p.skipWhitespace();
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
try p.parseIdentifier(buf.writer());
|
||||
const key = try buf.toOwnedSlice();
|
||||
errdefer alloc.free(key);
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
const key = try buf.toOwnedSlice(allocator);
|
||||
errdefer allocator.free(key);
|
||||
|
||||
lowerstr(key);
|
||||
|
||||
@@ -347,12 +382,12 @@ pub const Parser = struct {
|
||||
var is_val: bool = undefined;
|
||||
if (op == .regexp) {
|
||||
is_val = false;
|
||||
try p.parseRegex(buf.writer());
|
||||
try p.parseRegex(buf.writer(allocator));
|
||||
} else {
|
||||
is_val = true;
|
||||
switch (p.s[p.i]) {
|
||||
'\'', '"' => try p.parseString(buf.writer()),
|
||||
else => try p.parseIdentifier(buf.writer()),
|
||||
'\'', '"' => try p.parseString(buf.writer(allocator)),
|
||||
else => try p.parseIdentifier(buf.writer(allocator)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,8 +409,8 @@ pub const Parser = struct {
|
||||
|
||||
return .{ .attribute = .{
|
||||
.key = key,
|
||||
.val = if (is_val) try buf.toOwnedSlice() else null,
|
||||
.regexp = if (!is_val) try buf.toOwnedSlice() else null,
|
||||
.val = if (is_val) try buf.toOwnedSlice(allocator) else null,
|
||||
.regexp = if (!is_val) try buf.toOwnedSlice(allocator) else null,
|
||||
.op = op,
|
||||
.ci = ci,
|
||||
} };
|
||||
@@ -383,20 +418,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 +456,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;
|
||||
@@ -465,7 +503,7 @@ pub const Parser = struct {
|
||||
// parsePseudoclassSelector parses a pseudoclass selector like :not(p) or a pseudo-element
|
||||
// For backwards compatibility, both ':' and '::' prefix are allowed for pseudo-elements.
|
||||
// https://drafts.csswg.org/selectors-3/#pseudo-elements
|
||||
fn parsePseudoclassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
fn parsePseudoclassSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedPseudoClassSelector;
|
||||
if (p.s[p.i] != ':') return ParseError.ExpectedPseudoClassSelector;
|
||||
|
||||
@@ -478,10 +516,10 @@ pub const Parser = struct {
|
||||
p.i += 1;
|
||||
}
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
try p.parseIdentifier(buf.writer());
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
|
||||
const pseudo_class = try PseudoClass.parse(buf.items);
|
||||
|
||||
@@ -494,11 +532,11 @@ pub const Parser = struct {
|
||||
.not, .has, .haschild => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
|
||||
const sel = try p.parseSelectorGroup(alloc);
|
||||
const sel = try p.parseSelectorGroup(allocator);
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const s = try alloc.create(Selector);
|
||||
errdefer alloc.destroy(s);
|
||||
const s = try allocator.create(Selector);
|
||||
errdefer allocator.destroy(s);
|
||||
s.* = sel;
|
||||
|
||||
return .{ .pseudo_class_relative = .{ .pseudo_class = pseudo_class, .match = s } };
|
||||
@@ -508,16 +546,16 @@ pub const Parser = struct {
|
||||
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
|
||||
|
||||
switch (p.s[p.i]) {
|
||||
'\'', '"' => try p.parseString(buf.writer()),
|
||||
else => try p.parseString(buf.writer()),
|
||||
'\'', '"' => try p.parseString(buf.writer(allocator)),
|
||||
else => try p.parseString(buf.writer(allocator)),
|
||||
}
|
||||
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const val = try buf.toOwnedSlice();
|
||||
errdefer alloc.free(val);
|
||||
const val = try buf.toOwnedSlice(allocator);
|
||||
errdefer allocator.free(val);
|
||||
|
||||
lowerstr(val);
|
||||
|
||||
@@ -526,15 +564,15 @@ pub const Parser = struct {
|
||||
.matches, .matchesown => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
|
||||
try p.parseRegex(buf.writer());
|
||||
try p.parseRegex(buf.writer(allocator));
|
||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClassSelector;
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
return .{ .pseudo_class_regexp = .{ .own = pseudo_class == .matchesown, .regexp = try buf.toOwnedSlice() } };
|
||||
return .{ .pseudo_class_regexp = .{ .own = pseudo_class == .matchesown, .regexp = try buf.toOwnedSlice(allocator) } };
|
||||
},
|
||||
.nth_child, .nth_last_child, .nth_of_type, .nth_last_of_type => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
const nth = try p.parseNth(alloc);
|
||||
const nth = try p.parseNth(allocator);
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const last = pseudo_class == .nth_last_child or pseudo_class == .nth_last_of_type;
|
||||
@@ -549,18 +587,19 @@ 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;
|
||||
|
||||
try p.parseIdentifier(buf.writer());
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const val = try buf.toOwnedSlice();
|
||||
errdefer alloc.free(val);
|
||||
const val = try buf.toOwnedSlice(allocator);
|
||||
errdefer allocator.free(val);
|
||||
lowerstr(val);
|
||||
|
||||
return .{ .pseudo_class_lang = val };
|
||||
@@ -572,6 +611,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 },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,30 +627,32 @@ pub const Parser = struct {
|
||||
}
|
||||
|
||||
// parseSelectorGroup parses a group of selectors, separated by commas.
|
||||
fn parseSelectorGroup(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
const s = try p.parseSelector(alloc);
|
||||
fn parseSelectorGroup(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
const s = try p.parseSelector(allocator);
|
||||
|
||||
var buf = std.ArrayList(Selector).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(Selector) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
try buf.append(s);
|
||||
try buf.append(allocator, s);
|
||||
|
||||
while (p.i < p.s.len) {
|
||||
if (p.s[p.i] != ',') break;
|
||||
p.i += 1;
|
||||
const ss = try p.parseSelector(alloc);
|
||||
try buf.append(ss);
|
||||
const ss = try p.parseSelector(allocator);
|
||||
try buf.append(allocator, ss);
|
||||
}
|
||||
|
||||
if (buf.items.len == 1) return buf.items[0];
|
||||
if (buf.items.len == 1) {
|
||||
return buf.items[0];
|
||||
}
|
||||
|
||||
return .{ .group = try buf.toOwnedSlice() };
|
||||
return .{ .group = try buf.toOwnedSlice(allocator) };
|
||||
}
|
||||
|
||||
// parseSelector parses a selector that may include combinators.
|
||||
fn parseSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
fn parseSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
_ = p.skipWhitespace();
|
||||
var s = try p.parseSimpleSelectorSequence(alloc);
|
||||
var s = try p.parseSimpleSelectorSequence(allocator);
|
||||
|
||||
while (true) {
|
||||
var combinator: Combinator = .empty;
|
||||
@@ -638,17 +680,21 @@ pub const Parser = struct {
|
||||
return s;
|
||||
}
|
||||
|
||||
const c = try p.parseSimpleSelectorSequence(alloc);
|
||||
const c = try p.parseSimpleSelectorSequence(allocator);
|
||||
|
||||
const first = try alloc.create(Selector);
|
||||
errdefer alloc.destroy(first);
|
||||
const first = try allocator.create(Selector);
|
||||
errdefer allocator.destroy(first);
|
||||
first.* = s;
|
||||
|
||||
const second = try alloc.create(Selector);
|
||||
errdefer alloc.destroy(second);
|
||||
const second = try allocator.create(Selector);
|
||||
errdefer allocator.destroy(second);
|
||||
second.* = c;
|
||||
|
||||
s = Selector{ .combined = .{ .first = first, .second = second, .combinator = combinator } };
|
||||
s = Selector{ .combined = .{
|
||||
.first = first,
|
||||
.second = second,
|
||||
.combinator = combinator,
|
||||
} };
|
||||
}
|
||||
|
||||
return s;
|
||||
@@ -741,7 +787,7 @@ pub const Parser = struct {
|
||||
}
|
||||
|
||||
// parseNth parses the argument for :nth-child (normally of the form an+b).
|
||||
fn parseNth(p: *Parser, alloc: std.mem.Allocator) ParseError![2]isize {
|
||||
fn parseNth(p: *Parser, allocator: Allocator) ParseError![2]isize {
|
||||
// initial state
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
||||
return switch (p.s[p.i]) {
|
||||
@@ -759,10 +805,10 @@ pub const Parser = struct {
|
||||
return p.parseNthReadN(1);
|
||||
},
|
||||
'o', 'O', 'e', 'E' => {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
try p.parseName(buf.writer());
|
||||
try p.parseName(buf.writer(allocator));
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("odd", buf.items)) return .{ 2, 1 };
|
||||
if (std.ascii.eqlIgnoreCase("even", buf.items)) return .{ 2, 0 };
|
||||
@@ -838,7 +884,7 @@ test "parser.skipWhitespace" {
|
||||
}
|
||||
|
||||
test "parser.parseIdentifier" {
|
||||
const alloc = std.testing.allocator;
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const testcases = [_]struct {
|
||||
s: []const u8, // given value
|
||||
@@ -854,14 +900,14 @@ test "parser.parseIdentifier" {
|
||||
.{ .s = "a\\\"b", .exp = "a\"b" },
|
||||
};
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
for (testcases) |tc| {
|
||||
buf.clearRetainingCapacity();
|
||||
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
p.parseIdentifier(buf.writer()) catch |e| {
|
||||
p.parseIdentifier(buf.writer(allocator)) catch |e| {
|
||||
// if error was expected, continue.
|
||||
if (tc.err) continue;
|
||||
|
||||
@@ -876,7 +922,7 @@ test "parser.parseIdentifier" {
|
||||
}
|
||||
|
||||
test "parser.parseString" {
|
||||
const alloc = std.testing.allocator;
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const testcases = [_]struct {
|
||||
s: []const u8, // given value
|
||||
@@ -895,14 +941,14 @@ test "parser.parseString" {
|
||||
.{ .s = "\"hello world\"", .exp = "hello world" },
|
||||
};
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
for (testcases) |tc| {
|
||||
buf.clearRetainingCapacity();
|
||||
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
p.parseString(buf.writer()) catch |e| {
|
||||
p.parseString(buf.writer(allocator)) catch |e| {
|
||||
// if error was expected, continue.
|
||||
if (tc.err) continue;
|
||||
|
||||
@@ -915,3 +961,36 @@ test "parser.parseString" {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "parser.parse" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const allocator = arena.allocator();
|
||||
|
||||
const testcases = [_]struct {
|
||||
s: []const u8, // given value
|
||||
exp: Selector, // expected value
|
||||
err: bool = false,
|
||||
}{
|
||||
.{ .s = "root", .exp = .{ .tag = "root" } },
|
||||
.{ .s = ".root", .exp = .{ .class = "root" } },
|
||||
.{ .s = ":root", .exp = .{ .pseudo_class = .root } },
|
||||
.{ .s = ".\\:bar", .exp = .{ .class = ":bar" } },
|
||||
.{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
const sel = p.parse(allocator) catch |e| {
|
||||
// if error was expected, continue.
|
||||
if (tc.err) continue;
|
||||
|
||||
std.debug.print("test case {s}\n", .{tc.s});
|
||||
return e;
|
||||
};
|
||||
std.testing.expectEqualDeep(tc.exp, sel) catch |e| {
|
||||
std.debug.print("test case {s} : {}\n", .{ tc.s, sel });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -246,8 +306,8 @@ pub const Selector = union(enum) {
|
||||
}
|
||||
|
||||
// match returns true if the node matches the selector query.
|
||||
pub fn match(s: Selector, n: anytype) !bool {
|
||||
return switch (s) {
|
||||
pub fn match(s: *const Selector, n: anytype) !bool {
|
||||
return switch (s.*) {
|
||||
.tag => |v| n.isElement() and std.ascii.eqlIgnoreCase(v, try n.tag()),
|
||||
.id => |v| return n.isElement() and std.mem.eql(u8, v, try n.attr("id") orelse return false),
|
||||
.class => |v| return n.isElement() and word(try n.attr("class") orelse return false, v, false),
|
||||
@@ -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.
|
||||
@@ -732,8 +794,8 @@ pub const Selector = union(enum) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn deinit(sel: Selector, alloc: std.mem.Allocator) void {
|
||||
switch (sel) {
|
||||
pub fn deinit(sel: *const Selector, alloc: std.mem.Allocator) void {
|
||||
switch (sel.*) {
|
||||
.group => |v| {
|
||||
for (v) |vv| vv.deinit(alloc);
|
||||
alloc.free(v);
|
||||
289
src/browser/cssom/CSSParser.zig
Normal file
289
src/browser/cssom/CSSParser.zig
Normal file
@@ -0,0 +1,289 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const CSSConstants = struct {
|
||||
const IMPORTANT = "!important";
|
||||
const URL_PREFIX = "url(";
|
||||
};
|
||||
|
||||
const CSSParserState = enum {
|
||||
seek_name,
|
||||
in_name,
|
||||
seek_colon,
|
||||
seek_value,
|
||||
in_value,
|
||||
in_quoted_value,
|
||||
in_single_quoted_value,
|
||||
in_url,
|
||||
in_important,
|
||||
};
|
||||
|
||||
const CSSDeclaration = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
is_important: bool,
|
||||
};
|
||||
|
||||
const CSSParser = @This();
|
||||
state: CSSParserState,
|
||||
name_start: usize,
|
||||
name_end: usize,
|
||||
value_start: usize,
|
||||
position: usize,
|
||||
paren_depth: usize,
|
||||
escape_next: bool,
|
||||
|
||||
pub fn init() CSSParser {
|
||||
return .{
|
||||
.state = .seek_name,
|
||||
.name_start = 0,
|
||||
.name_end = 0,
|
||||
.value_start = 0,
|
||||
.position = 0,
|
||||
.paren_depth = 0,
|
||||
.escape_next = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseDeclarations(arena: Allocator, text: []const u8) ![]CSSDeclaration {
|
||||
var parser = init();
|
||||
var declarations: std.ArrayListUnmanaged(CSSDeclaration) = .empty;
|
||||
|
||||
while (parser.position < text.len) {
|
||||
const c = text[parser.position];
|
||||
|
||||
switch (parser.state) {
|
||||
.seek_name => {
|
||||
if (!std.ascii.isWhitespace(c)) {
|
||||
parser.name_start = parser.position;
|
||||
parser.state = .in_name;
|
||||
continue;
|
||||
}
|
||||
},
|
||||
.in_name => {
|
||||
if (c == ':') {
|
||||
parser.name_end = parser.position;
|
||||
parser.state = .seek_value;
|
||||
} else if (std.ascii.isWhitespace(c)) {
|
||||
parser.name_end = parser.position;
|
||||
parser.state = .seek_colon;
|
||||
}
|
||||
},
|
||||
.seek_colon => {
|
||||
if (c == ':') {
|
||||
parser.state = .seek_value;
|
||||
} else if (!std.ascii.isWhitespace(c)) {
|
||||
parser.state = .seek_name;
|
||||
continue;
|
||||
}
|
||||
},
|
||||
.seek_value => {
|
||||
if (!std.ascii.isWhitespace(c)) {
|
||||
parser.value_start = parser.position;
|
||||
if (c == '"') {
|
||||
parser.state = .in_quoted_value;
|
||||
} else if (c == '\'') {
|
||||
parser.state = .in_single_quoted_value;
|
||||
} else if (c == 'u' and parser.position + CSSConstants.URL_PREFIX.len <= text.len and std.mem.startsWith(u8, text[parser.position..], CSSConstants.URL_PREFIX)) {
|
||||
parser.state = .in_url;
|
||||
parser.paren_depth = 1;
|
||||
parser.position += 3;
|
||||
} else {
|
||||
parser.state = .in_value;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
.in_value => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '(') {
|
||||
parser.paren_depth += 1;
|
||||
} else if (c == ')' and parser.paren_depth > 0) {
|
||||
parser.paren_depth -= 1;
|
||||
} else if (c == ';' and parser.paren_depth == 0) {
|
||||
try parser.finishDeclaration(arena, &declarations, text);
|
||||
parser.state = .seek_name;
|
||||
}
|
||||
},
|
||||
.in_quoted_value => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '"') {
|
||||
parser.state = .in_value;
|
||||
}
|
||||
},
|
||||
.in_single_quoted_value => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '\'') {
|
||||
parser.state = .in_value;
|
||||
}
|
||||
},
|
||||
.in_url => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '(') {
|
||||
parser.paren_depth += 1;
|
||||
} else if (c == ')') {
|
||||
parser.paren_depth -= 1;
|
||||
if (parser.paren_depth == 0) {
|
||||
parser.state = .in_value;
|
||||
}
|
||||
}
|
||||
},
|
||||
.in_important => {},
|
||||
}
|
||||
|
||||
parser.position += 1;
|
||||
}
|
||||
|
||||
try parser.finalize(arena, &declarations, text);
|
||||
|
||||
return declarations.items;
|
||||
}
|
||||
|
||||
fn finishDeclaration(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
|
||||
const name = std.mem.trim(u8, text[self.name_start..self.name_end], &std.ascii.whitespace);
|
||||
if (name.len == 0) return;
|
||||
|
||||
const raw_value = text[self.value_start..self.position];
|
||||
const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace);
|
||||
|
||||
var final_value = value;
|
||||
var is_important = false;
|
||||
|
||||
if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT)) {
|
||||
is_important = true;
|
||||
final_value = std.mem.trimRight(u8, value[0 .. value.len - CSSConstants.IMPORTANT.len], &std.ascii.whitespace);
|
||||
}
|
||||
|
||||
try declarations.append(arena, .{
|
||||
.name = name,
|
||||
.value = final_value,
|
||||
.is_important = is_important,
|
||||
});
|
||||
}
|
||||
|
||||
fn finalize(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
|
||||
if (self.state != .in_value) {
|
||||
return;
|
||||
}
|
||||
return self.finishDeclaration(arena, declarations, text);
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS.Parser - Simple property" {
|
||||
defer testing.reset();
|
||||
|
||||
const text = "color: red;";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("color", declarations[0].name);
|
||||
try testing.expectEqual("red", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - Property with !important" {
|
||||
defer testing.reset();
|
||||
const text = "margin: 10px !important;";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("margin", declarations[0].name);
|
||||
try testing.expectEqual("10px", declarations[0].value);
|
||||
try testing.expectEqual(true, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - Multiple properties" {
|
||||
defer testing.reset();
|
||||
const text = "color: red; font-size: 12px; margin: 5px !important;";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expect(declarations.len == 3);
|
||||
|
||||
try testing.expectEqual("color", declarations[0].name);
|
||||
try testing.expectEqual("red", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
|
||||
try testing.expectEqual("font-size", declarations[1].name);
|
||||
try testing.expectEqual("12px", declarations[1].value);
|
||||
try testing.expectEqual(false, declarations[1].is_important);
|
||||
|
||||
try testing.expectEqual("margin", declarations[2].name);
|
||||
try testing.expectEqual("5px", declarations[2].value);
|
||||
try testing.expectEqual(true, declarations[2].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - Quoted value with semicolon" {
|
||||
defer testing.reset();
|
||||
const text = "content: \"Hello; world!\";";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("content", declarations[0].name);
|
||||
try testing.expectEqual("\"Hello; world!\"", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - URL value" {
|
||||
defer testing.reset();
|
||||
const text = "background-image: url(\"test.png\");";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("background-image", declarations[0].name);
|
||||
try testing.expectEqual("url(\"test.png\")", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - Whitespace handling" {
|
||||
defer testing.reset();
|
||||
const text = " color : purple ; margin : 10px ; ";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(2, declarations.len);
|
||||
try testing.expectEqual("color", declarations[0].name);
|
||||
try testing.expectEqual("purple", declarations[0].value);
|
||||
try testing.expectEqual("margin", declarations[1].name);
|
||||
try testing.expectEqual("10px", declarations[1].value);
|
||||
}
|
||||
41
src/browser/cssom/CSSRule.zig
Normal file
41
src/browser/cssom/CSSRule.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 std = @import("std");
|
||||
|
||||
const CSSStyleSheet = @import("CSSStyleSheet.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
CSSRule,
|
||||
CSSImportRule,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
|
||||
const CSSRule = @This();
|
||||
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,
|
||||
};
|
||||
52
src/browser/cssom/CSSRuleList.zig
Normal file
52
src/browser/cssom/CSSRuleList.zig
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const CSSRule = @import("CSSRule.zig");
|
||||
const StyleSheet = @import("StyleSheet.zig").StyleSheet;
|
||||
|
||||
const CSSImportRule = CSSRule.CSSImportRule;
|
||||
|
||||
const CSSRuleList = @This();
|
||||
list: std.ArrayListUnmanaged([]const u8),
|
||||
|
||||
pub fn constructor() CSSRuleList {
|
||||
return .{ .list = .empty };
|
||||
}
|
||||
|
||||
pub fn _item(self: *CSSRuleList, _index: u32) ?CSSRule {
|
||||
const index: usize = @intCast(_index);
|
||||
|
||||
if (index > self.list.items.len) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// todo: for now, just return null.
|
||||
// this depends on properly parsing CSSRule
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn get_length(self: *CSSRuleList) u32 {
|
||||
return @intCast(self.list.items.len);
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS.CSSRuleList" {
|
||||
try testing.htmlRunner("cssom/css_rule_list.html");
|
||||
}
|
||||
958
src/browser/cssom/CSSStyleDeclaration.zig
Normal file
958
src/browser/cssom/CSSStyleDeclaration.zig
Normal file
@@ -0,0 +1,958 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const CSSRule = @import("CSSRule.zig");
|
||||
const CSSParser = @import("CSSParser.zig");
|
||||
|
||||
const Property = struct {
|
||||
value: []const u8,
|
||||
priority: bool,
|
||||
};
|
||||
|
||||
const CSSStyleDeclaration = @This();
|
||||
|
||||
properties: std.StringArrayHashMapUnmanaged(Property),
|
||||
|
||||
pub const empty: CSSStyleDeclaration = .{
|
||||
.properties = .empty,
|
||||
};
|
||||
|
||||
pub fn get_cssFloat(self: *const CSSStyleDeclaration) []const u8 {
|
||||
return self._getPropertyValue("float");
|
||||
}
|
||||
|
||||
pub fn set_cssFloat(self: *CSSStyleDeclaration, value: ?[]const u8, page: *Page) !void {
|
||||
const final_value = value orelse "";
|
||||
return self._setProperty("float", final_value, null, page);
|
||||
}
|
||||
|
||||
pub fn get_cssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 {
|
||||
var buffer: std.ArrayListUnmanaged(u8) = .empty;
|
||||
const writer = buffer.writer(page.call_arena);
|
||||
var it = self.properties.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const name = entry.key_ptr.*;
|
||||
const property = entry.value_ptr;
|
||||
const escaped = try escapeCSSValue(page.call_arena, property.value);
|
||||
try writer.print("{s}: {s}", .{ name, escaped });
|
||||
if (property.priority) {
|
||||
try writer.writeAll(" !important; ");
|
||||
} else {
|
||||
try writer.writeAll("; ");
|
||||
}
|
||||
}
|
||||
return buffer.items;
|
||||
}
|
||||
|
||||
// TODO Propagate also upward to parent node
|
||||
pub fn set_cssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void {
|
||||
self.properties.clearRetainingCapacity();
|
||||
|
||||
// call_arena is safe here, because _setProperty will dupe the name
|
||||
// using the page's longer-living arena.
|
||||
const declarations = try CSSParser.parseDeclarations(page.call_arena, text);
|
||||
|
||||
for (declarations) |decl| {
|
||||
if (!isValidPropertyName(decl.name)) {
|
||||
continue;
|
||||
}
|
||||
const priority: ?[]const u8 = if (decl.is_important) "important" else null;
|
||||
try self._setProperty(decl.name, decl.value, priority, page);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_length(self: *const CSSStyleDeclaration) usize {
|
||||
return self.properties.count();
|
||||
}
|
||||
|
||||
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _getPropertyPriority(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
||||
const property = self.properties.getPtr(name) orelse return "";
|
||||
return if (property.priority) "important" else "";
|
||||
}
|
||||
|
||||
// TODO should handle properly shorthand properties and canonical forms
|
||||
pub fn _getPropertyValue(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
||||
if (self.properties.getPtr(name)) |property| {
|
||||
return property.value;
|
||||
}
|
||||
|
||||
// default to everything being visible (unless it's been explicitly set)
|
||||
if (std.mem.eql(u8, name, "visibility")) {
|
||||
return "visible";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn _item(self: *const CSSStyleDeclaration, index: usize) []const u8 {
|
||||
const values = self.properties.entries.items(.key);
|
||||
if (index >= values.len) {
|
||||
return "";
|
||||
}
|
||||
return values[index];
|
||||
}
|
||||
|
||||
pub fn _removeProperty(self: *CSSStyleDeclaration, name: []const u8) ![]const u8 {
|
||||
const property = self.properties.fetchOrderedRemove(name) orelse return "";
|
||||
return property.value.value;
|
||||
}
|
||||
|
||||
pub fn _setProperty(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, priority: ?[]const u8, page: *Page) !void {
|
||||
const gop = try self.properties.getOrPut(page.arena, name);
|
||||
if (!gop.found_existing) {
|
||||
const owned_name = try page.arena.dupe(u8, name);
|
||||
gop.key_ptr.* = owned_name;
|
||||
}
|
||||
|
||||
const owned_value = try page.arena.dupe(u8, value);
|
||||
const is_important = priority != null and std.ascii.eqlIgnoreCase(priority.?, "important");
|
||||
gop.value_ptr.* = .{ .value = owned_value, .priority = is_important };
|
||||
}
|
||||
|
||||
pub fn named_get(self: *const CSSStyleDeclaration, name: []const u8, _: *bool) []const u8 {
|
||||
return self._getPropertyValue(name);
|
||||
}
|
||||
|
||||
pub fn named_set(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, _: *bool, page: *Page) !void {
|
||||
return self._setProperty(name, value, null, page);
|
||||
}
|
||||
|
||||
fn isNumericWithUnit(value: []const u8) bool {
|
||||
if (value.len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const first = value[0];
|
||||
|
||||
if (!std.ascii.isDigit(first) and first != '+' and first != '-' and first != '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
var has_digit = false;
|
||||
var decimal_point = false;
|
||||
|
||||
while (i < value.len) : (i += 1) {
|
||||
const c = value[i];
|
||||
if (std.ascii.isDigit(c)) {
|
||||
has_digit = true;
|
||||
} else if (c == '.' and !decimal_point) {
|
||||
decimal_point = true;
|
||||
} else if ((c == 'e' or c == 'E') and has_digit) {
|
||||
if (i + 1 >= value.len) return false;
|
||||
if (value[i + 1] != '+' and value[i + 1] != '-' and !std.ascii.isDigit(value[i + 1])) break;
|
||||
i += 1;
|
||||
if (value[i] == '+' or value[i] == '-') {
|
||||
i += 1;
|
||||
}
|
||||
var has_exp_digits = false;
|
||||
while (i < value.len and std.ascii.isDigit(value[i])) : (i += 1) {
|
||||
has_exp_digits = true;
|
||||
}
|
||||
if (!has_exp_digits) return false;
|
||||
break;
|
||||
} else if (c != '-' and c != '+') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_digit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (i == value.len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const unit = value[i..];
|
||||
return CSSKeywords.isValidUnit(unit);
|
||||
}
|
||||
|
||||
fn isHexColor(value: []const u8) bool {
|
||||
if (value.len == 0) {
|
||||
return false;
|
||||
}
|
||||
if (value[0] != '#') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hex_part = value[1..];
|
||||
if (hex_part.len != 3 and hex_part.len != 6 and hex_part.len != 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (hex_part) |c| {
|
||||
if (!std.ascii.isHex(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn isMultiValueProperty(value: []const u8) bool {
|
||||
var parts = std.mem.splitAny(u8, value, " ");
|
||||
var multi_value_parts: usize = 0;
|
||||
var all_parts_valid = true;
|
||||
|
||||
while (parts.next()) |part| {
|
||||
if (part.len == 0) continue;
|
||||
multi_value_parts += 1;
|
||||
|
||||
if (isNumericWithUnit(part)) {
|
||||
continue;
|
||||
}
|
||||
if (isHexColor(part)) {
|
||||
continue;
|
||||
}
|
||||
if (CSSKeywords.isKnownKeyword(part)) {
|
||||
continue;
|
||||
}
|
||||
if (CSSKeywords.startsWithFunction(part)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
all_parts_valid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return multi_value_parts >= 2 and all_parts_valid;
|
||||
}
|
||||
|
||||
fn isAlreadyQuoted(value: []const u8) bool {
|
||||
return value.len >= 2 and ((value[0] == '"' and value[value.len - 1] == '"') or
|
||||
(value[0] == '\'' and value[value.len - 1] == '\''));
|
||||
}
|
||||
|
||||
fn isValidPropertyName(name: []const u8) bool {
|
||||
if (name.len == 0) return false;
|
||||
|
||||
if (std.mem.startsWith(u8, name, "--")) {
|
||||
if (name.len == 2) return false;
|
||||
for (name[2..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '-' and c != '_') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const first_char = name[0];
|
||||
if (!std.ascii.isAlphabetic(first_char) and first_char != '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (first_char == '-') {
|
||||
if (name.len < 2) return false;
|
||||
|
||||
if (!std.ascii.isAlphabetic(name[1])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (name[2..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (name[1..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn extractImportant(value: []const u8) struct { value: []const u8, is_important: bool } {
|
||||
const trimmed = std.mem.trim(u8, value, &std.ascii.whitespace);
|
||||
|
||||
if (std.mem.endsWith(u8, trimmed, "!important")) {
|
||||
const clean_value = std.mem.trimRight(u8, trimmed[0 .. trimmed.len - 10], &std.ascii.whitespace);
|
||||
return .{ .value = clean_value, .is_important = true };
|
||||
}
|
||||
|
||||
return .{ .value = trimmed, .is_important = false };
|
||||
}
|
||||
|
||||
fn needsQuotes(value: []const u8) bool {
|
||||
if (value.len == 0) return true;
|
||||
if (isAlreadyQuoted(value)) return false;
|
||||
|
||||
if (CSSKeywords.containsSpecialChar(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.indexOfScalar(u8, value, ' ') == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const is_url = std.mem.startsWith(u8, value, "url(");
|
||||
const is_function = CSSKeywords.startsWithFunction(value);
|
||||
|
||||
return !isMultiValueProperty(value) and
|
||||
!is_url and
|
||||
!is_function;
|
||||
}
|
||||
|
||||
fn escapeCSSValue(arena: std.mem.Allocator, value: []const u8) ![]const u8 {
|
||||
if (!needsQuotes(value)) {
|
||||
return value;
|
||||
}
|
||||
var out: std.ArrayListUnmanaged(u8) = .empty;
|
||||
|
||||
// We'll need at least this much space, +2 for the quotes
|
||||
try out.ensureTotalCapacity(arena, value.len + 2);
|
||||
const writer = out.writer(arena);
|
||||
|
||||
try writer.writeByte('"');
|
||||
|
||||
for (value, 0..) |c, i| {
|
||||
switch (c) {
|
||||
'"' => try writer.writeAll("\\\""),
|
||||
'\\' => try writer.writeAll("\\\\"),
|
||||
'\n' => try writer.writeAll("\\A "),
|
||||
'\r' => try writer.writeAll("\\D "),
|
||||
'\t' => try writer.writeAll("\\9 "),
|
||||
0...8, 11, 12, 14...31, 127 => {
|
||||
try writer.print("\\{x}", .{c});
|
||||
if (i + 1 < value.len and std.ascii.isHex(value[i + 1])) {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
},
|
||||
else => try writer.writeByte(c),
|
||||
}
|
||||
}
|
||||
|
||||
try writer.writeByte('"');
|
||||
return out.items;
|
||||
}
|
||||
|
||||
fn isKnownKeyword(value: []const u8) bool {
|
||||
return CSSKeywords.isKnownKeyword(value);
|
||||
}
|
||||
|
||||
fn containsSpecialChar(value: []const u8) bool {
|
||||
return CSSKeywords.containsSpecialChar(value);
|
||||
}
|
||||
|
||||
const CSSKeywords = struct {
|
||||
const BORDER_STYLES = [_][]const u8{
|
||||
"none", "solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset",
|
||||
};
|
||||
|
||||
const COLOR_NAMES = [_][]const u8{
|
||||
"black", "white", "red", "green", "blue", "yellow", "purple", "gray", "transparent",
|
||||
"currentColor", "inherit",
|
||||
};
|
||||
|
||||
const POSITION_KEYWORDS = [_][]const u8{
|
||||
"auto", "center", "left", "right", "top", "bottom",
|
||||
};
|
||||
|
||||
const BACKGROUND_REPEAT = [_][]const u8{
|
||||
"repeat", "no-repeat", "repeat-x", "repeat-y", "space", "round",
|
||||
};
|
||||
|
||||
const FONT_STYLES = [_][]const u8{
|
||||
"normal", "italic", "oblique", "bold", "bolder", "lighter",
|
||||
};
|
||||
|
||||
const FONT_SIZES = [_][]const u8{
|
||||
"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large",
|
||||
"smaller", "larger",
|
||||
};
|
||||
|
||||
const FONT_FAMILIES = [_][]const u8{
|
||||
"serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
|
||||
};
|
||||
|
||||
const CSS_GLOBAL = [_][]const u8{
|
||||
"initial", "inherit", "unset", "revert",
|
||||
};
|
||||
|
||||
const DISPLAY_VALUES = [_][]const u8{
|
||||
"block", "inline", "inline-block", "flex", "grid", "none",
|
||||
};
|
||||
|
||||
const UNITS = [_][]const u8{
|
||||
// LENGTH
|
||||
"px", "em", "rem", "vw", "vh", "vmin", "vmax", "%", "pt", "pc", "in", "cm", "mm",
|
||||
"ex", "ch", "fr",
|
||||
|
||||
// ANGLE
|
||||
"deg", "rad", "grad", "turn",
|
||||
|
||||
// TIME
|
||||
"s", "ms",
|
||||
|
||||
// FREQUENCY
|
||||
"hz", "khz",
|
||||
|
||||
// RESOLUTION
|
||||
"dpi", "dpcm",
|
||||
"dppx",
|
||||
};
|
||||
|
||||
const SPECIAL_CHARS = [_]u8{
|
||||
'"', '\'', ';', '{', '}', '\\', '<', '>', '/', '\n', '\t', '\r', '\x00', '\x7F',
|
||||
};
|
||||
|
||||
const FUNCTIONS = [_][]const u8{
|
||||
"rgb(", "rgba(", "hsl(", "hsla(", "url(", "calc(", "var(", "attr(",
|
||||
"linear-gradient(", "radial-gradient(", "conic-gradient(", "translate(", "rotate(", "scale(", "skew(", "matrix(",
|
||||
};
|
||||
|
||||
const KEYWORDS = BORDER_STYLES ++ COLOR_NAMES ++ POSITION_KEYWORDS ++
|
||||
BACKGROUND_REPEAT ++ FONT_STYLES ++ FONT_SIZES ++ FONT_FAMILIES ++
|
||||
CSS_GLOBAL ++ DISPLAY_VALUES;
|
||||
|
||||
const MAX_KEYWORD_LEN = lengthOfLongestValue(&KEYWORDS);
|
||||
|
||||
pub fn isKnownKeyword(value: []const u8) bool {
|
||||
if (value.len > MAX_KEYWORD_LEN) {
|
||||
return false;
|
||||
}
|
||||
var buf: [MAX_KEYWORD_LEN]u8 = undefined;
|
||||
const normalized = std.ascii.lowerString(&buf, value);
|
||||
|
||||
for (KEYWORDS) |keyword| {
|
||||
if (std.ascii.eqlIgnoreCase(normalized, keyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn containsSpecialChar(value: []const u8) bool {
|
||||
return std.mem.indexOfAny(u8, value, &SPECIAL_CHARS) != null;
|
||||
}
|
||||
|
||||
const MAX_UNIT_LEN = lengthOfLongestValue(&UNITS);
|
||||
|
||||
pub fn isValidUnit(unit: []const u8) bool {
|
||||
if (unit.len > MAX_UNIT_LEN) {
|
||||
return false;
|
||||
}
|
||||
var buf: [MAX_UNIT_LEN]u8 = undefined;
|
||||
const normalized = std.ascii.lowerString(&buf, unit);
|
||||
|
||||
for (UNITS) |u| {
|
||||
if (std.mem.eql(u8, normalized, u)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn startsWithFunction(value: []const u8) bool {
|
||||
const pos = std.mem.indexOfScalar(u8, value, '(') orelse return false;
|
||||
if (pos == 0) return false;
|
||||
|
||||
if (std.mem.indexOfScalarPos(u8, value, pos, ')') == null) {
|
||||
return false;
|
||||
}
|
||||
const function_name = value[0..pos];
|
||||
return isValidFunctionName(function_name);
|
||||
}
|
||||
|
||||
fn isValidFunctionName(name: []const u8) bool {
|
||||
if (name.len == 0) return false;
|
||||
|
||||
const first = name[0];
|
||||
if (!std.ascii.isAlphabetic(first) and first != '_' and first != '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (name[1..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
fn lengthOfLongestValue(values: []const []const u8) usize {
|
||||
var max: usize = 0;
|
||||
for (values) |v| {
|
||||
max = @max(v.len, max);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS.StyleDeclaration" {
|
||||
try testing.htmlRunner("cssom/css_style_declaration.html");
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - valid numbers with units" {
|
||||
try testing.expect(isNumericWithUnit("10px"));
|
||||
try testing.expect(isNumericWithUnit("3.14em"));
|
||||
try testing.expect(isNumericWithUnit("-5rem"));
|
||||
try testing.expect(isNumericWithUnit("+12.5%"));
|
||||
try testing.expect(isNumericWithUnit("0vh"));
|
||||
try testing.expect(isNumericWithUnit(".5vw"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - scientific notation" {
|
||||
try testing.expect(isNumericWithUnit("1e5px"));
|
||||
try testing.expect(isNumericWithUnit("2.5E-3em"));
|
||||
try testing.expect(isNumericWithUnit("1e+2rem"));
|
||||
try testing.expect(isNumericWithUnit("-3.14e10px"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - edge cases and invalid inputs" {
|
||||
try testing.expect(!isNumericWithUnit(""));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("px"));
|
||||
try testing.expect(!isNumericWithUnit("--px"));
|
||||
try testing.expect(!isNumericWithUnit(".px"));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("1e"));
|
||||
try testing.expect(!isNumericWithUnit("1epx"));
|
||||
try testing.expect(!isNumericWithUnit("1e+"));
|
||||
try testing.expect(!isNumericWithUnit("1e+px"));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("1.2.3px"));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("10xyz"));
|
||||
try testing.expect(!isNumericWithUnit("5invalid"));
|
||||
|
||||
try testing.expect(isNumericWithUnit("10"));
|
||||
try testing.expect(isNumericWithUnit("3.14"));
|
||||
try testing.expect(isNumericWithUnit("-5"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isHexColor - valid hex colors" {
|
||||
try testing.expect(isHexColor("#000"));
|
||||
try testing.expect(isHexColor("#fff"));
|
||||
try testing.expect(isHexColor("#123456"));
|
||||
try testing.expect(isHexColor("#abcdef"));
|
||||
try testing.expect(isHexColor("#ABCDEF"));
|
||||
try testing.expect(isHexColor("#12345678"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isHexColor - invalid hex colors" {
|
||||
try testing.expect(!isHexColor(""));
|
||||
try testing.expect(!isHexColor("#"));
|
||||
try testing.expect(!isHexColor("000"));
|
||||
try testing.expect(!isHexColor("#00"));
|
||||
try testing.expect(!isHexColor("#0000"));
|
||||
try testing.expect(!isHexColor("#00000"));
|
||||
try testing.expect(!isHexColor("#0000000"));
|
||||
try testing.expect(!isHexColor("#000000000"));
|
||||
try testing.expect(!isHexColor("#gggggg"));
|
||||
try testing.expect(!isHexColor("#123xyz"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - valid multi-value properties" {
|
||||
try testing.expect(isMultiValueProperty("10px 20px"));
|
||||
try testing.expect(isMultiValueProperty("solid red"));
|
||||
try testing.expect(isMultiValueProperty("#fff black"));
|
||||
try testing.expect(isMultiValueProperty("1em 2em 3em 4em"));
|
||||
try testing.expect(isMultiValueProperty("rgb(255,0,0) solid"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - invalid multi-value properties" {
|
||||
try testing.expect(!isMultiValueProperty(""));
|
||||
try testing.expect(!isMultiValueProperty("10px"));
|
||||
try testing.expect(!isMultiValueProperty("invalid unknown"));
|
||||
try testing.expect(!isMultiValueProperty("10px invalid"));
|
||||
try testing.expect(!isMultiValueProperty(" "));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isAlreadyQuoted - various quoting scenarios" {
|
||||
try testing.expect(isAlreadyQuoted("\"hello\""));
|
||||
try testing.expect(isAlreadyQuoted("'world'"));
|
||||
try testing.expect(isAlreadyQuoted("\"\""));
|
||||
try testing.expect(isAlreadyQuoted("''"));
|
||||
|
||||
try testing.expect(!isAlreadyQuoted(""));
|
||||
try testing.expect(!isAlreadyQuoted("hello"));
|
||||
try testing.expect(!isAlreadyQuoted("\""));
|
||||
try testing.expect(!isAlreadyQuoted("'"));
|
||||
try testing.expect(!isAlreadyQuoted("\"hello'"));
|
||||
try testing.expect(!isAlreadyQuoted("'hello\""));
|
||||
try testing.expect(!isAlreadyQuoted("\"hello"));
|
||||
try testing.expect(!isAlreadyQuoted("hello\""));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isValidPropertyName - valid property names" {
|
||||
try testing.expect(isValidPropertyName("color"));
|
||||
try testing.expect(isValidPropertyName("background-color"));
|
||||
try testing.expect(isValidPropertyName("-webkit-transform"));
|
||||
try testing.expect(isValidPropertyName("font-size"));
|
||||
try testing.expect(isValidPropertyName("margin-top"));
|
||||
try testing.expect(isValidPropertyName("z-index"));
|
||||
try testing.expect(isValidPropertyName("line-height"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isValidPropertyName - invalid property names" {
|
||||
try testing.expect(!isValidPropertyName(""));
|
||||
try testing.expect(!isValidPropertyName("123color"));
|
||||
try testing.expect(!isValidPropertyName("color!"));
|
||||
try testing.expect(!isValidPropertyName("color space"));
|
||||
try testing.expect(!isValidPropertyName("@color"));
|
||||
try testing.expect(!isValidPropertyName("color.test"));
|
||||
try testing.expect(!isValidPropertyName("color_test"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: extractImportant - with and without !important" {
|
||||
var result = extractImportant("red !important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red", result.value);
|
||||
|
||||
result = extractImportant("blue");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("blue", result.value);
|
||||
|
||||
result = extractImportant(" green !important ");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("green", result.value);
|
||||
|
||||
result = extractImportant("!important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = extractImportant("important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("important", result.value);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: needsQuotes - various scenarios" {
|
||||
try testing.expect(needsQuotes(""));
|
||||
try testing.expect(needsQuotes("hello world"));
|
||||
try testing.expect(needsQuotes("test;"));
|
||||
try testing.expect(needsQuotes("a{b}"));
|
||||
try testing.expect(needsQuotes("test\"quote"));
|
||||
|
||||
try testing.expect(!needsQuotes("\"already quoted\""));
|
||||
try testing.expect(!needsQuotes("'already quoted'"));
|
||||
try testing.expect(!needsQuotes("url(image.png)"));
|
||||
try testing.expect(!needsQuotes("rgb(255, 0, 0)"));
|
||||
try testing.expect(!needsQuotes("10px 20px"));
|
||||
try testing.expect(!needsQuotes("simple"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: escapeCSSValue - escaping various characters" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
var result = try escapeCSSValue(allocator, "simple");
|
||||
try testing.expectEqual("simple", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "\"already quoted\"");
|
||||
try testing.expectEqual("\"already quoted\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\"quote");
|
||||
try testing.expectEqual("\"test\\\"quote\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\nline");
|
||||
try testing.expectEqual("\"test\\A line\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\\back");
|
||||
try testing.expectEqual("\"test\\\\back\"", result);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.isKnownKeyword - case sensitivity" {
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("red"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("solid"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("center"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("inherit"));
|
||||
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("RED"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("Red"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("SOLID"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("Center"));
|
||||
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword("invalid"));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword("unknown"));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.containsSpecialChar - various special characters" {
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test\"quote"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test'quote"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test;end"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test{brace"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test}brace"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test\\back"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test<angle"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test>angle"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test/slash"));
|
||||
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar("normal-text"));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar("text123"));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.isValidUnit - various units" {
|
||||
try testing.expect(CSSKeywords.isValidUnit("px"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("em"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("rem"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("%"));
|
||||
|
||||
try testing.expect(CSSKeywords.isValidUnit("deg"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("rad"));
|
||||
|
||||
try testing.expect(CSSKeywords.isValidUnit("s"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("ms"));
|
||||
|
||||
try testing.expect(CSSKeywords.isValidUnit("PX"));
|
||||
|
||||
try testing.expect(!CSSKeywords.isValidUnit("invalid"));
|
||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.startsWithFunction - function detection" {
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgb(255, 0, 0)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgba(255, 0, 0, 0.5)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("url(image.png)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("calc(100% - 20px)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("var(--custom-property)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("linear-gradient(to right, red, blue)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("custom-function(args)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("unknown(test)"));
|
||||
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("not-a-function"));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("missing-paren)"));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("missing-close("));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("rgb"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - whitespace handling" {
|
||||
try testing.expect(!isNumericWithUnit(" 10px"));
|
||||
try testing.expect(!isNumericWithUnit("10 px"));
|
||||
try testing.expect(!isNumericWithUnit("10px "));
|
||||
try testing.expect(!isNumericWithUnit(" 10 px "));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: extractImportant - whitespace edge cases" {
|
||||
var result = extractImportant(" ");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = extractImportant("\t\n\r !important\t\n");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = extractImportant("red\t!important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red", result.value);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isHexColor - mixed case handling" {
|
||||
try testing.expect(isHexColor("#AbC"));
|
||||
try testing.expect(isHexColor("#123aBc"));
|
||||
try testing.expect(isHexColor("#FFffFF"));
|
||||
try testing.expect(isHexColor("#000FFF"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: edge case - very long inputs" {
|
||||
const long_valid = "a" ** 1000 ++ "px";
|
||||
try testing.expect(!isNumericWithUnit(long_valid)); // not numeric
|
||||
|
||||
const long_property = "a-" ** 100 ++ "property";
|
||||
try testing.expect(isValidPropertyName(long_property));
|
||||
|
||||
const long_hex = "#" ++ "a" ** 20;
|
||||
try testing.expect(!isHexColor(long_hex));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: boundary conditions - numeric parsing" {
|
||||
try testing.expect(isNumericWithUnit("0px"));
|
||||
try testing.expect(isNumericWithUnit("0.0px"));
|
||||
try testing.expect(isNumericWithUnit(".0px"));
|
||||
try testing.expect(isNumericWithUnit("0.px"));
|
||||
|
||||
try testing.expect(isNumericWithUnit("999999999px"));
|
||||
try testing.expect(isNumericWithUnit("1.7976931348623157e+308px"));
|
||||
|
||||
try testing.expect(isNumericWithUnit("0.000000001px"));
|
||||
try testing.expect(isNumericWithUnit("1e-100px"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: extractImportant - malformed important declarations" {
|
||||
var result = extractImportant("red ! important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red ! important", result.value);
|
||||
|
||||
result = extractImportant("red !Important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red !Important", result.value);
|
||||
|
||||
result = extractImportant("red !IMPORTANT");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red !IMPORTANT", result.value);
|
||||
|
||||
result = extractImportant("!importantred");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("!importantred", result.value);
|
||||
|
||||
result = extractImportant("red !important !important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red !important", result.value);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - complex spacing scenarios" {
|
||||
try testing.expect(isMultiValueProperty("10px 20px"));
|
||||
try testing.expect(isMultiValueProperty("solid red"));
|
||||
|
||||
try testing.expect(isMultiValueProperty(" 10px 20px "));
|
||||
|
||||
try testing.expect(!isMultiValueProperty("10px\t20px"));
|
||||
try testing.expect(!isMultiValueProperty("10px\n20px"));
|
||||
|
||||
try testing.expect(isMultiValueProperty("10px 20px 30px"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isAlreadyQuoted - edge cases with quotes" {
|
||||
try testing.expect(isAlreadyQuoted("\"'hello'\""));
|
||||
try testing.expect(isAlreadyQuoted("'\"hello\"'"));
|
||||
|
||||
try testing.expect(isAlreadyQuoted("\"hello\\\"world\""));
|
||||
try testing.expect(isAlreadyQuoted("'hello\\'world'"));
|
||||
|
||||
try testing.expect(!isAlreadyQuoted("\"hello"));
|
||||
try testing.expect(!isAlreadyQuoted("hello\""));
|
||||
try testing.expect(!isAlreadyQuoted("'hello"));
|
||||
try testing.expect(!isAlreadyQuoted("hello'"));
|
||||
|
||||
try testing.expect(isAlreadyQuoted("\"a\""));
|
||||
try testing.expect(isAlreadyQuoted("'b'"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: needsQuotes - function and URL edge cases" {
|
||||
try testing.expect(!needsQuotes("rgb(255, 0, 0)"));
|
||||
try testing.expect(!needsQuotes("calc(100% - 20px)"));
|
||||
|
||||
try testing.expect(!needsQuotes("url(path with spaces.jpg)"));
|
||||
|
||||
try testing.expect(!needsQuotes("linear-gradient(to right, red, blue)"));
|
||||
|
||||
try testing.expect(needsQuotes("rgb(255, 0, 0"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: escapeCSSValue - control characters and Unicode" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
var result = try escapeCSSValue(allocator, "test\ttab");
|
||||
try testing.expectEqual("\"test\\9 tab\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\rreturn");
|
||||
try testing.expectEqual("\"test\\D return\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\x00null");
|
||||
try testing.expectEqual("\"test\\0null\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\x7Fdel");
|
||||
try testing.expectEqual("\"test\\7f del\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\"quote\nline\\back");
|
||||
try testing.expectEqual("\"test\\\"quote\\A line\\\\back\"", result);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isValidPropertyName - CSS custom properties and vendor prefixes" {
|
||||
try testing.expect(isValidPropertyName("--custom-color"));
|
||||
try testing.expect(isValidPropertyName("--my-variable"));
|
||||
try testing.expect(isValidPropertyName("--123"));
|
||||
|
||||
try testing.expect(isValidPropertyName("-webkit-transform"));
|
||||
try testing.expect(isValidPropertyName("-moz-border-radius"));
|
||||
try testing.expect(isValidPropertyName("-ms-filter"));
|
||||
try testing.expect(isValidPropertyName("-o-transition"));
|
||||
|
||||
try testing.expect(!isValidPropertyName("-123invalid"));
|
||||
try testing.expect(!isValidPropertyName("--"));
|
||||
try testing.expect(!isValidPropertyName("-"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: startsWithFunction - case sensitivity and partial matches" {
|
||||
try testing.expect(CSSKeywords.startsWithFunction("RGB(255, 0, 0)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("Rgb(255, 0, 0)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("URL(image.png)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rg(something)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("ur(something)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgb(1,2,3)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgba(1,2,3,4)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("my-custom-function(args)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("function-with-dashes(test)"));
|
||||
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("123function(test)"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isHexColor - Unicode and invalid characters" {
|
||||
try testing.expect(!isHexColor("#ghijkl"));
|
||||
try testing.expect(!isHexColor("#12345g"));
|
||||
try testing.expect(!isHexColor("#xyz"));
|
||||
|
||||
try testing.expect(!isHexColor("#АВС"));
|
||||
|
||||
try testing.expect(!isHexColor("#1234567g"));
|
||||
try testing.expect(!isHexColor("#g2345678"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: complex integration scenarios" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
try testing.expect(isMultiValueProperty("rgb(255,0,0) url(bg.jpg)"));
|
||||
|
||||
try testing.expect(!needsQuotes("calc(100% - 20px)"));
|
||||
|
||||
const result = try escapeCSSValue(allocator, "fake(function with spaces");
|
||||
try testing.expectEqual("\"fake(function with spaces\"", result);
|
||||
|
||||
const important_result = extractImportant("rgb(255,0,0) !important");
|
||||
try testing.expect(important_result.is_important);
|
||||
try testing.expectEqual("rgb(255,0,0)", important_result.value);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: performance edge cases - empty and minimal inputs" {
|
||||
try testing.expect(!isNumericWithUnit(""));
|
||||
try testing.expect(!isHexColor(""));
|
||||
try testing.expect(!isMultiValueProperty(""));
|
||||
try testing.expect(!isAlreadyQuoted(""));
|
||||
try testing.expect(!isValidPropertyName(""));
|
||||
try testing.expect(needsQuotes(""));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("a"));
|
||||
try testing.expect(!isHexColor("a"));
|
||||
try testing.expect(!isMultiValueProperty("a"));
|
||||
try testing.expect(!isAlreadyQuoted("a"));
|
||||
try testing.expect(isValidPropertyName("a"));
|
||||
try testing.expect(!needsQuotes("a"));
|
||||
}
|
||||
97
src/browser/cssom/CSSStyleSheet.zig
Normal file
97
src/browser/cssom/CSSStyleSheet.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 Page = @import("../page.zig").Page;
|
||||
const StyleSheet = @import("StyleSheet.zig");
|
||||
const CSSRuleList = @import("CSSRuleList.zig");
|
||||
const CSSImportRule = @import("CSSRule.zig").CSSImportRule;
|
||||
|
||||
const CSSStyleSheet = @This();
|
||||
pub const prototype = *StyleSheet;
|
||||
|
||||
proto: StyleSheet,
|
||||
css_rules: CSSRuleList,
|
||||
owner_rule: ?*CSSImportRule,
|
||||
|
||||
const CSSStyleSheetOpts = struct {
|
||||
base_url: ?[]const u8 = null,
|
||||
// TODO: Suupport media
|
||||
disabled: bool = false,
|
||||
};
|
||||
|
||||
pub fn constructor(_opts: ?CSSStyleSheetOpts) !CSSStyleSheet {
|
||||
const opts = _opts orelse CSSStyleSheetOpts{};
|
||||
return .{
|
||||
.proto = .{ .disabled = opts.disabled },
|
||||
.css_rules = .constructor(),
|
||||
.owner_rule = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_ownerRule(_: *CSSStyleSheet) ?*CSSImportRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn get_cssRules(self: *CSSStyleSheet) *CSSRuleList {
|
||||
return &self.css_rules;
|
||||
}
|
||||
|
||||
pub fn _insertRule(self: *CSSStyleSheet, rule: []const u8, _index: ?usize, page: *Page) !usize {
|
||||
const index = _index orelse 0;
|
||||
if (index > self.css_rules.list.items.len) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
|
||||
const arena = page.arena;
|
||||
try self.css_rules.list.insert(arena, index, try arena.dupe(u8, rule));
|
||||
return index;
|
||||
}
|
||||
|
||||
pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
|
||||
if (index > self.css_rules.list.items.len) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
|
||||
_ = self.css_rules.list.orderedRemove(index);
|
||||
}
|
||||
|
||||
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !Env.Promise {
|
||||
_ = self;
|
||||
_ = text;
|
||||
// TODO: clear self.css_rules
|
||||
// parse text and re-populate self.css_rules
|
||||
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
try resolver.resolve({});
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
|
||||
_ = self;
|
||||
_ = text;
|
||||
// TODO: clear self.css_rules
|
||||
// parse text and re-populate self.css_rules
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS.StyleSheet" {
|
||||
try testing.htmlRunner("cssom/css_stylesheet.html");
|
||||
}
|
||||
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
|
||||
const StyleSheet = @This();
|
||||
|
||||
disabled: bool = false,
|
||||
href: []const u8 = "",
|
||||
owner_node: ?*parser.Node = null,
|
||||
parent_stylesheet: ?*StyleSheet = null,
|
||||
title: []const u8 = "",
|
||||
type: []const u8 = "text/css",
|
||||
|
||||
pub fn get_disabled(self: *const StyleSheet) bool {
|
||||
return self.disabled;
|
||||
}
|
||||
|
||||
pub fn get_href(self: *const StyleSheet) []const u8 {
|
||||
return self.href;
|
||||
}
|
||||
|
||||
// TODO: media
|
||||
|
||||
pub fn get_ownerNode(self: *const StyleSheet) ?*parser.Node {
|
||||
return self.owner_node;
|
||||
}
|
||||
|
||||
pub fn get_parentStyleSheet(self: *const StyleSheet) ?*StyleSheet {
|
||||
return self.parent_stylesheet;
|
||||
}
|
||||
|
||||
pub fn get_title(self: *const StyleSheet) []const u8 {
|
||||
return self.title;
|
||||
}
|
||||
|
||||
pub fn get_type(self: *const StyleSheet) []const u8 {
|
||||
return self.type;
|
||||
}
|
||||
25
src/browser/cssom/cssom.zig
Normal file
25
src/browser/cssom/cssom.zig
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
pub const Interfaces = .{
|
||||
@import("StyleSheet.zig"),
|
||||
@import("CSSStyleSheet.zig"),
|
||||
@import("CSSStyleDeclaration.zig"),
|
||||
@import("CSSRuleList.zig"),
|
||||
@import("CSSRule.zig").Interfaces,
|
||||
};
|
||||
109
src/browser/dom/Animation.zig
Normal file
109
src/browser/dom/Animation.zig
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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: DOM.Animation" {
|
||||
try testing.htmlRunner("dom/animation.html");
|
||||
}
|
||||
291
src/browser/dom/MessageChannel.zig
Normal file
291
src/browser/dom/MessageChannel.zig
Normal file
@@ -0,0 +1,291 @@
|
||||
// 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: DOM.MessageChannel" {
|
||||
try testing.htmlRunner("dom/message_channel.html");
|
||||
}
|
||||
@@ -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,7 @@ 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 = "" },
|
||||
// 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);
|
||||
|
||||
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);
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Attribute" {
|
||||
try testing.htmlRunner("dom/attribute.html");
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
134
src/browser/dom/character_data.zig
Normal file
134
src/browser/dom/character_data.zig
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
const Comment = @import("comment.zig").Comment;
|
||||
const Text = @import("text.zig");
|
||||
const ProcessingInstruction = @import("processing_instruction.zig").ProcessingInstruction;
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
|
||||
// CharacterData interfaces
|
||||
pub const Interfaces = .{
|
||||
Comment,
|
||||
Text.Text,
|
||||
Text.Interfaces,
|
||||
ProcessingInstruction,
|
||||
};
|
||||
|
||||
// CharacterData implementation
|
||||
pub const CharacterData = struct {
|
||||
pub const Self = parser.CharacterData;
|
||||
pub const prototype = *Node;
|
||||
pub const subtype = .node;
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
// Read attributes
|
||||
|
||||
pub fn get_length(self: *parser.CharacterData) !u32 {
|
||||
return try parser.characterDataLength(self);
|
||||
}
|
||||
|
||||
pub fn get_nextElementSibling(self: *parser.CharacterData) !?ElementUnion {
|
||||
const res = try parser.nodeNextElementSibling(parser.characterDataToNode(self));
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try Element.toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_previousElementSibling(self: *parser.CharacterData) !?ElementUnion {
|
||||
const res = try parser.nodePreviousElementSibling(parser.characterDataToNode(self));
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try Element.toInterface(res.?);
|
||||
}
|
||||
|
||||
// Read/Write attributes
|
||||
|
||||
pub fn get_data(self: *parser.CharacterData) ![]const u8 {
|
||||
return try parser.characterDataData(self);
|
||||
}
|
||||
|
||||
pub fn set_data(self: *parser.CharacterData, data: []const u8) !void {
|
||||
return try parser.characterDataSetData(self, data);
|
||||
}
|
||||
|
||||
// JS methods
|
||||
// ----------
|
||||
|
||||
pub fn _appendData(self: *parser.CharacterData, data: []const u8) !void {
|
||||
return try parser.characterDataAppendData(self, data);
|
||||
}
|
||||
|
||||
pub fn _deleteData(self: *parser.CharacterData, offset: u32, count: u32) !void {
|
||||
return try parser.characterDataDeleteData(self, offset, count);
|
||||
}
|
||||
|
||||
pub fn _insertData(self: *parser.CharacterData, offset: u32, data: []const u8) !void {
|
||||
return try parser.characterDataInsertData(self, offset, data);
|
||||
}
|
||||
|
||||
pub fn _replaceData(self: *parser.CharacterData, offset: u32, count: u32, data: []const u8) !void {
|
||||
return try parser.characterDataReplaceData(self, offset, count, data);
|
||||
}
|
||||
|
||||
pub fn _substringData(self: *parser.CharacterData, offset: u32, count: u32) ![]const u8 {
|
||||
return 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(@ptrCast(@alignCast(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
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.CharacterData" {
|
||||
try testing.htmlRunner("dom/character_data.html");
|
||||
}
|
||||
@@ -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,7 @@ 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" },
|
||||
|
||||
.{ .src = "let emptycomment = new Comment()", .ex = "undefined" },
|
||||
.{ .src = "emptycomment.data", .ex = "" },
|
||||
};
|
||||
try checkCases(js_env, &constructor);
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Comment" {
|
||||
try testing.htmlRunner("dom/comment.html");
|
||||
}
|
||||
@@ -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;
|
||||
@@ -38,7 +38,7 @@ pub fn querySelector(alloc: std.mem.Allocator, n: *parser.Node, selector: []cons
|
||||
|
||||
var m = MatchFirst{};
|
||||
|
||||
_ = try css.matchFirst(ps, Node{ .node = n }, &m);
|
||||
_ = try css.matchFirst(&ps, Node{ .node = n }, &m);
|
||||
return m.n;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -74,6 +75,6 @@ pub fn querySelectorAll(alloc: std.mem.Allocator, n: *parser.Node, selector: []c
|
||||
var m = MatchAll.init(alloc);
|
||||
defer m.deinit();
|
||||
|
||||
try css.matchAll(ps, Node{ .node = n }, &m);
|
||||
try css.matchAll(&ps, Node{ .node = n }, &m);
|
||||
return m.toOwnedList();
|
||||
}
|
||||
329
src/browser/dom/document.zig
Normal file
329
src/browser/dom/document.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 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/CSSStyleSheet.zig");
|
||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
||||
const Range = @import("range.zig").Range;
|
||||
|
||||
const CustomEvent = @import("../events/custom_event.zig").CustomEvent;
|
||||
|
||||
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) !union(enum) {
|
||||
base: *parser.Event,
|
||||
custom: CustomEvent,
|
||||
} {
|
||||
if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) {
|
||||
return .{ .base = try parser.eventCreate() };
|
||||
}
|
||||
|
||||
// Not documented in MDN but supported in Chrome.
|
||||
// This is actually both instance of `Event` and `CustomEvent`.
|
||||
if (std.ascii.eqlIgnoreCase(eventCstr, "CustomEvent")) {
|
||||
return .{ .custom = try CustomEvent.constructor(eventCstr, null) };
|
||||
}
|
||||
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion {
|
||||
const e = try parser.documentGetElementById(self, id) orelse return null;
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
|
||||
pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
|
||||
// The element’s namespace is the HTML namespace when document is an HTML document
|
||||
// https://dom.spec.whatwg.org/#ref-for-dom-document-createelement%E2%91%A0
|
||||
const e = try parser.documentCreateElementNS(self, "http://www.w3.org/1999/xhtml", tag_name);
|
||||
return Element.toInterface(e);
|
||||
}
|
||||
|
||||
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {
|
||||
const e = try parser.documentCreateElementNS(self, ns, tag_name);
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
|
||||
// We can't simply use libdom dom_document_get_elements_by_tag_name here.
|
||||
// Indeed, netsurf implemented a previous dom spec when
|
||||
// getElementsByTagName returned a NodeList.
|
||||
// But since
|
||||
// https://github.com/whatwg/dom/commit/190700b7c12ecfd3b5ebdb359ab1d6ea9cbf7749
|
||||
// the spec changed to return an HTMLCollection instead.
|
||||
// That's why we reimplemented getElementsByTagName by using an
|
||||
// HTMLCollection in zig here.
|
||||
pub fn _getElementsByTagName(
|
||||
self: *parser.Document,
|
||||
tag_name: []const u8,
|
||||
page: *Page,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, .{
|
||||
.include_root = true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _getElementsByClassName(
|
||||
self: *parser.Document,
|
||||
classNames: []const u8,
|
||||
page: *Page,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, .{
|
||||
.include_root = true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment {
|
||||
return try parser.documentCreateDocumentFragment(self);
|
||||
}
|
||||
|
||||
pub fn _createTextNode(self: *parser.Document, data: []const u8) !*parser.Text {
|
||||
return try parser.documentCreateTextNode(self, data);
|
||||
}
|
||||
|
||||
pub fn _createCDATASection(self: *parser.Document, data: []const u8) !*parser.CDATASection {
|
||||
return try parser.documentCreateCDATASection(self, data);
|
||||
}
|
||||
|
||||
pub fn _createComment(self: *parser.Document, data: []const u8) !*parser.Comment {
|
||||
return try parser.documentCreateComment(self, data);
|
||||
}
|
||||
|
||||
pub fn _createProcessingInstruction(self: *parser.Document, target: []const u8, data: []const u8) !*parser.ProcessingInstruction {
|
||||
return try parser.documentCreateProcessingInstruction(self, target, data);
|
||||
}
|
||||
|
||||
pub fn _importNode(self: *parser.Document, node: *parser.Node, deep: ?bool) !NodeUnion {
|
||||
const n = try parser.documentImportNode(self, node, deep orelse false);
|
||||
return try Node.toInterface(n);
|
||||
}
|
||||
|
||||
pub fn _adoptNode(self: *parser.Document, node: *parser.Node) !NodeUnion {
|
||||
const n = try parser.documentAdoptNode(self, node);
|
||||
return try Node.toInterface(n);
|
||||
}
|
||||
|
||||
pub fn _createAttribute(self: *parser.Document, name: []const u8) !*parser.Attribute {
|
||||
return try parser.documentCreateAttribute(self, name);
|
||||
}
|
||||
|
||||
pub fn _createAttributeNS(self: *parser.Document, ns: []const u8, qname: []const u8) !*parser.Attribute {
|
||||
return try parser.documentCreateAttributeNS(self, ns, qname);
|
||||
}
|
||||
|
||||
// ParentNode
|
||||
// https://dom.spec.whatwg.org/#parentnode
|
||||
pub fn get_children(self: *parser.Document) !collection.HTMLCollection {
|
||||
return collection.HTMLCollectionChildren(parser.documentToNode(self), .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_firstElementChild(self: *parser.Document) !?ElementUnion {
|
||||
const elt = try parser.documentGetDocumentElement(self) orelse return null;
|
||||
return try Element.toInterface(elt);
|
||||
}
|
||||
|
||||
pub fn get_lastElementChild(self: *parser.Document) !?ElementUnion {
|
||||
const elt = try parser.documentGetDocumentElement(self) orelse return null;
|
||||
return try Element.toInterface(elt);
|
||||
}
|
||||
|
||||
pub fn get_childElementCount(self: *parser.Document) !u32 {
|
||||
_ = try parser.documentGetDocumentElement(self) orelse return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
pub fn _querySelector(self: *parser.Document, selector: []const u8, page: *Page) !?ElementUnion {
|
||||
if (selector.len == 0) return null;
|
||||
|
||||
const n = try css.querySelector(page.call_arena, parser.documentToNode(self), selector);
|
||||
|
||||
if (n == null) return null;
|
||||
|
||||
return try Element.toInterface(parser.nodeToElement(n.?));
|
||||
}
|
||||
|
||||
pub fn _querySelectorAll(self: *parser.Document, selector: []const u8, page: *Page) !NodeList {
|
||||
return css.querySelectorAll(page.arena, parser.documentToNode(self), selector);
|
||||
}
|
||||
|
||||
pub fn _prepend(self: *parser.Document, nodes: []const Node.NodeOrText) !void {
|
||||
return Node.prepend(parser.documentToNode(self), nodes);
|
||||
}
|
||||
|
||||
pub fn _append(self: *parser.Document, nodes: []const Node.NodeOrText) !void {
|
||||
return Node.append(parser.documentToNode(self), nodes);
|
||||
}
|
||||
|
||||
pub fn _replaceChildren(self: *parser.Document, nodes: []const Node.NodeOrText) !void {
|
||||
return Node.replaceChildren(parser.documentToNode(self), nodes);
|
||||
}
|
||||
|
||||
pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?TreeWalker.WhatToShow, filter: ?TreeWalker.TreeWalkerOpts) !TreeWalker {
|
||||
return TreeWalker.init(root, what_to_show, filter);
|
||||
}
|
||||
|
||||
pub fn _createNodeIterator(_: *parser.Document, root: *parser.Node, what_to_show: ?NodeIterator.WhatToShow, filter: ?NodeIterator.NodeIteratorOpts) !NodeIterator {
|
||||
return NodeIterator.init(root, what_to_show, filter);
|
||||
}
|
||||
|
||||
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
|
||||
if (page.getNodeState(@ptrCast(@alignCast(self)))) |state| {
|
||||
if (state.active_element) |ae| {
|
||||
return ae;
|
||||
}
|
||||
}
|
||||
|
||||
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
||||
return @ptrCast(@alignCast(body));
|
||||
}
|
||||
|
||||
return try parser.documentGetDocumentElement(self);
|
||||
}
|
||||
|
||||
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
|
||||
const ae = (try getActiveElement(self, page)) orelse return null;
|
||||
return try Element.toInterface(ae);
|
||||
}
|
||||
|
||||
// TODO: some elements can't be focused, like if they're disabled
|
||||
// but there doesn't seem to be a generic way to check this. For example
|
||||
// we could look for the "disabled" attribute, but that's only meaningful
|
||||
// on certain types, and libdom's vtable doesn't seem to expose this.
|
||||
pub fn setFocus(self: *parser.Document, e: *parser.ElementHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.active_element = @ptrCast(e);
|
||||
}
|
||||
|
||||
pub fn _createRange(_: *parser.Document, page: *Page) Range {
|
||||
return Range.constructor(page);
|
||||
}
|
||||
|
||||
// TODO: dummy implementation
|
||||
pub fn get_styleSheets(_: *parser.Document) []CSSStyleSheet {
|
||||
return &.{};
|
||||
}
|
||||
|
||||
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !Env.JsObject {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
if (state.adopted_style_sheets) |obj| {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const obj = try page.main_context.newArray(0).persist();
|
||||
state.adopted_style_sheets = obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: Env.JsObject, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.adopted_style_sheets = try sheets.persist();
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Document" {
|
||||
try testing.htmlRunner("dom/document.html");
|
||||
}
|
||||
96
src/browser/dom/document_fragment.zig
Normal file
96
src/browser/dom/document_fragment.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 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), .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _getElementById(self: *parser.DocumentFragment, id: []const u8) !?ElementUnion {
|
||||
const e = try parser.nodeGetElementById(@ptrCast(@alignCast(self)), id) orelse return null;
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.DocumentFragment" {
|
||||
try testing.htmlRunner("dom/document_fragment.html");
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
@@ -26,7 +26,7 @@ const Node = @import("node.zig").Node;
|
||||
pub const DocumentType = struct {
|
||||
pub const Self = parser.DocumentType;
|
||||
pub const prototype = *Node;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_name(self: *parser.DocumentType) ![]const u8 {
|
||||
return try parser.documentTypeGetName(self);
|
||||
@@ -39,4 +39,29 @@ pub const DocumentType = struct {
|
||||
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" {
|
||||
try testing.htmlRunner("dom/document_type.html");
|
||||
}
|
||||
@@ -20,19 +20,38 @@ 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 = .{
|
||||
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,
|
||||
};
|
||||
41
src/browser/dom/dom_parser.zig
Normal file
41
src/browser/dom/dom_parser.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 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.Parser" {
|
||||
try testing.htmlRunner("dom/dom_parser.html");
|
||||
}
|
||||
609
src/browser/dom/element.zig
Normal file
609
src/browser/dom/element.zig
Normal file
@@ -0,0 +1,609 @@
|
||||
// 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 toInterfaceT(Union, e);
|
||||
}
|
||||
|
||||
pub fn toInterfaceT(comptime T: type, e: *parser.Element) !T {
|
||||
const tagname = try parser.elementGetTagName(e) orelse {
|
||||
// If the owner's document is HTML, assume we have an HTMLElement.
|
||||
const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
|
||||
if (doc != null and !doc.?.is_html) {
|
||||
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
|
||||
}
|
||||
|
||||
return .{ .Element = e };
|
||||
};
|
||||
|
||||
// TODO SVGElement and MathML are not supported yet.
|
||||
|
||||
const tag = parser.Tag.fromString(tagname) catch {
|
||||
// If the owner's document is HTML, assume we have an HTMLElement.
|
||||
const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
|
||||
if (doc != null and doc.?.is_html) {
|
||||
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
|
||||
}
|
||||
|
||||
return .{ .Element = e };
|
||||
};
|
||||
|
||||
return HTMLElem.toInterfaceFromTag(T, e, tag);
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
pub fn get_namespaceURI(self: *parser.Element) !?[]const u8 {
|
||||
return 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 aw = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try dump.writeChildren(parser.elementToNode(self), .{}, &aw.writer);
|
||||
return aw.written();
|
||||
}
|
||||
|
||||
pub fn get_outerHTML(self: *parser.Element, page: *Page) ![]const u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try dump.writeNode(parser.elementToNode(self), .{}, &aw.writer);
|
||||
return aw.written();
|
||||
}
|
||||
|
||||
pub fn set_innerHTML(self: *parser.Element, str: []const u8, page: *Page) !void {
|
||||
const node = parser.elementToNode(self);
|
||||
const doc = 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);
|
||||
|
||||
const fragment_node = parser.documentFragmentToNode(fragment);
|
||||
|
||||
// I'm not sure what the exact behavior is supposed to be. Initially,
|
||||
// we were only copying the body of the document fragment. But it seems
|
||||
// like head elements should be copied too. Specifically, some sites
|
||||
// create script tags via innerHTML, which we need to capture.
|
||||
// If you play with this in a browser, you should notice that the
|
||||
// behavior is different depending on whether you're in a blank page
|
||||
// or an actual document. In a blank page, something like:
|
||||
// x.innerHTML = '<script></script>';
|
||||
// does _not_ create an empty script, but in a real page, it does. Weird.
|
||||
const html = try parser.nodeFirstChild(fragment_node) orelse return;
|
||||
const head = try parser.nodeFirstChild(html) orelse return;
|
||||
const body = try parser.nodeNextSibling(head) orelse return;
|
||||
|
||||
if (try parser.elementTag(self) == .template) {
|
||||
// HTMLElementTemplate is special. We don't append these as children
|
||||
// of the template, but instead set its content as the body of the
|
||||
// fragment. Simpler to do this by copying the body children into
|
||||
// a new fragment
|
||||
const clean = try parser.documentCreateDocumentFragment(doc);
|
||||
const children = try parser.nodeGetChildNodes(body);
|
||||
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(@ptrCast(@alignCast(clean)), child);
|
||||
}
|
||||
|
||||
const state = try page.getOrCreateNodeState(node);
|
||||
state.template_content = clean;
|
||||
return;
|
||||
}
|
||||
|
||||
// For any node other than a template, we copy the head and body elements
|
||||
// as child nodes of the element
|
||||
{
|
||||
// First, copy some of the head element
|
||||
const children = try parser.nodeGetChildNodes(head);
|
||||
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 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 _getAttributeNames(self: *parser.Element, page: *Page) ![]const []const u8 {
|
||||
const attributes = try parser.nodeGetAttributes(@ptrCast(self)) orelse return &.{};
|
||||
const ln = try parser.namedNodeMapGetLength(attributes);
|
||||
|
||||
const names = try page.call_arena.alloc([]const u8, ln);
|
||||
var at: usize = 0;
|
||||
|
||||
for (0..ln) |i| {
|
||||
const attribute = try parser.namedNodeMapItem(attributes, @intCast(i)) orelse break;
|
||||
names[at] = try parser.attributeGetName(attribute);
|
||||
at += 1;
|
||||
}
|
||||
|
||||
return names[0..at];
|
||||
}
|
||||
|
||||
pub fn _getAttributeNode(self: *parser.Element, name: []const u8) !?*parser.Attribute {
|
||||
return try parser.elementGetAttributeNode(self, name);
|
||||
}
|
||||
|
||||
pub fn _getAttributeNodeNS(self: *parser.Element, ns: []const u8, name: []const u8) !?*parser.Attribute {
|
||||
return try parser.elementGetAttributeNodeNS(self, ns, name);
|
||||
}
|
||||
|
||||
pub fn _setAttributeNode(self: *parser.Element, attr: *parser.Attribute) !?*parser.Attribute {
|
||||
return try parser.elementSetAttributeNode(self, attr);
|
||||
}
|
||||
|
||||
pub fn _setAttributeNodeNS(self: *parser.Element, attr: *parser.Attribute) !?*parser.Attribute {
|
||||
return try parser.elementSetAttributeNodeNS(self, attr);
|
||||
}
|
||||
|
||||
pub fn _removeAttributeNode(self: *parser.Element, attr: *parser.Attribute) !*parser.Attribute {
|
||||
return try parser.elementRemoveAttributeNode(self, attr);
|
||||
}
|
||||
|
||||
pub fn _getElementsByTagName(
|
||||
self: *parser.Element,
|
||||
tag_name: []const u8,
|
||||
page: *Page,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(
|
||||
page.arena,
|
||||
parser.elementToNode(self),
|
||||
tag_name,
|
||||
.{ .include_root = false },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _getElementsByClassName(
|
||||
self: *parser.Element,
|
||||
classNames: []const u8,
|
||||
page: *Page,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByClassName(
|
||||
page.arena,
|
||||
parser.elementToNode(self),
|
||||
classNames,
|
||||
.{ .include_root = false },
|
||||
);
|
||||
}
|
||||
|
||||
// ParentNode
|
||||
// https://dom.spec.whatwg.org/#parentnode
|
||||
pub fn get_children(self: *parser.Element) !collection.HTMLCollection {
|
||||
return collection.HTMLCollectionChildren(parser.elementToNode(self), .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_firstElementChild(self: *parser.Element) !?Union {
|
||||
var children = try get_children(self);
|
||||
return try children._item(0);
|
||||
}
|
||||
|
||||
pub fn get_lastElementChild(self: *parser.Element) !?Union {
|
||||
// TODO we could check the last child node first, if it's an element,
|
||||
// we can return it directly instead of looping twice over the
|
||||
// children.
|
||||
var children = try get_children(self);
|
||||
const ln = try children.get_length();
|
||||
if (ln == 0) return null;
|
||||
return try children._item(ln - 1);
|
||||
}
|
||||
|
||||
pub fn get_childElementCount(self: *parser.Element) !u32 {
|
||||
var children = try get_children(self);
|
||||
return try children.get_length();
|
||||
}
|
||||
|
||||
// NonDocumentTypeChildNode
|
||||
// https://dom.spec.whatwg.org/#interface-nondocumenttypechildnode
|
||||
pub fn get_previousElementSibling(self: *parser.Element) !?Union {
|
||||
const res = try parser.nodePreviousElementSibling(parser.elementToNode(self));
|
||||
if (res == null) return null;
|
||||
return try toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_nextElementSibling(self: *parser.Element) !?Union {
|
||||
const res = try parser.nodeNextElementSibling(parser.elementToNode(self));
|
||||
if (res == null) return null;
|
||||
return try toInterface(res.?);
|
||||
}
|
||||
|
||||
fn getElementById(self: *parser.Element, id: []const u8) !?*parser.Node {
|
||||
// walk over the node tree fo find the node by id.
|
||||
const root = parser.elementToNode(self);
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try walker.get_next(root, next) orelse return null;
|
||||
// ignore non-element nodes.
|
||||
if (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(@ptrCast(@alignCast(self)));
|
||||
if (state.shadow_root) |sr| {
|
||||
if (mode != sr.mode) {
|
||||
// this is the behavior per the spec
|
||||
return error.NotSupportedError;
|
||||
}
|
||||
|
||||
try Node.removeChildren(@ptrCast(@alignCast(sr.proto)));
|
||||
return sr;
|
||||
}
|
||||
|
||||
// Not sure what to do if there is no owner document
|
||||
const doc = 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;
|
||||
parser.documentFragmentSetHost(sr.proto, @ptrCast(@alignCast(self)));
|
||||
|
||||
// Storing the ShadowRoot on the element makes sense, it's the ShadowRoot's
|
||||
// parent. When we render, we go top-down, so we'll have the element, get
|
||||
// its shadowroot, and go on. that's what the above code does.
|
||||
// But we sometimes need to go bottom-up, e.g when we have a slot element
|
||||
// and want to find the containing parent. Unforatunately , we don't have
|
||||
// that link, so we need to create it. In the DOM, the ShadowRoot is
|
||||
// represented by this DocumentFragment (it's the ShadowRoot's base prototype)
|
||||
// So we can also store the ShadowRoot in the DocumentFragment's state.
|
||||
const fragment_state = try page.getOrCreateNodeState(@ptrCast(@alignCast(fragment)));
|
||||
fragment_state.shadow_root = sr;
|
||||
|
||||
return sr;
|
||||
}
|
||||
|
||||
pub fn get_shadowRoot(self: *parser.Element, page: *Page) ?*ShadowRoot {
|
||||
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
|
||||
const sr = state.shadow_root orelse return null;
|
||||
if (sr.mode == .closed) {
|
||||
return null;
|
||||
}
|
||||
return sr;
|
||||
}
|
||||
|
||||
pub fn _animate(self: *parser.Element, effect: 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" {
|
||||
try testing.htmlRunner("dom/element.html");
|
||||
}
|
||||
159
src/browser/dom/event_target.zig
Normal file
159
src/browser/dom/event_target.zig
Normal file
@@ -0,0 +1,159 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const 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,
|
||||
screen: *@import("../html/screen.zig").Screen,
|
||||
screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
|
||||
performance: *@import("performance.zig").Performance,
|
||||
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
|
||||
};
|
||||
|
||||
// EventTarget implementation
|
||||
pub const EventTarget = struct {
|
||||
pub const Self = parser.EventTarget;
|
||||
pub const Exception = DOMException;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain },
|
||||
|
||||
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
|
||||
// libdom assumes that all event targets are libdom nodes. They are not.
|
||||
|
||||
switch (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))) };
|
||||
},
|
||||
.screen => {
|
||||
return .{ .screen = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
.screen_orientation => {
|
||||
return .{ .screen_orientation = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
.performance => {
|
||||
return .{ .performance = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
.media_query_list => {
|
||||
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
pub fn constructor(page: *Page) !*parser.EventTarget {
|
||||
const et = try page.arena.create(EventTarget);
|
||||
return @ptrCast(&et.base);
|
||||
}
|
||||
|
||||
pub fn _addEventListener(
|
||||
self: *parser.EventTarget,
|
||||
typ: []const u8,
|
||||
listener: EventHandler.Listener,
|
||||
opts: ?EventHandler.Opts,
|
||||
page: *Page,
|
||||
) !void {
|
||||
_ = try EventHandler.register(page.arena, self, typ, listener, opts);
|
||||
}
|
||||
|
||||
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" {
|
||||
try testing.htmlRunner("dom/event_target.html");
|
||||
}
|
||||
@@ -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,34 +56,84 @@ 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 {
|
||||
const errCast = @as(parser.DOMError, @errorCast(err));
|
||||
const errName = DOMException.name(errCast);
|
||||
const str = switch (errCast) {
|
||||
pub fn init(alloc: std.mem.Allocator, err: anyerror, caller_name: []const u8) !DOMException {
|
||||
const dom_error = @as(parser.DOMError, @errorCast(err));
|
||||
const error_name = DOMException.name(dom_error);
|
||||
const str = switch (dom_error) {
|
||||
error.HierarchyRequest => try allocPrint(
|
||||
alloc,
|
||||
"{s}: Failed to execute '{s}' on 'Node': The new child element contains the parent.",
|
||||
.{ errName, callerName },
|
||||
.{ error_name, caller_name },
|
||||
),
|
||||
error.NoError => unreachable,
|
||||
// todo add more custom error messages
|
||||
else => try allocPrint(
|
||||
alloc,
|
||||
"{s}: TODO message", // TODO: implement other messages
|
||||
.{DOMException.name(errCast)},
|
||||
"{s}: Failed to execute '{s}' : {s}",
|
||||
.{ error_name, caller_name, error_name },
|
||||
),
|
||||
error.NoError => unreachable,
|
||||
};
|
||||
return .{ .err = errCast, .str = str };
|
||||
return .{ .err = dom_error, .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 +143,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 +165,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 +203,22 @@ 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
|
||||
// -----
|
||||
|
||||
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" },
|
||||
// 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);
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Exceptions" {
|
||||
try testing.htmlRunner("dom/exceptions.html");
|
||||
}
|
||||
@@ -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,50 @@ 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,
|
||||
opts: Opts,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByTagName = try MatchByTagName.init(alloc, tag_name),
|
||||
},
|
||||
.include_root = include_root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) },
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.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,36 +104,29 @@ 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,
|
||||
opts: Opts,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByClassName = try MatchByClassName.init(alloc, classNames),
|
||||
},
|
||||
.include_root = include_root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) },
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
};
|
||||
}
|
||||
|
||||
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,57 +135,79 @@ 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,
|
||||
opts: Opts,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByName = try MatchByName.init(alloc, name),
|
||||
},
|
||||
.include_root = include_root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByName = try MatchByName.init(arena, name) },
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.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,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
opts: Opts,
|
||||
) HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerChildren = .{} },
|
||||
.matcher = Matcher{ .matchTrue = .{} },
|
||||
.include_root = include_root,
|
||||
.walker = .{ .walkerChildren = .{} },
|
||||
.matcher = .{ .matchTrue = .{} },
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn HTMLCollectionEmpty() !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = null,
|
||||
.walker = Walker{ .walkerNone = .{} },
|
||||
.matcher = Matcher{ .matchFalse = .{} },
|
||||
.walker = .{ .walkerNone = .{} },
|
||||
.matcher = .{ .matchFalse = .{} },
|
||||
.include_root = false,
|
||||
};
|
||||
}
|
||||
@@ -239,15 +228,14 @@ pub const MatchByLinks = struct {
|
||||
|
||||
pub fn HTMLCollectionByLinks(
|
||||
root: ?*parser.Node,
|
||||
include_root: bool,
|
||||
opts: Opts,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByLinks = MatchByLinks{},
|
||||
},
|
||||
.include_root = include_root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByLinks = MatchByLinks{} },
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -266,21 +254,18 @@ pub const MatchByAnchors = struct {
|
||||
|
||||
pub fn HTMLCollectionByAnchors(
|
||||
root: ?*parser.Node,
|
||||
include_root: bool,
|
||||
opts: Opts,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByAnchors = MatchByAnchors{},
|
||||
},
|
||||
.include_root = include_root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByAnchors = MatchByAnchors{} },
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
};
|
||||
}
|
||||
|
||||
pub const HTMLCollectionIterator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
coll: *HTMLCollection,
|
||||
index: u32 = 0,
|
||||
|
||||
@@ -306,13 +291,16 @@ pub const HTMLCollectionIterator = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const Opts = struct {
|
||||
include_root: bool,
|
||||
mutable: bool = false,
|
||||
};
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection
|
||||
// HTMLCollection is re implemented in zig here because libdom
|
||||
// dom_html_collection expects a comparison function callback as arguement.
|
||||
// But we wanted a dynamically comparison here, according to the match tagname.
|
||||
pub const HTMLCollection = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
matcher: Matcher,
|
||||
walker: Walker,
|
||||
|
||||
@@ -323,16 +311,14 @@ pub const HTMLCollection = struct {
|
||||
// itself.
|
||||
include_root: bool = false,
|
||||
|
||||
// save a state for the collection to improve the _item speed.
|
||||
cur_idx: ?u32 = undefined,
|
||||
cur_node: ?*parser.Node = undefined,
|
||||
mutable: bool = false,
|
||||
|
||||
// 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) = .{},
|
||||
// save a state for the collection to improve the _item speed.
|
||||
cur_idx: ?u32 = null,
|
||||
cur_node: ?*parser.Node = null,
|
||||
|
||||
// start returns the first node to walk on.
|
||||
fn start(self: HTMLCollection) !?*parser.Node {
|
||||
fn start(self: *const HTMLCollection) !?*parser.Node {
|
||||
if (self.root == null) return null;
|
||||
|
||||
if (self.include_root) {
|
||||
@@ -377,7 +363,7 @@ pub const HTMLCollection = struct {
|
||||
var node: *parser.Node = undefined;
|
||||
|
||||
// Use the current state to improve speed if possible.
|
||||
if (self.cur_idx != null and index >= self.cur_idx.?) {
|
||||
if (self.mutable == false and self.cur_idx != null and index >= self.cur_idx.?) {
|
||||
i = self.cur_idx.?;
|
||||
node = self.cur_node.?;
|
||||
} else {
|
||||
@@ -412,7 +398,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 +440,28 @@ 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
|
||||
// -----
|
||||
|
||||
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" },
|
||||
|
||||
// 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" },
|
||||
|
||||
.{ .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" },
|
||||
|
||||
.{ .src = "document.children.length", .ex = "1" },
|
||||
.{ .src = "document.getElementById('content').children.length", .ex = "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);
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.HTMLCollection" {
|
||||
try testing.htmlRunner("dom/html_collection.html");
|
||||
}
|
||||
56
src/browser/dom/implementation.zig
Normal file
56
src/browser/dom/implementation.zig
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#domimplementation
|
||||
pub const DOMImplementation = struct {
|
||||
pub const Exception = DOMException;
|
||||
|
||||
pub fn _createDocumentType(
|
||||
_: *DOMImplementation,
|
||||
qname: [:0]const u8,
|
||||
publicId: [:0]const u8,
|
||||
systemId: [:0]const u8,
|
||||
) !*parser.DocumentType {
|
||||
return try parser.domImplementationCreateDocumentType(qname, publicId, systemId);
|
||||
}
|
||||
|
||||
pub fn _createDocument(
|
||||
_: *DOMImplementation,
|
||||
namespace: ?[:0]const u8,
|
||||
qname: ?[:0]const u8,
|
||||
doctype: ?*parser.DocumentType,
|
||||
) !*parser.Document {
|
||||
return try parser.domImplementationCreateDocument(namespace, qname, doctype);
|
||||
}
|
||||
|
||||
pub fn _createHTMLDocument(_: *DOMImplementation, title: ?[]const u8) !*parser.DocumentHTML {
|
||||
return try parser.domImplementationCreateHTMLDocument(title);
|
||||
}
|
||||
|
||||
pub fn _hasFeature(_: *DOMImplementation) bool {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Implementation" {
|
||||
try testing.htmlRunner("dom/implementation.html");
|
||||
}
|
||||
186
src/browser/dom/intersection_observer.zig
Normal file
186
src/browser/dom/intersection_observer.zig
Normal file
@@ -0,0 +1,186 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Element = @import("element.zig").Element;
|
||||
|
||||
pub const Interfaces = .{
|
||||
IntersectionObserver,
|
||||
IntersectionObserverEntry,
|
||||
};
|
||||
|
||||
// This is supposed to listen to change between the root and observation targets.
|
||||
// However, our rendered stores everything as 1 pixel sized boxes in a long row that never changes.
|
||||
// As such, there are no changes to intersections between the root and any target.
|
||||
// Instead we keep a list of all entries that are being observed.
|
||||
// The callback is called with all entries everytime a new entry is added(observed).
|
||||
// Potentially we should also call the callback at a regular interval.
|
||||
// The returned Entries are phony, they always indicate full intersection.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
|
||||
pub const IntersectionObserver = struct {
|
||||
page: *Page,
|
||||
callback: Env.Function,
|
||||
options: IntersectionObserverOptions,
|
||||
|
||||
observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry),
|
||||
|
||||
// new IntersectionObserver(callback)
|
||||
// new IntersectionObserver(callback, options) [not supported yet]
|
||||
pub fn constructor(callback: Env.Function, options_: ?IntersectionObserverOptions, page: *Page) !IntersectionObserver {
|
||||
var options = IntersectionObserverOptions{
|
||||
.root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
|
||||
.rootMargin = "0px 0px 0px 0px",
|
||||
.threshold = .{ .single = 0.0 },
|
||||
};
|
||||
if (options_) |*o| {
|
||||
if (o.root) |root| {
|
||||
options.root = root;
|
||||
} // Other properties are not used due to the way we render
|
||||
}
|
||||
|
||||
return .{
|
||||
.page = page,
|
||||
.callback = callback,
|
||||
.options = options,
|
||||
.observed_entries = .{},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _disconnect(self: *IntersectionObserver) !void {
|
||||
self.observed_entries = .{}; // We don't free as it is on an arena
|
||||
}
|
||||
|
||||
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element) !void {
|
||||
for (self.observed_entries.items) |*observer| {
|
||||
if (observer.target == target_element) {
|
||||
return; // Already observed
|
||||
}
|
||||
}
|
||||
|
||||
try self.observed_entries.append(self.page.arena, .{
|
||||
.page = self.page,
|
||||
.target = target_element,
|
||||
.options = &self.options,
|
||||
});
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.source = "intersection observer",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
||||
for (self.observed_entries.items, 0..) |*observer, index| {
|
||||
if (observer.target == target) {
|
||||
_ = self.observed_entries.swapRemove(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _takeRecords(self: *IntersectionObserver) []IntersectionObserverEntry {
|
||||
return self.observed_entries.items;
|
||||
}
|
||||
};
|
||||
|
||||
const IntersectionObserverOptions = struct {
|
||||
root: ?*parser.Node, // Element or Document
|
||||
rootMargin: ?[]const u8,
|
||||
threshold: ?Threshold,
|
||||
|
||||
const Threshold = union(enum) {
|
||||
single: f32,
|
||||
list: []const f32,
|
||||
};
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
|
||||
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
||||
pub const IntersectionObserverEntry = struct {
|
||||
page: *Page,
|
||||
target: *parser.Element,
|
||||
options: *IntersectionObserverOptions,
|
||||
|
||||
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
|
||||
pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
||||
return Element._getBoundingClientRect(self.target, self.page);
|
||||
}
|
||||
|
||||
// Returns the ratio of the intersectionRect to the boundingClientRect.
|
||||
pub fn get_intersectionRatio(_: *const IntersectionObserverEntry) f32 {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Returns a DOMRectReadOnly representing the target's visible area.
|
||||
pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
||||
return Element._getBoundingClientRect(self.target, self.page);
|
||||
}
|
||||
|
||||
// A Boolean value which is true if the target element intersects with the
|
||||
// intersection observer's root. If this is true, then, the
|
||||
// IntersectionObserverEntry describes a transition into a state of
|
||||
// intersection; if it's false, then you know the transition is from
|
||||
// intersecting to not-intersecting.
|
||||
pub fn get_isIntersecting(_: *const IntersectionObserverEntry) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns a DOMRectReadOnly for the intersection observer's root.
|
||||
pub fn get_rootBounds(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
||||
const root = self.options.root.?;
|
||||
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
|
||||
return self.page.renderer.boundingRect();
|
||||
}
|
||||
|
||||
const root_type = try parser.nodeType(root);
|
||||
|
||||
var element: *parser.Element = undefined;
|
||||
switch (root_type) {
|
||||
.element => element = parser.nodeToElement(root),
|
||||
.document => {
|
||||
const doc = parser.nodeToDocument(root);
|
||||
element = (try parser.documentGetDocumentElement(doc)).?;
|
||||
},
|
||||
else => return error.InvalidState,
|
||||
}
|
||||
|
||||
return Element._getBoundingClientRect(element, self.page);
|
||||
}
|
||||
|
||||
// The Element whose intersection with the root changed.
|
||||
pub fn get_target(self: *const IntersectionObserverEntry) *parser.Element {
|
||||
return self.target;
|
||||
}
|
||||
|
||||
// TODO: pub fn get_time(self: *const IntersectionObserverEntry)
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.IntersectionObserver" {
|
||||
try testing.htmlRunner("dom/intersection_observer.html");
|
||||
}
|
||||
358
src/browser/dom/mutation_observer.zig
Normal file
358
src/browser/dom/mutation_observer.zig
Normal file
@@ -0,0 +1,358 @@
|
||||
// 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 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 {
|
||||
page: *Page,
|
||||
cbk: Env.Function,
|
||||
connected: bool,
|
||||
scheduled: bool,
|
||||
|
||||
// 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,
|
||||
.page = page,
|
||||
.observed = .{},
|
||||
.connected = true,
|
||||
.scheduled = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?Options) !void {
|
||||
const arena = self.page.arena;
|
||||
var options = options_ orelse Options{};
|
||||
if (options.attributeFilter.len > 0) {
|
||||
options.attributeFilter = try arena.dupe([]const u8, options.attributeFilter);
|
||||
}
|
||||
|
||||
const observer = try arena.create(Observer);
|
||||
observer.* = .{
|
||||
.node = node,
|
||||
.options = options,
|
||||
.mutation_observer = self,
|
||||
.event_node = .{ .id = self.cbk.id, .func = Observer.handle },
|
||||
};
|
||||
|
||||
// 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(ctx: *anyopaque) ?u32 {
|
||||
const self: *MutationObserver = @ptrCast(@alignCast(ctx));
|
||||
if (self.connected == false) {
|
||||
self.scheduled = true;
|
||||
return null;
|
||||
}
|
||||
self.scheduled = false;
|
||||
|
||||
const records = self.observed.items;
|
||||
if (records.len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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",
|
||||
});
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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.page.arena;
|
||||
switch (event_type) {
|
||||
.DOMAttrModified => {
|
||||
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
||||
if (self.options.attributeOldValue) {
|
||||
record.old_value = parser.mutationEventPrevValue(mutation_event) 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.page.scheduler.add(
|
||||
mutation_observer,
|
||||
MutationObserver.callback,
|
||||
0,
|
||||
.{ .name = "mutation_observer" },
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const MutationEventType = enum {
|
||||
DOMAttrModified,
|
||||
DOMCharacterDataModified,
|
||||
DOMNodeInserted,
|
||||
DOMNodeRemoved,
|
||||
|
||||
fn recordType(self: MutationEventType) []const u8 {
|
||||
return switch (self) {
|
||||
.DOMAttrModified => "attributes",
|
||||
.DOMCharacterDataModified => "characterData",
|
||||
.DOMNodeInserted => "childList",
|
||||
.DOMNodeRemoved => "childList",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.MutationObserver" {
|
||||
try testing.htmlRunner("dom/mutation_observer.html");
|
||||
}
|
||||
@@ -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,53 @@ 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 };
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
pub const NamedNodeMapIterator = struct {
|
||||
index: u32 = 0,
|
||||
map: *parser.NamedNodeMap,
|
||||
|
||||
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]" },
|
||||
pub const Return = struct {
|
||||
done: bool,
|
||||
value: ?*parser.Attribute,
|
||||
};
|
||||
try checkCases(js_env, &setItem);
|
||||
|
||||
pub fn _next(self: *NamedNodeMapIterator) !Return {
|
||||
const e = try NamedNodeMap._item(self.map, self.index);
|
||||
if (e == null) {
|
||||
return .{
|
||||
.value = null,
|
||||
.done = true,
|
||||
};
|
||||
}
|
||||
|
||||
self.index += 1;
|
||||
return .{
|
||||
.value = e,
|
||||
.done = false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.NamedNodeMap" {
|
||||
try testing.htmlRunner("dom/named_node_map.html");
|
||||
}
|
||||
892
src/browser/dom/node.zig
Normal file
892
src/browser/dom/node.zig
Normal file
@@ -0,0 +1,892 @@
|
||||
// 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 ElementUnion = @import("element.zig").Union;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
const Document = @import("document.zig").Document;
|
||||
const DocumentType = @import("document_type.zig").DocumentType;
|
||||
const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
|
||||
const HTMLCollection = @import("html_collection.zig").HTMLCollection;
|
||||
const HTMLAllCollection = @import("html_collection.zig").HTMLAllCollection;
|
||||
const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator;
|
||||
const ShadowRoot = @import("shadow_root.zig").ShadowRoot;
|
||||
const Walker = @import("walker.zig").WalkerDepthFirst;
|
||||
|
||||
// HTML
|
||||
const HTML = @import("../html/html.zig");
|
||||
|
||||
// Node interfaces
|
||||
pub const Interfaces = .{
|
||||
Attr,
|
||||
CData.CharacterData,
|
||||
CData.Interfaces,
|
||||
Element,
|
||||
Document,
|
||||
DocumentType,
|
||||
DocumentFragment,
|
||||
HTMLCollection,
|
||||
HTMLAllCollection,
|
||||
HTMLCollectionIterator,
|
||||
HTML.Interfaces,
|
||||
};
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
|
||||
// Node implementation
|
||||
pub const Node = struct {
|
||||
pub const Self = parser.Node;
|
||||
pub const prototype = *EventTarget;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn toInterface(node: *parser.Node) !Union {
|
||||
return switch (try parser.nodeType(node)) {
|
||||
.element => try Element.toInterfaceT(
|
||||
Union,
|
||||
@as(*parser.Element, @ptrCast(node)),
|
||||
),
|
||||
.comment => .{ .Comment = @as(*parser.Comment, @ptrCast(node)) },
|
||||
.text => .{ .Text = @as(*parser.Text, @ptrCast(node)) },
|
||||
.cdata_section => .{ .CDATASection = @as(*parser.CDATASection, @ptrCast(node)) },
|
||||
.processing_instruction => .{ .ProcessingInstruction = @as(*parser.ProcessingInstruction, @ptrCast(node)) },
|
||||
.document => blk: {
|
||||
const doc: *parser.Document = @ptrCast(node);
|
||||
if (doc.is_html) {
|
||||
break :blk .{ .HTMLDocument = @as(*parser.DocumentHTML, @ptrCast(node)) };
|
||||
}
|
||||
|
||||
break :blk .{ .Document = doc };
|
||||
},
|
||||
.document_type => .{ .DocumentType = @as(*parser.DocumentType, @ptrCast(node)) },
|
||||
.attribute => .{ .Attr = @as(*parser.Attribute, @ptrCast(node)) },
|
||||
.document_fragment => .{ .DocumentFragment = @as(*parser.DocumentFragment, @ptrCast(node)) },
|
||||
else => @panic("node type not handled"), // TODO
|
||||
};
|
||||
}
|
||||
|
||||
// class attributes
|
||||
|
||||
pub const _ELEMENT_NODE = @intFromEnum(parser.NodeType.element);
|
||||
pub const _ATTRIBUTE_NODE = @intFromEnum(parser.NodeType.attribute);
|
||||
pub const _TEXT_NODE = @intFromEnum(parser.NodeType.text);
|
||||
pub const _CDATA_SECTION_NODE = @intFromEnum(parser.NodeType.cdata_section);
|
||||
pub const _PROCESSING_INSTRUCTION_NODE = @intFromEnum(parser.NodeType.processing_instruction);
|
||||
pub const _COMMENT_NODE = @intFromEnum(parser.NodeType.comment);
|
||||
pub const _DOCUMENT_NODE = @intFromEnum(parser.NodeType.document);
|
||||
pub const _DOCUMENT_TYPE_NODE = @intFromEnum(parser.NodeType.document_type);
|
||||
pub const _DOCUMENT_FRAGMENT_NODE = @intFromEnum(parser.NodeType.document_fragment);
|
||||
|
||||
// These 3 are deprecated, but both Chrome and Firefox still expose them
|
||||
pub const _ENTITY_REFERENCE_NODE = @intFromEnum(parser.NodeType.entity_reference);
|
||||
pub const _ENTITY_NODE = @intFromEnum(parser.NodeType.entity);
|
||||
pub const _NOTATION_NODE = @intFromEnum(parser.NodeType.notation);
|
||||
|
||||
pub const _DOCUMENT_POSITION_DISCONNECTED = @intFromEnum(parser.DocumentPosition.disconnected);
|
||||
pub const _DOCUMENT_POSITION_PRECEDING = @intFromEnum(parser.DocumentPosition.preceding);
|
||||
pub const _DOCUMENT_POSITION_FOLLOWING = @intFromEnum(parser.DocumentPosition.following);
|
||||
pub const _DOCUMENT_POSITION_CONTAINS = @intFromEnum(parser.DocumentPosition.contains);
|
||||
pub const _DOCUMENT_POSITION_CONTAINED_BY = @intFromEnum(parser.DocumentPosition.contained_by);
|
||||
pub const _DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = @intFromEnum(parser.DocumentPosition.implementation_specific);
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
// Read-only attributes
|
||||
pub fn get_baseURI(_: *parser.Node, page: *Page) ![]const u8 {
|
||||
return page.url.raw;
|
||||
}
|
||||
|
||||
pub fn get_firstChild(self: *parser.Node) !?Union {
|
||||
const res = 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) !?ElementUnion {
|
||||
const res = try parser.nodeParentElement(self);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try Element.toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_nodeName(self: *parser.Node) ![]const u8 {
|
||||
return try parser.nodeName(self);
|
||||
}
|
||||
|
||||
pub fn get_nodeType(self: *parser.Node) !u8 {
|
||||
return @intFromEnum(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 {
|
||||
var node = self;
|
||||
while (true) {
|
||||
const node_type = try parser.nodeType(node);
|
||||
if (node_type == .document) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (try parser.nodeParentNode(node)) |parent| {
|
||||
// didn't find a document, but node has a parent, let's see
|
||||
// if it's connected;
|
||||
node = parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node_type != .document_fragment) {
|
||||
// doesn't have a parent and isn't a document_fragment
|
||||
// can't be connected
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parser.documentFragmentGetHost(@ptrCast(node))) |host| {
|
||||
// node doesn't have a parent, but it's a document fragment
|
||||
// with a host. The host is like the parent, but we only want to
|
||||
// traverse up (or down) to it in specific cases, like isConnected.
|
||||
node = host;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Read/Write attributes
|
||||
|
||||
pub fn get_nodeValue(self: *parser.Node) !?[]const u8 {
|
||||
return 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 {
|
||||
const self_owner = try parser.nodeOwnerDocument(self);
|
||||
const child_owner = try parser.nodeOwnerDocument(child);
|
||||
|
||||
// If the node to be inserted has a different ownerDocument than the parent node,
|
||||
// modern browsers automatically adopt the node and its descendants into
|
||||
// the parent's ownerDocument.
|
||||
// This process is known as adoption.
|
||||
// (7.1) https://dom.spec.whatwg.org/#concept-node-insert
|
||||
if (child_owner == null or (self_owner != null and child_owner.? != self_owner.?)) {
|
||||
const w = Walker{};
|
||||
var current = child;
|
||||
while (true) {
|
||||
current.owner = self_owner;
|
||||
current = try w.get_next(child, current) orelse break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: DocumentFragment special case
|
||||
const res = try parser.nodeAppendChild(self, child);
|
||||
return try Node.toInterface(res);
|
||||
}
|
||||
|
||||
pub fn _cloneNode(self: *parser.Node, deep: ?bool) !Union {
|
||||
const clone = try parser.nodeCloneNode(self, deep orelse false);
|
||||
return try Node.toInterface(clone);
|
||||
}
|
||||
|
||||
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) !u32 {
|
||||
if (self == other) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const docself = try parser.nodeOwnerDocument(self) orelse blk: {
|
||||
if (try parser.nodeType(self) == .document) {
|
||||
break :blk @as(*parser.Document, @ptrCast(self));
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
const docother = try parser.nodeOwnerDocument(other) orelse blk: {
|
||||
if (try parser.nodeType(other) == .document) {
|
||||
break :blk @as(*parser.Document, @ptrCast(other));
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
// Both are in different document.
|
||||
if (docself == null or docother == null or docself.? != docother.?) {
|
||||
return @intFromEnum(parser.DocumentPosition.disconnected) +
|
||||
@intFromEnum(parser.DocumentPosition.implementation_specific) +
|
||||
@intFromEnum(parser.DocumentPosition.preceding);
|
||||
}
|
||||
|
||||
if (@intFromPtr(self) == @intFromPtr(docself.?)) {
|
||||
// if self is the document, and we already know other is in the
|
||||
// document, then other is contained by and following self.
|
||||
return @intFromEnum(parser.DocumentPosition.following) +
|
||||
@intFromEnum(parser.DocumentPosition.contained_by);
|
||||
}
|
||||
|
||||
const rootself = try parser.nodeGetRootNode(self);
|
||||
const rootother = try parser.nodeGetRootNode(other);
|
||||
if (rootself != rootother) {
|
||||
return @intFromEnum(parser.DocumentPosition.disconnected) +
|
||||
@intFromEnum(parser.DocumentPosition.implementation_specific) +
|
||||
@intFromEnum(parser.DocumentPosition.preceding);
|
||||
}
|
||||
|
||||
// TODO Both are in a different trees in the same document.
|
||||
|
||||
const w = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
|
||||
// Is other a descendant of self?
|
||||
while (true) {
|
||||
next = try w.get_next(self, next) orelse break;
|
||||
if (other == next) {
|
||||
return @intFromEnum(parser.DocumentPosition.following) +
|
||||
@intFromEnum(parser.DocumentPosition.contained_by);
|
||||
}
|
||||
}
|
||||
|
||||
// Is self a descendant of other?
|
||||
next = null;
|
||||
while (true) {
|
||||
next = try w.get_next(other, next) orelse break;
|
||||
if (self == next) {
|
||||
return @intFromEnum(parser.DocumentPosition.contains) +
|
||||
@intFromEnum(parser.DocumentPosition.preceding);
|
||||
}
|
||||
}
|
||||
|
||||
next = null;
|
||||
while (true) {
|
||||
next = try w.get_next(parser.documentToNode(docself.?), next) orelse break;
|
||||
if (other == next) {
|
||||
// other precedes self.
|
||||
return @intFromEnum(parser.DocumentPosition.preceding);
|
||||
}
|
||||
if (self == next) {
|
||||
// other follows self.
|
||||
return @intFromEnum(parser.DocumentPosition.following);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn _contains(self: *parser.Node, other: *parser.Node) !bool {
|
||||
return 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
|
||||
const GetRootNodeResult = union(enum) {
|
||||
shadow_root: *ShadowRoot,
|
||||
node: Union,
|
||||
};
|
||||
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
|
||||
if (options) |options_| if (options_.composed) {
|
||||
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
|
||||
};
|
||||
|
||||
const root = try parser.nodeGetRootNode(self);
|
||||
if (page.getNodeState(root)) |state| {
|
||||
if (state.shadow_root) |sr| {
|
||||
return .{ .shadow_root = sr };
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .node = try Node.toInterface(root) };
|
||||
}
|
||||
|
||||
pub fn _hasChildNodes(self: *parser.Node) !bool {
|
||||
return 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_ == null) {
|
||||
return _appendChild(self, new_node);
|
||||
}
|
||||
|
||||
const self_owner = try parser.nodeOwnerDocument(self);
|
||||
const new_node_owner = try parser.nodeOwnerDocument(new_node);
|
||||
|
||||
// If the node to be inserted has a different ownerDocument than the parent node,
|
||||
// modern browsers automatically adopt the node and its descendants into
|
||||
// the parent's ownerDocument.
|
||||
// This process is known as adoption.
|
||||
// (7.1) https://dom.spec.whatwg.org/#concept-node-insert
|
||||
if (new_node_owner == null or (self_owner != null and new_node_owner.? != self_owner.?)) {
|
||||
const w = Walker{};
|
||||
var current = new_node;
|
||||
while (true) {
|
||||
current.owner = self_owner;
|
||||
current = try w.get_next(new_node, current) orelse break;
|
||||
}
|
||||
}
|
||||
|
||||
return Node.toInterface(try parser.nodeInsertBefore(self, new_node, ref_node_.?));
|
||||
}
|
||||
|
||||
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
|
||||
return 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| @ptrCast(@alignCast(try parser.documentCreateTextNode(doc, txt))),
|
||||
};
|
||||
}
|
||||
|
||||
// Whether the node represented by the NodeOrText is the same as the
|
||||
// given Node. Always false for text values as these represent as-of-yet
|
||||
// created Text nodes.
|
||||
fn is(self: NodeOrText, other: *parser.Node) bool {
|
||||
return switch (self) {
|
||||
.text => false,
|
||||
.node => |n| n == other,
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.node" {
|
||||
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" },
|
||||
.{ "const connDiv = document.createElement('div')", null },
|
||||
.{ "connDiv.isConnected", "false" },
|
||||
.{ "const connParentDiv = document.createElement('div')", null },
|
||||
.{ "connParentDiv.appendChild(connDiv)", null },
|
||||
.{ "connDiv.isConnected", "false" },
|
||||
.{ "content.appendChild(connParentDiv)", null },
|
||||
.{ "connDiv.isConnected", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "last_child.nodeValue === 'comment'", "true" },
|
||||
.{ "link.nodeValue === null", "true" },
|
||||
.{ "let text = link.firstChild", "undefined" },
|
||||
.{ "text.nodeValue === 'OK'", "true" },
|
||||
.{ "text.nodeValue = 'OK modified'", "OK modified" },
|
||||
.{ "text.nodeValue === 'OK modified'", "true" },
|
||||
.{ "link.nodeValue = 'nothing'", "nothing" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "text.textContent === 'OK modified'", "true" },
|
||||
.{ "trimAndReplace(content.textContent) === 'OK modified And'", "true" },
|
||||
.{ "text.textContent = 'OK'", "OK" },
|
||||
.{ "text.textContent", "OK" },
|
||||
.{ "trimAndReplace(document.getElementById('para-empty').textContent)", "" },
|
||||
.{ "document.getElementById('para-empty').textContent = 'OK'", "OK" },
|
||||
.{ "document.getElementById('para-empty').firstChild.nodeName === '#text'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let append = document.createElement('h1')", "undefined" },
|
||||
.{ "content.appendChild(append).toString()", "[object HTMLHeadingElement]" },
|
||||
.{ "content.lastChild.__proto__.constructor.name", "HTMLHeadingElement" },
|
||||
.{ "content.appendChild(link).toString()", "[object HTMLAnchorElement]" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let clone = link.cloneNode()", "undefined" },
|
||||
.{ "clone.toString()", "[object HTMLAnchorElement]" },
|
||||
.{ "clone.parentNode === null", "true" },
|
||||
.{ "clone.firstChild === null", "true" },
|
||||
.{ "let clone_deep = link.cloneNode(true)", "undefined" },
|
||||
.{ "clone_deep.firstChild.nodeName === '#text'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "link.contains(text)", "true" },
|
||||
.{ "text.contains(link)", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "link.hasChildNodes()", "true" },
|
||||
.{ "text.hasChildNodes()", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "link.childNodes.length", "1" },
|
||||
.{ "text.childNodes.length", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let insertBefore = document.createElement('a')", "undefined" },
|
||||
.{ "link.insertBefore(insertBefore, text) !== undefined", "true" },
|
||||
.{ "link.firstChild.localName === 'a'", "true" },
|
||||
|
||||
.{ "let insertBefore2 = document.createElement('b')", null },
|
||||
.{ "link.insertBefore(insertBefore2, null).localName", "b" },
|
||||
.{ "link.childNodes[link.childNodes.length - 1].localName", "b" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
// TODO: does not seems to work
|
||||
// .{ "link.isDefaultNamespace('')", "true" },
|
||||
.{ "link.isDefaultNamespace('false')", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let equal1 = document.createElement('a')", "undefined" },
|
||||
.{ "let equal2 = document.createElement('a')", "undefined" },
|
||||
.{ "equal1.textContent = 'is equal'", "is equal" },
|
||||
.{ "equal2.textContent = 'is equal'", "is equal" },
|
||||
// TODO: does not seems to work
|
||||
// .{ "equal1.isEqualNode(equal2)", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.body.isSameNode(document.body)", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
// TODO: no test
|
||||
.{ "link.normalize()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "link.baseURI", "https://lightpanda.io/opensource-browser/" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "content.removeChild(append) !== undefined", "true" },
|
||||
.{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let replace = document.createElement('div')", "undefined" },
|
||||
.{ "link.replaceChild(replace, insertBefore) !== undefined", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "Node.ELEMENT_NODE", "1" },
|
||||
.{ "Node.ATTRIBUTE_NODE", "2" },
|
||||
.{ "Node.TEXT_NODE", "3" },
|
||||
.{ "Node.CDATA_SECTION_NODE", "4" },
|
||||
.{ "Node.PROCESSING_INSTRUCTION_NODE", "7" },
|
||||
.{ "Node.COMMENT_NODE", "8" },
|
||||
.{ "Node.DOCUMENT_NODE", "9" },
|
||||
.{ "Node.DOCUMENT_TYPE_NODE", "10" },
|
||||
.{ "Node.DOCUMENT_FRAGMENT_NODE", "11" },
|
||||
.{ "Node.ENTITY_REFERENCE_NODE", "5" },
|
||||
.{ "Node.ENTITY_NODE", "6" },
|
||||
.{ "Node.NOTATION_NODE", "12" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
test "Browser.DOM.node.owner" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
||||
\\ <div id="target-container">
|
||||
\\ <p id="reference-node">
|
||||
\\ I am the original reference node.
|
||||
\\ </p>
|
||||
\\ </div>"
|
||||
});
|
||||
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ const parser = new DOMParser();
|
||||
\\ const newDoc = parser.parseFromString('<div id="new-node"><p>Hey</p><span>Marked</span></div>', 'text/html');
|
||||
\\ const newNode = newDoc.getElementById('new-node');
|
||||
\\ const parent = document.getElementById('target-container');
|
||||
\\ const referenceNode = document.getElementById('reference-node');
|
||||
\\ parent.insertBefore(newNode, referenceNode);
|
||||
\\ const k = document.getElementById('new-node');
|
||||
\\ const ptag = k.querySelector('p');
|
||||
\\ const spanTag = k.querySelector('span');
|
||||
\\ const anotherDoc = parser.parseFromString('<div id="another-new-node"></div>', 'text/html');
|
||||
\\ const anotherNewNode = anotherDoc.getElementById('another-new-node');
|
||||
\\
|
||||
\\ parent.appendChild(anotherNewNode)
|
||||
,
|
||||
"[object HTMLDivElement]",
|
||||
},
|
||||
|
||||
.{ "parent.ownerDocument === newNode.ownerDocument", "true" },
|
||||
.{ "parent.ownerDocument === anotherNewNode.ownerDocument", "true" },
|
||||
.{ "newNode.firstChild.nodeName", "P" },
|
||||
.{ "ptag.ownerDocument === parent.ownerDocument", "true" },
|
||||
.{ "spanTag.ownerDocument === parent.ownerDocument", "true" },
|
||||
.{ "parent.contains(newNode)", "true" },
|
||||
.{ "parent.contains(anotherNewNode)", "true" },
|
||||
.{ "anotherDoc.contains(anotherNewNode)", "false" },
|
||||
.{ "newDoc.contains(newNode)", "false" },
|
||||
}, .{});
|
||||
}
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
338
src/browser/dom/node_iterator.zig
Normal file
338
src/browser/dom/node_iterator.zig
Normal file
@@ -0,0 +1,338 @@
|
||||
// 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;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator
|
||||
// While this is similar to TreeWalker it has its own implementation as there are several subtle differences
|
||||
// For example:
|
||||
// - nextNode returns the reference node, whereas TreeWalker returns the next node
|
||||
// - Skip and reject are equivalent for NodeIterator, for TreeWalker they are different
|
||||
pub const NodeIterator = struct {
|
||||
pub const Exception = DOMException;
|
||||
|
||||
root: *parser.Node,
|
||||
reference_node: *parser.Node,
|
||||
what_to_show: u32,
|
||||
filter: ?NodeIteratorOpts,
|
||||
filter_func: ?Env.Function,
|
||||
pointer_before_current: bool = true,
|
||||
// used to track / block recursive filters
|
||||
is_in_callback: bool = false,
|
||||
|
||||
// One of the few cases where null and undefined resolve to different default.
|
||||
// We need the raw JsObject so that we can probe the tri state:
|
||||
// null, undefined or i32.
|
||||
pub const WhatToShow = Env.JsObject;
|
||||
|
||||
pub const NodeIteratorOpts = union(enum) {
|
||||
function: Env.Function,
|
||||
object: struct { acceptNode: Env.Function },
|
||||
};
|
||||
|
||||
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator {
|
||||
var filter_func: ?Env.Function = null;
|
||||
if (filter) |f| {
|
||||
filter_func = switch (f) {
|
||||
.function => |func| func,
|
||||
.object => |o| o.acceptNode,
|
||||
};
|
||||
}
|
||||
|
||||
var what_to_show: u32 = undefined;
|
||||
if (what_to_show_) |wts| {
|
||||
switch (try wts.triState(NodeIterator, "what_to_show", u32)) {
|
||||
.null => what_to_show = 0,
|
||||
.undefined => what_to_show = NodeFilter.NodeFilter._SHOW_ALL,
|
||||
.value => |v| what_to_show = v,
|
||||
}
|
||||
} else {
|
||||
what_to_show = NodeFilter.NodeFilter._SHOW_ALL;
|
||||
}
|
||||
|
||||
return .{
|
||||
.root = node,
|
||||
.reference_node = node,
|
||||
.what_to_show = what_to_show,
|
||||
.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 {
|
||||
try self.callbackStart();
|
||||
defer self.callbackEnd();
|
||||
|
||||
if (self.pointer_before_current) {
|
||||
// Unlike TreeWalker, NodeIterator starts at the first node
|
||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
||||
self.pointer_before_current = false;
|
||||
return try Node.toInterface(self.reference_node);
|
||||
}
|
||||
}
|
||||
|
||||
if (try self.firstChild(self.reference_node)) |child| {
|
||||
self.reference_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
var current = self.reference_node;
|
||||
while (current != self.root) {
|
||||
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 {
|
||||
try self.callbackStart();
|
||||
defer self.callbackEnd();
|
||||
|
||||
if (!self.pointer_before_current) {
|
||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
||||
self.pointer_before_current = true;
|
||||
// Still need to verify as last may be first as well
|
||||
return try Node.toInterface(self.reference_node);
|
||||
}
|
||||
}
|
||||
if (self.reference_node == self.root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var current = self.reference_node;
|
||||
while (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;
|
||||
}
|
||||
|
||||
pub fn _detach(self: *const NodeIterator) void {
|
||||
// no-op as per spec
|
||||
_ = self;
|
||||
}
|
||||
|
||||
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
const child_count = 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;
|
||||
}
|
||||
|
||||
fn callbackStart(self: *NodeIterator) !void {
|
||||
if (self.is_in_callback) {
|
||||
// this is the correct DOMExeption
|
||||
return error.InvalidState;
|
||||
}
|
||||
self.is_in_callback = true;
|
||||
}
|
||||
|
||||
fn callbackEnd(self: *NodeIterator) void {
|
||||
self.is_in_callback = false;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.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,21 +18,17 @@
|
||||
|
||||
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 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 = .{
|
||||
@@ -41,8 +37,6 @@ pub const Interfaces = .{
|
||||
};
|
||||
|
||||
pub const NodeListIterator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
coll: *NodeList,
|
||||
index: u32 = 0,
|
||||
|
||||
@@ -69,8 +63,6 @@ pub const NodeListIterator = struct {
|
||||
};
|
||||
|
||||
pub const NodeListEntriesIterator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
coll: *NodeList,
|
||||
index: u32 = 0,
|
||||
|
||||
@@ -104,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
|
||||
@@ -126,11 +110,11 @@ pub const NodeList = struct {
|
||||
try self.nodes.append(alloc, node);
|
||||
}
|
||||
|
||||
pub fn get_length(self: *NodeList) u32 {
|
||||
pub fn get_length(self: *const NodeList) u32 {
|
||||
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;
|
||||
}
|
||||
@@ -139,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 });
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -171,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",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
224
src/browser/dom/performance.zig
Normal file
224
src/browser/dom/performance.zig
Normal file
@@ -0,0 +1,224 @@
|
||||
// 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;
|
||||
|
||||
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
|
||||
|
||||
pub const Interfaces = .{
|
||||
Performance,
|
||||
PerformanceEntry,
|
||||
PerformanceMark,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
|
||||
pub const Performance = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .performance },
|
||||
|
||||
time_origin: u64,
|
||||
// if (Window.crossOriginIsolated) -> Resolution in isolated contexts: 5 microseconds
|
||||
// else -> Resolution in non-isolated contexts: 100 microseconds
|
||||
const ms_resolution = 100;
|
||||
|
||||
pub fn init() Performance {
|
||||
return .{
|
||||
.time_origin = milliTimestamp(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_timeOrigin(self: *const Performance) u64 {
|
||||
return self.time_origin;
|
||||
}
|
||||
|
||||
pub fn reset(self: *Performance) void {
|
||||
self.time_origin = milliTimestamp();
|
||||
}
|
||||
|
||||
pub fn _now(self: *const Performance) u64 {
|
||||
return milliTimestamp() - self.time_origin;
|
||||
}
|
||||
|
||||
pub fn _mark(_: *Performance, name: []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;
|
||||
}
|
||||
|
||||
// TODO: fn _measures should record the marks in a lookup
|
||||
pub fn _getEntriesByName(_: *Performance, name: []const u8, typ: ?[]const u8) []PerformanceEntry {
|
||||
_ = name;
|
||||
_ = typ;
|
||||
return &.{};
|
||||
}
|
||||
|
||||
// TODO: fn _measures should record the marks in a lookup
|
||||
pub fn _getEntriesByType(_: *Performance, typ: []const u8) []PerformanceEntry {
|
||||
_ = typ;
|
||||
return &.{};
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry
|
||||
pub const PerformanceEntry = struct {
|
||||
const PerformanceEntryType = enum {
|
||||
element,
|
||||
event,
|
||||
first_input,
|
||||
largest_contentful_paint,
|
||||
layout_shift,
|
||||
long_animation_frame,
|
||||
longtask,
|
||||
mark,
|
||||
measure,
|
||||
navigation,
|
||||
paint,
|
||||
resource,
|
||||
taskattribution,
|
||||
visibility_state,
|
||||
|
||||
pub fn toString(self: PerformanceEntryType) []const u8 {
|
||||
return switch (self) {
|
||||
.first_input => "first-input",
|
||||
.largest_contentful_paint => "largest-contentful-paint",
|
||||
.layout_shift => "layout-shift",
|
||||
.long_animation_frame => "long-animation-frame",
|
||||
.visibility_state => "visibility-state",
|
||||
else => @tagName(self),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
duration: f64 = 0.0,
|
||||
entry_type: PerformanceEntryType,
|
||||
name: []const u8,
|
||||
start_time: f64 = 0.0,
|
||||
|
||||
pub fn get_duration(self: *const PerformanceEntry) f64 {
|
||||
return self.duration;
|
||||
}
|
||||
|
||||
pub fn get_entryType(self: *const PerformanceEntry) PerformanceEntryType {
|
||||
return self.entry_type;
|
||||
}
|
||||
|
||||
pub fn get_name(self: *const PerformanceEntry) []const u8 {
|
||||
return self.name;
|
||||
}
|
||||
|
||||
pub fn get_startTime(self: *const PerformanceEntry) f64 {
|
||||
return self.start_time;
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMark
|
||||
pub const PerformanceMark = struct {
|
||||
pub const prototype = *PerformanceEntry;
|
||||
|
||||
proto: PerformanceEntry,
|
||||
detail: ?Env.JsObject,
|
||||
|
||||
const Options = struct {
|
||||
detail: ?Env.JsObject = null,
|
||||
startTime: ?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.startTime orelse @as(f64, @floatFromInt(perf._now()));
|
||||
|
||||
if (start_time < 0.0) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const detail = if (options.detail) |d| try d.persist() else null;
|
||||
|
||||
const 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.init();
|
||||
const time_origin = perf.get_timeOrigin();
|
||||
try testing.expect(time_origin >= 0);
|
||||
}
|
||||
|
||||
test "Performance: now" {
|
||||
var perf = Performance.init();
|
||||
|
||||
// Monotonically increasing
|
||||
var now = perf._now();
|
||||
while (now <= 0) { // Loop for now to not be 0
|
||||
try testing.expectEqual(now, 0);
|
||||
now = perf._now();
|
||||
}
|
||||
|
||||
var after = perf._now();
|
||||
while (after <= now) { // Loop untill after > now
|
||||
try testing.expectEqual(after, now);
|
||||
after = perf._now();
|
||||
}
|
||||
}
|
||||
|
||||
test "Browser.Performance.Mark" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let performance = window.performance", null },
|
||||
.{ "performance instanceof Performance", "true" },
|
||||
|
||||
.{ "let mark1 = performance.mark(\"start\")", null },
|
||||
.{ "mark1 instanceof PerformanceMark", "true" },
|
||||
.{ "mark1.name", "start" },
|
||||
.{ "mark1.entryType", "mark" },
|
||||
.{ "mark1.duration", "0" },
|
||||
.{ "mark1.detail", "null" },
|
||||
|
||||
.{ "let mark2 = performance.mark(\"start\", {startTime: 32939393.9})", null },
|
||||
.{ "mark2.startTime", "32939393.9" },
|
||||
}, .{});
|
||||
}
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
428
src/browser/dom/range.zig
Normal file
428
src/browser/dom/range.zig
Normal file
@@ -0,0 +1,428 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
pub const Interfaces = .{
|
||||
AbstractRange,
|
||||
Range,
|
||||
};
|
||||
|
||||
pub const AbstractRange = struct {
|
||||
collapsed: bool,
|
||||
end_node: *parser.Node,
|
||||
end_offset: u32,
|
||||
start_node: *parser.Node,
|
||||
start_offset: u32,
|
||||
|
||||
pub fn updateCollapsed(self: *AbstractRange) void {
|
||||
// TODO: Eventually, compare properly.
|
||||
self.collapsed = false;
|
||||
}
|
||||
|
||||
pub fn get_collapsed(self: *const AbstractRange) bool {
|
||||
return self.collapsed;
|
||||
}
|
||||
|
||||
pub fn get_endContainer(self: *const AbstractRange) !NodeUnion {
|
||||
return Node.toInterface(self.end_node);
|
||||
}
|
||||
|
||||
pub fn get_endOffset(self: *const AbstractRange) u32 {
|
||||
return self.end_offset;
|
||||
}
|
||||
|
||||
pub fn get_startContainer(self: *const AbstractRange) !NodeUnion {
|
||||
return Node.toInterface(self.start_node);
|
||||
}
|
||||
|
||||
pub fn get_startOffset(self: *const AbstractRange) u32 {
|
||||
return self.start_offset;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Range = struct {
|
||||
pub const Exception = DOMException;
|
||||
pub const prototype = *AbstractRange;
|
||||
|
||||
proto: AbstractRange,
|
||||
|
||||
pub const _START_TO_START = 0;
|
||||
pub const _START_TO_END = 1;
|
||||
pub const _END_TO_END = 2;
|
||||
pub const _END_TO_START = 3;
|
||||
|
||||
// The Range() constructor returns a newly created Range object whose start
|
||||
// and end is the global Document object.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Range/Range
|
||||
pub fn constructor(page: *Page) Range {
|
||||
const proto: AbstractRange = .{
|
||||
.collapsed = true,
|
||||
.end_node = parser.documentHTMLToNode(page.window.document),
|
||||
.end_offset = 0,
|
||||
.start_node = parser.documentHTMLToNode(page.window.document),
|
||||
.start_offset = 0,
|
||||
};
|
||||
|
||||
return .{ .proto = proto };
|
||||
}
|
||||
|
||||
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
|
||||
try ensureValidOffset(node, offset_);
|
||||
const offset: u32 = @intCast(offset_);
|
||||
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
|
||||
error.WrongDocument => blk: {
|
||||
// allow a node with a different root than the current, or
|
||||
// a disconnected one. Treat it as if it's "after", so that
|
||||
// we also update the end_offset and end_node.
|
||||
break :blk 1;
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
|
||||
if (position == 1) {
|
||||
// if we're setting the node after the current start, the end must
|
||||
// be set too.
|
||||
self.proto.end_offset = offset;
|
||||
self.proto.end_node = node;
|
||||
}
|
||||
self.proto.start_node = node;
|
||||
self.proto.start_offset = offset;
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
pub fn _setStartBefore(self: *Range, node: *parser.Node) !void {
|
||||
const parent, const index = try getParentAndIndex(node);
|
||||
self.proto.start_node = parent;
|
||||
self.proto.start_offset = index;
|
||||
}
|
||||
|
||||
pub fn _setStartAfter(self: *Range, node: *parser.Node) !void {
|
||||
const parent, const index = try getParentAndIndex(node);
|
||||
self.proto.start_node = parent;
|
||||
self.proto.start_offset = index + 1;
|
||||
}
|
||||
|
||||
pub fn _setEnd(self: *Range, node: *parser.Node, offset_: i32) !void {
|
||||
try ensureValidOffset(node, offset_);
|
||||
const offset: u32 = @intCast(offset_);
|
||||
|
||||
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
|
||||
error.WrongDocument => blk: {
|
||||
// allow a node with a different root than the current, or
|
||||
// a disconnected one. Treat it as if it's "before", so that
|
||||
// we also update the end_offset and end_node.
|
||||
break :blk -1;
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
|
||||
if (position == -1) {
|
||||
// if we're setting the node before the current start, the start
|
||||
// must be set too.
|
||||
self.proto.start_offset = offset;
|
||||
self.proto.start_node = node;
|
||||
}
|
||||
|
||||
self.proto.end_node = node;
|
||||
self.proto.end_offset = offset;
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
pub fn _setEndBefore(self: *Range, node: *parser.Node) !void {
|
||||
const parent, const index = try getParentAndIndex(node);
|
||||
self.proto.end_node = parent;
|
||||
self.proto.end_offset = index;
|
||||
}
|
||||
|
||||
pub fn _setEndAfter(self: *Range, node: *parser.Node) !void {
|
||||
const parent, const index = try getParentAndIndex(node);
|
||||
self.proto.end_node = parent;
|
||||
self.proto.end_offset = index + 1;
|
||||
}
|
||||
|
||||
pub fn _createContextualFragment(_: *Range, fragment: []const u8, page: *Page) !*parser.DocumentFragment {
|
||||
const document_html = page.window.document;
|
||||
const document = parser.documentHTMLToDocument(document_html);
|
||||
const doc_frag = try parser.documentParseFragmentFromStr(document, fragment);
|
||||
return doc_frag;
|
||||
}
|
||||
|
||||
pub fn _selectNodeContents(self: *Range, node: *parser.Node) !void {
|
||||
self.proto.start_node = node;
|
||||
self.proto.start_offset = 0;
|
||||
self.proto.end_node = node;
|
||||
|
||||
// Set end_offset
|
||||
switch (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();
|
||||
}
|
||||
|
||||
// creates a copy
|
||||
pub fn _cloneRange(self: *const Range) Range {
|
||||
return .{
|
||||
.proto = .{
|
||||
.collapsed = self.proto.collapsed,
|
||||
.end_node = self.proto.end_node,
|
||||
.end_offset = self.proto.end_offset,
|
||||
.start_node = self.proto.start_node,
|
||||
.start_offset = self.proto.start_offset,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _comparePoint(self: *const Range, node: *parser.Node, offset_: i32) !i32 {
|
||||
const start = self.proto.start_node;
|
||||
if (try parser.nodeGetRootNode(start) != try parser.nodeGetRootNode(node)) {
|
||||
// WPT really wants this error to be first. Later, when we check
|
||||
// if the relative position is 'disconnected', it'll also catch this
|
||||
// case, but WPT will complain because it sometimes also sends
|
||||
// invalid offsets, and it wants WrongDocument to be raised.
|
||||
return error.WrongDocument;
|
||||
}
|
||||
|
||||
if (try parser.nodeType(node) == .document_type) {
|
||||
return error.InvalidNodeType;
|
||||
}
|
||||
|
||||
try ensureValidOffset(node, offset_);
|
||||
|
||||
const offset: u32 = @intCast(offset_);
|
||||
if (try compare(node, offset, start, self.proto.start_offset) == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (try compare(node, offset, self.proto.end_node, self.proto.end_offset) == 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn _isPointInRange(self: *const Range, node: *parser.Node, offset_: i32) !bool {
|
||||
return self._comparePoint(node, offset_) catch |err| switch (err) {
|
||||
error.WrongDocument => return false,
|
||||
else => return err,
|
||||
} == 0;
|
||||
}
|
||||
|
||||
pub fn _intersectsNode(self: *const Range, node: *parser.Node) !bool {
|
||||
const start_root = try parser.nodeGetRootNode(self.proto.start_node);
|
||||
const node_root = try parser.nodeGetRootNode(node);
|
||||
if (start_root != node_root) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parent, const index = getParentAndIndex(node) catch |err| switch (err) {
|
||||
error.InvalidNodeType => return true, // if node has no parent, we return true.
|
||||
else => return err,
|
||||
};
|
||||
|
||||
if (try compare(parent, index + 1, self.proto.start_node, self.proto.start_offset) != 1) {
|
||||
// node isn't after start, can't intersect
|
||||
return false;
|
||||
}
|
||||
|
||||
if (try compare(parent, index, self.proto.end_node, self.proto.end_offset) != -1) {
|
||||
// node isn't before end, can't intersect
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn _compareBoundaryPoints(self: *const Range, how: i32, other: *const Range) !i32 {
|
||||
return switch (how) {
|
||||
_START_TO_START => compare(self.proto.start_node, self.proto.start_offset, other.proto.start_node, other.proto.start_offset),
|
||||
_START_TO_END => compare(self.proto.start_node, self.proto.start_offset, other.proto.end_node, other.proto.end_offset),
|
||||
_END_TO_END => compare(self.proto.end_node, self.proto.end_offset, other.proto.end_node, other.proto.end_offset),
|
||||
_END_TO_START => compare(self.proto.end_node, self.proto.end_offset, other.proto.start_node, other.proto.start_offset),
|
||||
else => error.NotSupported, // this is the correct DOM Exception to return
|
||||
};
|
||||
}
|
||||
|
||||
// The Range.detach() method does nothing. It used to disable the Range
|
||||
// object and enable the browser to release associated resources. The
|
||||
// method has been kept for compatibility.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Range/detach
|
||||
pub fn _detach(_: *Range) void {}
|
||||
};
|
||||
|
||||
fn ensureValidOffset(node: *parser.Node, offset: i32) !void {
|
||||
if (offset < 0) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
|
||||
// not >= because 0 seems to represent the node itself.
|
||||
if (offset > try nodeLength(node)) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
}
|
||||
|
||||
fn nodeLength(node: *parser.Node) !usize {
|
||||
switch (try isTextual(node)) {
|
||||
true => return ((try parser.nodeTextContent(node)) orelse "").len,
|
||||
false => {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
return @intCast(try parser.nodeListLength(children));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn isTextual(node: *parser.Node) !bool {
|
||||
return switch (try parser.nodeType(node)) {
|
||||
.text, .comment, .cdata_section => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } {
|
||||
const parent = (try parser.nodeParentNode(child)) orelse return error.InvalidNodeType;
|
||||
const children = try parser.nodeGetChildNodes(parent);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
defer i += 1;
|
||||
const c = try parser.nodeListItem(children, i) orelse continue;
|
||||
if (c == child) {
|
||||
return .{ parent, i };
|
||||
}
|
||||
}
|
||||
|
||||
// should not be possible to reach this point
|
||||
return error.InvalidNodeType;
|
||||
}
|
||||
|
||||
// implementation is largely copied from the WPT helper called getPosition in
|
||||
// the common.js of the dom folder.
|
||||
fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b: u32) !i32 {
|
||||
if (node_a == node_b) {
|
||||
// This is a simple and common case, where the two nodes are the same
|
||||
// We just need to compare their offsets
|
||||
if (offset_a == offset_b) {
|
||||
return 0;
|
||||
}
|
||||
return if (offset_a < offset_b) -1 else 1;
|
||||
}
|
||||
|
||||
// We're probably comparing two different nodes. "Probably", because the
|
||||
// above case on considered the offset if the two nodes were the same
|
||||
// as-is. They could still be the same here, if we first consider the
|
||||
// offset.
|
||||
const position = try Node._compareDocumentPosition(node_b, node_a);
|
||||
if (position & @intFromEnum(parser.DocumentPosition.disconnected) == @intFromEnum(parser.DocumentPosition.disconnected)) {
|
||||
return error.WrongDocument;
|
||||
}
|
||||
|
||||
if (position & @intFromEnum(parser.DocumentPosition.following) == @intFromEnum(parser.DocumentPosition.following)) {
|
||||
return switch (try compare(node_b, offset_b, node_a, offset_a)) {
|
||||
-1 => 1,
|
||||
1 => -1,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
if (position & @intFromEnum(parser.DocumentPosition.contains) == @intFromEnum(parser.DocumentPosition.contains)) {
|
||||
// node_a contains node_b
|
||||
var child = node_b;
|
||||
while (try parser.nodeParentNode(child)) |parent| {
|
||||
if (parent == node_a) {
|
||||
// child.parentNode == node_a
|
||||
break;
|
||||
}
|
||||
child = parent;
|
||||
} else {
|
||||
// this should not happen, because Node._compareDocumentPosition
|
||||
// has told us that node_a contains node_b, so one of node_b's
|
||||
// parent's MUST be node_a. But somehow we do end up here sometimes.
|
||||
return -1;
|
||||
}
|
||||
|
||||
const child_parent, const child_index = try getParentAndIndex(child);
|
||||
std.debug.assert(node_a == child_parent);
|
||||
return if (child_index < offset_a) -1 else 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Range" {
|
||||
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,
|
||||
};
|
||||
155
src/browser/dom/shadow_root.zig
Normal file
155
src/browser/dom/shadow_root.zig
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const dump = @import("../dump.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Node = @import("node.zig").Node;
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#interface-shadowroot
|
||||
pub const ShadowRoot = struct {
|
||||
pub const prototype = *parser.DocumentFragment;
|
||||
pub const subtype = .node;
|
||||
|
||||
mode: Mode,
|
||||
host: *parser.Element,
|
||||
proto: *parser.DocumentFragment,
|
||||
adopted_style_sheets: ?Env.JsObject = null,
|
||||
|
||||
pub const Mode = enum {
|
||||
open,
|
||||
closed,
|
||||
};
|
||||
|
||||
pub fn get_host(self: *const ShadowRoot) !ElementUnion {
|
||||
return Element.toInterface(self.host);
|
||||
}
|
||||
|
||||
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject {
|
||||
if (self.adopted_style_sheets) |obj| {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const obj = try page.main_context.newArray(0).persist();
|
||||
self.adopted_style_sheets = obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
|
||||
self.adopted_style_sheets = try sheets.persist();
|
||||
}
|
||||
|
||||
pub fn get_innerHTML(self: *ShadowRoot, page: *Page) ![]const u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try dump.writeChildren(parser.documentFragmentToNode(self.proto), .{}, &aw.writer);
|
||||
return aw.written();
|
||||
}
|
||||
|
||||
pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void {
|
||||
const sr_doc = parser.documentFragmentToNode(self.proto);
|
||||
const doc = try parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument;
|
||||
try Node.removeChildren(sr_doc);
|
||||
const str = str_ orelse return;
|
||||
|
||||
const fragment = try parser.documentParseFragmentFromStr(doc, str);
|
||||
const fragment_node = parser.documentFragmentToNode(fragment);
|
||||
|
||||
// Element.set_innerHTML also has some weirdness here. It isn't clear
|
||||
// what should and shouldn't be set. Whatever string you pass to libdom,
|
||||
// it always creates a full HTML document, with an html, head and body
|
||||
// element.
|
||||
// For ShadowRoot, it appears the only the children within the body should
|
||||
// be set.
|
||||
const html = try parser.nodeFirstChild(fragment_node) orelse return;
|
||||
const head = try parser.nodeFirstChild(html) orelse return;
|
||||
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(sr_doc, child);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.ShadowRoot" {
|
||||
defer testing.reset();
|
||||
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
||||
\\ <div id=conflict>nope</div>
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const div1 = document.createElement('div');", null },
|
||||
.{ "let sr1 = div1.attachShadow({mode: 'open'})", null },
|
||||
.{ "sr1.host == div1", "true" },
|
||||
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
|
||||
.{ "div1.shadowRoot == sr1", "true" },
|
||||
|
||||
.{ "try { div1.attachShadow({mode: 'closed'}) } catch (e) { e }", "Error: NotSupportedError" },
|
||||
|
||||
.{ " sr1.append(document.createElement('div'))", null },
|
||||
.{ " sr1.append(document.createElement('span'))", null },
|
||||
.{ "sr1.childElementCount", "2" },
|
||||
// re-attaching clears it
|
||||
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
|
||||
.{ "sr1.childElementCount", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const div2 = document.createElement('di2');", null },
|
||||
.{ "let sr2 = div2.attachShadow({mode: 'closed'})", null },
|
||||
.{ "sr2.host == div2", "true" },
|
||||
.{ "div2.shadowRoot", "null" }, // null when attached with 'closed'
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "sr2.getElementById('conflict')", "null" },
|
||||
.{ "const n1 = document.createElement('div')", null },
|
||||
.{ "n1.id = 'conflict'", null },
|
||||
.{ "sr2.append(n1)", null },
|
||||
.{ "sr2.getElementById('conflict') == n1", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const acss = sr2.adoptedStyleSheets", null },
|
||||
.{ "acss.length", "0" },
|
||||
.{ "acss.push(new CSSStyleSheet())", null },
|
||||
.{ "sr2.adoptedStyleSheets.length", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "sr1.innerHTML = '<p>hello</p>'", null },
|
||||
.{ "sr1.innerHTML", "<p>hello</p>" },
|
||||
.{ "sr1.querySelector('*')", "[object HTMLParagraphElement]" },
|
||||
|
||||
.{ "sr1.innerHTML = null", null },
|
||||
.{ "sr1.innerHTML", "" },
|
||||
.{ "sr1.querySelector('*')", "null" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -16,19 +16,12 @@
|
||||
// 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 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 = .{
|
||||
CDATASection,
|
||||
@@ -37,11 +30,11 @@ pub const Interfaces = .{
|
||||
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 "",
|
||||
);
|
||||
}
|
||||
@@ -66,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" },
|
||||
}, .{});
|
||||
}
|
||||
281
src/browser/dom/tree_walker.zig
Normal file
281
src/browser/dom/tree_walker.zig
Normal file
@@ -0,0 +1,281 @@
|
||||
// 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,
|
||||
|
||||
// One of the few cases where null and undefined resolve to different default.
|
||||
// We need the raw JsObject so that we can probe the tri state:
|
||||
// null, undefined or i32.
|
||||
pub const WhatToShow = Env.JsObject;
|
||||
|
||||
pub const TreeWalkerOpts = union(enum) {
|
||||
function: Env.Function,
|
||||
object: struct { acceptNode: Env.Function },
|
||||
};
|
||||
|
||||
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker {
|
||||
var filter_func: ?Env.Function = null;
|
||||
|
||||
if (filter) |f| {
|
||||
filter_func = switch (f) {
|
||||
.function => |func| func,
|
||||
.object => |o| o.acceptNode,
|
||||
};
|
||||
}
|
||||
|
||||
var what_to_show: u32 = undefined;
|
||||
if (what_to_show_) |wts| {
|
||||
switch (try wts.triState(TreeWalker, "what_to_show", u32)) {
|
||||
.null => what_to_show = 0,
|
||||
.undefined => what_to_show = NodeFilter.NodeFilter._SHOW_ALL,
|
||||
.value => |v| what_to_show = v,
|
||||
}
|
||||
} else {
|
||||
what_to_show = NodeFilter.NodeFilter._SHOW_ALL;
|
||||
}
|
||||
|
||||
return .{
|
||||
.root = node,
|
||||
.current_node = node,
|
||||
.what_to_show = what_to_show,
|
||||
.filter = filter,
|
||||
.filter_func = filter_func,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_root(self: *TreeWalker) !NodeUnion {
|
||||
return try Node.toInterface(self.root);
|
||||
}
|
||||
|
||||
pub fn get_currentNode(self: *TreeWalker) !NodeUnion {
|
||||
return try Node.toInterface(self.current_node);
|
||||
}
|
||||
|
||||
pub fn get_whatToShow(self: *TreeWalker) u32 {
|
||||
return self.what_to_show;
|
||||
}
|
||||
|
||||
pub fn get_filter(self: *TreeWalker) ?TreeWalkerOpts {
|
||||
return self.filter;
|
||||
}
|
||||
|
||||
pub fn set_currentNode(self: *TreeWalker, node: *parser.Node) !void {
|
||||
self.current_node = node;
|
||||
}
|
||||
|
||||
fn firstChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
const child_count = 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,49 +17,99 @@
|
||||
// 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 Page = @import("page.zig").Page;
|
||||
const Walker = @import("dom/walker.zig").WalkerChildren;
|
||||
|
||||
pub const Opts = struct {
|
||||
// set to include element shadowroots in the dump
|
||||
page: ?*const Page = null,
|
||||
|
||||
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: *std.Io.Writer) !void {
|
||||
try writer.writeAll("<!DOCTYPE html>\n");
|
||||
try writeChildren(parser.documentToNode(doc), writer);
|
||||
try writeChildren(parser.documentToNode(doc), opts, writer);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
|
||||
// Spec: https://www.w3.org/TR/xml/#sec-prolog-dtd
|
||||
pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !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: *std.Io.Writer) anyerror!void {
|
||||
switch (try parser.nodeType(node)) {
|
||||
.element => {
|
||||
// open the tag
|
||||
const tag_type = try parser.nodeHTMLGetTagType(node) orelse .undef;
|
||||
if (opts.exclude_scripts and try isScriptOrRelated(tag_type, node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = try parser.nodeLocalName(node);
|
||||
try writer.writeAll("<");
|
||||
try writer.writeAll(tag);
|
||||
|
||||
// write the attributes
|
||||
const map = try parser.nodeGetAttributes(node);
|
||||
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("=\"");
|
||||
const attribute_value = try parser.attributeGetValue(attr) orelse "";
|
||||
try writeEscapedAttributeValue(writer, attribute_value);
|
||||
try writer.writeAll("\"");
|
||||
i += 1;
|
||||
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(">");
|
||||
|
||||
if (opts.page) |page| {
|
||||
if (page.getNodeState(node)) |state| {
|
||||
if (state.shadow_root) |sr| {
|
||||
try writeChildren(@ptrCast(@alignCast(sr.proto)), opts, writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// void elements can't have any content.
|
||||
if (try isVoid(parser.nodeToElement(node))) return;
|
||||
|
||||
// write the children
|
||||
// TODO avoid recursion
|
||||
try writeChildren(node, writer);
|
||||
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("</");
|
||||
@@ -89,7 +139,7 @@ pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
|
||||
.document_fragment => return,
|
||||
// document will never be called, but required for completeness.
|
||||
.document => return,
|
||||
// done globally instead, but required for completeness.
|
||||
// done globally instead, but required for completeness. Only the outer DOCTYPE should be written
|
||||
.document_type => return,
|
||||
// deprecated
|
||||
.attribute => return,
|
||||
@@ -100,19 +150,38 @@ pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
|
||||
}
|
||||
|
||||
// writer must be a std.io.Writer
|
||||
pub fn writeChildren(root: *parser.Node, writer: anytype) !void {
|
||||
pub fn writeChildren(root: *parser.Node, opts: Opts, writer: *std.Io.Writer) !void {
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try walker.get_next(root, next) orelse break;
|
||||
try writeNode(next.?, writer);
|
||||
try writeNode(next.?, opts, writer);
|
||||
}
|
||||
}
|
||||
|
||||
// When `exclude_scripts` is passed to dump, we don't include <script> tags.
|
||||
// We also want to omit <link rel=preload as=ascript>
|
||||
fn isScriptOrRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
|
||||
if (tag_type == .script) {
|
||||
return true;
|
||||
}
|
||||
if (tag_type == .link) {
|
||||
const el = parser.nodeToElement(node);
|
||||
const as = try parser.elementGetAttribute(el, "as") orelse return false;
|
||||
if (!std.ascii.eqlIgnoreCase(as, "script")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rel = try parser.elementGetAttribute(el, "rel") orelse return false;
|
||||
return std.ascii.eqlIgnoreCase(rel, "preload");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
|
||||
// https://html.spec.whatwg.org/#void-elements
|
||||
fn isVoid(elem: *parser.Element) !bool {
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(elem)));
|
||||
const tag = try parser.elementTag(elem);
|
||||
return switch (tag) {
|
||||
.area, .base, .br, .col, .embed, .hr, .img, .input, .link => true,
|
||||
.meta, .source, .track, .wbr => true,
|
||||
@@ -157,6 +226,9 @@ fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
|
||||
|
||||
const testing = std.testing;
|
||||
test "dump.writeHTML" {
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
|
||||
try testWriteHTML(
|
||||
"<div id=\"content\">Over 9000!</div>",
|
||||
"<div id=\"content\">Over 9000!</div>",
|
||||
@@ -183,6 +255,11 @@ test "dump.writeHTML" {
|
||||
\\</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 {
|
||||
@@ -194,13 +271,13 @@ fn testWriteHTML(comptime expected_body: []const u8, src: []const u8) !void {
|
||||
}
|
||||
|
||||
fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
|
||||
var buf = std.ArrayListUnmanaged(u8){};
|
||||
defer buf.deinit(testing.allocator);
|
||||
var aw = std.Io.Writer.Allocating.init(testing.allocator);
|
||||
defer aw.deinit();
|
||||
|
||||
const doc_html = try parser.documentHTMLParseFromStr(src);
|
||||
defer parser.documentHTMLClose(doc_html) catch {};
|
||||
|
||||
const doc = parser.documentHTMLToDocument(doc_html);
|
||||
try writeHTML(doc, buf.writer(testing.allocator));
|
||||
try testing.expectEqualStrings(expected, buf.items);
|
||||
try writeHTML(doc, .{}, &aw.writer);
|
||||
try testing.expectEqualStrings(expected, aw.written());
|
||||
}
|
||||
|
||||
84
src/browser/encoding/TextDecoder.zig
Normal file
84
src/browser/encoding/TextDecoder.zig
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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" {
|
||||
try testing.htmlRunner("encoding/decoder.html");
|
||||
}
|
||||
48
src/browser/encoding/TextEncoder.zig
Normal file
48
src/browser/encoding/TextEncoder.zig
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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" {
|
||||
try testing.htmlRunner("encoding/encoder.html");
|
||||
}
|
||||
@@ -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"),
|
||||
};
|
||||
49
src/browser/env.zig
Normal file
49
src/browser/env.zig
Normal file
@@ -0,0 +1,49 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Page = @import("page.zig").Page;
|
||||
const js = @import("../runtime/js.zig");
|
||||
const generate = @import("../runtime/generate.zig");
|
||||
|
||||
const WebApis = struct {
|
||||
// Wrapped like this for debug ergonomics.
|
||||
// When we create our Env, a few lines down, we define it as:
|
||||
// pub const Env = js.Env(*Page, WebApis);
|
||||
//
|
||||
// If there's a compile time error witht he Env, it's type will be readable,
|
||||
// i.e.: runtime.js.Env(*browser.env.Page, browser.env.WebApis)
|
||||
//
|
||||
// But if we didn't wrap it in the struct, like we once didn't, and defined
|
||||
// env as:
|
||||
// pub const Env = js.Env(*Page, Interfaces);
|
||||
//
|
||||
// Because Interfaces is an anynoumous type, it doesn't have a friendly name
|
||||
// and errors would be something like:
|
||||
// runtime.js.Env(*browser.Page, .{...A HUNDRED TYPES...})
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
@import("crypto/crypto.zig").Crypto,
|
||||
@import("console/console.zig").Console,
|
||||
@import("css/css.zig").Interfaces,
|
||||
@import("cssom/cssom.zig").Interfaces,
|
||||
@import("dom/dom.zig").Interfaces,
|
||||
@import("dom/shadow_root.zig").ShadowRoot,
|
||||
@import("encoding/encoding.zig").Interfaces,
|
||||
@import("events/event.zig").Interfaces,
|
||||
@import("html/html.zig").Interfaces,
|
||||
@import("iterator/iterator.zig").Interfaces,
|
||||
@import("storage/storage.zig").Interfaces,
|
||||
@import("url/url.zig").Interfaces,
|
||||
@import("xhr/xhr.zig").Interfaces,
|
||||
@import("xhr/form_data.zig").Interfaces,
|
||||
@import("xhr/File.zig"),
|
||||
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
||||
});
|
||||
};
|
||||
|
||||
pub const JsThis = Env.JsThis;
|
||||
pub const JsObject = Env.JsObject;
|
||||
pub const Function = Env.Function;
|
||||
pub const Promise = Env.Promise;
|
||||
pub const PromiseResolver = Env.PromiseResolver;
|
||||
|
||||
pub const Env = js.Env(*Page, WebApis);
|
||||
pub const Global = @import("html/window.zig").Window;
|
||||
85
src/browser/events/custom_event.zig
Normal file
85
src/browser/events/custom_event.zig
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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;
|
||||
const netsurf = @import("../netsurf.zig");
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-customevent
|
||||
pub const CustomEvent = struct {
|
||||
pub const prototype = *Event;
|
||||
pub const union_make_copy = true;
|
||||
|
||||
proto: parser.Event,
|
||||
detail: ?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;
|
||||
}
|
||||
|
||||
// Initializes an already created `CustomEvent`.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/initCustomEvent
|
||||
pub fn _initCustomEvent(
|
||||
self: *CustomEvent,
|
||||
event_type: []const u8,
|
||||
can_bubble: bool,
|
||||
cancelable: bool,
|
||||
maybe_detail: ?JsObject,
|
||||
) !void {
|
||||
// This function can only be called after the constructor has called.
|
||||
// So we assume proto is initialized already by constructor.
|
||||
self.proto.type = try netsurf.strFromData(event_type);
|
||||
self.proto.bubble = can_bubble;
|
||||
self.proto.cancelable = cancelable;
|
||||
self.proto.is_initialised = true;
|
||||
// Detail is stored separately.
|
||||
if (maybe_detail) |detail| {
|
||||
self.detail = try detail.persist();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Events.Custom" {
|
||||
try testing.htmlRunner("events/custom.html");
|
||||
}
|
||||
394
src/browser/events/event.zig
Normal file
394
src/browser/events/event.zig
Normal file
@@ -0,0 +1,394 @@
|
||||
// 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 Node = @import("../dom/node.zig").Node;
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
||||
const AbortSignal = @import("../html/AbortController.zig").AbortSignal;
|
||||
|
||||
const CustomEvent = @import("custom_event.zig").CustomEvent;
|
||||
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
||||
const MouseEvent = @import("mouse_event.zig").MouseEvent;
|
||||
const 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);
|
||||
}
|
||||
|
||||
// Even though this is supposed to to provide microsecond resolution, browser
|
||||
// return coarser values to protect against fingerprinting. libdom returns
|
||||
// seconds, which is good enough.
|
||||
pub fn get_timeStamp(self: *parser.Event) !u32 {
|
||||
return parser.eventTimestamp(self);
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
pub fn _initEvent(
|
||||
self: *parser.Event,
|
||||
eventType: []const u8,
|
||||
bubbles: ?bool,
|
||||
cancelable: ?bool,
|
||||
) !void {
|
||||
const opts = EventInit{
|
||||
.bubbles = bubbles orelse false,
|
||||
.cancelable = cancelable orelse false,
|
||||
};
|
||||
return try parser.eventInit(self, eventType, opts);
|
||||
}
|
||||
|
||||
pub fn _stopPropagation(self: *parser.Event) !void {
|
||||
return 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 fn _composedPath(self: *parser.Event, page: *Page) ![]const EventTargetUnion {
|
||||
const et_ = try parser.eventTarget(self);
|
||||
const et = et_ orelse return &.{};
|
||||
|
||||
var node: ?*parser.Node = switch (try parser.eventTargetInternalType(et)) {
|
||||
.libdom_node => @as(*parser.Node, @ptrCast(et)),
|
||||
.plain => parser.eventTargetToNode(et),
|
||||
else => {
|
||||
// Window, XHR, MessagePort, etc...no path beyond the event itself
|
||||
return &.{try EventTarget.toInterface(et, page)};
|
||||
},
|
||||
};
|
||||
|
||||
const arena = page.call_arena;
|
||||
var path: std.ArrayListUnmanaged(EventTargetUnion) = .empty;
|
||||
while (node) |n| {
|
||||
try path.append(arena, .{
|
||||
.node = try Node.toInterface(n),
|
||||
});
|
||||
|
||||
node = try parser.nodeParentNode(n);
|
||||
if (node == null and try parser.nodeType(n) == .document_fragment) {
|
||||
// we have a non-continuous hook from a shadowroot to its host (
|
||||
// it's parent element). libdom doesn't really support ShdowRoots
|
||||
// and, for the most part, that works out well since it naturally
|
||||
// provides isolation. But events don't follow the same
|
||||
// shadowroot isolation as most other things, so, if this is
|
||||
// a parent-less document fragment, we need to check if it has
|
||||
// a host.
|
||||
if (parser.documentFragmentGetHost(@ptrCast(n))) |host| {
|
||||
node = host;
|
||||
|
||||
// If a document fragment has a host, then that host
|
||||
// _has_ to have a state and that state _has_ to have
|
||||
// a shadow_root field. All of this is set in Element._attachShadow
|
||||
if (page.getNodeState(host).?.shadow_root.?.mode == .closed) {
|
||||
// if the shadow root is closed, then the composedPath
|
||||
// starts at the host element.
|
||||
path.clearRetainingCapacity();
|
||||
}
|
||||
} else {
|
||||
// Our document fragement has no parent and no host, we
|
||||
// can break out of the loop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path.getLastOrNull()) |last| {
|
||||
// the Window isn't part of the DOM hierarchy, but for events, it
|
||||
// is, so we need to glue it on.
|
||||
if (last.node == .HTMLDocument and last.node.HTMLDocument == page.window.document) {
|
||||
try path.append(arena, .{ .node = .{ .Window = &page.window } });
|
||||
}
|
||||
}
|
||||
return path.items;
|
||||
}
|
||||
};
|
||||
|
||||
pub const EventHandler = struct {
|
||||
once: bool,
|
||||
capture: bool,
|
||||
callback: 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" {
|
||||
try testing.htmlRunner("events/event.html");
|
||||
}
|
||||
108
src/browser/events/mouse_event.zig
Normal file
108
src/browser/events/mouse_event.zig
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Event = @import("event.zig").Event;
|
||||
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),
|
||||
});
|
||||
|
||||
return mouse_event;
|
||||
}
|
||||
|
||||
pub fn get_button(self: *parser.MouseEvent) u16 {
|
||||
return self.button;
|
||||
}
|
||||
|
||||
// These is just an alias for clientX.
|
||||
pub fn get_x(self: *parser.MouseEvent) i32 {
|
||||
return self.cx;
|
||||
}
|
||||
|
||||
// These is just an alias for clientY.
|
||||
pub fn get_y(self: *parser.MouseEvent) i32 {
|
||||
return self.cy;
|
||||
}
|
||||
|
||||
pub fn get_clientX(self: *parser.MouseEvent) i32 {
|
||||
return self.cx;
|
||||
}
|
||||
|
||||
pub fn get_clientY(self: *parser.MouseEvent) i32 {
|
||||
return self.cy;
|
||||
}
|
||||
|
||||
pub fn get_screenX(self: *parser.MouseEvent) i32 {
|
||||
return self.sx;
|
||||
}
|
||||
|
||||
pub fn get_screenY(self: *parser.MouseEvent) i32 {
|
||||
return self.sy;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Events.Mouse" {
|
||||
try testing.htmlRunner("events/mouse.html");
|
||||
}
|
||||
181
src/browser/html/AbortController.zig
Normal file
181
src/browser/html/AbortController.zig
Normal file
@@ -0,0 +1,181 @@
|
||||
// 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 EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
pub const Interfaces = .{
|
||||
AbortController,
|
||||
AbortSignal,
|
||||
};
|
||||
|
||||
const AbortController = @This();
|
||||
|
||||
signal: *AbortSignal,
|
||||
|
||||
pub fn constructor(page: *Page) !AbortController {
|
||||
// Why do we allocate this rather than storing directly in the struct?
|
||||
// https://github.com/lightpanda-io/project/discussions/165
|
||||
const signal = try page.arena.create(AbortSignal);
|
||||
signal.* = .init;
|
||||
|
||||
return .{
|
||||
.signal = signal,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_signal(self: *AbortController) *AbortSignal {
|
||||
return self.signal;
|
||||
}
|
||||
|
||||
pub fn _abort(self: *AbortController, reason_: ?[]const u8) !void {
|
||||
return self.signal.abort(reason_);
|
||||
}
|
||||
|
||||
pub const AbortSignal = struct {
|
||||
const DEFAULT_REASON = "AbortError";
|
||||
|
||||
pub const prototype = *EventTarget;
|
||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .abort_signal },
|
||||
|
||||
aborted: bool,
|
||||
reason: ?[]const u8,
|
||||
|
||||
pub const init: AbortSignal = .{
|
||||
.reason = null,
|
||||
.aborted = false,
|
||||
};
|
||||
|
||||
pub fn static_abort(reason_: ?[]const u8) AbortSignal {
|
||||
return .{
|
||||
.aborted = true,
|
||||
.reason = reason_ orelse DEFAULT_REASON,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn static_timeout(delay: u32, page: *Page) !*AbortSignal {
|
||||
const callback = try page.arena.create(TimeoutCallback);
|
||||
callback.* = .{
|
||||
.signal = .init,
|
||||
};
|
||||
|
||||
try page.scheduler.add(callback, TimeoutCallback.run, delay, .{ .name = "abort_signal" });
|
||||
return &callback.signal;
|
||||
}
|
||||
|
||||
pub fn get_aborted(self: *const AbortSignal) bool {
|
||||
return self.aborted;
|
||||
}
|
||||
|
||||
fn abort(self: *AbortSignal, reason_: ?[]const u8) !void {
|
||||
self.aborted = true;
|
||||
self.reason = reason_ orelse DEFAULT_REASON;
|
||||
|
||||
const abort_event = try parser.eventCreate();
|
||||
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,
|
||||
|
||||
fn run(ctx: *anyopaque) ?u32 {
|
||||
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
|
||||
self.signal.abort("TimeoutError") catch |err| {
|
||||
log.warn(.app, "abort signal timeout", .{ .err = err });
|
||||
};
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.AbortController" {
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
429
src/browser/html/document.zig
Normal file
429
src/browser/html/document.zig
Normal file
@@ -0,0 +1,429 @@
|
||||
// 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), .{
|
||||
.is_http = false,
|
||||
.is_navigation = true,
|
||||
});
|
||||
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, .{
|
||||
.include_root = 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", .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", .{
|
||||
.include_root = 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", .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", .{
|
||||
.include_root = 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), .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{
|
||||
.include_root = 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(@ptrCast(@alignCast(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(@ptrCast(@alignCast(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(@ptrCast(@alignCast(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" },
|
||||
}, .{});
|
||||
}
|
||||
1620
src/browser/html/elements.zig
Normal file
1620
src/browser/html/elements.zig
Normal file
File diff suppressed because it is too large
Load Diff
86
src/browser/html/error_event.zig
Normal file
86
src/browser/html/error_event.zig
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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" {
|
||||
try testing.htmlRunner("html/error_event.html");
|
||||
}
|
||||
38
src/browser/html/form.zig
Normal file
38
src/browser/html/form.zig
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||
const FormData = @import("../xhr/form_data.zig").FormData;
|
||||
|
||||
pub const HTMLFormElement = struct {
|
||||
pub const Self = parser.Form;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn _submit(self: *parser.Form, page: *Page) !void {
|
||||
return page.submitForm(self, null);
|
||||
}
|
||||
|
||||
pub fn _reset(self: *parser.Form) !void {
|
||||
try parser.formElementReset(self);
|
||||
}
|
||||
};
|
||||
@@ -18,16 +18,8 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const jsruntime = @import("jsruntime");
|
||||
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
||||
pub const History = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
const ScrollRestorationMode = enum {
|
||||
auto,
|
||||
manual,
|
||||
@@ -98,31 +90,31 @@ pub const History = struct {
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var history = [_]Case{
|
||||
.{ .src = "history.scrollRestoration", .ex = "auto" },
|
||||
.{ .src = "history.scrollRestoration = 'manual'", .ex = "manual" },
|
||||
.{ .src = "history.scrollRestoration = 'foo'", .ex = "foo" },
|
||||
.{ .src = "history.scrollRestoration", .ex = "manual" },
|
||||
.{ .src = "history.scrollRestoration = 'auto'", .ex = "auto" },
|
||||
.{ .src = "history.scrollRestoration", .ex = "auto" },
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.History" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
.{ .src = "history.state", .ex = "null" },
|
||||
try runner.testCases(&.{
|
||||
.{ "history.scrollRestoration", "auto" },
|
||||
.{ "history.scrollRestoration = 'manual'", "manual" },
|
||||
.{ "history.scrollRestoration = 'foo'", "foo" },
|
||||
.{ "history.scrollRestoration", "manual" },
|
||||
.{ "history.scrollRestoration = 'auto'", "auto" },
|
||||
.{ "history.scrollRestoration", "auto" },
|
||||
|
||||
.{ .src = "history.pushState({}, null, '')", .ex = "undefined" },
|
||||
.{ "history.state", "null" },
|
||||
|
||||
.{ .src = "history.replaceState({}, null, '')", .ex = "undefined" },
|
||||
.{ "history.pushState({}, null, '')", "undefined" },
|
||||
|
||||
.{ .src = "history.go()", .ex = "undefined" },
|
||||
.{ .src = "history.go(1)", .ex = "undefined" },
|
||||
.{ .src = "history.go(-1)", .ex = "undefined" },
|
||||
.{ "history.replaceState({}, null, '')", "undefined" },
|
||||
|
||||
.{ .src = "history.forward()", .ex = "undefined" },
|
||||
.{ "history.go()", "undefined" },
|
||||
.{ "history.go(1)", "undefined" },
|
||||
.{ "history.go(-1)", "undefined" },
|
||||
|
||||
.{ .src = "history.back()", .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &history);
|
||||
.{ "history.forward()", "undefined" },
|
||||
|
||||
.{ "history.back()", "undefined" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -16,22 +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;
|
||||
|
||||
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,
|
||||
};
|
||||
30
src/browser/html/iframe.zig
Normal file
30
src/browser/html/iframe.zig
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#htmliframeelement
|
||||
pub const HTMLIFrameElement = struct {
|
||||
pub const Self = parser.IFrame;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
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 {}
|
||||
};
|
||||
@@ -19,15 +19,9 @@
|
||||
const std = @import("std");
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const jsruntime = @import("jsruntime");
|
||||
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/system-state.html#navigator
|
||||
pub const Navigator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
agent: []const u8 = "Lightpanda/1.0",
|
||||
version: []const u8 = "1.0",
|
||||
vendor: []const u8 = "",
|
||||
@@ -89,14 +83,14 @@ pub const Navigator = struct {
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var navigator = [_]Case{
|
||||
.{ .src = "navigator.userAgent", .ex = "Lightpanda/1.0" },
|
||||
.{ .src = "navigator.appVersion", .ex = "1.0" },
|
||||
.{ .src = "navigator.language", .ex = "en-US" },
|
||||
};
|
||||
try checkCases(js_env, &navigator);
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
103
src/browser/html/screen.zig
Normal file
103
src/browser/html/screen.zig
Normal file
@@ -0,0 +1,103 @@
|
||||
// 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 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;
|
||||
|
||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .screen },
|
||||
|
||||
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,
|
||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .screen_orientation },
|
||||
|
||||
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" {
|
||||
try testing.htmlRunner("html/screen.html");
|
||||
}
|
||||
278
src/browser/html/select.zig
Normal file
278
src/browser/html/select.zig
Normal file
@@ -0,0 +1,278 @@
|
||||
// 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 collection = @import("../dom/html_collection.zig");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||
|
||||
pub const Interfaces = .{
|
||||
HTMLSelectElement,
|
||||
HTMLOptionElement,
|
||||
HTMLOptionsCollection,
|
||||
};
|
||||
|
||||
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(@ptrCast(@alignCast(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(@ptrCast(@alignCast(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);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_options(select: *parser.Select) HTMLOptionsCollection {
|
||||
return .{
|
||||
.select = select,
|
||||
.proto = collection.HTMLCollectionChildren(@ptrCast(@alignCast(select)), .{
|
||||
.mutable = true,
|
||||
.include_root = false,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLOptionElement = struct {
|
||||
pub const Self = parser.Option;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_value(self: *parser.Option) ![]const u8 {
|
||||
return parser.optionGetValue(self);
|
||||
}
|
||||
pub fn set_value(self: *parser.Option, value: []const u8) !void {
|
||||
return parser.optionSetValue(self, value);
|
||||
}
|
||||
|
||||
pub fn get_label(self: *parser.Option) ![]const u8 {
|
||||
return parser.optionGetLabel(self);
|
||||
}
|
||||
pub fn set_label(self: *parser.Option, label: []const u8) !void {
|
||||
return parser.optionSetLabel(self, label);
|
||||
}
|
||||
|
||||
pub fn get_selected(self: *parser.Option) !bool {
|
||||
return parser.optionGetSelected(self);
|
||||
}
|
||||
pub fn set_selected(self: *parser.Option, value: bool) !void {
|
||||
return parser.optionSetSelected(self, value);
|
||||
}
|
||||
|
||||
pub fn get_disabled(self: *parser.Option) !bool {
|
||||
return parser.optionGetDisabled(self);
|
||||
}
|
||||
pub fn set_disabled(self: *parser.Option, value: bool) !void {
|
||||
return parser.optionSetDisabled(self, value);
|
||||
}
|
||||
|
||||
pub fn get_text(self: *parser.Option) ![]const u8 {
|
||||
return parser.optionGetText(self);
|
||||
}
|
||||
|
||||
pub fn get_form(self: *parser.Option) !?*parser.Form {
|
||||
return parser.optionGetForm(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLOptionsCollection = struct {
|
||||
pub const prototype = *collection.HTMLCollection;
|
||||
|
||||
proto: collection.HTMLCollection,
|
||||
select: *parser.Select,
|
||||
|
||||
pub fn get_selectedIndex(self: *HTMLOptionsCollection, page: *Page) !i32 {
|
||||
return HTMLSelectElement.get_selectedIndex(self.select, page);
|
||||
}
|
||||
|
||||
pub fn set_selectedIndex(self: *HTMLOptionsCollection, index: i32, page: *Page) !void {
|
||||
return HTMLSelectElement.set_selectedIndex(self.select, index, page);
|
||||
}
|
||||
|
||||
const BeforeOpts = union(enum) {
|
||||
index: u32,
|
||||
option: *parser.Option,
|
||||
};
|
||||
pub fn _add(self: *HTMLOptionsCollection, option: *parser.Option, before_: ?BeforeOpts) !void {
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const before = before_ orelse {
|
||||
return self.appendOption(option);
|
||||
};
|
||||
|
||||
const insert_before: *parser.Node = switch (before) {
|
||||
.option => |o| @ptrCast(@alignCast(o)),
|
||||
.index => |i| (try self.proto.item(i)) orelse return self.appendOption(option),
|
||||
};
|
||||
return Node.before(insert_before, &.{
|
||||
.{ .node = @ptrCast(@alignCast(option)) },
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _remove(self: *HTMLOptionsCollection, index: u32) !void {
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const option = (try self.proto.item(index)) orelse return;
|
||||
_ = try Node._removeChild(@ptrCast(@alignCast(self.select)), option);
|
||||
}
|
||||
|
||||
fn appendOption(self: *HTMLOptionsCollection, option: *parser.Option) !void {
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
return Node.append(@ptrCast(@alignCast(self.select)), &.{
|
||||
.{ .node = @ptrCast(@alignCast(option)) },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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" },
|
||||
|
||||
.{ "let options = s.options", null },
|
||||
.{ "options.length", "2" },
|
||||
.{ "options.item(1).value", "o2" },
|
||||
.{ "options.selectedIndex", "-1" },
|
||||
|
||||
.{ "let o3 = document.createElement('option');", null },
|
||||
.{ "o3.value = 'o3';", null },
|
||||
.{ "options.add(o3)", null },
|
||||
.{ "options.length", "3" },
|
||||
.{ "options.item(2).value", "o3" },
|
||||
|
||||
.{ "let o4 = document.createElement('option');", null },
|
||||
.{ "o4.value = 'o4';", null },
|
||||
.{ "options.add(o4, 1)", null },
|
||||
.{ "options.length", "4" },
|
||||
.{ "options.item(1).value", "o4" },
|
||||
|
||||
.{ "let o5 = document.createElement('option');", null },
|
||||
.{ "o5.value = 'o5';", null },
|
||||
.{ "options.add(o5, o3)", null },
|
||||
.{ "options.length", "5" },
|
||||
.{ "options.item(3).value", "o5" },
|
||||
|
||||
.{ "options.remove(3)", null },
|
||||
.{ "options.length", "4" },
|
||||
.{ "options.item(3).value", "o3" },
|
||||
}, .{});
|
||||
}
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
442
src/browser/html/window.zig
Normal file
442
src/browser/html/window.zig
Normal file
@@ -0,0 +1,442 @@
|
||||
// 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 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/CSSStyleDeclaration.zig");
|
||||
const Screen = @import("screen.zig").Screen;
|
||||
const domcss = @import("../dom/css.zig");
|
||||
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, void) = .{},
|
||||
|
||||
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 = Performance.init(),
|
||||
};
|
||||
}
|
||||
|
||||
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.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;
|
||||
}
|
||||
|
||||
// frames return the window itself, but accessing it via a pseudo
|
||||
// array returns the Window object corresponding to the given frame or
|
||||
// iframe.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/frames
|
||||
pub fn get_frames(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn indexed_get(self: *Window, index: u32, has_value: *bool, page: *Page) !*Window {
|
||||
const frames = try domcss.querySelectorAll(
|
||||
page.call_arena,
|
||||
parser.documentHTMLToNode(self.document),
|
||||
"iframe",
|
||||
);
|
||||
|
||||
if (index >= frames.nodes.items.len) {
|
||||
has_value.* = false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
has_value.* = true;
|
||||
// TODO return the correct frame's window
|
||||
// frames.nodes.items[indexed]
|
||||
return error.TODO;
|
||||
}
|
||||
|
||||
// Retrieve the numbre of frames/iframes from the DOM dynamically.
|
||||
pub fn get_length(self: *const Window, page: *Page) !u32 {
|
||||
const frames = try domcss.querySelectorAll(
|
||||
page.call_arena,
|
||||
parser.documentHTMLToNode(self.document),
|
||||
"iframe",
|
||||
);
|
||||
|
||||
return frames.get_length();
|
||||
}
|
||||
|
||||
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,
|
||||
.name = "animationFrame",
|
||||
.low_priority = true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _cancelAnimationFrame(self: *Window, id: u32) !void {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
|
||||
}
|
||||
|
||||
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
|
||||
}
|
||||
|
||||
pub fn _clearTimeout(self: *Window, id: u32) !void {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _clearInterval(self: *Window, id: u32) !void {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
|
||||
}
|
||||
|
||||
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 {
|
||||
name: []const u8,
|
||||
args: []Env.JsObject = &.{},
|
||||
repeat: bool = false,
|
||||
animation_frame: bool = false,
|
||||
low_priority: bool = false,
|
||||
};
|
||||
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
|
||||
const delay = delay_ orelse 0;
|
||||
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;
|
||||
} else {
|
||||
gop.value_ptr.* = {};
|
||||
}
|
||||
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 callback = try arena.create(TimerCallback);
|
||||
callback.* = .{
|
||||
.cbk = cbk,
|
||||
.window = self,
|
||||
.timer_id = timer_id,
|
||||
.args = persisted_args,
|
||||
.animation_frame = opts.animation_frame,
|
||||
// setting a repeat time of 0 is illegal, doing + 1 is a simple way to avoid that
|
||||
.repeat = if (opts.repeat) delay + 1 else null,
|
||||
};
|
||||
|
||||
try page.scheduler.add(callback, TimerCallback.run, delay, .{
|
||||
.name = opts.name,
|
||||
.low_priority = opts.low_priority,
|
||||
});
|
||||
|
||||
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 id of our timer (windows.timers key)
|
||||
timer_id: u31,
|
||||
|
||||
// if false, we'll remove the timer_id from the window.timers lookup on run
|
||||
repeat: ?u32,
|
||||
|
||||
// The JavaScript callback to execute
|
||||
cbk: Function,
|
||||
|
||||
animation_frame: bool = false,
|
||||
|
||||
window: *Window,
|
||||
|
||||
args: []Env.JsObject = &.{},
|
||||
|
||||
fn run(ctx: *anyopaque) ?u32 {
|
||||
const self: *TimerCallback = @ptrCast(@alignCast(ctx));
|
||||
if (self.repeat != null) {
|
||||
if (self.window.timers.contains(self.timer_id) == false) {
|
||||
// it was called
|
||||
return null;
|
||||
}
|
||||
} else if (self.window.timers.remove(self.timer_id) == false) {
|
||||
// it was cancelled
|
||||
return null;
|
||||
}
|
||||
|
||||
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",
|
||||
});
|
||||
};
|
||||
|
||||
return self.repeat;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Window" {
|
||||
try testing.htmlRunner("window/window.html");
|
||||
try testing.htmlRunner("window/frames.html");
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user