mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-30 07:31:47 +00:00
Compare commits
756 Commits
url-set-pa
...
zigdom
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c966211481 | ||
|
|
5ae1190ddd | ||
|
|
fb9cce747d | ||
|
|
1a04ebce35 | ||
|
|
59bbfc4e06 | ||
|
|
d3973172e8 | ||
|
|
cdd31353c5 | ||
|
|
b047cb6dc1 | ||
|
|
c52dce1c48 | ||
|
|
0b4a1b4a1b | ||
|
|
cc0c1bcf3a | ||
|
|
55746f1a1d | ||
|
|
7bb8581a95 | ||
|
|
521c0f8460 | ||
|
|
4bfe3b6fe1 | ||
|
|
b610aa1c0c | ||
|
|
73da04bea2 | ||
|
|
18c851e53f | ||
|
|
41f4533bc0 | ||
|
|
4db8a967b6 | ||
|
|
ff70f4e79f | ||
|
|
c9517aff7d | ||
|
|
3657a49a2c | ||
|
|
71e7aa5262 | ||
|
|
2e435f5d4e | ||
|
|
859b03c4a6 | ||
|
|
ee8786444f | ||
|
|
d87d782fd5 | ||
|
|
afac4fc37f | ||
|
|
de83521e08 | ||
|
|
99f8fe1592 | ||
|
|
02c092a122 | ||
|
|
70ca74747f | ||
|
|
594d754022 | ||
|
|
c381e4153d | ||
|
|
e761c7e8f4 | ||
|
|
b8d4e3ac50 | ||
|
|
4c2b95d00b | ||
|
|
cea4f052ba | ||
|
|
9b4ea7a040 | ||
|
|
26c2b258b4 | ||
|
|
27c9e18535 | ||
|
|
b53c2bfa0c | ||
|
|
80605633c4 | ||
|
|
acf06fdd8f | ||
|
|
58cc5b4684 | ||
|
|
c502bd901e | ||
|
|
55027747fd | ||
|
|
f6d77afe2e | ||
|
|
cd9466dafa | ||
|
|
4bf79e4bc9 | ||
|
|
7afecf0f85 | ||
|
|
0b38b7d473 | ||
|
|
1b462da4aa | ||
|
|
07948304b2 | ||
|
|
0634acdac4 | ||
|
|
75e0637d2d | ||
|
|
852c30b2e5 | ||
|
|
dc85c6552a | ||
|
|
76e8506022 | ||
|
|
2d6e2551f6 | ||
|
|
080b1d9a7c | ||
|
|
fe008b0966 | ||
|
|
4ad10d057b | ||
|
|
a65aa9f312 | ||
|
|
5b43c16f35 | ||
|
|
9cb37dc011 | ||
|
|
2ba6737c41 | ||
|
|
33d737f957 | ||
|
|
381a18a40e | ||
|
|
207f0655dd | ||
|
|
88d64da257 | ||
|
|
cf378dfd6d | ||
|
|
a3939d9a66 | ||
|
|
ef363209a4 | ||
|
|
fe9a10c617 | ||
|
|
2e734fae57 | ||
|
|
432e3c3a5e | ||
|
|
a4b13a80ce | ||
|
|
a6997a7e85 | ||
|
|
a60d06af6b | ||
|
|
dab8012b6a | ||
|
|
66f82fd9cc | ||
|
|
0bff8ba632 | ||
|
|
32226297ab | ||
|
|
ab18c90b36 | ||
|
|
27b6fd561a | ||
|
|
15b64d5a25 | ||
|
|
08a50a8ada | ||
|
|
9d172bb29d | ||
|
|
c891322129 | ||
|
|
77434850f7 | ||
|
|
69b65dbd41 | ||
|
|
c335a545a3 | ||
|
|
5bcccec610 | ||
|
|
20ae9c3a53 | ||
|
|
92ca7c5a4b | ||
|
|
37fa41b4a2 | ||
|
|
298f959e13 | ||
|
|
1cb431f204 | ||
|
|
74dc7b278b | ||
|
|
b47d8a794c | ||
|
|
eaf845959c | ||
|
|
651521d346 | ||
|
|
fb37b29671 | ||
|
|
2ecf9016ba | ||
|
|
444b08be32 | ||
|
|
2b84712eee | ||
|
|
20cb6cdd8b | ||
|
|
477a5e5338 | ||
|
|
2a151229cb | ||
|
|
1d50e091c7 | ||
|
|
c587e380a0 | ||
|
|
54f9bfba84 | ||
|
|
489ba131c5 | ||
|
|
5eac1a146f | ||
|
|
d7ce6bdeff | ||
|
|
e88473d090 | ||
|
|
b9024ab032 | ||
|
|
98906be0f6 | ||
|
|
220775715d | ||
|
|
ecbf52157b | ||
|
|
a579977f66 | ||
|
|
418dc6fdc2 | ||
|
|
2aa4b03673 | ||
|
|
f236a65a79 | ||
|
|
f7b08a1160 | ||
|
|
eed10dd1bb | ||
|
|
9992bd0999 | ||
|
|
6912175e7e | ||
|
|
a59c32757e | ||
|
|
2438a0e60b | ||
|
|
a850a902ce | ||
|
|
b7ba993ba6 | ||
|
|
3eb0d57d5b | ||
|
|
6bf2ff9168 | ||
|
|
92226a8d06 | ||
|
|
134424dfdc | ||
|
|
58ceb66452 | ||
|
|
902b8fc789 | ||
|
|
923491a510 | ||
|
|
255b45d07b | ||
|
|
8f68b5b289 | ||
|
|
252fd78473 | ||
|
|
b692c5db60 | ||
|
|
eff7d58f4b | ||
|
|
17e9bdf8e8 | ||
|
|
22d2694b71 | ||
|
|
e74d7fa454 | ||
|
|
464f42a121 | ||
|
|
05e7079178 | ||
|
|
f03fcc9a31 | ||
|
|
c3ad054bb3 | ||
|
|
202e137d77 | ||
|
|
6b35664e37 | ||
|
|
1a7dbd56ac | ||
|
|
1a40853aae | ||
|
|
6bad2b16e4 | ||
|
|
db166b4633 | ||
|
|
71bc624a74 | ||
|
|
907a941795 | ||
|
|
559783eed7 | ||
|
|
68585c8837 | ||
|
|
eccbc9d9b3 | ||
|
|
e7d1d55170 | ||
|
|
f04754c254 | ||
|
|
a8e5a48b87 | ||
|
|
283a9af406 | ||
|
|
e3896455db | ||
|
|
5e6d2700a2 | ||
|
|
dfd0dfe0f6 | ||
|
|
e6b9be5020 | ||
|
|
6f7c87516f | ||
|
|
516a78326d | ||
|
|
853b7f84ef | ||
|
|
b248a2515e | ||
|
|
6826c42c65 | ||
|
|
4f041e48a3 | ||
|
|
ec6800500b | ||
|
|
856d65a8e9 | ||
|
|
8a2efde365 | ||
|
|
2ddcc6d9e6 | ||
|
|
25962326d2 | ||
|
|
bbc2fbf984 | ||
|
|
edc53d6de3 | ||
|
|
47710210bd | ||
|
|
823b7f0670 | ||
|
|
f5130ce48f | ||
|
|
347524a5b3 | ||
|
|
51830f5907 | ||
|
|
346f538c3b | ||
|
|
9d2948ff50 | ||
|
|
36ce227bf6 | ||
|
|
024f7ad9ef | ||
|
|
f8425fe614 | ||
|
|
7802a1b5a4 | ||
|
|
17549d8a43 | ||
|
|
f6ed706855 | ||
|
|
89ef25501b | ||
|
|
4870125e64 | ||
|
|
2d24e3c7f7 | ||
|
|
cdb3f46506 | ||
|
|
e225ed9f19 | ||
|
|
17bebf4f3a | ||
|
|
26550129ea | ||
|
|
66362c2762 | ||
|
|
f6f0e141a1 | ||
|
|
f22ee54bd8 | ||
|
|
2a969f911e | ||
|
|
2a0964f66b | ||
|
|
c553a2cd38 | ||
|
|
24330a7491 | ||
|
|
cd763a7a35 | ||
|
|
ed11eab0a7 | ||
|
|
a875ce4d68 | ||
|
|
969bfb4e53 | ||
|
|
76dae43103 | ||
|
|
af75ce79ac | ||
|
|
fe89c2ff9b | ||
|
|
bb2595eca5 | ||
|
|
618fff0191 | ||
|
|
9bbd06ce76 | ||
|
|
20463a662b | ||
|
|
9251180501 | ||
|
|
2659043afd | ||
|
|
7766892ad2 | ||
|
|
a7848f43cd | ||
|
|
cf8f76b454 | ||
|
|
f68f184c68 | ||
|
|
463440bce4 | ||
|
|
51ee313910 | ||
|
|
744b0bfff7 | ||
|
|
949479aa81 | ||
|
|
8743841145 | ||
|
|
6225cb38ae | ||
|
|
8dcba37672 | ||
|
|
38b922df75 | ||
|
|
6d884382a1 | ||
|
|
752e75e94b | ||
|
|
5ca41b5e13 | ||
|
|
1b3707ad33 | ||
|
|
c6e82d5af6 | ||
|
|
814e41122a | ||
|
|
a133a71eb9 | ||
|
|
dc2addb0ed | ||
|
|
f9014bb90c | ||
|
|
df0b6d5b07 | ||
|
|
56c6e8be06 | ||
|
|
b47b8297d6 | ||
|
|
5d1e17c598 | ||
|
|
94fe34bd10 | ||
|
|
e68ff62723 | ||
|
|
04487b6b91 | ||
|
|
49a27a67bc | ||
|
|
745de2ede2 | ||
|
|
82e5698f1d | ||
|
|
c4090851c5 | ||
|
|
9cb4431e89 | ||
|
|
2221d0cb6f | ||
|
|
5ea97c4910 | ||
|
|
a40590b4bf | ||
|
|
58acb2b821 | ||
|
|
6b9dc90639 | ||
|
|
b7d26cf0d5 | ||
|
|
59b4033ab2 | ||
|
|
13a7219dbd | ||
|
|
eae8a90a89 | ||
|
|
a87f4abd5f | ||
|
|
1b73691c69 | ||
|
|
e00066466b | ||
|
|
b87a8ba97d | ||
|
|
57aa270032 | ||
|
|
90a96fd8a7 | ||
|
|
c05470515f | ||
|
|
81ed4f3699 | ||
|
|
c9ac1eab11 | ||
|
|
1ba542fb3b | ||
|
|
4f127c9de3 | ||
|
|
16656f6c13 | ||
|
|
0f13e062fe | ||
|
|
2e68407fbe | ||
|
|
974f350f27 | ||
|
|
27ffea9052 | ||
|
|
9b2b35e8a2 | ||
|
|
3b51ca3947 | ||
|
|
62a2d08b53 | ||
|
|
e790bde717 | ||
|
|
0ab6b15292 | ||
|
|
2aeeb14c21 | ||
|
|
e5e57ab3bd | ||
|
|
f3ce5dcfbd | ||
|
|
bc341e98fc | ||
|
|
80851f4861 | ||
|
|
22b4456bce | ||
|
|
8d67502997 | ||
|
|
8f31fd778b | ||
|
|
f79f25bcf4 | ||
|
|
68e237eec5 | ||
|
|
8895c70c7f | ||
|
|
3964f8649d | ||
|
|
b7fb0ef1d3 | ||
|
|
66e403c5b4 | ||
|
|
0913abe806 | ||
|
|
6d3065c4c6 | ||
|
|
9092d1f8eb | ||
|
|
1bd1f123a3 | ||
|
|
44c072dcbb | ||
|
|
45c59e2990 | ||
|
|
75f0cd6e62 | ||
|
|
80f758018c | ||
|
|
b5e2c62fdd | ||
|
|
ede35718ae | ||
|
|
31fe2807aa | ||
|
|
f77693d768 | ||
|
|
96e3c16cca | ||
|
|
edd41b37f0 | ||
|
|
139d0038f2 | ||
|
|
d25fc64d7a | ||
|
|
9c83b268b9 | ||
|
|
42092ac16a | ||
|
|
e4860d5bae | ||
|
|
a5d9b658fb | ||
|
|
f464e89415 | ||
|
|
cdc439c4ef | ||
|
|
746168f9ed | ||
|
|
5ad4885102 | ||
|
|
7eb53ca2bc | ||
|
|
10fc056184 | ||
|
|
7517937155 | ||
|
|
a86fa8cc37 | ||
|
|
e1c765e78a | ||
|
|
56b08bddd8 | ||
|
|
2ed8a1c0ec | ||
|
|
389dff7031 | ||
|
|
123d69e595 | ||
|
|
4ab7fe26fc | ||
|
|
0aa1e0200f | ||
|
|
575f827958 | ||
|
|
7136851893 | ||
|
|
67935b11c9 | ||
|
|
85f60cbc7b | ||
|
|
9c35f8a24e | ||
|
|
9971de2ccd | ||
|
|
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 | ||
|
|
98cad6bf8d | ||
|
|
7e5daedc8c | ||
|
|
24ccfca279 | ||
|
|
34b3c3982b | ||
|
|
2cdc9e9f5f | ||
|
|
13c623755c | ||
|
|
bdfceec520 | ||
|
|
941dace7f9 |
24
.github/actions/install/action.yml
vendored
24
.github/actions/install/action.yml
vendored
@@ -5,7 +5,7 @@ inputs:
|
|||||||
zig:
|
zig:
|
||||||
description: 'Zig version to install'
|
description: 'Zig version to install'
|
||||||
required: false
|
required: false
|
||||||
default: '0.14.1'
|
default: '0.15.2'
|
||||||
arch:
|
arch:
|
||||||
description: 'CPU arch used to select the v8 lib'
|
description: 'CPU arch used to select the v8 lib'
|
||||||
required: false
|
required: false
|
||||||
@@ -17,11 +17,11 @@ inputs:
|
|||||||
zig-v8:
|
zig-v8:
|
||||||
description: 'zig v8 version to install'
|
description: 'zig v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
default: 'v0.1.27'
|
default: 'v0.1.33'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
default: '13.6.233.8'
|
default: '14.0.365.4'
|
||||||
cache-dir:
|
cache-dir:
|
||||||
description: 'cache dir to use'
|
description: 'cache dir to use'
|
||||||
required: false
|
required: false
|
||||||
@@ -67,9 +67,23 @@ runs:
|
|||||||
mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/
|
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
|
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a
|
||||||
|
|
||||||
- name: libiconv
|
- name: Cache libiconv
|
||||||
|
id: cache-libiconv
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-libiconv
|
||||||
|
with:
|
||||||
|
path: ${{ inputs.cache-dir }}/libiconv
|
||||||
|
key: vendor/libiconv/libiconv-1.17
|
||||||
|
|
||||||
|
- name: download libiconv
|
||||||
|
if: ${{ steps.cache-libiconv.outputs.cache-hit != 'true' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: make install-libiconv
|
run: make download-libiconv
|
||||||
|
|
||||||
|
- name: build libiconv
|
||||||
|
shell: bash
|
||||||
|
run: make build-libiconv
|
||||||
|
|
||||||
- name: build mimalloc
|
- name: build mimalloc
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
@@ -98,6 +98,8 @@ jobs:
|
|||||||
ARCH: aarch64
|
ARCH: aarch64
|
||||||
OS: macos
|
OS: macos
|
||||||
|
|
||||||
|
# macos-14 runs on arm CPU. see
|
||||||
|
# https://github.com/actions/runner-images?tab=readme-ov-file
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
@@ -136,7 +138,12 @@ jobs:
|
|||||||
ARCH: x86_64
|
ARCH: x86_64
|
||||||
OS: macos
|
OS: macos
|
||||||
|
|
||||||
runs-on: macos-14
|
# 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
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
23
.github/workflows/e2e-test.yml
vendored
23
.github/workflows/e2e-test.yml
vendored
@@ -93,9 +93,30 @@ jobs:
|
|||||||
- name: run end to end tests
|
- name: run end to end tests
|
||||||
run: |
|
run: |
|
||||||
./lightpanda serve & echo $! > LPD.pid
|
./lightpanda serve & echo $! > LPD.pid
|
||||||
go run runner/main.go --verbose
|
go run runner/main.go
|
||||||
kill `cat LPD.pid`
|
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`
|
||||||
|
|
||||||
|
- name: run request interception through proxy
|
||||||
|
run: |
|
||||||
|
export PROXY_USERNAME=username PROXY_PASSWORD=password
|
||||||
|
./proxy/proxy & echo $! > PROXY.id
|
||||||
|
./lightpanda serve & echo $! > LPD.pid
|
||||||
|
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
|
||||||
|
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
|
||||||
|
kill `cat LPD.pid` `cat PROXY.id`
|
||||||
|
|
||||||
cdp-and-hyperfine-bench:
|
cdp-and-hyperfine-bench:
|
||||||
name: cdp-and-hyperfine-bench
|
name: cdp-and-hyperfine-bench
|
||||||
needs: zig-build-release
|
needs: zig-build-release
|
||||||
|
|||||||
1
.github/workflows/wpt.yml
vendored
1
.github/workflows/wpt.yml
vendored
@@ -5,6 +5,7 @@ env:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
||||||
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
||||||
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
|
|||||||
2
.github/workflows/zig-fmt.yml
vendored
2
.github/workflows/zig-fmt.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: zig-fmt
|
name: zig-fmt
|
||||||
|
|
||||||
env:
|
env:
|
||||||
ZIG_VERSION: 0.14.1
|
ZIG_VERSION: 0.15.2
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,7 +1,6 @@
|
|||||||
zig-cache
|
|
||||||
/.zig-cache/
|
/.zig-cache/
|
||||||
zig-out
|
/zig-out/
|
||||||
/vendor/netsurf/out
|
|
||||||
/vendor/libiconv/
|
|
||||||
lightpanda.id
|
lightpanda.id
|
||||||
/v8/
|
/v8/
|
||||||
|
/build/
|
||||||
|
src/html5ever/target/
|
||||||
|
|||||||
33
.gitmodules
vendored
33
.gitmodules
vendored
@@ -1,21 +1,18 @@
|
|||||||
[submodule "vendor/netsurf/libwapcaplet"]
|
|
||||||
path = vendor/netsurf/libwapcaplet
|
|
||||||
url = https://github.com/lightpanda-io/libwapcaplet.git/
|
|
||||||
[submodule "vendor/netsurf/libparserutils"]
|
|
||||||
path = vendor/netsurf/libparserutils
|
|
||||||
url = https://github.com/lightpanda-io/libparserutils.git/
|
|
||||||
[submodule "vendor/netsurf/libdom"]
|
|
||||||
path = vendor/netsurf/libdom
|
|
||||||
url = https://github.com/lightpanda-io/libdom.git/
|
|
||||||
[submodule "vendor/netsurf/share/netsurf-buildsystem"]
|
|
||||||
path = vendor/netsurf/share/netsurf-buildsystem
|
|
||||||
url = https://source.netsurf-browser.org/buildsystem.git
|
|
||||||
[submodule "vendor/netsurf/libhubbub"]
|
|
||||||
path = vendor/netsurf/libhubbub
|
|
||||||
url = https://github.com/lightpanda-io/libhubbub.git/
|
|
||||||
[submodule "tests/wpt"]
|
[submodule "tests/wpt"]
|
||||||
path = tests/wpt
|
path = tests/wpt
|
||||||
url = https://github.com/lightpanda-io/wpt
|
url = https://github.com/lightpanda-io/wpt
|
||||||
[submodule "vendor/mimalloc"]
|
[submodule "vendor/nghttp2"]
|
||||||
path = vendor/mimalloc
|
path = vendor/nghttp2
|
||||||
url = https://github.com/microsoft/mimalloc.git/
|
url = https://github.com/nghttp2/nghttp2.git
|
||||||
|
[submodule "vendor/mbedtls"]
|
||||||
|
path = vendor/mbedtls
|
||||||
|
url = https://github.com/Mbed-TLS/mbedtls.git
|
||||||
|
[submodule "vendor/zlib"]
|
||||||
|
path = vendor/zlib
|
||||||
|
url = https://github.com/madler/zlib.git
|
||||||
|
[submodule "vendor/curl"]
|
||||||
|
path = vendor/curl
|
||||||
|
url = https://github.com/curl/curl.git
|
||||||
|
[submodule "vendor/brotli"]
|
||||||
|
path = vendor/brotli
|
||||||
|
url = https://github.com/google/brotli
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
FROM debian:stable
|
FROM debian:stable
|
||||||
|
|
||||||
ARG MINISIG=0.12
|
ARG MINISIG=0.12
|
||||||
ARG ZIG=0.14.1
|
ARG ZIG=0.15.2
|
||||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||||
ARG V8=13.6.233.8
|
ARG V8=14.0.365.4
|
||||||
ARG ZIG_V8=v0.1.27
|
ARG ZIG_V8=v0.1.33
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update -yq && \
|
RUN apt-get update -yq && \
|
||||||
|
|||||||
129
Makefile
129
Makefile
@@ -96,9 +96,16 @@ wpt-summary:
|
|||||||
@printf "\e[36mBuilding wpt...\e[0m\n"
|
@printf "\e[36mBuilding wpt...\e[0m\n"
|
||||||
@$(ZIG) build wpt -- --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 - `grep` is used to filter out the huge compile command on build
|
||||||
|
ifeq ($(OS), macos)
|
||||||
test:
|
test:
|
||||||
@TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
|
@script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \
|
||||||
|
| grep --line-buffered -v "^/.*zig test -freference-trace"
|
||||||
|
else
|
||||||
|
test:
|
||||||
|
@script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' /dev/null 2>&1 \
|
||||||
|
| grep --line-buffered -v "^/.*zig test -freference-trace"
|
||||||
|
endif
|
||||||
|
|
||||||
## Run demo/runner end to end tests
|
## Run demo/runner end to end tests
|
||||||
end2end:
|
end2end:
|
||||||
@@ -120,126 +127,24 @@ build-v8:
|
|||||||
|
|
||||||
# Install and build required dependencies commands
|
# Install and build required dependencies commands
|
||||||
# ------------
|
# ------------
|
||||||
.PHONY: install-submodule
|
.PHONY: install-html5ever install-html5ever-dev
|
||||||
.PHONY: install-libiconv
|
.PHONY: install install-dev
|
||||||
.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 and build dependencies for release
|
||||||
install: install-submodule install-libiconv install-netsurf install-mimalloc
|
install: install-submodule install-html5ever
|
||||||
|
|
||||||
## Install and build dependencies for dev
|
## Install and build dependencies for dev
|
||||||
install-dev: install-submodule install-libiconv install-netsurf-dev install-mimalloc-dev
|
install-dev: install-submodule install-html5ever-dev
|
||||||
|
|
||||||
install-netsurf-dev: _install-netsurf
|
install-html5ever:
|
||||||
install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
|
cd src/html5ever && cargo build --release --target-dir ../../build/html5ever/
|
||||||
|
|
||||||
install-netsurf: _install-netsurf
|
install-html5ever-dev:
|
||||||
install-netsurf: OPTCFLAGS := -DNDEBUG
|
cd src/html5ever && cargo build --target-dir ../../build/html5ever/
|
||||||
|
|
||||||
BC_NS := $(BC)vendor/netsurf/out/$(OS)-$(ARCH)
|
|
||||||
ICONV := $(BC)vendor/libiconv/out/$(OS)-$(ARCH)
|
|
||||||
# TODO: add Linux iconv path (I guess it depends on the distro)
|
|
||||||
# TODO: this way of linking libiconv is not ideal. We should have a more generic way
|
|
||||||
# and stick to a specif version. Maybe build from source. Anyway not now.
|
|
||||||
_install-netsurf: clean-netsurf
|
|
||||||
@printf "\e[36mInstalling NetSurf...\e[0m\n" && \
|
|
||||||
ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\e[33mERROR: you need to execute 'make install-libiconv'\e[0m\n"; exit 1;) && \
|
|
||||||
mkdir -p $(BC_NS) && \
|
|
||||||
cp -R vendor/netsurf/share $(BC_NS) && \
|
|
||||||
export PREFIX=$(BC_NS) && \
|
|
||||||
export OPTLDFLAGS="-L$(ICONV)/lib" && \
|
|
||||||
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
|
|
||||||
printf "\e[33mInstalling libwapcaplet...\e[0m\n" && \
|
|
||||||
cd vendor/netsurf/libwapcaplet && \
|
|
||||||
BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \
|
|
||||||
cd ../libparserutils && \
|
|
||||||
printf "\e[33mInstalling libparserutils...\e[0m\n" && \
|
|
||||||
BUILDDIR=$(BC_NS)/build/libparserutils make install && \
|
|
||||||
cd ../libhubbub && \
|
|
||||||
printf "\e[33mInstalling libhubbub...\e[0m\n" && \
|
|
||||||
BUILDDIR=$(BC_NS)/build/libhubbub make install && \
|
|
||||||
rm src/treebuilder/autogenerated-element-type.c && \
|
|
||||||
cd ../libdom && \
|
|
||||||
printf "\e[33mInstalling libdom...\e[0m\n" && \
|
|
||||||
BUILDDIR=$(BC_NS)/build/libdom make install && \
|
|
||||||
printf "\e[33mRunning libdom example...\e[0m\n" && \
|
|
||||||
cd examples && \
|
|
||||||
$(ZIG) cc \
|
|
||||||
-I$(ICONV)/include \
|
|
||||||
-I$(BC_NS)/include \
|
|
||||||
-L$(ICONV)/lib \
|
|
||||||
-L$(BC_NS)/lib \
|
|
||||||
-liconv \
|
|
||||||
-ldom \
|
|
||||||
-lhubbub \
|
|
||||||
-lparserutils \
|
|
||||||
-lwapcaplet \
|
|
||||||
-o a.out \
|
|
||||||
dom-structure-dump.c \
|
|
||||||
$(ICONV)/lib/libiconv.a && \
|
|
||||||
./a.out > /dev/null && \
|
|
||||||
rm a.out && \
|
|
||||||
printf "\e[36mDone NetSurf $(OS)\e[0m\n"
|
|
||||||
|
|
||||||
clean-netsurf:
|
|
||||||
@printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
|
|
||||||
rm -Rf $(BC_NS)
|
|
||||||
|
|
||||||
test-netsurf:
|
|
||||||
@printf "\e[36mTesting NetSurf...\e[0m\n" && \
|
|
||||||
export PREFIX=$(BC_NS) && \
|
|
||||||
export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \
|
|
||||||
export CFLAGS="-I$(ICONV)/include -I$(BC_NS)/include" && \
|
|
||||||
cd vendor/netsurf/libdom && \
|
|
||||||
BUILDDIR=$(BC_NS)/build/libdom make test
|
|
||||||
|
|
||||||
download-libiconv:
|
|
||||||
ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","")
|
|
||||||
@mkdir -p vendor/libiconv
|
|
||||||
@cd vendor/libiconv && \
|
|
||||||
curl https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.17.tar.gz | tar -xvzf -
|
|
||||||
endif
|
|
||||||
|
|
||||||
install-libiconv: download-libiconv clean-libiconv
|
|
||||||
@cd vendor/libiconv/libiconv-1.17 && \
|
|
||||||
./configure --prefix=$(ICONV) --enable-static && \
|
|
||||||
make && make install
|
|
||||||
|
|
||||||
clean-libiconv:
|
|
||||||
ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
|
|
||||||
@cd vendor/libiconv/libiconv-1.17 && \
|
|
||||||
make clean
|
|
||||||
endif
|
|
||||||
|
|
||||||
data:
|
data:
|
||||||
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
||||||
|
|
||||||
.PHONY: _build_mimalloc
|
|
||||||
|
|
||||||
MIMALLOC := $(BC)vendor/mimalloc/out/$(OS)-$(ARCH)
|
|
||||||
_build_mimalloc: clean-mimalloc
|
|
||||||
@mkdir -p $(MIMALLOC)/build && \
|
|
||||||
cd $(MIMALLOC)/build && \
|
|
||||||
cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) ../../.. && \
|
|
||||||
make && \
|
|
||||||
mkdir -p $(MIMALLOC)/lib
|
|
||||||
|
|
||||||
install-mimalloc-dev: _build_mimalloc
|
|
||||||
install-mimalloc-dev: OPTS=-DCMAKE_BUILD_TYPE=Debug
|
|
||||||
install-mimalloc-dev:
|
|
||||||
@cd $(MIMALLOC) && \
|
|
||||||
mv build/libmimalloc-debug.a lib/libmimalloc.a
|
|
||||||
|
|
||||||
install-mimalloc: _build_mimalloc
|
|
||||||
install-mimalloc:
|
|
||||||
@cd $(MIMALLOC) && \
|
|
||||||
mv build/libmimalloc.a lib/libmimalloc.a
|
|
||||||
|
|
||||||
clean-mimalloc:
|
|
||||||
@rm -Rf $(MIMALLOC)/build
|
|
||||||
|
|
||||||
## Init and update git submodule
|
## Init and update git submodule
|
||||||
install-submodule:
|
install-submodule:
|
||||||
@git submodule init && \
|
@git submodule init && \
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -71,9 +71,8 @@ Lightpanda provides [official Docker
|
|||||||
images](https://hub.docker.com/r/lightpanda/browser) for both Linux amd64 and
|
images](https://hub.docker.com/r/lightpanda/browser) for both Linux amd64 and
|
||||||
arm64 architectures.
|
arm64 architectures.
|
||||||
The following command fetches the Docker image and starts a new container exposing Lightpanda's CDP server on port `9222`.
|
The following command fetches the Docker image and starts a new container exposing Lightpanda's CDP server on port `9222`.
|
||||||
The `--privileged` option is required because the browser requires `io_uring` syscalls which are blocked by default by Docker.
|
|
||||||
```console
|
```console
|
||||||
docker run -d --name lightpanda -p 9222:9222 --privileged lightpanda/browser:nightly
|
docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dump a URL
|
### Dump a URL
|
||||||
@@ -141,8 +140,7 @@ You may still encounter errors or crashes. Please open an issue with specifics i
|
|||||||
|
|
||||||
Here are the key features we have implemented:
|
Here are the key features we have implemented:
|
||||||
|
|
||||||
- [x] HTTP loader
|
- [x] HTTP loader (based on Libcurl)
|
||||||
- [x] HTTP loader
|
|
||||||
- [x] HTML parser and DOM tree (based on Netsurf libs)
|
- [x] HTML parser and DOM tree (based on Netsurf libs)
|
||||||
- [x] Javascript support (v8)
|
- [x] Javascript support (v8)
|
||||||
- [x] DOM APIs
|
- [x] DOM APIs
|
||||||
@@ -155,8 +153,8 @@ Here are the key features we have implemented:
|
|||||||
- [x] Input form
|
- [x] Input form
|
||||||
- [x] Cookies
|
- [x] Cookies
|
||||||
- [x] Custom HTTP headers
|
- [x] Custom HTTP headers
|
||||||
- [ ] Proxy support
|
- [x] Proxy support
|
||||||
- [ ] Network interception
|
- [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.
|
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||||
|
|
||||||
@@ -166,11 +164,12 @@ You can also follow the progress of our Javascript support in our dedicated [zig
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.14.1`. You have to
|
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to
|
||||||
install it with the right version in order to build the project.
|
install it with the right version in order to build the project.
|
||||||
|
|
||||||
Lightpanda also depends on
|
Lightpanda also depends on
|
||||||
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
|
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
|
||||||
|
[Libcurl](https://curl.se/libcurl/),
|
||||||
[Netsurf libs](https://www.netsurf-browser.org/) and
|
[Netsurf libs](https://www.netsurf-browser.org/) and
|
||||||
[Mimalloc](https://microsoft.github.io/mimalloc).
|
[Mimalloc](https://microsoft.github.io/mimalloc).
|
||||||
|
|
||||||
|
|||||||
795
build.zig
795
build.zig
@@ -19,11 +19,13 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const Build = std.Build;
|
||||||
|
|
||||||
/// Do not rename this constant. It is scanned by some scripts to determine
|
/// Do not rename this constant. It is scanned by some scripts to determine
|
||||||
/// which zig version to install.
|
/// which zig version to install.
|
||||||
const recommended_zig_version = "0.14.1";
|
const recommended_zig_version = "0.15.2";
|
||||||
|
|
||||||
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)) {
|
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
||||||
.eq => {},
|
.eq => {},
|
||||||
.lt => {
|
.lt => {
|
||||||
@@ -47,32 +49,95 @@ pub fn build(b: *std.Build) !void {
|
|||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
{
|
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer");
|
||||||
// browser
|
const enable_csan = b.option(std.zig.SanitizeC, "csan", "Enable C Sanitizers");
|
||||||
// -------
|
|
||||||
|
|
||||||
// compile and install
|
const lightpanda_module = blk: {
|
||||||
const exe = b.addExecutable(.{
|
const mod = b.addModule("lightpanda", .{
|
||||||
.name = "lightpanda",
|
.root_source_file = b.path("src/lightpanda.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
.root_source_file = b.path("src/main.zig"),
|
.link_libc = true,
|
||||||
|
.link_libcpp = true,
|
||||||
|
.sanitize_c = enable_csan,
|
||||||
|
.sanitize_thread = enable_tsan,
|
||||||
});
|
});
|
||||||
|
|
||||||
try common(b, opts, exe);
|
try addDependencies(b, mod, opts);
|
||||||
|
|
||||||
|
if (optimize == .ReleaseFast or optimize == .ReleaseSmall) {
|
||||||
|
mod.addLibraryPath(b.path("build/html5ever/release"));
|
||||||
|
} else {
|
||||||
|
mod.addLibraryPath(b.path("build/html5ever/debug"));
|
||||||
|
}
|
||||||
|
mod.linkSystemLibrary("litefetch_html5ever", .{});
|
||||||
|
|
||||||
|
break :blk mod;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
// browser
|
||||||
|
const exe = b.addExecutable(.{
|
||||||
|
.name = "lightpanda",
|
||||||
|
.use_llvm = true,
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.sanitize_c = enable_csan,
|
||||||
|
.sanitize_thread = enable_tsan,
|
||||||
|
.imports = &.{
|
||||||
|
.{.name = "lightpanda", .module = lightpanda_module},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
|
|
||||||
// run
|
|
||||||
const run_cmd = b.addRunArtifact(exe);
|
const run_cmd = b.addRunArtifact(exe);
|
||||||
if (b.args) |args| {
|
if (b.args) |args| {
|
||||||
run_cmd.addArgs(args);
|
run_cmd.addArgs(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// step
|
|
||||||
const run_step = b.step("run", "Run the app");
|
const run_step = b.step("run", "Run the app");
|
||||||
run_step.dependOn(&run_cmd.step);
|
run_step.dependOn(&run_cmd.step);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// test
|
||||||
|
const tests = b.addTest(.{
|
||||||
|
.root_module = lightpanda_module,
|
||||||
|
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
|
||||||
|
});
|
||||||
|
const run_tests = b.addRunArtifact(tests);
|
||||||
|
const test_step = b.step("test", "Run unit tests");
|
||||||
|
test_step.dependOn(&run_tests.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// wpt
|
||||||
|
const exe = b.addExecutable(.{
|
||||||
|
.name = "lightpanda-wpt",
|
||||||
|
.use_llvm = true,
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/main_wpt.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.sanitize_c = enable_csan,
|
||||||
|
.sanitize_thread = enable_tsan,
|
||||||
|
.imports = &.{
|
||||||
|
.{.name = "lightpanda", .module = lightpanda_module},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
const run_cmd = b.addRunArtifact(exe);
|
||||||
|
if (b.args) |args| {
|
||||||
|
run_cmd.addArgs(args);
|
||||||
|
}
|
||||||
|
const run_step = b.step("wpt", "Run WPT tests");
|
||||||
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// get v8
|
// get v8
|
||||||
// -------
|
// -------
|
||||||
@@ -90,63 +155,18 @@ pub fn build(b: *std.Build) !void {
|
|||||||
const build_step = b.step("build-v8", "Build v8");
|
const build_step = b.step("build-v8", "Build v8");
|
||||||
build_step.dependOn(&build_v8.step);
|
build_step.dependOn(&build_v8.step);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
// tests
|
|
||||||
// ----
|
|
||||||
|
|
||||||
// compile
|
|
||||||
const tests = b.addTest(.{
|
|
||||||
.root_source_file = b.path("src/main.zig"),
|
|
||||||
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
try common(b, opts, tests);
|
|
||||||
|
|
||||||
const run_tests = b.addRunArtifact(tests);
|
|
||||||
if (b.args) |args| {
|
|
||||||
run_tests.addArgs(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
// step
|
|
||||||
const tests_step = b.step("test", "Run unit tests");
|
|
||||||
tests_step.dependOn(&run_tests.step);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// wpt
|
|
||||||
// -----
|
|
||||||
|
|
||||||
// compile and install
|
|
||||||
const wpt = b.addExecutable(.{
|
|
||||||
.name = "lightpanda-wpt",
|
|
||||||
.root_source_file = b.path("src/main_wpt.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
try common(b, opts, wpt);
|
|
||||||
|
|
||||||
// run
|
|
||||||
const wpt_cmd = b.addRunArtifact(wpt);
|
|
||||||
if (b.args) |args| {
|
|
||||||
wpt_cmd.addArgs(args);
|
|
||||||
}
|
|
||||||
// step
|
|
||||||
const wpt_step = b.step("wpt", "WPT tests");
|
|
||||||
wpt_step.dependOn(&wpt_cmd.step);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Compile) !void {
|
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !void {
|
||||||
const mod = step.root_module;
|
mod.addImport("build_config", opts.createModule());
|
||||||
const target = mod.resolved_target.?;
|
|
||||||
const optimize = mod.optimize.?;
|
|
||||||
const dep_opts = .{ .target = target, .optimize = optimize };
|
|
||||||
|
|
||||||
try moduleNetSurf(b, step, target);
|
const target = mod.resolved_target.?;
|
||||||
mod.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
|
const dep_opts = .{
|
||||||
mod.addImport("tigerbeetle-io", b.dependency("tigerbeetle_io", .{}).module("tigerbeetle_io"));
|
.target = target,
|
||||||
|
.optimize = mod.optimize.?,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod.addIncludePath(b.path("vendor/lightpanda"));
|
||||||
|
|
||||||
{
|
{
|
||||||
// v8
|
// v8
|
||||||
@@ -156,11 +176,7 @@ fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Co
|
|||||||
const v8_mod = b.dependency("v8", dep_opts).module("v8");
|
const v8_mod = b.dependency("v8", dep_opts).module("v8");
|
||||||
v8_mod.addOptions("default_exports", v8_opts);
|
v8_mod.addOptions("default_exports", v8_opts);
|
||||||
mod.addImport("v8", v8_mod);
|
mod.addImport("v8", v8_mod);
|
||||||
}
|
|
||||||
|
|
||||||
mod.link_libcpp = true;
|
|
||||||
|
|
||||||
{
|
|
||||||
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
|
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
|
||||||
const os = switch (target.result.os.tag) {
|
const os = switch (target.result.os.tag) {
|
||||||
.linux => "linux",
|
.linux => "linux",
|
||||||
@@ -181,7 +197,6 @@ fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Co
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
mod.addObjectFile(mod.owner.path(lib_path));
|
mod.addObjectFile(mod.owner.path(lib_path));
|
||||||
}
|
|
||||||
|
|
||||||
switch (target.result.os.tag) {
|
switch (target.result.os.tag) {
|
||||||
.macos => {
|
.macos => {
|
||||||
@@ -191,62 +206,594 @@ fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Co
|
|||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
mod.addImport("build_config", opts.createModule());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build.ResolvedTarget) !void {
|
|
||||||
const os = target.result.os.tag;
|
|
||||||
const arch = target.result.cpu.arch;
|
|
||||||
|
|
||||||
// iconv
|
|
||||||
const libiconv_lib_path = try std.fmt.allocPrint(
|
|
||||||
b.allocator,
|
|
||||||
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
|
|
||||||
.{ @tagName(os), @tagName(arch) },
|
|
||||||
);
|
|
||||||
const libiconv_include_path = try std.fmt.allocPrint(
|
|
||||||
b.allocator,
|
|
||||||
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
|
|
||||||
.{ @tagName(os), @tagName(arch) },
|
|
||||||
);
|
|
||||||
step.addObjectFile(b.path(libiconv_lib_path));
|
|
||||||
step.addIncludePath(b.path(libiconv_include_path));
|
|
||||||
|
|
||||||
{
|
{
|
||||||
// mimalloc
|
//curl
|
||||||
const mimalloc = "vendor/mimalloc";
|
{
|
||||||
const lib_path = try std.fmt.allocPrint(
|
const is_linux = target.result.os.tag == .linux;
|
||||||
b.allocator,
|
if (is_linux) {
|
||||||
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
|
mod.addCMacro("HAVE_LINUX_TCP_H", "1");
|
||||||
.{ @tagName(os), @tagName(arch) },
|
mod.addCMacro("HAVE_MSG_NOSIGNAL", "1");
|
||||||
);
|
mod.addCMacro("HAVE_GETHOSTBYNAME_R", "1");
|
||||||
step.addObjectFile(b.path(lib_path));
|
}
|
||||||
step.addIncludePath(b.path(mimalloc ++ "/include"));
|
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_BROTLI", "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");
|
||||||
}
|
}
|
||||||
|
|
||||||
// netsurf libs
|
try buildZlib(b, mod);
|
||||||
const ns = "vendor/netsurf";
|
try buildBrotli(b, mod);
|
||||||
const ns_include_path = try std.fmt.allocPrint(
|
try buildMbedtls(b, mod);
|
||||||
b.allocator,
|
try buildNghttp2(b, mod);
|
||||||
ns ++ "/out/{s}-{s}/include",
|
try buildCurl(b, mod);
|
||||||
.{ @tagName(os), @tagName(arch) },
|
|
||||||
);
|
|
||||||
step.addIncludePath(b.path(ns_include_path));
|
|
||||||
|
|
||||||
const libs: [4][]const u8 = .{
|
switch (target.result.os.tag) {
|
||||||
"libdom",
|
.macos => {
|
||||||
"libhubbub",
|
// needed for proxying on mac
|
||||||
"libparserutils",
|
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
|
||||||
"libwapcaplet",
|
mod.linkFramework("CoreFoundation", .{});
|
||||||
};
|
mod.linkFramework("SystemConfiguration", .{});
|
||||||
inline for (libs) |lib| {
|
},
|
||||||
const ns_lib_path = try std.fmt.allocPrint(
|
else => {},
|
||||||
b.allocator,
|
}
|
||||||
ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a",
|
|
||||||
.{ @tagName(os), @tagName(arch) },
|
|
||||||
);
|
|
||||||
step.addObjectFile(b.path(ns_lib_path));
|
|
||||||
step.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn buildZlib(b: *Build, m: *Build.Module) !void {
|
||||||
|
const zlib = b.addLibrary(.{
|
||||||
|
.name = "zlib",
|
||||||
|
.root_module = m,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 buildBrotli(b: *Build, m: *Build.Module) !void {
|
||||||
|
const brotli = b.addLibrary(.{
|
||||||
|
.name = "brotli",
|
||||||
|
.root_module = m,
|
||||||
|
});
|
||||||
|
|
||||||
|
const root = "vendor/brotli/c/";
|
||||||
|
brotli.addIncludePath(b.path(root ++ "include"));
|
||||||
|
brotli.addCSourceFiles(.{ .flags = &.{}, .files = &.{
|
||||||
|
root ++ "common/constants.c",
|
||||||
|
root ++ "common/context.c",
|
||||||
|
root ++ "common/dictionary.c",
|
||||||
|
root ++ "common/platform.c",
|
||||||
|
root ++ "common/shared_dictionary.c",
|
||||||
|
root ++ "common/transform.c",
|
||||||
|
root ++ "dec/bit_reader.c",
|
||||||
|
root ++ "dec/decode.c",
|
||||||
|
root ++ "dec/huffman.c",
|
||||||
|
root ++ "dec/prefix.c",
|
||||||
|
root ++ "dec/state.c",
|
||||||
|
root ++ "dec/static_init.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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,19 +4,10 @@
|
|||||||
.version = "0.0.0",
|
.version = "0.0.0",
|
||||||
.fingerprint = 0xda130f3af836cea0,
|
.fingerprint = 0xda130f3af836cea0,
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.tls = .{
|
|
||||||
.url = "https://github.com/ianic/tls.zig/archive/55845f755d9e2e821458ea55693f85c737cd0c7a.tar.gz",
|
|
||||||
.hash = "tls-0.1.0-ER2e0m43BQAshi8ixj1qf3w2u2lqKtXtkrxUJ4AGZDcl",
|
|
||||||
},
|
|
||||||
.tigerbeetle_io = .{
|
|
||||||
.url = "https://github.com/lightpanda-io/tigerbeetle-io/archive/61d9652f1a957b7f4db723ea6aa0ce9635e840ce.tar.gz",
|
|
||||||
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
|
|
||||||
},
|
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/dd087771378ea854452bcb010309fa9ffe5a9cac.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/305bb3706716d32d59b2ffa674731556caa1002b.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH66e8AwBL3O_A8yWQYQIyfMbKHFNVQr_NqM6YjU11",
|
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
|
||||||
},
|
},
|
||||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
//.v8 = .{ .path = "../zig-v8-fork" }
|
||||||
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
127
flake.lock
generated
127
flake.lock
generated
@@ -1,5 +1,21 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"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": {
|
"flake-utils": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
@@ -18,13 +34,52 @@
|
|||||||
"type": "github"
|
"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": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1748964450,
|
"lastModified": 1756822655,
|
||||||
"narHash": "sha256-ZouDiXkUk8mkMnah10QcoQ9Nu6UW6AFAHLScS3En6aI=",
|
"narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9ff500cd9e123f46c55855eca64beccead29b152",
|
"rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -37,7 +92,9 @@
|
|||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs",
|
||||||
|
"zigPkgs": "zigPkgs",
|
||||||
|
"zlsPkg": "zlsPkg"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"systems": {
|
"systems": {
|
||||||
@@ -54,6 +111,68 @@
|
|||||||
"repo": "default",
|
"repo": "default",
|
||||||
"type": "github"
|
"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",
|
"root": "root",
|
||||||
|
|||||||
22
flake.nix
22
flake.nix
@@ -3,20 +3,37 @@
|
|||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/release-25.05";
|
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";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{
|
{
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
|
zigPkgs,
|
||||||
|
zlsPkg,
|
||||||
flake-utils,
|
flake-utils,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
flake-utils.lib.eachDefaultSystem (
|
flake-utils.lib.eachDefaultSystem (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
|
overlays = [
|
||||||
|
(final: prev: {
|
||||||
|
zigpkgs = zigPkgs.packages.${prev.system};
|
||||||
|
zls = zlsPkg.packages.${prev.system}.default;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system overlays;
|
||||||
};
|
};
|
||||||
|
|
||||||
# We need crtbeginS.o for building.
|
# We need crtbeginS.o for building.
|
||||||
@@ -32,7 +49,7 @@
|
|||||||
targetPkgs =
|
targetPkgs =
|
||||||
pkgs: with pkgs; [
|
pkgs: with pkgs; [
|
||||||
# Build Tools
|
# Build Tools
|
||||||
zig
|
zigpkgs."0.15.2"
|
||||||
zls
|
zls
|
||||||
python3
|
python3
|
||||||
pkg-config
|
pkg-config
|
||||||
@@ -49,6 +66,7 @@
|
|||||||
glib.dev
|
glib.dev
|
||||||
glibc.dev
|
glibc.dev
|
||||||
zlib
|
zlib
|
||||||
|
zlib.dev
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|||||||
109
src/App.zig
Normal file
109
src/App.zig
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const log = @import("log.zig");
|
||||||
|
const Http = @import("http/Http.zig");
|
||||||
|
const Platform = @import("browser/js/Platform.zig");
|
||||||
|
|
||||||
|
const Notification = @import("Notification.zig");
|
||||||
|
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||||
|
|
||||||
|
// Container for global state / objects that various parts of the system
|
||||||
|
// might need.
|
||||||
|
const App = @This();
|
||||||
|
|
||||||
|
http: Http,
|
||||||
|
config: Config,
|
||||||
|
platform: Platform,
|
||||||
|
telemetry: Telemetry,
|
||||||
|
allocator: Allocator,
|
||||||
|
app_dir_path: ?[]const u8,
|
||||||
|
notification: *Notification,
|
||||||
|
|
||||||
|
pub const RunMode = enum {
|
||||||
|
help,
|
||||||
|
fetch,
|
||||||
|
serve,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Config = struct {
|
||||||
|
run_mode: RunMode,
|
||||||
|
tls_verify_host: bool = true,
|
||||||
|
http_proxy: ?[:0]const u8 = null,
|
||||||
|
proxy_bearer_token: ?[:0]const u8 = null,
|
||||||
|
http_timeout_ms: ?u31 = null,
|
||||||
|
http_connect_timeout_ms: ?u31 = null,
|
||||||
|
http_max_host_open: ?u8 = null,
|
||||||
|
http_max_concurrent: ?u8 = null,
|
||||||
|
user_agent: [:0]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, config: Config) !*App {
|
||||||
|
const app = try allocator.create(App);
|
||||||
|
errdefer allocator.destroy(app);
|
||||||
|
|
||||||
|
app.config = config;
|
||||||
|
app.allocator = allocator;
|
||||||
|
|
||||||
|
app.notification = try Notification.init(allocator, null);
|
||||||
|
errdefer app.notification.deinit();
|
||||||
|
|
||||||
|
app.http = try Http.init(allocator, .{
|
||||||
|
.max_host_open = config.http_max_host_open orelse 4,
|
||||||
|
.max_concurrent = config.http_max_concurrent orelse 10,
|
||||||
|
.timeout_ms = config.http_timeout_ms orelse 5000,
|
||||||
|
.connect_timeout_ms = config.http_connect_timeout_ms orelse 0,
|
||||||
|
.http_proxy = config.http_proxy,
|
||||||
|
.tls_verify_host = config.tls_verify_host,
|
||||||
|
.proxy_bearer_token = config.proxy_bearer_token,
|
||||||
|
.user_agent = config.user_agent,
|
||||||
|
});
|
||||||
|
errdefer app.http.deinit();
|
||||||
|
|
||||||
|
app.platform = try Platform.init();
|
||||||
|
errdefer app.platform.deinit();
|
||||||
|
|
||||||
|
app.app_dir_path = getAndMakeAppDir(allocator);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
386
src/Notification.zig
Normal file
386
src/Notification.zig
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const log = @import("log.zig");
|
||||||
|
const Page = @import("browser/Page.zig");
|
||||||
|
const Transfer = @import("http/Client.zig").Transfer;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const List = std.DoublyLinkedList;
|
||||||
|
|
||||||
|
// Allows code to register for and emit events.
|
||||||
|
// Keeps two lists
|
||||||
|
// 1 - for a given event type, a linked list of all the listeners
|
||||||
|
// 2 - for a given listener, a list of all it's registration
|
||||||
|
// The 2nd one is so that a listener can unregister all of it's listeners
|
||||||
|
// (there's currently no need for a listener to unregister only 1 or more
|
||||||
|
// specific listener).
|
||||||
|
//
|
||||||
|
// Scoping is important. Imagine we created a global singleton registry, and our
|
||||||
|
// CDP code registers for the "network_bytes_sent" event, because it needs to
|
||||||
|
// send messages to the client when this happens. Our HTTP client could then
|
||||||
|
// emit a "network_bytes_sent" message. It would be easy, and it would work.
|
||||||
|
// That is, it would work until the Telemetry code makes an HTTP request, and
|
||||||
|
// because everything's just one big global, that gets picked up by the
|
||||||
|
// registered CDP listener, and the telemetry network activity gets sent to the
|
||||||
|
// CDP client.
|
||||||
|
//
|
||||||
|
// To avoid this, one way or another, we need scoping. We could still have
|
||||||
|
// a global registry but every "register" and every "emit" has some type of
|
||||||
|
// "scope". This would have a run-time cost and still require some coordination
|
||||||
|
// between components to share a common scope.
|
||||||
|
//
|
||||||
|
// Instead, the approach that we take is to have a notification instance per
|
||||||
|
// scope. This makes some things harder, but we only plan on having 2
|
||||||
|
// notification instances at a given time: one in a Browser and one in the App.
|
||||||
|
// What about something like Telemetry, which lives outside of a Browser but
|
||||||
|
// still cares about Browser-events (like .page_navigate)? When the Browser
|
||||||
|
// notification is created, a `notification_created` event is raised in the
|
||||||
|
// App's notification, which Telemetry is registered for. This allows Telemetry
|
||||||
|
// to register for events in the Browser notification. See the Telemetry's
|
||||||
|
// register function.
|
||||||
|
const Notification = @This();
|
||||||
|
// Every event type (which are hard-coded), has a list of Listeners.
|
||||||
|
// When the event happens, we dispatch to those listener.
|
||||||
|
event_listeners: EventListeners,
|
||||||
|
|
||||||
|
// list of listeners for a specified receiver
|
||||||
|
// @intFromPtr(receiver) -> [listener1, listener2, ...]
|
||||||
|
// Used when `unregisterAll` is called.
|
||||||
|
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayListUnmanaged(*Listener)),
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
mem_pool: std.heap.MemoryPool(Listener),
|
||||||
|
|
||||||
|
const EventListeners = struct {
|
||||||
|
page_remove: List = .{},
|
||||||
|
page_created: List = .{},
|
||||||
|
page_navigate: List = .{},
|
||||||
|
page_navigated: List = .{},
|
||||||
|
page_network_idle: List = .{},
|
||||||
|
page_network_almost_idle: List = .{},
|
||||||
|
http_request_fail: List = .{},
|
||||||
|
http_request_start: List = .{},
|
||||||
|
http_request_intercept: List = .{},
|
||||||
|
http_request_done: List = .{},
|
||||||
|
http_request_auth_required: List = .{},
|
||||||
|
http_response_data: List = .{},
|
||||||
|
http_response_header_done: List = .{},
|
||||||
|
notification_created: List = .{},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Events = union(enum) {
|
||||||
|
page_remove: PageRemove,
|
||||||
|
page_created: *Page,
|
||||||
|
page_navigate: *const PageNavigate,
|
||||||
|
page_navigated: *const PageNavigated,
|
||||||
|
page_network_idle: *const PageNetworkIdle,
|
||||||
|
page_network_almost_idle: *const PageNetworkAlmostIdle,
|
||||||
|
http_request_fail: *const RequestFail,
|
||||||
|
http_request_start: *const RequestStart,
|
||||||
|
http_request_intercept: *const RequestIntercept,
|
||||||
|
http_request_auth_required: *const RequestAuthRequired,
|
||||||
|
http_request_done: *const RequestDone,
|
||||||
|
http_response_data: *const ResponseData,
|
||||||
|
http_response_header_done: *const ResponseHeaderDone,
|
||||||
|
notification_created: *Notification,
|
||||||
|
};
|
||||||
|
const EventType = std.meta.FieldEnum(Events);
|
||||||
|
|
||||||
|
pub const PageRemove = struct {};
|
||||||
|
|
||||||
|
pub const PageNavigate = struct {
|
||||||
|
timestamp: u64,
|
||||||
|
url: []const u8,
|
||||||
|
opts: Page.NavigateOpts,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PageNavigated = struct {
|
||||||
|
timestamp: u64,
|
||||||
|
url: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PageNetworkIdle = struct {
|
||||||
|
timestamp: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PageNetworkAlmostIdle = struct {
|
||||||
|
timestamp: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestStart = struct {
|
||||||
|
transfer: *Transfer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestIntercept = struct {
|
||||||
|
transfer: *Transfer,
|
||||||
|
wait_for_interception: *bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestAuthRequired = struct {
|
||||||
|
transfer: *Transfer,
|
||||||
|
wait_for_interception: *bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ResponseData = struct {
|
||||||
|
data: []const u8,
|
||||||
|
transfer: *Transfer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ResponseHeaderDone = struct {
|
||||||
|
transfer: *Transfer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestDone = struct {
|
||||||
|
transfer: *Transfer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestFail = struct {
|
||||||
|
transfer: *Transfer,
|
||||||
|
err: anyerror,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, parent: ?*Notification) !*Notification {
|
||||||
|
|
||||||
|
// This is put on the heap because we want to raise a .notification_created
|
||||||
|
// event, so that, something like Telemetry, can receive the
|
||||||
|
// .page_navigate event on all notification instances. That can only work
|
||||||
|
// if we dispatch .notification_created with a *Notification.
|
||||||
|
const notification = try allocator.create(Notification);
|
||||||
|
errdefer allocator.destroy(notification);
|
||||||
|
|
||||||
|
notification.* = .{
|
||||||
|
.listeners = .{},
|
||||||
|
.event_listeners = .{},
|
||||||
|
.allocator = allocator,
|
||||||
|
.mem_pool = std.heap.MemoryPool(Listener).init(allocator),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (parent) |pn| {
|
||||||
|
pn.dispatch(.notification_created, notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Notification) void {
|
||||||
|
const allocator = self.allocator;
|
||||||
|
|
||||||
|
var it = self.listeners.valueIterator();
|
||||||
|
while (it.next()) |listener| {
|
||||||
|
listener.deinit(allocator);
|
||||||
|
}
|
||||||
|
self.listeners.deinit(allocator);
|
||||||
|
self.mem_pool.deinit();
|
||||||
|
allocator.destroy(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(self: *Notification, comptime event: EventType, receiver: anytype, func: EventFunc(event)) !void {
|
||||||
|
var list = &@field(self.event_listeners, @tagName(event));
|
||||||
|
|
||||||
|
var listener = try self.mem_pool.create();
|
||||||
|
errdefer self.mem_pool.destroy(listener);
|
||||||
|
|
||||||
|
listener.* = .{
|
||||||
|
.node = .{},
|
||||||
|
.list = list,
|
||||||
|
.receiver = receiver,
|
||||||
|
.event = event,
|
||||||
|
.func = @ptrCast(func),
|
||||||
|
.struct_name = @typeName(@typeInfo(@TypeOf(receiver)).pointer.child),
|
||||||
|
};
|
||||||
|
|
||||||
|
const allocator = self.allocator;
|
||||||
|
const gop = try self.listeners.getOrPut(allocator, @intFromPtr(receiver));
|
||||||
|
if (gop.found_existing == false) {
|
||||||
|
gop.value_ptr.* = .{};
|
||||||
|
}
|
||||||
|
try gop.value_ptr.append(allocator, listener);
|
||||||
|
|
||||||
|
// we don't add this until we've successfully added the entry to
|
||||||
|
// self.listeners
|
||||||
|
list.append(&listener.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregister(self: *Notification, comptime event: EventType, receiver: anytype) void {
|
||||||
|
var listeners = self.listeners.getPtr(@intFromPtr(receiver)) orelse return;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < listeners.items.len) {
|
||||||
|
const listener = listeners.items[i];
|
||||||
|
if (listener.event != event) {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
listener.list.remove(&listener.node);
|
||||||
|
self.mem_pool.destroy(listener);
|
||||||
|
_ = listeners.swapRemove(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listeners.items.len == 0) {
|
||||||
|
listeners.deinit(self.allocator);
|
||||||
|
const removed = self.listeners.remove(@intFromPtr(receiver));
|
||||||
|
std.debug.assert(removed == true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregisterAll(self: *Notification, receiver: *anyopaque) void {
|
||||||
|
var kv = self.listeners.fetchRemove(@intFromPtr(receiver)) orelse return;
|
||||||
|
for (kv.value.items) |listener| {
|
||||||
|
listener.list.remove(&listener.node);
|
||||||
|
self.mem_pool.destroy(listener);
|
||||||
|
}
|
||||||
|
kv.value.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch(self: *Notification, comptime event: EventType, data: ArgType(event)) void {
|
||||||
|
const list = &@field(self.event_listeners, @tagName(event));
|
||||||
|
|
||||||
|
var node = list.first;
|
||||||
|
while (node) |n| {
|
||||||
|
const listener: *Listener = @fieldParentPtr("node", n);
|
||||||
|
const func: EventFunc(event) = @ptrCast(@alignCast(listener.func));
|
||||||
|
func(listener.receiver, data) catch |err| {
|
||||||
|
log.err(.app, "dispatch error", .{
|
||||||
|
.err = err,
|
||||||
|
.event = event,
|
||||||
|
.source = "notification",
|
||||||
|
.listener = listener.struct_name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
node = n.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given an event type enum, returns the type of arg the event emits
|
||||||
|
fn ArgType(comptime event: Notification.EventType) type {
|
||||||
|
inline for (std.meta.fields(Notification.Events)) |f| {
|
||||||
|
if (std.mem.eql(u8, f.name, @tagName(event))) {
|
||||||
|
return f.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given an event type enum, returns the listening function type
|
||||||
|
fn EventFunc(comptime event: Notification.EventType) type {
|
||||||
|
return *const fn (*anyopaque, ArgType(event)) anyerror!void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A listener. This is 1 receiver, with its function, and the linked list
|
||||||
|
// node that goes in the appropriate EventListeners list.
|
||||||
|
const Listener = struct {
|
||||||
|
// the receiver of the event, i.e. the self parameter to `func`
|
||||||
|
receiver: *anyopaque,
|
||||||
|
|
||||||
|
// the function to call
|
||||||
|
func: *const anyopaque,
|
||||||
|
|
||||||
|
// For logging slightly better error
|
||||||
|
struct_name: []const u8,
|
||||||
|
|
||||||
|
event: Notification.EventType,
|
||||||
|
|
||||||
|
// intrusive linked list node
|
||||||
|
node: List.Node,
|
||||||
|
|
||||||
|
// The event list this listener belongs to.
|
||||||
|
// We need this in order to be able to remove the node from the list
|
||||||
|
list: *List,
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
test "Notification" {
|
||||||
|
var notifier = try Notification.init(testing.allocator, null);
|
||||||
|
defer notifier.deinit();
|
||||||
|
|
||||||
|
// noop
|
||||||
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.timestamp = 4,
|
||||||
|
.url = undefined,
|
||||||
|
.opts = .{},
|
||||||
|
});
|
||||||
|
|
||||||
|
var tc = TestClient{};
|
||||||
|
|
||||||
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.timestamp = 4,
|
||||||
|
.url = undefined,
|
||||||
|
.opts = .{},
|
||||||
|
});
|
||||||
|
try testing.expectEqual(4, tc.page_navigate);
|
||||||
|
|
||||||
|
notifier.unregisterAll(&tc);
|
||||||
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.timestamp = 10,
|
||||||
|
.url = undefined,
|
||||||
|
.opts = .{},
|
||||||
|
});
|
||||||
|
try testing.expectEqual(4, tc.page_navigate);
|
||||||
|
|
||||||
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
|
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
||||||
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.timestamp = 10,
|
||||||
|
.url = undefined,
|
||||||
|
.opts = .{},
|
||||||
|
});
|
||||||
|
notifier.dispatch(.page_navigated, &.{ .timestamp = 6, .url = undefined });
|
||||||
|
try testing.expectEqual(14, tc.page_navigate);
|
||||||
|
try testing.expectEqual(6, tc.page_navigated);
|
||||||
|
|
||||||
|
notifier.unregisterAll(&tc);
|
||||||
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.timestamp = 100,
|
||||||
|
.url = undefined,
|
||||||
|
.opts = .{},
|
||||||
|
});
|
||||||
|
notifier.dispatch(.page_navigated, &.{ .timestamp = 100, .url = undefined });
|
||||||
|
try testing.expectEqual(14, tc.page_navigate);
|
||||||
|
try testing.expectEqual(6, tc.page_navigated);
|
||||||
|
|
||||||
|
{
|
||||||
|
// unregister
|
||||||
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
|
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
||||||
|
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
|
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
|
||||||
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
|
try testing.expectEqual(1006, tc.page_navigated);
|
||||||
|
|
||||||
|
notifier.unregister(.page_navigate, &tc);
|
||||||
|
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
|
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
|
||||||
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
|
try testing.expectEqual(2006, tc.page_navigated);
|
||||||
|
|
||||||
|
notifier.unregister(.page_navigated, &tc);
|
||||||
|
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
|
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
|
||||||
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
|
try testing.expectEqual(2006, tc.page_navigated);
|
||||||
|
|
||||||
|
// already unregistered, try anyways
|
||||||
|
notifier.unregister(.page_navigated, &tc);
|
||||||
|
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
|
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
|
||||||
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
|
try testing.expectEqual(2006, tc.page_navigated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestClient = struct {
|
||||||
|
page_navigate: u64 = 0,
|
||||||
|
page_navigated: u64 = 0,
|
||||||
|
|
||||||
|
fn pageNavigate(ptr: *anyopaque, data: *const Notification.PageNavigate) !void {
|
||||||
|
const self: *TestClient = @ptrCast(@alignCast(ptr));
|
||||||
|
self.page_navigate += data.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pageNavigated(ptr: *anyopaque, data: *const Notification.PageNavigated) !void {
|
||||||
|
const self: *TestClient = @ptrCast(@alignCast(ptr));
|
||||||
|
self.page_navigated += data.timestamp;
|
||||||
|
}
|
||||||
|
};
|
||||||
88
src/Scheduler.zig
Normal file
88
src/Scheduler.zig
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const log = @import("log.zig");
|
||||||
|
|
||||||
|
const timestamp = @import("datetime.zig").milliTimestamp;
|
||||||
|
|
||||||
|
const Queue = std.PriorityQueue(Task, void, struct {
|
||||||
|
fn compare(_: void, a: Task, b: Task) std.math.Order {
|
||||||
|
return std.math.order(a.run_at, b.run_at);
|
||||||
|
}
|
||||||
|
}.compare);
|
||||||
|
|
||||||
|
const Scheduler = @This();
|
||||||
|
|
||||||
|
low_priority: Queue,
|
||||||
|
high_priority: Queue,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator) Scheduler {
|
||||||
|
return .{
|
||||||
|
.low_priority = Queue.init(allocator, {}),
|
||||||
|
.high_priority = Queue.init(allocator, {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Scheduler) void {
|
||||||
|
self.low_priority.cap = 0;
|
||||||
|
self.low_priority.items.len = 0;
|
||||||
|
|
||||||
|
self.high_priority.cap = 0;
|
||||||
|
self.high_priority.items.len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddOpts = struct {
|
||||||
|
name: []const u8 = "",
|
||||||
|
low_priority: bool = false,
|
||||||
|
};
|
||||||
|
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
|
||||||
|
log.debug(.scheduler, "scheduler.add", .{ .name = opts.name, .run_in_ms = run_in_ms, .low_priority = opts.low_priority });
|
||||||
|
var queue = if (opts.low_priority) &self.low_priority else &self.high_priority;
|
||||||
|
return queue.add(.{
|
||||||
|
.ctx = ctx,
|
||||||
|
.callback = cb,
|
||||||
|
.name = opts.name,
|
||||||
|
.run_at = timestamp(.monotonic) + run_in_ms,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(self: *Scheduler) !?u64 {
|
||||||
|
_ = try self.runQueue(&self.low_priority);
|
||||||
|
return self.runQueue(&self.high_priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
||||||
|
if (queue.count() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = timestamp(.monotonic);
|
||||||
|
|
||||||
|
while (queue.peek()) |*task_| {
|
||||||
|
if (task_.run_at > now) {
|
||||||
|
return @intCast(task_.run_at - now);
|
||||||
|
}
|
||||||
|
var task = queue.remove();
|
||||||
|
log.debug(.scheduler, "scheduler.runTask", .{ .name = task.name });
|
||||||
|
|
||||||
|
const repeat_in_ms = task.callback(task.ctx) catch |err| {
|
||||||
|
log.warn(.scheduler, "task.callback", .{ .name = task.name, .err = err });
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (repeat_in_ms) |ms| {
|
||||||
|
// Task cannot be repeated immediately, and they should know that
|
||||||
|
std.debug.assert(ms != 0);
|
||||||
|
task.run_at = now + ms;
|
||||||
|
try self.low_priority.add(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Task = struct {
|
||||||
|
run_at: u64,
|
||||||
|
ctx: *anyopaque,
|
||||||
|
name: []const u8,
|
||||||
|
callback: Callback,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
|
||||||
@@ -26,131 +26,170 @@ const Allocator = std.mem.Allocator;
|
|||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
const IO = @import("runtime/loop.zig").IO;
|
const App = @import("App.zig");
|
||||||
const Completion = IO.Completion;
|
|
||||||
const AcceptError = IO.AcceptError;
|
|
||||||
const RecvError = IO.RecvError;
|
|
||||||
const SendError = IO.SendError;
|
|
||||||
const TimeoutError = IO.TimeoutError;
|
|
||||||
const Loop = @import("runtime/loop.zig").Loop;
|
|
||||||
|
|
||||||
const App = @import("app.zig").App;
|
|
||||||
const CDP = @import("cdp/cdp.zig").CDP;
|
const CDP = @import("cdp/cdp.zig").CDP;
|
||||||
|
|
||||||
const TimeoutCheck = std.time.ns_per_ms * 100;
|
|
||||||
|
|
||||||
const MAX_HTTP_REQUEST_SIZE = 4096;
|
const MAX_HTTP_REQUEST_SIZE = 4096;
|
||||||
|
|
||||||
// max message size
|
// max message size
|
||||||
// +14 for max websocket payload overhead
|
// +14 for max websocket payload overhead
|
||||||
// +140 for the max control packet that might be interleaved in a message
|
// +140 for the max control packet that might be interleaved in a message
|
||||||
const MAX_MESSAGE_SIZE = 512 * 1024 + 14;
|
const MAX_MESSAGE_SIZE = 512 * 1024 + 14 + 140;
|
||||||
|
|
||||||
const Server = struct {
|
const Server = @This();
|
||||||
app: *App,
|
app: *App,
|
||||||
allocator: Allocator,
|
shutdown: bool,
|
||||||
loop: *Loop,
|
allocator: Allocator,
|
||||||
|
client: ?posix.socket_t,
|
||||||
|
listener: ?posix.socket_t,
|
||||||
|
json_version_response: []const u8,
|
||||||
|
|
||||||
// internal fields
|
pub fn init(app: *App, address: net.Address) !Server {
|
||||||
listener: posix.socket_t,
|
const allocator = app.allocator;
|
||||||
timeout: u64,
|
const json_version_response = try buildJSONVersionResponse(allocator, address);
|
||||||
|
errdefer allocator.free(json_version_response);
|
||||||
|
|
||||||
// I/O fields
|
return .{
|
||||||
accept_completion: Completion,
|
.app = app,
|
||||||
|
.client = null,
|
||||||
// The response to send on a GET /json/version request
|
.listener = null,
|
||||||
json_version_response: []const u8,
|
.shutdown = false,
|
||||||
|
.allocator = allocator,
|
||||||
fn deinit(self: *Server) void {
|
.json_version_response = json_version_response,
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn queueAccept(self: *Server) void {
|
|
||||||
log.debug(.app, "accepting connection", .{});
|
|
||||||
self.loop.io.accept(
|
|
||||||
*Server,
|
|
||||||
self,
|
|
||||||
callbackAccept,
|
|
||||||
&self.accept_completion,
|
|
||||||
self.listener,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn callbackAccept(
|
|
||||||
self: *Server,
|
|
||||||
completion: *Completion,
|
|
||||||
result: AcceptError!posix.socket_t,
|
|
||||||
) void {
|
|
||||||
std.debug.assert(completion == &self.accept_completion);
|
|
||||||
self.doCallbackAccept(result) catch |err| {
|
|
||||||
log.err(.app, "server accept error", .{ .err = err });
|
|
||||||
self.queueAccept();
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Server) void {
|
||||||
|
self.shutdown = true;
|
||||||
|
if (self.listener) |listener| {
|
||||||
|
posix.close(listener);
|
||||||
|
}
|
||||||
|
// *if* server.run is running, we should really wait for it to return
|
||||||
|
// before existing from here.
|
||||||
|
self.allocator.free(self.json_version_response);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(self: *Server, address: net.Address, timeout_ms: u32) !void {
|
||||||
|
const flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC;
|
||||||
|
const listener = try posix.socket(address.any.family, flags, posix.IPPROTO.TCP);
|
||||||
|
self.listener = listener;
|
||||||
|
|
||||||
|
try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
|
||||||
|
if (@hasDecl(posix.TCP, "NODELAY")) {
|
||||||
|
try posix.setsockopt(listener, posix.IPPROTO.TCP, posix.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn doCallbackAccept(
|
try posix.bind(listener, &address.any, address.getOsSockLen());
|
||||||
self: *Server,
|
try posix.listen(listener, 1);
|
||||||
result: AcceptError!posix.socket_t,
|
|
||||||
) !void {
|
log.info(.app, "server running", .{ .address = address });
|
||||||
const socket = try result;
|
while (true) {
|
||||||
const client = try self.allocator.create(Client);
|
const socket = posix.accept(listener, null, null, posix.SOCK.NONBLOCK) catch |err| {
|
||||||
client.* = Client.init(socket, self);
|
if (self.shutdown) {
|
||||||
client.start();
|
return;
|
||||||
|
}
|
||||||
|
log.err(.app, "CDP accept", .{ .err = err });
|
||||||
|
std.Thread.sleep(std.time.ns_per_s);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.client = socket;
|
||||||
|
defer if (self.client) |s| {
|
||||||
|
posix.close(s);
|
||||||
|
self.client = null;
|
||||||
|
};
|
||||||
|
|
||||||
if (log.enabled(.app, .info)) {
|
if (log.enabled(.app, .info)) {
|
||||||
var address: std.net.Address = undefined;
|
var client_address: std.net.Address = undefined;
|
||||||
var socklen: posix.socklen_t = @sizeOf(net.Address);
|
var socklen: posix.socklen_t = @sizeOf(net.Address);
|
||||||
try std.posix.getsockname(socket, &address.any, &socklen);
|
try std.posix.getsockname(socket, &client_address.any, &socklen);
|
||||||
log.info(.app, "client connected", .{ .ip = address });
|
log.info(.app, "client connected", .{ .ip = client_address });
|
||||||
|
}
|
||||||
|
|
||||||
|
self.readLoop(socket, timeout_ms) catch |err| {
|
||||||
|
log.err(.app, "CDP client loop", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void {
|
||||||
|
// This shouldn't be necessary, but the Client is HUGE (> 512KB) because
|
||||||
|
// it has a large read buffer. I don't know why, but v8 crashes if this
|
||||||
|
// is on the stack (and I assume it's related to its size).
|
||||||
|
const client = try self.allocator.create(Client);
|
||||||
|
defer self.allocator.destroy(client);
|
||||||
|
|
||||||
|
client.* = try Client.init(socket, self);
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
var http = &self.app.http;
|
||||||
|
http.monitorSocket(socket);
|
||||||
|
defer http.unmonitorSocket();
|
||||||
|
|
||||||
|
std.debug.assert(client.mode == .http);
|
||||||
|
while (true) {
|
||||||
|
if (http.poll(timeout_ms) != .extra_socket) {
|
||||||
|
log.info(.app, "CDP timeout", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (try client.readSocket() == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.mode == .cdp) {
|
||||||
|
break; // switch to our CDP loop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn releaseClient(self: *Server, client: *Client) void {
|
var cdp = &client.mode.cdp;
|
||||||
self.allocator.destroy(client);
|
var last_message = timestamp(.monotonic);
|
||||||
|
var ms_remaining = timeout_ms;
|
||||||
|
while (true) {
|
||||||
|
switch (cdp.pageWait(ms_remaining)) {
|
||||||
|
.extra_socket => {
|
||||||
|
if (try client.readSocket() == false) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
last_message = timestamp(.monotonic);
|
||||||
|
ms_remaining = timeout_ms;
|
||||||
// Client
|
},
|
||||||
// --------
|
.no_page => {
|
||||||
|
if (http.poll(ms_remaining) != .extra_socket) {
|
||||||
|
log.info(.app, "CDP timeout", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (try client.readSocket() == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
last_message = timestamp(.monotonic);
|
||||||
|
ms_remaining = timeout_ms;
|
||||||
|
},
|
||||||
|
.done => {
|
||||||
|
const elapsed = timestamp(.monotonic) - last_message;
|
||||||
|
if (elapsed > ms_remaining) {
|
||||||
|
log.info(.app, "CDP timeout", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ms_remaining -= @intCast(elapsed);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const Client = struct {
|
pub const Client = struct {
|
||||||
// The client is initially serving HTTP requests but, under normal circumstances
|
// The client is initially serving HTTP requests but, under normal circumstances
|
||||||
// should eventually be upgraded to a websocket connections
|
// should eventually be upgraded to a websocket connections
|
||||||
mode: Mode,
|
mode: union(enum) {
|
||||||
|
http: void,
|
||||||
|
cdp: CDP,
|
||||||
|
},
|
||||||
|
|
||||||
// The CDP instance that processes messages from this client
|
|
||||||
// (a generic so we can test with a mock
|
|
||||||
// null until mode == .websocket
|
|
||||||
cdp: ?CDP,
|
|
||||||
|
|
||||||
// Our Server (a generic so we can test with a mock)
|
|
||||||
server: *Server,
|
server: *Server,
|
||||||
reader: Reader(true),
|
reader: Reader(true),
|
||||||
socket: posix.socket_t,
|
socket: posix.socket_t,
|
||||||
last_active: std.time.Instant,
|
socket_flags: usize,
|
||||||
|
send_arena: ArenaAllocator,
|
||||||
// queue of messages to send
|
|
||||||
send_queue: SendQueue,
|
|
||||||
send_queue_node_pool: std.heap.MemoryPool(SendQueue.Node),
|
|
||||||
|
|
||||||
read_pending: bool,
|
|
||||||
read_completion: Completion,
|
|
||||||
|
|
||||||
write_pending: bool,
|
|
||||||
write_completion: Completion,
|
|
||||||
|
|
||||||
timeout_pending: bool,
|
|
||||||
timeout_completion: Completion,
|
|
||||||
|
|
||||||
// Used along with xyx_pending to figure out the lifetime of
|
|
||||||
// the client. When connected == false and we have no more pending
|
|
||||||
// completions, we can kill the client
|
|
||||||
connected: bool,
|
|
||||||
|
|
||||||
const Mode = enum {
|
|
||||||
http,
|
|
||||||
websocket,
|
|
||||||
};
|
|
||||||
|
|
||||||
const EMPTY_PONG = [_]u8{ 138, 0 };
|
const EMPTY_PONG = [_]u8{ 138, 0 };
|
||||||
|
|
||||||
@@ -161,140 +200,62 @@ pub const Client = struct {
|
|||||||
// "private-use" close codes must be from 4000-49999
|
// "private-use" close codes must be from 4000-49999
|
||||||
const CLOSE_TIMEOUT = [_]u8{ 136, 2, 15, 160 }; // code: 4000
|
const CLOSE_TIMEOUT = [_]u8{ 136, 2, 15, 160 }; // code: 4000
|
||||||
|
|
||||||
const SendQueue = std.DoublyLinkedList(Outgoing);
|
fn init(socket: posix.socket_t, server: *Server) !Client {
|
||||||
|
const socket_flags = try posix.fcntl(socket, posix.F.GETFL, 0);
|
||||||
|
const nonblocking = @as(u32, @bitCast(posix.O{ .NONBLOCK = true }));
|
||||||
|
// we expect the socket to come to us as nonblocking
|
||||||
|
std.debug.assert(socket_flags & nonblocking == nonblocking);
|
||||||
|
|
||||||
const Self = @This();
|
var reader = try Reader(true).init(server.allocator);
|
||||||
|
errdefer reader.deinit();
|
||||||
|
|
||||||
fn init(socket: posix.socket_t, server: *Server) Self {
|
|
||||||
return .{
|
return .{
|
||||||
.cdp = null,
|
|
||||||
.mode = .http,
|
|
||||||
.socket = socket,
|
.socket = socket,
|
||||||
.server = server,
|
.server = server,
|
||||||
.last_active = now(),
|
.reader = reader,
|
||||||
.send_queue = .{},
|
.mode = .{ .http = {} },
|
||||||
.read_pending = false,
|
.socket_flags = socket_flags,
|
||||||
.read_completion = undefined,
|
.send_arena = ArenaAllocator.init(server.allocator),
|
||||||
.write_pending = false,
|
|
||||||
.write_completion = undefined,
|
|
||||||
.timeout_pending = false,
|
|
||||||
.timeout_completion = undefined,
|
|
||||||
.connected = true,
|
|
||||||
.reader = .{ .allocator = server.allocator },
|
|
||||||
.send_queue_node_pool = std.heap.MemoryPool(SendQueue.Node).init(server.allocator),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maybeDeinit(self: *Self) void {
|
fn deinit(self: *Client) void {
|
||||||
if (self.read_pending or self.write_pending) {
|
switch (self.mode) {
|
||||||
// We cannot do anything as long as we still have these pending
|
.cdp => |*cdp| cdp.deinit(),
|
||||||
// They should not be pending for long as we're only here after
|
.http => {},
|
||||||
// having shutdown the socket
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't have a read nor a write completion pending, we can start
|
|
||||||
// to shutdown.
|
|
||||||
|
|
||||||
self.reader.deinit();
|
self.reader.deinit();
|
||||||
var node = self.send_queue.first;
|
self.send_arena.deinit();
|
||||||
while (node) |n| {
|
|
||||||
if (n.data.arena) |*arena| {
|
|
||||||
arena.deinit();
|
|
||||||
}
|
|
||||||
node = n.next;
|
|
||||||
}
|
|
||||||
if (self.cdp) |*cdp| {
|
|
||||||
cdp.deinit();
|
|
||||||
}
|
|
||||||
self.send_queue_node_pool.deinit();
|
|
||||||
posix.close(self.socket);
|
|
||||||
|
|
||||||
// let the client accept a new connection
|
|
||||||
self.server.queueAccept();
|
|
||||||
|
|
||||||
if (self.timeout_pending == false) {
|
|
||||||
// We also don't have a pending timeout, we can release the client.
|
|
||||||
// See callbackTimeout for more explanation about this. But, TL;DR
|
|
||||||
// we want to call `queueAccept` as soon as we have no more read/write
|
|
||||||
// but we don't want to wait for the timeout callback.
|
|
||||||
self.server.releaseClient(self);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close(self: *Self) void {
|
fn readSocket(self: *Client) !bool {
|
||||||
log.info(.app, "client disconnected", .{});
|
const n = posix.read(self.socket, self.readBuf()) catch |err| {
|
||||||
self.connected = false;
|
log.warn(.app, "CDP read", .{ .err = err });
|
||||||
// recv only, because we might have pending writes we'd like to get
|
return false;
|
||||||
// out (like the HTTP error response)
|
|
||||||
posix.shutdown(self.socket, .recv) catch {};
|
|
||||||
self.maybeDeinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start(self: *Self) void {
|
|
||||||
self.queueRead();
|
|
||||||
self.queueTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn queueRead(self: *Self) void {
|
|
||||||
self.server.loop.io.recv(
|
|
||||||
*Self,
|
|
||||||
self,
|
|
||||||
callbackRead,
|
|
||||||
&self.read_completion,
|
|
||||||
self.socket,
|
|
||||||
self.readBuf(),
|
|
||||||
);
|
|
||||||
self.read_pending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn callbackRead(self: *Self, _: *Completion, result: RecvError!usize) void {
|
|
||||||
self.read_pending = false;
|
|
||||||
if (self.connected == false) {
|
|
||||||
self.maybeDeinit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const size = result catch |err| {
|
|
||||||
log.err(.app, "server read error", .{ .err = err });
|
|
||||||
self.close();
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (size == 0) {
|
if (n == 0) {
|
||||||
self.close();
|
log.info(.app, "CDP disconnect", .{});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const more = self.processData(size) catch {
|
return self.processData(n) catch false;
|
||||||
self.close();
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// if more == false, the client is disconnecting
|
|
||||||
if (more) {
|
|
||||||
self.queueRead();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn readBuf(self: *Self) []u8 {
|
fn readBuf(self: *Client) []u8 {
|
||||||
return self.reader.readBuf();
|
return self.reader.readBuf();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn processData(self: *Self, len: usize) !bool {
|
fn processData(self: *Client, len: usize) !bool {
|
||||||
self.last_active = now();
|
|
||||||
self.reader.len += len;
|
self.reader.len += len;
|
||||||
|
|
||||||
switch (self.mode) {
|
switch (self.mode) {
|
||||||
.http => {
|
.cdp => |*cdp| return self.processWebsocketMessage(cdp),
|
||||||
try self.processHTTPRequest();
|
.http => return self.processHTTPRequest(),
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.websocket => return self.processWebsocketMessage(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn processHTTPRequest(self: *Self) !void {
|
fn processHTTPRequest(self: *Client) !bool {
|
||||||
std.debug.assert(self.reader.pos == 0);
|
std.debug.assert(self.reader.pos == 0);
|
||||||
const request = self.reader.buf[0..self.reader.len];
|
const request = self.reader.buf[0..self.reader.len];
|
||||||
|
|
||||||
@@ -306,10 +267,12 @@ pub const Client = struct {
|
|||||||
// we're only expecting [body-less] GET requests.
|
// we're only expecting [body-less] GET requests.
|
||||||
if (std.mem.endsWith(u8, request, "\r\n\r\n") == false) {
|
if (std.mem.endsWith(u8, request, "\r\n\r\n") == false) {
|
||||||
// we need more data, put any more data here
|
// we need more data, put any more data here
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.handleHTTPRequest(request) catch |err| {
|
// the next incoming data can go to the front of our buffer
|
||||||
|
defer self.reader.len = 0;
|
||||||
|
return self.handleHTTPRequest(request) catch |err| {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
error.NotFound => self.writeHTTPErrorResponse(404, "Not found"),
|
error.NotFound => self.writeHTTPErrorResponse(404, "Not found"),
|
||||||
error.InvalidRequest => self.writeHTTPErrorResponse(400, "Invalid request"),
|
error.InvalidRequest => self.writeHTTPErrorResponse(400, "Invalid request"),
|
||||||
@@ -325,12 +288,9 @@ pub const Client = struct {
|
|||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|
||||||
// the next incoming data can go to the front of our buffer
|
|
||||||
self.reader.len = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handleHTTPRequest(self: *Self, request: []u8) !void {
|
fn handleHTTPRequest(self: *Client, request: []u8) !bool {
|
||||||
if (request.len < 18) {
|
if (request.len < 18) {
|
||||||
// 18 is [generously] the smallest acceptable HTTP request
|
// 18 is [generously] the smallest acceptable HTTP request
|
||||||
return error.InvalidRequest;
|
return error.InvalidRequest;
|
||||||
@@ -347,11 +307,12 @@ pub const Client = struct {
|
|||||||
const url = request[4..url_end];
|
const url = request[4..url_end];
|
||||||
|
|
||||||
if (std.mem.eql(u8, url, "/")) {
|
if (std.mem.eql(u8, url, "/")) {
|
||||||
return self.upgradeConnection(request);
|
try self.upgradeConnection(request);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, url, "/json/version")) {
|
if (std.mem.eql(u8, url, "/json/version")) {
|
||||||
try self.send(null, self.server.json_version_response);
|
try self.send(self.server.json_version_response);
|
||||||
// Chromedp (a Go driver) does an http request to /json/version
|
// Chromedp (a Go driver) does an http request to /json/version
|
||||||
// then to / (websocket upgrade) using a different connection.
|
// then to / (websocket upgrade) using a different connection.
|
||||||
// Since we only allow 1 connection at a time, the 2nd one (the
|
// Since we only allow 1 connection at a time, the 2nd one (the
|
||||||
@@ -359,13 +320,13 @@ pub const Client = struct {
|
|||||||
// We can avoid that by closing the connection. json_version_response
|
// We can avoid that by closing the connection. json_version_response
|
||||||
// has a Connection: Close header too.
|
// has a Connection: Close header too.
|
||||||
try posix.shutdown(self.socket, .recv);
|
try posix.shutdown(self.socket, .recv);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return error.NotFound;
|
return error.NotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upgradeConnection(self: *Self, request: []u8) !void {
|
fn upgradeConnection(self: *Client, request: []u8) !void {
|
||||||
// our caller already confirmed that we have a trailing \r\n\r\n
|
// our caller already confirmed that we have a trailing \r\n\r\n
|
||||||
const request_line_end = std.mem.indexOfScalar(u8, request, '\r') orelse unreachable;
|
const request_line_end = std.mem.indexOfScalar(u8, request, '\r') orelse unreachable;
|
||||||
const request_line = request[0..request_line_end];
|
const request_line = request[0..request_line_end];
|
||||||
@@ -425,8 +386,7 @@ pub const Client = struct {
|
|||||||
// our caller has already made sure this request ended in \r\n\r\n
|
// our caller has already made sure this request ended in \r\n\r\n
|
||||||
// so it isn't something we need to check again
|
// so it isn't something we need to check again
|
||||||
|
|
||||||
var arena = ArenaAllocator.init(self.server.allocator);
|
const allocator = self.send_arena.allocator();
|
||||||
errdefer arena.deinit();
|
|
||||||
|
|
||||||
const response = blk: {
|
const response = blk: {
|
||||||
// Response to an ugprade request is always this, with
|
// Response to an ugprade request is always this, with
|
||||||
@@ -441,7 +401,7 @@ pub const Client = struct {
|
|||||||
|
|
||||||
// The response will be sent via the IO Loop and thus has to have its
|
// The response will be sent via the IO Loop and thus has to have its
|
||||||
// own lifetime.
|
// own lifetime.
|
||||||
const res = try arena.allocator().dupe(u8, template);
|
const res = try allocator.dupe(u8, template);
|
||||||
|
|
||||||
// magic response
|
// magic response
|
||||||
const key_pos = res.len - 32;
|
const key_pos = res.len - 32;
|
||||||
@@ -457,12 +417,11 @@ pub const Client = struct {
|
|||||||
break :blk res;
|
break :blk res;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.mode = .websocket;
|
self.mode = .{ .cdp = try CDP.init(self.server.app, self) };
|
||||||
self.cdp = try CDP.init(self.server.app, self);
|
return self.send(response);
|
||||||
return self.send(arena, response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn writeHTTPErrorResponse(self: *Self, comptime status: u16, comptime body: []const u8) void {
|
fn writeHTTPErrorResponse(self: *Client, comptime status: u16, comptime body: []const u8) void {
|
||||||
const response = std.fmt.comptimePrint(
|
const response = std.fmt.comptimePrint(
|
||||||
"HTTP/1.1 {d} \r\nConnection: Close\r\nContent-Length: {d}\r\n\r\n{s}",
|
"HTTP/1.1 {d} \r\nConnection: Close\r\nContent-Length: {d}\r\n\r\n{s}",
|
||||||
.{ status, body.len, body },
|
.{ status, body.len, body },
|
||||||
@@ -470,24 +429,21 @@ pub const Client = struct {
|
|||||||
|
|
||||||
// we're going to close this connection anyways, swallowing any
|
// we're going to close this connection anyways, swallowing any
|
||||||
// error seems safe
|
// error seems safe
|
||||||
self.send(null, response) catch {};
|
self.send(response) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn processWebsocketMessage(self: *Self) !bool {
|
fn processWebsocketMessage(self: *Client, cdp: *CDP) !bool {
|
||||||
errdefer self.close();
|
|
||||||
|
|
||||||
var reader = &self.reader;
|
var reader = &self.reader;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const msg = reader.next() catch |err| {
|
const msg = reader.next() catch |err| {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
error.TooLarge => self.send(null, &CLOSE_TOO_BIG) catch {},
|
error.TooLarge => self.send(&CLOSE_TOO_BIG) catch {},
|
||||||
error.NotMasked => self.send(null, &CLOSE_PROTOCOL_ERROR) catch {},
|
error.NotMasked => self.send(&CLOSE_PROTOCOL_ERROR) catch {},
|
||||||
error.ReservedFlags => self.send(null, &CLOSE_PROTOCOL_ERROR) catch {},
|
error.ReservedFlags => self.send(&CLOSE_PROTOCOL_ERROR) catch {},
|
||||||
error.InvalidMessageType => self.send(null, &CLOSE_PROTOCOL_ERROR) catch {},
|
error.InvalidMessageType => self.send(&CLOSE_PROTOCOL_ERROR) catch {},
|
||||||
error.ControlTooLarge => self.send(null, &CLOSE_PROTOCOL_ERROR) catch {},
|
error.ControlTooLarge => self.send(&CLOSE_PROTOCOL_ERROR) catch {},
|
||||||
error.InvalidContinuation => self.send(null, &CLOSE_PROTOCOL_ERROR) catch {},
|
error.InvalidContinuation => self.send(&CLOSE_PROTOCOL_ERROR) catch {},
|
||||||
error.NestedFragementation => self.send(null, &CLOSE_PROTOCOL_ERROR) catch {},
|
error.NestedFragementation => self.send(&CLOSE_PROTOCOL_ERROR) catch {},
|
||||||
error.OutOfMemory => {}, // don't borther trying to send an error in this case
|
error.OutOfMemory => {}, // don't borther trying to send an error in this case
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
@@ -497,12 +453,10 @@ pub const Client = struct {
|
|||||||
.pong => {},
|
.pong => {},
|
||||||
.ping => try self.sendPong(msg.data),
|
.ping => try self.sendPong(msg.data),
|
||||||
.close => {
|
.close => {
|
||||||
self.send(null, &CLOSE_NORMAL) catch {};
|
self.send(&CLOSE_NORMAL) catch {};
|
||||||
self.close();
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
.text, .binary => if (self.cdp.?.handleMessage(msg.data) == false) {
|
.text, .binary => if (cdp.handleMessage(msg.data) == false) {
|
||||||
self.close();
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -517,20 +471,18 @@ pub const Client = struct {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sendPong(self: *Self, data: []const u8) !void {
|
fn sendPong(self: *Client, data: []const u8) !void {
|
||||||
if (data.len == 0) {
|
if (data.len == 0) {
|
||||||
return self.send(null, &EMPTY_PONG);
|
return self.send(&EMPTY_PONG);
|
||||||
}
|
}
|
||||||
var header_buf: [10]u8 = undefined;
|
var header_buf: [10]u8 = undefined;
|
||||||
const header = websocketHeader(&header_buf, .pong, data.len);
|
const header = websocketHeader(&header_buf, .pong, data.len);
|
||||||
|
|
||||||
var arena = ArenaAllocator.init(self.server.allocator);
|
const allocator = self.send_arena.allocator();
|
||||||
errdefer arena.deinit();
|
var framed = try allocator.alloc(u8, header.len + data.len);
|
||||||
|
|
||||||
var framed = try arena.allocator().alloc(u8, header.len + data.len);
|
|
||||||
@memcpy(framed[0..header.len], header);
|
@memcpy(framed[0..header.len], header);
|
||||||
@memcpy(framed[header.len..], data);
|
@memcpy(framed[header.len..], data);
|
||||||
return self.send(arena, framed);
|
return self.send(framed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// called by CDP
|
// called by CDP
|
||||||
@@ -539,154 +491,69 @@ pub const Client = struct {
|
|||||||
// writev, so we need to get creative. We'll JSON serialize to a
|
// writev, so we need to get creative. We'll JSON serialize to a
|
||||||
// buffer, where the first 10 bytes are reserved. We can then backfill
|
// buffer, where the first 10 bytes are reserved. We can then backfill
|
||||||
// the header and send the slice.
|
// the header and send the slice.
|
||||||
pub fn sendJSON(self: *Self, message: anytype, opts: std.json.StringifyOptions) !void {
|
pub fn sendJSON(self: *Client, message: anytype, opts: std.json.Stringify.Options) !void {
|
||||||
var arena = ArenaAllocator.init(self.server.allocator);
|
const allocator = self.send_arena.allocator();
|
||||||
errdefer arena.deinit();
|
|
||||||
|
|
||||||
const allocator = arena.allocator();
|
var aw = try std.Io.Writer.Allocating.initCapacity(allocator, 512);
|
||||||
|
|
||||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
|
||||||
try buf.ensureTotalCapacity(allocator, 512);
|
|
||||||
|
|
||||||
// reserve space for the maximum possible header
|
// reserve space for the maximum possible header
|
||||||
buf.appendSliceAssumeCapacity(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
|
try aw.writer.writeAll(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
|
||||||
|
try std.json.Stringify.value(message, opts, &aw.writer);
|
||||||
try std.json.stringify(message, opts, buf.writer(allocator));
|
const framed = fillWebsocketHeader(aw.toArrayList());
|
||||||
const framed = fillWebsocketHeader(buf);
|
return self.send(framed);
|
||||||
return self.send(arena, framed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sendJSONRaw(
|
pub fn sendJSONRaw(
|
||||||
self: *Self,
|
self: *Client,
|
||||||
arena: ArenaAllocator,
|
|
||||||
buf: std.ArrayListUnmanaged(u8),
|
buf: std.ArrayListUnmanaged(u8),
|
||||||
) !void {
|
) !void {
|
||||||
// Dangerous API!. We assume the caller has reserved the first 10
|
// Dangerous API!. We assume the caller has reserved the first 10
|
||||||
// bytes in `buf`.
|
// bytes in `buf`.
|
||||||
const framed = fillWebsocketHeader(buf);
|
const framed = fillWebsocketHeader(buf);
|
||||||
return self.send(arena, framed);
|
return self.send(framed);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn queueTimeout(self: *Self) void {
|
fn send(self: *Client, data: []const u8) !void {
|
||||||
self.server.loop.io.timeout(
|
var pos: usize = 0;
|
||||||
*Self,
|
var changed_to_blocking: bool = false;
|
||||||
self,
|
defer _ = self.send_arena.reset(.{ .retain_with_limit = 1024 * 32 });
|
||||||
callbackTimeout,
|
|
||||||
&self.timeout_completion,
|
|
||||||
TimeoutCheck,
|
|
||||||
);
|
|
||||||
self.timeout_pending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn callbackTimeout(self: *Self, _: *Completion, result: TimeoutError!void) void {
|
defer if (changed_to_blocking) {
|
||||||
self.timeout_pending = false;
|
// We had to change our socket to blocking me to get our write out
|
||||||
if (self.connected == false) {
|
// We need to change it back to non-blocking.
|
||||||
if (self.read_pending == false and self.write_pending == false) {
|
_ = posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags) catch |err| {
|
||||||
// Timeout is problematic. Ideally, we'd just call maybeDeinit
|
log.err(.app, "CDP restore nonblocking", .{ .err = err });
|
||||||
// here and check for timeout_pending == true. But that would
|
|
||||||
// mean not being able to accept a new connection until this
|
|
||||||
// callback fires - introducing a noticeable delay.
|
|
||||||
// So, when read_pending and write_pending are both false, we
|
|
||||||
// clean up as much as we can, and let the server accept a new
|
|
||||||
// connection but we keep the client around to handle this
|
|
||||||
// completion (if only we could cancel a completion!).
|
|
||||||
// If we're here, with connected == false, read_pending == false
|
|
||||||
// and write_pending == false, then everything has already been
|
|
||||||
// cleaned up, and we just need to release the client.
|
|
||||||
self.server.releaseClient(self);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result) |_| {
|
|
||||||
if (now().since(self.last_active) >= self.server.timeout) {
|
|
||||||
log.info(.app, "client connection timeout", .{});
|
|
||||||
if (self.mode == .websocket) {
|
|
||||||
self.send(null, &CLOSE_TIMEOUT) catch {};
|
|
||||||
}
|
|
||||||
self.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else |err| {
|
|
||||||
log.err(.app, "server timeout error", .{ .err = err });
|
|
||||||
}
|
|
||||||
|
|
||||||
self.queueTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(self: *Self, arena: ?ArenaAllocator, data: []const u8) !void {
|
|
||||||
const node = try self.send_queue_node_pool.create();
|
|
||||||
errdefer self.send_queue_node_pool.destroy(node);
|
|
||||||
|
|
||||||
node.data = Outgoing{
|
|
||||||
.arena = arena,
|
|
||||||
.to_send = data,
|
|
||||||
};
|
};
|
||||||
self.send_queue.append(node);
|
|
||||||
|
|
||||||
if (self.send_queue.len > 1) {
|
|
||||||
// if we already had a message in the queue, then our send loop
|
|
||||||
// is already setup.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.queueSend();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn queueSend(self: *Self) void {
|
|
||||||
const node = self.send_queue.first orelse {
|
|
||||||
// no more messages to send;
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.server.loop.io.send(
|
LOOP: while (pos < data.len) {
|
||||||
*Self,
|
const written = posix.write(self.socket, data[pos..]) catch |err| switch (err) {
|
||||||
self,
|
error.WouldBlock => {
|
||||||
sendCallback,
|
// self.socket is nonblocking, because we don't want to block
|
||||||
&self.write_completion,
|
// reads. But our life is a lot easier if we block writes,
|
||||||
self.socket,
|
// largely, because we don't have to maintain a queue of pending
|
||||||
node.data.to_send,
|
// writes (which would each need their own allocations). So
|
||||||
);
|
// if we get a WouldBlock error, we'll switch the socket to
|
||||||
self.write_pending = true;
|
// blocking and switch it back to non-blocking after the write
|
||||||
}
|
// is complete. Doesn't seem particularly efficiently, but
|
||||||
|
// this should virtually never happen.
|
||||||
fn sendCallback(self: *Self, _: *Completion, result: SendError!usize) void {
|
std.debug.assert(changed_to_blocking == false);
|
||||||
self.write_pending = false;
|
log.debug(.app, "CDP write would block", .{});
|
||||||
if (self.connected == false) {
|
changed_to_blocking = true;
|
||||||
self.maybeDeinit();
|
_ = try posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags & ~@as(u32, @bitCast(posix.O{ .NONBLOCK = true })));
|
||||||
return;
|
continue :LOOP;
|
||||||
}
|
},
|
||||||
|
else => return err,
|
||||||
const sent = result catch |err| {
|
|
||||||
log.warn(.app, "server send error", .{ .err = err });
|
|
||||||
self.close();
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const node = self.send_queue.popFirst().?;
|
if (written == 0) {
|
||||||
const outgoing = &node.data;
|
return error.Closed;
|
||||||
if (sent == outgoing.to_send.len) {
|
|
||||||
if (outgoing.arena) |*arena| {
|
|
||||||
arena.deinit();
|
|
||||||
}
|
}
|
||||||
self.send_queue_node_pool.destroy(node);
|
pos += written;
|
||||||
} else {
|
|
||||||
// oops, we shouldn't have popped this node off, we need
|
|
||||||
// to add it back to the front in order to send the unsent data
|
|
||||||
// (this is less likely to happen, which is why we eagerly
|
|
||||||
// pop it off)
|
|
||||||
std.debug.assert(sent < outgoing.to_send.len);
|
|
||||||
node.data.to_send = outgoing.to_send[sent..];
|
|
||||||
self.send_queue.prepend(node);
|
|
||||||
}
|
}
|
||||||
self.queueSend();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Outgoing = struct {
|
|
||||||
to_send: []const u8,
|
|
||||||
arena: ?ArenaAllocator,
|
|
||||||
};
|
|
||||||
|
|
||||||
// WebSocket message reader. Given websocket message, acts as an iterator that
|
// WebSocket message reader. Given websocket message, acts as an iterator that
|
||||||
// can return zero or more Messages. When next returns null, any incomplete
|
// can return zero or more Messages. When next returns null, any incomplete
|
||||||
// message will remain in reader.data
|
// message will remain in reader.data
|
||||||
@@ -703,14 +570,23 @@ fn Reader(comptime EXPECT_MASK: bool) type {
|
|||||||
|
|
||||||
// we add 140 to allow 1 control message (ping/pong/close) to be
|
// we add 140 to allow 1 control message (ping/pong/close) to be
|
||||||
// fragmented into a normal message.
|
// fragmented into a normal message.
|
||||||
buf: [MAX_MESSAGE_SIZE + 140]u8 = undefined,
|
buf: []u8,
|
||||||
|
|
||||||
fragments: ?Fragments = null,
|
fragments: ?Fragments = null,
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
|
fn init(allocator: Allocator) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, 16 * 1024);
|
||||||
|
return .{
|
||||||
|
.buf = buf,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn deinit(self: *Self) void {
|
fn deinit(self: *Self) void {
|
||||||
self.cleanup();
|
self.cleanup();
|
||||||
|
self.allocator.free(self.buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cleanup(self: *Self) void {
|
fn cleanup(self: *Self) void {
|
||||||
@@ -779,9 +655,14 @@ fn Reader(comptime EXPECT_MASK: bool) type {
|
|||||||
}
|
}
|
||||||
} else if (message_len > MAX_MESSAGE_SIZE) {
|
} else if (message_len > MAX_MESSAGE_SIZE) {
|
||||||
return error.TooLarge;
|
return error.TooLarge;
|
||||||
}
|
} else if (message_len > self.buf.len) {
|
||||||
|
const len = self.buf.len;
|
||||||
if (buf.len < message_len) {
|
self.buf = try growBuffer(self.allocator, self.buf, message_len);
|
||||||
|
buf = self.buf[0..len];
|
||||||
|
// we need more data
|
||||||
|
return null;
|
||||||
|
} else if (buf.len < message_len) {
|
||||||
|
// we need more data
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -919,13 +800,32 @@ fn Reader(comptime EXPECT_MASK: bool) type {
|
|||||||
|
|
||||||
// We're here because we either don't have enough bytes of the next
|
// We're here because we either don't have enough bytes of the next
|
||||||
// message, or we know that it won't fit in our buffer as-is.
|
// message, or we know that it won't fit in our buffer as-is.
|
||||||
std.mem.copyForwards(u8, &self.buf, partial);
|
std.mem.copyForwards(u8, self.buf, partial);
|
||||||
self.pos = 0;
|
self.pos = 0;
|
||||||
self.len = partial_bytes;
|
self.len = partial_bytes;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn growBuffer(allocator: Allocator, buf: []u8, required_capacity: usize) ![]u8 {
|
||||||
|
// from std.ArrayList
|
||||||
|
var new_capacity = buf.len;
|
||||||
|
while (true) {
|
||||||
|
new_capacity +|= new_capacity / 2 + 8;
|
||||||
|
if (new_capacity >= required_capacity) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(.app, "CDP buffer growth", .{ .from = buf.len, .to = new_capacity });
|
||||||
|
|
||||||
|
if (allocator.resize(buf, new_capacity)) {
|
||||||
|
return buf.ptr[0..new_capacity];
|
||||||
|
}
|
||||||
|
const new_buffer = try allocator.alloc(u8, new_capacity);
|
||||||
|
@memcpy(new_buffer[0..buf.len], buf);
|
||||||
|
allocator.free(buf);
|
||||||
|
return new_buffer;
|
||||||
|
}
|
||||||
|
|
||||||
const Fragments = struct {
|
const Fragments = struct {
|
||||||
type: Message.Type,
|
type: Message.Type,
|
||||||
message: std.ArrayListUnmanaged(u8),
|
message: std.ArrayListUnmanaged(u8),
|
||||||
@@ -1003,56 +903,6 @@ fn websocketHeader(buf: []u8, op_code: OpCode, payload_len: usize) []const u8 {
|
|||||||
return buf[0..10];
|
return buf[0..10];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(
|
|
||||||
app: *App,
|
|
||||||
address: net.Address,
|
|
||||||
timeout: u64,
|
|
||||||
) !void {
|
|
||||||
// create socket
|
|
||||||
const flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK;
|
|
||||||
const listener = try posix.socket(address.any.family, flags, posix.IPPROTO.TCP);
|
|
||||||
defer posix.close(listener);
|
|
||||||
|
|
||||||
try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
|
|
||||||
// TODO: Broken on darwin
|
|
||||||
// https://github.com/ziglang/zig/issues/17260 (fixed in Zig 0.14)
|
|
||||||
// if (@hasDecl(os.TCP, "NODELAY")) {
|
|
||||||
// try os.setsockopt(socket.sockfd.?, os.IPPROTO.TCP, os.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1)));
|
|
||||||
// }
|
|
||||||
try posix.setsockopt(listener, posix.IPPROTO.TCP, 1, &std.mem.toBytes(@as(c_int, 1)));
|
|
||||||
|
|
||||||
// bind & listen
|
|
||||||
try posix.bind(listener, &address.any, address.getOsSockLen());
|
|
||||||
try posix.listen(listener, 1);
|
|
||||||
|
|
||||||
var loop = app.loop;
|
|
||||||
const allocator = app.allocator;
|
|
||||||
const json_version_response = try buildJSONVersionResponse(allocator, address);
|
|
||||||
defer allocator.free(json_version_response);
|
|
||||||
|
|
||||||
var server = Server{
|
|
||||||
.app = app,
|
|
||||||
.loop = loop,
|
|
||||||
.timeout = timeout,
|
|
||||||
.listener = listener,
|
|
||||||
.allocator = allocator,
|
|
||||||
.accept_completion = undefined,
|
|
||||||
.json_version_response = json_version_response,
|
|
||||||
};
|
|
||||||
defer server.deinit();
|
|
||||||
|
|
||||||
// accept an connection
|
|
||||||
server.queueAccept();
|
|
||||||
log.info(.app, "server running", .{ .address = address });
|
|
||||||
|
|
||||||
// infinite loop on I/O events, either:
|
|
||||||
// - cmd from incoming connection on server socket
|
|
||||||
// - JS callbacks events from scripts
|
|
||||||
while (true) {
|
|
||||||
try loop.io.run_for_ns(10 * std.time.ns_per_ms);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
// --------
|
// --------
|
||||||
|
|
||||||
@@ -1060,7 +910,7 @@ fn buildJSONVersionResponse(
|
|||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
address: net.Address,
|
address: net.Address,
|
||||||
) ![]const u8 {
|
) ![]const u8 {
|
||||||
const body_format = "{{\"webSocketDebuggerUrl\": \"ws://{}/\"}}";
|
const body_format = "{{\"webSocketDebuggerUrl\": \"ws://{f}/\"}}";
|
||||||
const body_len = std.fmt.count(body_format, .{address});
|
const body_len = std.fmt.count(body_format, .{address});
|
||||||
|
|
||||||
// We send a Connection: Close (and actually close the connection)
|
// We send a Connection: Close (and actually close the connection)
|
||||||
@@ -1078,10 +928,7 @@ fn buildJSONVersionResponse(
|
|||||||
return try std.fmt.allocPrint(allocator, response_format, .{ body_len, address });
|
return try std.fmt.allocPrint(allocator, response_format, .{ body_len, address });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn now() std.time.Instant {
|
pub const timestamp = @import("datetime.zig").timestamp;
|
||||||
// can only fail on platforms we don't support
|
|
||||||
return std.time.Instant.now() catch unreachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In-place string lowercase
|
// In-place string lowercase
|
||||||
fn toLower(str: []u8) []u8 {
|
fn toLower(str: []u8) []u8 {
|
||||||
@@ -1253,8 +1100,8 @@ test "Client: read invalid websocket message" {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// length of message is 0000 0401, i.e: 1024 * 512 + 1
|
// length of message is 0000 0810, i.e: 1024 * 512 + 265
|
||||||
try assertWebSocketError(1009, &.{ 129, 255, 0, 0, 0, 0, 0, 8, 0, 1, 'm', 'a', 's', 'k' });
|
try assertWebSocketError(1009, &.{ 129, 255, 0, 0, 0, 0, 0, 8, 1, 0, 'm', 'a', 's', 'k' });
|
||||||
|
|
||||||
// continuation type message must come after a normal message
|
// continuation type message must come after a normal message
|
||||||
// even when not a fin frame
|
// even when not a fin frame
|
||||||
@@ -1444,8 +1291,7 @@ const MockCDP = struct {
|
|||||||
|
|
||||||
allocator: Allocator = testing.allocator,
|
allocator: Allocator = testing.allocator,
|
||||||
|
|
||||||
fn init(_: Allocator, client: anytype, loop: *Loop) MockCDP {
|
fn init(_: Allocator, client: anytype) MockCDP {
|
||||||
_ = loop;
|
|
||||||
_ = client;
|
_ = client;
|
||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
@@ -1477,7 +1323,10 @@ fn createTestClient() !TestClient {
|
|||||||
try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout);
|
try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout);
|
||||||
return .{
|
return .{
|
||||||
.stream = stream,
|
.stream = stream,
|
||||||
.reader = .{ .allocator = testing.allocator },
|
.reader = .{
|
||||||
|
.allocator = testing.allocator,
|
||||||
|
.buf = try testing.allocator.alloc(u8, 1024 * 16),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
119
src/TestHTTPServer.zig
Normal file
119
src/TestHTTPServer.zig
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
109
src/app.zig
109
src/app.zig
@@ -1,109 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const log = @import("log.zig");
|
|
||||||
const Loop = @import("runtime/loop.zig").Loop;
|
|
||||||
const http = @import("http/client.zig");
|
|
||||||
const Platform = @import("runtime/js.zig").Platform;
|
|
||||||
|
|
||||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
|
||||||
const Notification = @import("notification.zig").Notification;
|
|
||||||
|
|
||||||
// Container for global state / objects that various parts of the system
|
|
||||||
// might need.
|
|
||||||
pub const App = struct {
|
|
||||||
loop: *Loop,
|
|
||||||
config: Config,
|
|
||||||
platform: ?*const Platform,
|
|
||||||
allocator: Allocator,
|
|
||||||
telemetry: Telemetry,
|
|
||||||
http_client: http.Client,
|
|
||||||
app_dir_path: ?[]const u8,
|
|
||||||
notification: *Notification,
|
|
||||||
|
|
||||||
pub const RunMode = enum {
|
|
||||||
help,
|
|
||||||
fetch,
|
|
||||||
serve,
|
|
||||||
version,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Config = struct {
|
|
||||||
run_mode: RunMode,
|
|
||||||
platform: ?*const Platform = null,
|
|
||||||
tls_verify_host: bool = true,
|
|
||||||
http_proxy: ?std.Uri = null,
|
|
||||||
proxy_type: ?http.ProxyType = null,
|
|
||||||
proxy_auth: ?http.ProxyAuth = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, config: Config) !*App {
|
|
||||||
const app = try allocator.create(App);
|
|
||||||
errdefer allocator.destroy(app);
|
|
||||||
|
|
||||||
const loop = try allocator.create(Loop);
|
|
||||||
errdefer allocator.destroy(loop);
|
|
||||||
|
|
||||||
loop.* = try Loop.init(allocator);
|
|
||||||
errdefer loop.deinit();
|
|
||||||
|
|
||||||
const notification = try Notification.init(allocator, null);
|
|
||||||
errdefer notification.deinit();
|
|
||||||
|
|
||||||
const app_dir_path = getAndMakeAppDir(allocator);
|
|
||||||
|
|
||||||
app.* = .{
|
|
||||||
.loop = loop,
|
|
||||||
.allocator = allocator,
|
|
||||||
.telemetry = undefined,
|
|
||||||
.platform = config.platform,
|
|
||||||
.app_dir_path = app_dir_path,
|
|
||||||
.notification = notification,
|
|
||||||
.http_client = try http.Client.init(allocator, loop, .{
|
|
||||||
.max_concurrent = 3,
|
|
||||||
.http_proxy = config.http_proxy,
|
|
||||||
.proxy_type = config.proxy_type,
|
|
||||||
.proxy_auth = config.proxy_auth,
|
|
||||||
.tls_verify_host = config.tls_verify_host,
|
|
||||||
}),
|
|
||||||
.config = config,
|
|
||||||
};
|
|
||||||
app.telemetry = Telemetry.init(app, config.run_mode);
|
|
||||||
try app.telemetry.register(app.notification);
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *App) void {
|
|
||||||
const allocator = self.allocator;
|
|
||||||
if (self.app_dir_path) |app_dir_path| {
|
|
||||||
allocator.free(app_dir_path);
|
|
||||||
}
|
|
||||||
self.telemetry.deinit();
|
|
||||||
self.loop.deinit();
|
|
||||||
allocator.destroy(self.loop);
|
|
||||||
self.http_client.deinit();
|
|
||||||
self.notification.deinit();
|
|
||||||
allocator.destroy(self);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 {
|
|
||||||
if (@import("builtin").is_test) {
|
|
||||||
return allocator.dupe(u8, "/tmp") catch unreachable;
|
|
||||||
}
|
|
||||||
const app_dir_path = std.fs.getAppDataDir(allocator, "lightpanda") catch |err| {
|
|
||||||
log.warn(.app, "get data dir", .{ .err = err });
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
std.fs.cwd().makePath(app_dir_path) catch |err| switch (err) {
|
|
||||||
error.PathAlreadyExists => return app_dir_path,
|
|
||||||
else => {
|
|
||||||
allocator.free(app_dir_path);
|
|
||||||
log.warn(.app, "create data dir", .{ .err = err, .path = app_dir_path });
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return app_dir_path;
|
|
||||||
}
|
|
||||||
115
src/browser/Browser.zig
Normal file
115
src/browser/Browser.zig
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
|
const js = @import("js/js.zig");
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
const App = @import("../App.zig");
|
||||||
|
const HttpClient = @import("../http/Client.zig");
|
||||||
|
const Notification = @import("../Notification.zig");
|
||||||
|
|
||||||
|
const Session = @import("Session.zig");
|
||||||
|
|
||||||
|
// Browser is an instance of the browser.
|
||||||
|
// You can create multiple browser instances.
|
||||||
|
// A browser contains only one session.
|
||||||
|
const Browser = @This();
|
||||||
|
|
||||||
|
env: *js.Env,
|
||||||
|
app: *App,
|
||||||
|
session: ?Session,
|
||||||
|
allocator: Allocator,
|
||||||
|
http_client: *HttpClient,
|
||||||
|
call_arena: ArenaAllocator,
|
||||||
|
page_arena: ArenaAllocator,
|
||||||
|
session_arena: ArenaAllocator,
|
||||||
|
transfer_arena: ArenaAllocator,
|
||||||
|
notification: *Notification,
|
||||||
|
|
||||||
|
pub fn init(app: *App) !Browser {
|
||||||
|
const allocator = app.allocator;
|
||||||
|
|
||||||
|
const env = try js.Env.init(allocator, &app.platform, .{});
|
||||||
|
errdefer env.deinit();
|
||||||
|
|
||||||
|
const notification = try Notification.init(allocator, app.notification);
|
||||||
|
app.http.client.notification = notification;
|
||||||
|
app.http.client.next_request_id = 0; // Should we track ids in CDP only?
|
||||||
|
errdefer notification.deinit();
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.app = app,
|
||||||
|
.env = env,
|
||||||
|
.session = null,
|
||||||
|
.allocator = allocator,
|
||||||
|
.notification = notification,
|
||||||
|
.http_client = app.http.client,
|
||||||
|
.call_arena = ArenaAllocator.init(allocator),
|
||||||
|
.page_arena = ArenaAllocator.init(allocator),
|
||||||
|
.session_arena = ArenaAllocator.init(allocator),
|
||||||
|
.transfer_arena = ArenaAllocator.init(allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Browser) void {
|
||||||
|
self.closeSession();
|
||||||
|
self.env.deinit();
|
||||||
|
self.call_arena.deinit();
|
||||||
|
self.page_arena.deinit();
|
||||||
|
self.session_arena.deinit();
|
||||||
|
self.transfer_arena.deinit();
|
||||||
|
self.http_client.notification = null;
|
||||||
|
self.notification.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn newSession(self: *Browser) !*Session {
|
||||||
|
self.closeSession();
|
||||||
|
self.session = @as(Session, undefined);
|
||||||
|
const session = &self.session.?;
|
||||||
|
try Session.init(session, self);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn closeSession(self: *Browser) void {
|
||||||
|
if (self.session) |*session| {
|
||||||
|
session.deinit();
|
||||||
|
self.session = null;
|
||||||
|
_ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||||
|
self.env.lowMemoryNotification();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runMicrotasks(self: *const Browser) void {
|
||||||
|
self.env.runMicrotasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runMessageLoop(self: *const Browser) void {
|
||||||
|
while (self.env.pumpMessageLoop()) {
|
||||||
|
log.debug(.browser, "pumpMessageLoop", .{});
|
||||||
|
}
|
||||||
|
self.env.runIdleTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
test "Browser" {
|
||||||
|
try testing.htmlRunner("browser.html", .{});
|
||||||
|
}
|
||||||
294
src/browser/EventManager.zig
Normal file
294
src/browser/EventManager.zig
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
const String = @import("../string.zig").String;
|
||||||
|
|
||||||
|
const js = @import("js/js.zig");
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
const Event = @import("webapi/Event.zig");
|
||||||
|
const EventTarget = @import("webapi/EventTarget.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
|
pub const EventManager = @This();
|
||||||
|
|
||||||
|
page: *Page,
|
||||||
|
arena: Allocator,
|
||||||
|
listener_pool: std.heap.MemoryPool(Listener),
|
||||||
|
lookup: std.AutoHashMapUnmanaged(usize, std.DoublyLinkedList),
|
||||||
|
|
||||||
|
pub fn init(page: *Page) EventManager {
|
||||||
|
return .{
|
||||||
|
.page = page,
|
||||||
|
.lookup = .{},
|
||||||
|
.arena = page.arena,
|
||||||
|
.listener_pool = std.heap.MemoryPool(Listener).init(page.arena),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const RegisterOptions = struct {
|
||||||
|
once: bool = false,
|
||||||
|
capture: bool = false,
|
||||||
|
passive: bool = false,
|
||||||
|
signal: ?*@import("webapi/AbortSignal.zig") = null,
|
||||||
|
};
|
||||||
|
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, function: js.Function, opts: RegisterOptions) !void {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a signal is provided and already aborted, don't register the listener
|
||||||
|
if (opts.signal) |signal| {
|
||||||
|
if (signal.getAborted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gop = try self.lookup.getOrPut(self.arena, @intFromPtr(target));
|
||||||
|
if (gop.found_existing) {
|
||||||
|
// check for duplicate functions already registered
|
||||||
|
var node = gop.value_ptr.first;
|
||||||
|
while (node) |n| {
|
||||||
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
|
if (listener.function.eql(function) and listener.capture == opts.capture) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node = n.next;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gop.value_ptr.* = .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener = try self.listener_pool.create();
|
||||||
|
listener.* = .{
|
||||||
|
.node = .{},
|
||||||
|
.once = opts.once,
|
||||||
|
.capture = opts.capture,
|
||||||
|
.passive = opts.passive,
|
||||||
|
.function = .{ .value = function },
|
||||||
|
.signal = opts.signal,
|
||||||
|
.typ = try String.init(self.arena, typ, .{}),
|
||||||
|
};
|
||||||
|
// append the listener to the list of listeners for this target
|
||||||
|
gop.value_ptr.append(&listener.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, function: js.Function, use_capture: bool) void {
|
||||||
|
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
|
||||||
|
if (findListener(list, typ, function, use_capture)) |listener| {
|
||||||
|
self.removeListener(list, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
||||||
|
}
|
||||||
|
event._target = target;
|
||||||
|
switch (target._type) {
|
||||||
|
.node => |node| try self.dispatchNode(node, event),
|
||||||
|
.xhr, .window, .abort_signal => {
|
||||||
|
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
|
||||||
|
try self.dispatchAll(list, target, event);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are a lot of events that can be attached via addEventListener or as
|
||||||
|
// a property, like the XHR events, or window.onload. You might think that the
|
||||||
|
// property is just a shortcut for calling addEventListener, but they are distinct.
|
||||||
|
// An event set via property cannot be removed by removeEventListener. If you
|
||||||
|
// set both the property and add a listener, they both execute.
|
||||||
|
const DispatchWithFunctionOptions = struct {
|
||||||
|
context: []const u8,
|
||||||
|
inject_target: bool = true,
|
||||||
|
};
|
||||||
|
pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *Event, function_: ?js.Function, comptime opts: DispatchWithFunctionOptions) !void {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.event, "dispatchWithFunction", .{ .type = event._type_string.str(), .context = opts.context, .has_function = function_ != null });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime opts.inject_target) {
|
||||||
|
event._target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (function_) |func| {
|
||||||
|
event._current_target = target;
|
||||||
|
func.call(void, .{event}) catch |err| {
|
||||||
|
// a non-JS error
|
||||||
|
log.warn(.event, opts.context, .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
|
||||||
|
try self.dispatchAll(list, target, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
|
||||||
|
var path_len: usize = 0;
|
||||||
|
var path_buffer: [128]*EventTarget = undefined;
|
||||||
|
|
||||||
|
var node: ?*Node = target;
|
||||||
|
while (node) |n| : (node = n._parent) {
|
||||||
|
if (path_len >= path_buffer.len) break;
|
||||||
|
path_buffer[path_len] = n.asEventTarget();
|
||||||
|
path_len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even though the window isn't part of the DOM, events always propagate
|
||||||
|
// through it in the capture phase
|
||||||
|
if (path_len < path_buffer.len) {
|
||||||
|
path_buffer[path_len] = self.page.window.asEventTarget();
|
||||||
|
path_len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = path_buffer[0..path_len];
|
||||||
|
|
||||||
|
// Phase 1: Capturing phase (root → target, excluding target)
|
||||||
|
// This happens for all events, regardless of bubbling
|
||||||
|
event._event_phase = .capturing_phase;
|
||||||
|
var i: usize = path_len;
|
||||||
|
while (i > 1) {
|
||||||
|
i -= 1;
|
||||||
|
const current_target = path[i];
|
||||||
|
if (self.lookup.getPtr(@intFromPtr(current_target))) |list| {
|
||||||
|
try self.dispatchPhase(list, current_target, event, true);
|
||||||
|
if (event._stop_propagation) {
|
||||||
|
event._event_phase = .none;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: At target
|
||||||
|
event._event_phase = .at_target;
|
||||||
|
const target_et = target.asEventTarget();
|
||||||
|
if (self.lookup.getPtr(@intFromPtr(target_et))) |list| {
|
||||||
|
try self.dispatchPhase(list, target_et, event, null);
|
||||||
|
if (event._stop_propagation) {
|
||||||
|
event._event_phase = .none;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Bubbling phase (target → root, excluding target)
|
||||||
|
// This only happens if the event bubbles
|
||||||
|
if (event._bubbles) {
|
||||||
|
event._event_phase = .bubbling_phase;
|
||||||
|
for (path[1..]) |current_target| {
|
||||||
|
if (self.lookup.getPtr(@intFromPtr(current_target))) |list| {
|
||||||
|
try self.dispatchPhase(list, current_target, event, false);
|
||||||
|
if (event._stop_propagation) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event._event_phase = .none;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, comptime capture_only: ?bool) !void {
|
||||||
|
const page = self.page;
|
||||||
|
const typ = event._type_string;
|
||||||
|
|
||||||
|
var node = list.first;
|
||||||
|
while (node) |n| {
|
||||||
|
// do this now, in case we need to remove n (once: true or aborted signal)
|
||||||
|
node = n.next;
|
||||||
|
|
||||||
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
|
if (!listener.typ.eql(typ)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can be null when dispatching to the target itself
|
||||||
|
if (comptime capture_only) |capture| {
|
||||||
|
if (listener.capture != capture) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the listener has an aborted signal, remove it and skip
|
||||||
|
if (listener.signal) |signal| {
|
||||||
|
if (signal.getAborted()) {
|
||||||
|
self.removeListener(list, listener);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event._current_target = current_target;
|
||||||
|
|
||||||
|
switch (listener.function) {
|
||||||
|
.value => |value| try value.call(void, .{event}),
|
||||||
|
.string => |string| {
|
||||||
|
const str = try page.call_arena.dupeZ(u8, string.str());
|
||||||
|
try self.page.js.eval(str, null);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listener.once) {
|
||||||
|
self.removeListener(list, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event._stop_immediate_propagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-Node dispatching (XHR, Window without propagation)
|
||||||
|
fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event) !void {
|
||||||
|
return self.dispatchPhase(list, current_target, event, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {
|
||||||
|
list.remove(&listener.node);
|
||||||
|
self.listener_pool.destroy(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, function: js.Function, capture: bool) ?*Listener {
|
||||||
|
var node = list.first;
|
||||||
|
while (node) |n| {
|
||||||
|
node = n.next;
|
||||||
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
|
if (!listener.function.eql(function)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (listener.capture != capture) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!listener.typ.eqlSlice(typ)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return listener;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Listener = struct {
|
||||||
|
typ: String,
|
||||||
|
once: bool,
|
||||||
|
capture: bool,
|
||||||
|
passive: bool,
|
||||||
|
function: Function,
|
||||||
|
signal: ?*@import("webapi/AbortSignal.zig") = null,
|
||||||
|
node: std.DoublyLinkedList.Node,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Function = union(enum) {
|
||||||
|
value: js.Function,
|
||||||
|
string: String,
|
||||||
|
|
||||||
|
fn eql(self: Function, func: js.Function) bool {
|
||||||
|
return switch (self) {
|
||||||
|
.string => false,
|
||||||
|
.value => |v| return v.id == func.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
378
src/browser/Factory.zig
Normal file
378
src/browser/Factory.zig
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const reflect = @import("reflect.zig");
|
||||||
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
const String = @import("../string.zig").String;
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
const Event = @import("webapi/Event.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Document = @import("webapi/Document.zig");
|
||||||
|
const EventTarget = @import("webapi/EventTarget.zig");
|
||||||
|
const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig");
|
||||||
|
|
||||||
|
const MemoryPoolAligned = std.heap.MemoryPoolAligned;
|
||||||
|
|
||||||
|
// 1. Generally, wrapping an ArenaAllocator within an ArenaAllocator doesn't make
|
||||||
|
// much sense. But wrapping a MemoryPool within an Arena does. Specifically, by
|
||||||
|
// doing so, we solve a major issue with Arena: freed memory can be re-used [for
|
||||||
|
// more of the same size].
|
||||||
|
// 2. Normally, you have a MemoryPool(T) where T is a `User` or something. Then
|
||||||
|
// the MemoryPool can be used for creating users. But in reality, that memory
|
||||||
|
// created by that pool could be re-used for anything with the same size (or less)
|
||||||
|
// than a User (and a compatible alignment). So that's what we do - we have size
|
||||||
|
// (and alignment) based pools.
|
||||||
|
const Factory = @This();
|
||||||
|
_page: *Page,
|
||||||
|
_size_1_8: MemoryPoolAligned([1]u8, .@"8"),
|
||||||
|
_size_8_8: MemoryPoolAligned([8]u8, .@"8"),
|
||||||
|
_size_16_8: MemoryPoolAligned([16]u8, .@"8"),
|
||||||
|
_size_24_8: MemoryPoolAligned([24]u8, .@"8"),
|
||||||
|
_size_32_8: MemoryPoolAligned([32]u8, .@"8"),
|
||||||
|
_size_32_16: MemoryPoolAligned([32]u8, .@"16"),
|
||||||
|
_size_40_8: MemoryPoolAligned([40]u8, .@"8"),
|
||||||
|
_size_48_16: MemoryPoolAligned([48]u8, .@"16"),
|
||||||
|
_size_56_8: MemoryPoolAligned([56]u8, .@"8"),
|
||||||
|
_size_64_16: MemoryPoolAligned([64]u8, .@"16"),
|
||||||
|
_size_72_8: MemoryPoolAligned([72]u8, .@"8"),
|
||||||
|
_size_80_16: MemoryPoolAligned([80]u8, .@"16"),
|
||||||
|
_size_88_8: MemoryPoolAligned([88]u8, .@"8"),
|
||||||
|
_size_96_16: MemoryPoolAligned([96]u8, .@"16"),
|
||||||
|
_size_104_8: MemoryPoolAligned([104]u8, .@"8"),
|
||||||
|
_size_112_8: MemoryPoolAligned([112]u8, .@"8"),
|
||||||
|
_size_120_8: MemoryPoolAligned([120]u8, .@"8"),
|
||||||
|
_size_128_8: MemoryPoolAligned([128]u8, .@"8"),
|
||||||
|
_size_144_8: MemoryPoolAligned([144]u8, .@"8"),
|
||||||
|
_size_456_8: MemoryPoolAligned([456]u8, .@"8"),
|
||||||
|
_size_520_8: MemoryPoolAligned([520]u8, .@"8"),
|
||||||
|
_size_648_8: MemoryPoolAligned([648]u8, .@"8"),
|
||||||
|
|
||||||
|
pub fn init(page: *Page) Factory {
|
||||||
|
return .{
|
||||||
|
._page = page,
|
||||||
|
._size_1_8 = MemoryPoolAligned([1]u8, .@"8").init(page.arena),
|
||||||
|
._size_8_8 = MemoryPoolAligned([8]u8, .@"8").init(page.arena),
|
||||||
|
._size_16_8 = MemoryPoolAligned([16]u8, .@"8").init(page.arena),
|
||||||
|
._size_24_8 = MemoryPoolAligned([24]u8, .@"8").init(page.arena),
|
||||||
|
._size_32_8 = MemoryPoolAligned([32]u8, .@"8").init(page.arena),
|
||||||
|
._size_32_16 = MemoryPoolAligned([32]u8, .@"16").init(page.arena),
|
||||||
|
._size_40_8 = MemoryPoolAligned([40]u8, .@"8").init(page.arena),
|
||||||
|
._size_48_16 = MemoryPoolAligned([48]u8, .@"16").init(page.arena),
|
||||||
|
._size_56_8 = MemoryPoolAligned([56]u8, .@"8").init(page.arena),
|
||||||
|
._size_64_16 = MemoryPoolAligned([64]u8, .@"16").init(page.arena),
|
||||||
|
._size_72_8 = MemoryPoolAligned([72]u8, .@"8").init(page.arena),
|
||||||
|
._size_80_16 = MemoryPoolAligned([80]u8, .@"16").init(page.arena),
|
||||||
|
._size_88_8 = MemoryPoolAligned([88]u8, .@"8").init(page.arena),
|
||||||
|
._size_96_16 = MemoryPoolAligned([96]u8, .@"16").init(page.arena),
|
||||||
|
._size_104_8 = MemoryPoolAligned([104]u8, .@"8").init(page.arena),
|
||||||
|
._size_112_8 = MemoryPoolAligned([112]u8, .@"8").init(page.arena),
|
||||||
|
._size_120_8 = MemoryPoolAligned([120]u8, .@"8").init(page.arena),
|
||||||
|
._size_128_8 = MemoryPoolAligned([128]u8, .@"8").init(page.arena),
|
||||||
|
._size_144_8 = MemoryPoolAligned([144]u8, .@"8").init(page.arena),
|
||||||
|
._size_456_8 = MemoryPoolAligned([456]u8, .@"8").init(page.arena),
|
||||||
|
._size_520_8 = MemoryPoolAligned([520]u8, .@"8").init(page.arena),
|
||||||
|
._size_648_8 = MemoryPoolAligned([648]u8, .@"8").init(page.arena),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a root object
|
||||||
|
pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
const child_ptr = try self.createT(@TypeOf(child));
|
||||||
|
child_ptr.* = child;
|
||||||
|
|
||||||
|
const et = try self.createT(EventTarget);
|
||||||
|
child_ptr._proto = et;
|
||||||
|
et.* = .{ ._type = unionInit(EventTarget.Type, child_ptr) };
|
||||||
|
return child_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
const child_ptr = try self.createT(@TypeOf(child));
|
||||||
|
child_ptr.* = child;
|
||||||
|
child_ptr._proto = try self.eventTarget(Node{
|
||||||
|
._proto = undefined,
|
||||||
|
._type = unionInit(Node.Type, child_ptr),
|
||||||
|
});
|
||||||
|
return child_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn document(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
const child_ptr = try self.createT(@TypeOf(child));
|
||||||
|
child_ptr.* = child;
|
||||||
|
child_ptr._proto = try self.node(Document{
|
||||||
|
._proto = undefined,
|
||||||
|
._type = unionInit(Document.Type, child_ptr),
|
||||||
|
});
|
||||||
|
return child_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn element(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
const child_ptr = try self.createT(@TypeOf(child));
|
||||||
|
child_ptr.* = child;
|
||||||
|
child_ptr._proto = try self.node(Element{
|
||||||
|
._proto = undefined,
|
||||||
|
._type = unionInit(Element.Type, child_ptr),
|
||||||
|
});
|
||||||
|
return child_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn htmlElement(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
if (comptime fieldIsPointer(Element.Html.Type, @TypeOf(child))) {
|
||||||
|
const child_ptr = try self.createT(@TypeOf(child));
|
||||||
|
child_ptr.* = child;
|
||||||
|
child_ptr._proto = try self.element(Element.Html{
|
||||||
|
._proto = undefined,
|
||||||
|
._type = unionInit(Element.Html.Type, child_ptr),
|
||||||
|
});
|
||||||
|
return child_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our union type fields are usually pointers. But, at the leaf, they
|
||||||
|
// can be struct (if all they contain is the `_proto` field, then we might
|
||||||
|
// as well store it directly in the struct).
|
||||||
|
|
||||||
|
const html = try self.element(Element.Html{
|
||||||
|
._proto = undefined,
|
||||||
|
._type = unionInit(Element.Html.Type, child),
|
||||||
|
});
|
||||||
|
const field_name = comptime unionFieldName(Element.Html.Type, @TypeOf(child));
|
||||||
|
var child_ptr = &@field(html._type, field_name);
|
||||||
|
child_ptr._proto = html;
|
||||||
|
return child_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeOf(child) {
|
||||||
|
if (@TypeOf(child) == Element.Svg) {
|
||||||
|
return self.element(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
// will never allocate, can't fail
|
||||||
|
const tag_name_str = String.init(undefined, tag_name, .{}) catch unreachable;
|
||||||
|
|
||||||
|
if (comptime fieldIsPointer(Element.Svg.Type, @TypeOf(child))) {
|
||||||
|
const child_ptr = try self.createT(@TypeOf(child));
|
||||||
|
child_ptr.* = child;
|
||||||
|
child_ptr._proto = try self.element(Element.Svg{
|
||||||
|
._proto = undefined,
|
||||||
|
._tag_name = tag_name_str,
|
||||||
|
._type = unionInit(Element.Svg.Type, child_ptr),
|
||||||
|
});
|
||||||
|
return child_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our union type fields are usually pointers. But, at the leaf, they
|
||||||
|
// can be struct (if all they contain is the `_proto` field, then we might
|
||||||
|
// as well store it directly in the struct).
|
||||||
|
const svg = try self.element(Element.Svg{
|
||||||
|
._proto = undefined,
|
||||||
|
._tag_name = tag_name_str,
|
||||||
|
._type = unionInit(Element.Svg.Type, child),
|
||||||
|
});
|
||||||
|
const field_name = comptime unionFieldName(Element.Svg.Type, @TypeOf(child));
|
||||||
|
var child_ptr = &@field(svg._type, field_name);
|
||||||
|
child_ptr._proto = svg;
|
||||||
|
return child_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a root object
|
||||||
|
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
|
||||||
|
const child_ptr = try self.createT(@TypeOf(child));
|
||||||
|
child_ptr.* = child;
|
||||||
|
|
||||||
|
const e = try self.createT(Event);
|
||||||
|
child_ptr._proto = e;
|
||||||
|
e.* = .{
|
||||||
|
._type = unionInit(Event.Type, child_ptr),
|
||||||
|
._type_string = try String.init(self._page.arena, typ, .{}),
|
||||||
|
};
|
||||||
|
return child_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
const et = try self.eventTarget(XMLHttpRequestEventTarget{
|
||||||
|
._proto = undefined,
|
||||||
|
._type = unionInit(XMLHttpRequestEventTarget.Type, child),
|
||||||
|
});
|
||||||
|
const field_name = comptime unionFieldName(XMLHttpRequestEventTarget.Type, @TypeOf(child));
|
||||||
|
var child_ptr = &@field(et._type, field_name);
|
||||||
|
child_ptr._proto = et;
|
||||||
|
return child_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(self: *Factory, value: anytype) !*@TypeOf(value) {
|
||||||
|
const ptr = try self.createT(@TypeOf(value));
|
||||||
|
ptr.* = value;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createT(self: *Factory, comptime T: type) !*T {
|
||||||
|
const SO = @sizeOf(T);
|
||||||
|
if (comptime SO == 1) return @ptrCast(try self._size_1_8.create());
|
||||||
|
if (comptime SO == 8) return @ptrCast(try self._size_8_8.create());
|
||||||
|
if (comptime SO == 16) return @ptrCast(try self._size_16_8.create());
|
||||||
|
if (comptime SO == 24) return @ptrCast(try self._size_24_8.create());
|
||||||
|
if (comptime SO == 32) {
|
||||||
|
if (comptime @alignOf(T) == 8) return @ptrCast(try self._size_32_8.create());
|
||||||
|
if (comptime @alignOf(T) == 16) return @ptrCast(try self._size_32_16.create());
|
||||||
|
}
|
||||||
|
if (comptime SO == 40) return @ptrCast(try self._size_40_8.create());
|
||||||
|
if (comptime SO == 48) return @ptrCast(try self._size_48_16.create());
|
||||||
|
if (comptime SO == 56) return @ptrCast(try self._size_56_8.create());
|
||||||
|
if (comptime SO == 64) return @ptrCast(try self._size_64_16.create());
|
||||||
|
if (comptime SO == 72) return @ptrCast(try self._size_72_8.create());
|
||||||
|
if (comptime SO == 80) return @ptrCast(try self._size_80_16.create());
|
||||||
|
if (comptime SO == 88) return @ptrCast(try self._size_88_8.create());
|
||||||
|
if (comptime SO == 96) return @ptrCast(try self._size_96_16.create());
|
||||||
|
if (comptime SO == 104) return @ptrCast(try self._size_104_8.create());
|
||||||
|
if (comptime SO == 112) return @ptrCast(try self._size_112_8.create());
|
||||||
|
if (comptime SO == 120) return @ptrCast(try self._size_120_8.create());
|
||||||
|
if (comptime SO == 128) return @ptrCast(try self._size_128_8.create());
|
||||||
|
if (comptime SO == 144) return @ptrCast(try self._size_144_8.create());
|
||||||
|
if (comptime SO == 456) return @ptrCast(try self._size_456_8.create());
|
||||||
|
if (comptime SO == 520) return @ptrCast(try self._size_520_8.create());
|
||||||
|
if (comptime SO == 648) return @ptrCast(try self._size_648_8.create());
|
||||||
|
@compileError(std.fmt.comptimePrint("No pool configured for @sizeOf({d}), @alignOf({d}): ({s})", .{ SO, @alignOf(T), @typeName(T) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn destroy(self: *Factory, value: anytype) void {
|
||||||
|
const S = reflect.Struct(@TypeOf(value));
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
// We should always destroy from the leaf down.
|
||||||
|
if (@hasField(S, "_type") and @typeInfo(@TypeOf(value._type)) == .@"union") {
|
||||||
|
// A Event{._type == .generic} (or any other similar types)
|
||||||
|
// _should_ be destoyed directly. The _type = .generic is a pseudo
|
||||||
|
// child
|
||||||
|
if (S != Event or value._type != .generic) {
|
||||||
|
log.fatal(.bug, "factory.destroy.event", .{ .type = @typeName(S) });
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.destroyChain(value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void {
|
||||||
|
const S = reflect.Struct(@TypeOf(value));
|
||||||
|
|
||||||
|
// This is initially called from a deinit. We don't want to call that
|
||||||
|
// same deinit. So when this is the first time destroyChain is called
|
||||||
|
// we don't call deinit (because we're in that deinit)
|
||||||
|
if (!comptime first) {
|
||||||
|
// But if it isn't the first time
|
||||||
|
if (@hasDecl(S, "deinit")) {
|
||||||
|
// And it has a deinit, we'll call it
|
||||||
|
switch (@typeInfo(@TypeOf(S.deinit)).@"fn".params.len) {
|
||||||
|
1 => value.deinit(),
|
||||||
|
2 => value.deinit(self._page),
|
||||||
|
else => @compileLog(@typeName(S) ++ " has an invalid deinit function"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@hasField(S, "_proto")) {
|
||||||
|
self.destroyChain(value._proto, false);
|
||||||
|
} else if (@hasDecl(S, "JsApi")) {
|
||||||
|
// Doesn't have a _proto, but has a JsApi.
|
||||||
|
if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| {
|
||||||
|
self._size_24_8.destroy(@ptrCast(tagged));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaf types are allowed by be placed directly within their _proto
|
||||||
|
// (which makes sense when the @sizeOf(Leaf) == 8). These don't need to
|
||||||
|
// be (cannot be) freed. But we'll still free the chain.
|
||||||
|
if (comptime wasAllocated(S)) {
|
||||||
|
switch (@sizeOf(S)) {
|
||||||
|
1 => self._size_1_8.destroy(@ptrCast(@alignCast(value))),
|
||||||
|
8 => self._size_8_8.destroy(@ptrCast(@alignCast(value))),
|
||||||
|
16 => self._size_16_8.destroy(@ptrCast(value)),
|
||||||
|
24 => self._size_24_8.destroy(@ptrCast(value)),
|
||||||
|
32 => {
|
||||||
|
if (comptime @alignOf(S) == 8) {
|
||||||
|
self._size_32_8.destroy(@ptrCast(value));
|
||||||
|
} else if (comptime @alignOf(S) == 16) {
|
||||||
|
self._size_32_16.destroy(@ptrCast(value));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
40 => self._size_40_8.destroy(@ptrCast(value)),
|
||||||
|
48 => self._size_48_16.destroy(@ptrCast(@alignCast(value))),
|
||||||
|
56 => self._size_56_8.destroy(@ptrCast(value)),
|
||||||
|
64 => self._size_64_16.destroy(@ptrCast(@alignCast(value))),
|
||||||
|
72 => self._size_72_8.destroy(@ptrCast(@alignCast(value))),
|
||||||
|
80 => self._size_80_16.destroy(@ptrCast(@alignCast(value))),
|
||||||
|
88 => self._size_88_8.destroy(@ptrCast(@alignCast(value))),
|
||||||
|
96 => self._size_96_16.destroy(@ptrCast(@alignCast(value))),
|
||||||
|
104 => self._size_104_8.destroy(@ptrCast(value)),
|
||||||
|
112 => self._size_112_8.destroy(@ptrCast(value)),
|
||||||
|
120 => self._size_120_8.destroy(@ptrCast(value)),
|
||||||
|
128 => self._size_128_8.destroy(@ptrCast(value)),
|
||||||
|
144 => self._size_144_8.destroy(@ptrCast(value)),
|
||||||
|
456 => self._size_456_8.destroy(@ptrCast(value)),
|
||||||
|
520 => self._size_520_8.destroy(@ptrCast(value)),
|
||||||
|
648 => self._size_648_8.destroy(@ptrCast(value)),
|
||||||
|
else => |SO| @compileError(std.fmt.comptimePrint("Don't know what I'm being asked to destroy @sizeOf({d}), @alignOf({d}): ({s})", .{ SO, @alignOf(S), @typeName(S) })),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wasAllocated(comptime S: type) bool {
|
||||||
|
// Whether it's heap allocate or not, we should have a pointer.
|
||||||
|
// (If it isn't heap allocated, it'll be a pointer from the proto's type
|
||||||
|
// e.g. &html._type.title)
|
||||||
|
if (!@hasField(S, "_proto")) {
|
||||||
|
// a root is always on the heap.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the _proto type
|
||||||
|
const P = reflect.Struct(std.meta.fieldInfo(S, ._proto).type);
|
||||||
|
|
||||||
|
// the _proto._type type (the parent's _type union)
|
||||||
|
const U = std.meta.fieldInfo(P, ._type).type;
|
||||||
|
inline for (@typeInfo(U).@"union".fields) |field| {
|
||||||
|
if (field.type == S) {
|
||||||
|
// One of the types in the proto's _type union is this non-pointer
|
||||||
|
// structure, so it isn't heap allocted.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unionInit(comptime T: type, value: anytype) T {
|
||||||
|
const V = @TypeOf(value);
|
||||||
|
const field_name = comptime unionFieldName(T, V);
|
||||||
|
return @unionInit(T, field_name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// There can be friction between comptime and runtime. Comptime has to
|
||||||
|
// account for all possible types, even if some runtime flow makes certain
|
||||||
|
// cases impossible. At runtime, we always call `unionFieldName` with the
|
||||||
|
// correct struct or pointer type. But at comptime time, `unionFieldName`
|
||||||
|
// is called with both variants (S and *S). So we use reflect.Struct().
|
||||||
|
// This only works because we never have a union with a field S and another
|
||||||
|
// field *S.
|
||||||
|
fn unionFieldName(comptime T: type, comptime V: type) []const u8 {
|
||||||
|
inline for (@typeInfo(T).@"union".fields) |field| {
|
||||||
|
if (reflect.Struct(field.type) == reflect.Struct(V)) {
|
||||||
|
return field.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@compileError(@typeName(V) ++ " is not a valid type for " ++ @typeName(T) ++ ".type");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fieldIsPointer(comptime T: type, comptime V: type) bool {
|
||||||
|
inline for (@typeInfo(T).@"union".fields) |field| {
|
||||||
|
if (field.type == V) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (field.type == *V) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@compileError(@typeName(V) ++ " is not a valid type for " ++ @typeName(T) ++ ".type");
|
||||||
|
}
|
||||||
518
src/browser/Mime.zig
Normal file
518
src/browser/Mime.zig
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Mime = @This();
|
||||||
|
content_type: ContentType,
|
||||||
|
params: []const u8 = "",
|
||||||
|
// IANA defines max. charset value length as 40.
|
||||||
|
// We keep 41 for null-termination since HTML parser expects in this format.
|
||||||
|
charset: [41]u8 = default_charset,
|
||||||
|
|
||||||
|
/// String "UTF-8" continued by null characters.
|
||||||
|
pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
||||||
|
|
||||||
|
/// Mime with unknown Content-Type, empty params and empty charset.
|
||||||
|
pub const unknown = Mime{ .content_type = .{ .unknown = {} } };
|
||||||
|
|
||||||
|
pub const ContentTypeEnum = enum {
|
||||||
|
text_xml,
|
||||||
|
text_html,
|
||||||
|
text_javascript,
|
||||||
|
text_plain,
|
||||||
|
text_css,
|
||||||
|
application_json,
|
||||||
|
unknown,
|
||||||
|
other,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ContentType = union(ContentTypeEnum) {
|
||||||
|
text_xml: void,
|
||||||
|
text_html: void,
|
||||||
|
text_javascript: void,
|
||||||
|
text_plain: void,
|
||||||
|
text_css: void,
|
||||||
|
application_json: void,
|
||||||
|
unknown: void,
|
||||||
|
other: struct { type: []const u8, sub_type: []const u8 },
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Returns the null-terminated charset value.
|
||||||
|
pub fn charsetString(mime: *const Mime) [:0]const u8 {
|
||||||
|
return @ptrCast(&mime.charset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes quotes of value if quotes are given.
|
||||||
|
///
|
||||||
|
/// Currently we don't validate the charset.
|
||||||
|
/// See section 2.3 Naming Requirements:
|
||||||
|
/// https://datatracker.ietf.org/doc/rfc2978/
|
||||||
|
fn parseCharset(value: []const u8) error{ CharsetTooBig, Invalid }![]const u8 {
|
||||||
|
// Cannot be larger than 40.
|
||||||
|
// https://datatracker.ietf.org/doc/rfc2978/
|
||||||
|
if (value.len > 40) return error.CharsetTooBig;
|
||||||
|
|
||||||
|
// If the first char is a quote, look for a pair.
|
||||||
|
if (value[0] == '"') {
|
||||||
|
if (value.len < 3 or value[value.len - 1] != '"') {
|
||||||
|
return error.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value[1 .. value.len - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// No quotes.
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(input: []u8) !Mime {
|
||||||
|
if (input.len > 255) {
|
||||||
|
return error.TooBig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zig's trim API is broken. The return type is always `[]const u8`,
|
||||||
|
// even if the input type is `[]u8`. @constCast is safe here.
|
||||||
|
var normalized = @constCast(std.mem.trim(u8, input, &std.ascii.whitespace));
|
||||||
|
_ = std.ascii.lowerString(normalized, normalized);
|
||||||
|
|
||||||
|
const content_type, const type_len = try parseContentType(normalized);
|
||||||
|
if (type_len >= normalized.len) {
|
||||||
|
return .{ .content_type = content_type };
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = trimLeft(normalized[type_len..]);
|
||||||
|
|
||||||
|
var charset: [41]u8 = undefined;
|
||||||
|
|
||||||
|
var it = std.mem.splitScalar(u8, params, ';');
|
||||||
|
while (it.next()) |attr| {
|
||||||
|
const i = std.mem.indexOfScalarPos(u8, attr, 0, '=') orelse return error.Invalid;
|
||||||
|
const name = trimLeft(attr[0..i]);
|
||||||
|
|
||||||
|
const value = trimRight(attr[i + 1 ..]);
|
||||||
|
if (value.len == 0) {
|
||||||
|
return error.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attribute_name = std.meta.stringToEnum(enum {
|
||||||
|
charset,
|
||||||
|
}, name) orelse continue;
|
||||||
|
|
||||||
|
switch (attribute_name) {
|
||||||
|
.charset => {
|
||||||
|
if (value.len == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attribute_value = try parseCharset(value);
|
||||||
|
@memcpy(charset[0..attribute_value.len], attribute_value);
|
||||||
|
// Null-terminate right after attribute value.
|
||||||
|
charset[attribute_value.len] = 0;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.params = params,
|
||||||
|
.charset = charset,
|
||||||
|
.content_type = content_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sniff(body: []const u8) ?Mime {
|
||||||
|
// 0x0C is form feed
|
||||||
|
const content = std.mem.trimLeft(u8, body, &.{ ' ', '\t', '\n', '\r', 0x0C });
|
||||||
|
if (content.len == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content[0] != '<') {
|
||||||
|
if (std.mem.startsWith(u8, content, &.{ 0xEF, 0xBB, 0xBF })) {
|
||||||
|
// UTF-8 BOM
|
||||||
|
return .{ .content_type = .{ .text_plain = {} } };
|
||||||
|
}
|
||||||
|
if (std.mem.startsWith(u8, content, &.{ 0xFE, 0xFF })) {
|
||||||
|
// UTF-16 big-endian BOM
|
||||||
|
return .{ .content_type = .{ .text_plain = {} } };
|
||||||
|
}
|
||||||
|
if (std.mem.startsWith(u8, content, &.{ 0xFF, 0xFE })) {
|
||||||
|
// UTF-16 little-endian BOM
|
||||||
|
return .{ .content_type = .{ .text_plain = {} } };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The longest prefix we have is "<!DOCTYPE HTML ", 15 bytes. If we're
|
||||||
|
// here, we already know content[0] == '<', so we can skip that. So 14
|
||||||
|
// bytes.
|
||||||
|
|
||||||
|
// +1 because we don't need the leading '<'
|
||||||
|
var buf: [14]u8 = undefined;
|
||||||
|
|
||||||
|
const stripped = content[1..];
|
||||||
|
const prefix_len = @min(stripped.len, buf.len);
|
||||||
|
const prefix = std.ascii.lowerString(&buf, stripped[0..prefix_len]);
|
||||||
|
|
||||||
|
// we already know it starts with a <
|
||||||
|
const known_prefixes = [_]struct { []const u8, ContentType }{
|
||||||
|
.{ "!doctype html", .{ .text_html = {} } },
|
||||||
|
.{ "html", .{ .text_html = {} } },
|
||||||
|
.{ "script", .{ .text_html = {} } },
|
||||||
|
.{ "iframe", .{ .text_html = {} } },
|
||||||
|
.{ "h1", .{ .text_html = {} } },
|
||||||
|
.{ "div", .{ .text_html = {} } },
|
||||||
|
.{ "font", .{ .text_html = {} } },
|
||||||
|
.{ "table", .{ .text_html = {} } },
|
||||||
|
.{ "a", .{ .text_html = {} } },
|
||||||
|
.{ "style", .{ .text_html = {} } },
|
||||||
|
.{ "title", .{ .text_html = {} } },
|
||||||
|
.{ "b", .{ .text_html = {} } },
|
||||||
|
.{ "body", .{ .text_html = {} } },
|
||||||
|
.{ "br", .{ .text_html = {} } },
|
||||||
|
.{ "p", .{ .text_html = {} } },
|
||||||
|
.{ "!--", .{ .text_html = {} } },
|
||||||
|
.{ "xml", .{ .text_xml = {} } },
|
||||||
|
};
|
||||||
|
inline for (known_prefixes) |kp| {
|
||||||
|
const known_prefix = kp.@"0";
|
||||||
|
if (std.mem.startsWith(u8, prefix, known_prefix) and prefix.len > known_prefix.len) {
|
||||||
|
const next = prefix[known_prefix.len];
|
||||||
|
// a "tag-terminating-byte"
|
||||||
|
if (next == ' ' or next == '>') {
|
||||||
|
return .{ .content_type = kp.@"1" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isHTML(self: *const Mime) bool {
|
||||||
|
return self.content_type == .text_html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we expect value to be lowercase
|
||||||
|
fn parseContentType(value: []const u8) !struct { ContentType, usize } {
|
||||||
|
const end = std.mem.indexOfScalarPos(u8, value, 0, ';') orelse value.len;
|
||||||
|
const type_name = trimRight(value[0..end]);
|
||||||
|
const attribute_start = end + 1;
|
||||||
|
|
||||||
|
if (std.meta.stringToEnum(enum {
|
||||||
|
@"text/xml",
|
||||||
|
@"text/html",
|
||||||
|
@"text/css",
|
||||||
|
@"text/plain",
|
||||||
|
|
||||||
|
@"text/javascript",
|
||||||
|
@"application/javascript",
|
||||||
|
@"application/x-javascript",
|
||||||
|
|
||||||
|
@"application/json",
|
||||||
|
}, type_name)) |known_type| {
|
||||||
|
const ct: ContentType = switch (known_type) {
|
||||||
|
.@"text/xml" => .{ .text_xml = {} },
|
||||||
|
.@"text/html" => .{ .text_html = {} },
|
||||||
|
.@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} },
|
||||||
|
.@"text/plain" => .{ .text_plain = {} },
|
||||||
|
.@"text/css" => .{ .text_css = {} },
|
||||||
|
.@"application/json" => .{ .application_json = {} },
|
||||||
|
};
|
||||||
|
return .{ ct, attribute_start };
|
||||||
|
}
|
||||||
|
|
||||||
|
const separator = std.mem.indexOfScalarPos(u8, type_name, 0, '/') orelse return error.Invalid;
|
||||||
|
|
||||||
|
const main_type = value[0..separator];
|
||||||
|
const sub_type = trimRight(value[separator + 1 .. end]);
|
||||||
|
|
||||||
|
if (main_type.len == 0 or validType(main_type) == false) {
|
||||||
|
return error.Invalid;
|
||||||
|
}
|
||||||
|
if (sub_type.len == 0 or validType(sub_type) == false) {
|
||||||
|
return error.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ .{ .other = .{
|
||||||
|
.type = main_type,
|
||||||
|
.sub_type = sub_type,
|
||||||
|
} }, attribute_start };
|
||||||
|
}
|
||||||
|
|
||||||
|
const T_SPECIAL = blk: {
|
||||||
|
var v = [_]bool{false} ** 256;
|
||||||
|
for ("()<>@,;:\\\"/[]?=") |b| {
|
||||||
|
v[b] = true;
|
||||||
|
}
|
||||||
|
break :blk v;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VALID_CODEPOINTS = blk: {
|
||||||
|
var v: [256]bool = undefined;
|
||||||
|
for (0..256) |i| {
|
||||||
|
v[i] = std.ascii.isAlphanumeric(i);
|
||||||
|
}
|
||||||
|
for ("!#$%&\\*+-.^'_`|~") |b| {
|
||||||
|
v[b] = true;
|
||||||
|
}
|
||||||
|
break :blk v;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn validType(value: []const u8) bool {
|
||||||
|
for (value) |b| {
|
||||||
|
if (VALID_CODEPOINTS[b] == false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trimLeft(s: []const u8) []const u8 {
|
||||||
|
return std.mem.trimLeft(u8, s, &std.ascii.whitespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trimRight(s: []const u8) []const u8 {
|
||||||
|
return std.mem.trimRight(u8, s, &std.ascii.whitespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
test "Mime: invalid" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const invalids = [_][]const u8{
|
||||||
|
"",
|
||||||
|
"text",
|
||||||
|
"text /html",
|
||||||
|
"text/ html",
|
||||||
|
"text / html",
|
||||||
|
"text/html other",
|
||||||
|
"text/html; x",
|
||||||
|
"text/html; x=",
|
||||||
|
"text/html; x= ",
|
||||||
|
"text/html; = ",
|
||||||
|
"text/html;=",
|
||||||
|
"text/html; charset=\"\"",
|
||||||
|
"text/html; charset=\"",
|
||||||
|
"text/html; charset=\"\\",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (invalids) |invalid| {
|
||||||
|
const mutable_input = try testing.arena_allocator.dupe(u8, invalid);
|
||||||
|
try testing.expectError(error.Invalid, Mime.parse(mutable_input));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Mime: parse common" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .text_xml = {} } }, "text/xml");
|
||||||
|
try expect(.{ .content_type = .{ .text_html = {} } }, "text/html");
|
||||||
|
try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .text_xml = {} } }, "text/xml;");
|
||||||
|
try expect(.{ .content_type = .{ .text_html = {} } }, "text/html;");
|
||||||
|
try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain;");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .text_xml = {} } }, " \ttext/xml");
|
||||||
|
try expect(.{ .content_type = .{ .text_html = {} } }, "text/html ");
|
||||||
|
try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain \t\t");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .text_xml = {} } }, "TEXT/xml");
|
||||||
|
try expect(.{ .content_type = .{ .text_html = {} } }, "text/Html");
|
||||||
|
try expect(.{ .content_type = .{ .text_plain = {} } }, "TEXT/PLAIN");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .text_xml = {} } }, " TeXT/xml");
|
||||||
|
try expect(.{ .content_type = .{ .text_html = {} } }, "teXt/HtML ;");
|
||||||
|
try expect(.{ .content_type = .{ .text_plain = {} } }, "tExT/PlAiN;");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .text_javascript = {} } }, "text/javascript");
|
||||||
|
try expect(.{ .content_type = .{ .text_javascript = {} } }, "Application/JavaScript");
|
||||||
|
try expect(.{ .content_type = .{ .text_javascript = {} } }, "application/x-javascript");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .application_json = {} } }, "application/json");
|
||||||
|
try expect(.{ .content_type = .{ .text_css = {} } }, "text/css");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Mime: parse uncommon" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const text_csv = Expectation{
|
||||||
|
.content_type = .{ .other = .{ .type = "text", .sub_type = "csv" } },
|
||||||
|
};
|
||||||
|
try expect(text_csv, "text/csv");
|
||||||
|
try expect(text_csv, "text/csv;");
|
||||||
|
try expect(text_csv, " text/csv\t ");
|
||||||
|
try expect(text_csv, " text/csv\t ;");
|
||||||
|
|
||||||
|
try expect(
|
||||||
|
.{ .content_type = .{ .other = .{ .type = "text", .sub_type = "csv" } } },
|
||||||
|
"Text/CSV",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Mime: parse charset" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_xml = {} },
|
||||||
|
.charset = "utf-8",
|
||||||
|
.params = "charset=utf-8",
|
||||||
|
}, "text/xml; charset=utf-8");
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_xml = {} },
|
||||||
|
.charset = "utf-8",
|
||||||
|
.params = "charset=\"utf-8\"",
|
||||||
|
}, "text/xml;charset=\"UTF-8\"");
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_html = {} },
|
||||||
|
.charset = "iso-8859-1",
|
||||||
|
.params = "charset=\"iso-8859-1\"",
|
||||||
|
}, "text/html; charset=\"iso-8859-1\"");
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_html = {} },
|
||||||
|
.charset = "iso-8859-1",
|
||||||
|
.params = "charset=\"iso-8859-1\"",
|
||||||
|
}, "text/html; charset=\"ISO-8859-1\"");
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_xml = {} },
|
||||||
|
.charset = "custom-non-standard-charset-value",
|
||||||
|
.params = "charset=\"custom-non-standard-charset-value\"",
|
||||||
|
}, "text/xml;charset=\"custom-non-standard-charset-value\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Mime: isHTML" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const assert = struct {
|
||||||
|
fn assert(expected: bool, input: []const u8) !void {
|
||||||
|
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||||
|
var mime = try Mime.parse(mutable_input);
|
||||||
|
try testing.expectEqual(expected, mime.isHTML());
|
||||||
|
}
|
||||||
|
}.assert;
|
||||||
|
try assert(true, "text/html");
|
||||||
|
try assert(true, "text/html;");
|
||||||
|
try assert(true, "text/html; charset=utf-8");
|
||||||
|
try assert(false, "text/htm"); // htm not html
|
||||||
|
try assert(false, "text/plain");
|
||||||
|
try assert(false, "over/9000");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Mime: sniff" {
|
||||||
|
try testing.expectEqual(null, Mime.sniff(""));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("<htm"));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("<html!"));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("<a_"));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("<!doctype html"));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("<!doctype html>"));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("\n <!doctype html>"));
|
||||||
|
try testing.expectEqual(null, Mime.sniff("\n \t <font/>"));
|
||||||
|
|
||||||
|
const expectHTML = struct {
|
||||||
|
fn expect(input: []const u8) !void {
|
||||||
|
try testing.expectEqual(.text_html, std.meta.activeTag(Mime.sniff(input).?.content_type));
|
||||||
|
}
|
||||||
|
}.expect;
|
||||||
|
|
||||||
|
try expectHTML("<!doctype html ");
|
||||||
|
try expectHTML("\n \t <!DOCTYPE HTML ");
|
||||||
|
|
||||||
|
try expectHTML("<html ");
|
||||||
|
try expectHTML("\n \t <HtmL> even more stufff");
|
||||||
|
|
||||||
|
try expectHTML("<script>");
|
||||||
|
try expectHTML("\n \t <SCRIpt >alert(document.cookies)</script>");
|
||||||
|
|
||||||
|
try expectHTML("<iframe>");
|
||||||
|
try expectHTML(" \t <ifRAME >");
|
||||||
|
|
||||||
|
try expectHTML("<h1>");
|
||||||
|
try expectHTML(" <H1>");
|
||||||
|
|
||||||
|
try expectHTML("<div>");
|
||||||
|
try expectHTML("\n\r\r <DiV>");
|
||||||
|
|
||||||
|
try expectHTML("<font>");
|
||||||
|
try expectHTML(" <fonT>");
|
||||||
|
|
||||||
|
try expectHTML("<table>");
|
||||||
|
try expectHTML("\t\t<TAblE>");
|
||||||
|
|
||||||
|
try expectHTML("<a>");
|
||||||
|
try expectHTML("\n\n<A>");
|
||||||
|
|
||||||
|
try expectHTML("<style>");
|
||||||
|
try expectHTML(" \n\t <STyLE>");
|
||||||
|
|
||||||
|
try expectHTML("<title>");
|
||||||
|
try expectHTML(" \n\t <TITLE>");
|
||||||
|
|
||||||
|
try expectHTML("<b>");
|
||||||
|
try expectHTML(" \n\t <B>");
|
||||||
|
|
||||||
|
try expectHTML("<body>");
|
||||||
|
try expectHTML(" \n\t <BODY>");
|
||||||
|
|
||||||
|
try expectHTML("<br>");
|
||||||
|
try expectHTML(" \n\t <BR>");
|
||||||
|
|
||||||
|
try expectHTML("<p>");
|
||||||
|
try expectHTML(" \n\t <P>");
|
||||||
|
|
||||||
|
try expectHTML("<!-->");
|
||||||
|
try expectHTML(" \n\t <!-->");
|
||||||
|
}
|
||||||
|
|
||||||
|
const Expectation = struct {
|
||||||
|
content_type: Mime.ContentType,
|
||||||
|
params: []const u8 = "",
|
||||||
|
charset: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn expect(expected: Expectation, input: []const u8) !void {
|
||||||
|
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||||
|
|
||||||
|
const actual = try Mime.parse(mutable_input);
|
||||||
|
try testing.expectEqual(
|
||||||
|
std.meta.activeTag(expected.content_type),
|
||||||
|
std.meta.activeTag(actual.content_type),
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (expected.content_type) {
|
||||||
|
.other => |e| {
|
||||||
|
const a = actual.content_type.other;
|
||||||
|
try testing.expectEqual(e.type, a.type);
|
||||||
|
try testing.expectEqual(e.sub_type, a.sub_type);
|
||||||
|
},
|
||||||
|
else => {}, // already asserted above
|
||||||
|
}
|
||||||
|
|
||||||
|
try testing.expectEqual(expected.params, actual.params);
|
||||||
|
|
||||||
|
if (expected.charset) |ec| {
|
||||||
|
// We remove the null characters for testing purposes here.
|
||||||
|
try testing.expectEqual(ec, actual.charsetString()[0..ec.len]);
|
||||||
|
} else {
|
||||||
|
const m: Mime = .unknown;
|
||||||
|
try testing.expectEqual(m.charsetString(), actual.charsetString());
|
||||||
|
}
|
||||||
|
}
|
||||||
1431
src/browser/Page.zig
Normal file
1431
src/browser/Page.zig
Normal file
File diff suppressed because it is too large
Load Diff
109
src/browser/Renderer.zig
Normal file
109
src/browser/Renderer.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 parser = @import("netsurf.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const Renderer = @This();
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
// key is a @ptrFromInt of the element
|
||||||
|
// value is the index position
|
||||||
|
positions: std.AutoHashMapUnmanaged(u64, u32),
|
||||||
|
|
||||||
|
// given an index, get the element
|
||||||
|
elements: std.ArrayListUnmanaged(u64),
|
||||||
|
|
||||||
|
const Element = @import("dom/element.zig").Element;
|
||||||
|
|
||||||
|
// we expect allocator to be an arena
|
||||||
|
pub fn init(allocator: Allocator) Renderer {
|
||||||
|
return .{
|
||||||
|
.elements = .{},
|
||||||
|
.positions = .{},
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The DOMRect is always relative to the viewport, not the document the element belongs to.
|
||||||
|
// Element that are not part of the main document, either detached or in a shadow DOM should not call this function.
|
||||||
|
pub fn getRect(self: *Renderer, e: *parser.Element) !Element.DOMRect {
|
||||||
|
var elements = &self.elements;
|
||||||
|
const gop = try self.positions.getOrPut(self.allocator, @intFromPtr(e));
|
||||||
|
var x: u32 = gop.value_ptr.*;
|
||||||
|
if (gop.found_existing == false) {
|
||||||
|
x = @intCast(elements.items.len);
|
||||||
|
try elements.append(self.allocator, @intFromPtr(e));
|
||||||
|
gop.value_ptr.* = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _x: f64 = @floatFromInt(x);
|
||||||
|
const y: f64 = 0.0;
|
||||||
|
const w: f64 = 1.0;
|
||||||
|
const h: f64 = 1.0;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.x = _x,
|
||||||
|
.y = y,
|
||||||
|
.width = w,
|
||||||
|
.height = h,
|
||||||
|
.left = _x,
|
||||||
|
.top = y,
|
||||||
|
.right = _x + w,
|
||||||
|
.bottom = y + h,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn boundingRect(self: *const Renderer) Element.DOMRect {
|
||||||
|
const x: f64 = 0.0;
|
||||||
|
const y: f64 = 0.0;
|
||||||
|
const w: f64 = @floatFromInt(self.width());
|
||||||
|
const h: f64 = @floatFromInt(self.width());
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
.width = w,
|
||||||
|
.height = h,
|
||||||
|
.left = x,
|
||||||
|
.top = y,
|
||||||
|
.right = x + w,
|
||||||
|
.bottom = y + h,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(self: *const Renderer) u32 {
|
||||||
|
return @max(@as(u32, @intCast(self.elements.items.len)), 1); // At least 1 pixel even if empty
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(_: *const Renderer) u32 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getElementAtPosition(self: *const Renderer, x: i32, y: i32) ?*parser.Element {
|
||||||
|
if (y != 0 or x < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = self.elements.items;
|
||||||
|
return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null;
|
||||||
|
}
|
||||||
95
src/browser/Scheduler.zig
Normal file
95
src/browser/Scheduler.zig
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
const timestamp = @import("../datetime.zig").milliTimestamp;
|
||||||
|
|
||||||
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
|
const Queue = std.PriorityQueue(Task, void, struct {
|
||||||
|
fn compare(_: void, a: Task, b: Task) std.math.Order {
|
||||||
|
return std.math.order(a.run_at, b.run_at);
|
||||||
|
}
|
||||||
|
}.compare);
|
||||||
|
|
||||||
|
const Scheduler = @This();
|
||||||
|
|
||||||
|
low_priority: Queue,
|
||||||
|
high_priority: Queue,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator) Scheduler {
|
||||||
|
return .{
|
||||||
|
.low_priority = Queue.init(allocator, {}),
|
||||||
|
.high_priority = Queue.init(allocator, {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Scheduler) void {
|
||||||
|
self.low_priority.cap = 0;
|
||||||
|
self.low_priority.items.len = 0;
|
||||||
|
|
||||||
|
self.high_priority.cap = 0;
|
||||||
|
self.high_priority.items.len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddOpts = struct {
|
||||||
|
name: []const u8 = "",
|
||||||
|
low_priority: bool = false,
|
||||||
|
};
|
||||||
|
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.scheduler, "scheduler.add", .{ .name = opts.name, .run_in_ms = run_in_ms, .low_priority = opts.low_priority });
|
||||||
|
}
|
||||||
|
var queue = if (opts.low_priority) &self.low_priority else &self.high_priority;
|
||||||
|
return queue.add(.{
|
||||||
|
.ctx = ctx,
|
||||||
|
.callback = cb,
|
||||||
|
.name = opts.name,
|
||||||
|
.run_at = timestamp(.monotonic) + run_in_ms,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(self: *Scheduler) !?u64 {
|
||||||
|
_ = try self.runQueue(&self.low_priority);
|
||||||
|
return self.runQueue(&self.high_priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
||||||
|
if (queue.count() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = timestamp(.monotonic);
|
||||||
|
|
||||||
|
while (queue.peek()) |*task_| {
|
||||||
|
if (task_.run_at > now) {
|
||||||
|
return @intCast(task_.run_at - now);
|
||||||
|
}
|
||||||
|
var task = queue.remove();
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.scheduler, "scheduler.runTask", .{ .name = task.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
const repeat_in_ms = task.callback(task.ctx) catch |err| {
|
||||||
|
log.warn(.scheduler, "task.callback", .{ .name = task.name, .err = err });
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (repeat_in_ms) |ms| {
|
||||||
|
// Task cannot be repeated immediately, and they should know that
|
||||||
|
std.debug.assert(ms != 0);
|
||||||
|
task.run_at = now + ms;
|
||||||
|
try self.low_priority.add(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Task = struct {
|
||||||
|
run_at: u64,
|
||||||
|
ctx: *anyopaque,
|
||||||
|
name: []const u8,
|
||||||
|
callback: Callback,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
|
||||||
1039
src/browser/ScriptManager.zig
Normal file
1039
src/browser/ScriptManager.zig
Normal file
File diff suppressed because it is too large
Load Diff
197
src/browser/Session.zig
Normal file
197
src/browser/Session.zig
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
// 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 js = @import("js/js.zig");
|
||||||
|
const storage = @import("webapi/storage/storage.zig");
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const Browser = @import("Browser.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const NavigateOpts = Page.NavigateOpts;
|
||||||
|
|
||||||
|
// Session is like a browser's tab.
|
||||||
|
// It owns the js env and the loader for all the pages of the session.
|
||||||
|
// You can create successively multiple pages for a session, but you must
|
||||||
|
// deinit a page before running another one.
|
||||||
|
const Session = @This();
|
||||||
|
|
||||||
|
browser: *Browser,
|
||||||
|
|
||||||
|
// Used to create our Inspector and in the BrowserContext.
|
||||||
|
arena: Allocator,
|
||||||
|
|
||||||
|
// The page's arena is unsuitable for data that has to existing while
|
||||||
|
// navigating from one page to another. For example, if we're clicking
|
||||||
|
// on an HREF, the URL exists in the original page (where the click
|
||||||
|
// originated) but also has to exist in the new page.
|
||||||
|
// While we could use the Session's arena, this could accumulate a lot of
|
||||||
|
// memory if we do many navigation events. The `transfer_arena` is meant to
|
||||||
|
// bridge the gap: existing long enough to store any data needed to end one
|
||||||
|
// page and start another.
|
||||||
|
transfer_arena: Allocator,
|
||||||
|
|
||||||
|
executor: js.ExecutionWorld,
|
||||||
|
cookie_jar: storage.Jar,
|
||||||
|
storage_shed: storage.Shed,
|
||||||
|
|
||||||
|
page: ?*Page = null,
|
||||||
|
|
||||||
|
// If the current page want to navigate to a new page
|
||||||
|
// (form submit, link click, top.location = xxx)
|
||||||
|
// the details are stored here so that, on the next call to session.wait
|
||||||
|
// we can destroy the current page and start a new one.
|
||||||
|
queued_navigation: ?QueuedNavigation,
|
||||||
|
|
||||||
|
pub fn init(self: *Session, browser: *Browser) !void {
|
||||||
|
var executor = try browser.env.newExecutionWorld();
|
||||||
|
errdefer executor.deinit();
|
||||||
|
|
||||||
|
const allocator = browser.app.allocator;
|
||||||
|
self.* = .{
|
||||||
|
.browser = browser,
|
||||||
|
.executor = executor,
|
||||||
|
.storage_shed = .{},
|
||||||
|
.queued_navigation = null,
|
||||||
|
.arena = browser.session_arena.allocator(),
|
||||||
|
.cookie_jar = storage.Jar.init(allocator),
|
||||||
|
.transfer_arena = browser.transfer_arena.allocator(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Session) void {
|
||||||
|
if (self.page != null) {
|
||||||
|
self.removePage();
|
||||||
|
}
|
||||||
|
self.cookie_jar.deinit();
|
||||||
|
self.storage_shed.deinit(self.browser.app.allocator);
|
||||||
|
self.executor.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
std.debug.assert(self.page == null);
|
||||||
|
|
||||||
|
const page_arena = &self.browser.page_arena;
|
||||||
|
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||||
|
|
||||||
|
self.page = try Page.init(page_arena.allocator(), self.browser.call_arena.allocator(), self);
|
||||||
|
const page = self.page.?;
|
||||||
|
|
||||||
|
log.debug(.browser, "create page", .{});
|
||||||
|
// start JS env
|
||||||
|
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||||
|
self.browser.notification.dispatch(.page_created, page);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn removePage(self: *Session) void {
|
||||||
|
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
|
||||||
|
self.browser.notification.dispatch(.page_remove, .{});
|
||||||
|
|
||||||
|
std.debug.assert(self.page != null);
|
||||||
|
|
||||||
|
// RemoveJsContext() will execute the destructor of any type that
|
||||||
|
// registered a destructor (e.g. XMLHttpRequest).
|
||||||
|
// Should be called before we deinit the page, because these objects
|
||||||
|
// could be referencing it.
|
||||||
|
self.executor.removeContext();
|
||||||
|
|
||||||
|
self.page.?.deinit();
|
||||||
|
self.page = null;
|
||||||
|
|
||||||
|
log.debug(.browser, "remove page", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn currentPage(self: *Session) ?*Page {
|
||||||
|
return self.page orelse return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const WaitResult = enum {
|
||||||
|
done,
|
||||||
|
no_page,
|
||||||
|
extra_socket,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
||||||
|
_ = self.processQueuedNavigation() catch {
|
||||||
|
// There was an error processing the queue navigation. This already
|
||||||
|
// logged the error, just return.
|
||||||
|
return .done;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (self.page) |page| {
|
||||||
|
return page.wait(wait_ms);
|
||||||
|
}
|
||||||
|
return .no_page;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetchWait(self: *Session, wait_ms: u32) void {
|
||||||
|
while (true) {
|
||||||
|
const page = self.page orelse return;
|
||||||
|
_ = page.wait(wait_ms);
|
||||||
|
const navigated = self.processQueuedNavigation() catch {
|
||||||
|
// There was an error processing the queue navigation. This already
|
||||||
|
// logged the error, just return.
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (navigated == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processQueuedNavigation(self: *Session) !bool {
|
||||||
|
const qn = self.queued_navigation orelse return false;
|
||||||
|
// This was already aborted on the page, but it would be pretty
|
||||||
|
// bad if old requests went to the new page, so let's make double sure
|
||||||
|
self.browser.http_client.abort();
|
||||||
|
|
||||||
|
// Page.navigateFromWebAPI terminatedExecution. If we don't resume
|
||||||
|
// it before doing a shutdown we'll get an error.
|
||||||
|
self.executor.resumeExecution();
|
||||||
|
self.removePage();
|
||||||
|
self.queued_navigation = null;
|
||||||
|
|
||||||
|
const page = self.createPage() catch |err| {
|
||||||
|
log.err(.browser, "queued navigation page error", .{
|
||||||
|
.err = err,
|
||||||
|
.url = qn.url,
|
||||||
|
});
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
page.navigate(qn.url, qn.opts) catch |err| {
|
||||||
|
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QueuedNavigation = struct {
|
||||||
|
url: [:0]const u8,
|
||||||
|
opts: NavigateOpts,
|
||||||
|
};
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Sometimes we need to extend libdom. For example, its HTMLDocument doesn't
|
|
||||||
// have a readyState. We have a couple different options, such as making the
|
|
||||||
// correction in libdom directly. Another option stems from the fact that every
|
|
||||||
// libdom node has an opaque embedder_data field. This is the struct that we
|
|
||||||
// lazily load into that field.
|
|
||||||
//
|
|
||||||
// It didn't originally start off as a collection of every single extension, but
|
|
||||||
// this quickly proved necessary, since different fields are needed on the same
|
|
||||||
// data at different levels of the prototype chain. This isn't memory efficient.
|
|
||||||
|
|
||||||
const Env = @import("env.zig").Env;
|
|
||||||
const parser = @import("netsurf.zig");
|
|
||||||
const DataSet = @import("html/DataSet.zig");
|
|
||||||
const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
|
||||||
|
|
||||||
// for HTMLScript (but probably needs to be added to more)
|
|
||||||
onload: ?Env.Function = null,
|
|
||||||
onerror: ?Env.Function = null,
|
|
||||||
|
|
||||||
// for HTMLElement
|
|
||||||
style: CSSStyleDeclaration = .empty,
|
|
||||||
dataset: ?DataSet = null,
|
|
||||||
|
|
||||||
// for html/document
|
|
||||||
ready_state: ReadyState = .loading,
|
|
||||||
|
|
||||||
// for dom/document
|
|
||||||
active_element: ?*parser.Element = null,
|
|
||||||
|
|
||||||
// for HTMLSelectElement
|
|
||||||
// By default, if no option is explicitly selected, the first option should
|
|
||||||
// be selected. However, libdom doesn't do this, and it sets the
|
|
||||||
// selectedIndex to -1, which is a valid value for "nothing selected".
|
|
||||||
// Therefore, when libdom says the selectedIndex == -1, we don't know if
|
|
||||||
// it means that nothing is selected, or if the first option is selected by
|
|
||||||
// default.
|
|
||||||
// There are cases where this won't work, but when selectedIndex is
|
|
||||||
// explicitly set, we set this boolean flag. Then, when we're getting then
|
|
||||||
// selectedIndex, if this flag is == false, which is to say that if
|
|
||||||
// selectedIndex hasn't been explicitly set AND if we have at least 1 option
|
|
||||||
// AND if it isn't a multi select, we can make the 1st item selected by
|
|
||||||
// default (by returning selectedIndex == 0).
|
|
||||||
explicit_index_set: bool = false,
|
|
||||||
|
|
||||||
template_content: ?*parser.DocumentFragment = null,
|
|
||||||
|
|
||||||
const ReadyState = enum {
|
|
||||||
loading,
|
|
||||||
interactive,
|
|
||||||
complete,
|
|
||||||
};
|
|
||||||
393
src/browser/URL.zig
Normal file
393
src/browser/URL.zig
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const ResolveOpts = struct {
|
||||||
|
always_dupe: bool = false,
|
||||||
|
};
|
||||||
|
// path is anytype, so that it can be used with both []const u8 and [:0]const u8
|
||||||
|
pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime opts: ResolveOpts) ![:0]const u8 {
|
||||||
|
const PT = @TypeOf(path);
|
||||||
|
if (base.len == 0 or isCompleteHTTPUrl(path)) {
|
||||||
|
if (comptime opts.always_dupe or !isNullTerminated(PT)) {
|
||||||
|
return allocator.dupeZ(u8, path);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.len == 0) {
|
||||||
|
if (comptime opts.always_dupe) {
|
||||||
|
return allocator.dupeZ(u8, base);
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path[0] == '?') {
|
||||||
|
const base_path_end = std.mem.indexOfAny(u8, base, "?#") orelse base.len;
|
||||||
|
return std.mem.joinZ(allocator, "", &.{ base[0..base_path_end], path });
|
||||||
|
}
|
||||||
|
if (path[0] == '#') {
|
||||||
|
const base_fragment_start = std.mem.indexOfScalar(u8, base, '#') orelse base.len;
|
||||||
|
return std.mem.joinZ(allocator, "", &.{ base[0..base_fragment_start], path });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, path, "//")) {
|
||||||
|
// network-path reference
|
||||||
|
const index = std.mem.indexOfScalar(u8, base, ':') orelse {
|
||||||
|
if (comptime isNullTerminated(PT)) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
return allocator.dupeZ(u8, path);
|
||||||
|
};
|
||||||
|
const protocol = base[0 .. index + 1];
|
||||||
|
return std.mem.joinZ(allocator, "", &.{ protocol, path });
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheme_end = std.mem.indexOf(u8, base, "://");
|
||||||
|
const authority_start = if (scheme_end) |end| end + 3 else 0;
|
||||||
|
const path_start = std.mem.indexOfScalarPos(u8, base, authority_start, '/') orelse base.len;
|
||||||
|
|
||||||
|
if (path[0] == '/') {
|
||||||
|
return std.mem.joinZ(allocator, "", &.{ base[0..path_start], path });
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized_base: []const u8 = base;
|
||||||
|
if (std.mem.lastIndexOfScalar(u8, normalized_base[authority_start..], '/')) |pos| {
|
||||||
|
normalized_base = normalized_base[0 .. pos + authority_start];
|
||||||
|
}
|
||||||
|
|
||||||
|
// trailing space so that we always have space to append the null terminator
|
||||||
|
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
|
||||||
|
const end = out.len - 1;
|
||||||
|
|
||||||
|
const path_marker = path_start + 1;
|
||||||
|
|
||||||
|
// Strip out ./ and ../. This is done in-place, because doing so can
|
||||||
|
// only ever make `out` smaller. After this, `out` cannot be freed by
|
||||||
|
// an allocator, which is ok, because we expect allocator to be an arena.
|
||||||
|
var in_i: usize = 0;
|
||||||
|
var out_i: usize = 0;
|
||||||
|
while (in_i < end) {
|
||||||
|
if (std.mem.startsWith(u8, out[in_i..], "./")) {
|
||||||
|
in_i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, out[in_i..], "../")) {
|
||||||
|
std.debug.assert(out[out_i - 1] == '/');
|
||||||
|
|
||||||
|
if (out_i > path_marker) {
|
||||||
|
// go back before the /
|
||||||
|
out_i -= 2;
|
||||||
|
while (out_i > 1 and out[out_i - 1] != '/') {
|
||||||
|
out_i -= 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if out_i == path_marker, than we've reached the start of
|
||||||
|
// the path. We can't ../ any more. E.g.:
|
||||||
|
// http://www.example.com/../hello.
|
||||||
|
// You might think that's an error, but, at least with
|
||||||
|
// new URL('../hello', 'http://www.example.com/')
|
||||||
|
// it just ignores the extra ../
|
||||||
|
}
|
||||||
|
in_i += 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out[out_i] = out[in_i];
|
||||||
|
in_i += 1;
|
||||||
|
out_i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we always have an extra space
|
||||||
|
out[out_i] = 0;
|
||||||
|
return out[0..out_i :0];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isNullTerminated(comptime value: type) bool {
|
||||||
|
return @typeInfo(value).pointer.sentinel_ptr != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isCompleteHTTPUrl(url: []const u8) bool {
|
||||||
|
if (url.len < 6) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// very common case
|
||||||
|
if (url[0] == '/') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std.ascii.startsWithIgnoreCase(url, "https://") or
|
||||||
|
std.ascii.startsWithIgnoreCase(url, "http://") or
|
||||||
|
std.ascii.startsWithIgnoreCase(url, "ftp://");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getUsername(raw: [:0]const u8) []const u8 {
|
||||||
|
const user_info = getUserInfo(raw) orelse return "";
|
||||||
|
const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return user_info;
|
||||||
|
return user_info[0..pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getPassword(raw: [:0]const u8) []const u8 {
|
||||||
|
const user_info = getUserInfo(raw) orelse return "";
|
||||||
|
const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return "";
|
||||||
|
return user_info[pos + 1 ..];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getPathname(raw: [:0]const u8) []const u8 {
|
||||||
|
const protocol_end = std.mem.indexOf(u8, raw, "://") orelse 0;
|
||||||
|
const path_start = std.mem.indexOfScalarPos(u8, raw, if (protocol_end > 0) protocol_end + 3 else 0, '/') orelse raw.len;
|
||||||
|
|
||||||
|
const query_or_hash_start = std.mem.indexOfAnyPos(u8, raw, path_start, "?#") orelse raw.len;
|
||||||
|
|
||||||
|
if (path_start >= query_or_hash_start) {
|
||||||
|
if (std.mem.indexOf(u8, raw, "://") != null) return "/";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw[path_start..query_or_hash_start];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getProtocol(raw: [:0]const u8) []const u8 {
|
||||||
|
const pos = std.mem.indexOfScalarPos(u8, raw, 0, ':') orelse return "";
|
||||||
|
return raw[0 .. pos + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getHostname(raw: [:0]const u8) []const u8 {
|
||||||
|
const host = getHost(raw);
|
||||||
|
const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return host;
|
||||||
|
return host[0..pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getPort(raw: [:0]const u8) []const u8 {
|
||||||
|
const host = getHost(raw);
|
||||||
|
const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return "";
|
||||||
|
|
||||||
|
if (pos + 1 >= host.len) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (host[pos + 1 ..]) |c| {
|
||||||
|
if (c < '0' or c > '9') {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return host[pos + 1 ..];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getSearch(raw: [:0]const u8) []const u8 {
|
||||||
|
const pos = std.mem.indexOfScalarPos(u8, raw, 0, '?') orelse return "";
|
||||||
|
const query_part = raw[pos..];
|
||||||
|
|
||||||
|
if (std.mem.indexOfScalarPos(u8, query_part, 0, '#')) |fragment_start| {
|
||||||
|
return query_part[0..fragment_start];
|
||||||
|
}
|
||||||
|
|
||||||
|
return query_part;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getHash(raw: [:0]const u8) []const u8 {
|
||||||
|
const start = std.mem.indexOfScalarPos(u8, raw, 0, '#') orelse return "";
|
||||||
|
return raw[start..];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOrigin(allocator: Allocator, raw: [:0]const u8) !?[]const u8 {
|
||||||
|
const port = getPort(raw);
|
||||||
|
const protocol = getProtocol(raw);
|
||||||
|
const hostname = getHostname(raw);
|
||||||
|
|
||||||
|
const p = std.meta.stringToEnum(KnownProtocol, getProtocol(raw)) orelse return null;
|
||||||
|
|
||||||
|
const include_port = blk: {
|
||||||
|
if (port.len == 0) {
|
||||||
|
break :blk false;
|
||||||
|
}
|
||||||
|
if (p == .@"https:" and std.mem.eql(u8, port, "443")) {
|
||||||
|
break :blk false;
|
||||||
|
}
|
||||||
|
if (p == .@"http:" and std.mem.eql(u8, port, "80")) {
|
||||||
|
break :blk false;
|
||||||
|
}
|
||||||
|
break :blk true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (include_port) {
|
||||||
|
return try std.fmt.allocPrint(allocator, "{s}//{s}:{s}", .{ protocol, hostname, port });
|
||||||
|
}
|
||||||
|
return try std.fmt.allocPrint(allocator, "{s}//{s}", .{ protocol, hostname });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getUserInfo(raw: [:0]const u8) ?[]const u8 {
|
||||||
|
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return null;
|
||||||
|
const authority_start = scheme_end + 3;
|
||||||
|
|
||||||
|
const pos = std.mem.indexOfScalar(u8, raw[authority_start..], '@') orelse return null;
|
||||||
|
const path_start = std.mem.indexOfScalarPos(u8, raw, authority_start, '/') orelse raw.len;
|
||||||
|
|
||||||
|
const full_pos = authority_start + pos;
|
||||||
|
if (full_pos < path_start) {
|
||||||
|
return raw[authority_start..full_pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getHost(raw: [:0]const u8) []const u8 {
|
||||||
|
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return "";
|
||||||
|
|
||||||
|
var authority_start = scheme_end + 3;
|
||||||
|
if (std.mem.indexOf(u8, raw[authority_start..], "@")) |pos| {
|
||||||
|
authority_start += pos + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authority = raw[authority_start..];
|
||||||
|
const path_start = std.mem.indexOfAny(u8, authority, "/?#") orelse return authority;
|
||||||
|
return authority[0..path_start];
|
||||||
|
}
|
||||||
|
|
||||||
|
const KnownProtocol = enum {
|
||||||
|
@"http:",
|
||||||
|
@"https:",
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
test "URL: isCompleteHTTPUrl" {
|
||||||
|
try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about"));
|
||||||
|
try testing.expectEqual(true, isCompleteHTTPUrl("HttP://example.com/about"));
|
||||||
|
try testing.expectEqual(true, isCompleteHTTPUrl("httpS://example.com/about"));
|
||||||
|
try testing.expectEqual(true, isCompleteHTTPUrl("HTTPs://example.com/about"));
|
||||||
|
try testing.expectEqual(true, isCompleteHTTPUrl("ftp://example.com/about"));
|
||||||
|
|
||||||
|
try testing.expectEqual(false, isCompleteHTTPUrl("/example.com"));
|
||||||
|
try testing.expectEqual(false, isCompleteHTTPUrl("../../about"));
|
||||||
|
try testing.expectEqual(false, isCompleteHTTPUrl("about"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "URL: resolve" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const Case = struct {
|
||||||
|
base: [:0]const u8,
|
||||||
|
path: [:0]const u8,
|
||||||
|
expected: [:0]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cases = [_]Case{
|
||||||
|
.{
|
||||||
|
.base = "https://example/xyz/abc/123",
|
||||||
|
.path = "something.js",
|
||||||
|
.expected = "https://example/xyz/abc/something.js",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/xyz/abc/123",
|
||||||
|
.path = "/something.js",
|
||||||
|
.expected = "https://example/something.js",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/",
|
||||||
|
.path = "something.js",
|
||||||
|
.expected = "https://example/something.js",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/",
|
||||||
|
.path = "/something.js",
|
||||||
|
.expected = "https://example/something.js",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example",
|
||||||
|
.path = "something.js",
|
||||||
|
.expected = "https://example/something.js",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example",
|
||||||
|
.path = "abc/something.js",
|
||||||
|
.expected = "https://example/abc/something.js",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/nested",
|
||||||
|
.path = "abc/something.js",
|
||||||
|
.expected = "https://example/abc/something.js",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/nested/",
|
||||||
|
.path = "abc/something.js",
|
||||||
|
.expected = "https://example/nested/abc/something.js",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/nested/",
|
||||||
|
.path = "/abc/something.js",
|
||||||
|
.expected = "https://example/abc/something.js",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/nested/",
|
||||||
|
.path = "http://www.github.com/example/",
|
||||||
|
.expected = "http://www.github.com/example/",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/nested/",
|
||||||
|
.path = "",
|
||||||
|
.expected = "https://example/nested/",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/abc/aaa",
|
||||||
|
.path = "./hello/./world",
|
||||||
|
.expected = "https://example/abc/hello/world",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/abc/aaa/",
|
||||||
|
.path = "../hello",
|
||||||
|
.expected = "https://example/abc/hello",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/abc/aaa",
|
||||||
|
.path = "../hello",
|
||||||
|
.expected = "https://example/hello",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example/abc/aaa/",
|
||||||
|
.path = "./.././.././hello",
|
||||||
|
.expected = "https://example/hello",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "some/page",
|
||||||
|
.path = "hello",
|
||||||
|
.expected = "some/hello",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "some/page/",
|
||||||
|
.path = "hello",
|
||||||
|
.expected = "some/page/hello",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "some/page/other",
|
||||||
|
.path = ".././hello",
|
||||||
|
.expected = "some/hello",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://www.example.com/hello/world",
|
||||||
|
.path = "//example/about",
|
||||||
|
.expected = "https://example/about",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "http:",
|
||||||
|
.path = "//example.com/over/9000",
|
||||||
|
.expected = "http://example.com/over/9000",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "../hello",
|
||||||
|
.expected = "https://example.com/hello",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://www.example.com/hello/world/",
|
||||||
|
.path = "../../../../example/about",
|
||||||
|
.expected = "https://www.example.com/example/about",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (cases) |case| {
|
||||||
|
const result = try resolve(testing.arena_allocator, case.base, case.path, .{});
|
||||||
|
try testing.expectString(case.expected, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
||||||
|
|
||||||
const 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 log = @import("../log.zig");
|
|
||||||
|
|
||||||
const http = @import("../http/client.zig");
|
|
||||||
|
|
||||||
// Browser is an instance of the browser.
|
|
||||||
// You can create multiple browser instances.
|
|
||||||
// A browser contains only one session.
|
|
||||||
pub const Browser = struct {
|
|
||||||
env: *Env,
|
|
||||||
app: *App,
|
|
||||||
session: ?Session,
|
|
||||||
allocator: Allocator,
|
|
||||||
http_client: *http.Client,
|
|
||||||
page_arena: ArenaAllocator,
|
|
||||||
session_arena: ArenaAllocator,
|
|
||||||
transfer_arena: ArenaAllocator,
|
|
||||||
notification: *Notification,
|
|
||||||
state_pool: std.heap.MemoryPool(State),
|
|
||||||
|
|
||||||
pub fn init(app: *App) !Browser {
|
|
||||||
const allocator = app.allocator;
|
|
||||||
|
|
||||||
const env = try Env.init(allocator, app.platform, .{});
|
|
||||||
errdefer env.deinit();
|
|
||||||
|
|
||||||
const notification = try Notification.init(allocator, app.notification);
|
|
||||||
errdefer notification.deinit();
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.app = app,
|
|
||||||
.env = env,
|
|
||||||
.session = null,
|
|
||||||
.allocator = allocator,
|
|
||||||
.notification = notification,
|
|
||||||
.http_client = &app.http_client,
|
|
||||||
.page_arena = ArenaAllocator.init(allocator),
|
|
||||||
.session_arena = ArenaAllocator.init(allocator),
|
|
||||||
.transfer_arena = ArenaAllocator.init(allocator),
|
|
||||||
.state_pool = std.heap.MemoryPool(State).init(allocator),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Browser) void {
|
|
||||||
self.closeSession();
|
|
||||||
self.env.deinit();
|
|
||||||
self.page_arena.deinit();
|
|
||||||
self.session_arena.deinit();
|
|
||||||
self.transfer_arena.deinit();
|
|
||||||
self.notification.deinit();
|
|
||||||
self.state_pool.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn newSession(self: *Browser) !*Session {
|
|
||||||
self.closeSession();
|
|
||||||
self.session = @as(Session, undefined);
|
|
||||||
const session = &self.session.?;
|
|
||||||
try Session.init(session, self);
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn closeSession(self: *Browser) void {
|
|
||||||
if (self.session) |*session| {
|
|
||||||
session.deinit();
|
|
||||||
self.session = null;
|
|
||||||
_ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
|
||||||
self.env.lowMemoryNotification();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn runMicrotasks(self: *const Browser) void {
|
|
||||||
self.env.runMicrotasks();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn runMessageLoop(self: *const Browser) void {
|
|
||||||
while (self.env.pumpMessageLoop()) {
|
|
||||||
log.debug(.browser, "pumpMessageLoop", .{});
|
|
||||||
}
|
|
||||||
self.env.runIdleTasks();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
|
||||||
test "Browser" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
// this will crash if ICU isn't properly configured / ininitialized
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "new Intl.DateTimeFormat()", "[object Intl.DateTimeFormat]" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const builtin = @import("builtin");
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const JsObject = @import("../env.zig").Env.JsObject;
|
|
||||||
|
|
||||||
const log = if (builtin.is_test) &test_capture else @import("../../log.zig");
|
|
||||||
|
|
||||||
pub const Console = struct {
|
|
||||||
// TODO: configurable writer
|
|
||||||
timers: std.StringHashMapUnmanaged(u32) = .{},
|
|
||||||
counts: std.StringHashMapUnmanaged(u32) = .{},
|
|
||||||
|
|
||||||
pub fn _lp(values: []JsObject, page: *Page) !void {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _log(values: []JsObject, page: *Page) !void {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _info(values: []JsObject, page: *Page) !void {
|
|
||||||
return _log(values, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _debug(values: []JsObject, page: *Page) !void {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _warn(values: []JsObject, page: *Page) !void {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _error(values: []JsObject, page: *Page) !void {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(.console, "error", .{
|
|
||||||
.args = try serializeValues(values, page),
|
|
||||||
.stack = page.stackTrace() catch "???",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _clear() void {}
|
|
||||||
|
|
||||||
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
|
|
||||||
const label = label_ orelse "default";
|
|
||||||
const gop = try self.counts.getOrPut(page.arena, label);
|
|
||||||
|
|
||||||
var current: u32 = 0;
|
|
||||||
if (gop.found_existing) {
|
|
||||||
current = gop.value_ptr.*;
|
|
||||||
} else {
|
|
||||||
gop.key_ptr.* = try page.arena.dupe(u8, label);
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = current + 1;
|
|
||||||
gop.value_ptr.* = count;
|
|
||||||
|
|
||||||
log.info(.console, "count", .{ .label = label, .count = count });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _countReset(self: *Console, label_: ?[]const u8) !void {
|
|
||||||
const label = label_ orelse "default";
|
|
||||||
const kv = self.counts.fetchRemove(label) orelse {
|
|
||||||
log.info(.console, "invalid counter", .{ .label = label });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
log.info(.console, "count reset", .{ .label = label, .count = kv.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _time(self: *Console, label_: ?[]const u8, page: *Page) !void {
|
|
||||||
const label = label_ orelse "default";
|
|
||||||
const gop = try self.timers.getOrPut(page.arena, label);
|
|
||||||
|
|
||||||
if (gop.found_existing) {
|
|
||||||
log.info(.console, "duplicate timer", .{ .label = label });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
gop.key_ptr.* = try page.arena.dupe(u8, label);
|
|
||||||
gop.value_ptr.* = timestamp();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _timeLog(self: *Console, label_: ?[]const u8) void {
|
|
||||||
const elapsed = timestamp();
|
|
||||||
const label = label_ orelse "default";
|
|
||||||
const start = self.timers.get(label) orelse {
|
|
||||||
log.info(.console, "invalid timer", .{ .label = label });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
log.info(.console, "timer", .{ .label = label, .elapsed = elapsed - start });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _timeStop(self: *Console, label_: ?[]const u8) void {
|
|
||||||
const elapsed = timestamp();
|
|
||||||
const label = label_ orelse "default";
|
|
||||||
const kv = self.timers.fetchRemove(label) orelse {
|
|
||||||
log.info(.console, "invalid timer", .{ .label = label });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
|
|
||||||
if (assertion.isTruthy()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var serialized_values: []const u8 = "";
|
|
||||||
if (values.len > 0) {
|
|
||||||
serialized_values = try serializeValues(values, page);
|
|
||||||
}
|
|
||||||
log.info(.console, "assertion failed", .{ .values = serialized_values });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serializeValues(values: []JsObject, page: *Page) ![]const u8 {
|
|
||||||
if (values.len == 0) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const arena = page.call_arena;
|
|
||||||
const separator = log.separator();
|
|
||||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
|
||||||
|
|
||||||
for (values, 1..) |value, i| {
|
|
||||||
try arr.appendSlice(arena, separator);
|
|
||||||
try arr.writer(arena).print("{d}: ", .{i});
|
|
||||||
const serialized = if (builtin.mode == .Debug) value.toDetailString() else value.toString();
|
|
||||||
try arr.appendSlice(arena, try serialized);
|
|
||||||
}
|
|
||||||
return arr.items;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn timestamp() u32 {
|
|
||||||
const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch unreachable;
|
|
||||||
return @intCast(ts.sec);
|
|
||||||
}
|
|
||||||
|
|
||||||
var test_capture = TestCapture{};
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.Console" {
|
|
||||||
defer testing.reset();
|
|
||||||
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
{
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "console.log('a')", "undefined" },
|
|
||||||
.{ "console.warn('hello world', 23, true, new Object())", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
const captured = test_capture.captured.items;
|
|
||||||
try testing.expectEqual("[info] args= 1: a", captured[0]);
|
|
||||||
try testing.expectEqual("[warn] args= 1: hello world 2: 23 3: true 4: #<Object>", captured[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
test_capture.reset();
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "console.countReset()", "undefined" },
|
|
||||||
.{ "console.count()", "undefined" },
|
|
||||||
.{ "console.count('teg')", "undefined" },
|
|
||||||
.{ "console.count('teg')", "undefined" },
|
|
||||||
.{ "console.count('teg')", "undefined" },
|
|
||||||
.{ "console.count()", "undefined" },
|
|
||||||
.{ "console.countReset('teg')", "undefined" },
|
|
||||||
.{ "console.countReset()", "undefined" },
|
|
||||||
.{ "console.count()", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
const captured = test_capture.captured.items;
|
|
||||||
try testing.expectEqual("[invalid counter] label=default", captured[0]);
|
|
||||||
try testing.expectEqual("[count] label=default count=1", captured[1]);
|
|
||||||
try testing.expectEqual("[count] label=teg count=1", captured[2]);
|
|
||||||
try testing.expectEqual("[count] label=teg count=2", captured[3]);
|
|
||||||
try testing.expectEqual("[count] label=teg count=3", captured[4]);
|
|
||||||
try testing.expectEqual("[count] label=default count=2", captured[5]);
|
|
||||||
try testing.expectEqual("[count reset] label=teg count=3", captured[6]);
|
|
||||||
try testing.expectEqual("[count reset] label=default count=2", captured[7]);
|
|
||||||
try testing.expectEqual("[count] label=default count=1", captured[8]);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
test_capture.reset();
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "console.assert(true)", "undefined" },
|
|
||||||
.{ "console.assert('a', 2, 3, 4)", "undefined" },
|
|
||||||
.{ "console.assert('')", "undefined" },
|
|
||||||
.{ "console.assert('', 'x', true)", "undefined" },
|
|
||||||
.{ "console.assert(false, 'x')", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
const captured = test_capture.captured.items;
|
|
||||||
try testing.expectEqual("[assertion failed] values=", captured[0]);
|
|
||||||
try testing.expectEqual("[assertion failed] values= 1: x 2: true", captured[1]);
|
|
||||||
try testing.expectEqual("[assertion failed] values= 1: x", captured[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
test_capture.reset();
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "[1].forEach(console.log)", null },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
const captured = test_capture.captured.items;
|
|
||||||
try testing.expectEqual("[info] args= 1: 1 2: 0 3: [1]", captured[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TestCapture = struct {
|
|
||||||
captured: std.ArrayListUnmanaged([]const u8) = .{},
|
|
||||||
|
|
||||||
fn separator(_: *const TestCapture) []const u8 {
|
|
||||||
return " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(self: *TestCapture) void {
|
|
||||||
self.captured = .{};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debug(
|
|
||||||
self: *TestCapture,
|
|
||||||
comptime scope: @Type(.enum_literal),
|
|
||||||
comptime msg: []const u8,
|
|
||||||
args: anytype,
|
|
||||||
) void {
|
|
||||||
self.capture(scope, msg, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn info(
|
|
||||||
self: *TestCapture,
|
|
||||||
comptime scope: @Type(.enum_literal),
|
|
||||||
comptime msg: []const u8,
|
|
||||||
args: anytype,
|
|
||||||
) void {
|
|
||||||
self.capture(scope, msg, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn warn(
|
|
||||||
self: *TestCapture,
|
|
||||||
comptime scope: @Type(.enum_literal),
|
|
||||||
comptime msg: []const u8,
|
|
||||||
args: anytype,
|
|
||||||
) void {
|
|
||||||
self.capture(scope, msg, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn err(
|
|
||||||
self: *TestCapture,
|
|
||||||
comptime scope: @Type(.enum_literal),
|
|
||||||
comptime msg: []const u8,
|
|
||||||
args: anytype,
|
|
||||||
) void {
|
|
||||||
self.capture(scope, msg, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fatal(
|
|
||||||
self: *TestCapture,
|
|
||||||
comptime scope: @Type(.enum_literal),
|
|
||||||
comptime msg: []const u8,
|
|
||||||
args: anytype,
|
|
||||||
) void {
|
|
||||||
self.capture(scope, msg, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn capture(
|
|
||||||
self: *TestCapture,
|
|
||||||
comptime scope: @Type(.enum_literal),
|
|
||||||
comptime msg: []const u8,
|
|
||||||
args: anytype,
|
|
||||||
) void {
|
|
||||||
self._capture(scope, msg, args) catch unreachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _capture(
|
|
||||||
self: *TestCapture,
|
|
||||||
comptime scope: @Type(.enum_literal),
|
|
||||||
comptime msg: []const u8,
|
|
||||||
args: anytype,
|
|
||||||
) !void {
|
|
||||||
std.debug.assert(scope == .console);
|
|
||||||
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
try buf.appendSlice(allocator, "[" ++ msg ++ "] ");
|
|
||||||
|
|
||||||
inline for (@typeInfo(@TypeOf(args)).@"struct".fields) |f| {
|
|
||||||
try buf.appendSlice(allocator, f.name);
|
|
||||||
try buf.append(allocator, '=');
|
|
||||||
try @import("../../log.zig").writeValue(.pretty, @field(args, f.name), buf.writer(allocator));
|
|
||||||
try buf.append(allocator, ' ');
|
|
||||||
}
|
|
||||||
self.captured.append(testing.arena_allocator, std.mem.trimRight(u8, buf.items, " ")) catch unreachable;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const uuidv4 = @import("../../id.zig").uuidv4;
|
|
||||||
|
|
||||||
// https://w3c.github.io/webcrypto/#crypto-interface
|
|
||||||
pub const Crypto = struct {
|
|
||||||
_not_empty: bool = true,
|
|
||||||
|
|
||||||
pub fn _getRandomValues(_: *const Crypto, js_obj: Env.JsObject) !Env.JsObject {
|
|
||||||
var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues);
|
|
||||||
const buf = into.asBuffer();
|
|
||||||
if (buf.len > 65_536) {
|
|
||||||
return error.QuotaExceededError;
|
|
||||||
}
|
|
||||||
std.crypto.random.bytes(buf);
|
|
||||||
return js_obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _randomUUID(_: *const Crypto) [36]u8 {
|
|
||||||
var hex: [36]u8 = undefined;
|
|
||||||
uuidv4(&hex);
|
|
||||||
return hex;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const RandomValues = union(enum) {
|
|
||||||
int8: []i8,
|
|
||||||
uint8: []u8,
|
|
||||||
int16: []i16,
|
|
||||||
uint16: []u16,
|
|
||||||
int32: []i32,
|
|
||||||
uint32: []u32,
|
|
||||||
int64: []i64,
|
|
||||||
uint64: []u64,
|
|
||||||
|
|
||||||
fn asBuffer(self: RandomValues) []u8 {
|
|
||||||
return switch (self) {
|
|
||||||
.int8 => |b| (@as([]u8, @ptrCast(b)))[0..b.len],
|
|
||||||
.uint8 => |b| (@as([]u8, @ptrCast(b)))[0..b.len],
|
|
||||||
.int16 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
|
||||||
.uint16 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
|
||||||
.int32 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
|
||||||
.uint32 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
|
||||||
.int64 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
|
||||||
.uint64 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.Crypto" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const a = crypto.randomUUID();", "undefined" },
|
|
||||||
.{ "const b = crypto.randomUUID();", "undefined" },
|
|
||||||
.{ "a.length;", "36" },
|
|
||||||
.{ "b.length;", "36" },
|
|
||||||
.{ "a == b;", "false" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "try { crypto.getRandomValues(new BigUint64Array(8193)) } catch(e) { e.message == 'QuotaExceededError' }", "true" },
|
|
||||||
.{ "let r1 = new Int32Array(5)", "undefined" },
|
|
||||||
.{ "let r2 = crypto.getRandomValues(r1)", "undefined" },
|
|
||||||
.{ "new Set(r1).size", "5" },
|
|
||||||
.{ "new Set(r2).size", "5" },
|
|
||||||
.{ "r1.every((v, i) => v === r2[i])", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var r3 = new Uint8Array(16)", null },
|
|
||||||
.{ "let r4 = crypto.getRandomValues(r3)", "undefined" },
|
|
||||||
.{ "r4[6] = 10", null },
|
|
||||||
.{ "r4[6]", "10" },
|
|
||||||
.{ "r3[6]", "10" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
# css
|
|
||||||
|
|
||||||
Lightpanda css implements CSS selectors parsing and matching in Zig.
|
|
||||||
This package is a port of the Go lib [andybalholm/cascadia](https://github.com/andybalholm/cascadia).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Query parser
|
|
||||||
|
|
||||||
```zig
|
|
||||||
const css = @import("css.zig");
|
|
||||||
|
|
||||||
const selector = try css.parse(alloc, "h1", .{});
|
|
||||||
defer selector.deinit(alloc);
|
|
||||||
```
|
|
||||||
|
|
||||||
### DOM tree match
|
|
||||||
|
|
||||||
The lib expects a `Node` interface implementation to match your DOM tree.
|
|
||||||
|
|
||||||
```zig
|
|
||||||
pub const Node = struct {
|
|
||||||
pub fn firstChild(_: Node) !?Node {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lastChild(_: Node) !?Node {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nextSibling(_: Node) !?Node {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prevSibling(_: Node) !?Node {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parent(_: Node) !?Node {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isElement(_: Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isDocument(_: Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isComment(_: Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isText(_: Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isEmptyText(_: Node) !bool {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tag(_: Node) ![]const u8 {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn attr(_: Node, _: []const u8) !?[]const u8 {
|
|
||||||
return error.TODO;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eql(_: Node, _: Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
You also need do define a `Matcher` implementing a `match` function to
|
|
||||||
accumulate the results.
|
|
||||||
|
|
||||||
```zig
|
|
||||||
const Matcher = struct {
|
|
||||||
const Nodes = std.ArrayList(Node);
|
|
||||||
|
|
||||||
nodes: Nodes,
|
|
||||||
|
|
||||||
fn init(alloc: std.mem.Allocator) Matcher {
|
|
||||||
return .{ .nodes = Nodes.init(alloc) };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(m: *Matcher) void {
|
|
||||||
m.nodes.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(m: *Matcher, n: Node) !void {
|
|
||||||
try m.nodes.append(n);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can use the lib itself.
|
|
||||||
|
|
||||||
```zig
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
try css.matchAll(selector, node, &matcher);
|
|
||||||
_ = try css.matchFirst(selector, node, &matcher); // returns true if a node matched.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* [x] parse query selector
|
|
||||||
* [x] `matchAll`
|
|
||||||
* [x] `matchFirst`
|
|
||||||
* [ ] specificity
|
|
||||||
|
|
||||||
### Selectors implemented
|
|
||||||
|
|
||||||
#### Selectors
|
|
||||||
|
|
||||||
* [x] Class selectors
|
|
||||||
* [x] Id selectors
|
|
||||||
* [x] Type selectors
|
|
||||||
* [x] Universal selectors
|
|
||||||
* [ ] Nesting selectors
|
|
||||||
|
|
||||||
#### Combinators
|
|
||||||
|
|
||||||
* [x] Child combinator
|
|
||||||
* [ ] Column combinator
|
|
||||||
* [x] Descendant combinator
|
|
||||||
* [ ] Namespace combinator
|
|
||||||
* [x] Next-sibling combinator
|
|
||||||
* [x] Selector list combinator
|
|
||||||
* [x] Subsequent-sibling combinator
|
|
||||||
|
|
||||||
#### Attribute
|
|
||||||
|
|
||||||
* [x] `[attr]`
|
|
||||||
* [x] `[attr=value]`
|
|
||||||
* [x] `[attr|=value]`
|
|
||||||
* [x] `[attr^=value]`
|
|
||||||
* [x] `[attr$=value]`
|
|
||||||
* [ ] `[attr*=value]`
|
|
||||||
* [x] `[attr operator value i]`
|
|
||||||
* [ ] `[attr operator value s]`
|
|
||||||
|
|
||||||
#### Pseudo classes
|
|
||||||
|
|
||||||
* [ ] `:active`
|
|
||||||
* [ ] `:any-link`
|
|
||||||
* [ ] `:autofill`
|
|
||||||
* [ ] `:blank Experimental`
|
|
||||||
* [x] `:checked`
|
|
||||||
* [ ] `:current Experimental`
|
|
||||||
* [ ] `:default`
|
|
||||||
* [ ] `:defined`
|
|
||||||
* [ ] `:dir() Experimental`
|
|
||||||
* [x] `:disabled`
|
|
||||||
* [x] `:empty`
|
|
||||||
* [x] `:enabled`
|
|
||||||
* [ ] `:first`
|
|
||||||
* [x] `:first-child`
|
|
||||||
* [x] `:first-of-type`
|
|
||||||
* [ ] `:focus`
|
|
||||||
* [ ] `:focus-visible`
|
|
||||||
* [ ] `:focus-within`
|
|
||||||
* [ ] `:fullscreen`
|
|
||||||
* [ ] `:future Experimental`
|
|
||||||
* [x] `:has() Experimental`
|
|
||||||
* [ ] `:host`
|
|
||||||
* [ ] `:host()`
|
|
||||||
* [ ] `:host-context() Experimental`
|
|
||||||
* [ ] `:hover`
|
|
||||||
* [ ] `:indeterminate`
|
|
||||||
* [ ] `:in-range`
|
|
||||||
* [ ] `:invalid`
|
|
||||||
* [ ] `:is()`
|
|
||||||
* [x] `:lang()`
|
|
||||||
* [x] `:last-child`
|
|
||||||
* [x] `:last-of-type`
|
|
||||||
* [ ] `:left`
|
|
||||||
* [x] `:link`
|
|
||||||
* [ ] `:local-link Experimental`
|
|
||||||
* [ ] `:modal`
|
|
||||||
* [x] `:not()`
|
|
||||||
* [x] `:nth-child()`
|
|
||||||
* [x] `:nth-last-child()`
|
|
||||||
* [x] `:nth-last-of-type()`
|
|
||||||
* [x] `:nth-of-type()`
|
|
||||||
* [x] `:only-child`
|
|
||||||
* [x] `:only-of-type`
|
|
||||||
* [ ] `:optional`
|
|
||||||
* [ ] `:out-of-range`
|
|
||||||
* [ ] `:past Experimental`
|
|
||||||
* [ ] `:paused`
|
|
||||||
* [ ] `:picture-in-picture`
|
|
||||||
* [ ] `:placeholder-shown`
|
|
||||||
* [ ] `:playing`
|
|
||||||
* [ ] `:read-only`
|
|
||||||
* [ ] `:read-write`
|
|
||||||
* [ ] `:required`
|
|
||||||
* [ ] `:right`
|
|
||||||
* [x] `:root`
|
|
||||||
* [ ] `:scope`
|
|
||||||
* [ ] `:state() Experimental`
|
|
||||||
* [ ] `:target`
|
|
||||||
* [ ] `:target-within Experimental`
|
|
||||||
* [ ] `:user-invalid Experimental`
|
|
||||||
* [ ] `:valid`
|
|
||||||
* [ ] `:visited`
|
|
||||||
* [ ] `:where()`
|
|
||||||
* [ ] `:contains()`
|
|
||||||
* [ ] `:containsown()`
|
|
||||||
* [ ] `:matched()`
|
|
||||||
* [ ] `:matchesown()`
|
|
||||||
* [x] `:root`
|
|
||||||
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// CSS Selector parser and query
|
|
||||||
// This package is a rewrite in Zig of Cascadia CSS Selector parser.
|
|
||||||
// see https://github.com/andybalholm/cascadia
|
|
||||||
const std = @import("std");
|
|
||||||
const Selector = @import("selector.zig").Selector;
|
|
||||||
const parser = @import("parser.zig");
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
Css,
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/CSS
|
|
||||||
pub const Css = struct {
|
|
||||||
_not_empty: bool = true,
|
|
||||||
|
|
||||||
pub fn _supports(_: *Css, _: []const u8, _: ?[]const u8) bool {
|
|
||||||
// TODO: Actually respond with which CSS features we support.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// parse parse a selector string and returns the parsed result or an error.
|
|
||||||
pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions) parser.ParseError!Selector {
|
|
||||||
var p = parser.Parser{ .s = s, .i = 0, .opts = opts };
|
|
||||||
return p.parse(alloc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchFirst call m.match with the first node that matches the selector s, from the
|
|
||||||
// descendants of n and returns true. If none matches, it returns false.
|
|
||||||
pub fn matchFirst(s: Selector, node: anytype, m: anytype) !bool {
|
|
||||||
var c = try node.firstChild();
|
|
||||||
while (true) {
|
|
||||||
if (c == null) break;
|
|
||||||
|
|
||||||
if (try s.match(c.?)) {
|
|
||||||
try m.match(c.?);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try matchFirst(s, c.?, m)) return true;
|
|
||||||
c = try c.?.nextSibling();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchAll call m.match with the all the nodes that matches the selector s, from the
|
|
||||||
// descendants of n.
|
|
||||||
pub fn matchAll(s: Selector, node: anytype, m: anytype) !void {
|
|
||||||
var c = try node.firstChild();
|
|
||||||
while (true) {
|
|
||||||
if (c == null) break;
|
|
||||||
|
|
||||||
if (try s.match(c.?)) try m.match(c.?);
|
|
||||||
try matchAll(s, c.?, m);
|
|
||||||
c = try c.?.nextSibling();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parse" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
const testcases = [_][]const u8{
|
|
||||||
"address",
|
|
||||||
"*",
|
|
||||||
"#foo",
|
|
||||||
"li#t1",
|
|
||||||
"*#t4",
|
|
||||||
".t1",
|
|
||||||
"p.t1",
|
|
||||||
"div.teST",
|
|
||||||
".t1.fail",
|
|
||||||
"p.t1.t2",
|
|
||||||
"p.--t1",
|
|
||||||
"p.--t1.--t2",
|
|
||||||
"p[title]",
|
|
||||||
"div[class=\"red\" i]",
|
|
||||||
"address[title=\"foo\"]",
|
|
||||||
"address[title=\"FoOIgnoRECaSe\" i]",
|
|
||||||
"address[title!=\"foo\"]",
|
|
||||||
"address[title!=\"foo\" i]",
|
|
||||||
"p[title!=\"FooBarUFoo\" i]",
|
|
||||||
"[ \t title ~= foo ]",
|
|
||||||
"p[title~=\"FOO\" i]",
|
|
||||||
"p[title~=toofoo i]",
|
|
||||||
"[title~=\"hello world\"]",
|
|
||||||
"[title~=\"hello\" i]",
|
|
||||||
"[title~=\"hello\" I]",
|
|
||||||
"[lang|=\"en\"]",
|
|
||||||
"[lang|=\"EN\" i]",
|
|
||||||
"[lang|=\"EN\" i]",
|
|
||||||
"[title^=\"foo\"]",
|
|
||||||
"[title^=\"foo\" i]",
|
|
||||||
"[title$=\"bar\"]",
|
|
||||||
"[title$=\"BAR\" i]",
|
|
||||||
"[title*=\"bar\"]",
|
|
||||||
"[title*=\"BaRu\" i]",
|
|
||||||
"[title*=\"BaRu\" I]",
|
|
||||||
"p[class$=\" \"]",
|
|
||||||
"p[class$=\"\"]",
|
|
||||||
"p[class^=\" \"]",
|
|
||||||
"p[class^=\"\"]",
|
|
||||||
"p[class*=\" \"]",
|
|
||||||
"p[class*=\"\"]",
|
|
||||||
"input[name=Sex][value=F]",
|
|
||||||
"table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]",
|
|
||||||
".t1:not(.t2)",
|
|
||||||
"div:not(.t1)",
|
|
||||||
"div:not([class=\"t2\"])",
|
|
||||||
"li:nth-child(odd)",
|
|
||||||
"li:nth-child(even)",
|
|
||||||
"li:nth-child(-n+2)",
|
|
||||||
"li:nth-child(3n+1)",
|
|
||||||
"li:nth-last-child(odd)",
|
|
||||||
"li:nth-last-child(even)",
|
|
||||||
"li:nth-last-child(-n+2)",
|
|
||||||
"li:nth-last-child(3n+1)",
|
|
||||||
"span:first-child",
|
|
||||||
"span:last-child",
|
|
||||||
"p:nth-of-type(2)",
|
|
||||||
"p:nth-last-of-type(2)",
|
|
||||||
"p:last-of-type",
|
|
||||||
"p:first-of-type",
|
|
||||||
"p:only-child",
|
|
||||||
"p:only-of-type",
|
|
||||||
":empty",
|
|
||||||
"div p",
|
|
||||||
"div table p",
|
|
||||||
"div > p",
|
|
||||||
"p ~ p",
|
|
||||||
"p + p",
|
|
||||||
"li, p",
|
|
||||||
"p +/*This is a comment*/ p",
|
|
||||||
"p:contains(\"that wraps\")",
|
|
||||||
"p:containsOwn(\"that wraps\")",
|
|
||||||
":containsOwn(\"inner\")",
|
|
||||||
"p:containsOwn(\"block\")",
|
|
||||||
"div:has(#p1)",
|
|
||||||
"div:has(:containsOwn(\"2\"))",
|
|
||||||
"body :has(:containsOwn(\"2\"))",
|
|
||||||
"body :haschild(:containsOwn(\"2\"))",
|
|
||||||
"p:matches([\\d])",
|
|
||||||
"p:matches([a-z])",
|
|
||||||
"p:matches([a-zA-Z])",
|
|
||||||
"p:matches([^\\d])",
|
|
||||||
"p:matches(^(0|a))",
|
|
||||||
"p:matches(^\\d+$)",
|
|
||||||
"p:not(:matches(^\\d+$))",
|
|
||||||
"div :matchesOwn(^\\d+$)",
|
|
||||||
"[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])",
|
|
||||||
"[href#=(^https:\\/\\/[^\\/]*\\/?news)]",
|
|
||||||
":input",
|
|
||||||
":root",
|
|
||||||
"*:root",
|
|
||||||
"html:nth-child(1)",
|
|
||||||
"*:root:first-child",
|
|
||||||
"*:root:nth-child(1)",
|
|
||||||
"a:not(:root)",
|
|
||||||
"body > *:nth-child(3n+2)",
|
|
||||||
"input:disabled",
|
|
||||||
":disabled",
|
|
||||||
":enabled",
|
|
||||||
"div.class1, div.class2",
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
const s = parse(alloc, tc, .{}) catch |e| {
|
|
||||||
std.debug.print("query {s}", .{tc});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.HTML.CSS" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "CSS.supports('display: flex')", "true" },
|
|
||||||
.{ "CSS.supports('text-decoration-style', 'blink')", "true" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
// Node implementation with Netsurf Libdom C lib.
|
|
||||||
pub const Node = struct {
|
|
||||||
node: *parser.Node,
|
|
||||||
|
|
||||||
pub fn firstChild(n: Node) !?Node {
|
|
||||||
const c = try parser.nodeFirstChild(n.node);
|
|
||||||
if (c) |cc| return .{ .node = cc };
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lastChild(n: Node) !?Node {
|
|
||||||
const c = try parser.nodeLastChild(n.node);
|
|
||||||
if (c) |cc| return .{ .node = cc };
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nextSibling(n: Node) !?Node {
|
|
||||||
const c = try parser.nodeNextSibling(n.node);
|
|
||||||
if (c) |cc| return .{ .node = cc };
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prevSibling(n: Node) !?Node {
|
|
||||||
const c = try parser.nodePreviousSibling(n.node);
|
|
||||||
if (c) |cc| return .{ .node = cc };
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parent(n: Node) !?Node {
|
|
||||||
const c = try parser.nodeParentNode(n.node);
|
|
||||||
if (c) |cc| return .{ .node = cc };
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isElement(n: Node) bool {
|
|
||||||
const t = parser.nodeType(n.node) catch return false;
|
|
||||||
return t == .element;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isDocument(n: Node) bool {
|
|
||||||
const t = parser.nodeType(n.node) catch return false;
|
|
||||||
return t == .document;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isComment(n: Node) bool {
|
|
||||||
const t = parser.nodeType(n.node) catch return false;
|
|
||||||
return t == .comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isText(n: Node) bool {
|
|
||||||
const t = parser.nodeType(n.node) catch return false;
|
|
||||||
return t == .text;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isEmptyText(n: Node) !bool {
|
|
||||||
const data = try parser.nodeTextContent(n.node);
|
|
||||||
if (data == null) return true;
|
|
||||||
if (data.?.len == 0) return true;
|
|
||||||
|
|
||||||
return std.mem.trim(u8, data.?, &std.ascii.whitespace).len == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tag(n: Node) ![]const u8 {
|
|
||||||
return try parser.nodeName(n.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn attr(n: Node, key: []const u8) !?[]const u8 {
|
|
||||||
if (!n.isElement()) return null;
|
|
||||||
return try parser.elementGetAttribute(parser.nodeToElement(n.node), key);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eql(a: Node, b: Node) bool {
|
|
||||||
return a.node == b.node;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const css = @import("css.zig");
|
|
||||||
const Node = @import("libdom.zig").Node;
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const Matcher = struct {
|
|
||||||
const Nodes = std.ArrayList(Node);
|
|
||||||
|
|
||||||
nodes: Nodes,
|
|
||||||
|
|
||||||
fn init(alloc: std.mem.Allocator) Matcher {
|
|
||||||
return .{ .nodes = Nodes.init(alloc) };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(m: *Matcher) void {
|
|
||||||
m.nodes.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(m: *Matcher) void {
|
|
||||||
m.nodes.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(m: *Matcher, n: Node) !void {
|
|
||||||
try m.nodes.append(n);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "matchFirst" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
html: []const u8,
|
|
||||||
exp: usize,
|
|
||||||
}{
|
|
||||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
|
||||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
|
||||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
|
||||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
|
||||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
|
||||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
|
||||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
|
||||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
|
||||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
|
||||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
|
||||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
|
||||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
|
||||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
|
|
||||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
|
|
||||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
|
|
||||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
|
||||||
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
|
||||||
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
|
||||||
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
|
||||||
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
|
|
||||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
|
||||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
|
||||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
|
|
||||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
|
||||||
defer parser.documentHTMLClose(doc) catch {};
|
|
||||||
|
|
||||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
|
||||||
std.debug.print("parse, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
|
||||||
|
|
||||||
_ = css.matchFirst(s, node, &matcher) catch |e| {
|
|
||||||
std.debug.print("match, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "matchAll" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
html: []const u8,
|
|
||||||
exp: usize,
|
|
||||||
}{
|
|
||||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
|
||||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
|
|
||||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
|
|
||||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
|
||||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
|
||||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
|
||||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
|
||||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
|
||||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
|
||||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
|
|
||||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
|
|
||||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
|
||||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
|
||||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
|
||||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
|
||||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
|
||||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
|
||||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
|
||||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
|
||||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
|
||||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
|
||||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
|
||||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
|
||||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
|
||||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
|
||||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
|
||||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
|
||||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
|
|
||||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
|
|
||||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
|
|
||||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
|
|
||||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
|
|
||||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
|
||||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
|
||||||
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
|
||||||
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
|
||||||
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
|
||||||
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
|
|
||||||
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
|
|
||||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
|
||||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
|
||||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
|
|
||||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
|
|
||||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
|
||||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
|
|
||||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
|
|
||||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
|
||||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 2 },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
|
||||||
defer parser.documentHTMLClose(doc) catch {};
|
|
||||||
|
|
||||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
|
||||||
std.debug.print("parse, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
|
||||||
|
|
||||||
_ = css.matchAll(s, node, &matcher) catch |e| {
|
|
||||||
std.debug.print("match, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,587 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const css = @import("css.zig");
|
|
||||||
|
|
||||||
// Node mock implementation for test only.
|
|
||||||
pub const Node = struct {
|
|
||||||
child: ?*const Node = null,
|
|
||||||
last: ?*const Node = null,
|
|
||||||
sibling: ?*const Node = null,
|
|
||||||
prev: ?*const Node = null,
|
|
||||||
par: ?*const Node = null,
|
|
||||||
|
|
||||||
name: []const u8 = "",
|
|
||||||
att: ?[]const u8 = null,
|
|
||||||
|
|
||||||
pub fn firstChild(n: *const Node) !?*const Node {
|
|
||||||
return n.child;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lastChild(n: *const Node) !?*const Node {
|
|
||||||
return n.last;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nextSibling(n: *const Node) !?*const Node {
|
|
||||||
return n.sibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prevSibling(n: *const Node) !?*const Node {
|
|
||||||
return n.prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parent(n: *const Node) !?*const Node {
|
|
||||||
return n.par;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isElement(_: *const Node) bool {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isDocument(_: *const Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isComment(_: *const Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isText(_: *const Node) bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isEmptyText(_: *const Node) !bool {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tag(n: *const Node) ![]const u8 {
|
|
||||||
return n.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn attr(n: *const Node, _: []const u8) !?[]const u8 {
|
|
||||||
return n.att;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eql(a: *const Node, b: *const Node) bool {
|
|
||||||
return a == b;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Matcher = struct {
|
|
||||||
const Nodes = std.ArrayList(*const Node);
|
|
||||||
|
|
||||||
nodes: Nodes,
|
|
||||||
|
|
||||||
fn init(alloc: std.mem.Allocator) Matcher {
|
|
||||||
return .{ .nodes = Nodes.init(alloc) };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(m: *Matcher) void {
|
|
||||||
m.nodes.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(m: *Matcher) void {
|
|
||||||
m.nodes.clearRetainingCapacity();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(m: *Matcher, n: *const Node) !void {
|
|
||||||
try m.nodes.append(n);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "matchFirst" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
n: Node,
|
|
||||||
exp: usize,
|
|
||||||
}{
|
|
||||||
.{
|
|
||||||
.q = "address",
|
|
||||||
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "#foo",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = ".t1",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = ".t1",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo=baz]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo!=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo!=baz]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo~=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo~=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo^=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo$=baz]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo*=rb]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo|=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo|=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo|=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "strong, a",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p a",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p a",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
|
|
||||||
.name = "a",
|
|
||||||
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
|
|
||||||
} } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = ":not(p)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:has(a)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:has(strong)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:haschild(a)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:haschild(strong)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:lang(en)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "a:lang(en)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const s = try css.parse(alloc, tc.q, .{});
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
_ = css.matchFirst(s, &tc.n, &matcher) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "matchAll" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
n: Node,
|
|
||||||
exp: usize,
|
|
||||||
}{
|
|
||||||
.{
|
|
||||||
.q = "address",
|
|
||||||
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "#foo",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = ".t1",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = ".t1",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo=baz]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo!=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo!=baz]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 2,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo~=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo~=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo^=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo$=baz]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo*=rb]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo|=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo|=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "[foo|=bar]",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "strong, a",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 2,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p a",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p a",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
|
|
||||||
.name = "a",
|
|
||||||
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
|
|
||||||
} } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = ":not(p)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 2,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:has(a)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:has(strong)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:haschild(a)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:haschild(strong)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
|
||||||
.exp = 0,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "p:lang(en)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.q = "a:lang(en)",
|
|
||||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
|
|
||||||
.exp = 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const s = try css.parse(alloc, tc.q, .{});
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
css.matchAll(s, &tc.n, &matcher) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "pseudo class" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
var p1: Node = .{ .name = "p" };
|
|
||||||
var p2: Node = .{ .name = "p" };
|
|
||||||
var a1: Node = .{ .name = "a" };
|
|
||||||
|
|
||||||
p1.sibling = &p2;
|
|
||||||
p2.prev = &p1;
|
|
||||||
|
|
||||||
p2.sibling = &a1;
|
|
||||||
a1.prev = &p2;
|
|
||||||
|
|
||||||
var root: Node = .{ .child = &p1, .last = &a1 };
|
|
||||||
p1.par = &root;
|
|
||||||
p2.par = &root;
|
|
||||||
a1.par = &root;
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
n: Node,
|
|
||||||
exp: ?*const Node,
|
|
||||||
}{
|
|
||||||
.{ .q = "p:only-child", .n = root, .exp = null },
|
|
||||||
.{ .q = "a:only-of-type", .n = root, .exp = &a1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const s = try css.parse(alloc, tc.q, .{});
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
css.matchAll(s, &tc.n, &matcher) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tc.exp) |exp_n| {
|
|
||||||
const exp: usize = 1;
|
|
||||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exp: usize = 0;
|
|
||||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "nth pseudo class" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
var matcher = Matcher.init(alloc);
|
|
||||||
defer matcher.deinit();
|
|
||||||
|
|
||||||
var p1: Node = .{ .name = "p" };
|
|
||||||
var p2: Node = .{ .name = "p" };
|
|
||||||
|
|
||||||
p1.sibling = &p2;
|
|
||||||
p2.prev = &p1;
|
|
||||||
|
|
||||||
var root: Node = .{ .child = &p1, .last = &p2 };
|
|
||||||
p1.par = &root;
|
|
||||||
p2.par = &root;
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
q: []const u8,
|
|
||||||
n: Node,
|
|
||||||
exp: ?*const Node,
|
|
||||||
}{
|
|
||||||
.{ .q = "a:nth-of-type(1)", .n = root, .exp = null },
|
|
||||||
.{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 },
|
|
||||||
.{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 },
|
|
||||||
.{ .q = "p:nth-of-type(0)", .n = root, .exp = null },
|
|
||||||
.{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 },
|
|
||||||
.{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 },
|
|
||||||
.{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 },
|
|
||||||
.{ .q = "p:nth-child(1)", .n = root, .exp = &p1 },
|
|
||||||
.{ .q = "p:nth-child(2)", .n = root, .exp = &p2 },
|
|
||||||
.{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 },
|
|
||||||
.{ .q = "p:nth-child(even)", .n = root, .exp = &p2 },
|
|
||||||
.{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
matcher.reset();
|
|
||||||
|
|
||||||
const s = try css.parse(alloc, tc.q, .{});
|
|
||||||
defer s.deinit(alloc);
|
|
||||||
|
|
||||||
css.matchAll(s, &tc.n, &matcher) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tc.exp) |exp_n| {
|
|
||||||
const exp: usize = 1;
|
|
||||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exp: usize = 0;
|
|
||||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
|
||||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,951 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// CSS Selector parser
|
|
||||||
// This file is a rewrite in Zig of Cascadia CSS Selector parser.
|
|
||||||
// see https://github.com/andybalholm/cascadia
|
|
||||||
// see https://github.com/andybalholm/cascadia/blob/master/parser.go
|
|
||||||
const std = @import("std");
|
|
||||||
const ascii = std.ascii;
|
|
||||||
|
|
||||||
const selector = @import("selector.zig");
|
|
||||||
const Selector = selector.Selector;
|
|
||||||
const PseudoClass = selector.PseudoClass;
|
|
||||||
const AttributeOP = selector.AttributeOP;
|
|
||||||
const Combinator = selector.Combinator;
|
|
||||||
|
|
||||||
const REPLACEMENT_CHARACTER = &.{ 239, 191, 189 };
|
|
||||||
|
|
||||||
pub const ParseError = error{
|
|
||||||
ExpectedSelector,
|
|
||||||
ExpectedIdentifier,
|
|
||||||
ExpectedName,
|
|
||||||
ExpectedIDSelector,
|
|
||||||
ExpectedClassSelector,
|
|
||||||
ExpectedAttributeSelector,
|
|
||||||
ExpectedString,
|
|
||||||
ExpectedRegexp,
|
|
||||||
ExpectedPseudoClassSelector,
|
|
||||||
ExpectedParenthesis,
|
|
||||||
ExpectedParenthesisClose,
|
|
||||||
ExpectedNthExpression,
|
|
||||||
ExpectedInteger,
|
|
||||||
InvalidEscape,
|
|
||||||
EscapeLineEndingOutsideString,
|
|
||||||
InvalidUnicode,
|
|
||||||
UnicodeIsNotHandled,
|
|
||||||
WriteError,
|
|
||||||
PseudoElementNotAtSelectorEnd,
|
|
||||||
PseudoElementNotUnique,
|
|
||||||
PseudoElementDisabled,
|
|
||||||
InvalidAttributeOperator,
|
|
||||||
InvalidAttributeSelector,
|
|
||||||
InvalidString,
|
|
||||||
InvalidRegexp,
|
|
||||||
InvalidPseudoClassSelector,
|
|
||||||
EmptyPseudoClassSelector,
|
|
||||||
InvalidPseudoClass,
|
|
||||||
InvalidPseudoElement,
|
|
||||||
UnmatchParenthesis,
|
|
||||||
NotHandled,
|
|
||||||
UnknownPseudoSelector,
|
|
||||||
InvalidNthExpression,
|
|
||||||
} || PseudoClass.Error || Combinator.Error || std.mem.Allocator.Error;
|
|
||||||
|
|
||||||
pub const ParseOptions = struct {
|
|
||||||
accept_pseudo_elts: bool = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Parser = struct {
|
|
||||||
s: []const u8, // string to parse
|
|
||||||
i: usize = 0, // current position
|
|
||||||
|
|
||||||
opts: ParseOptions,
|
|
||||||
|
|
||||||
pub fn parse(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
|
||||||
return p.parseSelectorGroup(alloc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// skipWhitespace consumes whitespace characters and comments.
|
|
||||||
// It returns true if there was actually anything to skip.
|
|
||||||
fn skipWhitespace(p: *Parser) bool {
|
|
||||||
var i = p.i;
|
|
||||||
while (i < p.s.len) {
|
|
||||||
const c = p.s[i];
|
|
||||||
// Whitespaces.
|
|
||||||
if (ascii.isWhitespace(c)) {
|
|
||||||
i += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comments.
|
|
||||||
if (c == '/') {
|
|
||||||
if (std.mem.startsWith(u8, p.s[i..], "/*")) {
|
|
||||||
if (std.mem.indexOf(u8, p.s[i..], "*/")) |end| {
|
|
||||||
i += end + "*/".len;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i > p.i) {
|
|
||||||
p.i = i;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseSimpleSelectorSequence parses a selector sequence that applies to
|
|
||||||
// a single element.
|
|
||||||
fn parseSimpleSelectorSequence(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
|
||||||
if (p.i >= p.s.len) {
|
|
||||||
return ParseError.ExpectedSelector;
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf = std.ArrayList(Selector).init(alloc);
|
|
||||||
defer buf.deinit();
|
|
||||||
|
|
||||||
switch (p.s[p.i]) {
|
|
||||||
'*' => {
|
|
||||||
// It's the universal selector. Just skip over it, since it
|
|
||||||
// doesn't affect the meaning.
|
|
||||||
p.i += 1;
|
|
||||||
|
|
||||||
// other version of universal selector
|
|
||||||
if (p.i + 2 < p.s.len and std.mem.eql(u8, "|*", p.s[p.i .. p.i + 2])) {
|
|
||||||
p.i += 2;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'#', '.', '[', ':' => {
|
|
||||||
// There's no type selector. Wait to process the other till the
|
|
||||||
// main loop.
|
|
||||||
},
|
|
||||||
else => try buf.append(try p.parseTypeSelector(alloc)),
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
else => break :loop,
|
|
||||||
};
|
|
||||||
errdefer ns.deinit(alloc);
|
|
||||||
|
|
||||||
// From https://drafts.csswg.org/selectors-3/#pseudo-elements :
|
|
||||||
// "Only one pseudo-element may appear per selector, and if present
|
|
||||||
// it must appear after the sequence of simple selectors that
|
|
||||||
// represents the subjects of the selector.""
|
|
||||||
switch (ns) {
|
|
||||||
.pseudo_element => |e| {
|
|
||||||
// We found a pseudo-element.
|
|
||||||
// Only one pseudo-element is accepted per selector.
|
|
||||||
if (pseudo_elt != null) return ParseError.PseudoElementNotUnique;
|
|
||||||
if (!p.opts.accept_pseudo_elts) return ParseError.PseudoElementDisabled;
|
|
||||||
|
|
||||||
pseudo_elt = e;
|
|
||||||
ns.deinit(alloc);
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
if (pseudo_elt != null) return ParseError.PseudoElementNotAtSelectorEnd;
|
|
||||||
try buf.append(ns);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no need wrap the selectors in compoundSelector
|
|
||||||
if (buf.items.len == 1 and pseudo_elt == null) return buf.items[0];
|
|
||||||
|
|
||||||
return .{ .compound = .{ .selectors = try buf.toOwnedSlice(), .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());
|
|
||||||
|
|
||||||
return .{ .tag = try buf.toOwnedSlice() };
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseIdentifier parses an identifier.
|
|
||||||
fn parseIdentifier(p: *Parser, w: anytype) ParseError!void {
|
|
||||||
const prefix = '-';
|
|
||||||
var numPrefix: usize = 0;
|
|
||||||
|
|
||||||
while (p.s.len > p.i and p.s[p.i] == prefix) {
|
|
||||||
p.i += 1;
|
|
||||||
numPrefix += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p.s.len <= p.i) {
|
|
||||||
return ParseError.ExpectedSelector;
|
|
||||||
}
|
|
||||||
|
|
||||||
const c = p.s[p.i];
|
|
||||||
if (!nameStart(c) or c == '\\') {
|
|
||||||
return ParseError.ExpectedSelector;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ii: usize = 0;
|
|
||||||
while (ii < numPrefix) {
|
|
||||||
w.writeByte(prefix) catch return ParseError.WriteError;
|
|
||||||
ii += 1;
|
|
||||||
}
|
|
||||||
try parseName(p, w);
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseName parses a name (which is like an identifier, but doesn't have
|
|
||||||
// extra restrictions on the first character).
|
|
||||||
fn parseName(p: *Parser, w: anytype) ParseError!void {
|
|
||||||
const sel = p.s;
|
|
||||||
const sel_len = sel.len;
|
|
||||||
|
|
||||||
var i = p.i;
|
|
||||||
var ok = false;
|
|
||||||
|
|
||||||
while (i < sel_len) {
|
|
||||||
const c = sel[i];
|
|
||||||
|
|
||||||
if (nameChar(c)) {
|
|
||||||
const start = i;
|
|
||||||
while (i < sel_len and nameChar(sel[i])) i += 1;
|
|
||||||
w.writeAll(sel[start..i]) catch return ParseError.WriteError;
|
|
||||||
ok = true;
|
|
||||||
} else if (c == '\\') {
|
|
||||||
p.i = i;
|
|
||||||
try p.parseEscape(w);
|
|
||||||
i = p.i;
|
|
||||||
ok = true;
|
|
||||||
} else if (c == 0) {
|
|
||||||
w.writeAll(REPLACEMENT_CHARACTER) catch return ParseError.WriteError;
|
|
||||||
i += 1;
|
|
||||||
if (i == sel_len) {
|
|
||||||
ok = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ok) return ParseError.ExpectedName;
|
|
||||||
p.i = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseEscape parses a backslash escape.
|
|
||||||
// The returned string is owned by the caller.
|
|
||||||
fn parseEscape(p: *Parser, w: anytype) ParseError!void {
|
|
||||||
const sel = p.s;
|
|
||||||
const sel_len = sel.len;
|
|
||||||
|
|
||||||
if (sel_len < p.i + 2 or sel[p.i] != '\\') {
|
|
||||||
p.i += 1;
|
|
||||||
w.writeAll(REPLACEMENT_CHARACTER) catch return ParseError.WriteError;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = p.i + 1;
|
|
||||||
const c = sel[start];
|
|
||||||
|
|
||||||
// unicode escape (hex)
|
|
||||||
if (ascii.isHex(c)) {
|
|
||||||
var i: usize = start;
|
|
||||||
while (i < start + 6 and i < sel_len and ascii.isHex(sel[i])) {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const v = std.fmt.parseUnsigned(u21, sel[start..i], 16) catch {
|
|
||||||
p.i = i;
|
|
||||||
w.writeAll(REPLACEMENT_CHARACTER) catch return ParseError.WriteError;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sel_len >= i) {
|
|
||||||
if (sel_len > i) {
|
|
||||||
switch (sel[i]) {
|
|
||||||
'\r' => {
|
|
||||||
i += 1;
|
|
||||||
if (sel_len > i and sel[i] == '\n') i += 1;
|
|
||||||
},
|
|
||||||
' ', '\t', '\n', std.ascii.control_code.ff => i += 1,
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.i = i;
|
|
||||||
if (v == 0) {
|
|
||||||
w.writeAll(REPLACEMENT_CHARACTER) catch return ParseError.WriteError;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var buf: [4]u8 = undefined;
|
|
||||||
const ln = std.unicode.utf8Encode(v, &buf) catch {
|
|
||||||
w.writeAll(REPLACEMENT_CHARACTER) catch return ParseError.WriteError;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
w.writeAll(buf[0..ln]) catch return ParseError.WriteError;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the literal character after the backslash.
|
|
||||||
p.i += 2;
|
|
||||||
w.writeByte(sel[start]) catch return ParseError.WriteError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseIDSelector parses a selector that matches by id attribute.
|
|
||||||
fn parseIDSelector(p: *Parser, alloc: std.mem.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();
|
|
||||||
|
|
||||||
try p.parseName(buf.writer());
|
|
||||||
return .{ .id = try buf.toOwnedSlice() };
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseClassSelector parses a selector that matches by class attribute.
|
|
||||||
fn parseClassSelector(p: *Parser, alloc: std.mem.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();
|
|
||||||
|
|
||||||
try p.parseIdentifier(buf.writer());
|
|
||||||
return .{ .class = try buf.toOwnedSlice() };
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseAttributeSelector parses a selector that matches by attribute value.
|
|
||||||
fn parseAttributeSelector(p: *Parser, alloc: std.mem.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();
|
|
||||||
|
|
||||||
try p.parseIdentifier(buf.writer());
|
|
||||||
const key = try buf.toOwnedSlice();
|
|
||||||
errdefer alloc.free(key);
|
|
||||||
|
|
||||||
lowerstr(key);
|
|
||||||
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
|
||||||
if (p.s[p.i] == ']') {
|
|
||||||
p.i += 1;
|
|
||||||
return .{ .attribute = .{ .key = key } };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p.i + 2 >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
|
||||||
|
|
||||||
const op = try parseAttributeOP(p.s[p.i .. p.i + 2]);
|
|
||||||
p.i += op.len();
|
|
||||||
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
|
||||||
|
|
||||||
buf.clearRetainingCapacity();
|
|
||||||
var is_val: bool = undefined;
|
|
||||||
if (op == .regexp) {
|
|
||||||
is_val = false;
|
|
||||||
try p.parseRegex(buf.writer());
|
|
||||||
} else {
|
|
||||||
is_val = true;
|
|
||||||
switch (p.s[p.i]) {
|
|
||||||
'\'', '"' => try p.parseString(buf.writer()),
|
|
||||||
else => try p.parseIdentifier(buf.writer()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
|
||||||
|
|
||||||
// check if the attribute contains an ignore case flag
|
|
||||||
var ci = false;
|
|
||||||
if (p.s[p.i] == 'i' or p.s[p.i] == 'I') {
|
|
||||||
ci = true;
|
|
||||||
p.i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
|
||||||
|
|
||||||
if (p.s[p.i] != ']') return ParseError.InvalidAttributeSelector;
|
|
||||||
p.i += 1;
|
|
||||||
|
|
||||||
return .{ .attribute = .{
|
|
||||||
.key = key,
|
|
||||||
.val = if (is_val) try buf.toOwnedSlice() else null,
|
|
||||||
.regexp = if (!is_val) try buf.toOwnedSlice() else null,
|
|
||||||
.op = op,
|
|
||||||
.ci = ci,
|
|
||||||
} };
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseString parses a single- or double-quoted string.
|
|
||||||
fn parseString(p: *Parser, writer: anytype) ParseError!void {
|
|
||||||
const sel = p.s;
|
|
||||||
const sel_len = sel.len;
|
|
||||||
|
|
||||||
var i = p.i;
|
|
||||||
if (sel_len < i + 2) return ParseError.ExpectedString;
|
|
||||||
|
|
||||||
const quote = sel[i];
|
|
||||||
i += 1;
|
|
||||||
|
|
||||||
loop: while (i < sel_len) {
|
|
||||||
switch (sel[i]) {
|
|
||||||
'\\' => {
|
|
||||||
if (sel_len > i + 1) {
|
|
||||||
const c = sel[i + 1];
|
|
||||||
switch (c) {
|
|
||||||
'\r' => {
|
|
||||||
if (sel_len > i + 2 and sel[i + 2] == '\n') {
|
|
||||||
i += 3;
|
|
||||||
continue :loop;
|
|
||||||
}
|
|
||||||
i += 2;
|
|
||||||
continue :loop;
|
|
||||||
},
|
|
||||||
'\n', std.ascii.control_code.ff => {
|
|
||||||
i += 2;
|
|
||||||
continue :loop;
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.i = i;
|
|
||||||
try p.parseEscape(writer);
|
|
||||||
i = p.i;
|
|
||||||
},
|
|
||||||
'\r', '\n', std.ascii.control_code.ff => return ParseError.InvalidString,
|
|
||||||
else => |c| {
|
|
||||||
if (c == quote) break :loop;
|
|
||||||
const start = i;
|
|
||||||
while (i < sel_len) {
|
|
||||||
const cc = sel[i];
|
|
||||||
if (cc == quote or cc == '\\' or c == '\r' or c == '\n' or c == std.ascii.control_code.ff) break;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
writer.writeAll(sel[start..i]) catch return ParseError.WriteError;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i >= sel_len) return ParseError.InvalidString;
|
|
||||||
|
|
||||||
// Consume the final quote.
|
|
||||||
i += 1;
|
|
||||||
p.i = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseRegex parses a regular expression; the end is defined by encountering an
|
|
||||||
// unmatched closing ')' or ']' which is not consumed
|
|
||||||
fn parseRegex(p: *Parser, writer: anytype) ParseError!void {
|
|
||||||
var i = p.i;
|
|
||||||
if (p.s.len < i + 2) return ParseError.ExpectedRegexp;
|
|
||||||
|
|
||||||
// number of open parens or brackets;
|
|
||||||
// when it becomes negative, finished parsing regex
|
|
||||||
var open: isize = 0;
|
|
||||||
|
|
||||||
loop: while (i < p.s.len) {
|
|
||||||
switch (p.s[i]) {
|
|
||||||
'(', '[' => open += 1,
|
|
||||||
')', ']' => {
|
|
||||||
open -= 1;
|
|
||||||
if (open < 0) break :loop;
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i >= p.s.len) return ParseError.InvalidRegexp;
|
|
||||||
writer.writeAll(p.s[p.i..i]) catch return ParseError.WriteError;
|
|
||||||
p.i = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parsePseudoclassSelector parses a pseudoclass selector like :not(p) or a pseudo-element
|
|
||||||
// For backwards compatibility, both ':' and '::' prefix are allowed for pseudo-elements.
|
|
||||||
// https://drafts.csswg.org/selectors-3/#pseudo-elements
|
|
||||||
fn parsePseudoclassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedPseudoClassSelector;
|
|
||||||
if (p.s[p.i] != ':') return ParseError.ExpectedPseudoClassSelector;
|
|
||||||
|
|
||||||
p.i += 1;
|
|
||||||
|
|
||||||
var must_pseudo_elt: bool = false;
|
|
||||||
if (p.i >= p.s.len) return ParseError.EmptyPseudoClassSelector;
|
|
||||||
if (p.s[p.i] == ':') { // we found a pseudo-element
|
|
||||||
must_pseudo_elt = true;
|
|
||||||
p.i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf = std.ArrayList(u8).init(alloc);
|
|
||||||
defer buf.deinit();
|
|
||||||
|
|
||||||
try p.parseIdentifier(buf.writer());
|
|
||||||
|
|
||||||
const pseudo_class = try PseudoClass.parse(buf.items);
|
|
||||||
|
|
||||||
// reset the buffer to reuse it.
|
|
||||||
buf.clearRetainingCapacity();
|
|
||||||
|
|
||||||
if (must_pseudo_elt and !pseudo_class.isPseudoElement()) return ParseError.InvalidPseudoElement;
|
|
||||||
|
|
||||||
switch (pseudo_class) {
|
|
||||||
.not, .has, .haschild => {
|
|
||||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
|
||||||
|
|
||||||
const sel = try p.parseSelectorGroup(alloc);
|
|
||||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
|
||||||
|
|
||||||
const s = try alloc.create(Selector);
|
|
||||||
errdefer alloc.destroy(s);
|
|
||||||
s.* = sel;
|
|
||||||
|
|
||||||
return .{ .pseudo_class_relative = .{ .pseudo_class = pseudo_class, .match = s } };
|
|
||||||
},
|
|
||||||
.contains, .containsown => {
|
|
||||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
|
||||||
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
|
|
||||||
|
|
||||||
switch (p.s[p.i]) {
|
|
||||||
'\'', '"' => try p.parseString(buf.writer()),
|
|
||||||
else => try p.parseString(buf.writer()),
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = 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);
|
|
||||||
|
|
||||||
lowerstr(val);
|
|
||||||
|
|
||||||
return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } };
|
|
||||||
},
|
|
||||||
.matches, .matchesown => {
|
|
||||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
|
||||||
|
|
||||||
try p.parseRegex(buf.writer());
|
|
||||||
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() } };
|
|
||||||
},
|
|
||||||
.nth_child, .nth_last_child, .nth_of_type, .nth_last_of_type => {
|
|
||||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
|
||||||
const nth = try p.parseNth(alloc);
|
|
||||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
|
||||||
|
|
||||||
const last = pseudo_class == .nth_last_child or pseudo_class == .nth_last_of_type;
|
|
||||||
const of_type = pseudo_class == .nth_of_type or pseudo_class == .nth_last_of_type;
|
|
||||||
return .{ .pseudo_class_nth = .{ .a = nth[0], .b = nth[1], .of_type = of_type, .last = last } };
|
|
||||||
},
|
|
||||||
.first_child => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = false, .last = false } },
|
|
||||||
.last_child => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = false, .last = true } },
|
|
||||||
.first_of_type => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = true, .last = false } },
|
|
||||||
.last_of_type => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = true, .last = true } },
|
|
||||||
.only_child => return .{ .pseudo_class_only_child = false },
|
|
||||||
.only_of_type => return .{ .pseudo_class_only_child = true },
|
|
||||||
.input, .empty, .root, .link => return .{ .pseudo_class = pseudo_class },
|
|
||||||
.enabled, .disabled, .checked => return .{ .pseudo_class = pseudo_class },
|
|
||||||
.lang => {
|
|
||||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
|
||||||
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
|
|
||||||
|
|
||||||
try p.parseIdentifier(buf.writer());
|
|
||||||
|
|
||||||
_ = 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);
|
|
||||||
lowerstr(val);
|
|
||||||
|
|
||||||
return .{ .pseudo_class_lang = val };
|
|
||||||
},
|
|
||||||
.visited, .hover, .active, .focus, .target => {
|
|
||||||
// Not applicable in a static context: never match.
|
|
||||||
return .{ .never_match = pseudo_class };
|
|
||||||
},
|
|
||||||
.after, .backdrop, .before, .cue, .first_letter => return .{ .pseudo_element = pseudo_class },
|
|
||||||
.first_line, .grammar_error, .marker, .placeholder => return .{ .pseudo_element = pseudo_class },
|
|
||||||
.selection, .spelling_error => return .{ .pseudo_element = pseudo_class },
|
|
||||||
.modal => return .{ .pseudo_element = pseudo_class },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// consumeParenthesis consumes an opening parenthesis and any following
|
|
||||||
// whitespace. It returns true if there was actually a parenthesis to skip.
|
|
||||||
fn consumeParenthesis(p: *Parser) bool {
|
|
||||||
if (p.i < p.s.len and p.s[p.i] == '(') {
|
|
||||||
p.i += 1;
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseSelectorGroup parses a group of selectors, separated by commas.
|
|
||||||
fn parseSelectorGroup(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
|
||||||
const s = try p.parseSelector(alloc);
|
|
||||||
|
|
||||||
var buf = std.ArrayList(Selector).init(alloc);
|
|
||||||
defer buf.deinit();
|
|
||||||
|
|
||||||
try buf.append(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buf.items.len == 1) return buf.items[0];
|
|
||||||
|
|
||||||
return .{ .group = try buf.toOwnedSlice() };
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseSelector parses a selector that may include combinators.
|
|
||||||
fn parseSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
var s = try p.parseSimpleSelectorSequence(alloc);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
var combinator: Combinator = .empty;
|
|
||||||
if (p.skipWhitespace()) {
|
|
||||||
combinator = .descendant;
|
|
||||||
}
|
|
||||||
if (p.i >= p.s.len) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (p.s[p.i]) {
|
|
||||||
'+', '>', '~' => {
|
|
||||||
combinator = try Combinator.parse(p.s[p.i]);
|
|
||||||
p.i += 1;
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
},
|
|
||||||
// These characters can't begin a selector, but they can legally occur after one.
|
|
||||||
',', ')' => {
|
|
||||||
return s;
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (combinator == .empty) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
const c = try p.parseSimpleSelectorSequence(alloc);
|
|
||||||
|
|
||||||
const first = try alloc.create(Selector);
|
|
||||||
errdefer alloc.destroy(first);
|
|
||||||
first.* = s;
|
|
||||||
|
|
||||||
const second = try alloc.create(Selector);
|
|
||||||
errdefer alloc.destroy(second);
|
|
||||||
second.* = c;
|
|
||||||
|
|
||||||
s = Selector{ .combined = .{ .first = first, .second = second, .combinator = combinator } };
|
|
||||||
}
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
// consumeClosingParenthesis consumes a closing parenthesis and any preceding
|
|
||||||
// whitespace. It returns true if there was actually a parenthesis to skip.
|
|
||||||
fn consumeClosingParenthesis(p: *Parser) bool {
|
|
||||||
const i = p.i;
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i < p.s.len and p.s[p.i] == ')') {
|
|
||||||
p.i += 1;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
p.i = i;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseInteger parses a decimal integer.
|
|
||||||
fn parseInteger(p: *Parser) ParseError!isize {
|
|
||||||
var i = p.i;
|
|
||||||
const start = i;
|
|
||||||
while (i < p.s.len and '0' <= p.s[i] and p.s[i] <= '9') i += 1;
|
|
||||||
if (i == start) return ParseError.ExpectedInteger;
|
|
||||||
p.i = i;
|
|
||||||
|
|
||||||
return std.fmt.parseUnsigned(isize, p.s[start..i], 10) catch ParseError.ExpectedInteger;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseNthReadN(p: *Parser, a: isize) ParseError![2]isize {
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
|
||||||
|
|
||||||
return switch (p.s[p.i]) {
|
|
||||||
'+' => {
|
|
||||||
p.i += 1;
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
const b = try p.parseInteger();
|
|
||||||
return .{ a, b };
|
|
||||||
},
|
|
||||||
'-' => {
|
|
||||||
p.i += 1;
|
|
||||||
_ = p.skipWhitespace();
|
|
||||||
const b = try p.parseInteger();
|
|
||||||
return .{ a, -b };
|
|
||||||
},
|
|
||||||
else => .{ a, 0 },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseNthReadA(p: *Parser, a: isize) ParseError![2]isize {
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
|
||||||
return switch (p.s[p.i]) {
|
|
||||||
'n', 'N' => {
|
|
||||||
p.i += 1;
|
|
||||||
return p.parseNthReadN(a);
|
|
||||||
},
|
|
||||||
else => .{ 0, a },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseNthNegativeA(p: *Parser) ParseError![2]isize {
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
|
||||||
const c = p.s[p.i];
|
|
||||||
if (std.ascii.isDigit(c)) {
|
|
||||||
const a = try p.parseInteger() * -1;
|
|
||||||
return p.parseNthReadA(a);
|
|
||||||
}
|
|
||||||
if (c == 'n' or c == 'N') {
|
|
||||||
p.i += 1;
|
|
||||||
return p.parseNthReadN(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseError.InvalidNthExpression;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseNthPositiveA(p: *Parser) ParseError![2]isize {
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
|
||||||
const c = p.s[p.i];
|
|
||||||
if (std.ascii.isDigit(c)) {
|
|
||||||
const a = try p.parseInteger();
|
|
||||||
return p.parseNthReadA(a);
|
|
||||||
}
|
|
||||||
if (c == 'n' or c == 'N') {
|
|
||||||
p.i += 1;
|
|
||||||
return p.parseNthReadN(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseError.InvalidNthExpression;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseNth parses the argument for :nth-child (normally of the form an+b).
|
|
||||||
fn parseNth(p: *Parser, alloc: std.mem.Allocator) ParseError![2]isize {
|
|
||||||
// initial state
|
|
||||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
|
||||||
return switch (p.s[p.i]) {
|
|
||||||
'-' => {
|
|
||||||
p.i += 1;
|
|
||||||
return p.parseNthNegativeA();
|
|
||||||
},
|
|
||||||
'+' => {
|
|
||||||
p.i += 1;
|
|
||||||
return p.parseNthPositiveA();
|
|
||||||
},
|
|
||||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => p.parseNthPositiveA(),
|
|
||||||
'n', 'N' => {
|
|
||||||
p.i += 1;
|
|
||||||
return p.parseNthReadN(1);
|
|
||||||
},
|
|
||||||
'o', 'O', 'e', 'E' => {
|
|
||||||
var buf = std.ArrayList(u8).init(alloc);
|
|
||||||
defer buf.deinit();
|
|
||||||
|
|
||||||
try p.parseName(buf.writer());
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase("odd", buf.items)) return .{ 2, 1 };
|
|
||||||
if (std.ascii.eqlIgnoreCase("even", buf.items)) return .{ 2, 0 };
|
|
||||||
|
|
||||||
return ParseError.InvalidNthExpression;
|
|
||||||
},
|
|
||||||
else => ParseError.InvalidNthExpression,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// nameStart returns whether c can be the first character of an identifier
|
|
||||||
// (not counting an initial hyphen, or an escape sequence).
|
|
||||||
fn nameStart(c: u8) bool {
|
|
||||||
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127;
|
|
||||||
}
|
|
||||||
|
|
||||||
// nameChar returns whether c can be a character within an identifier
|
|
||||||
// (not counting an escape sequence).
|
|
||||||
fn nameChar(c: u8) bool {
|
|
||||||
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127 or
|
|
||||||
c == '-' or '0' <= c and c <= '9';
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lowerstr(str: []u8) void {
|
|
||||||
for (str, 0..) |c, i| {
|
|
||||||
str[i] = std.ascii.toLower(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseAttributeOP parses an AttributeOP from a string of 1 or 2 bytes.
|
|
||||||
fn parseAttributeOP(s: []const u8) ParseError!AttributeOP {
|
|
||||||
if (s.len < 1 or s.len > 2) return ParseError.InvalidAttributeOperator;
|
|
||||||
|
|
||||||
// if the first sign is equal, we don't check anything else.
|
|
||||||
if (s[0] == '=') return .eql;
|
|
||||||
|
|
||||||
if (s.len != 2 or s[1] != '=') return ParseError.InvalidAttributeOperator;
|
|
||||||
|
|
||||||
return switch (s[0]) {
|
|
||||||
'=' => .eql,
|
|
||||||
'!' => .not_eql,
|
|
||||||
'~' => .one_of,
|
|
||||||
'|' => .prefix_hyphen,
|
|
||||||
'^' => .prefix,
|
|
||||||
'$' => .suffix,
|
|
||||||
'*' => .contains,
|
|
||||||
'#' => .regexp,
|
|
||||||
else => ParseError.InvalidAttributeOperator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parser.skipWhitespace" {
|
|
||||||
const testcases = [_]struct {
|
|
||||||
s: []const u8,
|
|
||||||
i: usize,
|
|
||||||
r: bool,
|
|
||||||
}{
|
|
||||||
.{ .s = "", .i = 0, .r = false },
|
|
||||||
.{ .s = "foo", .i = 0, .r = false },
|
|
||||||
.{ .s = " ", .i = 1, .r = true },
|
|
||||||
.{ .s = " foo", .i = 1, .r = true },
|
|
||||||
.{ .s = "/* foo */ bar", .i = 10, .r = true },
|
|
||||||
.{ .s = "/* foo", .i = 0, .r = false },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
|
||||||
const res = p.skipWhitespace();
|
|
||||||
try std.testing.expectEqual(tc.r, res);
|
|
||||||
try std.testing.expectEqual(tc.i, p.i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parser.parseIdentifier" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
s: []const u8, // given value
|
|
||||||
exp: []const u8, // expected value
|
|
||||||
err: bool = false,
|
|
||||||
}{
|
|
||||||
.{ .s = "x", .exp = "x" },
|
|
||||||
.{ .s = "96", .exp = "", .err = true },
|
|
||||||
.{ .s = "-x", .exp = "-x" },
|
|
||||||
.{ .s = "r\\e9 sumé", .exp = "résumé" },
|
|
||||||
.{ .s = "r\\0000e9 sumé", .exp = "résumé" },
|
|
||||||
.{ .s = "r\\0000e9sumé", .exp = "résumé" },
|
|
||||||
.{ .s = "a\\\"b", .exp = "a\"b" },
|
|
||||||
};
|
|
||||||
|
|
||||||
var buf = std.ArrayList(u8).init(alloc);
|
|
||||||
defer buf.deinit();
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
buf.clearRetainingCapacity();
|
|
||||||
|
|
||||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
|
||||||
p.parseIdentifier(buf.writer()) catch |e| {
|
|
||||||
// if error was expected, continue.
|
|
||||||
if (tc.err) continue;
|
|
||||||
|
|
||||||
std.debug.print("test case {s}\n", .{tc.s});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
std.testing.expectEqualDeep(tc.exp, buf.items) catch |e| {
|
|
||||||
std.debug.print("test case {s} : {s}\n", .{ tc.s, buf.items });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parser.parseString" {
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
|
|
||||||
const testcases = [_]struct {
|
|
||||||
s: []const u8, // given value
|
|
||||||
exp: []const u8, // expected value
|
|
||||||
err: bool = false,
|
|
||||||
}{
|
|
||||||
.{ .s = "\"x\"", .exp = "x" },
|
|
||||||
.{ .s = "'x'", .exp = "x" },
|
|
||||||
.{ .s = "'x", .exp = "", .err = true },
|
|
||||||
.{ .s = "'x\\\r\nx'", .exp = "xx" },
|
|
||||||
.{ .s = "\"r\\e9 sumé\"", .exp = "résumé" },
|
|
||||||
.{ .s = "\"r\\0000e9 sumé\"", .exp = "résumé" },
|
|
||||||
.{ .s = "\"r\\0000e9sumé\"", .exp = "résumé" },
|
|
||||||
.{ .s = "\"a\\\"b\"", .exp = "a\"b" },
|
|
||||||
.{ .s = "\"\\\n\"", .exp = "" },
|
|
||||||
.{ .s = "\"hello world\"", .exp = "hello world" },
|
|
||||||
};
|
|
||||||
|
|
||||||
var buf = std.ArrayList(u8).init(alloc);
|
|
||||||
defer buf.deinit();
|
|
||||||
|
|
||||||
for (testcases) |tc| {
|
|
||||||
buf.clearRetainingCapacity();
|
|
||||||
|
|
||||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
|
||||||
p.parseString(buf.writer()) catch |e| {
|
|
||||||
// if error was expected, continue.
|
|
||||||
if (tc.err) continue;
|
|
||||||
|
|
||||||
std.debug.print("test case {s}\n", .{tc.s});
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
std.testing.expectEqualDeep(tc.exp, buf.items) catch |e| {
|
|
||||||
std.debug.print("test case {s} : {s}\n", .{ tc.s, buf.items });
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,769 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
pub const AttributeOP = enum {
|
|
||||||
eql, // =
|
|
||||||
not_eql, // !=
|
|
||||||
one_of, // ~=
|
|
||||||
prefix_hyphen, // |=
|
|
||||||
prefix, // ^=
|
|
||||||
suffix, // $=
|
|
||||||
contains, // *=
|
|
||||||
regexp, // #=
|
|
||||||
|
|
||||||
pub fn len(op: AttributeOP) u2 {
|
|
||||||
if (op == .eql) return 1;
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Combinator = enum {
|
|
||||||
empty,
|
|
||||||
descendant, // space
|
|
||||||
child, // >
|
|
||||||
next_sibling, // +
|
|
||||||
subsequent_sibling, // ~
|
|
||||||
|
|
||||||
pub const Error = error{
|
|
||||||
InvalidCombinator,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn parse(c: u8) Error!Combinator {
|
|
||||||
return switch (c) {
|
|
||||||
' ' => .descendant,
|
|
||||||
'>' => .child,
|
|
||||||
'+' => .next_sibling,
|
|
||||||
'~' => .subsequent_sibling,
|
|
||||||
else => Error.InvalidCombinator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const PseudoClass = enum {
|
|
||||||
not,
|
|
||||||
has,
|
|
||||||
haschild,
|
|
||||||
contains,
|
|
||||||
containsown,
|
|
||||||
matches,
|
|
||||||
matchesown,
|
|
||||||
nth_child,
|
|
||||||
nth_last_child,
|
|
||||||
nth_of_type,
|
|
||||||
nth_last_of_type,
|
|
||||||
first_child,
|
|
||||||
last_child,
|
|
||||||
first_of_type,
|
|
||||||
last_of_type,
|
|
||||||
only_child,
|
|
||||||
only_of_type,
|
|
||||||
input,
|
|
||||||
empty,
|
|
||||||
root,
|
|
||||||
link,
|
|
||||||
lang,
|
|
||||||
enabled,
|
|
||||||
disabled,
|
|
||||||
checked,
|
|
||||||
visited,
|
|
||||||
hover,
|
|
||||||
active,
|
|
||||||
focus,
|
|
||||||
target,
|
|
||||||
after,
|
|
||||||
backdrop,
|
|
||||||
before,
|
|
||||||
cue,
|
|
||||||
first_letter,
|
|
||||||
first_line,
|
|
||||||
grammar_error,
|
|
||||||
marker,
|
|
||||||
placeholder,
|
|
||||||
selection,
|
|
||||||
spelling_error,
|
|
||||||
modal,
|
|
||||||
|
|
||||||
pub const Error = error{
|
|
||||||
InvalidPseudoClass,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn isPseudoElement(pc: PseudoClass) bool {
|
|
||||||
return switch (pc) {
|
|
||||||
.after, .backdrop, .before, .cue, .first_letter => true,
|
|
||||||
.first_line, .grammar_error, .marker, .placeholder => true,
|
|
||||||
.selection, .spelling_error => true,
|
|
||||||
else => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
if (std.ascii.eqlIgnoreCase(s, "modal")) return .modal;
|
|
||||||
return Error.InvalidPseudoClass;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Selector = union(enum) {
|
|
||||||
pub const Error = error{
|
|
||||||
UnknownCombinedCombinator,
|
|
||||||
UnsupportedRelativePseudoClass,
|
|
||||||
UnsupportedContainsPseudoClass,
|
|
||||||
UnsupportedPseudoClass,
|
|
||||||
UnsupportedPseudoElement,
|
|
||||||
UnsupportedRegexpPseudoClass,
|
|
||||||
UnsupportedAttrRegexpOperator,
|
|
||||||
};
|
|
||||||
|
|
||||||
compound: struct {
|
|
||||||
selectors: []Selector,
|
|
||||||
pseudo_elt: ?PseudoClass,
|
|
||||||
},
|
|
||||||
group: []Selector,
|
|
||||||
tag: []const u8,
|
|
||||||
id: []const u8,
|
|
||||||
class: []const u8,
|
|
||||||
attribute: struct {
|
|
||||||
key: []const u8,
|
|
||||||
val: ?[]const u8 = null,
|
|
||||||
op: ?AttributeOP = null,
|
|
||||||
regexp: ?[]const u8 = null,
|
|
||||||
ci: bool = false,
|
|
||||||
},
|
|
||||||
combined: struct {
|
|
||||||
first: *Selector,
|
|
||||||
second: *Selector,
|
|
||||||
combinator: Combinator,
|
|
||||||
},
|
|
||||||
|
|
||||||
never_match: PseudoClass,
|
|
||||||
|
|
||||||
pseudo_class: PseudoClass,
|
|
||||||
pseudo_class_only_child: bool,
|
|
||||||
pseudo_class_lang: []const u8,
|
|
||||||
pseudo_class_relative: struct {
|
|
||||||
pseudo_class: PseudoClass,
|
|
||||||
match: *Selector,
|
|
||||||
},
|
|
||||||
pseudo_class_contains: struct {
|
|
||||||
own: bool,
|
|
||||||
val: []const u8,
|
|
||||||
},
|
|
||||||
pseudo_class_regexp: struct {
|
|
||||||
own: bool,
|
|
||||||
regexp: []const u8,
|
|
||||||
},
|
|
||||||
pseudo_class_nth: struct {
|
|
||||||
a: isize,
|
|
||||||
b: isize,
|
|
||||||
of_type: bool,
|
|
||||||
last: bool,
|
|
||||||
},
|
|
||||||
pseudo_element: PseudoClass,
|
|
||||||
|
|
||||||
// returns true if s is a whitespace-separated list that includes val.
|
|
||||||
fn word(haystack: []const u8, needle: []const u8, ci: bool) bool {
|
|
||||||
if (haystack.len == 0) return false;
|
|
||||||
var it = std.mem.splitAny(u8, haystack, " \t\r\n"); // TODO add \f
|
|
||||||
while (it.next()) |part| {
|
|
||||||
if (eql(part, needle, ci)) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eql(a: []const u8, b: []const u8, ci: bool) bool {
|
|
||||||
if (ci) return std.ascii.eqlIgnoreCase(a, b);
|
|
||||||
return std.mem.eql(u8, a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn starts(haystack: []const u8, needle: []const u8, ci: bool) bool {
|
|
||||||
if (ci) return std.ascii.startsWithIgnoreCase(haystack, needle);
|
|
||||||
return std.mem.startsWith(u8, haystack, needle);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ends(haystack: []const u8, needle: []const u8, ci: bool) bool {
|
|
||||||
if (ci) return std.ascii.endsWithIgnoreCase(haystack, needle);
|
|
||||||
return std.mem.endsWith(u8, haystack, needle);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn contains(haystack: []const u8, needle: []const u8, ci: bool) bool {
|
|
||||||
if (ci) return std.ascii.indexOfIgnoreCase(haystack, needle) != null;
|
|
||||||
return std.mem.indexOf(u8, haystack, needle) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// match returns true if the node matches the selector query.
|
|
||||||
pub fn match(s: 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),
|
|
||||||
.group => |v| {
|
|
||||||
for (v) |sel| {
|
|
||||||
if (try sel.match(n)) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
.compound => |v| {
|
|
||||||
if (v.selectors.len == 0) return n.isElement();
|
|
||||||
|
|
||||||
for (v.selectors) |sel| {
|
|
||||||
if (!try sel.match(n)) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.combined => |v| {
|
|
||||||
return switch (v.combinator) {
|
|
||||||
.empty => try v.first.match(n),
|
|
||||||
.descendant => {
|
|
||||||
if (!try v.second.match(n)) return false;
|
|
||||||
|
|
||||||
// The first must match a ascendent.
|
|
||||||
var p = try n.parent();
|
|
||||||
while (p != null) {
|
|
||||||
if (try v.first.match(p.?)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
p = try p.?.parent();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
.child => {
|
|
||||||
const p = try n.parent();
|
|
||||||
if (p == null) return false;
|
|
||||||
|
|
||||||
return try v.second.match(n) and try v.first.match(p.?);
|
|
||||||
},
|
|
||||||
.next_sibling => {
|
|
||||||
if (!try v.second.match(n)) return false;
|
|
||||||
var c = try n.prevSibling();
|
|
||||||
while (c != null) {
|
|
||||||
if (c.?.isText() or c.?.isComment()) {
|
|
||||||
c = try c.?.prevSibling();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return try v.first.match(c.?);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
.subsequent_sibling => {
|
|
||||||
if (!try v.second.match(n)) return false;
|
|
||||||
|
|
||||||
var c = try n.prevSibling();
|
|
||||||
while (c != null) {
|
|
||||||
if (try v.first.match(c.?)) return true;
|
|
||||||
c = try c.?.prevSibling();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.attribute => |v| {
|
|
||||||
var attr = try n.attr(v.key);
|
|
||||||
|
|
||||||
if (v.op == null) return attr != null;
|
|
||||||
if (v.val == null or v.val.?.len == 0) return false;
|
|
||||||
|
|
||||||
const val = v.val.?;
|
|
||||||
|
|
||||||
return switch (v.op.?) {
|
|
||||||
.eql => attr != null and eql(attr.?, val, v.ci),
|
|
||||||
.not_eql => attr == null or !eql(attr.?, val, v.ci),
|
|
||||||
.one_of => attr != null and word(attr.?, val, v.ci),
|
|
||||||
.prefix => {
|
|
||||||
if (attr == null) return false;
|
|
||||||
attr.? = std.mem.trim(u8, attr.?, &std.ascii.whitespace);
|
|
||||||
|
|
||||||
if (attr.?.len == 0) return false;
|
|
||||||
|
|
||||||
return starts(attr.?, val, v.ci);
|
|
||||||
},
|
|
||||||
.suffix => {
|
|
||||||
if (attr == null) return false;
|
|
||||||
attr.? = std.mem.trim(u8, attr.?, &std.ascii.whitespace);
|
|
||||||
|
|
||||||
if (attr.?.len == 0) return false;
|
|
||||||
|
|
||||||
return ends(attr.?, val, v.ci);
|
|
||||||
},
|
|
||||||
.contains => {
|
|
||||||
if (attr == null) return false;
|
|
||||||
attr.? = std.mem.trim(u8, attr.?, &std.ascii.whitespace);
|
|
||||||
|
|
||||||
if (attr.?.len == 0) return false;
|
|
||||||
|
|
||||||
return contains(attr.?, val, v.ci);
|
|
||||||
},
|
|
||||||
.prefix_hyphen => {
|
|
||||||
if (attr == null) return false;
|
|
||||||
if (eql(attr.?, val, v.ci)) return true;
|
|
||||||
|
|
||||||
if (attr.?.len <= val.len) return false;
|
|
||||||
|
|
||||||
if (!starts(attr.?, val, v.ci)) return false;
|
|
||||||
|
|
||||||
return attr.?[val.len] == '-';
|
|
||||||
},
|
|
||||||
.regexp => return Error.UnsupportedAttrRegexpOperator, // TODO handle regexp attribute operator.
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.never_match => return false,
|
|
||||||
.pseudo_class_relative => |v| {
|
|
||||||
if (!n.isElement()) return false;
|
|
||||||
|
|
||||||
return switch (v.pseudo_class) {
|
|
||||||
.not => !try v.match.match(n),
|
|
||||||
.has => try hasDescendantMatch(v.match, n),
|
|
||||||
.haschild => try hasChildMatch(v.match, n),
|
|
||||||
else => Error.UnsupportedRelativePseudoClass,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.pseudo_class_contains => return Error.UnsupportedContainsPseudoClass, // TODO, need mem allocation.
|
|
||||||
.pseudo_class_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation.
|
|
||||||
.pseudo_class_nth => |v| {
|
|
||||||
if (v.a == 0) {
|
|
||||||
if (v.last) {
|
|
||||||
return simpleNthLastChildMatch(v.b, v.of_type, n);
|
|
||||||
}
|
|
||||||
return simpleNthChildMatch(v.b, v.of_type, n);
|
|
||||||
}
|
|
||||||
return nthChildMatch(v.a, v.b, v.last, v.of_type, n);
|
|
||||||
},
|
|
||||||
.pseudo_class => |v| {
|
|
||||||
return switch (v) {
|
|
||||||
.input => {
|
|
||||||
if (!n.isElement()) return false;
|
|
||||||
const ntag = try n.tag();
|
|
||||||
|
|
||||||
return std.ascii.eqlIgnoreCase("input", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("select", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("button", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("textarea", ntag);
|
|
||||||
},
|
|
||||||
.empty => {
|
|
||||||
if (!n.isElement()) return false;
|
|
||||||
|
|
||||||
var c = try n.firstChild();
|
|
||||||
while (c != null) {
|
|
||||||
if (c.?.isElement()) return false;
|
|
||||||
|
|
||||||
if (c.?.isText()) {
|
|
||||||
if (try c.?.isEmptyText()) continue;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
c = try c.?.nextSibling();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
.root => {
|
|
||||||
if (!n.isElement()) return false;
|
|
||||||
|
|
||||||
const p = try n.parent();
|
|
||||||
return (p != null and p.?.isDocument());
|
|
||||||
},
|
|
||||||
.link => {
|
|
||||||
const ntag = try n.tag();
|
|
||||||
|
|
||||||
return std.ascii.eqlIgnoreCase("a", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("area", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("link", ntag);
|
|
||||||
},
|
|
||||||
.enabled => {
|
|
||||||
if (!n.isElement()) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase("a", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("area", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("link", ntag))
|
|
||||||
{
|
|
||||||
return try n.attr("href") != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase("optgroup", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("menuitem", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("fieldset", ntag))
|
|
||||||
{
|
|
||||||
return try n.attr("disabled") == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase("input", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("button", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("select", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("textarea", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("option", ntag))
|
|
||||||
{
|
|
||||||
return try n.attr("disabled") == null and
|
|
||||||
!try inDisabledFieldset(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
.disabled => {
|
|
||||||
if (!n.isElement()) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase("optgroup", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("menuitem", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("fieldset", ntag))
|
|
||||||
{
|
|
||||||
return try n.attr("disabled") != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase("input", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("button", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("select", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("textarea", ntag) or
|
|
||||||
std.ascii.eqlIgnoreCase("option", ntag))
|
|
||||||
{
|
|
||||||
return try n.attr("disabled") != null or
|
|
||||||
try inDisabledFieldset(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
.checked => {
|
|
||||||
if (!n.isElement()) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase("intput", ntag)) {
|
|
||||||
const ntype = try n.attr("type");
|
|
||||||
if (ntype == null) return false;
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, ntype.?, "checkbox") or
|
|
||||||
std.mem.eql(u8, ntype.?, "radio"))
|
|
||||||
{
|
|
||||||
return try n.attr("checked") != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (std.ascii.eqlIgnoreCase("option", ntag)) {
|
|
||||||
return try n.attr("selected") != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
.visited => return false,
|
|
||||||
.hover => return false,
|
|
||||||
.active => return false,
|
|
||||||
.focus => return false,
|
|
||||||
// TODO implement using the url fragment.
|
|
||||||
// see https://developer.mozilla.org/en-US/docs/Web/CSS/:target
|
|
||||||
.target => return false,
|
|
||||||
|
|
||||||
// all others pseudo class are handled by specialized
|
|
||||||
// pseudo_class_X selectors.
|
|
||||||
else => return Error.UnsupportedPseudoClass,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
.pseudo_class_only_child => |v| onlyChildMatch(v, n),
|
|
||||||
.pseudo_class_lang => |v| langMatch(v, n),
|
|
||||||
|
|
||||||
// pseudo elements doesn't make sense in the matching process.
|
|
||||||
// > A CSS pseudo-element is a keyword added to a selector that
|
|
||||||
// > lets you style a specific part of the selected element(s).
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
|
|
||||||
.pseudo_element => return Error.UnsupportedPseudoElement,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hasLegendInPreviousSiblings(n: anytype) anyerror!bool {
|
|
||||||
var c = try n.prevSibling();
|
|
||||||
while (c != null) {
|
|
||||||
const ctag = try c.?.tag();
|
|
||||||
if (std.ascii.eqlIgnoreCase("legend", ctag)) return true;
|
|
||||||
c = try c.?.prevSibling();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inDisabledFieldset(n: anytype) anyerror!bool {
|
|
||||||
const p = try n.parent();
|
|
||||||
if (p == null) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
|
||||||
const ptag = try p.?.tag();
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase("fieldset", ptag) and
|
|
||||||
try p.?.attr("disabled") != null and
|
|
||||||
(!std.ascii.eqlIgnoreCase("legend", ntag) or try hasLegendInPreviousSiblings(n)))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO should we handle legend like cascadia does?
|
|
||||||
// The implemention below looks suspicious, I didn't find a test case
|
|
||||||
// in cascadia and I didn't find the reference about legend in the
|
|
||||||
// specs. For now I do prefer ignoring this part.
|
|
||||||
//
|
|
||||||
// ```
|
|
||||||
// (n.DataAtom != atom.Legend || hasLegendInPreviousSiblings(n)) {
|
|
||||||
// ```
|
|
||||||
// https://github.com/andybalholm/cascadia/blob/master/pseudo_classes.go#L434
|
|
||||||
|
|
||||||
return try inDisabledFieldset(p.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn langMatch(lang: []const u8, n: anytype) anyerror!bool {
|
|
||||||
if (try n.attr("lang")) |own| {
|
|
||||||
if (std.mem.eql(u8, own, lang)) return true;
|
|
||||||
|
|
||||||
// check if the lang attr starts with lang+'-'
|
|
||||||
if (std.mem.startsWith(u8, own, lang)) {
|
|
||||||
if (own.len > lang.len and own[lang.len] == '-') return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the tag doesn't match, try the parent.
|
|
||||||
const p = try n.parent();
|
|
||||||
if (p == null) return false;
|
|
||||||
|
|
||||||
return langMatch(lang, p.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
// onlyChildMatch implements :only-child
|
|
||||||
// If `ofType` is true, it implements :only-of-type instead.
|
|
||||||
fn onlyChildMatch(of_type: bool, n: anytype) anyerror!bool {
|
|
||||||
if (!n.isElement()) return false;
|
|
||||||
|
|
||||||
const p = try n.parent();
|
|
||||||
if (p == null) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
|
||||||
|
|
||||||
var count: usize = 0;
|
|
||||||
var c = try p.?.firstChild();
|
|
||||||
// loop hover all n siblings.
|
|
||||||
while (c != null) {
|
|
||||||
// ignore non elements or others tags if of-type is true.
|
|
||||||
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
|
|
||||||
c = try c.?.nextSibling();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
count += 1;
|
|
||||||
if (count > 1) return false;
|
|
||||||
|
|
||||||
c = try c.?.nextSibling();
|
|
||||||
}
|
|
||||||
|
|
||||||
return count == 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleNthLastChildMatch implements :nth-last-child(b).
|
|
||||||
// If ofType is true, implements :nth-last-of-type instead.
|
|
||||||
fn simpleNthLastChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
|
|
||||||
if (!n.isElement()) return false;
|
|
||||||
|
|
||||||
const p = try n.parent();
|
|
||||||
if (p == null) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
|
||||||
|
|
||||||
var count: isize = 0;
|
|
||||||
var c = try p.?.lastChild();
|
|
||||||
// loop hover all n siblings.
|
|
||||||
while (c != null) {
|
|
||||||
// ignore non elements or others tags if of-type is true.
|
|
||||||
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
|
|
||||||
c = try c.?.prevSibling();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
count += 1;
|
|
||||||
|
|
||||||
if (n.eql(c.?)) return count == b;
|
|
||||||
if (count >= b) return false;
|
|
||||||
|
|
||||||
c = try c.?.prevSibling();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// simpleNthChildMatch implements :nth-child(b).
|
|
||||||
// If ofType is true, implements :nth-of-type instead.
|
|
||||||
fn simpleNthChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
|
|
||||||
if (!n.isElement()) return false;
|
|
||||||
|
|
||||||
const p = try n.parent();
|
|
||||||
if (p == null) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
|
||||||
|
|
||||||
var count: isize = 0;
|
|
||||||
var c = try p.?.firstChild();
|
|
||||||
// loop hover all n siblings.
|
|
||||||
while (c != null) {
|
|
||||||
// ignore non elements or others tags if of-type is true.
|
|
||||||
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
|
|
||||||
c = try c.?.nextSibling();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
count += 1;
|
|
||||||
|
|
||||||
if (n.eql(c.?)) return count == b;
|
|
||||||
if (count >= b) return false;
|
|
||||||
|
|
||||||
c = try c.?.nextSibling();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// nthChildMatch implements :nth-child(an+b).
|
|
||||||
// If last is true, implements :nth-last-child instead.
|
|
||||||
// If ofType is true, implements :nth-of-type instead.
|
|
||||||
fn nthChildMatch(a: isize, b: isize, last: bool, of_type: bool, n: anytype) anyerror!bool {
|
|
||||||
if (!n.isElement()) return false;
|
|
||||||
|
|
||||||
const p = try n.parent();
|
|
||||||
if (p == null) return false;
|
|
||||||
|
|
||||||
const ntag = try n.tag();
|
|
||||||
|
|
||||||
var i: isize = -1;
|
|
||||||
var count: isize = 0;
|
|
||||||
var c = try p.?.firstChild();
|
|
||||||
// loop hover all n siblings.
|
|
||||||
while (c != null) {
|
|
||||||
// ignore non elements or others tags if of-type is true.
|
|
||||||
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
|
|
||||||
c = try c.?.nextSibling();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
count += 1;
|
|
||||||
|
|
||||||
if (n.eql(c.?)) {
|
|
||||||
i = count;
|
|
||||||
if (!last) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
c = try c.?.nextSibling();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i == -1) return false;
|
|
||||||
|
|
||||||
if (last) i = count - i + 1;
|
|
||||||
|
|
||||||
i -= b;
|
|
||||||
if (a == 0) return i == 0;
|
|
||||||
return @mod(i, a) == 0 and @divTrunc(i, a) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hasDescendantMatch(s: *const Selector, n: anytype) anyerror!bool {
|
|
||||||
var c = try n.firstChild();
|
|
||||||
while (c != null) {
|
|
||||||
if (try s.match(c.?)) return true;
|
|
||||||
if (c.?.isElement() and try hasDescendantMatch(s, c.?)) return true;
|
|
||||||
c = try c.?.nextSibling();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hasChildMatch(s: *const Selector, n: anytype) anyerror!bool {
|
|
||||||
var c = try n.firstChild();
|
|
||||||
while (c != null) {
|
|
||||||
if (try s.match(c.?)) return true;
|
|
||||||
c = try c.?.nextSibling();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(sel: Selector, alloc: std.mem.Allocator) void {
|
|
||||||
switch (sel) {
|
|
||||||
.group => |v| {
|
|
||||||
for (v) |vv| vv.deinit(alloc);
|
|
||||||
alloc.free(v);
|
|
||||||
},
|
|
||||||
.compound => |v| {
|
|
||||||
for (v.selectors) |vv| vv.deinit(alloc);
|
|
||||||
alloc.free(v.selectors);
|
|
||||||
},
|
|
||||||
.tag, .id, .class, .pseudo_class_lang => |v| alloc.free(v),
|
|
||||||
.attribute => |att| {
|
|
||||||
alloc.free(att.key);
|
|
||||||
if (att.val) |v| alloc.free(v);
|
|
||||||
if (att.regexp) |v| alloc.free(v);
|
|
||||||
},
|
|
||||||
.combined => |c| {
|
|
||||||
c.first.deinit(alloc);
|
|
||||||
alloc.destroy(c.first);
|
|
||||||
c.second.deinit(alloc);
|
|
||||||
alloc.destroy(c.second);
|
|
||||||
},
|
|
||||||
.pseudo_class_relative => |v| {
|
|
||||||
v.match.deinit(alloc);
|
|
||||||
alloc.destroy(v.match);
|
|
||||||
},
|
|
||||||
.pseudo_class_contains => |v| alloc.free(v.val),
|
|
||||||
.pseudo_class_regexp => |v| alloc.free(v.regexp),
|
|
||||||
.pseudo_class, .pseudo_element, .never_match => {},
|
|
||||||
.pseudo_class_nth, .pseudo_class_only_child => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const CSSConstants = struct {
|
|
||||||
const IMPORTANT = "!important";
|
|
||||||
const URL_PREFIX = "url(";
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const CSSParserState = enum {
|
|
||||||
seek_name,
|
|
||||||
in_name,
|
|
||||||
seek_colon,
|
|
||||||
seek_value,
|
|
||||||
in_value,
|
|
||||||
in_quoted_value,
|
|
||||||
in_single_quoted_value,
|
|
||||||
in_url,
|
|
||||||
in_important,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const CSSDeclaration = struct {
|
|
||||||
name: []const u8,
|
|
||||||
value: []const u8,
|
|
||||||
is_important: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const CSSParser = struct {
|
|
||||||
state: CSSParserState,
|
|
||||||
name_start: usize,
|
|
||||||
name_end: usize,
|
|
||||||
value_start: usize,
|
|
||||||
position: usize,
|
|
||||||
paren_depth: usize,
|
|
||||||
escape_next: bool,
|
|
||||||
|
|
||||||
pub fn init() CSSParser {
|
|
||||||
return .{
|
|
||||||
.state = .seek_name,
|
|
||||||
.name_start = 0,
|
|
||||||
.name_end = 0,
|
|
||||||
.value_start = 0,
|
|
||||||
.position = 0,
|
|
||||||
.paren_depth = 0,
|
|
||||||
.escape_next = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parseDeclarations(arena: Allocator, text: []const u8) ![]CSSDeclaration {
|
|
||||||
var parser = init();
|
|
||||||
var declarations: std.ArrayListUnmanaged(CSSDeclaration) = .empty;
|
|
||||||
|
|
||||||
while (parser.position < text.len) {
|
|
||||||
const c = text[parser.position];
|
|
||||||
|
|
||||||
switch (parser.state) {
|
|
||||||
.seek_name => {
|
|
||||||
if (!std.ascii.isWhitespace(c)) {
|
|
||||||
parser.name_start = parser.position;
|
|
||||||
parser.state = .in_name;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.in_name => {
|
|
||||||
if (c == ':') {
|
|
||||||
parser.name_end = parser.position;
|
|
||||||
parser.state = .seek_value;
|
|
||||||
} else if (std.ascii.isWhitespace(c)) {
|
|
||||||
parser.name_end = parser.position;
|
|
||||||
parser.state = .seek_colon;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.seek_colon => {
|
|
||||||
if (c == ':') {
|
|
||||||
parser.state = .seek_value;
|
|
||||||
} else if (!std.ascii.isWhitespace(c)) {
|
|
||||||
parser.state = .seek_name;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.seek_value => {
|
|
||||||
if (!std.ascii.isWhitespace(c)) {
|
|
||||||
parser.value_start = parser.position;
|
|
||||||
if (c == '"') {
|
|
||||||
parser.state = .in_quoted_value;
|
|
||||||
} else if (c == '\'') {
|
|
||||||
parser.state = .in_single_quoted_value;
|
|
||||||
} else if (c == 'u' and parser.position + CSSConstants.URL_PREFIX.len <= text.len and std.mem.startsWith(u8, text[parser.position..], CSSConstants.URL_PREFIX)) {
|
|
||||||
parser.state = .in_url;
|
|
||||||
parser.paren_depth = 1;
|
|
||||||
parser.position += 3;
|
|
||||||
} else {
|
|
||||||
parser.state = .in_value;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.in_value => {
|
|
||||||
if (parser.escape_next) {
|
|
||||||
parser.escape_next = false;
|
|
||||||
} else if (c == '\\') {
|
|
||||||
parser.escape_next = true;
|
|
||||||
} else if (c == '(') {
|
|
||||||
parser.paren_depth += 1;
|
|
||||||
} else if (c == ')' and parser.paren_depth > 0) {
|
|
||||||
parser.paren_depth -= 1;
|
|
||||||
} else if (c == ';' and parser.paren_depth == 0) {
|
|
||||||
try parser.finishDeclaration(arena, &declarations, text);
|
|
||||||
parser.state = .seek_name;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.in_quoted_value => {
|
|
||||||
if (parser.escape_next) {
|
|
||||||
parser.escape_next = false;
|
|
||||||
} else if (c == '\\') {
|
|
||||||
parser.escape_next = true;
|
|
||||||
} else if (c == '"') {
|
|
||||||
parser.state = .in_value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.in_single_quoted_value => {
|
|
||||||
if (parser.escape_next) {
|
|
||||||
parser.escape_next = false;
|
|
||||||
} else if (c == '\\') {
|
|
||||||
parser.escape_next = true;
|
|
||||||
} else if (c == '\'') {
|
|
||||||
parser.state = .in_value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.in_url => {
|
|
||||||
if (parser.escape_next) {
|
|
||||||
parser.escape_next = false;
|
|
||||||
} else if (c == '\\') {
|
|
||||||
parser.escape_next = true;
|
|
||||||
} else if (c == '(') {
|
|
||||||
parser.paren_depth += 1;
|
|
||||||
} else if (c == ')') {
|
|
||||||
parser.paren_depth -= 1;
|
|
||||||
if (parser.paren_depth == 0) {
|
|
||||||
parser.state = .in_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.in_important => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
parser.position += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
try parser.finalize(arena, &declarations, text);
|
|
||||||
|
|
||||||
return declarations.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finishDeclaration(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
|
|
||||||
const name = std.mem.trim(u8, text[self.name_start..self.name_end], &std.ascii.whitespace);
|
|
||||||
if (name.len == 0) return;
|
|
||||||
|
|
||||||
const raw_value = text[self.value_start..self.position];
|
|
||||||
const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace);
|
|
||||||
|
|
||||||
var final_value = value;
|
|
||||||
var is_important = false;
|
|
||||||
|
|
||||||
if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT)) {
|
|
||||||
is_important = true;
|
|
||||||
final_value = std.mem.trimRight(u8, value[0 .. value.len - CSSConstants.IMPORTANT.len], &std.ascii.whitespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
try declarations.append(arena, .{
|
|
||||||
.name = name,
|
|
||||||
.value = final_value,
|
|
||||||
.is_important = is_important,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finalize(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
|
|
||||||
if (self.state != .in_value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return self.finishDeclaration(arena, declarations, text);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
|
|
||||||
test "CSSParser - Simple property" {
|
|
||||||
defer testing.reset();
|
|
||||||
|
|
||||||
const text = "color: red;";
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
|
||||||
|
|
||||||
try testing.expectEqual(1, declarations.len);
|
|
||||||
try testing.expectEqual("color", declarations[0].name);
|
|
||||||
try testing.expectEqual("red", declarations[0].value);
|
|
||||||
try testing.expectEqual(false, declarations[0].is_important);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSParser - Property with !important" {
|
|
||||||
defer testing.reset();
|
|
||||||
const text = "margin: 10px !important;";
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
|
||||||
|
|
||||||
try testing.expectEqual(1, declarations.len);
|
|
||||||
try testing.expectEqual("margin", declarations[0].name);
|
|
||||||
try testing.expectEqual("10px", declarations[0].value);
|
|
||||||
try testing.expectEqual(true, declarations[0].is_important);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSParser - Multiple properties" {
|
|
||||||
defer testing.reset();
|
|
||||||
const text = "color: red; font-size: 12px; margin: 5px !important;";
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
|
||||||
|
|
||||||
try testing.expect(declarations.len == 3);
|
|
||||||
|
|
||||||
try testing.expectEqual("color", declarations[0].name);
|
|
||||||
try testing.expectEqual("red", declarations[0].value);
|
|
||||||
try testing.expectEqual(false, declarations[0].is_important);
|
|
||||||
|
|
||||||
try testing.expectEqual("font-size", declarations[1].name);
|
|
||||||
try testing.expectEqual("12px", declarations[1].value);
|
|
||||||
try testing.expectEqual(false, declarations[1].is_important);
|
|
||||||
|
|
||||||
try testing.expectEqual("margin", declarations[2].name);
|
|
||||||
try testing.expectEqual("5px", declarations[2].value);
|
|
||||||
try testing.expectEqual(true, declarations[2].is_important);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSParser - Quoted value with semicolon" {
|
|
||||||
defer testing.reset();
|
|
||||||
const text = "content: \"Hello; world!\";";
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
|
||||||
|
|
||||||
try testing.expectEqual(1, declarations.len);
|
|
||||||
try testing.expectEqual("content", declarations[0].name);
|
|
||||||
try testing.expectEqual("\"Hello; world!\"", declarations[0].value);
|
|
||||||
try testing.expectEqual(false, declarations[0].is_important);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSParser - URL value" {
|
|
||||||
defer testing.reset();
|
|
||||||
const text = "background-image: url(\"test.png\");";
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
|
||||||
|
|
||||||
try testing.expectEqual(1, declarations.len);
|
|
||||||
try testing.expectEqual("background-image", declarations[0].name);
|
|
||||||
try testing.expectEqual("url(\"test.png\")", declarations[0].value);
|
|
||||||
try testing.expectEqual(false, declarations[0].is_important);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSParser - Whitespace handling" {
|
|
||||||
defer testing.reset();
|
|
||||||
const text = " color : purple ; margin : 10px ; ";
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
|
||||||
|
|
||||||
try testing.expectEqual(2, declarations.len);
|
|
||||||
try testing.expectEqual("color", declarations[0].name);
|
|
||||||
try testing.expectEqual("purple", declarations[0].value);
|
|
||||||
try testing.expectEqual("margin", declarations[1].name);
|
|
||||||
try testing.expectEqual("10px", declarations[1].value);
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const CSSStyleSheet = @import("css_stylesheet.zig").CSSStyleSheet;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
CSSRule,
|
|
||||||
CSSImportRule,
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
|
|
||||||
pub const CSSRule = struct {
|
|
||||||
css_text: []const u8,
|
|
||||||
parent_rule: ?*CSSRule = null,
|
|
||||||
parent_stylesheet: ?*CSSStyleSheet = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const CSSImportRule = struct {
|
|
||||||
pub const prototype = *CSSRule;
|
|
||||||
href: []const u8,
|
|
||||||
layer_name: ?[]const u8,
|
|
||||||
media: void,
|
|
||||||
style_sheet: CSSStyleSheet,
|
|
||||||
supports_text: ?[]const u8,
|
|
||||||
};
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const StyleSheet = @import("stylesheet.zig").StyleSheet;
|
|
||||||
const CSSRule = @import("css_rule.zig").CSSRule;
|
|
||||||
const CSSImportRule = @import("css_rule.zig").CSSImportRule;
|
|
||||||
|
|
||||||
pub const CSSRuleList = struct {
|
|
||||||
list: std.ArrayListUnmanaged([]const u8),
|
|
||||||
|
|
||||||
pub fn constructor() CSSRuleList {
|
|
||||||
return .{ .list = .empty };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _item(self: *CSSRuleList, _index: u32) ?CSSRule {
|
|
||||||
const index: usize = @intCast(_index);
|
|
||||||
|
|
||||||
if (index > self.list.items.len) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: for now, just return null.
|
|
||||||
// this depends on properly parsing CSSRule
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_length(self: *CSSRuleList) u32 {
|
|
||||||
return @intCast(self.list.items.len);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.CSS.CSSRuleList" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let list = new CSSRuleList()", "undefined" },
|
|
||||||
.{ "list instanceof CSSRuleList", "true" },
|
|
||||||
.{ "list.length", "0" },
|
|
||||||
.{ "list.item(0)", "null" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const CSSParser = @import("./css_parser.zig").CSSParser;
|
|
||||||
const CSSValueAnalyzer = @import("./css_value_analyzer.zig").CSSValueAnalyzer;
|
|
||||||
const CSSRule = @import("css_rule.zig").CSSRule;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
pub const CSSStyleDeclaration = struct {
|
|
||||||
store: std.StringHashMapUnmanaged(Property),
|
|
||||||
order: std.ArrayListUnmanaged([]const u8),
|
|
||||||
|
|
||||||
pub const empty: CSSStyleDeclaration = .{
|
|
||||||
.store = .empty,
|
|
||||||
.order = .empty,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Property = struct {
|
|
||||||
value: []const u8,
|
|
||||||
priority: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn get_cssFloat(self: *const CSSStyleDeclaration) []const u8 {
|
|
||||||
return self._getPropertyValue("float");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_cssFloat(self: *CSSStyleDeclaration, value: ?[]const u8, page: *Page) !void {
|
|
||||||
const final_value = value orelse "";
|
|
||||||
return self._setProperty("float", final_value, null, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 {
|
|
||||||
var buffer: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
const writer = buffer.writer(page.call_arena);
|
|
||||||
for (self.order.items) |name| {
|
|
||||||
const prop = self.store.get(name).?;
|
|
||||||
const escaped = try CSSValueAnalyzer.escapeCSSValue(page.call_arena, prop.value);
|
|
||||||
try writer.print("{s}: {s}", .{ name, escaped });
|
|
||||||
if (prop.priority) try writer.writeAll(" !important");
|
|
||||||
try writer.writeAll("; ");
|
|
||||||
}
|
|
||||||
return buffer.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Propagate also upward to parent node
|
|
||||||
pub fn set_cssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void {
|
|
||||||
self.store.clearRetainingCapacity();
|
|
||||||
self.order.clearRetainingCapacity();
|
|
||||||
|
|
||||||
// call_arena is safe here, because _setProperty will dupe the name
|
|
||||||
// using the page's longer-living arena.
|
|
||||||
const declarations = try CSSParser.parseDeclarations(page.call_arena, text);
|
|
||||||
|
|
||||||
for (declarations) |decl| {
|
|
||||||
if (!CSSValueAnalyzer.isValidPropertyName(decl.name)) continue;
|
|
||||||
const priority: ?[]const u8 = if (decl.is_important) "important" else null;
|
|
||||||
try self._setProperty(decl.name, decl.value, priority, page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_length(self: *const CSSStyleDeclaration) usize {
|
|
||||||
return self.order.items.len;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getPropertyPriority(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
|
||||||
return if (self.store.get(name)) |prop| (if (prop.priority) "important" else "") else "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO should handle properly shorthand properties and canonical forms
|
|
||||||
pub fn _getPropertyValue(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
|
||||||
if (self.store.get(name)) |prop| {
|
|
||||||
return prop.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// default to everything being visible (unless it's been explicitly set)
|
|
||||||
if (std.mem.eql(u8, name, "visibility")) {
|
|
||||||
return "visible";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _item(self: *const CSSStyleDeclaration, index: usize) []const u8 {
|
|
||||||
return if (index < self.order.items.len) self.order.items[index] else "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeProperty(self: *CSSStyleDeclaration, name: []const u8) ![]const u8 {
|
|
||||||
const prop = self.store.fetchRemove(name) orelse return "";
|
|
||||||
for (self.order.items, 0..) |item, i| {
|
|
||||||
if (std.mem.eql(u8, item, name)) {
|
|
||||||
_ = self.order.orderedRemove(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// safe to return, since it's in our page.arena
|
|
||||||
return prop.value.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setProperty(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, priority: ?[]const u8, page: *Page) !void {
|
|
||||||
const owned_value = try page.arena.dupe(u8, value);
|
|
||||||
const is_important = priority != null and std.ascii.eqlIgnoreCase(priority.?, "important");
|
|
||||||
|
|
||||||
const gop = try self.store.getOrPut(page.arena, name);
|
|
||||||
if (!gop.found_existing) {
|
|
||||||
const owned_name = try page.arena.dupe(u8, name);
|
|
||||||
gop.key_ptr.* = owned_name;
|
|
||||||
try self.order.append(page.arena, owned_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
gop.value_ptr.* = .{ .value = owned_value, .priority = is_important };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn named_get(self: *const CSSStyleDeclaration, name: []const u8, _: *bool) []const u8 {
|
|
||||||
return self._getPropertyValue(name);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
|
|
||||||
test "CSSOM.CSSStyleDeclaration" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let style = document.getElementById('content').style", "undefined" },
|
|
||||||
.{ "style.cssText = 'color: red; font-size: 12px; margin: 5px !important;'", "color: red; font-size: 12px; margin: 5px !important;" },
|
|
||||||
.{ "style.length", "3" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "style.getPropertyValue('color')", "red" },
|
|
||||||
.{ "style.getPropertyValue('font-size')", "12px" },
|
|
||||||
.{ "style.getPropertyValue('unknown-property')", "" },
|
|
||||||
|
|
||||||
.{ "style.getPropertyPriority('margin')", "important" },
|
|
||||||
.{ "style.getPropertyPriority('color')", "" },
|
|
||||||
.{ "style.getPropertyPriority('unknown-property')", "" },
|
|
||||||
|
|
||||||
.{ "style.item(0)", "color" },
|
|
||||||
.{ "style.item(1)", "font-size" },
|
|
||||||
.{ "style.item(2)", "margin" },
|
|
||||||
.{ "style.item(3)", "" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "style.setProperty('background-color', 'blue')", "undefined" },
|
|
||||||
.{ "style.getPropertyValue('background-color')", "blue" },
|
|
||||||
.{ "style.length", "4" },
|
|
||||||
|
|
||||||
.{ "style.setProperty('color', 'green')", "undefined" },
|
|
||||||
.{ "style.getPropertyValue('color')", "green" },
|
|
||||||
.{ "style.length", "4" },
|
|
||||||
.{ "style.color", "green" },
|
|
||||||
|
|
||||||
.{ "style.setProperty('padding', '10px', 'important')", "undefined" },
|
|
||||||
.{ "style.getPropertyValue('padding')", "10px" },
|
|
||||||
.{ "style.getPropertyPriority('padding')", "important" },
|
|
||||||
|
|
||||||
.{ "style.setProperty('border', '1px solid black', 'IMPORTANT')", "undefined" },
|
|
||||||
.{ "style.getPropertyPriority('border')", "important" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "style.removeProperty('color')", "green" },
|
|
||||||
.{ "style.getPropertyValue('color')", "" },
|
|
||||||
.{ "style.length", "5" },
|
|
||||||
|
|
||||||
.{ "style.removeProperty('unknown-property')", "" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "style.cssText.includes('font-size: 12px;')", "true" },
|
|
||||||
.{ "style.cssText.includes('margin: 5px !important;')", "true" },
|
|
||||||
.{ "style.cssText.includes('padding: 10px !important;')", "true" },
|
|
||||||
.{ "style.cssText.includes('border: 1px solid black !important;')", "true" },
|
|
||||||
|
|
||||||
.{ "style.cssText = 'color: purple; text-align: center;'", "color: purple; text-align: center;" },
|
|
||||||
.{ "style.length", "2" },
|
|
||||||
.{ "style.getPropertyValue('color')", "purple" },
|
|
||||||
.{ "style.getPropertyValue('text-align')", "center" },
|
|
||||||
.{ "style.getPropertyValue('font-size')", "" },
|
|
||||||
|
|
||||||
.{ "style.setProperty('cont', 'Hello; world!')", "undefined" },
|
|
||||||
.{ "style.getPropertyValue('cont')", "Hello; world!" },
|
|
||||||
|
|
||||||
.{ "style.cssText = 'content: \"Hello; world!\"; background-image: url(\"test.png\");'", "content: \"Hello; world!\"; background-image: url(\"test.png\");" },
|
|
||||||
.{ "style.getPropertyValue('content')", "\"Hello; world!\"" },
|
|
||||||
.{ "style.getPropertyValue('background-image')", "url(\"test.png\")" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "style.cssFloat", "" },
|
|
||||||
.{ "style.cssFloat = 'left'", "left" },
|
|
||||||
.{ "style.cssFloat", "left" },
|
|
||||||
.{ "style.getPropertyValue('float')", "left" },
|
|
||||||
|
|
||||||
.{ "style.cssFloat = 'right'", "right" },
|
|
||||||
.{ "style.cssFloat", "right" },
|
|
||||||
|
|
||||||
.{ "style.cssFloat = null", "null" },
|
|
||||||
.{ "style.cssFloat", "" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "style.setProperty('display', '')", "undefined" },
|
|
||||||
.{ "style.getPropertyValue('display')", "" },
|
|
||||||
|
|
||||||
.{ "style.cssText = ' color : purple ; margin : 10px ; '", " color : purple ; margin : 10px ; " },
|
|
||||||
.{ "style.getPropertyValue('color')", "purple" },
|
|
||||||
.{ "style.getPropertyValue('margin')", "10px" },
|
|
||||||
|
|
||||||
.{ "style.setProperty('border-bottom-left-radius', '5px')", "undefined" },
|
|
||||||
.{ "style.getPropertyValue('border-bottom-left-radius')", "5px" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "style.visibility", "visible" },
|
|
||||||
.{ "style.getPropertyValue('visibility')", "visible" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const StyleSheet = @import("stylesheet.zig").StyleSheet;
|
|
||||||
|
|
||||||
const CSSRuleList = @import("css_rule_list.zig").CSSRuleList;
|
|
||||||
const CSSImportRule = @import("css_rule.zig").CSSImportRule;
|
|
||||||
|
|
||||||
pub const CSSStyleSheet = struct {
|
|
||||||
pub const prototype = *StyleSheet;
|
|
||||||
|
|
||||||
proto: StyleSheet,
|
|
||||||
css_rules: CSSRuleList,
|
|
||||||
owner_rule: ?*CSSImportRule,
|
|
||||||
|
|
||||||
const CSSStyleSheetOpts = struct {
|
|
||||||
base_url: ?[]const u8 = null,
|
|
||||||
// TODO: Suupport media
|
|
||||||
disabled: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn constructor(_opts: ?CSSStyleSheetOpts) !CSSStyleSheet {
|
|
||||||
const opts = _opts orelse CSSStyleSheetOpts{};
|
|
||||||
return .{
|
|
||||||
.proto = StyleSheet{ .disabled = opts.disabled },
|
|
||||||
.css_rules = .constructor(),
|
|
||||||
.owner_rule = null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ownerRule(_: *CSSStyleSheet) ?*CSSImportRule {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cssRules(self: *CSSStyleSheet) *CSSRuleList {
|
|
||||||
return &self.css_rules;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _insertRule(self: *CSSStyleSheet, rule: []const u8, _index: ?usize, page: *Page) !usize {
|
|
||||||
const index = _index orelse 0;
|
|
||||||
if (index > self.css_rules.list.items.len) {
|
|
||||||
return error.IndexSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
const arena = page.arena;
|
|
||||||
try self.css_rules.list.insert(arena, index, try arena.dupe(u8, rule));
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
|
|
||||||
if (index > self.css_rules.list.items.len) {
|
|
||||||
return error.IndexSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = self.css_rules.list.orderedRemove(index);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.CSS.StyleSheet" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let css = new CSSStyleSheet()", "undefined" },
|
|
||||||
.{ "css instanceof CSSStyleSheet", "true" },
|
|
||||||
.{ "css.cssRules.length", "0" },
|
|
||||||
.{ "css.ownerRule", "null" },
|
|
||||||
.{ "let index1 = css.insertRule('body { color: red; }', 0)", "undefined" },
|
|
||||||
.{ "index1", "0" },
|
|
||||||
.{ "css.cssRules.length", "1" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,811 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
pub const CSSValueAnalyzer = struct {
|
|
||||||
pub fn isNumericWithUnit(value: []const u8) bool {
|
|
||||||
if (value.len == 0) return false;
|
|
||||||
|
|
||||||
if (!std.ascii.isDigit(value[0]) and
|
|
||||||
value[0] != '+' and value[0] != '-' and value[0] != '.')
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
var has_digit = false;
|
|
||||||
var decimal_point = false;
|
|
||||||
|
|
||||||
while (i < value.len) : (i += 1) {
|
|
||||||
const c = value[i];
|
|
||||||
if (std.ascii.isDigit(c)) {
|
|
||||||
has_digit = true;
|
|
||||||
} else if (c == '.' and !decimal_point) {
|
|
||||||
decimal_point = true;
|
|
||||||
} else if ((c == 'e' or c == 'E') and has_digit) {
|
|
||||||
if (i + 1 >= value.len) return false;
|
|
||||||
if (value[i + 1] != '+' and value[i + 1] != '-' and !std.ascii.isDigit(value[i + 1])) break;
|
|
||||||
i += 1;
|
|
||||||
if (value[i] == '+' or value[i] == '-') {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
var has_exp_digits = false;
|
|
||||||
while (i < value.len and std.ascii.isDigit(value[i])) : (i += 1) {
|
|
||||||
has_exp_digits = true;
|
|
||||||
}
|
|
||||||
if (!has_exp_digits) return false;
|
|
||||||
break;
|
|
||||||
} else if (c != '-' and c != '+') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!has_digit) return false;
|
|
||||||
|
|
||||||
if (i == value.len) return true;
|
|
||||||
|
|
||||||
const unit = value[i..];
|
|
||||||
return CSSKeywords.isValidUnit(unit);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isHexColor(value: []const u8) bool {
|
|
||||||
if (!std.mem.startsWith(u8, value, "#")) return false;
|
|
||||||
|
|
||||||
const hex_part = value[1..];
|
|
||||||
if (hex_part.len != 3 and hex_part.len != 6 and hex_part.len != 8) return false;
|
|
||||||
|
|
||||||
for (hex_part) |c| {
|
|
||||||
if (!std.ascii.isHex(c)) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isMultiValueProperty(value: []const u8) bool {
|
|
||||||
var parts = std.mem.splitAny(u8, value, " ");
|
|
||||||
var multi_value_parts: usize = 0;
|
|
||||||
var all_parts_valid = true;
|
|
||||||
|
|
||||||
while (parts.next()) |part| {
|
|
||||||
if (part.len == 0) continue;
|
|
||||||
multi_value_parts += 1;
|
|
||||||
|
|
||||||
const is_numeric = isNumericWithUnit(part);
|
|
||||||
const is_hex_color = isHexColor(part);
|
|
||||||
const is_known_keyword = CSSKeywords.isKnownKeyword(part);
|
|
||||||
const is_function = CSSKeywords.startsWithFunction(part);
|
|
||||||
|
|
||||||
if (!is_numeric and !is_hex_color and !is_known_keyword and !is_function) {
|
|
||||||
all_parts_valid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return multi_value_parts >= 2 and all_parts_valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isAlreadyQuoted(value: []const u8) bool {
|
|
||||||
return value.len >= 2 and ((value[0] == '"' and value[value.len - 1] == '"') or
|
|
||||||
(value[0] == '\'' and value[value.len - 1] == '\''));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isValidPropertyName(name: []const u8) bool {
|
|
||||||
if (name.len == 0) return false;
|
|
||||||
|
|
||||||
if (std.mem.startsWith(u8, name, "--")) {
|
|
||||||
if (name.len == 2) return false;
|
|
||||||
for (name[2..]) |c| {
|
|
||||||
if (!std.ascii.isAlphanumeric(c) and c != '-' and c != '_') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const first_char = name[0];
|
|
||||||
if (!std.ascii.isAlphabetic(first_char) and first_char != '-') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (first_char == '-') {
|
|
||||||
if (name.len < 2) return false;
|
|
||||||
|
|
||||||
if (!std.ascii.isAlphabetic(name[1])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (name[2..]) |c| {
|
|
||||||
if (!std.ascii.isAlphanumeric(c) and c != '-') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (name[1..]) |c| {
|
|
||||||
if (!std.ascii.isAlphanumeric(c) and c != '-') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extractImportant(value: []const u8) struct { value: []const u8, is_important: bool } {
|
|
||||||
const trimmed = std.mem.trim(u8, value, &std.ascii.whitespace);
|
|
||||||
|
|
||||||
if (std.mem.endsWith(u8, trimmed, "!important")) {
|
|
||||||
const clean_value = std.mem.trimRight(u8, trimmed[0 .. trimmed.len - 10], &std.ascii.whitespace);
|
|
||||||
return .{ .value = clean_value, .is_important = true };
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{ .value = trimmed, .is_important = false };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn needsQuotes(value: []const u8) bool {
|
|
||||||
if (value.len == 0) return true;
|
|
||||||
if (isAlreadyQuoted(value)) return false;
|
|
||||||
|
|
||||||
if (CSSKeywords.containsSpecialChar(value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.indexOfScalar(u8, value, ' ') == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const is_url = std.mem.startsWith(u8, value, "url(");
|
|
||||||
const is_function = CSSKeywords.startsWithFunction(value);
|
|
||||||
|
|
||||||
return !isMultiValueProperty(value) and
|
|
||||||
!is_url and
|
|
||||||
!is_function;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn escapeCSSValue(arena: std.mem.Allocator, value: []const u8) ![]const u8 {
|
|
||||||
if (!needsQuotes(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
var out: std.ArrayListUnmanaged(u8) = .empty;
|
|
||||||
|
|
||||||
// We'll need at least this much space, +2 for the quotes
|
|
||||||
try out.ensureTotalCapacity(arena, value.len + 2);
|
|
||||||
const writer = out.writer(arena);
|
|
||||||
|
|
||||||
try writer.writeByte('"');
|
|
||||||
|
|
||||||
for (value, 0..) |c, i| {
|
|
||||||
switch (c) {
|
|
||||||
'"' => try writer.writeAll("\\\""),
|
|
||||||
'\\' => try writer.writeAll("\\\\"),
|
|
||||||
'\n' => try writer.writeAll("\\A "),
|
|
||||||
'\r' => try writer.writeAll("\\D "),
|
|
||||||
'\t' => try writer.writeAll("\\9 "),
|
|
||||||
0...8, 11, 12, 14...31, 127 => {
|
|
||||||
try writer.print("\\{x}", .{c});
|
|
||||||
if (i + 1 < value.len and std.ascii.isHex(value[i + 1])) {
|
|
||||||
try writer.writeByte(' ');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => try writer.writeByte(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try writer.writeByte('"');
|
|
||||||
return out.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isKnownKeyword(value: []const u8) bool {
|
|
||||||
return CSSKeywords.isKnownKeyword(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn containsSpecialChar(value: []const u8) bool {
|
|
||||||
return CSSKeywords.containsSpecialChar(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const CSSKeywords = struct {
|
|
||||||
const border_styles = [_][]const u8{
|
|
||||||
"none", "solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset",
|
|
||||||
};
|
|
||||||
|
|
||||||
const color_names = [_][]const u8{
|
|
||||||
"black", "white", "red", "green", "blue", "yellow", "purple", "gray", "transparent",
|
|
||||||
"currentColor", "inherit",
|
|
||||||
};
|
|
||||||
|
|
||||||
const position_keywords = [_][]const u8{
|
|
||||||
"auto", "center", "left", "right", "top", "bottom",
|
|
||||||
};
|
|
||||||
|
|
||||||
const background_repeat = [_][]const u8{
|
|
||||||
"repeat", "no-repeat", "repeat-x", "repeat-y", "space", "round",
|
|
||||||
};
|
|
||||||
|
|
||||||
const font_styles = [_][]const u8{
|
|
||||||
"normal", "italic", "oblique", "bold", "bolder", "lighter",
|
|
||||||
};
|
|
||||||
|
|
||||||
const font_sizes = [_][]const u8{
|
|
||||||
"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large",
|
|
||||||
"smaller", "larger",
|
|
||||||
};
|
|
||||||
|
|
||||||
const font_families = [_][]const u8{
|
|
||||||
"serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
|
|
||||||
};
|
|
||||||
|
|
||||||
const css_global = [_][]const u8{
|
|
||||||
"initial", "inherit", "unset", "revert",
|
|
||||||
};
|
|
||||||
|
|
||||||
const display_values = [_][]const u8{
|
|
||||||
"block", "inline", "inline-block", "flex", "grid", "none",
|
|
||||||
};
|
|
||||||
|
|
||||||
const length_units = [_][]const u8{
|
|
||||||
"px", "em", "rem", "vw", "vh", "vmin", "vmax", "%", "pt", "pc", "in", "cm", "mm",
|
|
||||||
"ex", "ch", "fr",
|
|
||||||
};
|
|
||||||
|
|
||||||
const angle_units = [_][]const u8{
|
|
||||||
"deg", "rad", "grad", "turn",
|
|
||||||
};
|
|
||||||
|
|
||||||
const time_units = [_][]const u8{
|
|
||||||
"s", "ms",
|
|
||||||
};
|
|
||||||
|
|
||||||
const frequency_units = [_][]const u8{
|
|
||||||
"Hz", "kHz",
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolution_units = [_][]const u8{
|
|
||||||
"dpi", "dpcm", "dppx",
|
|
||||||
};
|
|
||||||
|
|
||||||
const special_chars = [_]u8{
|
|
||||||
'"', '\'', ';', '{', '}', '\\', '<', '>', '/', '\n', '\t', '\r', '\x00', '\x7F',
|
|
||||||
};
|
|
||||||
|
|
||||||
const functions = [_][]const u8{
|
|
||||||
"rgb(", "rgba(", "hsl(", "hsla(", "url(", "calc(", "var(", "attr(",
|
|
||||||
"linear-gradient(", "radial-gradient(", "conic-gradient(", "translate(", "rotate(", "scale(", "skew(", "matrix(",
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn isKnownKeyword(value: []const u8) bool {
|
|
||||||
const all_categories = [_][]const []const u8{
|
|
||||||
&border_styles, &color_names, &position_keywords, &background_repeat,
|
|
||||||
&font_styles, &font_sizes, &font_families, &css_global,
|
|
||||||
&display_values,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (all_categories) |category| {
|
|
||||||
for (category) |keyword| {
|
|
||||||
if (std.ascii.eqlIgnoreCase(value, keyword)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn containsSpecialChar(value: []const u8) bool {
|
|
||||||
for (value) |c| {
|
|
||||||
for (special_chars) |special| {
|
|
||||||
if (c == special) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isValidUnit(unit: []const u8) bool {
|
|
||||||
const all_units = [_][]const []const u8{
|
|
||||||
&length_units, &angle_units, &time_units, &frequency_units, &resolution_units,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (all_units) |category| {
|
|
||||||
for (category) |valid_unit| {
|
|
||||||
if (std.ascii.eqlIgnoreCase(unit, valid_unit)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn startsWithFunction(value: []const u8) bool {
|
|
||||||
const pos = std.mem.indexOfScalar(u8, value, '(') orelse return false;
|
|
||||||
if (pos == 0) return false;
|
|
||||||
|
|
||||||
if (std.mem.indexOfScalarPos(u8, value, pos, ')') == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const function_name = value[0..pos];
|
|
||||||
return isValidFunctionName(function_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isValidFunctionName(name: []const u8) bool {
|
|
||||||
if (name.len == 0) return false;
|
|
||||||
|
|
||||||
const first = name[0];
|
|
||||||
if (!std.ascii.isAlphabetic(first) and first != '_' and first != '-') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (name[1..]) |c| {
|
|
||||||
if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '-') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isNumericWithUnit - valid numbers with units" {
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("10px"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("3.14em"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-5rem"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("+12.5%"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0vh"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit(".5vw"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isNumericWithUnit - scientific notation" {
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e5px"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("2.5E-3em"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e+2rem"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-3.14e10px"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isNumericWithUnit - edge cases and invalid inputs" {
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(""));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("px"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("--px"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(".px"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1epx"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e+"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e+px"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1.2.3px"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10xyz"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("5invalid"));
|
|
||||||
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("10"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("3.14"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-5"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isHexColor - valid hex colors" {
|
|
||||||
try testing.expect(CSSValueAnalyzer.isHexColor("#000"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isHexColor("#fff"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isHexColor("#123456"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isHexColor("#abcdef"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isHexColor("#ABCDEF"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isHexColor("#12345678"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isHexColor - invalid hex colors" {
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor(""));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("000"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#00"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#0000"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#00000"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#0000000"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#000000000"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#gggggg"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#123xyz"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isMultiValueProperty - valid multi-value properties" {
|
|
||||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("solid red"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("#fff black"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("1em 2em 3em 4em"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("rgb(255,0,0) solid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isMultiValueProperty - invalid multi-value properties" {
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(""));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("invalid unknown"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px invalid"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(" "));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isAlreadyQuoted - various quoting scenarios" {
|
|
||||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"hello\""));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'world'"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"\""));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("''"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted(""));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\""));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello'"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'hello\""));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello\""));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isValidPropertyName - valid property names" {
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("color"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("background-color"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-webkit-transform"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("font-size"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("margin-top"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("z-index"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("line-height"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isValidPropertyName - invalid property names" {
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName(""));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("123color"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color!"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color space"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("@color"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color.test"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color_test"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: extractImportant - with and without !important" {
|
|
||||||
var result = CSSValueAnalyzer.extractImportant("red !important");
|
|
||||||
try testing.expect(result.is_important);
|
|
||||||
try testing.expectEqual("red", result.value);
|
|
||||||
|
|
||||||
result = CSSValueAnalyzer.extractImportant("blue");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("blue", result.value);
|
|
||||||
|
|
||||||
result = CSSValueAnalyzer.extractImportant(" green !important ");
|
|
||||||
try testing.expect(result.is_important);
|
|
||||||
try testing.expectEqual("green", result.value);
|
|
||||||
|
|
||||||
result = CSSValueAnalyzer.extractImportant("!important");
|
|
||||||
try testing.expect(result.is_important);
|
|
||||||
try testing.expectEqual("", result.value);
|
|
||||||
|
|
||||||
result = CSSValueAnalyzer.extractImportant("important");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("important", result.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: needsQuotes - various scenarios" {
|
|
||||||
try testing.expect(CSSValueAnalyzer.needsQuotes(""));
|
|
||||||
try testing.expect(CSSValueAnalyzer.needsQuotes("hello world"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.needsQuotes("test;"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.needsQuotes("a{b}"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.needsQuotes("test\"quote"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("\"already quoted\""));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("'already quoted'"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("url(image.png)"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0)"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("10px 20px"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("simple"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: escapeCSSValue - escaping various characters" {
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
var result = try CSSValueAnalyzer.escapeCSSValue(allocator, "simple");
|
|
||||||
try testing.expectEqual("simple", result);
|
|
||||||
|
|
||||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "\"already quoted\"");
|
|
||||||
try testing.expectEqual("\"already quoted\"", result);
|
|
||||||
|
|
||||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\"quote");
|
|
||||||
try testing.expectEqual("\"test\\\"quote\"", result);
|
|
||||||
|
|
||||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\nline");
|
|
||||||
try testing.expectEqual("\"test\\A line\"", result);
|
|
||||||
|
|
||||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\\back");
|
|
||||||
try testing.expectEqual("\"test\\\\back\"", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: CSSKeywords.isKnownKeyword - case sensitivity" {
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("red"));
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("solid"));
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("center"));
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("inherit"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("RED"));
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("Red"));
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("SOLID"));
|
|
||||||
try testing.expect(CSSKeywords.isKnownKeyword("Center"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSKeywords.isKnownKeyword("invalid"));
|
|
||||||
try testing.expect(!CSSKeywords.isKnownKeyword("unknown"));
|
|
||||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: CSSKeywords.containsSpecialChar - various special characters" {
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test\"quote"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test'quote"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test;end"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test{brace"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test}brace"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test\\back"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test<angle"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test>angle"));
|
|
||||||
try testing.expect(CSSKeywords.containsSpecialChar("test/slash"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSKeywords.containsSpecialChar("normal-text"));
|
|
||||||
try testing.expect(!CSSKeywords.containsSpecialChar("text123"));
|
|
||||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: CSSKeywords.isValidUnit - various units" {
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("px"));
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("em"));
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("rem"));
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("%"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("deg"));
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("rad"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("s"));
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("ms"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.isValidUnit("PX"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSKeywords.isValidUnit("invalid"));
|
|
||||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: CSSKeywords.startsWithFunction - function detection" {
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("rgb(255, 0, 0)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("rgba(255, 0, 0, 0.5)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("url(image.png)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("calc(100% - 20px)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("var(--custom-property)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("linear-gradient(to right, red, blue)"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("custom-function(args)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("unknown(test)"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction("not-a-function"));
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction("missing-paren)"));
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction("missing-close("));
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction("rgb"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isNumericWithUnit - whitespace handling" {
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(" 10px"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10 px"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10px "));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(" 10 px "));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: extractImportant - whitespace edge cases" {
|
|
||||||
var result = CSSValueAnalyzer.extractImportant(" ");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("", result.value);
|
|
||||||
|
|
||||||
result = CSSValueAnalyzer.extractImportant("\t\n\r !important\t\n");
|
|
||||||
try testing.expect(result.is_important);
|
|
||||||
try testing.expectEqual("", result.value);
|
|
||||||
|
|
||||||
result = CSSValueAnalyzer.extractImportant("red\t!important");
|
|
||||||
try testing.expect(result.is_important);
|
|
||||||
try testing.expectEqual("red", result.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isHexColor - mixed case handling" {
|
|
||||||
try testing.expect(CSSValueAnalyzer.isHexColor("#AbC"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isHexColor("#123aBc"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isHexColor("#FFffFF"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isHexColor("#000FFF"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: edge case - very long inputs" {
|
|
||||||
const long_valid = "a" ** 1000 ++ "px";
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(long_valid)); // not numeric
|
|
||||||
|
|
||||||
const long_property = "a-" ** 100 ++ "property";
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName(long_property));
|
|
||||||
|
|
||||||
const long_hex = "#" ++ "a" ** 20;
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor(long_hex));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: boundary conditions - numeric parsing" {
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0px"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.0px"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit(".0px"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.px"));
|
|
||||||
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("999999999px"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1.7976931348623157e+308px"));
|
|
||||||
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.000000001px"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e-100px"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: extractImportant - malformed important declarations" {
|
|
||||||
var result = CSSValueAnalyzer.extractImportant("red ! important");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("red ! important", result.value);
|
|
||||||
|
|
||||||
result = CSSValueAnalyzer.extractImportant("red !Important");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("red !Important", result.value);
|
|
||||||
|
|
||||||
result = CSSValueAnalyzer.extractImportant("red !IMPORTANT");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("red !IMPORTANT", result.value);
|
|
||||||
|
|
||||||
result = CSSValueAnalyzer.extractImportant("!importantred");
|
|
||||||
try testing.expect(!result.is_important);
|
|
||||||
try testing.expectEqual("!importantred", result.value);
|
|
||||||
|
|
||||||
result = CSSValueAnalyzer.extractImportant("red !important !important");
|
|
||||||
try testing.expect(result.is_important);
|
|
||||||
try testing.expectEqual("red !important", result.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isMultiValueProperty - complex spacing scenarios" {
|
|
||||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("solid red"));
|
|
||||||
|
|
||||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty(" 10px 20px "));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px\t20px"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px\n20px"));
|
|
||||||
|
|
||||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px 30px"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isAlreadyQuoted - edge cases with quotes" {
|
|
||||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"'hello'\""));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'\"hello\"'"));
|
|
||||||
|
|
||||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"hello\\\"world\""));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'hello\\'world'"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello\""));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'hello"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello'"));
|
|
||||||
|
|
||||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"a\""));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'b'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: needsQuotes - function and URL edge cases" {
|
|
||||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0)"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("calc(100% - 20px)"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("url(path with spaces.jpg)"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("linear-gradient(to right, red, blue)"));
|
|
||||||
|
|
||||||
try testing.expect(CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: escapeCSSValue - control characters and Unicode" {
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
var result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\ttab");
|
|
||||||
try testing.expectEqual("\"test\\9 tab\"", result);
|
|
||||||
|
|
||||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\rreturn");
|
|
||||||
try testing.expectEqual("\"test\\D return\"", result);
|
|
||||||
|
|
||||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\x00null");
|
|
||||||
try testing.expectEqual("\"test\\0null\"", result);
|
|
||||||
|
|
||||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\x7Fdel");
|
|
||||||
try testing.expectEqual("\"test\\7f del\"", result);
|
|
||||||
|
|
||||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\"quote\nline\\back");
|
|
||||||
try testing.expectEqual("\"test\\\"quote\\A line\\\\back\"", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isValidPropertyName - CSS custom properties and vendor prefixes" {
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("--custom-color"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("--my-variable"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("--123"));
|
|
||||||
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-webkit-transform"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-moz-border-radius"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-ms-filter"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-o-transition"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("-123invalid"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("--"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("-"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: startsWithFunction - case sensitivity and partial matches" {
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("RGB(255, 0, 0)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("Rgb(255, 0, 0)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("URL(image.png)"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("rg(something)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("ur(something)"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("rgb(1,2,3)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("rgba(1,2,3,4)"));
|
|
||||||
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("my-custom-function(args)"));
|
|
||||||
try testing.expect(CSSKeywords.startsWithFunction("function-with-dashes(test)"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction("123function(test)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: isHexColor - Unicode and invalid characters" {
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#ghijkl"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#12345g"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#xyz"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#АВС"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#1234567g"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#g2345678"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: complex integration scenarios" {
|
|
||||||
const allocator = testing.arena_allocator;
|
|
||||||
|
|
||||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("rgb(255,0,0) url(bg.jpg)"));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("calc(100% - 20px)"));
|
|
||||||
|
|
||||||
const result = try CSSValueAnalyzer.escapeCSSValue(allocator, "fake(function with spaces");
|
|
||||||
try testing.expectEqual("\"fake(function with spaces\"", result);
|
|
||||||
|
|
||||||
const important_result = CSSValueAnalyzer.extractImportant("rgb(255,0,0) !important");
|
|
||||||
try testing.expect(important_result.is_important);
|
|
||||||
try testing.expectEqual("rgb(255,0,0)", important_result.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "CSSValueAnalyzer: performance edge cases - empty and minimal inputs" {
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(""));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor(""));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(""));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted(""));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName(""));
|
|
||||||
try testing.expect(CSSValueAnalyzer.needsQuotes(""));
|
|
||||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
|
||||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
|
||||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
|
||||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
|
||||||
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("a"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isHexColor("a"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("a"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("a"));
|
|
||||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("a"));
|
|
||||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("a"));
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
pub const Stylesheet = @import("stylesheet.zig").StyleSheet;
|
|
||||||
pub const CSSStylesheet = @import("css_stylesheet.zig").CSSStyleSheet;
|
|
||||||
pub const CSSStyleDeclaration = @import("css_style_declaration.zig").CSSStyleDeclaration;
|
|
||||||
pub const CSSRuleList = @import("css_rule_list.zig").CSSRuleList;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
Stylesheet,
|
|
||||||
CSSStylesheet,
|
|
||||||
CSSStyleDeclaration,
|
|
||||||
CSSRuleList,
|
|
||||||
@import("css_rule.zig").Interfaces,
|
|
||||||
};
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/StyleSheet#specifications
|
|
||||||
pub const StyleSheet = struct {
|
|
||||||
disabled: bool = false,
|
|
||||||
href: []const u8 = "",
|
|
||||||
owner_node: ?*parser.Node = null,
|
|
||||||
parent_stylesheet: ?*StyleSheet = null,
|
|
||||||
title: []const u8 = "",
|
|
||||||
type: []const u8 = "text/css",
|
|
||||||
|
|
||||||
pub fn get_disabled(self: *const StyleSheet) bool {
|
|
||||||
return self.disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_href(self: *const StyleSheet) []const u8 {
|
|
||||||
return self.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: media
|
|
||||||
|
|
||||||
pub fn get_ownerNode(self: *const StyleSheet) ?*parser.Node {
|
|
||||||
return self.owner_node;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_parentStyleSheet(self: *const StyleSheet) ?*StyleSheet {
|
|
||||||
return self.parent_stylesheet;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_title(self: *const StyleSheet) []const u8 {
|
|
||||||
return self.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_type(self: *const StyleSheet) []const u8 {
|
|
||||||
return self.type;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
// Represents https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data
|
|
||||||
pub const DataURI = struct {
|
|
||||||
was_base64_encoded: bool,
|
|
||||||
// The contents in the uri. It will be base64 decoded but not prepared in
|
|
||||||
// any way for mime.charset.
|
|
||||||
data: []const u8,
|
|
||||||
|
|
||||||
// Parses data:[<media-type>][;base64],<data>
|
|
||||||
pub fn parse(allocator: Allocator, src: []const u8) !?DataURI {
|
|
||||||
if (!std.mem.startsWith(u8, src, "data:")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uri = src[5..];
|
|
||||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
|
||||||
|
|
||||||
// Extract the encoding.
|
|
||||||
var metadata = uri[0..data_starts];
|
|
||||||
var base64_encoded = false;
|
|
||||||
if (std.mem.endsWith(u8, metadata, ";base64")) {
|
|
||||||
base64_encoded = true;
|
|
||||||
metadata = metadata[0 .. metadata.len - 7];
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Extract mime type. This not trivial because Mime.parse requires
|
|
||||||
// a []u8 and might mutate the src. And, the DataURI.parse references atm
|
|
||||||
// do not have deinit calls.
|
|
||||||
|
|
||||||
// Prepare the data.
|
|
||||||
var data = uri[data_starts + 1 ..];
|
|
||||||
if (base64_encoded) {
|
|
||||||
const decoder = std.base64.standard.Decoder;
|
|
||||||
const decoded_size = try decoder.calcSizeForSlice(data);
|
|
||||||
|
|
||||||
const buffer = try allocator.alloc(u8, decoded_size);
|
|
||||||
errdefer allocator.free(buffer);
|
|
||||||
|
|
||||||
try decoder.decode(buffer, data);
|
|
||||||
data = buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.was_base64_encoded = base64_encoded,
|
|
||||||
.data = data,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *const DataURI, allocator: Allocator) void {
|
|
||||||
if (self.was_base64_encoded) {
|
|
||||||
allocator.free(self.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = std.testing;
|
|
||||||
test "DataURI: parse valid" {
|
|
||||||
try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo");
|
|
||||||
try test_valid("data:text/javascript; charset=utf-8;,foo", "foo");
|
|
||||||
try test_valid("data:,foo", "foo");
|
|
||||||
}
|
|
||||||
|
|
||||||
test "DataURI: parse invalid" {
|
|
||||||
try test_cannot_parse("atad:,foo");
|
|
||||||
try test_cannot_parse("data:foo");
|
|
||||||
try test_cannot_parse("data:");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_valid(uri: []const u8, expected: []const u8) !void {
|
|
||||||
const data_uri = try DataURI.parse(std.testing.allocator, uri) orelse return error.TestFailed;
|
|
||||||
defer data_uri.deinit(testing.allocator);
|
|
||||||
try testing.expectEqualStrings(expected, data_uri.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_cannot_parse(uri: []const u8) !void {
|
|
||||||
try testing.expectEqual(null, DataURI.parse(std.testing.allocator, uri));
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#attr
|
|
||||||
pub const Attr = struct {
|
|
||||||
pub const Self = parser.Attribute;
|
|
||||||
pub const prototype = *Node;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn get_namespaceURI(self: *parser.Attribute) !?[]const u8 {
|
|
||||||
return try parser.nodeGetNamespace(parser.attributeToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_prefix(self: *parser.Attribute) !?[]const u8 {
|
|
||||||
return try parser.nodeGetPrefix(parser.attributeToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_localName(self: *parser.Attribute) ![]const u8 {
|
|
||||||
return try parser.nodeLocalName(parser.attributeToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_name(self: *parser.Attribute) ![]const u8 {
|
|
||||||
return try parser.attributeGetName(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_value(self: *parser.Attribute) !?[]const u8 {
|
|
||||||
return try parser.attributeGetValue(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 {
|
|
||||||
if (try parser.attributeGetOwnerElement(self)) |el| {
|
|
||||||
// if possible, go through the element, as that triggers a
|
|
||||||
// DOMAttrModified event (which MutationObserver cares about)
|
|
||||||
const name = try parser.attributeGetName(self);
|
|
||||||
try parser.elementSetAttribute(el, name, v);
|
|
||||||
} else {
|
|
||||||
try parser.attributeSetValue(self, v);
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ownerElement(self: *parser.Attribute) !?*parser.Element {
|
|
||||||
return try parser.attributeGetOwnerElement(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_specified(_: *parser.Attribute) bool {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.Attribute" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let a = document.createAttributeNS('foo', 'bar')", "undefined" },
|
|
||||||
.{ "a.namespaceURI", "foo" },
|
|
||||||
.{ "a.prefix", "null" },
|
|
||||||
.{ "a.localName", "bar" },
|
|
||||||
.{ "a.name", "bar" },
|
|
||||||
.{ "a.value", "" },
|
|
||||||
// TODO: libdom has a bug here: the created attr has no parent, it
|
|
||||||
// causes a panic w/ libdom when setting the value.
|
|
||||||
//.{ "a.value = 'nok'", "nok" },
|
|
||||||
.{ "a.ownerElement", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let b = document.getElementById('link').getAttributeNode('class')", "undefined" },
|
|
||||||
.{ "b.name", "class" },
|
|
||||||
.{ "b.value", "ok" },
|
|
||||||
.{ "b.value = 'nok'", "nok" },
|
|
||||||
.{ "b.value", "nok" },
|
|
||||||
.{ "b.value = null", "null" },
|
|
||||||
.{ "b.value", "null" },
|
|
||||||
.{ "b.value = 'ok'", "ok" },
|
|
||||||
.{ "b.ownerElement.id", "link" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const Text = @import("text.zig").Text;
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#cdatasection
|
|
||||||
pub const CDATASection = struct {
|
|
||||||
pub const Self = parser.CDATASection;
|
|
||||||
pub const prototype = *Text;
|
|
||||||
pub const subtype = .node;
|
|
||||||
};
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
const Comment = @import("comment.zig").Comment;
|
|
||||||
const Text = @import("text.zig");
|
|
||||||
const ProcessingInstruction = @import("processing_instruction.zig").ProcessingInstruction;
|
|
||||||
const HTMLElem = @import("../html/elements.zig");
|
|
||||||
|
|
||||||
// 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) !?HTMLElem.Union {
|
|
||||||
const res = try parser.nodeNextElementSibling(parser.characterDataToNode(self));
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_previousElementSibling(self: *parser.CharacterData) !?HTMLElem.Union {
|
|
||||||
const res = try parser.nodePreviousElementSibling(parser.characterDataToNode(self));
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try HTMLElem.toInterface(HTMLElem.Union, 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(@alignCast(@ptrCast(self))) != try parser.nodeType(other_node)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const other: *parser.CharacterData = @ptrCast(other_node);
|
|
||||||
if (std.mem.eql(u8, try get_data(self), try get_data(other)) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _before(self: *parser.CharacterData, nodes: []const Node.NodeOrText) !void {
|
|
||||||
const ref_node = parser.characterDataToNode(self);
|
|
||||||
return Node.before(ref_node, nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _after(self: *parser.CharacterData, nodes: []const Node.NodeOrText) !void {
|
|
||||||
const ref_node = parser.characterDataToNode(self);
|
|
||||||
return Node.after(ref_node, nodes);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.CharacterData" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let link = document.getElementById('link')", "undefined" },
|
|
||||||
.{ "let cdata = link.firstChild", "undefined" },
|
|
||||||
.{ "cdata.data", "OK" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "cdata.data = 'OK modified'", "OK modified" },
|
|
||||||
.{ "cdata.data === 'OK modified'", "true" },
|
|
||||||
.{ "cdata.data = 'OK'", "OK" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "cdata.length === 2", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "cdata.nextElementSibling === null", "true" },
|
|
||||||
// create a next element
|
|
||||||
.{ "let next = document.createElement('a')", "undefined" },
|
|
||||||
.{ "link.appendChild(next, cdata) !== undefined", "true" },
|
|
||||||
.{ "cdata.nextElementSibling.localName === 'a' ", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "cdata.previousElementSibling === null", "true" },
|
|
||||||
// create a prev element
|
|
||||||
.{ "let prev = document.createElement('div')", "undefined" },
|
|
||||||
.{ "link.insertBefore(prev, cdata) !== undefined", "true" },
|
|
||||||
.{ "cdata.previousElementSibling.localName === 'div' ", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "cdata.appendData(' modified')", "undefined" },
|
|
||||||
.{ "cdata.data === 'OK modified' ", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "cdata.deleteData('OK'.length, ' modified'.length)", "undefined" },
|
|
||||||
.{ "cdata.data == 'OK'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "cdata.insertData('OK'.length-1, 'modified')", "undefined" },
|
|
||||||
.{ "cdata.data == 'OmodifiedK'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", "undefined" },
|
|
||||||
.{ "cdata.data == 'OreplacedK'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
|
|
||||||
.{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
|
|
||||||
.{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const CharacterData = @import("character_data.zig").CharacterData;
|
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#interface-comment
|
|
||||||
pub const Comment = struct {
|
|
||||||
pub const Self = parser.Comment;
|
|
||||||
pub const prototype = *CharacterData;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn constructor(data: ?[]const u8, page: *const Page) !*parser.Comment {
|
|
||||||
return parser.documentCreateComment(
|
|
||||||
parser.documentHTMLToDocument(page.window.document),
|
|
||||||
data orelse "",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.Comment" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let comment = new Comment('foo')", "undefined" },
|
|
||||||
.{ "comment.data", "foo" },
|
|
||||||
|
|
||||||
.{ "let emptycomment = new Comment()", "undefined" },
|
|
||||||
.{ "emptycomment.data", "" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const css = @import("../css/css.zig");
|
|
||||||
const Node = @import("../css/libdom.zig").Node;
|
|
||||||
const NodeList = @import("nodelist.zig").NodeList;
|
|
||||||
|
|
||||||
const MatchFirst = struct {
|
|
||||||
n: ?*parser.Node = null,
|
|
||||||
|
|
||||||
pub fn match(m: *MatchFirst, n: Node) !void {
|
|
||||||
m.n = n.node;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn querySelector(alloc: std.mem.Allocator, n: *parser.Node, selector: []const u8) !?*parser.Node {
|
|
||||||
const ps = try css.parse(alloc, selector, .{ .accept_pseudo_elts = true });
|
|
||||||
defer ps.deinit(alloc);
|
|
||||||
|
|
||||||
var m = MatchFirst{};
|
|
||||||
|
|
||||||
_ = try css.matchFirst(ps, Node{ .node = n }, &m);
|
|
||||||
return m.n;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MatchAll = struct {
|
|
||||||
alloc: std.mem.Allocator,
|
|
||||||
nl: NodeList,
|
|
||||||
|
|
||||||
fn init(alloc: std.mem.Allocator) MatchAll {
|
|
||||||
return .{
|
|
||||||
.alloc = alloc,
|
|
||||||
.nl = .{},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(m: *MatchAll) void {
|
|
||||||
m.nl.deinit(m.alloc);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(m: *MatchAll, n: Node) !void {
|
|
||||||
try m.nl.append(m.alloc, n.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toOwnedList(m: *MatchAll) NodeList {
|
|
||||||
// reset it.
|
|
||||||
defer m.nl = .{};
|
|
||||||
return m.nl;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn querySelectorAll(alloc: std.mem.Allocator, n: *parser.Node, selector: []const u8) !NodeList {
|
|
||||||
const ps = try css.parse(alloc, selector, .{ .accept_pseudo_elts = true });
|
|
||||||
defer ps.deinit(alloc);
|
|
||||||
|
|
||||||
var m = MatchAll.init(alloc);
|
|
||||||
defer m.deinit();
|
|
||||||
|
|
||||||
try css.matchAll(ps, Node{ .node = n }, &m);
|
|
||||||
return m.toOwnedList();
|
|
||||||
}
|
|
||||||
@@ -1,497 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const 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 Range = @import("range.zig").Range;
|
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
|
|
||||||
const DOMImplementation = @import("implementation.zig").DOMImplementation;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#document
|
|
||||||
pub const Document = struct {
|
|
||||||
pub const Self = parser.Document;
|
|
||||||
pub const prototype = *Node;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn constructor(page: *const Page) !*parser.DocumentHTML {
|
|
||||||
const doc = try parser.documentCreateDocument(
|
|
||||||
try parser.documentHTMLGetTitle(page.window.document),
|
|
||||||
);
|
|
||||||
|
|
||||||
// we have to work w/ document instead of html document.
|
|
||||||
const ddoc = parser.documentHTMLToDocument(doc);
|
|
||||||
const ccur = parser.documentHTMLToDocument(page.window.document);
|
|
||||||
try parser.documentSetDocumentURI(ddoc, try parser.documentGetDocumentURI(ccur));
|
|
||||||
try parser.documentSetInputEncoding(ddoc, try parser.documentGetInputEncoding(ccur));
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS funcs
|
|
||||||
// --------
|
|
||||||
pub fn get_implementation(_: *parser.Document) DOMImplementation {
|
|
||||||
return DOMImplementation{};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_documentElement(self: *parser.Document) !?ElementUnion {
|
|
||||||
const e = try parser.documentGetDocumentElement(self);
|
|
||||||
if (e == null) return null;
|
|
||||||
return try Element.toInterface(e.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_documentURI(self: *parser.Document) ![]const u8 {
|
|
||||||
return try parser.documentGetDocumentURI(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_URL(self: *parser.Document) ![]const u8 {
|
|
||||||
return try get_documentURI(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement contentType
|
|
||||||
pub fn get_contentType(self: *parser.Document) []const u8 {
|
|
||||||
_ = self;
|
|
||||||
return "text/html";
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement compactMode
|
|
||||||
pub fn get_compatMode(self: *parser.Document) []const u8 {
|
|
||||||
_ = self;
|
|
||||||
return "CSS1Compat";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_characterSet(self: *parser.Document) ![]const u8 {
|
|
||||||
return try parser.documentGetInputEncoding(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// alias of get_characterSet
|
|
||||||
pub fn get_charset(self: *parser.Document) ![]const u8 {
|
|
||||||
return try get_characterSet(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// alias of get_characterSet
|
|
||||||
pub fn get_inputEncoding(self: *parser.Document) ![]const u8 {
|
|
||||||
return try get_characterSet(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_doctype(self: *parser.Document) !?*parser.DocumentType {
|
|
||||||
return try parser.documentGetDoctype(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !*parser.Event {
|
|
||||||
// TODO: for now only "Event" constructor is supported
|
|
||||||
// see table on https://dom.spec.whatwg.org/#dom-document-createevent $2
|
|
||||||
if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) {
|
|
||||||
return try parser.eventCreate();
|
|
||||||
}
|
|
||||||
return parser.DOMError.NotSupported;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion {
|
|
||||||
const e = try parser.documentGetElementById(self, id) orelse return null;
|
|
||||||
return try Element.toInterface(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateElementResult = union(enum) {
|
|
||||||
element: ElementUnion,
|
|
||||||
custom: Env.JsObject,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !CreateElementResult {
|
|
||||||
const custom_element = page.window.custom_elements._get(tag_name) orelse {
|
|
||||||
const e = try parser.documentCreateElement(self, tag_name);
|
|
||||||
return .{ .element = try Element.toInterface(e) };
|
|
||||||
};
|
|
||||||
|
|
||||||
var result: Env.Function.Result = undefined;
|
|
||||||
const js_obj = custom_element.newInstance(&result) catch |err| {
|
|
||||||
log.fatal(.user_script, "newInstance error", .{
|
|
||||||
.err = result.exception,
|
|
||||||
.stack = result.stack,
|
|
||||||
.tag_name = tag_name,
|
|
||||||
.source = "createElement",
|
|
||||||
});
|
|
||||||
return err;
|
|
||||||
};
|
|
||||||
return .{ .custom = js_obj };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {
|
|
||||||
const e = try parser.documentCreateElementNS(self, ns, tag_name);
|
|
||||||
return try Element.toInterface(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can't simply use libdom dom_document_get_elements_by_tag_name here.
|
|
||||||
// Indeed, netsurf implemented a previous dom spec when
|
|
||||||
// getElementsByTagName returned a NodeList.
|
|
||||||
// But since
|
|
||||||
// https://github.com/whatwg/dom/commit/190700b7c12ecfd3b5ebdb359ab1d6ea9cbf7749
|
|
||||||
// the spec changed to return an HTMLCollection instead.
|
|
||||||
// That's why we reimplemented getElementsByTagName by using an
|
|
||||||
// HTMLCollection in zig here.
|
|
||||||
pub fn _getElementsByTagName(
|
|
||||||
self: *parser.Document,
|
|
||||||
tag_name: []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getElementsByClassName(
|
|
||||||
self: *parser.Document,
|
|
||||||
classNames: []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment {
|
|
||||||
return try parser.documentCreateDocumentFragment(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createTextNode(self: *parser.Document, data: []const u8) !*parser.Text {
|
|
||||||
return try parser.documentCreateTextNode(self, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createCDATASection(self: *parser.Document, data: []const u8) !*parser.CDATASection {
|
|
||||||
return try parser.documentCreateCDATASection(self, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createComment(self: *parser.Document, data: []const u8) !*parser.Comment {
|
|
||||||
return try parser.documentCreateComment(self, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createProcessingInstruction(self: *parser.Document, target: []const u8, data: []const u8) !*parser.ProcessingInstruction {
|
|
||||||
return try parser.documentCreateProcessingInstruction(self, target, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _importNode(self: *parser.Document, node: *parser.Node, deep: ?bool) !NodeUnion {
|
|
||||||
const n = try parser.documentImportNode(self, node, deep orelse false);
|
|
||||||
return try Node.toInterface(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _adoptNode(self: *parser.Document, node: *parser.Node) !NodeUnion {
|
|
||||||
const n = try parser.documentAdoptNode(self, node);
|
|
||||||
return try Node.toInterface(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createAttribute(self: *parser.Document, name: []const u8) !*parser.Attribute {
|
|
||||||
return try parser.documentCreateAttribute(self, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createAttributeNS(self: *parser.Document, ns: []const u8, qname: []const u8) !*parser.Attribute {
|
|
||||||
return try parser.documentCreateAttributeNS(self, ns, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParentNode
|
|
||||||
// https://dom.spec.whatwg.org/#parentnode
|
|
||||||
pub fn get_children(self: *parser.Document) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionChildren(parser.documentToNode(self), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_firstElementChild(self: *parser.Document) !?ElementUnion {
|
|
||||||
const elt = try parser.documentGetDocumentElement(self) orelse return null;
|
|
||||||
return try Element.toInterface(elt);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_lastElementChild(self: *parser.Document) !?ElementUnion {
|
|
||||||
const elt = try parser.documentGetDocumentElement(self) orelse return null;
|
|
||||||
return try Element.toInterface(elt);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_childElementCount(self: *parser.Document) !u32 {
|
|
||||||
_ = try parser.documentGetDocumentElement(self) orelse return 0;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _querySelector(self: *parser.Document, selector: []const u8, page: *Page) !?ElementUnion {
|
|
||||||
if (selector.len == 0) return null;
|
|
||||||
|
|
||||||
const n = try css.querySelector(page.call_arena, parser.documentToNode(self), selector);
|
|
||||||
|
|
||||||
if (n == null) return null;
|
|
||||||
|
|
||||||
return try Element.toInterface(parser.nodeToElement(n.?));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _querySelectorAll(self: *parser.Document, selector: []const u8, page: *Page) !NodeList {
|
|
||||||
return css.querySelectorAll(page.arena, parser.documentToNode(self), selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _prepend(self: *parser.Document, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.prepend(parser.documentToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _append(self: *parser.Document, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.append(parser.documentToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _replaceChildren(self: *parser.Document, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.replaceChildren(parser.documentToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?TreeWalker.TreeWalkerOpts) !TreeWalker {
|
|
||||||
return try TreeWalker.init(root, what_to_show, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
|
|
||||||
if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| {
|
|
||||||
if (state.active_element) |ae| {
|
|
||||||
return ae;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
|
||||||
return @alignCast(@ptrCast(body));
|
|
||||||
}
|
|
||||||
|
|
||||||
return try parser.documentGetDocumentElement(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
|
|
||||||
const ae = (try getActiveElement(self, page)) orelse return null;
|
|
||||||
return try Element.toInterface(ae);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: some elements can't be focused, like if they're disabled
|
|
||||||
// but there doesn't seem to be a generic way to check this. For example
|
|
||||||
// we could look for the "disabled" attribute, but that's only meaningful
|
|
||||||
// on certain types, and libdom's vtable doesn't seem to expose this.
|
|
||||||
pub fn setFocus(self: *parser.Document, e: *parser.ElementHTML, page: *Page) !void {
|
|
||||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
|
||||||
state.active_element = @ptrCast(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createRange(_: *parser.Document, page: *Page) Range {
|
|
||||||
return Range.constructor(page);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.Document" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{
|
|
||||||
.url = "about:blank",
|
|
||||||
});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.__proto__.__proto__.constructor.name", "Document" },
|
|
||||||
.{ "document.__proto__.__proto__.__proto__.constructor.name", "Node" },
|
|
||||||
.{ "document.__proto__.__proto__.__proto__.__proto__.constructor.name", "EventTarget" },
|
|
||||||
|
|
||||||
.{ "let newdoc = new Document()", "undefined" },
|
|
||||||
.{ "newdoc.documentElement", "null" },
|
|
||||||
.{ "newdoc.children.length", "0" },
|
|
||||||
.{ "newdoc.getElementsByTagName('*').length", "0" },
|
|
||||||
.{ "newdoc.getElementsByTagName('*').item(0)", "null" },
|
|
||||||
.{ "newdoc.inputEncoding === document.inputEncoding", "true" },
|
|
||||||
.{ "newdoc.documentURI === document.documentURI", "true" },
|
|
||||||
.{ "newdoc.URL === document.URL", "true" },
|
|
||||||
.{ "newdoc.compatMode === document.compatMode", "true" },
|
|
||||||
.{ "newdoc.characterSet === document.characterSet", "true" },
|
|
||||||
.{ "newdoc.charset === document.charset", "true" },
|
|
||||||
.{ "newdoc.contentType === document.contentType", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let getElementById = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "getElementById.constructor.name", "HTMLDivElement" },
|
|
||||||
.{ "getElementById.localName", "div" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
|
|
||||||
.{ "getElementsByTagName.length", "2" },
|
|
||||||
.{ "getElementsByTagName.item(0).localName", "p" },
|
|
||||||
.{ "getElementsByTagName.item(1).localName", "p" },
|
|
||||||
.{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
|
|
||||||
.{ "getElementsByTagNameAll.length", "8" },
|
|
||||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
|
||||||
.{ "getElementsByTagNameAll.item(7).localName", "p" },
|
|
||||||
.{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let ok = document.getElementsByClassName('ok')", "undefined" },
|
|
||||||
.{ "ok.length", "2" },
|
|
||||||
.{ "let empty = document.getElementsByClassName('empty')", "undefined" },
|
|
||||||
.{ "empty.length", "1" },
|
|
||||||
.{ "let emptyok = document.getElementsByClassName('empty ok')", "undefined" },
|
|
||||||
.{ "emptyok.length", "1" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let e = document.documentElement", "undefined" },
|
|
||||||
.{ "e.localName", "html" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.characterSet", "UTF-8" },
|
|
||||||
.{ "document.charset", "UTF-8" },
|
|
||||||
.{ "document.inputEncoding", "UTF-8" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.compatMode", "CSS1Compat" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.contentType", "text/html" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.documentURI", "about:blank" },
|
|
||||||
.{ "document.URL", "about:blank" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let impl = document.implementation", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let d = new Document()", "undefined" },
|
|
||||||
.{ "d.characterSet", "UTF-8" },
|
|
||||||
.{ "d.URL", "about:blank" },
|
|
||||||
.{ "d.documentURI", "about:blank" },
|
|
||||||
.{ "d.compatMode", "CSS1Compat" },
|
|
||||||
.{ "d.contentType", "text/html" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var v = document.createDocumentFragment()", "undefined" },
|
|
||||||
.{ "v.nodeName", "#document-fragment" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var v = document.createTextNode('foo')", "undefined" },
|
|
||||||
.{ "v.nodeName", "#text" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var v = document.createCDATASection('foo')", "undefined" },
|
|
||||||
.{ "v.nodeName", "#cdata-section" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var v = document.createComment('foo')", "undefined" },
|
|
||||||
.{ "v.nodeName", "#comment" },
|
|
||||||
.{ "let v2 = v.cloneNode()", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" },
|
|
||||||
.{ "pi.target", "foo" },
|
|
||||||
.{ "let pi2 = pi.cloneNode()", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let nimp = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "var v = document.importNode(nimp)", "undefined" },
|
|
||||||
.{ "v.nodeName", "DIV" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var v = document.createAttribute('foo')", "undefined" },
|
|
||||||
.{ "v.nodeName", "foo" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.children.length", "1" },
|
|
||||||
.{ "document.children.item(0).nodeName", "HTML" },
|
|
||||||
.{ "document.firstElementChild.nodeName", "HTML" },
|
|
||||||
.{ "document.lastElementChild.nodeName", "HTML" },
|
|
||||||
.{ "document.childElementCount", "1" },
|
|
||||||
|
|
||||||
.{ "let nd = new Document()", "undefined" },
|
|
||||||
.{ "nd.children.length", "0" },
|
|
||||||
.{ "nd.children.item(0)", "null" },
|
|
||||||
.{ "nd.firstElementChild", "null" },
|
|
||||||
.{ "nd.lastElementChild", "null" },
|
|
||||||
.{ "nd.childElementCount", "0" },
|
|
||||||
|
|
||||||
.{ "let emptydoc = document.createElement('html')", "undefined" },
|
|
||||||
.{ "emptydoc.prepend(document.createElement('html'))", "undefined" },
|
|
||||||
|
|
||||||
.{ "let emptydoc2 = document.createElement('html')", "undefined" },
|
|
||||||
.{ "emptydoc2.append(document.createElement('html'))", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.querySelector('')", "null" },
|
|
||||||
.{ "document.querySelector('*').nodeName", "HTML" },
|
|
||||||
.{ "document.querySelector('#content').id", "content" },
|
|
||||||
.{ "document.querySelector('#para').id", "para" },
|
|
||||||
.{ "document.querySelector('.ok').id", "link" },
|
|
||||||
.{ "document.querySelector('a ~ p').id", "para-empty" },
|
|
||||||
.{ "document.querySelector(':root').nodeName", "HTML" },
|
|
||||||
|
|
||||||
.{ "document.querySelectorAll('p').length", "2" },
|
|
||||||
.{
|
|
||||||
\\ Array.from(document.querySelectorAll('#content > p#para-empty'))
|
|
||||||
\\ .map(row => row.querySelector('span').textContent)
|
|
||||||
\\ .length;
|
|
||||||
,
|
|
||||||
"1",
|
|
||||||
},
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.activeElement === document.body", "true" },
|
|
||||||
.{ "document.getElementById('link').focus()", "undefined" },
|
|
||||||
.{ "document.activeElement === document.getElementById('link')", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// this test breaks the doc structure, keep it at the end of the test
|
|
||||||
// suite.
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let nadop = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "var v = document.adoptNode(nadop)", "undefined" },
|
|
||||||
.{ "v.nodeName", "DIV" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
const Case = testing.JsRunner.Case;
|
|
||||||
const tags = comptime parser.Tag.all();
|
|
||||||
var createElements: [(tags.len) * 2]Case = undefined;
|
|
||||||
inline for (tags, 0..) |tag, i| {
|
|
||||||
const tag_name = @tagName(tag);
|
|
||||||
createElements[i * 2] = Case{
|
|
||||||
"var " ++ tag_name ++ "Elem = document.createElement('" ++ tag_name ++ "')",
|
|
||||||
"undefined",
|
|
||||||
};
|
|
||||||
createElements[(i * 2) + 1] = Case{
|
|
||||||
tag_name ++ "Elem.localName",
|
|
||||||
tag_name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
try runner.testCases(&createElements, .{});
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const 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 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.DocumentFragment" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const dc = new DocumentFragment()", "undefined" },
|
|
||||||
.{ "dc.constructor.name", "DocumentFragment" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const dc1 = new DocumentFragment()", "undefined" },
|
|
||||||
.{ "const dc2 = new DocumentFragment()", "undefined" },
|
|
||||||
.{ "dc1.isEqualNode(dc1)", "true" },
|
|
||||||
.{ "dc1.isEqualNode(dc2)", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let f = document.createDocumentFragment()", null },
|
|
||||||
.{ "let d = document.createElement('div');", null },
|
|
||||||
.{ "d.id = 'x';", null },
|
|
||||||
.{ "document.getElementById('x') == null;", "true" },
|
|
||||||
|
|
||||||
.{ "f.append(d);", null },
|
|
||||||
.{ "document.getElementById('x') == null;", "true" },
|
|
||||||
|
|
||||||
.{ "document.getElementsByTagName('body')[0].append(f.cloneNode(true));", null },
|
|
||||||
.{ "document.getElementById('x') != null;", "true" },
|
|
||||||
|
|
||||||
.{ "document.querySelector('.hello')", "null" },
|
|
||||||
.{ "document.querySelectorAll('.hello').length", "0" },
|
|
||||||
|
|
||||||
.{ "document.querySelector('#x').id", "x" },
|
|
||||||
.{ "document.querySelectorAll('#x')[0].id", "x" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#documenttype
|
|
||||||
pub const DocumentType = struct {
|
|
||||||
pub const Self = parser.DocumentType;
|
|
||||||
pub const prototype = *Node;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn get_name(self: *parser.DocumentType) ![]const u8 {
|
|
||||||
return try parser.documentTypeGetName(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_publicId(self: *parser.DocumentType) ![]const u8 {
|
|
||||||
return try parser.documentTypeGetPublicId(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_systemId(self: *parser.DocumentType) ![]const u8 {
|
|
||||||
return try parser.documentTypeGetSystemId(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// netsurf's DocumentType doesn't implement the dom_node_get_attributes
|
|
||||||
// and thus will crash if we try to call nodeIsEqualNode.
|
|
||||||
pub fn _isEqualNode(self: *parser.DocumentType, other_node: *parser.Node) !bool {
|
|
||||||
if (try parser.nodeType(other_node) != .document_type) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const other: *parser.DocumentType = @ptrCast(other_node);
|
|
||||||
if (std.mem.eql(u8, try get_name(self), try get_name(other)) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, try get_publicId(self), try get_publicId(other)) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, try get_systemId(self), try get_systemId(other)) == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.DocumentType" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let dt1 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');", "undefined" },
|
|
||||||
.{ "let dt2 = document.implementation.createDocumentType('qname2', 'pid2', 'sys2');", "undefined" },
|
|
||||||
.{ "let dt3 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');", "undefined" },
|
|
||||||
.{ "dt1.isEqualNode(dt1)", "true" },
|
|
||||||
.{ "dt1.isEqualNode(dt3)", "true" },
|
|
||||||
.{ "dt1.isEqualNode(dt2)", "false" },
|
|
||||||
.{ "dt2.isEqualNode(dt3)", "false" },
|
|
||||||
.{ "dt1.isEqualNode(document)", "false" },
|
|
||||||
.{ "document.isEqualNode(dt1)", "false" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
const EventTarget = @import("event_target.zig").EventTarget;
|
|
||||||
const DOMImplementation = @import("implementation.zig").DOMImplementation;
|
|
||||||
const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
|
|
||||||
const DOMTokenList = @import("token_list.zig");
|
|
||||||
const NodeList = @import("nodelist.zig");
|
|
||||||
const Node = @import("node.zig");
|
|
||||||
const ResizeObserver = @import("resize_observer.zig");
|
|
||||||
const MutationObserver = @import("mutation_observer.zig");
|
|
||||||
const IntersectionObserver = @import("intersection_observer.zig");
|
|
||||||
const DOMParser = @import("dom_parser.zig").DOMParser;
|
|
||||||
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
|
||||||
const NodeFilter = @import("node_filter.zig").NodeFilter;
|
|
||||||
const PerformanceObserver = @import("performance_observer.zig").PerformanceObserver;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
DOMException,
|
|
||||||
EventTarget,
|
|
||||||
DOMImplementation,
|
|
||||||
NamedNodeMap,
|
|
||||||
NamedNodeMap.Iterator,
|
|
||||||
DOMTokenList.Interfaces,
|
|
||||||
NodeList.Interfaces,
|
|
||||||
Node.Node,
|
|
||||||
Node.Interfaces,
|
|
||||||
ResizeObserver.Interfaces,
|
|
||||||
MutationObserver.Interfaces,
|
|
||||||
IntersectionObserver.Interfaces,
|
|
||||||
DOMParser,
|
|
||||||
TreeWalker,
|
|
||||||
NodeFilter,
|
|
||||||
@import("performance.zig").Interfaces,
|
|
||||||
PerformanceObserver,
|
|
||||||
@import("range.zig").Interfaces,
|
|
||||||
};
|
|
||||||
@@ -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 std = @import("std");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
|
|
||||||
pub const DOMParser = struct {
|
|
||||||
pub fn constructor() !DOMParser {
|
|
||||||
return .{};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _parseFromString(_: *DOMParser, string: []const u8, mime_type: []const u8) !*parser.DocumentHTML {
|
|
||||||
if (!std.mem.eql(u8, mime_type, "text/html")) {
|
|
||||||
// TODO: Support XML
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
|
|
||||||
return try parser.documentHTMLParseFromStr(string);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.DOMParser" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const dp = new DOMParser()", "undefined" },
|
|
||||||
.{ "dp.parseFromString('<div>abc</div>', 'text/html')", "[object HTMLDocument]" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,682 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const 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");
|
|
||||||
pub const Union = @import("../html/elements.zig").Union;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#element
|
|
||||||
pub const Element = struct {
|
|
||||||
pub const Self = parser.Element;
|
|
||||||
pub const prototype = *Node;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub const DOMRect = struct {
|
|
||||||
x: f64,
|
|
||||||
y: f64,
|
|
||||||
width: f64,
|
|
||||||
height: f64,
|
|
||||||
bottom: f64,
|
|
||||||
right: f64,
|
|
||||||
top: f64,
|
|
||||||
left: f64,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn toInterface(e: *parser.Element) !Union {
|
|
||||||
return try HTMLElem.toInterface(Union, e);
|
|
||||||
// SVGElement and MathML are not supported yet.
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS funcs
|
|
||||||
// --------
|
|
||||||
|
|
||||||
pub fn get_namespaceURI(self: *parser.Element) !?[]const u8 {
|
|
||||||
return try parser.nodeGetNamespace(parser.elementToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_prefix(self: *parser.Element) !?[]const u8 {
|
|
||||||
return try parser.nodeGetPrefix(parser.elementToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_localName(self: *parser.Element) ![]const u8 {
|
|
||||||
return try parser.nodeLocalName(parser.elementToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_tagName(self: *parser.Element) ![]const u8 {
|
|
||||||
return try parser.nodeName(parser.elementToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_id(self: *parser.Element) ![]const u8 {
|
|
||||||
return try parser.elementGetAttribute(self, "id") orelse "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_id(self: *parser.Element, id: []const u8) !void {
|
|
||||||
return try parser.elementSetAttribute(self, "id", id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_className(self: *parser.Element) ![]const u8 {
|
|
||||||
return try parser.elementGetAttribute(self, "class") orelse "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_className(self: *parser.Element, class: []const u8) !void {
|
|
||||||
return try parser.elementSetAttribute(self, "class", class);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_slot(self: *parser.Element) ![]const u8 {
|
|
||||||
return try parser.elementGetAttribute(self, "slot") orelse "";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_slot(self: *parser.Element, slot: []const u8) !void {
|
|
||||||
return try parser.elementSetAttribute(self, "slot", slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_classList(self: *parser.Element) !*parser.TokenList {
|
|
||||||
return try parser.tokenListCreate(self, "class");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_attributes(self: *parser.Element) !*parser.NamedNodeMap {
|
|
||||||
// An element must have non-nil attributes.
|
|
||||||
return try parser.nodeGetAttributes(parser.elementToNode(self)) orelse unreachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_innerHTML(self: *parser.Element, page: *Page) ![]const u8 {
|
|
||||||
var buf = std.ArrayList(u8).init(page.arena);
|
|
||||||
try dump.writeChildren(parser.elementToNode(self), buf.writer());
|
|
||||||
return buf.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_outerHTML(self: *parser.Element, page: *Page) ![]const u8 {
|
|
||||||
var buf = std.ArrayList(u8).init(page.arena);
|
|
||||||
try dump.writeNode(parser.elementToNode(self), buf.writer());
|
|
||||||
return buf.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_innerHTML(self: *parser.Element, str: []const u8) !void {
|
|
||||||
const node = parser.elementToNode(self);
|
|
||||||
const doc = try parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
|
|
||||||
// parse the fragment
|
|
||||||
const fragment = try parser.documentParseFragmentFromStr(doc, str);
|
|
||||||
|
|
||||||
// remove existing children
|
|
||||||
try Node.removeChildren(node);
|
|
||||||
|
|
||||||
// get fragment body children
|
|
||||||
const children = try parser.documentFragmentBodyChildren(fragment) orelse return;
|
|
||||||
|
|
||||||
// append children to the node
|
|
||||||
const ln = try parser.nodeListLength(children);
|
|
||||||
for (0..ln) |_| {
|
|
||||||
// always index 0, because ndoeAppendChild 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _hasAttributes(self: *parser.Element) !bool {
|
|
||||||
return try parser.nodeHasAttributes(parser.elementToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getAttribute(self: *parser.Element, qname: []const u8) !?[]const u8 {
|
|
||||||
return try parser.elementGetAttribute(self, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8) !?[]const u8 {
|
|
||||||
return try parser.elementGetAttributeNS(self, ns, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setAttribute(self: *parser.Element, qname: []const u8, value: []const u8) !void {
|
|
||||||
return try parser.elementSetAttribute(self, qname, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8, value: []const u8) !void {
|
|
||||||
return try parser.elementSetAttributeNS(self, ns, qname, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeAttribute(self: *parser.Element, qname: []const u8) !void {
|
|
||||||
return try parser.elementRemoveAttribute(self, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8) !void {
|
|
||||||
return try parser.elementRemoveAttributeNS(self, ns, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _hasAttribute(self: *parser.Element, qname: []const u8) !bool {
|
|
||||||
return try parser.elementHasAttribute(self, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _hasAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8) !bool {
|
|
||||||
return try parser.elementHasAttributeNS(self, ns, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#dom-element-toggleattribute
|
|
||||||
pub fn _toggleAttribute(self: *parser.Element, qname: []u8, force: ?bool) !bool {
|
|
||||||
_ = std.ascii.lowerString(qname, qname);
|
|
||||||
const exists = try parser.elementHasAttribute(self, qname);
|
|
||||||
|
|
||||||
// If attribute is null, then:
|
|
||||||
if (!exists) {
|
|
||||||
// If force is not given or is true, create an attribute whose
|
|
||||||
// local name is qualifiedName, value is the empty string and node
|
|
||||||
// document is this’s node document, then append this attribute to
|
|
||||||
// this, and then return true.
|
|
||||||
if (force == null or force.?) {
|
|
||||||
try parser.elementSetAttribute(self, qname, "");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (try parser.validateName(qname) == false) {
|
|
||||||
return parser.DOMError.InvalidCharacter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return false.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, if force is not given or is false, remove an attribute
|
|
||||||
// given qualifiedName and this, and then return false.
|
|
||||||
if (force == null or !force.?) {
|
|
||||||
try parser.elementRemoveAttribute(self, qname);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return true.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getAttributeNode(self: *parser.Element, name: []const u8) !?*parser.Attribute {
|
|
||||||
return try parser.elementGetAttributeNode(self, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getAttributeNodeNS(self: *parser.Element, ns: []const u8, name: []const u8) !?*parser.Attribute {
|
|
||||||
return try parser.elementGetAttributeNodeNS(self, ns, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setAttributeNode(self: *parser.Element, attr: *parser.Attribute) !?*parser.Attribute {
|
|
||||||
return try parser.elementSetAttributeNode(self, attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setAttributeNodeNS(self: *parser.Element, attr: *parser.Attribute) !?*parser.Attribute {
|
|
||||||
return try parser.elementSetAttributeNodeNS(self, attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeAttributeNode(self: *parser.Element, attr: *parser.Attribute) !*parser.Attribute {
|
|
||||||
return try parser.elementRemoveAttributeNode(self, attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getElementsByTagName(
|
|
||||||
self: *parser.Element,
|
|
||||||
tag_name: []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByTagName(
|
|
||||||
page.arena,
|
|
||||||
parser.elementToNode(self),
|
|
||||||
tag_name,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getElementsByClassName(
|
|
||||||
self: *parser.Element,
|
|
||||||
classNames: []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByClassName(
|
|
||||||
page.arena,
|
|
||||||
parser.elementToNode(self),
|
|
||||||
classNames,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParentNode
|
|
||||||
// https://dom.spec.whatwg.org/#parentnode
|
|
||||||
pub fn get_children(self: *parser.Element) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionChildren(parser.elementToNode(self), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_firstElementChild(self: *parser.Element) !?Union {
|
|
||||||
var children = try get_children(self);
|
|
||||||
return try children._item(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_lastElementChild(self: *parser.Element) !?Union {
|
|
||||||
// TODO we could check the last child node first, if it's an element,
|
|
||||||
// we can return it directly instead of looping twice over the
|
|
||||||
// children.
|
|
||||||
var children = try get_children(self);
|
|
||||||
const ln = try children.get_length();
|
|
||||||
if (ln == 0) return null;
|
|
||||||
return try children._item(ln - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_childElementCount(self: *parser.Element) !u32 {
|
|
||||||
var children = try get_children(self);
|
|
||||||
return try children.get_length();
|
|
||||||
}
|
|
||||||
|
|
||||||
// NonDocumentTypeChildNode
|
|
||||||
// https://dom.spec.whatwg.org/#interface-nondocumenttypechildnode
|
|
||||||
pub fn get_previousElementSibling(self: *parser.Element) !?Union {
|
|
||||||
const res = try parser.nodePreviousElementSibling(parser.elementToNode(self));
|
|
||||||
if (res == null) return null;
|
|
||||||
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_nextElementSibling(self: *parser.Element) !?Union {
|
|
||||||
const res = try parser.nodeNextElementSibling(parser.elementToNode(self));
|
|
||||||
if (res == null) return null;
|
|
||||||
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getElementById(self: *parser.Element, id: []const u8) !?*parser.Node {
|
|
||||||
// walk over the node tree fo find the node by id.
|
|
||||||
const root = parser.elementToNode(self);
|
|
||||||
const walker = Walker{};
|
|
||||||
var next: ?*parser.Node = null;
|
|
||||||
while (true) {
|
|
||||||
next = try walker.get_next(root, next) orelse return null;
|
|
||||||
// ignore non-element nodes.
|
|
||||||
if (try parser.nodeType(next.?) != .element) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const e = parser.nodeToElement(next.?);
|
|
||||||
if (std.mem.eql(u8, id, try get_id(e))) return next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _querySelector(self: *parser.Element, selector: []const u8, page: *Page) !?Union {
|
|
||||||
if (selector.len == 0) return null;
|
|
||||||
|
|
||||||
const n = try css.querySelector(page.call_arena, parser.elementToNode(self), selector);
|
|
||||||
|
|
||||||
if (n == null) return null;
|
|
||||||
|
|
||||||
return try toInterface(parser.nodeToElement(n.?));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _querySelectorAll(self: *parser.Element, selector: []const u8, page: *Page) !NodeList {
|
|
||||||
return css.querySelectorAll(page.arena, parser.elementToNode(self), selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _prepend(self: *parser.Element, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.prepend(parser.elementToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _append(self: *parser.Element, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.append(parser.elementToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _before(self: *parser.Element, nodes: []const Node.NodeOrText) !void {
|
|
||||||
const ref_node = parser.elementToNode(self);
|
|
||||||
return Node.before(ref_node, nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _after(self: *parser.Element, nodes: []const Node.NodeOrText) !void {
|
|
||||||
const ref_node = parser.elementToNode(self);
|
|
||||||
return Node.after(ref_node, nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _replaceChildren(self: *parser.Element, nodes: []const Node.NodeOrText) !void {
|
|
||||||
return Node.replaceChildren(parser.elementToNode(self), nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A DOMRect object providing information about the size of an element and its position relative to the viewport.
|
|
||||||
// Returns a 0 DOMRect object if the element is eventually detached from the main window
|
|
||||||
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect {
|
|
||||||
// Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes.
|
|
||||||
if (!try page.isNodeAttached(parser.elementToNode(self))) {
|
|
||||||
return DOMRect{
|
|
||||||
.x = 0,
|
|
||||||
.y = 0,
|
|
||||||
.width = 0,
|
|
||||||
.height = 0,
|
|
||||||
.bottom = 0,
|
|
||||||
.right = 0,
|
|
||||||
.top = 0,
|
|
||||||
.left = 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return page.renderer.getRect(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client.
|
|
||||||
// We do not render so it only always return the element's bounding rect.
|
|
||||||
// Returns an empty array if the element is eventually detached from the main window
|
|
||||||
pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect {
|
|
||||||
if (!try page.isNodeAttached(parser.elementToNode(self))) {
|
|
||||||
return &.{};
|
|
||||||
}
|
|
||||||
const heap_ptr = try page.call_arena.create(DOMRect);
|
|
||||||
heap_ptr.* = try page.renderer.getRect(self);
|
|
||||||
return heap_ptr[0..1];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_clientWidth(_: *parser.Element, page: *Page) u32 {
|
|
||||||
return page.renderer.width();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_clientHeight(_: *parser.Element, page: *Page) u32 {
|
|
||||||
return page.renderer.height();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _matches(self: *parser.Element, selectors: []const u8, page: *Page) !bool {
|
|
||||||
const cssParse = @import("../css/css.zig").parse;
|
|
||||||
const CssNodeWrap = @import("../css/libdom.zig").Node;
|
|
||||||
const s = try cssParse(page.call_arena, selectors, .{});
|
|
||||||
return s.match(CssNodeWrap{ .node = parser.elementToNode(self) });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _scrollIntoViewIfNeeded(_: *parser.Element, center_if_needed: ?bool) void {
|
|
||||||
_ = center_if_needed;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CheckVisibilityOpts = struct {
|
|
||||||
contentVisibilityAuto: bool,
|
|
||||||
opacityProperty: bool,
|
|
||||||
visibilityProperty: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool {
|
|
||||||
_ = self;
|
|
||||||
_ = opts;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.Element" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let g = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "g.namespaceURI", "http://www.w3.org/1999/xhtml" },
|
|
||||||
.{ "g.prefix", "null" },
|
|
||||||
.{ "g.localName", "div" },
|
|
||||||
.{ "g.tagName", "DIV" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let gs = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "gs.id", "content" },
|
|
||||||
.{ "gs.id = 'foo'", "foo" },
|
|
||||||
.{ "gs.id", "foo" },
|
|
||||||
.{ "gs.id = 'content'", "content" },
|
|
||||||
.{ "gs.className", "" },
|
|
||||||
.{ "let gs2 = document.getElementById('para-empty')", "undefined" },
|
|
||||||
.{ "gs2.className", "ok empty" },
|
|
||||||
.{ "gs2.className = 'foo bar baz'", "foo bar baz" },
|
|
||||||
.{ "gs2.className", "foo bar baz" },
|
|
||||||
.{ "gs2.className = 'ok empty'", "ok empty" },
|
|
||||||
.{ "let cl = gs2.classList", "undefined" },
|
|
||||||
.{ "cl.length", "2" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const el2 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "el2.id = 'closest'; el2.className = 'ok';", "ok" },
|
|
||||||
.{ "el2.closest('#closest')", "[object HTMLDivElement]" },
|
|
||||||
.{ "el2.closest('.ok')", "[object HTMLDivElement]" },
|
|
||||||
.{ "el2.closest('#9000')", "null" },
|
|
||||||
.{ "el2.closest('.notok')", "null" },
|
|
||||||
|
|
||||||
.{ "const sp = document.createElement('span');", "undefined" },
|
|
||||||
.{ "el2.appendChild(sp);", "[object HTMLSpanElement]" },
|
|
||||||
.{ "sp.closest('#closest')", "[object HTMLDivElement]" },
|
|
||||||
.{ "sp.closest('#9000')", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let a = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "a.hasAttributes()", "true" },
|
|
||||||
.{ "a.attributes.length", "1" },
|
|
||||||
.{ "a.getAttribute('id')", "content" },
|
|
||||||
.{ "a.attributes['id'].value", "content" },
|
|
||||||
.{
|
|
||||||
\\ let x = '';
|
|
||||||
\\ for (const attr of a.attributes) {
|
|
||||||
\\ x += attr.name + '=' + attr.value;
|
|
||||||
\\ }
|
|
||||||
\\ x;
|
|
||||||
,
|
|
||||||
"id=content",
|
|
||||||
},
|
|
||||||
|
|
||||||
.{ "a.hasAttribute('foo')", "false" },
|
|
||||||
.{ "a.getAttribute('foo')", "null" },
|
|
||||||
|
|
||||||
.{ "a.setAttribute('foo', 'bar')", "undefined" },
|
|
||||||
.{ "a.hasAttribute('foo')", "true" },
|
|
||||||
.{ "a.getAttribute('foo')", "bar" },
|
|
||||||
|
|
||||||
.{ "a.setAttribute('foo', 'baz')", "undefined" },
|
|
||||||
.{ "a.hasAttribute('foo')", "true" },
|
|
||||||
.{ "a.getAttribute('foo')", "baz" },
|
|
||||||
|
|
||||||
.{ "a.removeAttribute('foo')", "undefined" },
|
|
||||||
.{ "a.hasAttribute('foo')", "false" },
|
|
||||||
.{ "a.getAttribute('foo')", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let b = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "b.toggleAttribute('foo')", "true" },
|
|
||||||
.{ "b.hasAttribute('foo')", "true" },
|
|
||||||
.{ "b.getAttribute('foo')", "" },
|
|
||||||
|
|
||||||
.{ "b.toggleAttribute('foo')", "false" },
|
|
||||||
.{ "b.hasAttribute('foo')", "false" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let c = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "c.children.length", "3" },
|
|
||||||
.{ "c.firstElementChild.nodeName", "A" },
|
|
||||||
.{ "c.lastElementChild.nodeName", "P" },
|
|
||||||
.{ "c.childElementCount", "3" },
|
|
||||||
|
|
||||||
.{ "c.prepend(document.createTextNode('foo'))", "undefined" },
|
|
||||||
.{ "c.append(document.createTextNode('bar'))", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let d = document.getElementById('para')", "undefined" },
|
|
||||||
.{ "d.previousElementSibling.nodeName", "P" },
|
|
||||||
.{ "d.nextElementSibling", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let e = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "e.querySelector('foo')", "null" },
|
|
||||||
.{ "e.querySelector('#foo')", "null" },
|
|
||||||
.{ "e.querySelector('#link').id", "link" },
|
|
||||||
.{ "e.querySelector('#para').id", "para" },
|
|
||||||
.{ "e.querySelector('*').id", "link" },
|
|
||||||
.{ "e.querySelector('')", "null" },
|
|
||||||
.{ "e.querySelector('*').id", "link" },
|
|
||||||
.{ "e.querySelector('#content')", "null" },
|
|
||||||
.{ "e.querySelector('#para').id", "para" },
|
|
||||||
.{ "e.querySelector('.ok').id", "link" },
|
|
||||||
.{ "e.querySelector('a ~ p').id", "para-empty" },
|
|
||||||
|
|
||||||
.{ "e.querySelectorAll('foo').length", "0" },
|
|
||||||
.{ "e.querySelectorAll('#foo').length", "0" },
|
|
||||||
.{ "e.querySelectorAll('#link').length", "1" },
|
|
||||||
.{ "e.querySelectorAll('#link').item(0).id", "link" },
|
|
||||||
.{ "e.querySelectorAll('#para').length", "1" },
|
|
||||||
.{ "e.querySelectorAll('#para').item(0).id", "para" },
|
|
||||||
.{ "e.querySelectorAll('*').length", "4" },
|
|
||||||
.{ "e.querySelectorAll('p').length", "2" },
|
|
||||||
.{ "e.querySelectorAll('.ok').item(0).id", "link" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let f = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "let ff = document.createAttribute('foo')", "undefined" },
|
|
||||||
.{ "f.setAttributeNode(ff)", "null" },
|
|
||||||
.{ "f.getAttributeNode('foo').name", "foo" },
|
|
||||||
.{ "f.removeAttributeNode(ff).name", "foo" },
|
|
||||||
.{ "f.getAttributeNode('bar')", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.getElementById('para').innerHTML", " And" },
|
|
||||||
.{ "document.getElementById('para-empty').innerHTML.trim()", "<span id=\"para-empty-child\"></span>" },
|
|
||||||
|
|
||||||
.{ "let h = document.getElementById('para-empty')", "undefined" },
|
|
||||||
.{ "const prev = h.innerHTML", "undefined" },
|
|
||||||
.{ "h.innerHTML = '<p id=\"hello\">hello world</p>'", "<p id=\"hello\">hello world</p>" },
|
|
||||||
.{ "h.innerHTML", "<p id=\"hello\">hello world</p>" },
|
|
||||||
.{ "h.firstChild.nodeName", "P" },
|
|
||||||
.{ "h.firstChild.id", "hello" },
|
|
||||||
.{ "h.firstChild.textContent", "hello world" },
|
|
||||||
.{ "h.innerHTML = prev; true", "true" },
|
|
||||||
.{ "document.getElementById('para-empty').innerHTML.trim()", "<span id=\"para-empty-child\"></span>" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.getElementById('para').outerHTML", "<p id=\"para\"> And</p>" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.getElementById('para').clientWidth", "1" },
|
|
||||||
.{ "document.getElementById('para').clientHeight", "1" },
|
|
||||||
|
|
||||||
.{ "let r1 = document.getElementById('para').getBoundingClientRect()", "undefined" },
|
|
||||||
.{ "r1.x", "0" },
|
|
||||||
.{ "r1.y", "0" },
|
|
||||||
.{ "r1.width", "1" },
|
|
||||||
.{ "r1.height", "1" },
|
|
||||||
|
|
||||||
.{ "let r2 = document.getElementById('content').getBoundingClientRect()", "undefined" },
|
|
||||||
.{ "r2.x", "1" },
|
|
||||||
.{ "r2.y", "0" },
|
|
||||||
.{ "r2.width", "1" },
|
|
||||||
.{ "r2.height", "1" },
|
|
||||||
|
|
||||||
.{ "let r3 = document.getElementById('para').getBoundingClientRect()", "undefined" },
|
|
||||||
.{ "r3.x", "0" },
|
|
||||||
.{ "r3.y", "0" },
|
|
||||||
.{ "r3.width", "1" },
|
|
||||||
.{ "r3.height", "1" },
|
|
||||||
|
|
||||||
.{ "document.getElementById('para').clientWidth", "2" },
|
|
||||||
.{ "document.getElementById('para').clientHeight", "1" },
|
|
||||||
|
|
||||||
.{ "let r4 = document.createElement('div').getBoundingClientRect()", null },
|
|
||||||
.{ "r4.x", "0" },
|
|
||||||
.{ "r4.y", "0" },
|
|
||||||
.{ "r4.width", "0" },
|
|
||||||
.{ "r4.height", "0" },
|
|
||||||
|
|
||||||
// Test setup causes WrongDocument or HierarchyRequest error unlike in chrome/firefox
|
|
||||||
// .{ // An element of another document, even if created from the main document, is not rendered.
|
|
||||||
// \\ let div5 = document.createElement('div');
|
|
||||||
// \\ const newDoc = document.implementation.createHTMLDocument("New Document");
|
|
||||||
// \\ newDoc.body.appendChild(div5);
|
|
||||||
// \\ let r5 = div5.getBoundingClientRect();
|
|
||||||
// ,
|
|
||||||
// null,
|
|
||||||
// },
|
|
||||||
// .{ "r5.x", "0" },
|
|
||||||
// .{ "r5.y", "0" },
|
|
||||||
// .{ "r5.width", "0" },
|
|
||||||
// .{ "r5.height", "0" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const el = document.createElement('div');", "undefined" },
|
|
||||||
.{ "el.id = 'matches'; el.className = 'ok';", "ok" },
|
|
||||||
.{ "el.matches('#matches')", "true" },
|
|
||||||
.{ "el.matches('.ok')", "true" },
|
|
||||||
.{ "el.matches('#9000')", "false" },
|
|
||||||
.{ "el.matches('.notok')", "false" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const el3 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "el3.scrollIntoViewIfNeeded();", "undefined" },
|
|
||||||
.{ "el3.scrollIntoViewIfNeeded(false);", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// before
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const before_container = document.createElement('div');", "undefined" },
|
|
||||||
.{ "document.append(before_container);", "undefined" },
|
|
||||||
.{ "const b1 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "before_container.append(b1);", "undefined" },
|
|
||||||
|
|
||||||
.{ "const b1_a = document.createElement('p');", "undefined" },
|
|
||||||
.{ "b1.before(b1_a, 'over 9000');", "undefined" },
|
|
||||||
.{ "before_container.innerHTML", "<p></p>over 9000<div></div>" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// after
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const after_container = document.createElement('div');", "undefined" },
|
|
||||||
.{ "document.append(after_container);", "undefined" },
|
|
||||||
.{ "const a1 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "after_container.append(a1);", "undefined" },
|
|
||||||
|
|
||||||
.{ "const a1_a = document.createElement('p');", "undefined" },
|
|
||||||
.{ "a1.after('over 9000', a1_a);", "undefined" },
|
|
||||||
.{ "after_container.innerHTML", "<div></div>over 9000<p></p>" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var div1 = document.createElement('div');", null },
|
|
||||||
.{ "div1.innerHTML = \" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>\"", null },
|
|
||||||
.{ "div1.getElementsByTagName('a').length", "1" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,253 +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 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,
|
|
||||||
};
|
|
||||||
|
|
||||||
// EventTarget implementation
|
|
||||||
pub const EventTarget = struct {
|
|
||||||
pub const Self = parser.EventTarget;
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
|
|
||||||
pub fn toInterface(e: *parser.Event, et: *parser.EventTarget, page: *Page) !Union {
|
|
||||||
// libdom assumes that all event targets are libdom nodes. They are not.
|
|
||||||
|
|
||||||
// The window is a common non-node target, but it's easy to handle as
|
|
||||||
// its a singleton.
|
|
||||||
if (@intFromPtr(et) == @intFromPtr(&page.window.base)) {
|
|
||||||
return .{ .node = .{ .Window = &page.window } };
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbortSignal is another non-node target. It has a distinct usage though
|
|
||||||
// so we hijack the event internal type to identity if.
|
|
||||||
switch (try parser.eventGetInternalType(e)) {
|
|
||||||
.abort_signal => {
|
|
||||||
return .{ .node = .{ .AbortSignal = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) } };
|
|
||||||
},
|
|
||||||
.xhr_event => {
|
|
||||||
const XMLHttpRequestEventTarget = @import("../xhr/event_target.zig").XMLHttpRequestEventTarget;
|
|
||||||
const base: *XMLHttpRequestEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
|
|
||||||
return .{ .xhr = @fieldParentPtr("proto", base) };
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS funcs
|
|
||||||
// --------
|
|
||||||
pub fn _addEventListener(
|
|
||||||
self: *parser.EventTarget,
|
|
||||||
typ: []const u8,
|
|
||||||
listener: EventHandler.Listener,
|
|
||||||
opts: ?EventHandler.Opts,
|
|
||||||
page: *Page,
|
|
||||||
) !void {
|
|
||||||
_ = try EventHandler.register(page.arena, self, typ, listener, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
const RemoveEventListenerOpts = union(enum) {
|
|
||||||
opts: Opts,
|
|
||||||
capture: bool,
|
|
||||||
|
|
||||||
const Opts = struct {
|
|
||||||
capture: ?bool,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _removeEventListener(
|
|
||||||
self: *parser.EventTarget,
|
|
||||||
typ: []const u8,
|
|
||||||
listener: EventHandler.Listener,
|
|
||||||
opts_: ?RemoveEventListenerOpts,
|
|
||||||
) !void {
|
|
||||||
var capture = false;
|
|
||||||
if (opts_) |opts| {
|
|
||||||
capture = switch (opts) {
|
|
||||||
.capture => |c| c,
|
|
||||||
.opts => |o| o.capture orelse false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const cbk = (try listener.callback(self)) orelse return;
|
|
||||||
|
|
||||||
// check if event target has already this listener
|
|
||||||
const lst = try parser.eventTargetHasListener(
|
|
||||||
self,
|
|
||||||
typ,
|
|
||||||
capture,
|
|
||||||
cbk.id,
|
|
||||||
);
|
|
||||||
if (lst == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove listener
|
|
||||||
try parser.eventTargetRemoveEventListener(
|
|
||||||
self,
|
|
||||||
typ,
|
|
||||||
lst.?,
|
|
||||||
capture,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool {
|
|
||||||
return try parser.eventTargetDispatchEvent(self, event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.EventTarget" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let content = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "let para = document.getElementById('para')", "undefined" },
|
|
||||||
// NOTE: as some event properties will change during the event dispatching phases
|
|
||||||
// we need to copy thoses values in order to check them afterwards
|
|
||||||
.{
|
|
||||||
\\ var nb = 0; var evt; var phase; var cur;
|
|
||||||
\\ function cbk(event) {
|
|
||||||
\\ evt = event;
|
|
||||||
\\ phase = event.eventPhase;
|
|
||||||
\\ cur = event.currentTarget;
|
|
||||||
\\ nb ++;
|
|
||||||
\\ }
|
|
||||||
,
|
|
||||||
"undefined",
|
|
||||||
},
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "content.addEventListener('basic', cbk)", "undefined" },
|
|
||||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
|
||||||
.{ "nb", "1" },
|
|
||||||
.{ "evt instanceof Event", "true" },
|
|
||||||
.{ "evt.type", "basic" },
|
|
||||||
.{ "phase", "2" },
|
|
||||||
.{ "cur.getAttribute('id')", "content" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
|
||||||
.{ "para.dispatchEvent(new Event('basic'))", "true" },
|
|
||||||
.{ "nb", "0" }, // handler is not called, no capture, not the target, no bubbling
|
|
||||||
.{ "evt === undefined", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0", "0" },
|
|
||||||
.{ "content.addEventListener('basic', cbk)", "undefined" },
|
|
||||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
|
||||||
.{ "nb", "1" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0", "0" },
|
|
||||||
.{ "content.addEventListener('basic', cbk, true)", "undefined" },
|
|
||||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
|
||||||
.{ "nb", "2" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0", "0" },
|
|
||||||
.{ "content.removeEventListener('basic', cbk)", "undefined" },
|
|
||||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
|
||||||
.{ "nb", "1" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0", "0" },
|
|
||||||
.{ "content.removeEventListener('basic', cbk, {capture: true})", "undefined" },
|
|
||||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
|
||||||
.{ "nb", "0" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
|
||||||
.{ "content.addEventListener('capture', cbk, true)", "undefined" },
|
|
||||||
.{ "content.dispatchEvent(new Event('capture'))", "true" },
|
|
||||||
.{ "nb", "1" },
|
|
||||||
.{ "evt instanceof Event", "true" },
|
|
||||||
.{ "evt.type", "capture" },
|
|
||||||
.{ "phase", "2" },
|
|
||||||
.{ "cur.getAttribute('id')", "content" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
|
||||||
.{ "para.dispatchEvent(new Event('capture'))", "true" },
|
|
||||||
.{ "nb", "1" },
|
|
||||||
.{ "evt instanceof Event", "true" },
|
|
||||||
.{ "evt.type", "capture" },
|
|
||||||
.{ "phase", "1" },
|
|
||||||
.{ "cur.getAttribute('id')", "content" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
|
||||||
.{ "content.addEventListener('bubbles', cbk)", "undefined" },
|
|
||||||
.{ "content.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" },
|
|
||||||
.{ "nb", "1" },
|
|
||||||
.{ "evt instanceof Event", "true" },
|
|
||||||
.{ "evt.type", "bubbles" },
|
|
||||||
.{ "evt.bubbles", "true" },
|
|
||||||
.{ "phase", "2" },
|
|
||||||
.{ "cur.getAttribute('id')", "content" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
|
||||||
.{ "para.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" },
|
|
||||||
.{ "nb", "1" },
|
|
||||||
.{ "evt instanceof Event", "true" },
|
|
||||||
.{ "evt.type", "bubbles" },
|
|
||||||
.{ "phase", "3" },
|
|
||||||
.{ "cur.getAttribute('id')", "content" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const obj1 = {calls: 0, handleEvent: function() { this.calls += 1; } };", null },
|
|
||||||
.{ "content.addEventListener('he', obj1);", null },
|
|
||||||
.{ "content.dispatchEvent(new Event('he'));", null },
|
|
||||||
.{ "obj1.calls", "1" },
|
|
||||||
|
|
||||||
.{ "content.removeEventListener('he', obj1);", null },
|
|
||||||
.{ "content.dispatchEvent(new Event('he'));", null },
|
|
||||||
.{ "obj1.calls", "1" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// doesn't crash on null receiver
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "content.addEventListener('he2', null);", null },
|
|
||||||
.{ "content.dispatchEvent(new Event('he2'));", null },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const allocPrint = std.fmt.allocPrint;
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
// https://webidl.spec.whatwg.org/#idl-DOMException
|
|
||||||
pub const DOMException = struct {
|
|
||||||
err: ?parser.DOMError,
|
|
||||||
str: []const u8,
|
|
||||||
|
|
||||||
pub const ErrorSet = parser.DOMError;
|
|
||||||
|
|
||||||
// static attributes
|
|
||||||
pub const _INDEX_SIZE_ERR = 1;
|
|
||||||
pub const _DOMSTRING_SIZE_ERR = 2;
|
|
||||||
pub const _HIERARCHY_REQUEST_ERR = 3;
|
|
||||||
pub const _WRONG_DOCUMENT_ERR = 4;
|
|
||||||
pub const _INVALID_CHARACTER_ERR = 5;
|
|
||||||
pub const _NO_DATA_ALLOWED_ERR = 6;
|
|
||||||
pub const _NO_MODIFICATION_ALLOWED_ERR = 7;
|
|
||||||
pub const _NOT_FOUND_ERR = 8;
|
|
||||||
pub const _NOT_SUPPORTED_ERR = 9;
|
|
||||||
pub const _INUSE_ATTRIBUTE_ERR = 10;
|
|
||||||
pub const _INVALID_STATE_ERR = 11;
|
|
||||||
pub const _SYNTAX_ERR = 12;
|
|
||||||
pub const _INVALID_MODIFICATION_ERR = 13;
|
|
||||||
pub const _NAMESPACE_ERR = 14;
|
|
||||||
pub const _INVALID_ACCESS_ERR = 15;
|
|
||||||
pub const _VALIDATION_ERR = 16;
|
|
||||||
pub const _TYPE_MISMATCH_ERR = 17;
|
|
||||||
pub const _SECURITY_ERR = 18;
|
|
||||||
pub const _NETWORK_ERR = 19;
|
|
||||||
pub const _ABORT_ERR = 20;
|
|
||||||
pub const _URL_MISMATCH_ERR = 21;
|
|
||||||
pub const _QUOTA_EXCEEDED_ERR = 22;
|
|
||||||
pub const _TIMEOUT_ERR = 23;
|
|
||||||
pub const _INVALID_NODE_TYPE_ERR = 24;
|
|
||||||
pub const _DATA_CLONE_ERR = 25;
|
|
||||||
|
|
||||||
pub fn constructor(message_: ?[]const u8, name_: ?[]const u8, page: *const Page) !DOMException {
|
|
||||||
const message = message_ orelse "";
|
|
||||||
const err = if (name_) |n| error_from_str(n) else null;
|
|
||||||
const fixed_name = name(err);
|
|
||||||
|
|
||||||
if (message.len == 0) return .{ .err = err, .str = fixed_name };
|
|
||||||
|
|
||||||
const str = try allocPrint(page.arena, "{s}: {s}", .{ fixed_name, message });
|
|
||||||
return .{ .err = err, .str = str };
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: deinit
|
|
||||||
pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) !DOMException {
|
|
||||||
const errCast = @as(parser.DOMError, @errorCast(err));
|
|
||||||
const errName = DOMException.name(errCast);
|
|
||||||
const str = switch (errCast) {
|
|
||||||
error.HierarchyRequest => try allocPrint(
|
|
||||||
alloc,
|
|
||||||
"{s}: Failed to execute '{s}' on 'Node': The new child element contains the parent.",
|
|
||||||
.{ errName, callerName },
|
|
||||||
),
|
|
||||||
error.NoError => unreachable,
|
|
||||||
else => try allocPrint(
|
|
||||||
alloc,
|
|
||||||
"{s}: TODO message", // TODO: implement other messages
|
|
||||||
.{DOMException.name(errCast)},
|
|
||||||
),
|
|
||||||
};
|
|
||||||
return .{ .err = errCast, .str = str };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error_from_str(name_: []const u8) ?parser.DOMError {
|
|
||||||
// @speed: Consider length first, left as is for maintainability, awaiting switch on string support
|
|
||||||
if (std.mem.eql(u8, name_, "IndexSizeError")) return error.IndexSize;
|
|
||||||
if (std.mem.eql(u8, name_, "StringSizeError")) return error.StringSize;
|
|
||||||
if (std.mem.eql(u8, name_, "HierarchyRequestError")) return error.HierarchyRequest;
|
|
||||||
if (std.mem.eql(u8, name_, "WrongDocumentError")) return error.WrongDocument;
|
|
||||||
if (std.mem.eql(u8, name_, "InvalidCharacterError")) return error.InvalidCharacter;
|
|
||||||
if (std.mem.eql(u8, name_, "NoDataAllowedError")) return error.NoDataAllowed;
|
|
||||||
if (std.mem.eql(u8, name_, "NoModificationAllowedError")) return error.NoModificationAllowed;
|
|
||||||
if (std.mem.eql(u8, name_, "NotFoundError")) return error.NotFound;
|
|
||||||
if (std.mem.eql(u8, name_, "NotSupportedError")) return error.NotSupported;
|
|
||||||
if (std.mem.eql(u8, name_, "InuseAttributeError")) return error.InuseAttribute;
|
|
||||||
if (std.mem.eql(u8, name_, "InvalidStateError")) return error.InvalidState;
|
|
||||||
if (std.mem.eql(u8, name_, "SyntaxError")) return error.Syntax;
|
|
||||||
if (std.mem.eql(u8, name_, "InvalidModificationError")) return error.InvalidModification;
|
|
||||||
if (std.mem.eql(u8, name_, "NamespaceError")) return error.Namespace;
|
|
||||||
if (std.mem.eql(u8, name_, "InvalidAccessError")) return error.InvalidAccess;
|
|
||||||
if (std.mem.eql(u8, name_, "ValidationError")) return error.Validation;
|
|
||||||
if (std.mem.eql(u8, name_, "TypeMismatchError")) return error.TypeMismatch;
|
|
||||||
if (std.mem.eql(u8, name_, "SecurityError")) return error.Security;
|
|
||||||
if (std.mem.eql(u8, name_, "NetworkError")) return error.Network;
|
|
||||||
if (std.mem.eql(u8, name_, "AbortError")) return error.Abort;
|
|
||||||
if (std.mem.eql(u8, name_, "URLismatchError")) return error.URLismatch;
|
|
||||||
if (std.mem.eql(u8, name_, "QuotaExceededError")) return error.QuotaExceeded;
|
|
||||||
if (std.mem.eql(u8, name_, "TimeoutError")) return error.Timeout;
|
|
||||||
if (std.mem.eql(u8, name_, "InvalidNodeTypeError")) return error.InvalidNodeType;
|
|
||||||
if (std.mem.eql(u8, name_, "DataCloneError")) return error.DataClone;
|
|
||||||
|
|
||||||
// custom netsurf error
|
|
||||||
if (std.mem.eql(u8, name_, "UnspecifiedEventTypeError")) return error.UnspecifiedEventType;
|
|
||||||
if (std.mem.eql(u8, name_, "DispatchRequestError")) return error.DispatchRequest;
|
|
||||||
if (std.mem.eql(u8, name_, "NoMemoryError")) return error.NoMemory;
|
|
||||||
if (std.mem.eql(u8, name_, "AttributeWrongTypeError")) return error.AttributeWrongType;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(err_: ?parser.DOMError) []const u8 {
|
|
||||||
const err = err_ orelse return "Error";
|
|
||||||
|
|
||||||
return switch (err) {
|
|
||||||
error.IndexSize => "IndexSizeError",
|
|
||||||
error.StringSize => "StringSizeError", // Legacy: DOMSTRING_SIZE_ERR
|
|
||||||
error.HierarchyRequest => "HierarchyRequestError",
|
|
||||||
error.WrongDocument => "WrongDocumentError",
|
|
||||||
error.InvalidCharacter => "InvalidCharacterError",
|
|
||||||
error.NoDataAllowed => "NoDataAllowedError", // Legacy: NO_DATA_ALLOWED_ERR
|
|
||||||
error.NoModificationAllowed => "NoModificationAllowedError",
|
|
||||||
error.NotFound => "NotFoundError",
|
|
||||||
error.NotSupported => "NotSupportedError",
|
|
||||||
error.InuseAttribute => "InuseAttributeError",
|
|
||||||
error.InvalidState => "InvalidStateError",
|
|
||||||
error.Syntax => "SyntaxError",
|
|
||||||
error.InvalidModification => "InvalidModificationError",
|
|
||||||
error.Namespace => "NamespaceError",
|
|
||||||
error.InvalidAccess => "InvalidAccessError",
|
|
||||||
error.Validation => "ValidationError", // Legacy: VALIDATION_ERR
|
|
||||||
error.TypeMismatch => "TypeMismatchError",
|
|
||||||
error.Security => "SecurityError",
|
|
||||||
error.Network => "NetworkError",
|
|
||||||
error.Abort => "AbortError",
|
|
||||||
error.URLismatch => "URLismatchError",
|
|
||||||
error.QuotaExceeded => "QuotaExceededError",
|
|
||||||
error.Timeout => "TimeoutError",
|
|
||||||
error.InvalidNodeType => "InvalidNodeTypeError",
|
|
||||||
error.DataClone => "DataCloneError",
|
|
||||||
error.NoError => unreachable,
|
|
||||||
|
|
||||||
// custom netsurf error
|
|
||||||
error.UnspecifiedEventType => "UnspecifiedEventTypeError",
|
|
||||||
error.DispatchRequest => "DispatchRequestError",
|
|
||||||
error.NoMemory => "NoMemoryError",
|
|
||||||
error.AttributeWrongType => "AttributeWrongTypeError",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS properties and methods
|
|
||||||
|
|
||||||
pub fn get_code(self: *const DOMException) u8 {
|
|
||||||
const err = self.err orelse return 0;
|
|
||||||
return switch (err) {
|
|
||||||
error.IndexSize => 1,
|
|
||||||
error.StringSize => 2,
|
|
||||||
error.HierarchyRequest => 3,
|
|
||||||
error.WrongDocument => 4,
|
|
||||||
error.InvalidCharacter => 5,
|
|
||||||
error.NoDataAllowed => 6,
|
|
||||||
error.NoModificationAllowed => 7,
|
|
||||||
error.NotFound => 8,
|
|
||||||
error.NotSupported => 9,
|
|
||||||
error.InuseAttribute => 10,
|
|
||||||
error.InvalidState => 11,
|
|
||||||
error.Syntax => 12,
|
|
||||||
error.InvalidModification => 13,
|
|
||||||
error.Namespace => 14,
|
|
||||||
error.InvalidAccess => 15,
|
|
||||||
error.Validation => 16,
|
|
||||||
error.TypeMismatch => 17,
|
|
||||||
error.Security => 18,
|
|
||||||
error.Network => 19,
|
|
||||||
error.Abort => 20,
|
|
||||||
error.URLismatch => 21,
|
|
||||||
error.QuotaExceeded => 22,
|
|
||||||
error.Timeout => 23,
|
|
||||||
error.InvalidNodeType => 24,
|
|
||||||
error.DataClone => 25,
|
|
||||||
error.NoError => unreachable,
|
|
||||||
|
|
||||||
// custom netsurf error
|
|
||||||
error.UnspecifiedEventType => 128,
|
|
||||||
error.DispatchRequest => 129,
|
|
||||||
error.NoMemory => 130,
|
|
||||||
error.AttributeWrongType => 131,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_name(self: *const DOMException) []const u8 {
|
|
||||||
return DOMException.name(self.err);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_message(self: *const DOMException) []const u8 {
|
|
||||||
const errName = DOMException.name(self.err);
|
|
||||||
if (self.str.len <= errName.len + 2) return "";
|
|
||||||
return self.str[errName.len + 2 ..]; // ! Requires str is formatted as "{name}: {message}"
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _toString(self: *const DOMException) []const u8 {
|
|
||||||
return self.str;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.Exception" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
const err = "Failed to execute 'appendChild' on 'Node': The new child element contains the parent.";
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let content = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "let link = document.getElementById('link')", "undefined" },
|
|
||||||
// HierarchyRequestError
|
|
||||||
.{
|
|
||||||
\\ var he;
|
|
||||||
\\ try { link.appendChild(content) } catch (error) { he = error}
|
|
||||||
\\ he.name
|
|
||||||
,
|
|
||||||
"HierarchyRequestError",
|
|
||||||
},
|
|
||||||
.{ "he.code", "3" },
|
|
||||||
.{ "he.message", err },
|
|
||||||
.{ "he.toString()", "HierarchyRequestError: " ++ err },
|
|
||||||
.{ "he instanceof DOMException", "true" },
|
|
||||||
.{ "he instanceof Error", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// Test DOMException constructor
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let exc0 = new DOMException()", "undefined" },
|
|
||||||
.{ "exc0.name", "Error" },
|
|
||||||
.{ "exc0.code", "0" },
|
|
||||||
.{ "exc0.message", "" },
|
|
||||||
.{ "exc0.toString()", "Error" },
|
|
||||||
|
|
||||||
.{ "let exc1 = new DOMException('Sandwich malfunction')", "undefined" },
|
|
||||||
.{ "exc1.name", "Error" },
|
|
||||||
.{ "exc1.code", "0" },
|
|
||||||
.{ "exc1.message", "Sandwich malfunction" },
|
|
||||||
.{ "exc1.toString()", "Error: Sandwich malfunction" },
|
|
||||||
|
|
||||||
.{ "let exc2 = new DOMException('Caterpillar turned into a butterfly', 'NoModificationAllowedError')", "undefined" },
|
|
||||||
.{ "exc2.name", "NoModificationAllowedError" },
|
|
||||||
.{ "exc2.code", "7" },
|
|
||||||
.{ "exc2.message", "Caterpillar turned into a butterfly" },
|
|
||||||
.{ "exc2.toString()", "NoModificationAllowedError: Caterpillar turned into a butterfly" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,500 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const Element = @import("element.zig").Element;
|
|
||||||
const Union = @import("element.zig").Union;
|
|
||||||
const JsThis = @import("../env.zig").JsThis;
|
|
||||||
const Walker = @import("walker.zig").Walker;
|
|
||||||
|
|
||||||
const Matcher = union(enum) {
|
|
||||||
matchByName: MatchByName,
|
|
||||||
matchByTagName: MatchByTagName,
|
|
||||||
matchByClassName: MatchByClassName,
|
|
||||||
matchByLinks: MatchByLinks,
|
|
||||||
matchByAnchors: MatchByAnchors,
|
|
||||||
matchTrue: struct {},
|
|
||||||
matchFalse: struct {},
|
|
||||||
|
|
||||||
pub fn match(self: Matcher, node: *parser.Node) !bool {
|
|
||||||
switch (self) {
|
|
||||||
.matchTrue => return true,
|
|
||||||
.matchFalse => return false,
|
|
||||||
.matchByLinks => return MatchByLinks.match(node),
|
|
||||||
.matchByAnchors => return MatchByAnchors.match(node),
|
|
||||||
inline else => |m| return m.match(node),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const MatchByTagName = struct {
|
|
||||||
// tag is used to select node against their name.
|
|
||||||
// tag comparison is case insensitive.
|
|
||||||
tag: []const u8,
|
|
||||||
is_wildcard: bool,
|
|
||||||
|
|
||||||
fn init(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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn HTMLCollectionByTagName(
|
|
||||||
arena: Allocator,
|
|
||||||
root: ?*parser.Node,
|
|
||||||
tag_name: []const u8,
|
|
||||||
include_root: bool,
|
|
||||||
) !HTMLCollection {
|
|
||||||
return HTMLCollection{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
|
||||||
.matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) },
|
|
||||||
.include_root = include_root,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const MatchByClassName = struct {
|
|
||||||
class_names: []const u8,
|
|
||||||
|
|
||||||
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 {
|
|
||||||
const e = parser.nodeToElement(node);
|
|
||||||
|
|
||||||
var it = std.mem.splitScalar(u8, self.class_names, ' ');
|
|
||||||
while (it.next()) |c| {
|
|
||||||
if (!try parser.elementHasClass(e, c)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn HTMLCollectionByClassName(
|
|
||||||
arena: Allocator,
|
|
||||||
root: ?*parser.Node,
|
|
||||||
classNames: []const u8,
|
|
||||||
include_root: bool,
|
|
||||||
) !HTMLCollection {
|
|
||||||
return HTMLCollection{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
|
||||||
.matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) },
|
|
||||||
.include_root = include_root,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const MatchByName = struct {
|
|
||||||
name: []const u8,
|
|
||||||
|
|
||||||
fn init(arena: Allocator, name: []const u8) !MatchByName {
|
|
||||||
return .{
|
|
||||||
.name = try arena.dupe(u8, name),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match(self: MatchByName, node: *parser.Node) !bool {
|
|
||||||
const e = parser.nodeToElement(node);
|
|
||||||
const nname = try parser.elementGetAttribute(e, "name") orelse return false;
|
|
||||||
return std.mem.eql(u8, self.name, nname);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn HTMLCollectionByName(
|
|
||||||
arena: Allocator,
|
|
||||||
root: ?*parser.Node,
|
|
||||||
name: []const u8,
|
|
||||||
include_root: bool,
|
|
||||||
) !HTMLCollection {
|
|
||||||
return HTMLCollection{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
|
||||||
.matcher = .{ .matchByName = try MatchByName.init(arena, name) },
|
|
||||||
.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 {
|
|
||||||
return HTMLCollection{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerChildren = .{} },
|
|
||||||
.matcher = .{ .matchTrue = .{} },
|
|
||||||
.include_root = include_root,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn HTMLCollectionEmpty() !HTMLCollection {
|
|
||||||
return HTMLCollection{
|
|
||||||
.root = null,
|
|
||||||
.walker = .{ .walkerNone = .{} },
|
|
||||||
.matcher = .{ .matchFalse = .{} },
|
|
||||||
.include_root = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchByLinks matches the a and area elements in the Document that have href
|
|
||||||
// attributes.
|
|
||||||
// https://html.spec.whatwg.org/#dom-document-links
|
|
||||||
pub const MatchByLinks = struct {
|
|
||||||
pub fn match(node: *parser.Node) !bool {
|
|
||||||
const tag = try parser.nodeName(node);
|
|
||||||
if (!std.ascii.eqlIgnoreCase(tag, "a") and !std.ascii.eqlIgnoreCase(tag, "area")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const elem = @as(*parser.Element, @ptrCast(node));
|
|
||||||
return parser.elementHasAttribute(elem, "href");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn HTMLCollectionByLinks(
|
|
||||||
root: ?*parser.Node,
|
|
||||||
include_root: bool,
|
|
||||||
) !HTMLCollection {
|
|
||||||
return HTMLCollection{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
|
||||||
.matcher = .{ .matchByLinks = MatchByLinks{} },
|
|
||||||
.include_root = include_root,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchByAnchors matches the a elements in the Document that have name
|
|
||||||
// attributes.
|
|
||||||
// https://html.spec.whatwg.org/#dom-document-anchors
|
|
||||||
pub const MatchByAnchors = struct {
|
|
||||||
pub fn match(node: *parser.Node) !bool {
|
|
||||||
const tag = try parser.nodeName(node);
|
|
||||||
if (!std.ascii.eqlIgnoreCase(tag, "a")) return false;
|
|
||||||
|
|
||||||
const elem = @as(*parser.Element, @ptrCast(node));
|
|
||||||
return parser.elementHasAttribute(elem, "name");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn HTMLCollectionByAnchors(
|
|
||||||
root: ?*parser.Node,
|
|
||||||
include_root: bool,
|
|
||||||
) !HTMLCollection {
|
|
||||||
return HTMLCollection{
|
|
||||||
.root = root,
|
|
||||||
.walker = .{ .walkerDepthFirst = .{} },
|
|
||||||
.matcher = .{ .matchByAnchors = MatchByAnchors{} },
|
|
||||||
.include_root = include_root,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const HTMLCollectionIterator = struct {
|
|
||||||
coll: *HTMLCollection,
|
|
||||||
index: u32 = 0,
|
|
||||||
|
|
||||||
pub const Return = struct {
|
|
||||||
value: ?Union,
|
|
||||||
done: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _next(self: *HTMLCollectionIterator) !Return {
|
|
||||||
const e = try self.coll._item(self.index);
|
|
||||||
if (e == null) {
|
|
||||||
return Return{
|
|
||||||
.value = null,
|
|
||||||
.done = true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.index += 1;
|
|
||||||
return Return{
|
|
||||||
.value = e,
|
|
||||||
.done = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection
|
|
||||||
// HTMLCollection is re implemented in zig here because libdom
|
|
||||||
// dom_html_collection expects a comparison function callback as arguement.
|
|
||||||
// But we wanted a dynamically comparison here, according to the match tagname.
|
|
||||||
pub const HTMLCollection = struct {
|
|
||||||
matcher: Matcher,
|
|
||||||
walker: Walker,
|
|
||||||
|
|
||||||
root: ?*parser.Node,
|
|
||||||
|
|
||||||
// By default the HTMLCollection walk on the root's descendant only.
|
|
||||||
// But on somes cases, like for dom document, we want to walk over the root
|
|
||||||
// itself.
|
|
||||||
include_root: bool = false,
|
|
||||||
|
|
||||||
// save a state for the collection to improve the _item speed.
|
|
||||||
cur_idx: ?u32 = null,
|
|
||||||
cur_node: ?*parser.Node = null,
|
|
||||||
|
|
||||||
// start returns the first node to walk on.
|
|
||||||
fn start(self: *const HTMLCollection) !?*parser.Node {
|
|
||||||
if (self.root == null) return null;
|
|
||||||
|
|
||||||
if (self.include_root) {
|
|
||||||
return self.root.?;
|
|
||||||
}
|
|
||||||
|
|
||||||
return try self.walker.get_next(self.root.?, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _symbol_iterator(self: *HTMLCollection) HTMLCollectionIterator {
|
|
||||||
return HTMLCollectionIterator{
|
|
||||||
.coll = self,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get_length computes the collection's length dynamically according to
|
|
||||||
/// the current root structure.
|
|
||||||
// TODO: nodes retrieved must be de-referenced.
|
|
||||||
pub fn get_length(self: *HTMLCollection) !u32 {
|
|
||||||
if (self.root == null) return 0;
|
|
||||||
|
|
||||||
var len: u32 = 0;
|
|
||||||
var node = try self.start() orelse return 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (try parser.nodeType(node) == .element) {
|
|
||||||
if (try self.matcher.match(node)) {
|
|
||||||
len += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node = try self.walker.get_next(self.root.?, node) orelse break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn item(self: *HTMLCollection, index: u32) !?*parser.Node {
|
|
||||||
if (self.root == null) return null;
|
|
||||||
|
|
||||||
var i: u32 = 0;
|
|
||||||
var node: *parser.Node = undefined;
|
|
||||||
|
|
||||||
// Use the current state to improve speed if possible.
|
|
||||||
if (self.cur_idx != null and index >= self.cur_idx.?) {
|
|
||||||
i = self.cur_idx.?;
|
|
||||||
node = self.cur_node.?;
|
|
||||||
} else {
|
|
||||||
node = try self.start() orelse return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (try parser.nodeType(node) == .element) {
|
|
||||||
if (try self.matcher.match(node)) {
|
|
||||||
// check if we found the searched element.
|
|
||||||
if (i == index) {
|
|
||||||
// save the current state
|
|
||||||
self.cur_node = node;
|
|
||||||
self.cur_idx = i;
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node = try self.walker.get_next(self.root.?, node) orelse break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _item(self: *HTMLCollection, index: u32) !?Union {
|
|
||||||
const node = try self.item(index) orelse return null;
|
|
||||||
const e = @as(*parser.Element, @ptrCast(node));
|
|
||||||
return try Element.toInterface(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _namedItem(self: *const HTMLCollection, name: []const u8) !?Union {
|
|
||||||
if (self.root == null) return null;
|
|
||||||
if (name.len == 0) return null;
|
|
||||||
|
|
||||||
var node = try self.start() orelse return null;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (try parser.nodeType(node) == .element) {
|
|
||||||
if (try self.matcher.match(node)) {
|
|
||||||
const elem = @as(*parser.Element, @ptrCast(node));
|
|
||||||
|
|
||||||
var attr = try parser.elementGetAttribute(elem, "id");
|
|
||||||
// check if the node id corresponds to the name argument.
|
|
||||||
if (attr != null and std.mem.eql(u8, name, attr.?)) {
|
|
||||||
return try Element.toInterface(elem);
|
|
||||||
}
|
|
||||||
|
|
||||||
attr = try parser.elementGetAttribute(elem, "name");
|
|
||||||
// check if the node id corresponds to the name argument.
|
|
||||||
if (attr != null and std.mem.eql(u8, name, attr.?)) {
|
|
||||||
return try Element.toInterface(elem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node = try self.walker.get_next(self.root.?, node) orelse break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn item_name(elt: *parser.Element) !?[]const u8 {
|
|
||||||
if (try parser.elementGetAttribute(elt, "id")) |v| {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
if (try parser.elementGetAttribute(elt, "name")) |v| {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn 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));
|
|
||||||
const as_interface = try Element.toInterface(e);
|
|
||||||
try js_this.setIndex(@intCast(i), as_interface, .{});
|
|
||||||
|
|
||||||
if (try item_name(e)) |name| {
|
|
||||||
// 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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.HTMLCollection" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
|
|
||||||
.{ "getElementsByTagName.length", "2" },
|
|
||||||
.{ "let getElementsByTagNameCI = document.getElementsByTagName('P')", "undefined" },
|
|
||||||
.{ "getElementsByTagNameCI.length", "2" },
|
|
||||||
.{ "getElementsByTagName.item(0).localName", "p" },
|
|
||||||
.{ "getElementsByTagName.item(1).localName", "p" },
|
|
||||||
.{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
|
|
||||||
.{ "getElementsByTagNameAll.length", "8" },
|
|
||||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
|
||||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
|
||||||
.{ "getElementsByTagNameAll.item(1).localName", "head" },
|
|
||||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
|
||||||
.{ "getElementsByTagNameAll.item(2).localName", "body" },
|
|
||||||
.{ "getElementsByTagNameAll.item(3).localName", "div" },
|
|
||||||
.{ "getElementsByTagNameAll.item(7).localName", "p" },
|
|
||||||
.{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
|
|
||||||
|
|
||||||
// array like
|
|
||||||
.{ "getElementsByTagNameAll[0].localName", "html" },
|
|
||||||
.{ "getElementsByTagNameAll[7].localName", "p" },
|
|
||||||
.{ "getElementsByTagNameAll[8]", "undefined" },
|
|
||||||
.{ "getElementsByTagNameAll['para-empty-child'].localName", "span" },
|
|
||||||
.{ "getElementsByTagNameAll['foo']", "undefined" },
|
|
||||||
|
|
||||||
.{ "document.getElementById('content').getElementsByTagName('*').length", "4" },
|
|
||||||
.{ "document.getElementById('content').getElementsByTagName('p').length", "2" },
|
|
||||||
.{ "document.getElementById('content').getElementsByTagName('div').length", "0" },
|
|
||||||
|
|
||||||
.{ "document.children.length", "1" },
|
|
||||||
.{ "document.getElementById('content').children.length", "3" },
|
|
||||||
|
|
||||||
// check liveness
|
|
||||||
.{ "let content = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "let pe = document.getElementById('para-empty')", "undefined" },
|
|
||||||
.{ "let p = document.createElement('p')", "undefined" },
|
|
||||||
.{ "p.textContent = 'OK live'", "OK live" },
|
|
||||||
.{ "getElementsByTagName.item(1).textContent", " And" },
|
|
||||||
.{ "content.appendChild(p) != undefined", "true" },
|
|
||||||
.{ "getElementsByTagName.length", "3" },
|
|
||||||
.{ "getElementsByTagName.item(2).textContent", "OK live" },
|
|
||||||
.{ "content.insertBefore(p, pe) != undefined", "true" },
|
|
||||||
.{ "getElementsByTagName.item(0).textContent", "OK live" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#domimplementation
|
|
||||||
pub const DOMImplementation = struct {
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
|
|
||||||
pub fn _createDocumentType(
|
|
||||||
_: *DOMImplementation,
|
|
||||||
qname: [:0]const u8,
|
|
||||||
publicId: [:0]const u8,
|
|
||||||
systemId: [:0]const u8,
|
|
||||||
) !*parser.DocumentType {
|
|
||||||
return try parser.domImplementationCreateDocumentType(qname, publicId, systemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createDocument(
|
|
||||||
_: *DOMImplementation,
|
|
||||||
namespace: ?[:0]const u8,
|
|
||||||
qname: ?[:0]const u8,
|
|
||||||
doctype: ?*parser.DocumentType,
|
|
||||||
) !*parser.Document {
|
|
||||||
return try parser.domImplementationCreateDocument(namespace, qname, doctype);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createHTMLDocument(_: *DOMImplementation, title: ?[]const u8) !*parser.DocumentHTML {
|
|
||||||
return try parser.domImplementationCreateHTMLDocument(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _hasFeature(_: *DOMImplementation) bool {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.Implementation" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let impl = document.implementation", "undefined" },
|
|
||||||
.{ "impl.createHTMLDocument();", "[object HTMLDocument]" },
|
|
||||||
.{ "const doc = impl.createHTMLDocument('foo');", "undefined" },
|
|
||||||
.{ "doc", "[object HTMLDocument]" },
|
|
||||||
.{ "doc.title", "foo" },
|
|
||||||
.{ "doc.body", "[object HTMLBodyElement]" },
|
|
||||||
.{ "impl.createDocument(null, 'foo');", "[object Document]" },
|
|
||||||
.{ "impl.createDocumentType('foo', 'bar', 'baz')", "[object DocumentType]" },
|
|
||||||
.{ "impl.hasFeature()", "true" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Element = @import("element.zig").Element;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
IntersectionObserver,
|
|
||||||
IntersectionObserverEntry,
|
|
||||||
};
|
|
||||||
|
|
||||||
// This is supposed to listen to change between the root and observation targets.
|
|
||||||
// However, our rendered stores everything as 1 pixel sized boxes in a long row that never changes.
|
|
||||||
// As such, there are no changes to intersections between the root and any target.
|
|
||||||
// Instead we keep a list of all entries that are being observed.
|
|
||||||
// The callback is called with all entries everytime a new entry is added(observed).
|
|
||||||
// Potentially we should also call the callback at a regular interval.
|
|
||||||
// The returned Entries are phony, they always indicate full intersection.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
|
|
||||||
pub const IntersectionObserver = struct {
|
|
||||||
page: *Page,
|
|
||||||
callback: Env.Function,
|
|
||||||
options: IntersectionObserverOptions,
|
|
||||||
|
|
||||||
observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry),
|
|
||||||
|
|
||||||
// new IntersectionObserver(callback)
|
|
||||||
// new IntersectionObserver(callback, options) [not supported yet]
|
|
||||||
pub fn constructor(callback: Env.Function, options_: ?IntersectionObserverOptions, page: *Page) !IntersectionObserver {
|
|
||||||
var options = IntersectionObserverOptions{
|
|
||||||
.root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
|
|
||||||
.rootMargin = "0px 0px 0px 0px",
|
|
||||||
.threshold = .{ .single = 0.0 },
|
|
||||||
};
|
|
||||||
if (options_) |*o| {
|
|
||||||
if (o.root) |root| {
|
|
||||||
options.root = root;
|
|
||||||
} // Other properties are not used due to the way we render
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.page = page,
|
|
||||||
.callback = callback,
|
|
||||||
.options = options,
|
|
||||||
.observed_entries = .{},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _disconnect(self: *IntersectionObserver) !void {
|
|
||||||
self.observed_entries = .{}; // We don't free as it is on an arena
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element) !void {
|
|
||||||
for (self.observed_entries.items) |*observer| {
|
|
||||||
if (observer.target == target_element) {
|
|
||||||
return; // Already observed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.observed_entries.append(self.page.arena, .{
|
|
||||||
.page = self.page,
|
|
||||||
.target = target_element,
|
|
||||||
.options = &self.options,
|
|
||||||
});
|
|
||||||
|
|
||||||
var result: Env.Function.Result = undefined;
|
|
||||||
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
|
|
||||||
log.debug(.user_script, "callback error", .{
|
|
||||||
.err = result.exception,
|
|
||||||
.stack = result.stack,
|
|
||||||
.source = "intersection observer",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
|
||||||
for (self.observed_entries.items, 0..) |*observer, index| {
|
|
||||||
if (observer.target == target) {
|
|
||||||
_ = self.observed_entries.swapRemove(index);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _takeRecords(self: *IntersectionObserver) []IntersectionObserverEntry {
|
|
||||||
return self.observed_entries.items;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const IntersectionObserverOptions = struct {
|
|
||||||
root: ?*parser.Node, // Element or Document
|
|
||||||
rootMargin: ?[]const u8,
|
|
||||||
threshold: ?Threshold,
|
|
||||||
|
|
||||||
const Threshold = union(enum) {
|
|
||||||
single: f32,
|
|
||||||
list: []const f32,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
|
|
||||||
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
|
||||||
pub const IntersectionObserverEntry = struct {
|
|
||||||
page: *Page,
|
|
||||||
target: *parser.Element,
|
|
||||||
options: *IntersectionObserverOptions,
|
|
||||||
|
|
||||||
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
|
|
||||||
pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
|
||||||
return Element._getBoundingClientRect(self.target, self.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the ratio of the intersectionRect to the boundingClientRect.
|
|
||||||
pub fn get_intersectionRatio(_: *const IntersectionObserverEntry) f32 {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a DOMRectReadOnly representing the target's visible area.
|
|
||||||
pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
|
||||||
return Element._getBoundingClientRect(self.target, self.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A Boolean value which is true if the target element intersects with the
|
|
||||||
// intersection observer's root. If this is true, then, the
|
|
||||||
// IntersectionObserverEntry describes a transition into a state of
|
|
||||||
// intersection; if it's false, then you know the transition is from
|
|
||||||
// intersecting to not-intersecting.
|
|
||||||
pub fn get_isIntersecting(_: *const IntersectionObserverEntry) bool {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a DOMRectReadOnly for the intersection observer's root.
|
|
||||||
pub fn get_rootBounds(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
|
||||||
const root = self.options.root.?;
|
|
||||||
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
|
|
||||||
return self.page.renderer.boundingRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
const root_type = try parser.nodeType(root);
|
|
||||||
|
|
||||||
var element: *parser.Element = undefined;
|
|
||||||
switch (root_type) {
|
|
||||||
.element => element = parser.nodeToElement(root),
|
|
||||||
.document => {
|
|
||||||
const doc = parser.nodeToDocument(root);
|
|
||||||
element = (try parser.documentGetDocumentElement(doc)).?;
|
|
||||||
},
|
|
||||||
else => return error.InvalidState,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Element._getBoundingClientRect(element, self.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Element whose intersection with the root changed.
|
|
||||||
pub fn get_target(self: *const IntersectionObserverEntry) *parser.Element {
|
|
||||||
return self.target;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: pub fn get_time(self: *const IntersectionObserverEntry)
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.IntersectionObserver" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "new IntersectionObserver(() => {}).observe(document.documentElement);", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let count_a = 0;", "undefined" },
|
|
||||||
.{ "const a1 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "new IntersectionObserver(entries => {count_a += 1;}).observe(a1);", "undefined" },
|
|
||||||
.{ "count_a;", "1" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// This test is documenting current behavior, not correct behavior.
|
|
||||||
// Currently every time observe is called, the callback is called with all entries.
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let count_b = 0;", "undefined" },
|
|
||||||
.{ "let observer_b = new IntersectionObserver(entries => {count_b = entries.length;});", "undefined" },
|
|
||||||
.{ "const b1 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "observer_b.observe(b1);", "undefined" },
|
|
||||||
.{ "count_b;", "1" },
|
|
||||||
.{ "const b2 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "observer_b.observe(b2);", "undefined" },
|
|
||||||
.{ "count_b;", "2" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// Re-observing is a no-op
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let count_bb = 0;", "undefined" },
|
|
||||||
.{ "let observer_bb = new IntersectionObserver(entries => {count_bb = entries.length;});", "undefined" },
|
|
||||||
.{ "const bb1 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "observer_bb.observe(bb1);", "undefined" },
|
|
||||||
.{ "count_bb;", "1" },
|
|
||||||
.{ "observer_bb.observe(bb1);", "undefined" },
|
|
||||||
.{ "count_bb;", "1" }, // Still 1, not 2
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// Unobserve
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let count_c = 0;", "undefined" },
|
|
||||||
.{ "let observer_c = new IntersectionObserver(entries => { count_c = entries.length;});", "undefined" },
|
|
||||||
.{ "const c1 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "observer_c.observe(c1);", "undefined" },
|
|
||||||
.{ "count_c;", "1" },
|
|
||||||
.{ "observer_c.unobserve(c1);", "undefined" },
|
|
||||||
.{ "const c2 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "observer_c.observe(c2);", "undefined" },
|
|
||||||
.{ "count_c;", "1" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// Disconnect
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let observer_d = new IntersectionObserver(entries => {});", "undefined" },
|
|
||||||
.{ "let d1 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "observer_d.observe(d1);", "undefined" },
|
|
||||||
.{ "observer_d.disconnect();", "undefined" },
|
|
||||||
.{ "observer_d.takeRecords().length;", "0" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// takeRecords
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let observer_e = new IntersectionObserver(entries => {});", "undefined" },
|
|
||||||
.{ "let e1 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "observer_e.observe(e1);", "undefined" },
|
|
||||||
.{ "const e2 = document.createElement('div');", "undefined" },
|
|
||||||
.{ "observer_e.observe(e2);", "undefined" },
|
|
||||||
.{ "observer_e.takeRecords().length;", "2" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// Entry
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let entry;", "undefined" },
|
|
||||||
.{ "let div1 = document.createElement('div')", null },
|
|
||||||
.{ "document.body.appendChild(div1);", null },
|
|
||||||
.{ "new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);", null },
|
|
||||||
.{ "entry.boundingClientRect.x;", "0" },
|
|
||||||
.{ "entry.intersectionRatio;", "1" },
|
|
||||||
.{ "entry.intersectionRect.x;", "0" },
|
|
||||||
.{ "entry.intersectionRect.y;", "0" },
|
|
||||||
.{ "entry.intersectionRect.width;", "1" },
|
|
||||||
.{ "entry.intersectionRect.height;", "1" },
|
|
||||||
.{ "entry.isIntersecting;", "true" },
|
|
||||||
.{ "entry.rootBounds.x;", "0" },
|
|
||||||
.{ "entry.rootBounds.y;", "0" },
|
|
||||||
.{ "entry.rootBounds.width;", "1" },
|
|
||||||
.{ "entry.rootBounds.height;", "1" },
|
|
||||||
.{ "entry.target;", "[object HTMLDivElement]" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// Options
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const new_root = document.createElement('span');", null },
|
|
||||||
.{ "document.body.appendChild(new_root);", null },
|
|
||||||
.{ "let new_entry;", "undefined" },
|
|
||||||
.{
|
|
||||||
\\ const new_observer = new IntersectionObserver(
|
|
||||||
\\ entries => { new_entry = entries[0]; },
|
|
||||||
\\ {root: new_root, rootMargin: '0px 0px 0px 0px', threshold: [0]});
|
|
||||||
,
|
|
||||||
"undefined",
|
|
||||||
},
|
|
||||||
.{ "new_observer.observe(document.createElement('div'));", "undefined" },
|
|
||||||
.{ "new_entry.rootBounds.x;", "1" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,436 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const NodeList = @import("nodelist.zig").NodeList;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
MutationObserver,
|
|
||||||
MutationRecord,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Walker = @import("../dom/walker.zig").WalkerChildren;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
|
||||||
pub const MutationObserver = struct {
|
|
||||||
loop: *Loop,
|
|
||||||
cbk: Env.Function,
|
|
||||||
arena: Allocator,
|
|
||||||
connected: bool,
|
|
||||||
scheduled: bool,
|
|
||||||
loop_node: Loop.CallbackNode,
|
|
||||||
|
|
||||||
// List of records which were observed. When the call scope ends, we need to
|
|
||||||
// execute our callback with it.
|
|
||||||
observed: std.ArrayListUnmanaged(MutationRecord),
|
|
||||||
|
|
||||||
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
|
|
||||||
return .{
|
|
||||||
.cbk = cbk,
|
|
||||||
.loop = page.loop,
|
|
||||||
.observed = .{},
|
|
||||||
.connected = true,
|
|
||||||
.scheduled = false,
|
|
||||||
.arena = page.arena,
|
|
||||||
.loop_node = .{ .func = callback },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?Options) !void {
|
|
||||||
const arena = self.arena;
|
|
||||||
var options = options_ orelse Options{};
|
|
||||||
if (options.attributeFilter.len > 0) {
|
|
||||||
options.attributeFilter = try arena.dupe([]const u8, options.attributeFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = try arena.create(Observer);
|
|
||||||
observer.* = .{
|
|
||||||
.node = node,
|
|
||||||
.options = options,
|
|
||||||
.mutation_observer = self,
|
|
||||||
.event_node = .{ .id = self.cbk.id, .func = Observer.handle },
|
|
||||||
};
|
|
||||||
|
|
||||||
// register node's events
|
|
||||||
if (options.childList or options.subtree) {
|
|
||||||
_ = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, node),
|
|
||||||
"DOMNodeInserted",
|
|
||||||
&observer.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
_ = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, node),
|
|
||||||
"DOMNodeRemoved",
|
|
||||||
&observer.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (options.attr()) {
|
|
||||||
_ = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, node),
|
|
||||||
"DOMAttrModified",
|
|
||||||
&observer.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (options.cdata()) {
|
|
||||||
_ = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, node),
|
|
||||||
"DOMCharacterDataModified",
|
|
||||||
&observer.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (options.subtree) {
|
|
||||||
_ = try parser.eventTargetAddEventListener(
|
|
||||||
parser.toEventTarget(parser.Node, node),
|
|
||||||
"DOMSubtreeModified",
|
|
||||||
&observer.event_node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn callback(node: *Loop.CallbackNode, _: *?u63) void {
|
|
||||||
const self: *MutationObserver = @fieldParentPtr("loop_node", node);
|
|
||||||
if (self.connected == false) {
|
|
||||||
self.scheduled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.scheduled = false;
|
|
||||||
|
|
||||||
const records = self.observed.items;
|
|
||||||
if (records.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
defer self.observed.clearRetainingCapacity();
|
|
||||||
|
|
||||||
var result: Env.Function.Result = undefined;
|
|
||||||
self.cbk.tryCall(void, .{records}, &result) catch {
|
|
||||||
log.debug(.user_script, "callback error", .{
|
|
||||||
.err = result.exception,
|
|
||||||
.stack = result.stack,
|
|
||||||
.source = "mutation observer",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
pub fn _disconnect(self: *MutationObserver) !void {
|
|
||||||
self.connected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
pub fn _takeRecords(_: *const MutationObserver) ?[]const u8 {
|
|
||||||
return &[_]u8{};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const MutationRecord = struct {
|
|
||||||
type: []const u8,
|
|
||||||
target: *parser.Node,
|
|
||||||
added_nodes: NodeList = .{},
|
|
||||||
removed_nodes: NodeList = .{},
|
|
||||||
previous_sibling: ?*parser.Node = null,
|
|
||||||
next_sibling: ?*parser.Node = null,
|
|
||||||
attribute_name: ?[]const u8 = null,
|
|
||||||
attribute_namespace: ?[]const u8 = null,
|
|
||||||
old_value: ?[]const u8 = null,
|
|
||||||
|
|
||||||
pub fn get_type(self: *const MutationRecord) []const u8 {
|
|
||||||
return self.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_addedNodes(self: *MutationRecord) *NodeList {
|
|
||||||
return &self.added_nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_removedNodes(self: *MutationRecord) *NodeList {
|
|
||||||
return &self.removed_nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_target(self: *const MutationRecord) *parser.Node {
|
|
||||||
return self.target;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_attributeName(self: *const MutationRecord) ?[]const u8 {
|
|
||||||
return self.attribute_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_attributeNamespace(self: *const MutationRecord) ?[]const u8 {
|
|
||||||
return self.attribute_namespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_previousSibling(self: *const MutationRecord) ?*parser.Node {
|
|
||||||
return self.previous_sibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_nextSibling(self: *const MutationRecord) ?*parser.Node {
|
|
||||||
return self.next_sibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_oldValue(self: *const MutationRecord) ?[]const u8 {
|
|
||||||
return self.old_value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Options = struct {
|
|
||||||
childList: bool = false,
|
|
||||||
attributes: bool = false,
|
|
||||||
characterData: bool = false,
|
|
||||||
subtree: bool = false,
|
|
||||||
attributeOldValue: bool = false,
|
|
||||||
characterDataOldValue: bool = false,
|
|
||||||
attributeFilter: [][]const u8 = &.{},
|
|
||||||
|
|
||||||
fn attr(self: Options) bool {
|
|
||||||
return self.attributes or self.attributeOldValue or self.attributeFilter.len > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cdata(self: Options) bool {
|
|
||||||
return self.characterData or self.characterDataOldValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Observer = struct {
|
|
||||||
node: *parser.Node,
|
|
||||||
options: Options,
|
|
||||||
|
|
||||||
// reference back to the MutationObserver so that we can access the arena
|
|
||||||
// and batch the mutation records.
|
|
||||||
mutation_observer: *MutationObserver,
|
|
||||||
|
|
||||||
event_node: parser.EventNode,
|
|
||||||
|
|
||||||
fn appliesTo(
|
|
||||||
self: *const Observer,
|
|
||||||
target: *parser.Node,
|
|
||||||
event_type: MutationEventType,
|
|
||||||
event: *parser.MutationEvent,
|
|
||||||
) !bool {
|
|
||||||
if (event_type == .DOMAttrModified and self.options.attributeFilter.len > 0) {
|
|
||||||
const attribute_name = try parser.mutationEventAttributeName(event);
|
|
||||||
for (self.options.attributeFilter) |needle| blk: {
|
|
||||||
if (std.mem.eql(u8, attribute_name, needle)) {
|
|
||||||
break :blk;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// mutation on any target is always ok.
|
|
||||||
if (self.options.subtree) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if target equals node, alway ok.
|
|
||||||
if (target == self.node) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// no subtree, no same target and no childlist, always noky.
|
|
||||||
if (!self.options.childList) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// target must be a child of o.node
|
|
||||||
const walker = Walker{};
|
|
||||||
var next: ?*parser.Node = null;
|
|
||||||
while (true) {
|
|
||||||
next = walker.get_next(self.node, next) catch break orelse break;
|
|
||||||
if (next.? == target) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle(en: *parser.EventNode, event: *parser.Event) void {
|
|
||||||
const self: *Observer = @fieldParentPtr("event_node", en);
|
|
||||||
self._handle(event) catch |err| {
|
|
||||||
log.err(.web_api, "handle error", .{ .err = err, .source = "mutation observer" });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _handle(self: *Observer, event: *parser.Event) !void {
|
|
||||||
var mutation_observer = self.mutation_observer;
|
|
||||||
|
|
||||||
const node = blk: {
|
|
||||||
const event_target = try parser.eventTarget(event) orelse return;
|
|
||||||
break :blk parser.eventTargetToNode(event_target);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mutation_event = parser.eventToMutationEvent(event);
|
|
||||||
const event_type = blk: {
|
|
||||||
const t = try parser.eventType(event);
|
|
||||||
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (try self.appliesTo(node, event_type, mutation_event) == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var record = MutationRecord{
|
|
||||||
.target = self.node,
|
|
||||||
.type = event_type.recordType(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const arena = mutation_observer.arena;
|
|
||||||
switch (event_type) {
|
|
||||||
.DOMAttrModified => {
|
|
||||||
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
|
||||||
if (self.options.attributeOldValue) {
|
|
||||||
record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.DOMCharacterDataModified => {
|
|
||||||
if (self.options.characterDataOldValue) {
|
|
||||||
record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.DOMNodeInserted => {
|
|
||||||
if (parser.mutationEventRelatedNode(mutation_event) catch null) |related_node| {
|
|
||||||
try record.added_nodes.append(arena, related_node);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.DOMNodeRemoved => {
|
|
||||||
if (parser.mutationEventRelatedNode(mutation_event) catch null) |related_node| {
|
|
||||||
try record.removed_nodes.append(arena, related_node);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
try mutation_observer.observed.append(arena, record);
|
|
||||||
|
|
||||||
if (mutation_observer.scheduled == false) {
|
|
||||||
mutation_observer.scheduled = true;
|
|
||||||
_ = try mutation_observer.loop.timeout(0, &mutation_observer.loop_node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const MutationEventType = enum {
|
|
||||||
DOMAttrModified,
|
|
||||||
DOMCharacterDataModified,
|
|
||||||
DOMNodeInserted,
|
|
||||||
DOMNodeRemoved,
|
|
||||||
|
|
||||||
fn recordType(self: MutationEventType) []const u8 {
|
|
||||||
return switch (self) {
|
|
||||||
.DOMAttrModified => "attributes",
|
|
||||||
.DOMCharacterDataModified => "characterData",
|
|
||||||
.DOMNodeInserted => "childList",
|
|
||||||
.DOMNodeRemoved => "childList",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.MutationObserver" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "new MutationObserver(() => {}).observe(document, { childList: true });", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{
|
|
||||||
\\ var nb = 0;
|
|
||||||
\\ var mrs;
|
|
||||||
\\ new MutationObserver((mu) => {
|
|
||||||
\\ mrs = mu;
|
|
||||||
\\ nb++;
|
|
||||||
\\ }).observe(document.firstElementChild, { attributes: true, attributeOldValue: true });
|
|
||||||
\\ document.firstElementChild.setAttribute("foo", "bar");
|
|
||||||
\\ // ignored b/c it's about another target.
|
|
||||||
\\ document.firstElementChild.firstChild.setAttribute("foo", "bar");
|
|
||||||
,
|
|
||||||
null,
|
|
||||||
},
|
|
||||||
.{ "nb", "1" },
|
|
||||||
.{ "mrs[0].type", "attributes" },
|
|
||||||
.{ "mrs[0].target == document.firstElementChild", "true" },
|
|
||||||
.{ "mrs[0].target.getAttribute('foo')", "bar" },
|
|
||||||
.{ "mrs[0].attributeName", "foo" },
|
|
||||||
.{ "mrs[0].oldValue", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{
|
|
||||||
\\ var node = document.getElementById("para").firstChild;
|
|
||||||
\\ var nb2 = 0;
|
|
||||||
\\ var mrs2;
|
|
||||||
\\ new MutationObserver((mu) => {
|
|
||||||
\\ mrs2 = mu;
|
|
||||||
\\ nb2++;
|
|
||||||
\\ }).observe(node, { characterData: true, characterDataOldValue: true });
|
|
||||||
\\ node.data = "foo";
|
|
||||||
,
|
|
||||||
null,
|
|
||||||
},
|
|
||||||
.{ "nb2", "1" },
|
|
||||||
.{ "mrs2[0].type", "characterData" },
|
|
||||||
.{ "mrs2[0].target == node", "true" },
|
|
||||||
.{ "mrs2[0].target.data", "foo" },
|
|
||||||
.{ "mrs2[0].oldValue", " And" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// tests that mutation observers that have a callback which trigger the
|
|
||||||
// mutation observer don't crash.
|
|
||||||
// https://github.com/lightpanda-io/browser/issues/550
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{
|
|
||||||
\\ var node = document.getElementById("para");
|
|
||||||
\\ new MutationObserver(() => {
|
|
||||||
\\ node.innerText = 'a';
|
|
||||||
\\ }).observe(document, { subtree:true,childList:true });
|
|
||||||
\\ node.innerText = "2";
|
|
||||||
,
|
|
||||||
null,
|
|
||||||
},
|
|
||||||
.{ "node.innerText", "a" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{
|
|
||||||
\\ var node = document.getElementById("para");
|
|
||||||
\\ var attrWatch = 0;
|
|
||||||
\\ new MutationObserver(() => {
|
|
||||||
\\ attrWatch++;
|
|
||||||
\\ }).observe(document, { attributeFilter: ["name"], subtree: true });
|
|
||||||
\\ node.setAttribute("id", "1");
|
|
||||||
,
|
|
||||||
null,
|
|
||||||
},
|
|
||||||
.{ "attrWatch", "0" },
|
|
||||||
.{ "node.setAttribute('name', 'other');", null },
|
|
||||||
.{ "attrWatch", "1" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#namednodemap
|
|
||||||
pub const NamedNodeMap = struct {
|
|
||||||
pub const Self = parser.NamedNodeMap;
|
|
||||||
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
pub const Iterator = NamedNodeMapIterator;
|
|
||||||
|
|
||||||
// TODO implement LegacyUnenumerableNamedProperties.
|
|
||||||
// https://webidl.spec.whatwg.org/#LegacyUnenumerableNamedProperties
|
|
||||||
|
|
||||||
pub fn get_length(self: *parser.NamedNodeMap) !u32 {
|
|
||||||
return try parser.namedNodeMapGetLength(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _item(self: *parser.NamedNodeMap, index: u32) !?*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapItem(self, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getNamedItem(self: *parser.NamedNodeMap, qname: []const u8) !?*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapGetNamedItem(self, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getNamedItemNS(
|
|
||||||
self: *parser.NamedNodeMap,
|
|
||||||
namespace: []const u8,
|
|
||||||
localname: []const u8,
|
|
||||||
) !?*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapGetNamedItemNS(self, namespace, localname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setNamedItem(self: *parser.NamedNodeMap, attr: *parser.Attribute) !?*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapSetNamedItem(self, attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setNamedItemNS(self: *parser.NamedNodeMap, attr: *parser.Attribute) !?*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapSetNamedItemNS(self, attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeNamedItem(self: *parser.NamedNodeMap, qname: []const u8) !*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapRemoveNamedItem(self, qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeNamedItemNS(
|
|
||||||
self: *parser.NamedNodeMap,
|
|
||||||
namespace: []const u8,
|
|
||||||
localname: []const u8,
|
|
||||||
) !*parser.Attribute {
|
|
||||||
return try parser.namedNodeMapRemoveNamedItemNS(self, namespace, localname);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn indexed_get(self: *parser.NamedNodeMap, index: u32, has_value: *bool) !*parser.Attribute {
|
|
||||||
return (try _item(self, index)) orelse {
|
|
||||||
has_value.* = false;
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn named_get(self: *parser.NamedNodeMap, name: []const u8, has_value: *bool) !*parser.Attribute {
|
|
||||||
return (try _getNamedItem(self, name)) orelse {
|
|
||||||
has_value.* = false;
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _symbol_iterator(self: *parser.NamedNodeMap) NamedNodeMapIterator {
|
|
||||||
return .{ .map = self };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const NamedNodeMapIterator = struct {
|
|
||||||
index: u32 = 0,
|
|
||||||
map: *parser.NamedNodeMap,
|
|
||||||
|
|
||||||
pub const Return = struct {
|
|
||||||
done: bool,
|
|
||||||
value: ?*parser.Attribute,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _next(self: *NamedNodeMapIterator) !Return {
|
|
||||||
const e = try NamedNodeMap._item(self.map, self.index);
|
|
||||||
if (e == null) {
|
|
||||||
return .{
|
|
||||||
.value = null,
|
|
||||||
.done = true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.index += 1;
|
|
||||||
return .{
|
|
||||||
.value = e,
|
|
||||||
.done = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.NamedNodeMap" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let a = document.getElementById('content').attributes", "undefined" },
|
|
||||||
.{ "a.length", "1" },
|
|
||||||
.{ "a.item(0)", "[object Attr]" },
|
|
||||||
.{ "a.item(1)", "null" },
|
|
||||||
.{ "a.getNamedItem('id')", "[object Attr]" },
|
|
||||||
.{ "a.getNamedItem('foo')", "null" },
|
|
||||||
.{ "a.setNamedItem(a.getNamedItem('id'))", "[object Attr]" },
|
|
||||||
.{ "a['id'].name", "id" },
|
|
||||||
.{ "a['id'].value", "content" },
|
|
||||||
.{ "a['other']", "undefined" },
|
|
||||||
.{ "a[0].value = 'abc123'", null },
|
|
||||||
.{ "a[0].value", "abc123" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,722 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const generate = @import("../../runtime/generate.zig");
|
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const EventTarget = @import("event_target.zig").EventTarget;
|
|
||||||
|
|
||||||
// DOM
|
|
||||||
const Attr = @import("attribute.zig").Attr;
|
|
||||||
const CData = @import("character_data.zig");
|
|
||||||
const Element = @import("element.zig").Element;
|
|
||||||
const NodeList = @import("nodelist.zig").NodeList;
|
|
||||||
const Document = @import("document.zig").Document;
|
|
||||||
const DocumentType = @import("document_type.zig").DocumentType;
|
|
||||||
const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
|
|
||||||
const HTMLCollection = @import("html_collection.zig").HTMLCollection;
|
|
||||||
const HTMLAllCollection = @import("html_collection.zig").HTMLAllCollection;
|
|
||||||
const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator;
|
|
||||||
const Walker = @import("walker.zig").WalkerDepthFirst;
|
|
||||||
|
|
||||||
// HTML
|
|
||||||
const HTML = @import("../html/html.zig");
|
|
||||||
const HTMLElem = @import("../html/elements.zig");
|
|
||||||
|
|
||||||
// Node interfaces
|
|
||||||
pub const Interfaces = .{
|
|
||||||
Attr,
|
|
||||||
CData.CharacterData,
|
|
||||||
CData.Interfaces,
|
|
||||||
Element,
|
|
||||||
Document,
|
|
||||||
DocumentType,
|
|
||||||
DocumentFragment,
|
|
||||||
HTMLCollection,
|
|
||||||
HTMLAllCollection,
|
|
||||||
HTMLCollectionIterator,
|
|
||||||
HTML.Interfaces,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Union = generate.Union(Interfaces);
|
|
||||||
|
|
||||||
// Node implementation
|
|
||||||
pub const Node = struct {
|
|
||||||
pub const Self = parser.Node;
|
|
||||||
pub const prototype = *EventTarget;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn toInterface(node: *parser.Node) !Union {
|
|
||||||
return switch (try parser.nodeType(node)) {
|
|
||||||
.element => try HTMLElem.toInterface(
|
|
||||||
Union,
|
|
||||||
@as(*parser.Element, @ptrCast(node)),
|
|
||||||
),
|
|
||||||
.comment => .{ .Comment = @as(*parser.Comment, @ptrCast(node)) },
|
|
||||||
.text => .{ .Text = @as(*parser.Text, @ptrCast(node)) },
|
|
||||||
.cdata_section => .{ .CDATASection = @as(*parser.CDATASection, @ptrCast(node)) },
|
|
||||||
.processing_instruction => .{ .ProcessingInstruction = @as(*parser.ProcessingInstruction, @ptrCast(node)) },
|
|
||||||
.document => .{ .HTMLDocument = @as(*parser.DocumentHTML, @ptrCast(node)) },
|
|
||||||
.document_type => .{ .DocumentType = @as(*parser.DocumentType, @ptrCast(node)) },
|
|
||||||
.attribute => .{ .Attr = @as(*parser.Attribute, @ptrCast(node)) },
|
|
||||||
.document_fragment => .{ .DocumentFragment = @as(*parser.DocumentFragment, @ptrCast(node)) },
|
|
||||||
else => @panic("node type not handled"), // TODO
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// class attributes
|
|
||||||
|
|
||||||
pub const _ELEMENT_NODE = @intFromEnum(parser.NodeType.element);
|
|
||||||
pub const _ATTRIBUTE_NODE = @intFromEnum(parser.NodeType.attribute);
|
|
||||||
pub const _TEXT_NODE = @intFromEnum(parser.NodeType.text);
|
|
||||||
pub const _CDATA_SECTION_NODE = @intFromEnum(parser.NodeType.cdata_section);
|
|
||||||
pub const _PROCESSING_INSTRUCTION_NODE = @intFromEnum(parser.NodeType.processing_instruction);
|
|
||||||
pub const _COMMENT_NODE = @intFromEnum(parser.NodeType.comment);
|
|
||||||
pub const _DOCUMENT_NODE = @intFromEnum(parser.NodeType.document);
|
|
||||||
pub const _DOCUMENT_TYPE_NODE = @intFromEnum(parser.NodeType.document_type);
|
|
||||||
pub const _DOCUMENT_FRAGMENT_NODE = @intFromEnum(parser.NodeType.document_fragment);
|
|
||||||
|
|
||||||
// These 3 are deprecated, but both Chrome and Firefox still expose them
|
|
||||||
pub const _ENTITY_REFERENCE_NODE = @intFromEnum(parser.NodeType.entity_reference);
|
|
||||||
pub const _ENTITY_NODE = @intFromEnum(parser.NodeType.entity);
|
|
||||||
pub const _NOTATION_NODE = @intFromEnum(parser.NodeType.notation);
|
|
||||||
|
|
||||||
// JS funcs
|
|
||||||
// --------
|
|
||||||
|
|
||||||
// Read-only attributes
|
|
||||||
|
|
||||||
pub fn get_firstChild(self: *parser.Node) !?Union {
|
|
||||||
const res = try parser.nodeFirstChild(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Node.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_lastChild(self: *parser.Node) !?Union {
|
|
||||||
const res = try parser.nodeLastChild(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Node.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_nextSibling(self: *parser.Node) !?Union {
|
|
||||||
const res = try parser.nodeNextSibling(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Node.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_previousSibling(self: *parser.Node) !?Union {
|
|
||||||
const res = try parser.nodePreviousSibling(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Node.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_parentNode(self: *parser.Node) !?Union {
|
|
||||||
const res = try parser.nodeParentNode(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try Node.toInterface(res.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_parentElement(self: *parser.Node) !?HTMLElem.Union {
|
|
||||||
const res = try parser.nodeParentElement(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try HTMLElem.toInterface(HTMLElem.Union, @as(*parser.Element, @ptrCast(res.?)));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_nodeName(self: *parser.Node) ![]const u8 {
|
|
||||||
return try parser.nodeName(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_nodeType(self: *parser.Node) !u8 {
|
|
||||||
return @intFromEnum(try parser.nodeType(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ownerDocument(self: *parser.Node) !?*parser.DocumentHTML {
|
|
||||||
const res = try parser.nodeOwnerDocument(self);
|
|
||||||
if (res == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return @as(*parser.DocumentHTML, @ptrCast(res.?));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_isConnected(self: *parser.Node) !bool {
|
|
||||||
// TODO: handle Shadow DOM
|
|
||||||
if (try parser.nodeType(self) == .document) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return try Node.get_parentNode(self) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read/Write attributes
|
|
||||||
|
|
||||||
pub fn get_nodeValue(self: *parser.Node) !?[]const u8 {
|
|
||||||
return try parser.nodeValue(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_nodeValue(self: *parser.Node, data: []u8) !void {
|
|
||||||
try parser.nodeSetValue(self, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_textContent(self: *parser.Node) !?[]const u8 {
|
|
||||||
return try parser.nodeTextContent(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_textContent(self: *parser.Node, data: []u8) !void {
|
|
||||||
return try parser.nodeSetTextContent(self, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
|
|
||||||
pub fn _appendChild(self: *parser.Node, child: *parser.Node) !Union {
|
|
||||||
// TODO: DocumentFragment special case
|
|
||||||
const res = try parser.nodeAppendChild(self, child);
|
|
||||||
return try Node.toInterface(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _cloneNode(self: *parser.Node, deep: ?bool) !Union {
|
|
||||||
const clone = try parser.nodeCloneNode(self, deep orelse false);
|
|
||||||
return try Node.toInterface(clone);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) !u32 {
|
|
||||||
if (self == other) return 0;
|
|
||||||
|
|
||||||
const docself = try parser.nodeOwnerDocument(self);
|
|
||||||
const docother = try parser.nodeOwnerDocument(other);
|
|
||||||
|
|
||||||
// Both are in different document.
|
|
||||||
if (docself == null or docother == null or docother.? != docself.?) {
|
|
||||||
return @intFromEnum(parser.DocumentPosition.disconnected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Both are in a different trees in the same document.
|
|
||||||
|
|
||||||
const w = Walker{};
|
|
||||||
var next: ?*parser.Node = null;
|
|
||||||
|
|
||||||
// Is other a descendant of self?
|
|
||||||
while (true) {
|
|
||||||
next = try w.get_next(self, next) orelse break;
|
|
||||||
if (other == next) {
|
|
||||||
return @intFromEnum(parser.DocumentPosition.following) +
|
|
||||||
@intFromEnum(parser.DocumentPosition.contained_by);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is self a descendant of other?
|
|
||||||
next = null;
|
|
||||||
while (true) {
|
|
||||||
next = try w.get_next(other, next) orelse break;
|
|
||||||
if (self == next) {
|
|
||||||
return @intFromEnum(parser.DocumentPosition.contains) +
|
|
||||||
@intFromEnum(parser.DocumentPosition.preceding);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next = null;
|
|
||||||
while (true) {
|
|
||||||
next = try w.get_next(parser.documentToNode(docself.?), next) orelse break;
|
|
||||||
if (other == next) {
|
|
||||||
// other precedes self.
|
|
||||||
return @intFromEnum(parser.DocumentPosition.preceding);
|
|
||||||
}
|
|
||||||
if (self == next) {
|
|
||||||
// other follows self.
|
|
||||||
return @intFromEnum(parser.DocumentPosition.following);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _contains(self: *parser.Node, other: *parser.Node) !bool {
|
|
||||||
return try parser.nodeContains(self, other);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns itself or ancestor object inheriting from Node.
|
|
||||||
// - An Element inside a standard web page will return an HTMLDocument object representing the entire page (or <iframe>).
|
|
||||||
// - An Element inside a shadow DOM will return the associated ShadowRoot.
|
|
||||||
// - An Element that is not attached to a document or a shadow tree will return the root of the DOM tree it belongs to
|
|
||||||
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }) !Union {
|
|
||||||
if (options) |options_| if (options_.composed) {
|
|
||||||
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
|
|
||||||
};
|
|
||||||
return try Node.toInterface(try parser.nodeGetRootNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _hasChildNodes(self: *parser.Node) !bool {
|
|
||||||
return try parser.nodeHasChildNodes(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
|
|
||||||
const allocator = page.arena;
|
|
||||||
var list: NodeList = .{};
|
|
||||||
|
|
||||||
var n = try parser.nodeFirstChild(self) orelse return list;
|
|
||||||
while (true) {
|
|
||||||
try list.append(allocator, n);
|
|
||||||
n = try parser.nodeNextSibling(n) orelse return list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _insertBefore(self: *parser.Node, new_node: *parser.Node, ref_node_: ?*parser.Node) !Union {
|
|
||||||
if (ref_node_) |ref_node| {
|
|
||||||
return Node.toInterface(try parser.nodeInsertBefore(self, new_node, ref_node));
|
|
||||||
}
|
|
||||||
return _appendChild(self, new_node);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
|
|
||||||
return try parser.nodeIsDefaultNamespace(self, namespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _isEqualNode(self: *parser.Node, other: *parser.Node) !bool {
|
|
||||||
// TODO: other is not an optional parameter, but can be null.
|
|
||||||
return try parser.nodeIsEqualNode(self, other);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _isSameNode(self: *parser.Node, other: *parser.Node) !bool {
|
|
||||||
// TODO: other is not an optional parameter, but can be null.
|
|
||||||
// NOTE: there is no need to use isSameNode(); instead use the === strict equality operator
|
|
||||||
return try parser.nodeIsSameNode(self, other);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _lookupPrefix(self: *parser.Node, namespace: ?[]const u8) !?[]const u8 {
|
|
||||||
// TODO: other is not an optional parameter, but can be null.
|
|
||||||
if (namespace == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, namespace.?, "")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return try parser.nodeLookupPrefix(self, namespace.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _lookupNamespaceURI(self: *parser.Node, prefix: ?[]const u8) !?[]const u8 {
|
|
||||||
// TODO: other is not an optional parameter, but can be null.
|
|
||||||
return try parser.nodeLookupNamespaceURI(self, prefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _normalize(self: *parser.Node) !void {
|
|
||||||
return try parser.nodeNormalize(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _removeChild(self: *parser.Node, child: *parser.Node) !Union {
|
|
||||||
const res = try parser.nodeRemoveChild(self, child);
|
|
||||||
return try Node.toInterface(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _replaceChild(self: *parser.Node, new_child: *parser.Node, old_child: *parser.Node) !Union {
|
|
||||||
const res = try parser.nodeReplaceChild(self, new_child, old_child);
|
|
||||||
return try Node.toInterface(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the hierarchy node tree constraints are respected.
|
|
||||||
// For now, it checks only if new nodes are not self.
|
|
||||||
// TODO implements the others contraints.
|
|
||||||
// see https://dom.spec.whatwg.org/#concept-node-tree
|
|
||||||
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
|
|
||||||
for (nodes) |n| {
|
|
||||||
if (n.is(self)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prepend(self: *parser.Node, nodes: []const NodeOrText) !void {
|
|
||||||
if (nodes.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check hierarchy
|
|
||||||
if (!hierarchy(self, nodes)) {
|
|
||||||
return parser.DOMError.HierarchyRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
const doc = (try parser.nodeOwnerDocument(self)) orelse return;
|
|
||||||
|
|
||||||
if (try parser.nodeFirstChild(self)) |first| {
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeInsertBefore(self, try node.toNode(doc), first);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeAppendChild(self, try node.toNode(doc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append(self: *parser.Node, nodes: []const NodeOrText) !void {
|
|
||||||
if (nodes.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check hierarchy
|
|
||||||
if (!hierarchy(self, nodes)) {
|
|
||||||
return parser.DOMError.HierarchyRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
const doc = (try parser.nodeOwnerDocument(self)) orelse return;
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeAppendChild(self, try node.toNode(doc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn replaceChildren(self: *parser.Node, nodes: []const NodeOrText) !void {
|
|
||||||
if (nodes.len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check hierarchy
|
|
||||||
if (!hierarchy(self, nodes)) {
|
|
||||||
return parser.DOMError.HierarchyRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove existing children
|
|
||||||
try removeChildren(self);
|
|
||||||
|
|
||||||
const doc = (try parser.nodeOwnerDocument(self)) orelse return;
|
|
||||||
// add new children
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeAppendChild(self, try node.toNode(doc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn removeChildren(self: *parser.Node) !void {
|
|
||||||
if (!try parser.nodeHasChildNodes(self)) return;
|
|
||||||
|
|
||||||
const children = try parser.nodeGetChildNodes(self);
|
|
||||||
const ln = try parser.nodeListLength(children);
|
|
||||||
var i: u32 = 0;
|
|
||||||
while (i < ln) {
|
|
||||||
defer i += 1;
|
|
||||||
// we always retrieve the 0 index child on purpose: libdom nodelist
|
|
||||||
// are dynamic. So the next child to remove is always as pos 0.
|
|
||||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
|
||||||
_ = try parser.nodeRemoveChild(self, child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn before(self: *parser.Node, nodes: []const NodeOrText) !void {
|
|
||||||
const parent = try parser.nodeParentNode(self) orelse return;
|
|
||||||
const doc = (try parser.nodeOwnerDocument(parent)) orelse return;
|
|
||||||
|
|
||||||
var sibling: ?*parser.Node = self;
|
|
||||||
// have to find the first sibling that isn't in nodes
|
|
||||||
CHECK: while (sibling) |s| {
|
|
||||||
for (nodes) |n| {
|
|
||||||
if (n.is(s)) {
|
|
||||||
sibling = try parser.nodePreviousSibling(s);
|
|
||||||
continue :CHECK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sibling == null) {
|
|
||||||
sibling = try parser.nodeFirstChild(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sibling) |ref_node| {
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeInsertBefore(parent, try node.toNode(doc), ref_node);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Node.prepend(self, nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn after(self: *parser.Node, nodes: []const NodeOrText) !void {
|
|
||||||
const parent = try parser.nodeParentNode(self) orelse return;
|
|
||||||
const doc = (try parser.nodeOwnerDocument(parent)) orelse return;
|
|
||||||
|
|
||||||
// have to find the first sibling that isn't in nodes
|
|
||||||
var sibling = try parser.nodeNextSibling(self);
|
|
||||||
CHECK: while (sibling) |s| {
|
|
||||||
for (nodes) |n| {
|
|
||||||
if (n.is(s)) {
|
|
||||||
sibling = try parser.nodeNextSibling(s);
|
|
||||||
continue :CHECK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sibling) |ref_node| {
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeInsertBefore(parent, try node.toNode(doc), ref_node);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (nodes) |node| {
|
|
||||||
_ = try parser.nodeAppendChild(parent, try node.toNode(doc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A lot of functions take either a node or text input.
|
|
||||||
// The text input is to be converted into a Text node.
|
|
||||||
pub const NodeOrText = union(enum) {
|
|
||||||
text: []const u8,
|
|
||||||
node: *parser.Node,
|
|
||||||
|
|
||||||
fn toNode(self: NodeOrText, doc: *parser.Document) !*parser.Node {
|
|
||||||
return switch (self) {
|
|
||||||
.node => |n| n,
|
|
||||||
.text => |txt| @alignCast(@ptrCast(try parser.documentCreateTextNode(doc, txt))),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whether the node represented by the NodeOrText is the same as the
|
|
||||||
// given Node. Always false for text values as these represent as-of-yet
|
|
||||||
// created Text nodes.
|
|
||||||
fn is(self: NodeOrText, other: *parser.Node) bool {
|
|
||||||
return switch (self) {
|
|
||||||
.text => false,
|
|
||||||
.node => |n| n == other,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.node" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
{
|
|
||||||
var err_out: ?[]const u8 = null;
|
|
||||||
try runner.exec(
|
|
||||||
\\ function trimAndReplace(str) {
|
|
||||||
\\ str = str.replace(/(\r\n|\n|\r)/gm,'');
|
|
||||||
\\ str = str.replace(/\s+/g, ' ');
|
|
||||||
\\ str = str.trim();
|
|
||||||
\\ return str;
|
|
||||||
\\ }
|
|
||||||
, "trimAndReplace", &err_out);
|
|
||||||
}
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.body.compareDocumentPosition(document.firstChild); ", "10" },
|
|
||||||
.{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", "10" },
|
|
||||||
.{ "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "20" },
|
|
||||||
.{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", "0" },
|
|
||||||
.{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", "2" },
|
|
||||||
.{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "4" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.getElementById('content').getRootNode().__proto__.constructor.name", "HTMLDocument" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
// for next test cases
|
|
||||||
.{ "let content = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "let link = document.getElementById('link')", "undefined" },
|
|
||||||
.{ "let first_child = content.firstChild.nextSibling", "undefined" }, // nextSibling because of line return \n
|
|
||||||
|
|
||||||
.{ "let body_first_child = document.body.firstChild", "undefined" },
|
|
||||||
.{ "body_first_child.localName", "div" },
|
|
||||||
.{ "body_first_child.__proto__.constructor.name", "HTMLDivElement" },
|
|
||||||
.{ "document.getElementById('para-empty').firstChild.firstChild", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let last_child = content.lastChild.previousSibling", "undefined" }, // previousSibling because of line return \n
|
|
||||||
.{ "last_child.__proto__.constructor.name", "Comment" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let next_sibling = link.nextSibling.nextSibling", "undefined" },
|
|
||||||
.{ "next_sibling.localName", "p" },
|
|
||||||
.{ "next_sibling.__proto__.constructor.name", "HTMLParagraphElement" },
|
|
||||||
.{ "content.nextSibling.nextSibling", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let prev_sibling = document.getElementById('para-empty').previousSibling.previousSibling", "undefined" },
|
|
||||||
.{ "prev_sibling.localName", "a" },
|
|
||||||
.{ "prev_sibling.__proto__.constructor.name", "HTMLAnchorElement" },
|
|
||||||
.{ "content.previousSibling", "null" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let parent = document.getElementById('para').parentElement", "undefined" },
|
|
||||||
.{ "parent.localName", "div" },
|
|
||||||
.{ "parent.__proto__.constructor.name", "HTMLDivElement" },
|
|
||||||
.{ "let h = content.parentElement.parentElement", "undefined" },
|
|
||||||
.{ "h.parentElement", "null" },
|
|
||||||
.{ "h.parentNode.__proto__.constructor.name", "HTMLDocument" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "first_child.nodeName === 'A'", "true" },
|
|
||||||
.{ "link.firstChild.nodeName === '#text'", "true" },
|
|
||||||
.{ "last_child.nodeName === '#comment'", "true" },
|
|
||||||
.{ "document.nodeName === '#document'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "first_child.nodeType === 1", "true" },
|
|
||||||
.{ "link.firstChild.nodeType === 3", "true" },
|
|
||||||
.{ "last_child.nodeType === 8", "true" },
|
|
||||||
.{ "document.nodeType === 9", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let owner = content.ownerDocument", "undefined" },
|
|
||||||
.{ "owner.__proto__.constructor.name", "HTMLDocument" },
|
|
||||||
.{ "document.ownerDocument", "null" },
|
|
||||||
.{ "let owner2 = document.createElement('div').ownerDocument", "undefined" },
|
|
||||||
.{ "owner2.__proto__.constructor.name", "HTMLDocument" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "content.isConnected", "true" },
|
|
||||||
.{ "document.isConnected", "true" },
|
|
||||||
.{ "document.createElement('div').isConnected", "false" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "last_child.nodeValue === 'comment'", "true" },
|
|
||||||
.{ "link.nodeValue === null", "true" },
|
|
||||||
.{ "let text = link.firstChild", "undefined" },
|
|
||||||
.{ "text.nodeValue === 'OK'", "true" },
|
|
||||||
.{ "text.nodeValue = 'OK modified'", "OK modified" },
|
|
||||||
.{ "text.nodeValue === 'OK modified'", "true" },
|
|
||||||
.{ "link.nodeValue = 'nothing'", "nothing" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "text.textContent === 'OK modified'", "true" },
|
|
||||||
.{ "trimAndReplace(content.textContent) === 'OK modified And'", "true" },
|
|
||||||
.{ "text.textContent = 'OK'", "OK" },
|
|
||||||
.{ "text.textContent", "OK" },
|
|
||||||
.{ "trimAndReplace(document.getElementById('para-empty').textContent)", "" },
|
|
||||||
.{ "document.getElementById('para-empty').textContent = 'OK'", "OK" },
|
|
||||||
.{ "document.getElementById('para-empty').firstChild.nodeName === '#text'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let append = document.createElement('h1')", "undefined" },
|
|
||||||
.{ "content.appendChild(append).toString()", "[object HTMLHeadingElement]" },
|
|
||||||
.{ "content.lastChild.__proto__.constructor.name", "HTMLHeadingElement" },
|
|
||||||
.{ "content.appendChild(link).toString()", "[object HTMLAnchorElement]" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let clone = link.cloneNode()", "undefined" },
|
|
||||||
.{ "clone.toString()", "[object HTMLAnchorElement]" },
|
|
||||||
.{ "clone.parentNode === null", "true" },
|
|
||||||
.{ "clone.firstChild === null", "true" },
|
|
||||||
.{ "let clone_deep = link.cloneNode(true)", "undefined" },
|
|
||||||
.{ "clone_deep.firstChild.nodeName === '#text'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "link.contains(text)", "true" },
|
|
||||||
.{ "text.contains(link)", "false" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "link.hasChildNodes()", "true" },
|
|
||||||
.{ "text.hasChildNodes()", "false" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "link.childNodes.length", "1" },
|
|
||||||
.{ "text.childNodes.length", "0" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let insertBefore = document.createElement('a')", "undefined" },
|
|
||||||
.{ "link.insertBefore(insertBefore, text) !== undefined", "true" },
|
|
||||||
.{ "link.firstChild.localName === 'a'", "true" },
|
|
||||||
|
|
||||||
.{ "let insertBefore2 = document.createElement('b')", null },
|
|
||||||
.{ "link.insertBefore(insertBefore2, null).localName", "b" },
|
|
||||||
.{ "link.childNodes[link.childNodes.length - 1].localName", "b" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
// TODO: does not seems to work
|
|
||||||
// .{ "link.isDefaultNamespace('')", "true" },
|
|
||||||
.{ "link.isDefaultNamespace('false')", "false" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let equal1 = document.createElement('a')", "undefined" },
|
|
||||||
.{ "let equal2 = document.createElement('a')", "undefined" },
|
|
||||||
.{ "equal1.textContent = 'is equal'", "is equal" },
|
|
||||||
.{ "equal2.textContent = 'is equal'", "is equal" },
|
|
||||||
// TODO: does not seems to work
|
|
||||||
// .{ "equal1.isEqualNode(equal2)", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.body.isSameNode(document.body)", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
// TODO: no test
|
|
||||||
.{ "link.normalize()", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "content.removeChild(append) !== undefined", "true" },
|
|
||||||
.{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let replace = document.createElement('div')", "undefined" },
|
|
||||||
.{ "link.replaceChild(replace, insertBefore) !== undefined", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "Node.ELEMENT_NODE", "1" },
|
|
||||||
.{ "Node.ATTRIBUTE_NODE", "2" },
|
|
||||||
.{ "Node.TEXT_NODE", "3" },
|
|
||||||
.{ "Node.CDATA_SECTION_NODE", "4" },
|
|
||||||
.{ "Node.PROCESSING_INSTRUCTION_NODE", "7" },
|
|
||||||
.{ "Node.COMMENT_NODE", "8" },
|
|
||||||
.{ "Node.DOCUMENT_NODE", "9" },
|
|
||||||
.{ "Node.DOCUMENT_TYPE_NODE", "10" },
|
|
||||||
.{ "Node.DOCUMENT_FRAGMENT_NODE", "11" },
|
|
||||||
.{ "Node.ENTITY_REFERENCE_NODE", "5" },
|
|
||||||
.{ "Node.ENTITY_NODE", "6" },
|
|
||||||
.{ "Node.NOTATION_NODE", "12" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
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 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" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const 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 DOMException = @import("exceptions.zig").DOMException;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
NodeListIterator,
|
|
||||||
NodeList,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const NodeListIterator = struct {
|
|
||||||
coll: *NodeList,
|
|
||||||
index: u32 = 0,
|
|
||||||
|
|
||||||
pub const Return = struct {
|
|
||||||
value: ?NodeUnion,
|
|
||||||
done: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _next(self: *NodeListIterator) !Return {
|
|
||||||
const e = try self.coll._item(self.index);
|
|
||||||
if (e == null) {
|
|
||||||
return Return{
|
|
||||||
.value = null,
|
|
||||||
.done = true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.index += 1;
|
|
||||||
return Return{
|
|
||||||
.value = e,
|
|
||||||
.done = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const NodeListEntriesIterator = struct {
|
|
||||||
coll: *NodeList,
|
|
||||||
index: u32 = 0,
|
|
||||||
|
|
||||||
pub const Return = struct {
|
|
||||||
value: ?NodeUnion,
|
|
||||||
done: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn _next(self: *NodeListEntriesIterator) !Return {
|
|
||||||
const e = try self.coll._item(self.index);
|
|
||||||
if (e == null) {
|
|
||||||
return Return{
|
|
||||||
.value = null,
|
|
||||||
.done = true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.index += 1;
|
|
||||||
return Return{
|
|
||||||
.value = e,
|
|
||||||
.done = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Nodelist is implemented in pure Zig b/c libdom's NodeList doesn't allow to
|
|
||||||
// append nodes.
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#nodelist
|
|
||||||
//
|
|
||||||
// TODO: a Nodelist can be either static or live. But the current
|
|
||||||
// implementation allows only static nodelist.
|
|
||||||
// see https://dom.spec.whatwg.org/#old-style-collections
|
|
||||||
pub const NodeList = struct {
|
|
||||||
pub const Exception = DOMException;
|
|
||||||
const NodesArrayList = std.ArrayListUnmanaged(*parser.Node);
|
|
||||||
|
|
||||||
nodes: NodesArrayList = .{},
|
|
||||||
|
|
||||||
pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void {
|
|
||||||
// TODO unref all nodes
|
|
||||||
self.nodes.deinit(alloc);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append(self: *NodeList, alloc: std.mem.Allocator, node: *parser.Node) !void {
|
|
||||||
try self.nodes.append(alloc, node);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_length(self: *NodeList) u32 {
|
|
||||||
return @intCast(self.nodes.items.len);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _item(self: *const NodeList, index: u32) !?NodeUnion {
|
|
||||||
if (index >= self.nodes.items.len) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const n = self.nodes.items[index];
|
|
||||||
return try Node.toInterface(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This code works, but it's _MUCH_ slower than using postAttach. The benefit
|
|
||||||
// of this version, is that it's "live"..but we're talking many orders of
|
|
||||||
// magnitude slower.
|
|
||||||
//
|
|
||||||
// You can test it by commenting out `postAttach`, uncommenting this and
|
|
||||||
// running:
|
|
||||||
// zig build wpt -- tests/wpt/dom/nodes/NodeList-static-length-getter-tampered-indexOf-1.html
|
|
||||||
//
|
|
||||||
// I think this _is_ the right way to do it, but I must be doing something
|
|
||||||
// wrong to make it so slow.
|
|
||||||
// pub fn indexed_get(self: *const NodeList, index: u32, has_value: *bool) !?NodeUnion {
|
|
||||||
// return (try self._item(index)) orelse {
|
|
||||||
// has_value.* = false;
|
|
||||||
// return null;
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub fn _forEach(self: *NodeList, cbk: Function) !void { // TODO handle thisArg
|
|
||||||
for (self.nodes.items, 0..) |n, i| {
|
|
||||||
const ii: u32 = @intCast(i);
|
|
||||||
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 });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _keys(self: *NodeList) U32Iterator {
|
|
||||||
return .{
|
|
||||||
.length = self.get_length(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _values(self: *NodeList) NodeListIterator {
|
|
||||||
return .{
|
|
||||||
.coll = self,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _symbol_iterator(self: *NodeList) NodeListIterator {
|
|
||||||
return self._values();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
|
|
||||||
pub fn postAttach(self: *NodeList, js_this: 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, .{});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.NodeList" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
Performance,
|
|
||||||
PerformanceEntry,
|
|
||||||
PerformanceMark,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MarkOptions = struct {
|
|
||||||
detail: ?Env.JsObject = null,
|
|
||||||
start_time: ?f64 = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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{},
|
|
||||||
|
|
||||||
time_origin: std.time.Timer,
|
|
||||||
// if (Window.crossOriginIsolated) -> Resolution in isolated contexts: 5 microseconds
|
|
||||||
// else -> Resolution in non-isolated contexts: 100 microseconds
|
|
||||||
const ms_resolution = 100;
|
|
||||||
|
|
||||||
fn limitedResolutionMs(nanoseconds: u64) f64 {
|
|
||||||
const elapsed_at_resolution = ((nanoseconds / std.time.ns_per_us) + ms_resolution / 2) / ms_resolution * ms_resolution;
|
|
||||||
const elapsed = @as(f64, @floatFromInt(elapsed_at_resolution));
|
|
||||||
return elapsed / @as(f64, std.time.us_per_ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_timeOrigin(self: *const Performance) f64 {
|
|
||||||
const is_posix = switch (@import("builtin").os.tag) { // From std.time.zig L125
|
|
||||||
.windows, .uefi, .wasi => false,
|
|
||||||
else => true,
|
|
||||||
};
|
|
||||||
const zero = std.time.Instant{ .timestamp = if (!is_posix) 0 else .{ .sec = 0, .nsec = 0 } };
|
|
||||||
const started = self.time_origin.started.since(zero);
|
|
||||||
return limitedResolutionMs(started);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _now(self: *Performance) f64 {
|
|
||||||
return limitedResolutionMs(self.time_origin.read());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _mark(_: *Performance, name: []const u8, _options: ?MarkOptions, page: *Page) !PerformanceMark {
|
|
||||||
const mark: PerformanceMark = try .constructor(name, _options, page);
|
|
||||||
// TODO: Should store this in an entries list
|
|
||||||
return mark;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
|
|
||||||
pub fn constructor(name: []const u8, _options: ?MarkOptions, page: *Page) !PerformanceMark {
|
|
||||||
const perf = &page.window.performance;
|
|
||||||
|
|
||||||
const options = _options orelse MarkOptions{};
|
|
||||||
const start_time = options.start_time orelse perf._now();
|
|
||||||
const detail = if (options.detail) |d| try d.persist() else null;
|
|
||||||
|
|
||||||
if (start_time < 0.0) {
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const duped_name = try page.arena.dupe(u8, name);
|
|
||||||
const proto = PerformanceEntry{ .name = duped_name, .entry_type = .mark, .start_time = start_time };
|
|
||||||
|
|
||||||
return .{ .proto = proto, .detail = detail };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_detail(self: *const PerformanceMark) ?Env.JsObject {
|
|
||||||
return self.detail;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("./../../testing.zig");
|
|
||||||
|
|
||||||
test "Performance: get_timeOrigin" {
|
|
||||||
var perf = Performance{ .time_origin = try std.time.Timer.start() };
|
|
||||||
const time_origin = perf.get_timeOrigin();
|
|
||||||
try testing.expect(time_origin >= 0);
|
|
||||||
|
|
||||||
// Check resolution
|
|
||||||
try testing.expectDelta(@rem(time_origin * std.time.us_per_ms, 100.0), 0.0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Performance: now" {
|
|
||||||
var perf = Performance{ .time_origin = try std.time.Timer.start() };
|
|
||||||
|
|
||||||
// Monotonically increasing
|
|
||||||
var now = perf._now();
|
|
||||||
while (now <= 0) { // Loop for now to not be 0
|
|
||||||
try testing.expectEqual(now, 0);
|
|
||||||
now = perf._now();
|
|
||||||
}
|
|
||||||
// Check resolution
|
|
||||||
try testing.expectDelta(@rem(now * std.time.us_per_ms, 100.0), 0.0, 0.1);
|
|
||||||
|
|
||||||
var after = perf._now();
|
|
||||||
while (after <= now) { // Loop untill after > now
|
|
||||||
try testing.expectEqual(after, now);
|
|
||||||
after = perf._now();
|
|
||||||
}
|
|
||||||
// Check resolution
|
|
||||||
try testing.expectDelta(@rem(after * std.time.us_per_ms, 100.0), 0.0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "Browser.Performance.Mark" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let performance = window.performance", "undefined" },
|
|
||||||
.{ "performance instanceof Performance", "true" },
|
|
||||||
.{ "let mark = performance.mark(\"start\")", "undefined" },
|
|
||||||
.{ "mark instanceof PerformanceMark", "true" },
|
|
||||||
.{ "mark.name", "start" },
|
|
||||||
.{ "mark.entryType", "mark" },
|
|
||||||
.{ "mark.duration", "0" },
|
|
||||||
.{ "mark.detail", "null" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#processinginstruction
|
|
||||||
pub const ProcessingInstruction = struct {
|
|
||||||
pub const Self = parser.ProcessingInstruction;
|
|
||||||
|
|
||||||
// TODO for libdom processing instruction inherit from node.
|
|
||||||
// But the spec says it must inherit from CDATA.
|
|
||||||
pub const prototype = *Node;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn get_target(self: *parser.ProcessingInstruction) ![]const u8 {
|
|
||||||
// libdom stores the ProcessingInstruction target in the node's name.
|
|
||||||
return try parser.nodeName(parser.processingInstructionToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
// There's something wrong when we try to clone a ProcessInstruction normally.
|
|
||||||
// The resulting object can't be cast back into a node (it crashes). This is
|
|
||||||
// a simple workaround.
|
|
||||||
pub fn _cloneNode(self: *parser.ProcessingInstruction, _: ?bool, page: *Page) !*parser.ProcessingInstruction {
|
|
||||||
return try parser.documentCreateProcessingInstruction(
|
|
||||||
@ptrCast(page.window.document),
|
|
||||||
try get_target(self),
|
|
||||||
(try get_data(self)) orelse "",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_data(self: *parser.ProcessingInstruction) !?[]const u8 {
|
|
||||||
return 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" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const NodeUnion = @import("node.zig").Union;
|
|
||||||
const Node = @import("node.zig").Node;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
AbstractRange,
|
|
||||||
Range,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const AbstractRange = struct {
|
|
||||||
collapsed: bool,
|
|
||||||
end_container: *parser.Node,
|
|
||||||
end_offset: i32,
|
|
||||||
start_container: *parser.Node,
|
|
||||||
start_offset: i32,
|
|
||||||
|
|
||||||
pub fn updateCollapsed(self: *AbstractRange) void {
|
|
||||||
// TODO: Eventually, compare properly.
|
|
||||||
self.collapsed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_collapsed(self: *const AbstractRange) bool {
|
|
||||||
return self.collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_endContainer(self: *const AbstractRange) !NodeUnion {
|
|
||||||
return Node.toInterface(self.end_container);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_endOffset(self: *const AbstractRange) i32 {
|
|
||||||
return self.end_offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_startContainer(self: *const AbstractRange) !NodeUnion {
|
|
||||||
return Node.toInterface(self.start_container);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_startOffset(self: *const AbstractRange) i32 {
|
|
||||||
return self.start_offset;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Range = struct {
|
|
||||||
pub const prototype = *AbstractRange;
|
|
||||||
|
|
||||||
proto: AbstractRange,
|
|
||||||
|
|
||||||
// The Range() constructor returns a newly created Range object whose start
|
|
||||||
// and end is the global Document object.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Range/Range
|
|
||||||
pub fn constructor(page: *Page) Range {
|
|
||||||
const proto: AbstractRange = .{
|
|
||||||
.collapsed = true,
|
|
||||||
.end_container = parser.documentHTMLToNode(page.window.document),
|
|
||||||
.end_offset = 0,
|
|
||||||
.start_container = parser.documentHTMLToNode(page.window.document),
|
|
||||||
.start_offset = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{ .proto = proto };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setStart(self: *Range, node: *parser.Node, offset: i32) void {
|
|
||||||
self.proto.start_container = node;
|
|
||||||
self.proto.start_offset = offset;
|
|
||||||
self.proto.updateCollapsed();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _setEnd(self: *Range, node: *parser.Node, offset: i32) void {
|
|
||||||
self.proto.end_container = node;
|
|
||||||
self.proto.end_offset = offset;
|
|
||||||
self.proto.updateCollapsed();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _createContextualFragment(_: *Range, fragment: []const u8, page: *Page) !*parser.DocumentFragment {
|
|
||||||
const document_html = page.window.document;
|
|
||||||
const document = parser.documentHTMLToDocument(document_html);
|
|
||||||
const doc_frag = try parser.documentParseFragmentFromStr(document, fragment);
|
|
||||||
return doc_frag;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _selectNodeContents(self: *Range, node: *parser.Node) !void {
|
|
||||||
self.proto.start_container = node;
|
|
||||||
self.proto.start_offset = 0;
|
|
||||||
self.proto.end_container = node;
|
|
||||||
|
|
||||||
// Set end_offset
|
|
||||||
switch (try parser.nodeType(node)) {
|
|
||||||
.text, .cdata_section, .comment, .processing_instruction => {
|
|
||||||
// For text-like nodes, end_offset should be the length of the text data
|
|
||||||
if (try parser.nodeValue(node)) |text_data| {
|
|
||||||
self.proto.end_offset = @intCast(text_data.len);
|
|
||||||
} else {
|
|
||||||
self.proto.end_offset = 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
// For element and other nodes, end_offset is the number of children
|
|
||||||
const child_nodes = try parser.nodeGetChildNodes(node);
|
|
||||||
const child_count = try parser.nodeListLength(child_nodes);
|
|
||||||
self.proto.end_offset = @intCast(child_count);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
self.proto.updateCollapsed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Range.detach() method does nothing. It used to disable the Range
|
|
||||||
// object and enable the browser to release associated resources. The
|
|
||||||
// method has been kept for compatibility.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Range/detach
|
|
||||||
pub fn _detach(_: *Range) void {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.Range" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
// Test Range constructor
|
|
||||||
.{ "let range = new Range()", "undefined" },
|
|
||||||
.{ "range instanceof Range", "true" },
|
|
||||||
.{ "range instanceof AbstractRange", "true" },
|
|
||||||
|
|
||||||
// Test initial state - collapsed range
|
|
||||||
.{ "range.collapsed", "true" },
|
|
||||||
.{ "range.startOffset", "0" },
|
|
||||||
.{ "range.endOffset", "0" },
|
|
||||||
.{ "range.startContainer instanceof HTMLDocument", "true" },
|
|
||||||
.{ "range.endContainer instanceof HTMLDocument", "true" },
|
|
||||||
|
|
||||||
// Test document.createRange()
|
|
||||||
.{ "let docRange = document.createRange()", "undefined" },
|
|
||||||
.{ "docRange instanceof Range", "true" },
|
|
||||||
.{ "docRange.collapsed", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const container = document.getElementById('content');", null },
|
|
||||||
|
|
||||||
// Test text range
|
|
||||||
.{ "const commentNode = container.childNodes[7];", null },
|
|
||||||
.{ "commentNode.nodeValue", "comment" },
|
|
||||||
.{ "const textRange = document.createRange();", null },
|
|
||||||
.{ "textRange.selectNodeContents(commentNode)", "undefined" },
|
|
||||||
.{ "textRange.startOffset", "0" },
|
|
||||||
.{ "textRange.endOffset", "7" }, // length of `comment`
|
|
||||||
|
|
||||||
// Test Node range
|
|
||||||
.{ "const nodeRange = document.createRange();", null },
|
|
||||||
.{ "nodeRange.selectNodeContents(container)", "undefined" },
|
|
||||||
.{ "nodeRange.startOffset", "0" },
|
|
||||||
.{ "nodeRange.endOffset", "9" }, // length of container.childNodes
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const 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,
|
|
||||||
};
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
const CharacterData = @import("character_data.zig").CharacterData;
|
|
||||||
const CDATASection = @import("cdata_section.zig").CDATASection;
|
|
||||||
|
|
||||||
// Text interfaces
|
|
||||||
pub const Interfaces = .{
|
|
||||||
CDATASection,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Text = struct {
|
|
||||||
pub const Self = parser.Text;
|
|
||||||
pub const prototype = *CharacterData;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn constructor(data: ?[]const u8, page: *const Page) !*parser.Text {
|
|
||||||
return parser.documentCreateTextNode(
|
|
||||||
parser.documentHTMLToDocument(page.window.document),
|
|
||||||
data orelse "",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS funcs
|
|
||||||
// --------
|
|
||||||
|
|
||||||
// Read attributes
|
|
||||||
|
|
||||||
pub fn get_wholeText(self: *parser.Text) ![]const u8 {
|
|
||||||
return try parser.textWholdeText(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS methods
|
|
||||||
// ----------
|
|
||||||
|
|
||||||
pub fn _splitText(self: *parser.Text, offset: u32) !*parser.Text {
|
|
||||||
return try parser.textSplitText(self, offset);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.DOM.Text" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let t = new Text('foo')", "undefined" },
|
|
||||||
.{ "t.data", "foo" },
|
|
||||||
|
|
||||||
.{ "let emptyt = new Text()", "undefined" },
|
|
||||||
.{ "emptyt.data", "" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
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" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const 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" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
const NodeFilter = @import("node_filter.zig").NodeFilter;
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
// 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: ?Env.Function,
|
|
||||||
|
|
||||||
pub const TreeWalkerOpts = union(enum) {
|
|
||||||
function: Env.Function,
|
|
||||||
object: struct { acceptNode: Env.Function },
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?TreeWalkerOpts) !TreeWalker {
|
|
||||||
var filter_func: ?Env.Function = null;
|
|
||||||
|
|
||||||
if (filter) |f| {
|
|
||||||
filter_func = switch (f) {
|
|
||||||
.function => |func| func,
|
|
||||||
.object => |o| o.acceptNode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.root = node,
|
|
||||||
.current_node = node,
|
|
||||||
.what_to_show = what_to_show orelse NodeFilter._SHOW_ALL,
|
|
||||||
.filter = filter_func,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const VerifyResult = enum { accept, skip, reject };
|
|
||||||
|
|
||||||
fn verify(self: *const TreeWalker, node: *parser.Node) !VerifyResult {
|
|
||||||
const node_type = try parser.nodeType(node);
|
|
||||||
const what_to_show = self.what_to_show;
|
|
||||||
|
|
||||||
// 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 (self.filter) |f| {
|
|
||||||
const filter = try f.call(u32, .{node});
|
|
||||||
return switch (filter) {
|
|
||||||
NodeFilter._FILTER_ACCEPT => .accept,
|
|
||||||
NodeFilter._FILTER_REJECT => .reject,
|
|
||||||
NodeFilter._FILTER_SKIP => .skip,
|
|
||||||
else => .reject,
|
|
||||||
};
|
|
||||||
} else return .accept;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_root(self: *TreeWalker) *parser.Node {
|
|
||||||
return self.root;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_currentNode(self: *TreeWalker) *parser.Node {
|
|
||||||
return self.current_node;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_whatToShow(self: *TreeWalker) u32 {
|
|
||||||
return self.what_to_show;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_filter(self: *TreeWalker) ?Env.Function {
|
|
||||||
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 self.verify(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 self.verify(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 self.verify(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 self.verify(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 self.verify(current)) {
|
|
||||||
.accept => return current,
|
|
||||||
.reject, .skip => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _firstChild(self: *TreeWalker) !?*parser.Node {
|
|
||||||
if (try self.firstChild(self.current_node)) |child| {
|
|
||||||
self.current_node = child;
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _lastChild(self: *TreeWalker) !?*parser.Node {
|
|
||||||
if (try self.lastChild(self.current_node)) |child| {
|
|
||||||
self.current_node = child;
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _nextNode(self: *TreeWalker) !?*parser.Node {
|
|
||||||
if (try self.firstChild(self.current_node)) |child| {
|
|
||||||
self.current_node = child;
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
var current = self.current_node;
|
|
||||||
while (current != self.root) {
|
|
||||||
if (try self.nextSibling(current)) |sibling| {
|
|
||||||
self.current_node = sibling;
|
|
||||||
return sibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
current = (try parser.nodeParentNode(current)) orelse break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _nextSibling(self: *TreeWalker) !?*parser.Node {
|
|
||||||
if (try self.nextSibling(self.current_node)) |sibling| {
|
|
||||||
self.current_node = sibling;
|
|
||||||
return sibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _parentNode(self: *TreeWalker) !?*parser.Node {
|
|
||||||
if (try self.parentNode(self.current_node)) |parent| {
|
|
||||||
self.current_node = parent;
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _previousNode(self: *TreeWalker) !?*parser.Node {
|
|
||||||
var current = self.current_node;
|
|
||||||
while (try parser.nodePreviousSibling(current)) |previous| {
|
|
||||||
current = previous;
|
|
||||||
|
|
||||||
switch (try self.verify(current)) {
|
|
||||||
.accept => {
|
|
||||||
// Get last child if it has one.
|
|
||||||
if (try self.lastChild(current)) |child| {
|
|
||||||
self.current_node = child;
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, this node is our previous one.
|
|
||||||
self.current_node = current;
|
|
||||||
return current;
|
|
||||||
},
|
|
||||||
.reject => continue,
|
|
||||||
.skip => {
|
|
||||||
// Get last child if it has one.
|
|
||||||
if (try self.lastChild(current)) |child| {
|
|
||||||
self.current_node = child;
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current != self.root) {
|
|
||||||
if (try self.parentNode(current)) |parent| {
|
|
||||||
self.current_node = parent;
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _previousSibling(self: *TreeWalker) !?*parser.Node {
|
|
||||||
if (try self.previousSibling(self.current_node)) |sibling| {
|
|
||||||
self.current_node = sibling;
|
|
||||||
return sibling;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
pub const Walker = union(enum) {
|
|
||||||
walkerDepthFirst: WalkerDepthFirst,
|
|
||||||
walkerChildren: WalkerChildren,
|
|
||||||
walkerNone: WalkerNone,
|
|
||||||
|
|
||||||
pub fn get_next(self: Walker, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
|
|
||||||
switch (self) {
|
|
||||||
inline else => |case| return case.get_next(root, cur),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// WalkerDepthFirst iterates over the DOM tree to return the next following
|
|
||||||
// node or null at the end.
|
|
||||||
//
|
|
||||||
// This implementation is a zig version of Netsurf code.
|
|
||||||
// http://source.netsurf-browser.org/libdom.git/tree/src/html/html_collection.c#n177
|
|
||||||
//
|
|
||||||
// The iteration is a depth first as required by the specification.
|
|
||||||
// https://dom.spec.whatwg.org/#htmlcollection
|
|
||||||
// https://dom.spec.whatwg.org/#concept-tree-order
|
|
||||||
pub const WalkerDepthFirst = struct {
|
|
||||||
pub fn get_next(_: WalkerDepthFirst, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
|
|
||||||
var n = cur orelse root;
|
|
||||||
|
|
||||||
// TODO deinit next
|
|
||||||
if (try parser.nodeFirstChild(n)) |next| {
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO deinit next
|
|
||||||
if (try parser.nodeNextSibling(n)) |next| {
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO deinit parent
|
|
||||||
// Back to the parent of cur.
|
|
||||||
// If cur has no parent, then the iteration is over.
|
|
||||||
var parent = try parser.nodeParentNode(n) orelse return null;
|
|
||||||
|
|
||||||
// TODO deinit lastchild
|
|
||||||
var lastchild = try parser.nodeLastChild(parent);
|
|
||||||
while (n != root and n == lastchild) {
|
|
||||||
n = parent;
|
|
||||||
|
|
||||||
// TODO deinit parent
|
|
||||||
// Back to the prev's parent.
|
|
||||||
// If prev has no parent, then the loop must stop.
|
|
||||||
parent = try parser.nodeParentNode(n) orelse break;
|
|
||||||
|
|
||||||
// TODO deinit lastchild
|
|
||||||
lastchild = try parser.nodeLastChild(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n == root) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return try parser.nodeNextSibling(n);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// WalkerChildren iterates over the root's children only.
|
|
||||||
pub const WalkerChildren = struct {
|
|
||||||
pub fn get_next(_: WalkerChildren, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
|
|
||||||
// On walk start, we return the first root's child.
|
|
||||||
if (cur == null) return try parser.nodeFirstChild(root);
|
|
||||||
|
|
||||||
// If cur is root, then return null.
|
|
||||||
// This is a special case, if the root is included in the walk, we
|
|
||||||
// don't want to go further to find children.
|
|
||||||
if (root == cur.?) return null;
|
|
||||||
|
|
||||||
return try parser.nodeNextSibling(cur.?);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const WalkerNone = struct {
|
|
||||||
pub fn get_next(_: WalkerNone, _: *parser.Node, _: ?*parser.Node) !?*parser.Node {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,243 +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 std = @import("std");
|
const std = @import("std");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
|
||||||
const parser = @import("netsurf.zig");
|
pub const Opts = struct {
|
||||||
const Walker = @import("dom/walker.zig").WalkerChildren;
|
// @ZIGDOM (none of these do anything)
|
||||||
|
with_base: bool = false,
|
||||||
|
strip_mode: StripMode = .{},
|
||||||
|
|
||||||
// writer must be a std.io.Writer
|
pub const StripMode = struct {
|
||||||
pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {
|
js: bool = false,
|
||||||
try writer.writeAll("<!DOCTYPE html>\n");
|
ui: bool = false,
|
||||||
try writeChildren(parser.documentToNode(doc), writer);
|
css: bool = false,
|
||||||
try writer.writeAll("\n");
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
// Spec: https://www.w3.org/TR/xml/#sec-prolog-dtd
|
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer) error{WriteFailed}!void {
|
||||||
pub fn writeDocType(doc_type: *parser.DocumentType, writer: anytype) !void {
|
switch (node._type) {
|
||||||
try writer.writeAll("<!DOCTYPE ");
|
.cdata => |cd| try writer.writeAll(cd.getData()),
|
||||||
try writer.writeAll(try parser.documentTypeGetName(doc_type));
|
.element => |el| {
|
||||||
|
try el.format(writer);
|
||||||
const public_id = try parser.documentTypeGetPublicId(doc_type);
|
try children(node, opts, writer);
|
||||||
const system_id = try parser.documentTypeGetSystemId(doc_type);
|
if (!isVoidElement(el)) {
|
||||||
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, writer: anytype) anyerror!void {
|
|
||||||
switch (try parser.nodeType(node)) {
|
|
||||||
.element => {
|
|
||||||
// open the tag
|
|
||||||
const tag = try parser.nodeLocalName(node);
|
|
||||||
try writer.writeAll("<");
|
|
||||||
try writer.writeAll(tag);
|
|
||||||
|
|
||||||
// write the attributes
|
|
||||||
const _map = try parser.nodeGetAttributes(node);
|
|
||||||
if (_map) |map| {
|
|
||||||
const ln = try parser.namedNodeMapGetLength(map);
|
|
||||||
for (0..ln) |i| {
|
|
||||||
const attr = try parser.namedNodeMapItem(map, @intCast(i)) orelse break;
|
|
||||||
try writer.writeAll(" ");
|
|
||||||
try writer.writeAll(try parser.attributeGetName(attr));
|
|
||||||
try writer.writeAll("=\"");
|
|
||||||
const attribute_value = try parser.attributeGetValue(attr) orelse "";
|
|
||||||
try writeEscapedAttributeValue(writer, attribute_value);
|
|
||||||
try writer.writeAll("\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try writer.writeAll(">");
|
|
||||||
|
|
||||||
// void elements can't have any content.
|
|
||||||
if (try isVoid(parser.nodeToElement(node))) return;
|
|
||||||
|
|
||||||
if (try parser.elementHTMLGetTagType(@ptrCast(node)) == .script) {
|
|
||||||
try writer.writeAll(try parser.nodeTextContent(node) orelse "");
|
|
||||||
} else {
|
|
||||||
// write the children
|
|
||||||
// TODO avoid recursion
|
|
||||||
try writeChildren(node, writer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// close the tag
|
|
||||||
try writer.writeAll("</");
|
try writer.writeAll("</");
|
||||||
try writer.writeAll(tag);
|
try writer.writeAll(el.getTagNameDump());
|
||||||
try writer.writeAll(">");
|
try writer.writeByte('>');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
.text => {
|
.document => try children(node, opts, writer),
|
||||||
const v = try parser.nodeValue(node) orelse return;
|
.document_fragment => try children(node, opts, writer),
|
||||||
try writeEscapedTextNode(writer, v);
|
.attribute => unreachable,
|
||||||
},
|
|
||||||
.cdata_section => {
|
|
||||||
const v = try parser.nodeValue(node) orelse return;
|
|
||||||
try writer.writeAll("<![CDATA[");
|
|
||||||
try writer.writeAll(v);
|
|
||||||
try writer.writeAll("]]>");
|
|
||||||
},
|
|
||||||
.comment => {
|
|
||||||
const v = try parser.nodeValue(node) orelse return;
|
|
||||||
try writer.writeAll("<!--");
|
|
||||||
try writer.writeAll(v);
|
|
||||||
try writer.writeAll("-->");
|
|
||||||
},
|
|
||||||
// TODO handle processing instruction dump
|
|
||||||
.processing_instruction => return,
|
|
||||||
// document fragment is outside of the main document DOM, so we
|
|
||||||
// don't output it.
|
|
||||||
.document_fragment => return,
|
|
||||||
// document will never be called, but required for completeness.
|
|
||||||
.document => return,
|
|
||||||
// done globally instead, but required for completeness. Only the outer DOCTYPE should be written
|
|
||||||
.document_type => return,
|
|
||||||
// deprecated
|
|
||||||
.attribute => return,
|
|
||||||
.entity_reference => return,
|
|
||||||
.entity => return,
|
|
||||||
.notation => return,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// writer must be a std.io.Writer
|
pub fn children(parent: *Node, opts: Opts, writer: *std.Io.Writer) !void {
|
||||||
pub fn writeChildren(root: *parser.Node, writer: anytype) !void {
|
var it = parent.childrenIterator();
|
||||||
const walker = Walker{};
|
while (it.next()) |child| {
|
||||||
var next: ?*parser.Node = null;
|
try deep(child, opts, writer);
|
||||||
while (true) {
|
|
||||||
next = try walker.get_next(root, next) orelse break;
|
|
||||||
try writeNode(next.?, writer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
|
pub fn toJSON(node: *Node, writer: *std.json.Stringify) !void {
|
||||||
// https://html.spec.whatwg.org/#void-elements
|
try writer.beginObject();
|
||||||
fn isVoid(elem: *parser.Element) !bool {
|
|
||||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(elem)));
|
try writer.objectField("type");
|
||||||
return switch (tag) {
|
switch (node.type) {
|
||||||
.area, .base, .br, .col, .embed, .hr, .img, .input, .link => true,
|
.cdata => {
|
||||||
.meta, .source, .track, .wbr => true,
|
try writer.write("cdata");
|
||||||
|
},
|
||||||
|
.document => {
|
||||||
|
try writer.write("document");
|
||||||
|
},
|
||||||
|
.element => |*el| {
|
||||||
|
try writer.write("element");
|
||||||
|
try writer.objectField("tag");
|
||||||
|
try writer.write(el.tagName());
|
||||||
|
|
||||||
|
try writer.objectField("attributes");
|
||||||
|
try writer.beginObject();
|
||||||
|
var it = el.attributeIterator();
|
||||||
|
while (it.next()) |attr| {
|
||||||
|
try writer.objectField(attr.name);
|
||||||
|
try writer.write(attr.value);
|
||||||
|
}
|
||||||
|
try writer.endObject();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.objectField("children");
|
||||||
|
try writer.beginArray();
|
||||||
|
var it = node.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
try toJSON(child, writer);
|
||||||
|
}
|
||||||
|
try writer.endArray();
|
||||||
|
try writer.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isVoidElement(el: *const Node.Element) bool {
|
||||||
|
return switch (el._type) {
|
||||||
|
.html => |html| switch (html._type) {
|
||||||
|
.br, .hr, .img, .input, .link, .meta => true,
|
||||||
else => false,
|
else => false,
|
||||||
|
},
|
||||||
|
.svg => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
|
|
||||||
var v = value;
|
|
||||||
while (v.len > 0) {
|
|
||||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
|
|
||||||
return writer.writeAll(v);
|
|
||||||
};
|
|
||||||
try writer.writeAll(v[0..index]);
|
|
||||||
switch (v[index]) {
|
|
||||||
'&' => try writer.writeAll("&"),
|
|
||||||
'<' => try writer.writeAll("<"),
|
|
||||||
'>' => try writer.writeAll(">"),
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
v = v[index + 1 ..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
|
|
||||||
var v = value;
|
|
||||||
while (v.len > 0) {
|
|
||||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
|
|
||||||
return writer.writeAll(v);
|
|
||||||
};
|
|
||||||
try writer.writeAll(v[0..index]);
|
|
||||||
switch (v[index]) {
|
|
||||||
'&' => try writer.writeAll("&"),
|
|
||||||
'<' => try writer.writeAll("<"),
|
|
||||||
'>' => try writer.writeAll(">"),
|
|
||||||
'"' => try writer.writeAll("""),
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
v = v[index + 1 ..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = std.testing;
|
|
||||||
test "dump.writeHTML" {
|
|
||||||
try parser.init();
|
|
||||||
defer parser.deinit();
|
|
||||||
|
|
||||||
try testWriteHTML(
|
|
||||||
"<div id=\"content\">Over 9000!</div>",
|
|
||||||
"<div id=\"content\">Over 9000!</div>",
|
|
||||||
);
|
|
||||||
|
|
||||||
try testWriteHTML(
|
|
||||||
"<root><!-- a comment --></root>",
|
|
||||||
"<root><!-- a comment --></root>",
|
|
||||||
);
|
|
||||||
|
|
||||||
try testWriteHTML(
|
|
||||||
"<p>< > &</p>",
|
|
||||||
"<p>< > &</p>",
|
|
||||||
);
|
|
||||||
|
|
||||||
try testWriteHTML(
|
|
||||||
"<p id=\""><&"''\">wat?</p>",
|
|
||||||
"<p id='\"><&"'''>wat?</p>",
|
|
||||||
);
|
|
||||||
|
|
||||||
try testWriteFullHTML(
|
|
||||||
\\<!DOCTYPE html>
|
|
||||||
\\<html><head><title>It's over what?</title><meta name="a" value="b">
|
|
||||||
\\</head><body>9000</body></html>
|
|
||||||
\\
|
|
||||||
, "<html><title>It's over what?</title><meta name=a value=\"b\">\n<body>9000");
|
|
||||||
|
|
||||||
try testWriteHTML(
|
|
||||||
"<p>hi</p><script>alert(power > 9000)</script>",
|
|
||||||
"<p>hi</p><script>alert(power > 9000)</script>",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn testWriteHTML(comptime expected_body: []const u8, src: []const u8) !void {
|
|
||||||
const expected =
|
|
||||||
"<!DOCTYPE html>\n<html><head></head><body>" ++
|
|
||||||
expected_body ++
|
|
||||||
"</body></html>\n";
|
|
||||||
return testWriteFullHTML(expected, src);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
|
|
||||||
var buf = std.ArrayListUnmanaged(u8){};
|
|
||||||
defer buf.deinit(testing.allocator);
|
|
||||||
|
|
||||||
const doc_html = try parser.documentHTMLParseFromStr(src);
|
|
||||||
defer parser.documentHTMLClose(doc_html) catch {};
|
|
||||||
|
|
||||||
const doc = parser.documentHTMLToDocument(doc_html);
|
|
||||||
try writeHTML(doc, buf.writer(testing.allocator));
|
|
||||||
try testing.expectEqualStrings(expected, buf.items);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
TextEncoder,
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://encoding.spec.whatwg.org/#interface-textencoder
|
|
||||||
pub const TextEncoder = struct {
|
|
||||||
pub fn constructor() !TextEncoder {
|
|
||||||
return .{};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_encoding(_: *const TextEncoder) []const u8 {
|
|
||||||
return "utf-8";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
|
|
||||||
// Ensure the input is a valid utf-8
|
|
||||||
// It seems chrome accepts invalid utf-8 sequence.
|
|
||||||
//
|
|
||||||
if (!std.unicode.utf8ValidateSlice(v)) {
|
|
||||||
return error.InvalidUtf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{ .values = v };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.Encoding.TextEncoder" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var encoder = new TextEncoder();", "undefined" },
|
|
||||||
.{ "encoder.encoding;", "utf-8" },
|
|
||||||
.{ "encoder.encode('€');", "226,130,172" },
|
|
||||||
|
|
||||||
// Invalid utf-8 sequence.
|
|
||||||
// Result with chrome:
|
|
||||||
// .{ "encoder.encode(new Uint8Array([0xE2,0x28,0xA1]))", "50,50,54,44,52,48,44,49,54,49" },
|
|
||||||
.{ "try {encoder.encode(new Uint8Array([0xE2,0x28,0xA1])) } catch (e) { e };", "Error: InvalidUtf8" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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("encoding/text_encoder.zig").Interfaces,
|
|
||||||
@import("events/event.zig").Interfaces,
|
|
||||||
@import("html/html.zig").Interfaces,
|
|
||||||
@import("iterator/iterator.zig").Interfaces,
|
|
||||||
@import("storage/storage.zig").Interfaces,
|
|
||||||
@import("url/url.zig").Interfaces,
|
|
||||||
@import("xhr/xhr.zig").Interfaces,
|
|
||||||
@import("xhr/form_data.zig").Interfaces,
|
|
||||||
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
|
||||||
@import("webcomponents/webcomponents.zig").Interfaces,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const JsThis = Env.JsThis;
|
|
||||||
pub const JsObject = Env.JsObject;
|
|
||||||
pub const Function = Env.Function;
|
|
||||||
pub const Env = js.Env(*Page, WebApis);
|
|
||||||
pub const Global = @import("html/window.zig").Window;
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Event = @import("event.zig").Event;
|
|
||||||
const JsObject = @import("../env.zig").JsObject;
|
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#interface-customevent
|
|
||||||
pub const CustomEvent = struct {
|
|
||||||
pub const prototype = *Event;
|
|
||||||
pub const union_make_copy = true;
|
|
||||||
|
|
||||||
proto: parser.Event,
|
|
||||||
detail: ?JsObject,
|
|
||||||
|
|
||||||
const CustomEventInit = struct {
|
|
||||||
bubbles: bool = false,
|
|
||||||
cancelable: bool = false,
|
|
||||||
composed: bool = false,
|
|
||||||
detail: ?JsObject = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent {
|
|
||||||
const opts = opts_ orelse CustomEventInit{};
|
|
||||||
|
|
||||||
const event = try parser.eventCreate();
|
|
||||||
defer parser.eventDestroy(event);
|
|
||||||
try parser.eventInit(event, event_type, .{
|
|
||||||
.bubbles = opts.bubbles,
|
|
||||||
.cancelable = opts.cancelable,
|
|
||||||
.composed = opts.composed,
|
|
||||||
});
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.proto = event.*,
|
|
||||||
.detail = if (opts.detail) |d| try d.persist() else null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_detail(self: *CustomEvent) ?JsObject {
|
|
||||||
return self.detail;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.CustomEvent" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let capture = null", "undefined" },
|
|
||||||
.{ "const el = document.createElement('div');", "undefined" },
|
|
||||||
.{ "el.addEventListener('c1', (e) => { capture = 'c1-' + new String(e.detail)})", "undefined" },
|
|
||||||
.{ "el.addEventListener('c2', (e) => { capture = 'c2-' + new String(e.detail.over)})", "undefined" },
|
|
||||||
|
|
||||||
.{ "el.dispatchEvent(new CustomEvent('c1'));", "true" },
|
|
||||||
.{ "capture", "c1-null" },
|
|
||||||
|
|
||||||
.{ "el.dispatchEvent(new CustomEvent('c1', {detail: '123'}));", "true" },
|
|
||||||
.{ "capture", "c1-123" },
|
|
||||||
|
|
||||||
.{ "el.dispatchEvent(new CustomEvent('c2', {detail: {over: 9000}}));", "true" },
|
|
||||||
.{ "capture", "c2-9000" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,447 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const generate = @import("../../runtime/generate.zig");
|
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
|
||||||
const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
|
||||||
const AbortSignal = @import("../html/AbortController.zig").AbortSignal;
|
|
||||||
|
|
||||||
const CustomEvent = @import("custom_event.zig").CustomEvent;
|
|
||||||
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
|
||||||
const MouseEvent = @import("mouse_event.zig").MouseEvent;
|
|
||||||
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
|
||||||
|
|
||||||
// Event interfaces
|
|
||||||
pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent };
|
|
||||||
|
|
||||||
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)).* },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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(self, 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(self, et.?, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_eventPhase(self: *parser.Event) !u8 {
|
|
||||||
return try parser.eventPhase(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_bubbles(self: *parser.Event) !bool {
|
|
||||||
return try parser.eventBubbles(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cancelable(self: *parser.Event) !bool {
|
|
||||||
return try parser.eventCancelable(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_defaultPrevented(self: *parser.Event) !bool {
|
|
||||||
return try parser.eventDefaultPrevented(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_isTrusted(self: *parser.Event) !bool {
|
|
||||||
return try parser.eventIsTrusted(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_timestamp(self: *parser.Event) !u32 {
|
|
||||||
return try parser.eventTimestamp(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
|
|
||||||
pub fn _initEvent(
|
|
||||||
self: *parser.Event,
|
|
||||||
eventType: []const u8,
|
|
||||||
bubbles: ?bool,
|
|
||||||
cancelable: ?bool,
|
|
||||||
) !void {
|
|
||||||
const opts = EventInit{
|
|
||||||
.bubbles = bubbles orelse false,
|
|
||||||
.cancelable = cancelable orelse false,
|
|
||||||
};
|
|
||||||
return try parser.eventInit(self, eventType, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _stopPropagation(self: *parser.Event) !void {
|
|
||||||
return try parser.eventStopPropagation(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _stopImmediatePropagation(self: *parser.Event) !void {
|
|
||||||
return try parser.eventStopImmediatePropagation(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _preventDefault(self: *parser.Event) !void {
|
|
||||||
return try parser.eventPreventDefault(self);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const EventHandler = struct {
|
|
||||||
once: bool,
|
|
||||||
capture: bool,
|
|
||||||
callback: Function,
|
|
||||||
node: parser.EventNode,
|
|
||||||
listener: *parser.EventListener,
|
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Function = Env.Function;
|
|
||||||
|
|
||||||
pub const Listener = union(enum) {
|
|
||||||
function: Function,
|
|
||||||
object: Env.JsObject,
|
|
||||||
|
|
||||||
pub fn callback(self: Listener, target: *parser.EventTarget) !?Function {
|
|
||||||
return switch (self) {
|
|
||||||
.function => |func| try func.withThis(target),
|
|
||||||
.object => |obj| blk: {
|
|
||||||
const func = (try obj.getFunction("handleEvent")) orelse return null;
|
|
||||||
break :blk try func.withThis(try obj.persist());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Opts = union(enum) {
|
|
||||||
flags: Flags,
|
|
||||||
capture: bool,
|
|
||||||
|
|
||||||
const Flags = struct {
|
|
||||||
once: ?bool,
|
|
||||||
capture: ?bool,
|
|
||||||
// We ignore this property. It seems to be largely used to help the
|
|
||||||
// browser make certain performance tweaks (i.e. the browser knows
|
|
||||||
// that the listener won't call preventDefault() and thus can safely
|
|
||||||
// run the default as needed).
|
|
||||||
passive: ?bool,
|
|
||||||
signal: ?*AbortSignal, // currently does nothing
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn register(
|
|
||||||
allocator: Allocator,
|
|
||||||
target: *parser.EventTarget,
|
|
||||||
typ: []const u8,
|
|
||||||
listener: Listener,
|
|
||||||
opts_: ?Opts,
|
|
||||||
) !?*EventHandler {
|
|
||||||
var once = false;
|
|
||||||
var capture = false;
|
|
||||||
var signal: ?*AbortSignal = null;
|
|
||||||
|
|
||||||
if (opts_) |opts| {
|
|
||||||
switch (opts) {
|
|
||||||
.capture => |c| capture = c,
|
|
||||||
.flags => |f| {
|
|
||||||
once = f.once orelse false;
|
|
||||||
signal = f.signal orelse null;
|
|
||||||
capture = f.capture orelse false;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const callback = (try listener.callback(target)) orelse return null;
|
|
||||||
|
|
||||||
if (signal) |s| {
|
|
||||||
const signal_target = parser.toEventTarget(AbortSignal, s);
|
|
||||||
|
|
||||||
const scb = try allocator.create(SignalCallback);
|
|
||||||
scb.* = .{
|
|
||||||
.target = target,
|
|
||||||
.capture = capture,
|
|
||||||
.callback_id = callback.id,
|
|
||||||
.typ = try allocator.dupe(u8, typ),
|
|
||||||
.signal_target = signal_target,
|
|
||||||
.signal_listener = undefined,
|
|
||||||
.node = .{ .func = SignalCallback.handle },
|
|
||||||
};
|
|
||||||
|
|
||||||
scb.signal_listener = try parser.eventTargetAddEventListener(
|
|
||||||
signal_target,
|
|
||||||
"abort",
|
|
||||||
&scb.node,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if event target has already this listener
|
|
||||||
if (try parser.eventTargetHasListener(target, typ, capture, callback.id) != null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const eh = try allocator.create(EventHandler);
|
|
||||||
eh.* = .{
|
|
||||||
.once = once,
|
|
||||||
.capture = capture,
|
|
||||||
.callback = callback,
|
|
||||||
.node = .{
|
|
||||||
.id = callback.id,
|
|
||||||
.func = handle,
|
|
||||||
},
|
|
||||||
.listener = undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
eh.listener = try parser.eventTargetAddEventListener(
|
|
||||||
target,
|
|
||||||
typ,
|
|
||||||
&eh.node,
|
|
||||||
capture,
|
|
||||||
);
|
|
||||||
return eh;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle(node: *parser.EventNode, event: *parser.Event) void {
|
|
||||||
const ievent = Event.toInterface(event) catch |err| {
|
|
||||||
log.err(.app, "toInterface error", .{ .err = err });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const self: *EventHandler = @fieldParentPtr("node", node);
|
|
||||||
var result: Function.Result = undefined;
|
|
||||||
self.callback.tryCall(void, .{ievent}, &result) catch {
|
|
||||||
log.debug(.user_script, "callback error", .{
|
|
||||||
.err = result.exception,
|
|
||||||
.stack = result.stack,
|
|
||||||
.source = "event handler",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (self.once) {
|
|
||||||
const target = (parser.eventTarget(event) catch return).?;
|
|
||||||
const typ = parser.eventType(event) catch return;
|
|
||||||
parser.eventTargetRemoveEventListener(
|
|
||||||
target,
|
|
||||||
typ,
|
|
||||||
self.listener,
|
|
||||||
self.capture,
|
|
||||||
) catch {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const SignalCallback = struct {
|
|
||||||
typ: []const u8,
|
|
||||||
capture: bool,
|
|
||||||
callback_id: usize,
|
|
||||||
node: parser.EventNode,
|
|
||||||
target: *parser.EventTarget,
|
|
||||||
signal_target: *parser.EventTarget,
|
|
||||||
signal_listener: *parser.EventListener,
|
|
||||||
|
|
||||||
fn handle(node: *parser.EventNode, _: *parser.Event) void {
|
|
||||||
const self: *SignalCallback = @fieldParentPtr("node", node);
|
|
||||||
self._handle() catch |err| {
|
|
||||||
log.err(.app, "event signal handler", .{ .err = err });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _handle(self: *SignalCallback) !void {
|
|
||||||
const lst = try parser.eventTargetHasListener(
|
|
||||||
self.target,
|
|
||||||
self.typ,
|
|
||||||
self.capture,
|
|
||||||
self.callback_id,
|
|
||||||
);
|
|
||||||
if (lst == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try parser.eventTargetRemoveEventListener(
|
|
||||||
self.target,
|
|
||||||
self.typ,
|
|
||||||
lst.?,
|
|
||||||
self.capture,
|
|
||||||
);
|
|
||||||
|
|
||||||
// remove the abort signal listener itself
|
|
||||||
try parser.eventTargetRemoveEventListener(
|
|
||||||
self.signal_target,
|
|
||||||
"abort",
|
|
||||||
self.signal_listener,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.Event" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let content = document.getElementById('content')", "undefined" },
|
|
||||||
.{ "let para = document.getElementById('para')", "undefined" },
|
|
||||||
.{ "var nb = 0; var evt", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{
|
|
||||||
\\ content.addEventListener('target', function(e) {
|
|
||||||
\\ evt = e; nb = nb + 1;
|
|
||||||
\\ e.preventDefault();
|
|
||||||
\\ })
|
|
||||||
,
|
|
||||||
"undefined",
|
|
||||||
},
|
|
||||||
.{ "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", "false" },
|
|
||||||
.{ "nb", "1" },
|
|
||||||
.{ "evt.target === content", "true" },
|
|
||||||
.{ "evt.bubbles", "true" },
|
|
||||||
.{ "evt.cancelable", "true" },
|
|
||||||
.{ "evt.defaultPrevented", "true" },
|
|
||||||
.{ "evt.isTrusted", "true" },
|
|
||||||
.{ "evt.timestamp > 1704063600", "true" }, // 2024/01/01 00:00
|
|
||||||
// event.type, event.currentTarget, event.phase checked in EventTarget
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0", "0" },
|
|
||||||
.{
|
|
||||||
\\ content.addEventListener('stop',function(e) {
|
|
||||||
\\ e.stopPropagation();
|
|
||||||
\\ nb = nb + 1;
|
|
||||||
\\ }, true)
|
|
||||||
,
|
|
||||||
"undefined",
|
|
||||||
},
|
|
||||||
// the following event listener will not be invoked
|
|
||||||
.{
|
|
||||||
\\ para.addEventListener('stop',function(e) {
|
|
||||||
\\ nb = nb + 1;
|
|
||||||
\\ })
|
|
||||||
,
|
|
||||||
"undefined",
|
|
||||||
},
|
|
||||||
.{ "para.dispatchEvent(new Event('stop'))", "true" },
|
|
||||||
.{ "nb", "1" }, // will be 2 if event was not stopped at content event listener
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0", "0" },
|
|
||||||
.{
|
|
||||||
\\ content.addEventListener('immediate', function(e) {
|
|
||||||
\\ e.stopImmediatePropagation();
|
|
||||||
\\ nb = nb + 1;
|
|
||||||
\\ })
|
|
||||||
,
|
|
||||||
"undefined",
|
|
||||||
},
|
|
||||||
// the following event listener will not be invoked
|
|
||||||
.{
|
|
||||||
\\ content.addEventListener('immediate', function(e) {
|
|
||||||
\\ nb = nb + 1;
|
|
||||||
\\ })
|
|
||||||
,
|
|
||||||
"undefined",
|
|
||||||
},
|
|
||||||
.{ "content.dispatchEvent(new Event('immediate'))", "true" },
|
|
||||||
.{ "nb", "1" }, // will be 2 if event was not stopped at first content event listener
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0", "0" },
|
|
||||||
.{
|
|
||||||
\\ content.addEventListener('legacy', function(e) {
|
|
||||||
\\ evt = e; nb = nb + 1;
|
|
||||||
\\ })
|
|
||||||
,
|
|
||||||
"undefined",
|
|
||||||
},
|
|
||||||
.{ "let evtLegacy = document.createEvent('Event')", "undefined" },
|
|
||||||
.{ "evtLegacy.initEvent('legacy')", "undefined" },
|
|
||||||
.{ "content.dispatchEvent(evtLegacy)", "true" },
|
|
||||||
.{ "nb", "1" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", "undefined" },
|
|
||||||
.{ "document.addEventListener('count', cbk)", "undefined" },
|
|
||||||
.{ "document.removeEventListener('count', cbk)", "undefined" },
|
|
||||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
|
||||||
.{ "nb", "0" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0; function cbk(event) { nb ++; }", null },
|
|
||||||
.{ "document.addEventListener('count', cbk, {once: true})", null },
|
|
||||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
|
||||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
|
||||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
|
||||||
.{ "nb", "1" },
|
|
||||||
.{ "document.removeEventListener('count', cbk)", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "nb = 0; function cbk(event) { nb ++; }", null },
|
|
||||||
.{ "let ac = new AbortController()", null },
|
|
||||||
.{ "document.addEventListener('count', cbk, {signal: ac.signal})", null },
|
|
||||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
|
||||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
|
||||||
.{ "ac.abort()", null },
|
|
||||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
|
||||||
.{ "nb", "2" },
|
|
||||||
.{ "document.removeEventListener('count', cbk)", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const log = std.log.scoped(.mouse_event);
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Event = @import("event.zig").Event;
|
|
||||||
const JsObject = @import("../env.zig").JsObject;
|
|
||||||
|
|
||||||
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
|
|
||||||
const UIEvent = Event;
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
|
|
||||||
pub const MouseEvent = struct {
|
|
||||||
pub const Self = parser.MouseEvent;
|
|
||||||
pub const prototype = *UIEvent;
|
|
||||||
|
|
||||||
const MouseButton = enum(u16) {
|
|
||||||
main_button = 0,
|
|
||||||
auxillary_button = 1,
|
|
||||||
secondary_button = 2,
|
|
||||||
fourth_button = 3,
|
|
||||||
fifth_button = 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MouseEventInit = struct {
|
|
||||||
screenX: i32 = 0,
|
|
||||||
screenY: i32 = 0,
|
|
||||||
clientX: i32 = 0,
|
|
||||||
clientY: i32 = 0,
|
|
||||||
ctrlKey: bool = false,
|
|
||||||
shiftKey: bool = false,
|
|
||||||
altKey: bool = false,
|
|
||||||
metaKey: bool = false,
|
|
||||||
button: MouseButton = .main_button,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent {
|
|
||||||
const opts = opts_ orelse MouseEventInit{};
|
|
||||||
|
|
||||||
var mouse_event = try parser.mouseEventCreate();
|
|
||||||
try parser.eventSetInternalType(@ptrCast(&mouse_event), .mouse_event);
|
|
||||||
|
|
||||||
try parser.mouseEventInit(mouse_event, event_type, .{
|
|
||||||
.x = opts.clientX,
|
|
||||||
.y = opts.clientY,
|
|
||||||
.ctrl = opts.ctrlKey,
|
|
||||||
.shift = opts.shiftKey,
|
|
||||||
.alt = opts.altKey,
|
|
||||||
.meta = opts.metaKey,
|
|
||||||
.button = @intFromEnum(opts.button),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!std.mem.eql(u8, event_type, "click")) {
|
|
||||||
log.warn("MouseEvent currently only supports listeners for 'click' events!", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
return mouse_event;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_button(self: *parser.MouseEvent) u16 {
|
|
||||||
return self.button;
|
|
||||||
}
|
|
||||||
|
|
||||||
// These is just an alias for clientX.
|
|
||||||
pub fn get_x(self: *parser.MouseEvent) i32 {
|
|
||||||
return self.cx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// These is just an alias for clientY.
|
|
||||||
pub fn get_y(self: *parser.MouseEvent) i32 {
|
|
||||||
return self.cy;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_clientX(self: *parser.MouseEvent) i32 {
|
|
||||||
return self.cx;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_clientY(self: *parser.MouseEvent) i32 {
|
|
||||||
return self.cy;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_screenX(self: *parser.MouseEvent) i32 {
|
|
||||||
return self.sx;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_screenY(self: *parser.MouseEvent) i32 {
|
|
||||||
return self.sy;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.MouseEvent" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
// Default MouseEvent
|
|
||||||
.{ "let event = new MouseEvent('click')", "undefined" },
|
|
||||||
.{ "event.type", "click" },
|
|
||||||
.{ "event instanceof MouseEvent", "true" },
|
|
||||||
.{ "event instanceof Event", "true" },
|
|
||||||
.{ "event.clientX", "0" },
|
|
||||||
.{ "event.clientY", "0" },
|
|
||||||
.{ "event.screenX", "0" },
|
|
||||||
.{ "event.screenY", "0" },
|
|
||||||
// MouseEvent with parameters
|
|
||||||
.{ "let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20 })", "undefined" },
|
|
||||||
.{ "new_event.button", "0" },
|
|
||||||
.{ "new_event.x", "10" },
|
|
||||||
.{ "new_event.y", "20" },
|
|
||||||
.{ "new_event.screenX", "10" },
|
|
||||||
.{ "new_event.screenY", "20" },
|
|
||||||
// MouseEvent Listener
|
|
||||||
.{ "let me = new MouseEvent('click')", "undefined" },
|
|
||||||
.{ "me instanceof Event", "true" },
|
|
||||||
.{ "var eevt = null; function ccbk(event) { eevt = event; }", "undefined" },
|
|
||||||
.{ "document.addEventListener('click', ccbk)", "undefined" },
|
|
||||||
.{ "document.dispatchEvent(me)", "true" },
|
|
||||||
.{ "eevt.type", "click" },
|
|
||||||
.{ "eevt instanceof MouseEvent", "true" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Env = @import("../env.zig").Env;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
AbortController,
|
|
||||||
AbortSignal,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AbortController = @This();
|
|
||||||
|
|
||||||
signal: *AbortSignal,
|
|
||||||
|
|
||||||
pub fn constructor(page: *Page) !AbortController {
|
|
||||||
// Why do we allocate this rather than storing directly in the struct?
|
|
||||||
// https://github.com/lightpanda-io/project/discussions/165
|
|
||||||
const signal = try page.arena.create(AbortSignal);
|
|
||||||
signal.* = .init;
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.signal = signal,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_signal(self: *AbortController) *AbortSignal {
|
|
||||||
return self.signal;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _abort(self: *AbortController, reason_: ?[]const u8) !void {
|
|
||||||
return self.signal.abort(reason_);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const AbortSignal = struct {
|
|
||||||
const DEFAULT_REASON = "AbortError";
|
|
||||||
|
|
||||||
pub const prototype = *EventTarget;
|
|
||||||
proto: parser.EventTargetTBase = .{},
|
|
||||||
|
|
||||||
aborted: bool,
|
|
||||||
reason: ?[]const u8,
|
|
||||||
|
|
||||||
pub const init: AbortSignal = .{
|
|
||||||
.proto = .{},
|
|
||||||
.reason = null,
|
|
||||||
.aborted = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn static_abort(reason_: ?[]const u8) AbortSignal {
|
|
||||||
return .{
|
|
||||||
.aborted = true,
|
|
||||||
.reason = reason_ orelse DEFAULT_REASON,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn static_timeout(delay: u32, page: *Page) !*AbortSignal {
|
|
||||||
const callback = try page.arena.create(TimeoutCallback);
|
|
||||||
callback.* = .{
|
|
||||||
.signal = .init,
|
|
||||||
.node = .{ .func = TimeoutCallback.run },
|
|
||||||
};
|
|
||||||
|
|
||||||
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
|
|
||||||
_ = try page.loop.timeout(delay_ms, &callback.node);
|
|
||||||
return &callback.signal;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_aborted(self: *const AbortSignal) bool {
|
|
||||||
return self.aborted;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn abort(self: *AbortSignal, reason_: ?[]const u8) !void {
|
|
||||||
self.aborted = true;
|
|
||||||
self.reason = reason_ orelse DEFAULT_REASON;
|
|
||||||
|
|
||||||
const abort_event = try parser.eventCreate();
|
|
||||||
try parser.eventSetInternalType(abort_event, .abort_signal);
|
|
||||||
|
|
||||||
defer parser.eventDestroy(abort_event);
|
|
||||||
try parser.eventInit(abort_event, "abort", .{});
|
|
||||||
_ = try parser.eventTargetDispatchEvent(
|
|
||||||
parser.toEventTarget(AbortSignal, self),
|
|
||||||
abort_event,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Reason = union(enum) {
|
|
||||||
reason: []const u8,
|
|
||||||
undefined: void,
|
|
||||||
};
|
|
||||||
pub fn get_reason(self: *const AbortSignal) Reason {
|
|
||||||
if (self.reason) |r| {
|
|
||||||
return .{ .reason = r };
|
|
||||||
}
|
|
||||||
return .{ .undefined = {} };
|
|
||||||
}
|
|
||||||
|
|
||||||
const ThrowIfAborted = union(enum) {
|
|
||||||
exception: Env.Exception,
|
|
||||||
undefined: void,
|
|
||||||
};
|
|
||||||
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
|
|
||||||
if (self.aborted) {
|
|
||||||
const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON);
|
|
||||||
return .{ .exception = ex };
|
|
||||||
}
|
|
||||||
return .{ .undefined = {} };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const TimeoutCallback = struct {
|
|
||||||
signal: AbortSignal,
|
|
||||||
|
|
||||||
// This is the internal data that the event loop tracks. We'll get this
|
|
||||||
// back in run and, from it, can get our TimeoutCallback instance
|
|
||||||
node: Loop.CallbackNode = undefined,
|
|
||||||
|
|
||||||
fn run(node: *Loop.CallbackNode, _: *?u63) void {
|
|
||||||
const self: *TimeoutCallback = @fieldParentPtr("node", node);
|
|
||||||
self.signal.abort("TimeoutError") catch |err| {
|
|
||||||
log.warn(.app, "abort signal timeout", .{ .err = err });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.HTML.AbortController" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var called = 0", null },
|
|
||||||
.{ "var a1 = new AbortController()", null },
|
|
||||||
.{ "var s1 = a1.signal", null },
|
|
||||||
.{ "s1.throwIfAborted()", "undefined" },
|
|
||||||
.{ "s1.reason", "undefined" },
|
|
||||||
.{ "var target;", null },
|
|
||||||
.{
|
|
||||||
\\ s1.addEventListener('abort', (e) => {
|
|
||||||
\\ called += 1;
|
|
||||||
\\ target = e.target;
|
|
||||||
\\
|
|
||||||
\\ });
|
|
||||||
,
|
|
||||||
null,
|
|
||||||
},
|
|
||||||
.{ "a1.abort()", null },
|
|
||||||
.{ "s1.aborted", "true" },
|
|
||||||
.{ "target == s1", "true" },
|
|
||||||
.{ "s1.reason", "AbortError" },
|
|
||||||
.{ "called", "1" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var s2 = AbortSignal.abort('over 9000')", null },
|
|
||||||
.{ "s2.aborted", "true" },
|
|
||||||
.{ "s2.reason", "over 9000" },
|
|
||||||
.{ "AbortSignal.abort().reason", "AbortError" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "var s3 = AbortSignal.timeout(10)", null },
|
|
||||||
.{ "s3.aborted", "true" },
|
|
||||||
.{ "s3.reason", "TimeoutError" },
|
|
||||||
.{ "try { s3.throwIfAborted() } catch (e) { e }", "Error: TimeoutError" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
const std = @import("std");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const 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" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,406 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const 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) ![]const u8 {
|
|
||||||
return try parser.documentHTMLGetDomain(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
|
|
||||||
return error.NotImplemented;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_referrer(self: *parser.DocumentHTML) ![]const u8 {
|
|
||||||
return try parser.documentHTMLGetReferrer(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_referrer(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
|
|
||||||
return error.NotImplemented;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_body(self: *parser.DocumentHTML) !?*parser.Body {
|
|
||||||
return try parser.documentHTMLBody(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_body(self: *parser.DocumentHTML, elt: ?*parser.ElementHTML) !?*parser.Body {
|
|
||||||
try parser.documentHTMLSetBody(self, elt);
|
|
||||||
return try get_body(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_head(self: *parser.DocumentHTML) !?*parser.Head {
|
|
||||||
const root = parser.documentHTMLToNode(self);
|
|
||||||
const walker = Walker{};
|
|
||||||
var next: ?*parser.Node = null;
|
|
||||||
while (true) {
|
|
||||||
next = try walker.get_next(root, next) orelse return null;
|
|
||||||
if (std.ascii.eqlIgnoreCase("head", try parser.nodeName(next.?))) {
|
|
||||||
return @as(*parser.Head, @ptrCast(next.?));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
|
||||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
|
||||||
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true, .is_http = false });
|
|
||||||
return buf.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, page: *Page) ![]const u8 {
|
|
||||||
// we use the cookie jar's allocator to parse the cookie because it
|
|
||||||
// outlives the page's arena.
|
|
||||||
const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str);
|
|
||||||
errdefer c.deinit();
|
|
||||||
if (c.http_only) {
|
|
||||||
c.deinit();
|
|
||||||
return ""; // HttpOnly cookies cannot be set from JS
|
|
||||||
}
|
|
||||||
try page.cookie_jar.add(c, std.time.timestamp());
|
|
||||||
return cookie_str;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_title(self: *parser.DocumentHTML) ![]const u8 {
|
|
||||||
return try parser.documentHTMLGetTitle(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_title(self: *parser.DocumentHTML, v: []const u8) ![]const u8 {
|
|
||||||
try parser.documentHTMLSetTitle(self, v);
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList {
|
|
||||||
const arena = page.arena;
|
|
||||||
var list: NodeList = .{};
|
|
||||||
|
|
||||||
if (name.len == 0) return list;
|
|
||||||
|
|
||||||
const root = parser.documentHTMLToNode(self);
|
|
||||||
var c = try collection.HTMLCollectionByName(arena, root, name, false);
|
|
||||||
|
|
||||||
const ln = try c.get_length();
|
|
||||||
var i: u32 = 0;
|
|
||||||
while (i < ln) {
|
|
||||||
const n = try c.item(i) orelse break;
|
|
||||||
try list.append(arena, n);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
|
||||||
return get_embeds(self, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_links(self: *parser.DocumentHTML) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection {
|
|
||||||
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_all(self: *parser.DocumentHTML) collection.HTMLAllCollection {
|
|
||||||
return collection.HTMLAllCollection.init(parser.documentHTMLToNode(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_currentScript(self: *parser.DocumentHTML) !?*parser.Script {
|
|
||||||
return try parser.documentHTMLGetCurrentScript(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_location(self: *parser.DocumentHTML) !?*Location {
|
|
||||||
return try parser.documentHTMLGetLocation(Location, self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
|
|
||||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
|
|
||||||
return "off";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_designMode(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
|
||||||
return "off";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_defaultView(_: *parser.DocumentHTML, page: *Page) *Window {
|
|
||||||
return &page.window;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_readyState(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
|
||||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
|
||||||
return @tagName(state.ready_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
// noop legacy functions
|
|
||||||
// https://html.spec.whatwg.org/#Document-partial
|
|
||||||
pub fn _clear(_: *parser.DocumentHTML) void {}
|
|
||||||
pub fn _captureEvents(_: *parser.DocumentHTML) void {}
|
|
||||||
pub fn _releaseEvents(_: *parser.DocumentHTML) void {}
|
|
||||||
|
|
||||||
pub fn get_fgColor(_: *parser.DocumentHTML) []const u8 {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
pub fn set_fgColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
pub fn get_linkColor(_: *parser.DocumentHTML) []const u8 {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
pub fn set_linkColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
pub fn get_vlinkColor(_: *parser.DocumentHTML) []const u8 {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
pub fn set_vlinkColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
pub fn get_alinkColor(_: *parser.DocumentHTML) []const u8 {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
pub fn set_alinkColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
pub fn get_bgColor(_: *parser.DocumentHTML) []const u8 {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
pub fn set_bgColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the topmost Element at the specified coordinates (relative to the viewport).
|
|
||||||
// Since LightPanda requires the client to know what they are clicking on we do not return the underlying element at this moment
|
|
||||||
// This can currenty only happen if the first pixel is clicked without having rendered any element. This will change when css properties are supported.
|
|
||||||
// This returns an ElementUnion instead of a *Parser.Element in case the element somehow hasn't passed through the js runtime yet.
|
|
||||||
// While x and y should be f32, here we take i32 since that's what our
|
|
||||||
// "renderer" uses. By specifying i32 here, rather than f32 and doing the
|
|
||||||
// conversion ourself, we rely on v8's type conversion which is both more
|
|
||||||
// flexible (e.g. handles NaN) and will be more consistent with a browser.
|
|
||||||
pub fn _elementFromPoint(_: *parser.DocumentHTML, x: i32, y: i32, page: *Page) !?ElementUnion {
|
|
||||||
const element = page.renderer.getElementAtPosition(x, y) orelse return null;
|
|
||||||
// TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?)
|
|
||||||
return try Element.toInterface(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an array of all elements at the specified coordinates (relative to the viewport). The elements are ordered from the topmost to the bottommost box of the viewport.
|
|
||||||
// While x and y should be f32, here we take i32 since that's what our
|
|
||||||
// "renderer" uses. By specifying i32 here, rather than f32 and doing the
|
|
||||||
// conversion ourself, we rely on v8's type conversion which is both more
|
|
||||||
// flexible (e.g. handles NaN) and will be more consistent with a browser.
|
|
||||||
pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: i32, y: i32, page: *Page) ![]ElementUnion {
|
|
||||||
const element = page.renderer.getElementAtPosition(x, y) orelse return &.{};
|
|
||||||
// TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?)
|
|
||||||
|
|
||||||
var list: std.ArrayListUnmanaged(ElementUnion) = .empty;
|
|
||||||
try list.ensureTotalCapacity(page.call_arena, 3);
|
|
||||||
list.appendAssumeCapacity(try Element.toInterface(element));
|
|
||||||
|
|
||||||
// Since we are using a flat renderer there is no hierarchy of elements. What we do know is that the element is part of the main document.
|
|
||||||
// Thus we can add the HtmlHtmlElement and it's child HTMLBodyElement to the returned list.
|
|
||||||
// TBD Should we instead return every parent that is an element? Note that a child does not physically need to be overlapping the parent.
|
|
||||||
// Should we do a render pass on demand?
|
|
||||||
const doc_elem = try parser.documentGetDocumentElement(parser.documentHTMLToDocument(page.window.document)) orelse {
|
|
||||||
return list.items;
|
|
||||||
};
|
|
||||||
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
|
||||||
list.appendAssumeCapacity(try Element.toInterface(parser.bodyToElement(body)));
|
|
||||||
}
|
|
||||||
list.appendAssumeCapacity(try Element.toInterface(doc_elem));
|
|
||||||
return list.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn documentIsLoaded(self: *parser.DocumentHTML, page: *Page) !void {
|
|
||||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
|
||||||
state.ready_state = .interactive;
|
|
||||||
|
|
||||||
const evt = try parser.eventCreate();
|
|
||||||
defer parser.eventDestroy(evt);
|
|
||||||
|
|
||||||
log.debug(.script_event, "dispatch event", .{
|
|
||||||
.type = "DOMContentLoaded",
|
|
||||||
.source = "document",
|
|
||||||
});
|
|
||||||
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
|
|
||||||
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, self), evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void {
|
|
||||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
|
||||||
state.ready_state = .complete;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
|
|
||||||
test "Browser.HTML.Document" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.__proto__.constructor.name", "HTMLDocument" },
|
|
||||||
.{ "document.__proto__.__proto__.constructor.name", "Document" },
|
|
||||||
.{ "document.body.localName == 'body'", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "document.domain", "" },
|
|
||||||
.{ "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" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,114 +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 Env = @import("../env.zig").Env;
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
|
|
||||||
pub const ErrorEvent = struct {
|
|
||||||
pub const prototype = *parser.Event;
|
|
||||||
pub const union_make_copy = true;
|
|
||||||
|
|
||||||
proto: parser.Event,
|
|
||||||
message: []const u8,
|
|
||||||
filename: []const u8,
|
|
||||||
lineno: i32,
|
|
||||||
colno: i32,
|
|
||||||
@"error": ?Env.JsObject,
|
|
||||||
|
|
||||||
const ErrorEventInit = struct {
|
|
||||||
message: []const u8 = "",
|
|
||||||
filename: []const u8 = "",
|
|
||||||
lineno: i32 = 0,
|
|
||||||
colno: i32 = 0,
|
|
||||||
@"error": ?Env.JsObject = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent {
|
|
||||||
const event = try parser.eventCreate();
|
|
||||||
defer parser.eventDestroy(event);
|
|
||||||
try parser.eventInit(event, event_type, .{});
|
|
||||||
try parser.eventSetInternalType(event, .event);
|
|
||||||
|
|
||||||
const o = opts orelse ErrorEventInit{};
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.proto = event.*,
|
|
||||||
.message = o.message,
|
|
||||||
.filename = o.filename,
|
|
||||||
.lineno = o.lineno,
|
|
||||||
.colno = o.colno,
|
|
||||||
.@"error" = if (o.@"error") |e| try e.persist() else null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_message(self: *const ErrorEvent) []const u8 {
|
|
||||||
return self.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_filename(self: *const ErrorEvent) []const u8 {
|
|
||||||
return self.filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_lineno(self: *const ErrorEvent) i32 {
|
|
||||||
return self.lineno;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_colno(self: *const ErrorEvent) i32 {
|
|
||||||
return self.colno;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_error(self: *const ErrorEvent) Env.UndefinedOr(Env.JsObject) {
|
|
||||||
if (self.@"error") |e| {
|
|
||||||
return .{ .value = e };
|
|
||||||
}
|
|
||||||
return .undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.HTML.ErrorEvent" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=c></div>" });
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let e1 = new ErrorEvent('err1')", null },
|
|
||||||
.{ "e1.message", "" },
|
|
||||||
.{ "e1.filename", "" },
|
|
||||||
.{ "e1.lineno", "0" },
|
|
||||||
.{ "e1.colno", "0" },
|
|
||||||
.{ "e1.error", "undefined" },
|
|
||||||
|
|
||||||
.{
|
|
||||||
\\ let e2 = new ErrorEvent('err1', {
|
|
||||||
\\ message: 'm1',
|
|
||||||
\\ filename: 'fx19',
|
|
||||||
\\ lineno: 443,
|
|
||||||
\\ colno: 8999,
|
|
||||||
\\ error: 'under 9000!',
|
|
||||||
\\
|
|
||||||
\\})
|
|
||||||
,
|
|
||||||
null,
|
|
||||||
},
|
|
||||||
.{ "e2.message", "m1" },
|
|
||||||
.{ "e2.filename", "fx19" },
|
|
||||||
.{ "e2.lineno", "443" },
|
|
||||||
.{ "e2.colno", "8999" },
|
|
||||||
.{ "e2.error", "under 9000!" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
const std = @import("std");
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
|
||||||
pub const History = struct {
|
|
||||||
const ScrollRestorationMode = enum {
|
|
||||||
auto,
|
|
||||||
manual,
|
|
||||||
};
|
|
||||||
|
|
||||||
scrollRestoration: ScrollRestorationMode = .auto,
|
|
||||||
state: std.json.Value = .null,
|
|
||||||
|
|
||||||
// count tracks the history length until we implement correctly pushstate.
|
|
||||||
count: u32 = 0,
|
|
||||||
|
|
||||||
pub fn get_length(self: *History) u32 {
|
|
||||||
// TODO return the real history length value.
|
|
||||||
return self.count;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_scrollRestoration(self: *History) []const u8 {
|
|
||||||
return switch (self.scrollRestoration) {
|
|
||||||
.auto => "auto",
|
|
||||||
.manual => "manual",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
|
|
||||||
if (std.mem.eql(u8, "manual", mode)) self.scrollRestoration = .manual;
|
|
||||||
if (std.mem.eql(u8, "auto", mode)) self.scrollRestoration = .auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_state(self: *History) std.json.Value {
|
|
||||||
return self.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement the function
|
|
||||||
// data must handle any argument. We could expect a std.json.Value but
|
|
||||||
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
|
|
||||||
pub fn _pushState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
|
|
||||||
self.count += 1;
|
|
||||||
_ = url;
|
|
||||||
_ = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement the function
|
|
||||||
// data must handle any argument. We could expect a std.json.Value but
|
|
||||||
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
|
|
||||||
pub fn _replaceState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
|
|
||||||
_ = self;
|
|
||||||
_ = url;
|
|
||||||
_ = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement the function
|
|
||||||
pub fn _go(self: *History, delta: ?i32) void {
|
|
||||||
_ = self;
|
|
||||||
_ = delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement the function
|
|
||||||
pub fn _back(self: *History) void {
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement the function
|
|
||||||
pub fn _forward(self: *History) void {
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.HTML.History" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "history.scrollRestoration", "auto" },
|
|
||||||
.{ "history.scrollRestoration = 'manual'", "manual" },
|
|
||||||
.{ "history.scrollRestoration = 'foo'", "foo" },
|
|
||||||
.{ "history.scrollRestoration", "manual" },
|
|
||||||
.{ "history.scrollRestoration = 'auto'", "auto" },
|
|
||||||
.{ "history.scrollRestoration", "auto" },
|
|
||||||
|
|
||||||
.{ "history.state", "null" },
|
|
||||||
|
|
||||||
.{ "history.pushState({}, null, '')", "undefined" },
|
|
||||||
|
|
||||||
.{ "history.replaceState({}, null, '')", "undefined" },
|
|
||||||
|
|
||||||
.{ "history.go()", "undefined" },
|
|
||||||
.{ "history.go(1)", "undefined" },
|
|
||||||
.{ "history.go(-1)", "undefined" },
|
|
||||||
|
|
||||||
.{ "history.forward()", "undefined" },
|
|
||||||
|
|
||||||
.{ "history.back()", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,43 +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 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,
|
|
||||||
};
|
|
||||||
@@ -1,106 +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 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", "" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const 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{},
|
|
||||||
|
|
||||||
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 {}
|
|
||||||
};
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const builtin = @import("builtin");
|
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/system-state.html#navigator
|
|
||||||
pub const Navigator = struct {
|
|
||||||
agent: []const u8 = "Lightpanda/1.0",
|
|
||||||
version: []const u8 = "1.0",
|
|
||||||
vendor: []const u8 = "",
|
|
||||||
platform: []const u8 = std.fmt.comptimePrint("{any} {any}", .{ builtin.os.tag, builtin.cpu.arch }),
|
|
||||||
|
|
||||||
language: []const u8 = "en-US",
|
|
||||||
|
|
||||||
pub fn get_userAgent(self: *Navigator) []const u8 {
|
|
||||||
return self.agent;
|
|
||||||
}
|
|
||||||
pub fn get_appCodeName(_: *Navigator) []const u8 {
|
|
||||||
return "Mozilla";
|
|
||||||
}
|
|
||||||
pub fn get_appName(_: *Navigator) []const u8 {
|
|
||||||
return "Netscape";
|
|
||||||
}
|
|
||||||
pub fn get_appVersion(self: *Navigator) []const u8 {
|
|
||||||
return self.version;
|
|
||||||
}
|
|
||||||
pub fn get_platform(self: *Navigator) []const u8 {
|
|
||||||
return self.platform;
|
|
||||||
}
|
|
||||||
pub fn get_product(_: *Navigator) []const u8 {
|
|
||||||
return "Gecko";
|
|
||||||
}
|
|
||||||
pub fn get_productSub(_: *Navigator) []const u8 {
|
|
||||||
return "20030107";
|
|
||||||
}
|
|
||||||
pub fn get_vendor(self: *Navigator) []const u8 {
|
|
||||||
return self.vendor;
|
|
||||||
}
|
|
||||||
pub fn get_vendorSub(_: *Navigator) []const u8 {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
pub fn get_language(self: *Navigator) []const u8 {
|
|
||||||
return self.language;
|
|
||||||
}
|
|
||||||
// TODO wait for arrays.
|
|
||||||
//pub fn get_languages(self: *Navigator) [][]const u8 {
|
|
||||||
// return .{self.language};
|
|
||||||
//}
|
|
||||||
pub fn get_online(_: *Navigator) bool {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
pub fn _registerProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
|
|
||||||
_ = scheme;
|
|
||||||
_ = url;
|
|
||||||
}
|
|
||||||
pub fn _unregisterProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
|
|
||||||
_ = scheme;
|
|
||||||
_ = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cookieEnabled(_: *Navigator) bool {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
// -----
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.HTML.Navigator" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "navigator.userAgent", "Lightpanda/1.0" },
|
|
||||||
.{ "navigator.appVersion", "1.0" },
|
|
||||||
.{ "navigator.language", "en-US" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
|
||||||
|
|
||||||
pub const Interfaces = .{
|
|
||||||
Screen,
|
|
||||||
ScreenOrientation,
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen
|
|
||||||
pub const Screen = struct {
|
|
||||||
pub const prototype = *EventTarget;
|
|
||||||
|
|
||||||
height: u32 = 1080,
|
|
||||||
width: u32 = 1920,
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/colorDepth
|
|
||||||
color_depth: u32 = 8,
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/pixelDepth
|
|
||||||
pixel_depth: u32 = 8,
|
|
||||||
orientation: ScreenOrientation = .{ .type = .landscape_primary },
|
|
||||||
|
|
||||||
pub fn get_availHeight(self: *const Screen) u32 {
|
|
||||||
return self.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_availWidth(self: *const Screen) u32 {
|
|
||||||
return self.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_height(self: *const Screen) u32 {
|
|
||||||
return self.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_width(self: *const Screen) u32 {
|
|
||||||
return self.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_pixelDepth(self: *const Screen) u32 {
|
|
||||||
return self.pixel_depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_orientation(self: *const Screen) ScreenOrientation {
|
|
||||||
return self.orientation;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ScreenOrientationType = enum {
|
|
||||||
portrait_primary,
|
|
||||||
portrait_secondary,
|
|
||||||
landscape_primary,
|
|
||||||
landscape_secondary,
|
|
||||||
|
|
||||||
pub fn toString(self: ScreenOrientationType) []const u8 {
|
|
||||||
return switch (self) {
|
|
||||||
.portrait_primary => "portrait-primary",
|
|
||||||
.portrait_secondary => "portrait-secondary",
|
|
||||||
.landscape_primary => "landscape-primary",
|
|
||||||
.landscape_secondary => "landscape-secondary",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const ScreenOrientation = struct {
|
|
||||||
pub const prototype = *EventTarget;
|
|
||||||
|
|
||||||
angle: u32 = 0,
|
|
||||||
type: ScreenOrientationType,
|
|
||||||
|
|
||||||
pub fn get_angle(self: *const ScreenOrientation) u32 {
|
|
||||||
return self.angle;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_type(self: *const ScreenOrientation) []const u8 {
|
|
||||||
return self.type.toString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.HTML.Screen" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let screen = window.screen", "undefined" },
|
|
||||||
.{ "screen.width === 1920", "true" },
|
|
||||||
.{ "screen.height === 1080", "true" },
|
|
||||||
.{ "let orientation = screen.orientation", "undefined" },
|
|
||||||
.{ "orientation.angle === 0", "true" },
|
|
||||||
.{ "orientation.type === \"landscape-primary\"", "true" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
|
|
||||||
pub const HTMLSelectElement = struct {
|
|
||||||
pub const Self = parser.Select;
|
|
||||||
pub const prototype = *HTMLElement;
|
|
||||||
pub const subtype = .node;
|
|
||||||
|
|
||||||
pub fn get_length(select: *parser.Select) !u32 {
|
|
||||||
return parser.selectGetLength(select);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_form(select: *parser.Select) !?*parser.Form {
|
|
||||||
return parser.selectGetForm(select);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_name(select: *parser.Select) ![]const u8 {
|
|
||||||
return parser.selectGetName(select);
|
|
||||||
}
|
|
||||||
pub fn set_name(select: *parser.Select, name: []const u8) !void {
|
|
||||||
return parser.selectSetName(select, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_disabled(select: *parser.Select) !bool {
|
|
||||||
return parser.selectGetDisabled(select);
|
|
||||||
}
|
|
||||||
pub fn set_disabled(select: *parser.Select, disabled: bool) !void {
|
|
||||||
return parser.selectSetDisabled(select, disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_multiple(select: *parser.Select) !bool {
|
|
||||||
return parser.selectGetMultiple(select);
|
|
||||||
}
|
|
||||||
pub fn set_multiple(select: *parser.Select, multiple: bool) !void {
|
|
||||||
return parser.selectSetMultiple(select, multiple);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_selectedIndex(select: *parser.Select, page: *Page) !i32 {
|
|
||||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(select)));
|
|
||||||
const selected_index = try parser.selectGetSelectedIndex(select);
|
|
||||||
|
|
||||||
// See the explicit_index_set field documentation
|
|
||||||
if (!state.explicit_index_set) {
|
|
||||||
if (selected_index == -1) {
|
|
||||||
if (try parser.selectGetMultiple(select) == false) {
|
|
||||||
if (try get_length(select) > 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return selected_index;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Libdom's dom_html_select_select_set_selected_index will crash if index
|
|
||||||
// is out of range, and it doesn't properly unset options
|
|
||||||
pub fn set_selectedIndex(select: *parser.Select, index: i32, page: *Page) !void {
|
|
||||||
var state = try page.getOrCreateNodeState(@alignCast(@ptrCast(select)));
|
|
||||||
state.explicit_index_set = true;
|
|
||||||
|
|
||||||
const options = try parser.selectGetOptions(select);
|
|
||||||
const len = try parser.optionCollectionGetLength(options);
|
|
||||||
for (0..len) |i| {
|
|
||||||
const option = try parser.optionCollectionItem(options, @intCast(i));
|
|
||||||
try parser.optionSetSelected(option, false);
|
|
||||||
}
|
|
||||||
if (index >= 0 and index < try get_length(select)) {
|
|
||||||
const option = try parser.optionCollectionItem(options, @intCast(index));
|
|
||||||
try parser.optionSetSelected(option, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.HTML.Select" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
|
||||||
\\ <form id=f1>
|
|
||||||
\\ <select id=s1 name=s1><option>o1<option>o2</select>
|
|
||||||
\\ </form>
|
|
||||||
\\ <select id=s2></select>
|
|
||||||
});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const s = document.getElementById('s1');", null },
|
|
||||||
.{ "s.form", "[object HTMLFormElement]" },
|
|
||||||
|
|
||||||
.{ "document.getElementById('s2').form", "null" },
|
|
||||||
|
|
||||||
.{ "s.disabled", "false" },
|
|
||||||
.{ "s.disabled = true", null },
|
|
||||||
.{ "s.disabled", "true" },
|
|
||||||
.{ "s.disabled = false", null },
|
|
||||||
.{ "s.disabled", "false" },
|
|
||||||
|
|
||||||
.{ "s.multiple", "false" },
|
|
||||||
.{ "s.multiple = true", null },
|
|
||||||
.{ "s.multiple", "true" },
|
|
||||||
.{ "s.multiple = false", null },
|
|
||||||
.{ "s.multiple", "false" },
|
|
||||||
|
|
||||||
.{ "s.name;", "s1" },
|
|
||||||
.{ "s.name = 'sel1';", null },
|
|
||||||
.{ "s.name", "sel1" },
|
|
||||||
|
|
||||||
.{ "s.length;", "2" },
|
|
||||||
|
|
||||||
.{ "s.selectedIndex", "0" },
|
|
||||||
.{ "s.selectedIndex = 2", null }, // out of range
|
|
||||||
.{ "s.selectedIndex", "-1" },
|
|
||||||
|
|
||||||
.{ "s.selectedIndex = -1", null },
|
|
||||||
.{ "s.selectedIndex", "-1" },
|
|
||||||
|
|
||||||
.{ "s.selectedIndex = 0", null },
|
|
||||||
.{ "s.selectedIndex", "0" },
|
|
||||||
|
|
||||||
.{ "s.selectedIndex = 1", null },
|
|
||||||
.{ "s.selectedIndex", "1" },
|
|
||||||
|
|
||||||
.{ "s.selectedIndex = -323", null },
|
|
||||||
.{ "s.selectedIndex", "-1" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,41 +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 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" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -1,471 +0,0 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
const parser = @import("../netsurf.zig");
|
|
||||||
const Function = @import("../env.zig").Function;
|
|
||||||
const Page = @import("../page.zig").Page;
|
|
||||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
|
||||||
|
|
||||||
const Navigator = @import("navigator.zig").Navigator;
|
|
||||||
const History = @import("history.zig").History;
|
|
||||||
const Location = @import("location.zig").Location;
|
|
||||||
const Crypto = @import("../crypto/crypto.zig").Crypto;
|
|
||||||
const Console = @import("../console/console.zig").Console;
|
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
|
||||||
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
|
|
||||||
const Performance = @import("../dom/performance.zig").Performance;
|
|
||||||
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
|
||||||
const CustomElementRegistry = @import("../webcomponents/custom_element_registry.zig").CustomElementRegistry;
|
|
||||||
const Screen = @import("screen.zig").Screen;
|
|
||||||
const Css = @import("../css/css.zig").Css;
|
|
||||||
|
|
||||||
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{},
|
|
||||||
|
|
||||||
document: *parser.DocumentHTML,
|
|
||||||
target: []const u8 = "",
|
|
||||||
history: History = .{},
|
|
||||||
location: Location = .{},
|
|
||||||
storage_shelf: ?*storage.Shelf = null,
|
|
||||||
|
|
||||||
// counter for having unique timer ids
|
|
||||||
timer_id: u30 = 0,
|
|
||||||
timers: std.AutoHashMapUnmanaged(u32, *TimerCallback) = .{},
|
|
||||||
|
|
||||||
crypto: Crypto = .{},
|
|
||||||
console: Console = .{},
|
|
||||||
navigator: Navigator = .{},
|
|
||||||
performance: Performance,
|
|
||||||
custom_elements: CustomElementRegistry = .{},
|
|
||||||
screen: Screen = .{},
|
|
||||||
css: Css = .{},
|
|
||||||
|
|
||||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
|
|
||||||
var fbs = std.io.fixedBufferStream("");
|
|
||||||
const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8");
|
|
||||||
const doc = parser.documentHTMLToDocument(html_doc);
|
|
||||||
try parser.documentSetDocumentURI(doc, "about:blank");
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.document = html_doc,
|
|
||||||
.target = target orelse "",
|
|
||||||
.navigator = navigator orelse .{},
|
|
||||||
.performance = .{ .time_origin = try std.time.Timer.start() },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn replaceLocation(self: *Window, loc: Location) !void {
|
|
||||||
self.location = loc;
|
|
||||||
try parser.documentHTMLSetLocation(Location, self.document, &self.location);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
|
|
||||||
self.performance.time_origin.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
|
|
||||||
self.document = doc;
|
|
||||||
try parser.documentHTMLSetLocation(Location, doc, &self.location);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setStorageShelf(self: *Window, shelf: *storage.Shelf) void {
|
|
||||||
self.storage_shelf = shelf;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_window(self: *Window) *Window {
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_navigator(self: *Window) *Navigator {
|
|
||||||
return &self.navigator;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_location(self: *Window) *Location {
|
|
||||||
return &self.location;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
|
|
||||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_console(self: *Window) *Console {
|
|
||||||
return &self.console;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_crypto(self: *Window) *Crypto {
|
|
||||||
return &self.crypto;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_self(self: *Window) *Window {
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_parent(self: *Window) *Window {
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: frames
|
|
||||||
pub fn get_top(self: *Window) *Window {
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_document(self: *Window) ?*parser.DocumentHTML {
|
|
||||||
return self.document;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_history(self: *Window) *History {
|
|
||||||
return &self.history;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
|
|
||||||
pub fn get_innerHeight(_: *Window, page: *Page) u32 {
|
|
||||||
// We do not have scrollbars or padding so this is the same as Element.clientHeight
|
|
||||||
return page.renderer.height();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The interior width of the window in pixels. That includes the width of the vertical scroll bar, if one is present.
|
|
||||||
pub fn get_innerWidth(_: *Window, page: *Page) u32 {
|
|
||||||
// We do not have scrollbars or padding so this is the same as Element.clientWidth
|
|
||||||
return page.renderer.width();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_name(self: *Window) []const u8 {
|
|
||||||
return self.target;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_localStorage(self: *Window) !*storage.Bottle {
|
|
||||||
if (self.storage_shelf == null) return parser.DOMError.NotSupported;
|
|
||||||
return &self.storage_shelf.?.bucket.local;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_sessionStorage(self: *Window) !*storage.Bottle {
|
|
||||||
if (self.storage_shelf == null) return parser.DOMError.NotSupported;
|
|
||||||
return &self.storage_shelf.?.bucket.session;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_performance(self: *Window) *Performance {
|
|
||||||
return &self.performance;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_customElements(self: *Window) *CustomElementRegistry {
|
|
||||||
return &self.custom_elements;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_screen(self: *Window) *Screen {
|
|
||||||
return &self.screen;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_CSS(self: *Window) *Css {
|
|
||||||
return &self.css;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
|
|
||||||
return self.createTimeout(cbk, 5, page, .{ .animation_frame = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _cancelAnimationFrame(self: *Window, id: u32, page: *Page) !void {
|
|
||||||
const kv = self.timers.fetchRemove(id) orelse return;
|
|
||||||
return page.loop.cancel(kv.value.loop_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO handle callback arguments.
|
|
||||||
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, page: *Page) !u32 {
|
|
||||||
return self.createTimeout(cbk, delay, page, .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO handle callback arguments.
|
|
||||||
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, page: *Page) !u32 {
|
|
||||||
return self.createTimeout(cbk, delay, page, .{ .repeat = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _clearTimeout(self: *Window, id: u32, page: *Page) !void {
|
|
||||||
const kv = self.timers.fetchRemove(id) orelse return;
|
|
||||||
return page.loop.cancel(kv.value.loop_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _clearInterval(self: *Window, id: u32, page: *Page) !void {
|
|
||||||
const kv = self.timers.fetchRemove(id) orelse return;
|
|
||||||
return page.loop.cancel(kv.value.loop_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _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 {
|
|
||||||
repeat: bool = false,
|
|
||||||
animation_frame: bool = false,
|
|
||||||
};
|
|
||||||
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, comptime opts: CreateTimeoutOpts) !u32 {
|
|
||||||
const delay = delay_ orelse 0;
|
|
||||||
if (delay > 5000) {
|
|
||||||
log.warn(.user_script, "long timeout ignored", .{ .delay = delay, .interval = opts.repeat });
|
|
||||||
// self.timer_id is u30, so the largest value we can generate is
|
|
||||||
// 1_073_741_824. Returning 2_000_000_000 makes sure that clients
|
|
||||||
// can call cancelTimer/cancelInterval without breaking anything.
|
|
||||||
return 2_000_000_000;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.timers.count() > 512) {
|
|
||||||
return error.TooManyTimeout;
|
|
||||||
}
|
|
||||||
const timer_id = self.timer_id +% 1;
|
|
||||||
self.timer_id = timer_id;
|
|
||||||
|
|
||||||
const arena = page.arena;
|
|
||||||
|
|
||||||
const gop = try self.timers.getOrPut(arena, timer_id);
|
|
||||||
if (gop.found_existing) {
|
|
||||||
// this can only happen if we've created 2^31 timeouts.
|
|
||||||
return error.TooManyTimeout;
|
|
||||||
}
|
|
||||||
errdefer _ = self.timers.remove(timer_id);
|
|
||||||
|
|
||||||
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
|
|
||||||
const callback = try arena.create(TimerCallback);
|
|
||||||
|
|
||||||
callback.* = .{
|
|
||||||
.cbk = cbk,
|
|
||||||
.loop_id = 0, // we're going to set this to a real value shortly
|
|
||||||
.window = self,
|
|
||||||
.timer_id = timer_id,
|
|
||||||
.node = .{ .func = TimerCallback.run },
|
|
||||||
.repeat = if (opts.repeat) delay_ms else null,
|
|
||||||
.animation_frame = opts.animation_frame,
|
|
||||||
};
|
|
||||||
callback.loop_id = try page.loop.timeout(delay_ms, &callback.node);
|
|
||||||
|
|
||||||
gop.value_ptr.* = callback;
|
|
||||||
return timer_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: getComputedStyle should return a read-only CSSStyleDeclaration.
|
|
||||||
// We currently don't have a read-only one, so we return a new instance on
|
|
||||||
// each call.
|
|
||||||
pub fn _getComputedStyle(_: *const Window, element: *parser.Element, pseudo_element: ?[]const u8) !CSSStyleDeclaration {
|
|
||||||
_ = element;
|
|
||||||
_ = pseudo_element;
|
|
||||||
return .empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ScrollToOpts = union(enum) {
|
|
||||||
x: i32,
|
|
||||||
opts: Opts,
|
|
||||||
|
|
||||||
const Opts = struct {
|
|
||||||
top: i32,
|
|
||||||
left: i32,
|
|
||||||
behavior: []const u8,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?u32) !void {
|
|
||||||
_ = opts;
|
|
||||||
_ = y;
|
|
||||||
|
|
||||||
{
|
|
||||||
const scroll_event = try parser.eventCreate();
|
|
||||||
defer parser.eventDestroy(scroll_event);
|
|
||||||
|
|
||||||
try parser.eventInit(scroll_event, "scroll", .{});
|
|
||||||
_ = try parser.eventTargetDispatchEvent(
|
|
||||||
parser.toEventTarget(Window, self),
|
|
||||||
scroll_event,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const scroll_end = try parser.eventCreate();
|
|
||||||
defer parser.eventDestroy(scroll_end);
|
|
||||||
|
|
||||||
try parser.eventInit(scroll_end, "scrollend", .{});
|
|
||||||
_ = try parser.eventTargetDispatchEvent(
|
|
||||||
parser.toEventTarget(parser.DocumentHTML, self.document),
|
|
||||||
scroll_end,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const TimerCallback = struct {
|
|
||||||
// the internal loop id, need it when cancelling
|
|
||||||
loop_id: usize,
|
|
||||||
|
|
||||||
// the id of our timer (windows.timers key)
|
|
||||||
timer_id: u31,
|
|
||||||
|
|
||||||
// The JavaScript callback to execute
|
|
||||||
cbk: Function,
|
|
||||||
|
|
||||||
// This is the internal data that the event loop tracks. We'll get this
|
|
||||||
// back in run and, from it, can get our TimerCallback instance
|
|
||||||
node: Loop.CallbackNode = undefined,
|
|
||||||
|
|
||||||
// if the event should be repeated
|
|
||||||
repeat: ?u63 = null,
|
|
||||||
|
|
||||||
animation_frame: bool = false,
|
|
||||||
|
|
||||||
window: *Window,
|
|
||||||
|
|
||||||
fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
|
||||||
const self: *TimerCallback = @fieldParentPtr("node", node);
|
|
||||||
|
|
||||||
var result: Function.Result = undefined;
|
|
||||||
|
|
||||||
var call: anyerror!void = undefined;
|
|
||||||
if (self.animation_frame) {
|
|
||||||
call = self.cbk.tryCall(void, .{self.window.performance._now()}, &result);
|
|
||||||
} else {
|
|
||||||
call = self.cbk.tryCall(void, .{}, &result);
|
|
||||||
}
|
|
||||||
|
|
||||||
call catch {
|
|
||||||
log.debug(.user_script, "callback error", .{
|
|
||||||
.err = result.exception,
|
|
||||||
.stack = result.stack,
|
|
||||||
.source = "window timeout",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (self.repeat) |r| {
|
|
||||||
// setInterval
|
|
||||||
repeat_delay.* = r;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// setTimeout
|
|
||||||
_ = self.window.timers.remove(self.timer_id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.HTML.Window" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "window.parent === window", "true" },
|
|
||||||
.{ "window.top === window", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// requestAnimationFrame should be able to wait by recursively calling itself
|
|
||||||
// Note however that we in this test do not wait as the request is just send to the browser
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{
|
|
||||||
\\ let start = 0;
|
|
||||||
\\ function step(timestamp) {
|
|
||||||
\\ start = timestamp;
|
|
||||||
\\ }
|
|
||||||
,
|
|
||||||
null,
|
|
||||||
},
|
|
||||||
.{ "requestAnimationFrame(step);", null }, // returned id is checked in the next test
|
|
||||||
.{ " start > 0", "true" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// cancelAnimationFrame should be able to cancel a request with the given id
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let request_id = requestAnimationFrame(timestamp => {});", null },
|
|
||||||
.{ "cancelAnimationFrame(request_id);", "undefined" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "innerHeight", "1" },
|
|
||||||
.{ "innerWidth", "1" }, // Width is 1 even if there are no elements
|
|
||||||
.{
|
|
||||||
\\ let div1 = document.createElement('div');
|
|
||||||
\\ document.body.appendChild(div1);
|
|
||||||
\\ div1.getClientRects();
|
|
||||||
,
|
|
||||||
null,
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
\\ let div2 = document.createElement('div');
|
|
||||||
\\ document.body.appendChild(div2);
|
|
||||||
\\ div2.getClientRects();
|
|
||||||
,
|
|
||||||
null,
|
|
||||||
},
|
|
||||||
.{ "innerHeight", "1" },
|
|
||||||
.{ "innerWidth", "2" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// cancelAnimationFrame should be able to cancel a request with the given id
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let longCall = false;", null },
|
|
||||||
.{ "window.setTimeout(() => {longCall = true}, 5001);", null },
|
|
||||||
.{ "longCall;", "false" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
// window event target
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{
|
|
||||||
\\ let called = false;
|
|
||||||
\\ window.addEventListener("ready", (e) => {
|
|
||||||
\\ called = (e.currentTarget == window);
|
|
||||||
\\ }, {capture: false, once: false});
|
|
||||||
\\ const evt = new Event("ready", { bubbles: true, cancelable: false });
|
|
||||||
\\ window.dispatchEvent(evt);
|
|
||||||
\\ called;
|
|
||||||
,
|
|
||||||
"true",
|
|
||||||
},
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "const b64 = btoa('https://ziglang.org/documentation/master/std/#std.base64.Base64Decoder')", "undefined" },
|
|
||||||
.{ "b64", "aHR0cHM6Ly96aWdsYW5nLm9yZy9kb2N1bWVudGF0aW9uL21hc3Rlci9zdGQvI3N0ZC5iYXNlNjQuQmFzZTY0RGVjb2Rlcg==" },
|
|
||||||
.{ "const str = atob(b64)", "undefined" },
|
|
||||||
.{ "str", "https://ziglang.org/documentation/master/std/#std.base64.Base64Decoder" },
|
|
||||||
.{ "try { atob('b') } catch (e) { e } ", "Error: InvalidCharacterError" },
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{ "let scroll = false; let scrolend = false", null },
|
|
||||||
.{ "window.addEventListener('scroll', () => {scroll = true});", null },
|
|
||||||
.{ "document.addEventListener('scrollend', () => {scrollend = true});", null },
|
|
||||||
.{ "window.scrollTo(0)", null },
|
|
||||||
.{ "scroll", "true" },
|
|
||||||
.{ "scrollend", "true" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user