mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
Compare commits
541 Commits
event-targ
...
update-wpt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0045924973 | ||
|
|
d605ea1b8b | ||
|
|
17ed502123 | ||
|
|
3466325d4d | ||
|
|
1613345dec | ||
|
|
759accef07 | ||
|
|
6d02669fc3 | ||
|
|
6d8d688063 | ||
|
|
5207bdfd85 | ||
|
|
690d4238e8 | ||
|
|
95ee78b1bd | ||
|
|
25eadc2263 | ||
|
|
28e4065890 | ||
|
|
e44388b506 | ||
|
|
540dea9fc3 | ||
|
|
c31290b794 | ||
|
|
f1fe4c0c70 | ||
|
|
921ac18876 | ||
|
|
505ad0380e | ||
|
|
2b7a7c0054 | ||
|
|
0dea4c51b7 | ||
|
|
3095f2110e | ||
|
|
e32d35b156 | ||
|
|
db28336e5d | ||
|
|
c5c5accaa8 | ||
|
|
78bfdd4515 | ||
|
|
01aa826a24 | ||
|
|
7f2506d8a6 | ||
|
|
7c2f7b6338 | ||
|
|
5f05de30a6 | ||
|
|
7741de7ae0 | ||
|
|
d4c8e8c50e | ||
|
|
bf36ff9cb9 | ||
|
|
8eadccdee2 | ||
|
|
b32839292c | ||
|
|
2402443dcc | ||
|
|
9f72c98967 | ||
|
|
f6f744aea1 | ||
|
|
cddc55694a | ||
|
|
8930e2f06e | ||
|
|
b8e5e130b9 | ||
|
|
a8c5087a38 | ||
|
|
d9f21e0475 | ||
|
|
ca3fa3dc40 | ||
|
|
ddd0a42b26 | ||
|
|
f884627927 | ||
|
|
9373cf9cf6 | ||
|
|
f04030904e | ||
|
|
271b2a0417 | ||
|
|
a4f7393fc8 | ||
|
|
8f851beda1 | ||
|
|
4489efa8d9 | ||
|
|
8b9084cb73 | ||
|
|
1146453dc2 | ||
|
|
bd54395948 | ||
|
|
89ac27ba97 | ||
|
|
74eaee53a4 | ||
|
|
20e4261aa7 | ||
|
|
312189fbde | ||
|
|
d05063ec61 | ||
|
|
47c14db54c | ||
|
|
f0e0650244 | ||
|
|
d2a68e62e9 | ||
|
|
09fbbc1e17 | ||
|
|
8971822247 | ||
|
|
1f0d1920bf | ||
|
|
cb7c8502b0 | ||
|
|
27d1f79839 | ||
|
|
83ef21e699 | ||
|
|
6c592669da | ||
|
|
88f7687646 | ||
|
|
f12a527ae3 | ||
|
|
7dde0be043 | ||
|
|
2910f4f527 | ||
|
|
93c0df33c2 | ||
|
|
7d9f6eef27 | ||
|
|
7d742d62b8 | ||
|
|
4db80cb9e7 | ||
|
|
addfbcb68f | ||
|
|
fac46d9d0b | ||
|
|
e38ff08de2 | ||
|
|
c31e2d91dd | ||
|
|
7309fec51d | ||
|
|
2e01fa738a | ||
|
|
9044925f32 | ||
|
|
2d5ff8252c | ||
|
|
072110481f | ||
|
|
0fb0532875 | ||
|
|
d8dd94c679 | ||
|
|
f3d7736acf | ||
|
|
8fbf5590f8 | ||
|
|
b8ac93045e | ||
|
|
89fea9b4df | ||
|
|
a3323dc8a7 | ||
|
|
ba0505c13c | ||
|
|
dd8432e8fd | ||
|
|
11c7f57745 | ||
|
|
89a3fac316 | ||
|
|
b0b3e92600 | ||
|
|
1fca035cfe | ||
|
|
4c89bb0e0a | ||
|
|
332508f563 | ||
|
|
158d11e93c | ||
|
|
18a49601a0 | ||
|
|
b971b4754f | ||
|
|
cfef22257e | ||
|
|
3153d8ee8c | ||
|
|
b2a975fe4a | ||
|
|
b2ba505954 | ||
|
|
a1b673175a | ||
|
|
d666de07a7 | ||
|
|
64508cec61 | ||
|
|
e0bcb625c2 | ||
|
|
9534e765e5 | ||
|
|
39124d2878 | ||
|
|
9ae4d66194 | ||
|
|
09850d7500 | ||
|
|
8897d9179c | ||
|
|
2d1b9d64bd | ||
|
|
e603a1707c | ||
|
|
6b1e7a1c5d | ||
|
|
5acd4b5fd4 | ||
|
|
9e88adb0da | ||
|
|
69eaf0d338 | ||
|
|
680de2dca1 | ||
|
|
837188f8d1 | ||
|
|
4a696b4053 | ||
|
|
0b2c4679fd | ||
|
|
5a08c92d02 | ||
|
|
faf93441f6 | ||
|
|
8aa3608a3c | ||
|
|
9727a9d000 | ||
|
|
1b74289c43 | ||
|
|
a698ff8309 | ||
|
|
5026c48805 | ||
|
|
2ac63b6985 | ||
|
|
114e11f52a | ||
|
|
3277d1baac | ||
|
|
f3d8ec040c | ||
|
|
0a29e9b3cf | ||
|
|
4b7c17ac03 | ||
|
|
1849f4c11d | ||
|
|
b9f61466ba | ||
|
|
d8fa9b8c4f | ||
|
|
42bc80e5b5 | ||
|
|
9f7446ba56 | ||
|
|
7bdea1befa | ||
|
|
66ec087416 | ||
|
|
9b4d1d442e | ||
|
|
16a30fa3b7 | ||
|
|
1cd3ebfc3f | ||
|
|
fd170df98f | ||
|
|
a2291b0713 | ||
|
|
3134ff81f4 | ||
|
|
072bc514f4 | ||
|
|
581a79f3fc | ||
|
|
cccb8e9645 | ||
|
|
646fcafa62 | ||
|
|
615453a687 | ||
|
|
361a1a21ac | ||
|
|
e3e3311dd0 | ||
|
|
74fa9a6b2b | ||
|
|
b62faef520 | ||
|
|
74391d59a5 | ||
|
|
1c08b3e5e4 | ||
|
|
8c489c2131 | ||
|
|
19976939b7 | ||
|
|
4e1659b98d | ||
|
|
26ef8deca5 | ||
|
|
4e5fe5ae1a | ||
|
|
7f308f59b4 | ||
|
|
f4e8bb6c66 | ||
|
|
e3638053d0 | ||
|
|
d688d8812d | ||
|
|
4a6bf38666 | ||
|
|
f532b62913 | ||
|
|
0080c8457f | ||
|
|
613904e3a4 | ||
|
|
753a093689 | ||
|
|
ea6f8ce4d9 | ||
|
|
180359e148 | ||
|
|
5816443ad3 | ||
|
|
e9fce9223e | ||
|
|
f6c43eaf9c | ||
|
|
8af71be551 | ||
|
|
9e36702eb2 | ||
|
|
cda6f89dba | ||
|
|
b8d7744563 | ||
|
|
25dcae7648 | ||
|
|
ee6382ef03 | ||
|
|
0310192660 | ||
|
|
c88bc65379 | ||
|
|
37340dc549 | ||
|
|
9b6764a852 | ||
|
|
b176857b8d | ||
|
|
f034065247 | ||
|
|
64bd4dee38 | ||
|
|
22307239ae | ||
|
|
3fc7ffadbf | ||
|
|
b87a80a32d | ||
|
|
c775de260a | ||
|
|
30fd358286 | ||
|
|
71c3d484a9 | ||
|
|
66bac32e33 | ||
|
|
4f0ea888ef | ||
|
|
bc1a83d04a | ||
|
|
32d9fc0d32 | ||
|
|
41bd3704ef | ||
|
|
be75b5b237 | ||
|
|
3a7da6665f | ||
|
|
2f47e04de7 | ||
|
|
7dc3add5fd | ||
|
|
8b296534a4 | ||
|
|
f9c4cefe59 | ||
|
|
d772eaf4a2 | ||
|
|
27ec1a13da | ||
|
|
07e8dfa257 | ||
|
|
0fbf48ab4d | ||
|
|
f38a0d2d67 | ||
|
|
b76875bf5d | ||
|
|
0253de80de | ||
|
|
647575261e | ||
|
|
3c2b348ce5 | ||
|
|
8aef6ca372 | ||
|
|
0139437c3d | ||
|
|
a7b91ee57d | ||
|
|
ad0117e060 | ||
|
|
309d70c142 | ||
|
|
c9ff59a433 | ||
|
|
ec9a1416a1 | ||
|
|
dac622fc46 | ||
|
|
92e2daf056 | ||
|
|
08e68a1cff | ||
|
|
8f4be9b76f | ||
|
|
fab6ec94fa | ||
|
|
5cbcb901f1 | ||
|
|
4d075818f6 | ||
|
|
4302be5619 | ||
|
|
68d1be3b94 | ||
|
|
af68b10c5d | ||
|
|
8b16d0e7ed | ||
|
|
2d5c24d8b5 | ||
|
|
0110ac62bf | ||
|
|
5bfa44b1b4 | ||
|
|
d21821a0fb | ||
|
|
84dfde2e51 | ||
|
|
22d33fa286 | ||
|
|
f6f83e2114 | ||
|
|
c6ad734de0 | ||
|
|
cf015b2ce7 | ||
|
|
fbe8086c98 | ||
|
|
95cae6e7de | ||
|
|
d12fd78ef0 | ||
|
|
b2d9f835bf | ||
|
|
735772f43a | ||
|
|
75f66a6cb2 | ||
|
|
24d5dfe3c6 | ||
|
|
be9e953971 | ||
|
|
82e67b7550 | ||
|
|
791549fda8 | ||
|
|
c763783d53 | ||
|
|
e347e7e5fb | ||
|
|
3f1d0df7f9 | ||
|
|
c6cb6d5eeb | ||
|
|
57025f8173 | ||
|
|
3e7f07374c | ||
|
|
fba9cb071d | ||
|
|
c6538e1038 | ||
|
|
3a1a582013 | ||
|
|
531a484cb0 | ||
|
|
16c477229a | ||
|
|
f2565049b8 | ||
|
|
afdb5d7233 | ||
|
|
18be1202db | ||
|
|
14cc87e1a5 | ||
|
|
2a0d1b0a48 | ||
|
|
22aa126b29 | ||
|
|
feb2046549 | ||
|
|
2f362f2aa2 | ||
|
|
de160d9170 | ||
|
|
226c18cb56 | ||
|
|
314aea4e1e | ||
|
|
807d3a600c | ||
|
|
fa8ea1ef43 | ||
|
|
2017d4785b | ||
|
|
fd35724aa8 | ||
|
|
e1a85d97e3 | ||
|
|
b972c9fe30 | ||
|
|
4c68150dec | ||
|
|
3d6dd06b99 | ||
|
|
81759fa57a | ||
|
|
20160cb071 | ||
|
|
8931506657 | ||
|
|
2aee346299 | ||
|
|
f89efd84d3 | ||
|
|
7607ab2c84 | ||
|
|
fe7f6bee1c | ||
|
|
b43658eb3f | ||
|
|
85caa09e63 | ||
|
|
c32853bfd6 | ||
|
|
e79cd58c8f | ||
|
|
0d291f1a36 | ||
|
|
24aa8e2a07 | ||
|
|
0a0c155292 | ||
|
|
55a942aa22 | ||
|
|
b51499e87b | ||
|
|
936048d478 | ||
|
|
bd6497743c | ||
|
|
6873d8d445 | ||
|
|
21c9dde858 | ||
|
|
17d3d620ff | ||
|
|
705603a088 | ||
|
|
ba8a0179d5 | ||
|
|
9fe10747ce | ||
|
|
4a4d9a9377 | ||
|
|
2e7342a59c | ||
|
|
c9bc5be42b | ||
|
|
b75b36dc61 | ||
|
|
1e6a1bd3af | ||
|
|
b0a2087015 | ||
|
|
a5ee34a2db | ||
|
|
a6a8130234 | ||
|
|
288761632f | ||
|
|
25bf4fa738 | ||
|
|
3b4de6a405 | ||
|
|
75512602c3 | ||
|
|
cd33a089d1 | ||
|
|
6b83281539 | ||
|
|
2609671982 | ||
|
|
accf2c0e5e | ||
|
|
53f6e66c23 | ||
|
|
56ddcc8e29 | ||
|
|
430779979e | ||
|
|
671dbcfd55 | ||
|
|
087a7b5f3c | ||
|
|
229844d399 | ||
|
|
36081653b0 | ||
|
|
9811c5d577 | ||
|
|
4394186dc3 | ||
|
|
725b48d8aa | ||
|
|
3fd8347943 | ||
|
|
5e7c26c34b | ||
|
|
d7019264a2 | ||
|
|
ade9fa5d0e | ||
|
|
f84c4393b9 | ||
|
|
48d01c0ab5 | ||
|
|
aca01d81d6 | ||
|
|
6a0b154d67 | ||
|
|
7ce69987d5 | ||
|
|
3fe28d5441 | ||
|
|
43f42f9ca0 | ||
|
|
3e288f1fcf | ||
|
|
8ccd75fdfb | ||
|
|
fd6aa6e54e | ||
|
|
4802a2ce82 | ||
|
|
e3409a27e7 | ||
|
|
5182edce6f | ||
|
|
763d8d025e | ||
|
|
a3045c9808 | ||
|
|
6b78b011b7 | ||
|
|
bd7b84e136 | ||
|
|
2a9bab3f13 | ||
|
|
6ca1e6c6dd | ||
|
|
f3a1a6a191 | ||
|
|
675932c65b | ||
|
|
708abb0e30 | ||
|
|
9de84aee2e | ||
|
|
adb8779d00 | ||
|
|
fbb0e675f5 | ||
|
|
a3e2b5246e | ||
|
|
ccacac0597 | ||
|
|
ca230aa230 | ||
|
|
7b775d2ad7 | ||
|
|
c5397bfbe2 | ||
|
|
9fec6ebc66 | ||
|
|
6bc38c5782 | ||
|
|
7f9d585d7f | ||
|
|
0b14d36c95 | ||
|
|
e22ca2d082 | ||
|
|
52a70cb7f5 | ||
|
|
a00d1d068a | ||
|
|
6ae4ed9fc3 | ||
|
|
6f5028612a | ||
|
|
c31c12d31a | ||
|
|
28008d835e | ||
|
|
08e99a32cb | ||
|
|
68fc87bc01 | ||
|
|
d0ba06c44b | ||
|
|
d501cbf765 | ||
|
|
488c7e6c27 | ||
|
|
155559c2c4 | ||
|
|
a22e1bc5e5 | ||
|
|
9519d3f7ce | ||
|
|
3f23e07c02 | ||
|
|
6c75177edc | ||
|
|
85df280447 | ||
|
|
734cf243f6 | ||
|
|
d8f7817eeb | ||
|
|
94b6b2636a | ||
|
|
1036f7580f | ||
|
|
908febb363 | ||
|
|
aefd091b44 | ||
|
|
99fb82e244 | ||
|
|
756d6620cc | ||
|
|
09505dba09 | ||
|
|
9401eff297 | ||
|
|
adbec3d272 | ||
|
|
e301ba0cdb | ||
|
|
b12eef218a | ||
|
|
bc4560877a | ||
|
|
521a740d3a | ||
|
|
be12b724cc | ||
|
|
073873a3e9 | ||
|
|
fcdcb50b8b | ||
|
|
61a7848fd9 | ||
|
|
6d6b840cf6 | ||
|
|
4dbba103d4 | ||
|
|
a2932f05f4 | ||
|
|
5d4efb7692 | ||
|
|
39a9efb73b | ||
|
|
5037bd07d5 | ||
|
|
73a2fa3f9c | ||
|
|
79387f469b | ||
|
|
f986cfecff | ||
|
|
4d51a9123b | ||
|
|
7602f15544 | ||
|
|
3180ba7de9 | ||
|
|
3e01cf19b0 | ||
|
|
14eebfe39e | ||
|
|
9176599b29 | ||
|
|
d6575faa9f | ||
|
|
24c5bf9ff4 | ||
|
|
cdcc5e106f | ||
|
|
1a8cc2d019 | ||
|
|
27e907491b | ||
|
|
0a1e6623c8 | ||
|
|
689dddd11a | ||
|
|
f8d01e1596 | ||
|
|
cd429f5935 | ||
|
|
03355f6a4a | ||
|
|
dc1d593019 | ||
|
|
9894cceeaa | ||
|
|
bcedbc845e | ||
|
|
f508288ce3 | ||
|
|
18080cef9f | ||
|
|
c4eeef2a86 | ||
|
|
b60a91f53c | ||
|
|
b1c3de6518 | ||
|
|
a43a6a299c | ||
|
|
d8fae5bc41 | ||
|
|
fa9b6f58e5 | ||
|
|
89ff1411e9 | ||
|
|
701e8277d6 | ||
|
|
4a11f80c45 | ||
|
|
f1b275d5d0 | ||
|
|
68e0ffc95c | ||
|
|
0753eb7691 | ||
|
|
92afcd174d | ||
|
|
94be7a0e79 | ||
|
|
0814daf99d | ||
|
|
b2e3419bff | ||
|
|
1846d0bc21 | ||
|
|
d282055e10 | ||
|
|
6ab64d155b | ||
|
|
6ba3e57f5f | ||
|
|
14fe4f65e1 | ||
|
|
bdb70444d6 | ||
|
|
4d9cc55a87 | ||
|
|
f41c1cbfd0 | ||
|
|
72eaab68be | ||
|
|
733c6b4c17 | ||
|
|
c0c0694fcc | ||
|
|
055530c8c6 | ||
|
|
fb3b38aec7 | ||
|
|
4e4a8f1bab | ||
|
|
39b3786776 | ||
|
|
8b22313ca1 | ||
|
|
402f72cfa8 | ||
|
|
e7dcb8a605 | ||
|
|
8f8a1fda85 | ||
|
|
26be25c3d5 | ||
|
|
50b53b00e0 | ||
|
|
94531cb3d0 | ||
|
|
842760255b | ||
|
|
c78b582d71 | ||
|
|
4ab02fab1c | ||
|
|
6863f3227f | ||
|
|
d01d43eccb | ||
|
|
2aa5f4fc82 | ||
|
|
3af0531111 | ||
|
|
6e58b98b3d | ||
|
|
62805cdf1d | ||
|
|
4229b1d2a4 | ||
|
|
2c4661a250 | ||
|
|
0c1a486ed9 | ||
|
|
688cb55c2b | ||
|
|
1594f148f8 | ||
|
|
fafd8c4af1 | ||
|
|
3d66758507 | ||
|
|
fc0ec860b0 | ||
|
|
00d332cd16 | ||
|
|
4c8c0f8738 | ||
|
|
54978132bb | ||
|
|
018abe0188 | ||
|
|
b186497fb0 | ||
|
|
27f9963ccb | ||
|
|
a4e3f03bf5 | ||
|
|
27a6be4ce0 | ||
|
|
76a2520e56 | ||
|
|
0a472681af | ||
|
|
6d530691f3 | ||
|
|
a74c9e8481 | ||
|
|
8aac26a331 | ||
|
|
fc59a0f6ab | ||
|
|
3fb16774b7 | ||
|
|
7b35bb4c0f | ||
|
|
318e2bd1c6 | ||
|
|
09ba4bcf43 | ||
|
|
0c89fa7b1e | ||
|
|
7eedb3320d | ||
|
|
cfac75ea49 | ||
|
|
f00a6c396f | ||
|
|
e74a9711ca | ||
|
|
636d3cdf90 | ||
|
|
71966affa1 | ||
|
|
bf4dc195ec | ||
|
|
dccca17e09 | ||
|
|
5381a4354c | ||
|
|
c70425fbf7 | ||
|
|
341f5725a4 | ||
|
|
d7069df80d | ||
|
|
579714a60b | ||
|
|
bbdf63635a | ||
|
|
fd7db18221 | ||
|
|
482ed8d958 | ||
|
|
673e16878d | ||
|
|
e11ceab029 | ||
|
|
7fe719f43c | ||
|
|
3fd3ac1de1 | ||
|
|
0e90a675af | ||
|
|
ee861c1f91 |
12
.github/actions/install/action.yml
vendored
12
.github/actions/install/action.yml
vendored
@@ -5,7 +5,7 @@ inputs:
|
||||
zig:
|
||||
description: 'Zig version to install'
|
||||
required: false
|
||||
default: '0.13.0'
|
||||
default: '0.14.0'
|
||||
arch:
|
||||
description: 'CPU arch used to select the v8 lib'
|
||||
required: false
|
||||
@@ -17,7 +17,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.1.11'
|
||||
default: 'v0.1.20'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
@@ -59,11 +59,11 @@ runs:
|
||||
- name: install v8
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug/libc_v8.a
|
||||
mkdir -p v8/build/${{inputs.arch}}-${{inputs.os}}/debug/ninja/obj/zig/
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/build/${{inputs.arch}}-${{inputs.os}}/debug/ninja/obj/zig/libc_v8.a
|
||||
|
||||
mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release/libc_v8.a
|
||||
mkdir -p v8/build/${{inputs.arch}}-${{inputs.os}}/release/ninja/obj/zig/
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/build/${{inputs.arch}}-${{inputs.os}}/release/ninja/obj/zig/libc_v8.a
|
||||
|
||||
- name: libiconv
|
||||
shell: bash
|
||||
|
||||
71
.github/workflows/build.yml
vendored
71
.github/workflows/build.yml
vendored
@@ -26,9 +26,44 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: zig build
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dcpu=x86_64
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: nightly
|
||||
|
||||
build-linux-aarch64:
|
||||
env:
|
||||
ARCH: aarch64
|
||||
OS: linux
|
||||
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: zig build
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
@@ -60,7 +95,39 @@ jobs:
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: zig build
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: nightly
|
||||
|
||||
build-macos-x86_64:
|
||||
env:
|
||||
ARCH: x86_64
|
||||
OS: macos
|
||||
|
||||
runs-on: macos-13
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: zig build
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'
|
||||
# branch should not be protected
|
||||
branch: 'main'
|
||||
allowlist: krichprollsch,francisbouvier,katie-lpd
|
||||
allowlist: krichprollsch,francisbouvier,katie-lpd,sjorsdonkers,bornlex
|
||||
|
||||
remote-organization-name: lightpanda-io
|
||||
remote-repository-name: cla
|
||||
|
||||
134
.github/workflows/e2e-test.yml
vendored
Normal file
134
.github/workflows/e2e-test.yml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
name: e2e-test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "build.zig"
|
||||
- "src/**/*.zig"
|
||||
- "src/*.zig"
|
||||
- "vendor/zig-js-runtime"
|
||||
- ".github/**"
|
||||
- "vendor/**"
|
||||
pull_request:
|
||||
|
||||
# By default GH trigger on types opened, synchronize and reopened.
|
||||
# see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
# Since we skip the job when the PR is in draft state, we want to force CI
|
||||
# running when the PR is marked ready_for_review w/o other change.
|
||||
# see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
paths:
|
||||
- ".github/**"
|
||||
- "build.zig"
|
||||
- "src/**/*.zig"
|
||||
- "src/*.zig"
|
||||
- "vendor/**"
|
||||
- ".github/**"
|
||||
- "vendor/**"
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
zig-build-release:
|
||||
name: zig build release
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- name: zig build release
|
||||
run: zig build -Doptimize=ReleaseSafe
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lightpanda-build-release
|
||||
path: |
|
||||
zig-out/bin/lightpanda
|
||||
retention-days: 1
|
||||
|
||||
puppeteer-perf:
|
||||
name: puppeteer-perf
|
||||
needs: zig-build-release
|
||||
|
||||
env:
|
||||
MAX_MEMORY: 28000
|
||||
MAX_AVG_DURATION: 24
|
||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'lightpanda-io/demo'
|
||||
fetch-depth: 0
|
||||
|
||||
- run: npm install
|
||||
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: lightpanda-build-release
|
||||
|
||||
- run: chmod a+x ./lightpanda
|
||||
|
||||
- name: run puppeteer
|
||||
run: |
|
||||
python3 -m http.server 1234 -d ./public & echo $! > PYTHON.pid
|
||||
./lightpanda serve --gc_hints & echo $! > LPD.pid
|
||||
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
|
||||
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
|
||||
kill `cat LPD.pid` `cat PYTHON.pid`
|
||||
|
||||
- name: puppeteer result
|
||||
run: cat puppeteer.out
|
||||
|
||||
- name: memory regression
|
||||
run: |
|
||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||
echo "Peak resident set size: $LPD_VmHWM"
|
||||
test "$LPD_VmHWM" -le "$MAX_MEMORY"
|
||||
|
||||
- name: duration regression
|
||||
run: |
|
||||
export PUPPETEER_AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
||||
echo "puppeteer avg duration: $PUPPETEER_AVG_DURATION"
|
||||
test "$PUPPETEER_AVG_DURATION" -le "$MAX_AVG_DURATION"
|
||||
|
||||
demo-scripts:
|
||||
name: demo-scripts
|
||||
needs: zig-build-release
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'lightpanda-io/demo'
|
||||
fetch-depth: 0
|
||||
|
||||
- run: npm install
|
||||
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: lightpanda-build-release
|
||||
|
||||
- run: chmod a+x ./lightpanda
|
||||
|
||||
- name: run end to end tests
|
||||
run: |
|
||||
./lightpanda serve & echo $! > LPD.pid
|
||||
go run runner/main.go --verbose
|
||||
kill `cat LPD.pid`
|
||||
43
.github/workflows/wpt.yml
vendored
43
.github/workflows/wpt.yml
vendored
@@ -26,14 +26,14 @@ on:
|
||||
# see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
paths:
|
||||
- ".github/**"
|
||||
- "build.zig"
|
||||
- "src/**/*.zig"
|
||||
- "src/*.zig"
|
||||
- "tests/wpt/**"
|
||||
- "vendor/**"
|
||||
- ".github/**"
|
||||
# paths:
|
||||
# - ".github/**"
|
||||
# - "build.zig"
|
||||
# - "src/**/*.zig"
|
||||
# - "src/*.zig"
|
||||
# - "tests/wpt/**"
|
||||
# - "vendor/**"
|
||||
# - ".github/**"
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -41,8 +41,8 @@ jobs:
|
||||
wpt:
|
||||
name: web platform tests
|
||||
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
# Only for PR without draft.
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.draft == false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -55,15 +55,32 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- run: zig build wpt -Dengine=v8 -- --safe --summary
|
||||
- run: zig build wpt -- --summary
|
||||
|
||||
# For now WPT tests doesn't pass at all.
|
||||
# We accept then to continue the job on failure.
|
||||
# TODO remove the continue-on-error when tests will pass.
|
||||
continue-on-error: true
|
||||
|
||||
wpt-json:
|
||||
name: web platform tests json output
|
||||
|
||||
# Don't execute on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- name: json output
|
||||
run: zig build wpt -Dengine=v8 -- --safe --json > wpt.json
|
||||
run: zig build wpt -- --json > wpt.json
|
||||
|
||||
- name: write commit
|
||||
run: |
|
||||
@@ -80,7 +97,7 @@ jobs:
|
||||
|
||||
perf-fmt:
|
||||
name: perf-fmt
|
||||
needs: wpt
|
||||
needs: wpt-json
|
||||
|
||||
# Don't execute on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
2
.github/workflows/zig-fmt.yml
vendored
2
.github/workflows/zig-fmt.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: zig-fmt
|
||||
|
||||
env:
|
||||
ZIG_VERSION: 0.13.0
|
||||
ZIG_VERSION: 0.14.0
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
54
.github/workflows/zig-test.yml
vendored
54
.github/workflows/zig-test.yml
vendored
@@ -16,6 +16,7 @@ on:
|
||||
- "src/*.zig"
|
||||
- "vendor/zig-js-runtime"
|
||||
- ".github/**"
|
||||
- "vendor/**"
|
||||
pull_request:
|
||||
|
||||
# By default GH trigger on types opened, synchronize and reopened.
|
||||
@@ -32,6 +33,7 @@ on:
|
||||
- "src/*.zig"
|
||||
- "vendor/**"
|
||||
- ".github/**"
|
||||
- "vendor/**"
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -54,7 +56,7 @@ jobs:
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- name: zig build debug
|
||||
run: zig build -Dengine=v8
|
||||
run: zig build
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -64,25 +66,24 @@ jobs:
|
||||
zig-out/bin/lightpanda
|
||||
retention-days: 1
|
||||
|
||||
zig-build-release:
|
||||
name: zig build release
|
||||
browser-fetch:
|
||||
name: browser fetch
|
||||
needs: zig-build-dev
|
||||
|
||||
# Don't run the CI on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
name: lightpanda-build-dev
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
- run: chmod a+x ./lightpanda
|
||||
|
||||
- name: zig build release
|
||||
run: zig build -Doptimize=ReleaseSafe -Dengine=v8
|
||||
- run: ./lightpanda fetch https://httpbin.io/xhr/get
|
||||
|
||||
zig-test:
|
||||
name: zig test
|
||||
@@ -102,7 +103,7 @@ jobs:
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- name: zig build test
|
||||
run: zig build test -Dengine=v8 -- --json > bench.json
|
||||
run: zig build test -- --json > bench.json
|
||||
|
||||
- name: write commit
|
||||
run: |
|
||||
@@ -139,30 +140,3 @@ jobs:
|
||||
|
||||
- name: format and send json result
|
||||
run: /perf-fmt bench-browser ${{ github.sha }} bench.json
|
||||
|
||||
demo-puppeteer:
|
||||
name: demo-puppeteer
|
||||
needs: zig-build-dev
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'lightpanda-io/demo'
|
||||
fetch-depth: 0
|
||||
|
||||
- run: npm install
|
||||
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: lightpanda-build-dev
|
||||
|
||||
- run: chmod a+x ./lightpanda
|
||||
|
||||
- name: run puppeteer
|
||||
run: |
|
||||
python3 -m http.server 1234 -d ./public &
|
||||
./lightpanda &
|
||||
RUNS=2 npm run bench-puppeteer-cdp
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@ zig-cache
|
||||
zig-out
|
||||
/vendor/netsurf/out
|
||||
/vendor/libiconv/
|
||||
lightpanda.id
|
||||
/v8/
|
||||
|
||||
23
.gitmodules
vendored
23
.gitmodules
vendored
@@ -1,34 +1,21 @@
|
||||
[submodule "vendor/zig-js-runtime"]
|
||||
path = vendor/zig-js-runtime
|
||||
url = git@github.com:lightpanda-io/zig-js-runtime.git
|
||||
[submodule "vendor/netsurf/libwapcaplet"]
|
||||
path = vendor/netsurf/libwapcaplet
|
||||
url = git@github.com:lightpanda-io/libwapcaplet.git
|
||||
url = https://github.com/lightpanda-io/libwapcaplet.git/
|
||||
[submodule "vendor/netsurf/libparserutils"]
|
||||
path = vendor/netsurf/libparserutils
|
||||
url = git@github.com:lightpanda-io/libparserutils.git
|
||||
url = https://github.com/lightpanda-io/libparserutils.git/
|
||||
[submodule "vendor/netsurf/libdom"]
|
||||
path = vendor/netsurf/libdom
|
||||
url = git@github.com:lightpanda-io/libdom.git
|
||||
url = https://github.com/lightpanda-io/libdom.git/
|
||||
[submodule "vendor/netsurf/share/netsurf-buildsystem"]
|
||||
path = vendor/netsurf/share/netsurf-buildsystem
|
||||
url = https://source.netsurf-browser.org/buildsystem.git
|
||||
[submodule "vendor/netsurf/libhubbub"]
|
||||
path = vendor/netsurf/libhubbub
|
||||
url = git@github.com:lightpanda-io/libhubbub.git
|
||||
url = https://github.com/lightpanda-io/libhubbub.git/
|
||||
[submodule "tests/wpt"]
|
||||
path = tests/wpt
|
||||
url = https://github.com/lightpanda-io/wpt
|
||||
[submodule "vendor/mimalloc"]
|
||||
path = vendor/mimalloc
|
||||
url = git@github.com:microsoft/mimalloc.git
|
||||
[submodule "vendor/tls.zig"]
|
||||
path = vendor/tls.zig
|
||||
url = git@github.com:ianic/tls.zig.git
|
||||
[submodule "vendor/zig-async-io"]
|
||||
path = vendor/zig-async-io
|
||||
url = git@github.com:lightpanda-io/zig-async-io.git
|
||||
[submodule "vendor/websocket.zig"]
|
||||
path = vendor/websocket.zig
|
||||
url = git@github.com:lightpanda-io/websocket.zig.git
|
||||
branch = lightpanda
|
||||
url = https://github.com/microsoft/mimalloc.git/
|
||||
|
||||
40
Dockerfile
40
Dockerfile
@@ -1,11 +1,11 @@
|
||||
FROM ubuntu:22.04
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ARG ZIG=0.13.0
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG=0.14.0
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG OS=linux
|
||||
ARG ARCH=x86_64
|
||||
ARG V8=11.1.134
|
||||
ARG ZIG_V8=v0.1.9
|
||||
ARG ZIG_V8=v0.1.20
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
apt-get install -yq xz-utils \
|
||||
@@ -16,25 +16,25 @@ RUN apt-get update -yq && \
|
||||
curl git
|
||||
|
||||
# install minisig
|
||||
RUN curl -L -O https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-linux.tar.gz && \
|
||||
tar xvzf minisign-0.11-linux.tar.gz
|
||||
RUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${MINISIG}/minisign-${MINISIG}-linux.tar.gz && \
|
||||
tar xvzf minisign-${MINISIG}-linux.tar.gz
|
||||
|
||||
# install zig
|
||||
RUN curl -O https://ziglang.org/download/${ZIG}/zig-linux-x86_64-${ZIG}.tar.xz && \
|
||||
curl -O https://ziglang.org/download/${ZIG}/zig-linux-x86_64-${ZIG}.tar.xz.minisig
|
||||
RUN curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-linux-${ARCH}-${ZIG}.tar.xz
|
||||
RUN curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-linux-${ARCH}-${ZIG}.tar.xz.minisig
|
||||
|
||||
RUN minisign-linux/x86_64/minisign -Vm zig-linux-x86_64-${ZIG}.tar.xz -P ${ZIG_MINISIG}
|
||||
RUN minisign-linux/${ARCH}/minisign -Vm zig-linux-${ARCH}-${ZIG}.tar.xz -P ${ZIG_MINISIG}
|
||||
|
||||
# clean minisg
|
||||
RUN rm -fr minisign-0.11-linux.tar.gz minisign-linux
|
||||
|
||||
# install zig
|
||||
RUN tar xvf zig-linux-x86_64-${ZIG}.tar.xz && \
|
||||
mv zig-linux-x86_64-${ZIG} /usr/local/lib && \
|
||||
ln -s /usr/local/lib/zig-linux-x86_64-${ZIG}/zig /usr/local/bin/zig
|
||||
RUN tar xvf zig-linux-${ARCH}-${ZIG}.tar.xz && \
|
||||
mv zig-linux-${ARCH}-${ZIG} /usr/local/lib && \
|
||||
ln -s /usr/local/lib/zig-linux-${ARCH}-${ZIG}/zig /usr/local/bin/zig
|
||||
|
||||
# clean up zig install
|
||||
RUN rm -fr zig-linux-x86_64-${ZIG}.tar.xz zig-linux-x86_64-${ZIG}.tar.xz.minisig
|
||||
RUN rm -fr zig-linux-${ARCH}-${ZIG}.tar.xz zig-linux-${ARCH}-${ZIG}.tar.xz.minisig
|
||||
|
||||
# force use of http instead of ssh with github
|
||||
RUN cat <<EOF > /root/.gitconfig
|
||||
@@ -51,23 +51,19 @@ WORKDIR /browser
|
||||
RUN git submodule init && \
|
||||
git submodule update --recursive
|
||||
|
||||
RUN cd vendor/zig-js-runtime && \
|
||||
git submodule init && \
|
||||
git submodule update --recursive
|
||||
|
||||
RUN make install-libiconv && \
|
||||
make install-netsurf && \
|
||||
make install-mimalloc
|
||||
|
||||
# download and install v8
|
||||
RUN curl -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_${OS}_${ARCH}.a && \
|
||||
mkdir -p vendor/zig-js-runtime/vendor/v8/${ARCH}-${OS}/release && \
|
||||
mv libc_v8.a vendor/zig-js-runtime/vendor/v8/${ARCH}-${OS}/release/libc_v8.a
|
||||
RUN curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
|
||||
mkdir -p v8/build/${ARCH}-linux/release/ninja/obj/zig/ && \
|
||||
mv libc_v8.a v8/build/${ARCH}-linux/release/ninja/obj/zig/libc_v8.a
|
||||
|
||||
# build release
|
||||
RUN make build
|
||||
|
||||
FROM ubuntu:22.04
|
||||
FROM ubuntu:24.04
|
||||
|
||||
# copy ca certificates
|
||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
@@ -76,4 +72,4 @@ COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
|
||||
|
||||
EXPOSE 9222/tcp
|
||||
|
||||
CMD ["/bin/lightpanda", "--host", "0.0.0.0", "--port", "9222"]
|
||||
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222"]
|
||||
|
||||
@@ -10,7 +10,6 @@ The default license for this project is [AGPL-3.0-only](LICENSE).
|
||||
The following files are licensed under MIT:
|
||||
|
||||
```
|
||||
src/http/Client.zig
|
||||
src/polyfill/fetch.js
|
||||
```
|
||||
|
||||
|
||||
57
Makefile
57
Makefile
@@ -3,12 +3,17 @@
|
||||
|
||||
ZIG := zig
|
||||
BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
# option test filter make test F="server"
|
||||
F=
|
||||
|
||||
# OS and ARCH
|
||||
kernel = $(shell uname -ms)
|
||||
ifeq ($(kernel), Darwin arm64)
|
||||
OS := macos
|
||||
ARCH := aarch64
|
||||
else ifeq ($(kernel), Darwin x86_64)
|
||||
OS := macos
|
||||
ARCH := x86_64
|
||||
else ifeq ($(kernel), Linux aarch64)
|
||||
OS := linux
|
||||
ARCH := aarch64
|
||||
@@ -42,7 +47,8 @@ help:
|
||||
|
||||
# $(ZIG) commands
|
||||
# ------------
|
||||
.PHONY: build build-dev run run-release shell test bench download-zig wpt
|
||||
.PHONY: build build-dev run run-release shell test bench download-zig wpt data get-v8 build-v8 build-v8-dev
|
||||
.PHONY: end2end
|
||||
|
||||
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
|
||||
|
||||
@@ -57,13 +63,13 @@ download-zig:
|
||||
## Build in release-safe mode
|
||||
build:
|
||||
@printf "\e[36mBuilding (release safe)...\e[0m\n"
|
||||
@$(ZIG) build -Doptimize=ReleaseSafe -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@printf "\e[33mBuild OK\e[0m\n"
|
||||
|
||||
## Build in debug mode
|
||||
build-dev:
|
||||
@printf "\e[36mBuilding (debug)...\e[0m\n"
|
||||
@$(ZIG) build -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@printf "\e[33mBuild OK\e[0m\n"
|
||||
|
||||
## Run the server in debug mode
|
||||
@@ -74,36 +80,52 @@ run: build
|
||||
## Run a JS shell in debug mode
|
||||
shell:
|
||||
@printf "\e[36mBuilding shell...\e[0m\n"
|
||||
@$(ZIG) build shell -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@$(ZIG) build shell || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
|
||||
## Run WPT tests
|
||||
wpt:
|
||||
@printf "\e[36mBuilding wpt...\e[0m\n"
|
||||
@$(ZIG) build wpt -Dengine=v8 -- --safe $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
|
||||
wpt-summary:
|
||||
@printf "\e[36mBuilding wpt...\e[0m\n"
|
||||
@$(ZIG) build wpt -Dengine=v8 -- --safe --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
|
||||
## Test
|
||||
test:
|
||||
@printf "\e[36mTesting...\e[0m\n"
|
||||
@$(ZIG) build test -Dengine=v8 || (printf "\e[33mTest ERROR\e[0m\n"; exit 1;)
|
||||
@printf "\e[33mTest OK\e[0m\n"
|
||||
@TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
|
||||
|
||||
## Run demo/runner end to end tests
|
||||
end2end:
|
||||
@test -d ../demo
|
||||
cd ../demo && go run runner/main.go
|
||||
|
||||
## v8
|
||||
get-v8:
|
||||
@printf "\e[36mGetting v8 source...\e[0m\n"
|
||||
@$(ZIG) build get-v8
|
||||
|
||||
build-v8-dev:
|
||||
@printf "\e[36mBuilding v8 (dev)...\e[0m\n"
|
||||
@$(ZIG) build build-v8
|
||||
|
||||
build-v8:
|
||||
@printf "\e[36mBuilding v8...\e[0m\n"
|
||||
@$(ZIG) build -Doptimize=ReleaseSafe build-v8
|
||||
|
||||
# Install and build required dependencies commands
|
||||
# ------------
|
||||
.PHONY: install-submodule
|
||||
.PHONY: install-zig-js-runtime install-zig-js-runtime-dev install-libiconv
|
||||
.PHONY: install-libiconv
|
||||
.PHONY: _install-netsurf install-netsurf clean-netsurf test-netsurf install-netsurf-dev
|
||||
.PHONY: install-mimalloc install-mimalloc-dev clean-mimalloc
|
||||
.PHONY: install-dev install
|
||||
|
||||
## Install and build dependencies for release
|
||||
install: install-submodule install-zig-js-runtime install-libiconv install-netsurf install-mimalloc
|
||||
install: install-submodule install-libiconv install-netsurf install-mimalloc
|
||||
|
||||
## Install and build dependencies for dev
|
||||
install-dev: install-submodule install-zig-js-runtime-dev install-libiconv install-netsurf-dev install-mimalloc-dev
|
||||
install-dev: install-submodule install-libiconv install-netsurf-dev install-mimalloc-dev
|
||||
|
||||
install-netsurf-dev: _install-netsurf
|
||||
install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
|
||||
@@ -139,7 +161,7 @@ _install-netsurf: clean-netsurf
|
||||
BUILDDIR=$(BC_NS)/build/libdom make install && \
|
||||
printf "\e[33mRunning libdom example...\e[0m\n" && \
|
||||
cd examples && \
|
||||
zig cc \
|
||||
$(ZIG) cc \
|
||||
-I$(ICONV)/include \
|
||||
-I$(BC_NS)/include \
|
||||
-L$(ICONV)/lib \
|
||||
@@ -186,13 +208,8 @@ ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
|
||||
make clean
|
||||
endif
|
||||
|
||||
install-zig-js-runtime-dev:
|
||||
@cd vendor/zig-js-runtime && \
|
||||
make install-dev
|
||||
|
||||
install-zig-js-runtime:
|
||||
@cd vendor/zig-js-runtime && \
|
||||
make install
|
||||
data:
|
||||
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
||||
|
||||
.PHONY: _build_mimalloc
|
||||
|
||||
|
||||
251
README.md
251
README.md
@@ -7,7 +7,11 @@
|
||||
<p align="center"><a href="https://lightpanda.io/">lightpanda.io</a></p>
|
||||
|
||||
<div align="center">
|
||||
<br />
|
||||
|
||||
[](https://github.com/lightpanda-io/browser/blob/main/LICENSE)
|
||||
[](https://twitter.com/lightpanda_io)
|
||||
[](https://github.com/lightpanda-io/browser)
|
||||
|
||||
</div>
|
||||
|
||||
Lightpanda is the open-source browser made for headless usage:
|
||||
@@ -16,39 +20,104 @@ Lightpanda is the open-source browser made for headless usage:
|
||||
- Support of Web APIs (partial, WIP)
|
||||
- Compatible with Playwright, Puppeteer through CDP (WIP)
|
||||
|
||||
Fast scraping and web automation with minimal memory footprint:
|
||||
Fast web automation for AI agents, LLM training, scraping and testing:
|
||||
|
||||
- Ultra-low memory footprint (9x less than Chrome)
|
||||
- Exceptionally fast execution (11x faster than Chrome) & instant startup
|
||||
- Exceptionally fast execution (11x faster than Chrome)
|
||||
- Instant startup
|
||||
|
||||
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark_2024-12-04.png">
|
||||
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/execution-time.svg">
|
||||
](https://github.com/lightpanda-io/demo)
|
||||
 
|
||||
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/memory-frame.svg">
|
||||
](https://github.com/lightpanda-io/demo)
|
||||
</div>
|
||||
|
||||
See [benchmark details](https://github.com/lightpanda-io/demo).
|
||||
_Puppeteer requesting 100 pages from a local website on a AWS EC2 m5.large instance.
|
||||
See [benchmark details](https://github.com/lightpanda-io/demo)._
|
||||
|
||||
## Why?
|
||||
## Quick start
|
||||
|
||||
### Javascript execution is mandatory for the modern web
|
||||
### Install from the nightly builds
|
||||
|
||||
In the good old days, scraping a webpage was as easy as making an HTTP request, cURL-like. It’s not possible anymore, because Javascript is everywhere, like it or not:
|
||||
You can download the last binary from the [nightly
|
||||
builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for
|
||||
Linux x86_64 and MacOS aarch64.
|
||||
|
||||
- Ajax, Single Page App, infinite loading, “click to display”, instant search, etc.
|
||||
- JS web frameworks: React, Vue, Angular & others
|
||||
*For Linux*
|
||||
```console
|
||||
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux && \
|
||||
chmod a+x ./lightpanda
|
||||
```
|
||||
|
||||
### Chrome is not the right tool
|
||||
*For MacOS*
|
||||
```console
|
||||
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-aarch64-macos && \
|
||||
chmod a+x ./lightpanda
|
||||
```
|
||||
|
||||
If we need Javascript, why not use a real web browser? Take a huge desktop application, hack it, and run it on the server. Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure it’s such a good idea?
|
||||
*For Windows + WSL2*
|
||||
|
||||
- Heavy on RAM and CPU, expensive to run
|
||||
- Hard to package, deploy and maintain at scale
|
||||
- Bloated, lots of features are not useful in headless usage
|
||||
The Lightpanda browser is compatible to run on windows inside WSL. Follow the Linux instruction for installation from a WSL terminal.
|
||||
It is recommended to install clients like Puppeteer on the Windows host.
|
||||
|
||||
### Lightpanda is built for performance
|
||||
### Dump a URL
|
||||
|
||||
If we want both Javascript and performance in a true headless browser, we need to start from scratch. Not another iteration of Chromium, really from a blank page. Crazy right? But that’s we did:
|
||||
```console
|
||||
./lightpanda fetch --dump https://lightpanda.io
|
||||
```
|
||||
```console
|
||||
info(browser): GET https://lightpanda.io/ http.Status.ok
|
||||
info(browser): fetch script https://api.website.lightpanda.io/js/script.js: http.Status.ok
|
||||
info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeError: Cannot read properties of undefined (reading 'pushState')
|
||||
<!DOCTYPE html>
|
||||
```
|
||||
|
||||
- Not based on Chromium, Blink or WebKit
|
||||
- Low-level system programming language (Zig) with optimisations in mind
|
||||
- Opinionated: without graphical rendering
|
||||
### Start a CDP server
|
||||
|
||||
```console
|
||||
./lightpanda serve --host 127.0.0.1 --port 9222
|
||||
```
|
||||
```console
|
||||
info(websocket): starting blocking worker to listen on 127.0.0.1:9222
|
||||
info(server): accepting new conn...
|
||||
```
|
||||
|
||||
Once the CDP server started, you can run a Puppeteer script by configuring the
|
||||
`browserWSEndpoint`.
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
|
||||
import puppeteer from 'puppeteer-core';
|
||||
|
||||
// use browserWSEndpoint to pass the Lightpanda's CDP server address.
|
||||
const browser = await puppeteer.connect({
|
||||
browserWSEndpoint: "ws://127.0.0.1:9222",
|
||||
});
|
||||
|
||||
// The rest of your script remains the same.
|
||||
const context = await browser.createBrowserContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
// Dump all the links from the page.
|
||||
await page.goto('https://wikipedia.com/');
|
||||
|
||||
const links = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll('a')).map(row => {
|
||||
return row.getAttribute('href');
|
||||
});
|
||||
});
|
||||
|
||||
console.log(links);
|
||||
|
||||
await page.close();
|
||||
await context.close();
|
||||
await browser.disconnect();
|
||||
```
|
||||
|
||||
### Telemetry
|
||||
By default, Lightpanda collects and sends usage telemetry. This can be disabled by setting an environment variable `LIGHTPANDA_DISABLE_TELEMETRY=true`. You can read Lightpanda's privacy policy at: [https://lightpanda.io/privacy-policy](https://lightpanda.io/privacy-policy).
|
||||
|
||||
## Status
|
||||
|
||||
@@ -72,79 +141,11 @@ NOTE: There are hundreds of Web APIs. Developing a browser (even just for headle
|
||||
|
||||
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
|
||||
|
||||
## Quick start
|
||||
|
||||
### Install from the nightly builds
|
||||
|
||||
You can download the last binary from the [nightly
|
||||
builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for
|
||||
Linux x86_64 and MacOS aarch64.
|
||||
|
||||
```console
|
||||
# Download the binary
|
||||
$ wget https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux
|
||||
$ chmod a+x ./lightpanda-x86_64-linux
|
||||
$ ./lightpanda-x86_64-linux -h
|
||||
usage: ./lightpanda-x86_64-linux [options] [URL]
|
||||
|
||||
start Lightpanda browser
|
||||
|
||||
* if an url is provided the browser will fetch the page and exit
|
||||
* otherwhise the browser starts a CDP server
|
||||
|
||||
-h, --help Print this help message and exit.
|
||||
--host Host of the CDP server (default "127.0.0.1")
|
||||
--port Port of the CDP server (default "9222")
|
||||
--timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
|
||||
--dump Dump document in stdout (fetch mode only)
|
||||
```
|
||||
|
||||
### Dump an URL
|
||||
|
||||
```console
|
||||
$ ./lightpanda-x86_64-linux --dump https://lightpanda.io
|
||||
info(browser): GET https://lightpanda.io/ http.Status.ok
|
||||
info(browser): fetch script https://api.website.lightpanda.io/js/script.js: http.Status.ok
|
||||
info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeError: Cannot read properties of undefined (reading 'pushState')
|
||||
<!DOCTYPE html>
|
||||
```
|
||||
|
||||
### Start a CDP server
|
||||
|
||||
```console
|
||||
$ ./lightpanda-x86_64-linux --host 127.0.0.1 --port 9222
|
||||
info(websocket): starting blocking worker to listen on 127.0.0.1:9222
|
||||
info(server): accepting new conn...
|
||||
```
|
||||
|
||||
Once the CDP server started, you can run a Puppeteer script by configuring the
|
||||
`browserWSEndpoint`.
|
||||
|
||||
```js
|
||||
'use scrict'
|
||||
|
||||
import puppeteer from 'puppeteer-core';
|
||||
|
||||
// use browserWSEndpoint to pass the Lightpanda's CDP server address.
|
||||
const browser = await puppeteer.connect({
|
||||
browserWSEndpoint: "ws://127.0.0.1:9222",
|
||||
});
|
||||
|
||||
// The rest of your script remains the same.
|
||||
const context = await browser.createBrowserContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('https://wikipedia.com/');
|
||||
|
||||
await page.close();
|
||||
await context.close();
|
||||
```
|
||||
|
||||
## Build from sources
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.13.0`. You have to
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.14.0`. You have to
|
||||
install it with the right version in order to build the project.
|
||||
|
||||
Lightpanda also depends on
|
||||
@@ -164,6 +165,11 @@ sudo apt install xz-utils \
|
||||
cmake clang
|
||||
```
|
||||
|
||||
For systems with [Nix](https://nixos.org/download/), you can use the devShell:
|
||||
```
|
||||
nix develop
|
||||
```
|
||||
|
||||
For MacOS, you only need cmake:
|
||||
|
||||
```
|
||||
@@ -176,9 +182,9 @@ brew install cmake
|
||||
|
||||
You can run `make install` to install deps all in one (or `make install-dev` if you need the development versions).
|
||||
|
||||
Be aware that the build task is very long and cpu consuming, as you will build from sources all dependancies, including the v8 Javascript engine.
|
||||
Be aware that the build task is very long and cpu consuming, as you will build from sources all dependencies, including the v8 Javascript engine.
|
||||
|
||||
#### Step by step build dependancy
|
||||
#### Step by step build dependency
|
||||
|
||||
The project uses git submodules for dependencies.
|
||||
|
||||
@@ -188,6 +194,14 @@ To init or update the submodules in the `vendor/` directory:
|
||||
make install-submodule
|
||||
```
|
||||
|
||||
**iconv**
|
||||
|
||||
libiconv is an internationalization library used by Netsurf.
|
||||
|
||||
```
|
||||
make install-libiconv
|
||||
```
|
||||
|
||||
**Netsurf libs**
|
||||
|
||||
Netsurf libs are used for HTML parsing and DOM tree generation.
|
||||
@@ -212,17 +226,21 @@ Note: when Mimalloc is built in dev mode, you can dump memory stats with the
|
||||
env var `MIMALLOC_SHOW_STATS=1`. See
|
||||
[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html).
|
||||
|
||||
**zig-js-runtime**
|
||||
**v8**
|
||||
|
||||
Our own Zig/Javascript runtime, which includes the v8 Javascript engine.
|
||||
|
||||
This build task is very long and cpu consuming, as you will build v8 from sources.
|
||||
First, get the tools necessary for building V8, as well as the V8 source code:
|
||||
|
||||
```
|
||||
make install-zig-js-runtime
|
||||
make get-v8
|
||||
```
|
||||
|
||||
For dev env, use `make iinstall-zig-js-runtime-dev`.
|
||||
Next, build v8. This build task is very long and cpu consuming, as you will build v8 from sources.
|
||||
|
||||
```
|
||||
make build-v8
|
||||
```
|
||||
|
||||
For dev env, use `make build-v8-dev`.
|
||||
|
||||
## Test
|
||||
|
||||
@@ -230,6 +248,20 @@ For dev env, use `make iinstall-zig-js-runtime-dev`.
|
||||
|
||||
You can test Lightpanda by running `make test`.
|
||||
|
||||
### End to end tests
|
||||
|
||||
To run end to end tests, you need to clone the [demo
|
||||
repository](https://github.com/lightpanda-io/demo) into `../demo` dir.
|
||||
|
||||
You have to install the [demo's node
|
||||
requirements](https://github.com/lightpanda-io/demo?tab=readme-ov-file#dependencies-1)
|
||||
|
||||
You also need to install [Go](https://go.dev) > v1.24.
|
||||
|
||||
```
|
||||
make end2end
|
||||
```
|
||||
|
||||
### Web Platform Tests
|
||||
|
||||
Lightpanda is tested against the standardized [Web Platform
|
||||
@@ -271,3 +303,28 @@ Lightpanda accepts pull requests through GitHub.
|
||||
|
||||
You have to sign our [CLA](CLA.md) during the pull request process otherwise
|
||||
we're not able to accept your contributions.
|
||||
|
||||
## Why?
|
||||
|
||||
### Javascript execution is mandatory for the modern web
|
||||
|
||||
In the good old days, scraping a webpage was as easy as making an HTTP request, cURL-like. It’s not possible anymore, because Javascript is everywhere, like it or not:
|
||||
|
||||
- Ajax, Single Page App, infinite loading, “click to display”, instant search, etc.
|
||||
- JS web frameworks: React, Vue, Angular & others
|
||||
|
||||
### Chrome is not the right tool
|
||||
|
||||
If we need Javascript, why not use a real web browser? Take a huge desktop application, hack it, and run it on the server. Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure it’s such a good idea?
|
||||
|
||||
- Heavy on RAM and CPU, expensive to run
|
||||
- Hard to package, deploy and maintain at scale
|
||||
- Bloated, lots of features are not useful in headless usage
|
||||
|
||||
### Lightpanda is built for performance
|
||||
|
||||
If we want both Javascript and performance in a true headless browser, we need to start from scratch. Not another iteration of Chromium, really from a blank page. Crazy right? But that’s what we did:
|
||||
|
||||
- Not based on Chromium, Blink or WebKit
|
||||
- Low-level system programming language (Zig) with optimisations in mind
|
||||
- Opinionated: without graphical rendering
|
||||
|
||||
331
build.zig
331
build.zig
@@ -17,16 +17,11 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const jsruntime_path = "vendor/zig-js-runtime/";
|
||||
const jsruntime = @import("vendor/zig-js-runtime/build.zig");
|
||||
const jsruntime_pkgs = jsruntime.packages(jsruntime_path);
|
||||
|
||||
/// Do not rename this constant. It is scanned by some scripts to determine
|
||||
/// which zig version to install.
|
||||
const recommended_zig_version = jsruntime.recommended_zig_version;
|
||||
const recommended_zig_version = "0.14.0";
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
||||
@@ -42,173 +37,190 @@ pub fn build(b: *std.Build) !void {
|
||||
},
|
||||
}
|
||||
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const mode = b.standardOptimizeOption(.{});
|
||||
|
||||
const options = jsruntime.buildOptions(b);
|
||||
|
||||
// browser
|
||||
// -------
|
||||
|
||||
// compile and install
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "lightpanda",
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(b, exe, options);
|
||||
b.installArtifact(exe);
|
||||
|
||||
// run
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
// shell
|
||||
// -----
|
||||
|
||||
// compile and install
|
||||
const shell = b.addExecutable(.{
|
||||
.name = "lightpanda-shell",
|
||||
.root_source_file = b.path("src/main_shell.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(b, shell, options);
|
||||
try jsruntime_pkgs.add_shell(shell);
|
||||
|
||||
// run
|
||||
const shell_cmd = b.addRunArtifact(shell);
|
||||
if (b.args) |args| {
|
||||
shell_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const shell_step = b.step("shell", "Run JS shell");
|
||||
shell_step.dependOn(&shell_cmd.step);
|
||||
|
||||
// test
|
||||
// ----
|
||||
|
||||
// compile
|
||||
const tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/run_tests.zig"),
|
||||
.test_runner = b.path("src/test_runner.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(b, tests, options);
|
||||
|
||||
// add jsruntime pretty deps
|
||||
tests.root_module.addAnonymousImport("pretty", .{
|
||||
.root_source_file = b.path("vendor/zig-js-runtime/src/pretty.zig"),
|
||||
});
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
if (b.args) |args| {
|
||||
run_tests.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_tests.step);
|
||||
|
||||
// wpt
|
||||
// -----
|
||||
|
||||
// compile and install
|
||||
const wpt = b.addExecutable(.{
|
||||
.name = "lightpanda-wpt",
|
||||
.root_source_file = b.path("src/main_wpt.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(b, wpt, options);
|
||||
|
||||
// run
|
||||
const wpt_cmd = b.addRunArtifact(wpt);
|
||||
if (b.args) |args| {
|
||||
wpt_cmd.addArgs(args);
|
||||
}
|
||||
// step
|
||||
const wpt_step = b.step("wpt", "WPT tests");
|
||||
wpt_step.dependOn(&wpt_cmd.step);
|
||||
}
|
||||
|
||||
fn common(
|
||||
b: *std.Build,
|
||||
step: *std.Build.Step.Compile,
|
||||
options: jsruntime.Options,
|
||||
) !void {
|
||||
const target = step.root_module.resolved_target.?;
|
||||
const jsruntimemod = try jsruntime_pkgs.module(
|
||||
b,
|
||||
options,
|
||||
step.root_module.optimize.?,
|
||||
target,
|
||||
var opts = b.addOptions();
|
||||
opts.addOption(
|
||||
[]const u8,
|
||||
"git_commit",
|
||||
b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
|
||||
);
|
||||
step.root_module.addImport("jsruntime", jsruntimemod);
|
||||
|
||||
const netsurf = try moduleNetSurf(b, target);
|
||||
netsurf.addImport("jsruntime", jsruntimemod);
|
||||
step.root_module.addImport("netsurf", netsurf);
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const asyncio = b.addModule("asyncio", .{
|
||||
.root_source_file = b.path("vendor/zig-async-io/src/lib.zig"),
|
||||
});
|
||||
step.root_module.addImport("asyncio", asyncio);
|
||||
{
|
||||
// browser
|
||||
// -------
|
||||
|
||||
const tlsmod = b.addModule("tls", .{
|
||||
.root_source_file = b.path("vendor/tls.zig/src/main.zig"),
|
||||
});
|
||||
step.root_module.addImport("tls", tlsmod);
|
||||
// compile and install
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "lightpanda",
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
});
|
||||
|
||||
const wsmod = b.addModule("websocket", .{
|
||||
.root_source_file = b.path("vendor/websocket.zig/src/websocket.zig"),
|
||||
});
|
||||
step.root_module.addImport("websocket", wsmod);
|
||||
try common(b, opts, exe);
|
||||
b.installArtifact(exe);
|
||||
|
||||
// run
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}
|
||||
|
||||
{
|
||||
// get v8
|
||||
// -------
|
||||
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
|
||||
const get_v8 = b.addRunArtifact(v8.artifact("get-v8"));
|
||||
const get_step = b.step("get-v8", "Get v8");
|
||||
get_step.dependOn(&get_v8.step);
|
||||
}
|
||||
|
||||
{
|
||||
// build v8
|
||||
// -------
|
||||
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
|
||||
const build_v8 = b.addRunArtifact(v8.artifact("build-v8"));
|
||||
const build_step = b.step("build-v8", "Build v8");
|
||||
build_step.dependOn(&build_v8.step);
|
||||
}
|
||||
|
||||
{
|
||||
// tests
|
||||
// ----
|
||||
|
||||
// compile
|
||||
const tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
try common(b, opts, tests);
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
if (b.args) |args| {
|
||||
run_tests.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const tests_step = b.step("test", "Run unit tests");
|
||||
tests_step.dependOn(&run_tests.step);
|
||||
}
|
||||
|
||||
{
|
||||
// wpt
|
||||
// -----
|
||||
|
||||
// compile and install
|
||||
const wpt = b.addExecutable(.{
|
||||
.name = "lightpanda-wpt",
|
||||
.root_source_file = b.path("src/main_wpt.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
try common(b, opts, wpt);
|
||||
|
||||
// run
|
||||
const wpt_cmd = b.addRunArtifact(wpt);
|
||||
if (b.args) |args| {
|
||||
wpt_cmd.addArgs(args);
|
||||
}
|
||||
// step
|
||||
const wpt_step = b.step("wpt", "WPT tests");
|
||||
wpt_step.dependOn(&wpt_cmd.step);
|
||||
}
|
||||
}
|
||||
|
||||
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
|
||||
const mod = b.addModule("netsurf", .{
|
||||
.root_source_file = b.path("src/netsurf/netsurf.zig"),
|
||||
.target = target,
|
||||
});
|
||||
fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Compile) !void {
|
||||
const mod = step.root_module;
|
||||
const target = mod.resolved_target.?;
|
||||
const optimize = mod.optimize.?;
|
||||
const dep_opts = .{ .target = target, .optimize = optimize };
|
||||
|
||||
try moduleNetSurf(b, step, target);
|
||||
mod.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
|
||||
mod.addImport("tigerbeetle-io", b.dependency("tigerbeetle_io", .{}).module("tigerbeetle_io"));
|
||||
|
||||
{
|
||||
// v8
|
||||
const v8_opts = b.addOptions();
|
||||
v8_opts.addOption(bool, "inspector_subtype", false);
|
||||
|
||||
const v8_mod = b.dependency("v8", dep_opts).module("v8");
|
||||
v8_mod.addOptions("default_exports", v8_opts);
|
||||
mod.addImport("v8", v8_mod);
|
||||
}
|
||||
|
||||
const mode_str: []const u8 = if (mod.optimize.? == .Debug) "debug" else "release";
|
||||
|
||||
// FIXME: we are tied to native v8 builds, currently:
|
||||
// - aarch64-macos
|
||||
// - x86_64-linux
|
||||
const os = target.result.os.tag;
|
||||
const arch = target.result.cpu.arch;
|
||||
switch (os) {
|
||||
.macos => {},
|
||||
.linux => {
|
||||
// TODO: why do we need it? It should be linked already when we built v8
|
||||
mod.link_libcpp = true;
|
||||
},
|
||||
else => return error.OsNotSupported,
|
||||
}
|
||||
|
||||
const lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
"v8/build/{s}-{s}/{s}/ninja/obj/zig/libc_v8.a",
|
||||
.{ @tagName(arch), @tagName(os), mode_str },
|
||||
);
|
||||
mod.addObjectFile(mod.owner.path(lib_path));
|
||||
mod.addImport("build_info", opts.createModule());
|
||||
}
|
||||
|
||||
fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build.ResolvedTarget) !void {
|
||||
const os = target.result.os.tag;
|
||||
const arch = target.result.cpu.arch;
|
||||
|
||||
// iconv
|
||||
const libiconv_lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
b.allocator,
|
||||
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
const libiconv_include_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
b.allocator,
|
||||
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addObjectFile(b.path(libiconv_lib_path));
|
||||
mod.addIncludePath(b.path(libiconv_include_path));
|
||||
step.addObjectFile(b.path(libiconv_lib_path));
|
||||
step.addIncludePath(b.path(libiconv_include_path));
|
||||
|
||||
// mimalloc
|
||||
mod.addImport("mimalloc", (try moduleMimalloc(b, target)));
|
||||
{
|
||||
// mimalloc
|
||||
const mimalloc = "vendor/mimalloc";
|
||||
const lib_path = try std.fmt.allocPrint(
|
||||
b.allocator,
|
||||
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
step.addObjectFile(b.path(lib_path));
|
||||
step.addIncludePath(b.path(mimalloc ++ "/include"));
|
||||
}
|
||||
|
||||
// netsurf libs
|
||||
const ns = "vendor/netsurf";
|
||||
const ns_include_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
b.allocator,
|
||||
ns ++ "/out/{s}-{s}/include",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addIncludePath(b.path(ns_include_path));
|
||||
step.addIncludePath(b.path(ns_include_path));
|
||||
|
||||
const libs: [4][]const u8 = .{
|
||||
"libdom",
|
||||
@@ -218,34 +230,11 @@ fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Mo
|
||||
};
|
||||
inline for (libs) |lib| {
|
||||
const ns_lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
b.allocator,
|
||||
ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addObjectFile(b.path(ns_lib_path));
|
||||
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
|
||||
step.addObjectFile(b.path(ns_lib_path));
|
||||
step.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
|
||||
}
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
fn moduleMimalloc(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
|
||||
const mod = b.addModule("mimalloc", .{
|
||||
.root_source_file = b.path("src/mimalloc/mimalloc.zig"),
|
||||
.target = target,
|
||||
});
|
||||
|
||||
const os = target.result.os.tag;
|
||||
const arch = target.result.cpu.arch;
|
||||
|
||||
const mimalloc = "vendor/mimalloc";
|
||||
const lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addObjectFile(b.path(lib_path));
|
||||
mod.addIncludePath(b.path(mimalloc ++ "/include"));
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
22
build.zig.zon
Normal file
22
build.zig.zon
Normal file
@@ -0,0 +1,22 @@
|
||||
.{
|
||||
.name = .browser,
|
||||
.paths = .{""},
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xda130f3af836cea0,
|
||||
.dependencies = .{
|
||||
.tls = .{
|
||||
.url = "https://github.com/ianic/tls.zig/archive/b29a8b45fc59fc2d202769c4f54509bb9e17d0a2.tar.gz",
|
||||
.hash = "tls-0.1.0-ER2e0uAxBQDm_TmSDdbiiyvAZoh4ejlDD4hW8Fl813xE",
|
||||
},
|
||||
.tigerbeetle_io = .{
|
||||
.url = "https://github.com/lightpanda-io/tigerbeetle-io/archive/61d9652f1a957b7f4db723ea6aa0ce9635e840ce.tar.gz",
|
||||
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
|
||||
},
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/f0c7eaaffe39f2f1a224fbe97e550daca0ca1801.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH62T4IADchAHFgo4nx79w1VedNDhIVErtSNgup-Tk",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
|
||||
},
|
||||
}
|
||||
202
flake.lock
generated
Normal file
202
flake.lock
generated
Normal file
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1733328505,
|
||||
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_3": {
|
||||
"inputs": {
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"iguana": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"zigPkgs": "zigPkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1746539192,
|
||||
"narHash": "sha256-32nN8JlRqNuCFfrDooyre+gDSnxZuCtK/qaHhRmGMhg=",
|
||||
"owner": "mookums",
|
||||
"repo": "iguana",
|
||||
"rev": "5569f95694edf59803429400ff6cb1c7522da801",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mookums",
|
||||
"repo": "iguana",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1746397377,
|
||||
"narHash": "sha256-5oLdRa3vWSRbuqPIFFmQBGGUqaYZBxX+GGtN9f/n4lU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ed30f8aba41605e3ab46421e3dcb4510ec560ff8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1746481231,
|
||||
"narHash": "sha256-U3VKPi5D2oLBFzaMI0jJLJp8J64ZLjz+EwodUS//QWc=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c6aca34d2ca2ce9e20b722f54e684cda64b275c2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "release-24.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"iguana": "iguana",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"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_3",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1746475050,
|
||||
"narHash": "sha256-KJC7BNY+NPCc1I+quGkWtoHXOMvFVEyer8Y0haOtTCA=",
|
||||
"owner": "mookums",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "dfa488aa462932e46f44fddf6677ff22f1244c22",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mookums",
|
||||
"repo": "zig-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
62
flake.nix
Normal file
62
flake.nix
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
description = "headless browser designed for AI and automation";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/release-24.11";
|
||||
|
||||
iguana.url = "github:mookums/iguana";
|
||||
iguana.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
iguana,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
zigVersion = "0_14_0";
|
||||
iguanaLib = iguana.lib.${system};
|
||||
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
(iguanaLib.mkZigOverlay zigVersion)
|
||||
(iguanaLib.mkZlsOverlay zigVersion)
|
||||
];
|
||||
};
|
||||
|
||||
# This build pipeline is very unhappy without an FHS-compliant env.
|
||||
fhs = pkgs.buildFHSUserEnv {
|
||||
name = "fhs-shell";
|
||||
targetPkgs =
|
||||
pkgs: with pkgs; [
|
||||
zig
|
||||
zls
|
||||
pkg-config
|
||||
cmake
|
||||
gperf
|
||||
expat.dev
|
||||
python3
|
||||
glib.dev
|
||||
glibc.dev
|
||||
zlib
|
||||
ninja
|
||||
gn
|
||||
gcc-unwrapped
|
||||
binutils
|
||||
clang
|
||||
clang-tools
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = fhs.env;
|
||||
}
|
||||
);
|
||||
}
|
||||
99
src/app.zig
Normal file
99
src/app.zig
Normal file
@@ -0,0 +1,99 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Loop = @import("runtime/loop.zig").Loop;
|
||||
const HttpClient = @import("http/client.zig").Client;
|
||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||
const Notification = @import("notification.zig").Notification;
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
// Container for global state / objects that various parts of the system
|
||||
// might need.
|
||||
pub const App = struct {
|
||||
loop: *Loop,
|
||||
config: Config,
|
||||
allocator: Allocator,
|
||||
telemetry: Telemetry,
|
||||
http_client: HttpClient,
|
||||
app_dir_path: ?[]const u8,
|
||||
notification: *Notification,
|
||||
|
||||
pub const RunMode = enum {
|
||||
help,
|
||||
fetch,
|
||||
serve,
|
||||
version,
|
||||
};
|
||||
|
||||
pub const Config = struct {
|
||||
run_mode: RunMode,
|
||||
gc_hints: bool = false,
|
||||
tls_verify_host: bool = true,
|
||||
};
|
||||
|
||||
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,
|
||||
.app_dir_path = app_dir_path,
|
||||
.notification = notification,
|
||||
.http_client = try HttpClient.init(allocator, 5, .{
|
||||
.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("failed to get lightpanda data dir: {}", .{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("failed to create lightpanda data dir: {}", .{err});
|
||||
return null;
|
||||
},
|
||||
};
|
||||
return app_dir_path;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
240
src/browser/console/console.zig
Normal file
240
src/browser/console/console.zig
Normal file
@@ -0,0 +1,240 @@
|
||||
// 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 JsObject = @import("../env.zig").Env.JsObject;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const log = if (builtin.is_test) &test_capture else std.log.scoped(.console);
|
||||
|
||||
pub const Console = struct {
|
||||
// TODO: configurable writer
|
||||
timers: std.StringHashMapUnmanaged(u32) = .{},
|
||||
counts: std.StringHashMapUnmanaged(u32) = .{},
|
||||
|
||||
pub fn _log(_: *const Console, values: []JsObject, state: *SessionState) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.info("{s}", .{try serializeValues(values, state)});
|
||||
}
|
||||
|
||||
pub fn _info(console: *const Console, values: []JsObject, state: *SessionState) !void {
|
||||
return console._log(values, state);
|
||||
}
|
||||
|
||||
pub fn _debug(_: *const Console, values: []JsObject, state: *SessionState) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.debug("{s}", .{try serializeValues(values, state)});
|
||||
}
|
||||
|
||||
pub fn _warn(_: *const Console, values: []JsObject, state: *SessionState) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.warn("{s}", .{try serializeValues(values, state)});
|
||||
}
|
||||
|
||||
pub fn _error(_: *const Console, values: []JsObject, state: *SessionState) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.err("{s}", .{try serializeValues(values, state)});
|
||||
}
|
||||
|
||||
pub fn _clear(_: *const Console) void {}
|
||||
|
||||
pub fn _count(self: *Console, label_: ?[]const u8, state: *SessionState) !void {
|
||||
const label = label_ orelse "default";
|
||||
const gop = try self.counts.getOrPut(state.arena, label);
|
||||
|
||||
var current: u32 = 0;
|
||||
if (gop.found_existing) {
|
||||
current = gop.value_ptr.*;
|
||||
} else {
|
||||
gop.key_ptr.* = try state.arena.dupe(u8, label);
|
||||
}
|
||||
|
||||
const count = current + 1;
|
||||
gop.value_ptr.* = count;
|
||||
|
||||
log.info("{s}: {d}", .{ label, count });
|
||||
}
|
||||
|
||||
pub fn _countReset(self: *Console, label_: ?[]const u8) !void {
|
||||
const label = label_ orelse "default";
|
||||
const kv = self.counts.fetchRemove(label) orelse {
|
||||
log.warn("Counter \"{s}\" doesn't exist.", .{label});
|
||||
return;
|
||||
};
|
||||
|
||||
log.info("{s}: {d}", .{ label, kv.value });
|
||||
}
|
||||
|
||||
pub fn _time(self: *Console, label_: ?[]const u8, state: *SessionState) !void {
|
||||
const label = label_ orelse "default";
|
||||
const gop = try self.timers.getOrPut(state.arena, label);
|
||||
|
||||
if (gop.found_existing) {
|
||||
log.warn("Timer \"{s}\" already exists.", .{label});
|
||||
return;
|
||||
}
|
||||
gop.key_ptr.* = try state.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.warn("Timer \"{s}\" doesn't exist.", .{label});
|
||||
return;
|
||||
};
|
||||
|
||||
log.info("\"{s}\": {d}ms", .{ label, 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.warn("Timer \"{s}\" doesn't exist.", .{label});
|
||||
return;
|
||||
};
|
||||
|
||||
log.info("\"{s}\": {d}ms - timer ended", .{ label, elapsed - kv.value });
|
||||
}
|
||||
|
||||
pub fn _assert(_: *Console, assertion: JsObject, values: []JsObject, state: *SessionState) !void {
|
||||
if (assertion.isTruthy()) {
|
||||
return;
|
||||
}
|
||||
var serialized_values: []const u8 = "";
|
||||
if (values.len > 0) {
|
||||
serialized_values = try serializeValues(values, state);
|
||||
}
|
||||
log.err("Assertion failed: {s}", .{serialized_values});
|
||||
}
|
||||
|
||||
fn serializeValues(values: []JsObject, state: *SessionState) ![]const u8 {
|
||||
const arena = state.call_arena;
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
try arr.appendSlice(arena, try values[0].toString());
|
||||
for (values[1..]) |value| {
|
||||
try arr.append(arena, ' ');
|
||||
try arr.appendSlice(arena, try value.toString());
|
||||
}
|
||||
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" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
defer testing.reset();
|
||||
|
||||
{
|
||||
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("a", captured[0]);
|
||||
try testing.expectEqual("hello world 23 true [object 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("Counter \"default\" doesn't exist.", captured[0]);
|
||||
try testing.expectEqual("default: 1", captured[1]);
|
||||
try testing.expectEqual("teg: 1", captured[2]);
|
||||
try testing.expectEqual("teg: 2", captured[3]);
|
||||
try testing.expectEqual("teg: 3", captured[4]);
|
||||
try testing.expectEqual("default: 2", captured[5]);
|
||||
try testing.expectEqual("teg: 3", captured[6]);
|
||||
try testing.expectEqual("default: 2", captured[7]);
|
||||
try testing.expectEqual("default: 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: ", captured[0]);
|
||||
try testing.expectEqual("Assertion failed: x true", captured[1]);
|
||||
try testing.expectEqual("Assertion failed: x", captured[2]);
|
||||
}
|
||||
}
|
||||
|
||||
const TestCapture = struct {
|
||||
captured: std.ArrayListUnmanaged([]const u8) = .{},
|
||||
|
||||
fn reset(self: *TestCapture) void {
|
||||
self.captured = .{};
|
||||
}
|
||||
|
||||
fn debug(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
|
||||
const str = std.fmt.allocPrint(testing.arena_allocator, fmt, args) catch unreachable;
|
||||
self.captured.append(testing.arena_allocator, str) catch unreachable;
|
||||
}
|
||||
|
||||
fn info(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
|
||||
self.debug(fmt, args);
|
||||
}
|
||||
|
||||
fn warn(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
|
||||
self.debug(fmt, args);
|
||||
}
|
||||
|
||||
fn err(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
|
||||
self.debug(fmt, args);
|
||||
}
|
||||
};
|
||||
82
src/browser/crypto/crypto.zig
Normal file
82
src/browser/crypto/crypto.zig
Normal file
@@ -0,0 +1,82 @@
|
||||
// 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 uuidv4 = @import("../../id.zig").uuidv4;
|
||||
|
||||
// https://w3c.github.io/webcrypto/#crypto-interface
|
||||
pub const Crypto = struct {
|
||||
pub fn _getRandomValues(_: *const Crypto, into: RandomValues) !void {
|
||||
const buf = into.asBuffer();
|
||||
if (buf.len > 65_536) {
|
||||
return error.QuotaExceededError;
|
||||
}
|
||||
std.crypto.random.bytes(buf);
|
||||
}
|
||||
|
||||
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 {
|
||||
switch (self) {
|
||||
.int8 => |b| return (@as([]u8, @ptrCast(b)))[0..b.len],
|
||||
.uint8 => |b| return (@as([]u8, @ptrCast(b)))[0..b.len],
|
||||
.int16 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
||||
.uint16 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
||||
.int32 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
||||
.uint32 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
||||
.int64 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
||||
.uint64 => |b| return (@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" },
|
||||
.{ "a.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" },
|
||||
.{ "crypto.getRandomValues(r1)", "undefined" },
|
||||
.{ "new Set(r1).size", "5" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
// Node implementation with Netsurf Libdom C lib.
|
||||
pub const Node = struct {
|
||||
@@ -19,7 +19,7 @@
|
||||
const std = @import("std");
|
||||
const css = @import("css.zig");
|
||||
const Node = @import("libdom.zig").Node;
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Matcher = struct {
|
||||
const Nodes = std.ArrayList(Node);
|
||||
79
src/browser/datauri.zig
Normal file
79
src/browser/datauri.zig
Normal file
@@ -0,0 +1,79 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// Represents https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data
|
||||
pub const DataURI = struct {
|
||||
was_base64_encoded: bool,
|
||||
// The contents in the uri. It will be base64 decoded but not prepared in
|
||||
// any way for mime.charset.
|
||||
data: []const u8,
|
||||
|
||||
// Parses data:[<media-type>][;base64],<data>
|
||||
pub fn parse(allocator: Allocator, src: []const u8) !?DataURI {
|
||||
if (!std.mem.startsWith(u8, src, "data:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uri = src[5..];
|
||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
||||
|
||||
// Extract the encoding.
|
||||
var metadata = uri[0..data_starts];
|
||||
var base64_encoded = false;
|
||||
if (std.mem.endsWith(u8, metadata, ";base64")) {
|
||||
base64_encoded = true;
|
||||
metadata = metadata[0 .. metadata.len - 7];
|
||||
}
|
||||
|
||||
// TODO: Extract mime type. This not trivial because Mime.parse requires
|
||||
// a []u8 and might mutate the src. And, the DataURI.parse references atm
|
||||
// do not have deinit calls.
|
||||
|
||||
// Prepare the data.
|
||||
var data = uri[data_starts + 1 ..];
|
||||
if (base64_encoded) {
|
||||
const decoder = std.base64.standard.Decoder;
|
||||
const decoded_size = try decoder.calcSizeForSlice(data);
|
||||
|
||||
const buffer = try allocator.alloc(u8, decoded_size);
|
||||
errdefer allocator.free(buffer);
|
||||
|
||||
try decoder.decode(buffer, data);
|
||||
data = buffer;
|
||||
}
|
||||
|
||||
return .{
|
||||
.was_base64_encoded = base64_encoded,
|
||||
.data = data,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const DataURI, allocator: Allocator) void {
|
||||
if (self.was_base64_encoded) {
|
||||
allocator.free(self.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = std.testing;
|
||||
test "DataURI: parse valid" {
|
||||
try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo");
|
||||
try test_valid("data:text/javascript; charset=utf-8;,foo", "foo");
|
||||
try test_valid("data:,foo", "foo");
|
||||
}
|
||||
|
||||
test "DataURI: parse invalid" {
|
||||
try test_cannot_parse("atad:,foo");
|
||||
try test_cannot_parse("data:foo");
|
||||
try test_cannot_parse("data:");
|
||||
}
|
||||
|
||||
fn test_valid(uri: []const u8, expected: []const u8) !void {
|
||||
const data_uri = try DataURI.parse(std.testing.allocator, uri) orelse return error.TestFailed;
|
||||
defer data_uri.deinit(testing.allocator);
|
||||
try testing.expectEqualStrings(expected, data_uri.data);
|
||||
}
|
||||
|
||||
fn test_cannot_parse(uri: []const u8) !void {
|
||||
try testing.expectEqual(null, DataURI.parse(std.testing.allocator, uri));
|
||||
}
|
||||
@@ -16,22 +16,15 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#attr
|
||||
pub const Attr = struct {
|
||||
pub const Self = parser.Attribute;
|
||||
pub const prototype = *Node;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_namespaceURI(self: *parser.Attribute) !?[]const u8 {
|
||||
return try parser.nodeGetNamespace(parser.attributeToNode(self));
|
||||
@@ -70,34 +63,33 @@ pub const Attr = struct {
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var getters = [_]Case{
|
||||
.{ .src = "let a = document.createAttributeNS('foo', 'bar')", .ex = "undefined" },
|
||||
.{ .src = "a.namespaceURI", .ex = "foo" },
|
||||
.{ .src = "a.prefix", .ex = "null" },
|
||||
.{ .src = "a.localName", .ex = "bar" },
|
||||
.{ .src = "a.name", .ex = "bar" },
|
||||
.{ .src = "a.value", .ex = "" },
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Attribute" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.createAttributeNS('foo', 'bar')", "undefined" },
|
||||
.{ "a.namespaceURI", "foo" },
|
||||
.{ "a.prefix", "null" },
|
||||
.{ "a.localName", "bar" },
|
||||
.{ "a.name", "bar" },
|
||||
.{ "a.value", "" },
|
||||
// TODO: libdom has a bug here: the created attr has no parent, it
|
||||
// causes a panic w/ libdom when setting the value.
|
||||
//.{ .src = "a.value = 'nok'", .ex = "nok" },
|
||||
.{ .src = "a.ownerElement", .ex = "null" },
|
||||
};
|
||||
try checkCases(js_env, &getters);
|
||||
//.{ "a.value = 'nok'", "nok" },
|
||||
.{ "a.ownerElement", "null" },
|
||||
}, .{});
|
||||
|
||||
var attr = [_]Case{
|
||||
.{ .src = "let b = document.getElementById('link').getAttributeNode('class')", .ex = "undefined" },
|
||||
.{ .src = "b.name", .ex = "class" },
|
||||
.{ .src = "b.value", .ex = "ok" },
|
||||
.{ .src = "b.value = 'nok'", .ex = "nok" },
|
||||
.{ .src = "b.value", .ex = "nok" },
|
||||
.{ .src = "b.value = null", .ex = "null" },
|
||||
.{ .src = "b.value", .ex = "null" },
|
||||
.{ .src = "b.value = 'ok'", .ex = "ok" },
|
||||
.{ .src = "b.ownerElement.id", .ex = "link" },
|
||||
};
|
||||
try checkCases(js_env, &attr);
|
||||
try runner.testCases(&.{
|
||||
.{ "let b = document.getElementById('link').getAttributeNode('class')", "undefined" },
|
||||
.{ "b.name", "class" },
|
||||
.{ "b.value", "ok" },
|
||||
.{ "b.value = 'nok'", "nok" },
|
||||
.{ "b.value", "nok" },
|
||||
.{ "b.value = null", "null" },
|
||||
.{ "b.value", "null" },
|
||||
.{ "b.value = 'ok'", "ok" },
|
||||
.{ "b.ownerElement.id", "link" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -16,9 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Text = @import("text.zig").Text;
|
||||
|
||||
@@ -26,5 +24,5 @@ const Text = @import("text.zig").Text;
|
||||
pub const CDATASection = struct {
|
||||
pub const Self = parser.CDATASection;
|
||||
pub const prototype = *Text;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
@@ -18,12 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
const Comment = @import("comment.zig").Comment;
|
||||
@@ -32,18 +27,18 @@ const ProcessingInstruction = @import("processing_instruction.zig").ProcessingIn
|
||||
const HTMLElem = @import("../html/elements.zig");
|
||||
|
||||
// CharacterData interfaces
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
pub const Interfaces = .{
|
||||
Comment,
|
||||
Text.Text,
|
||||
Text.Interfaces,
|
||||
ProcessingInstruction,
|
||||
});
|
||||
};
|
||||
|
||||
// CharacterData implementation
|
||||
pub const CharacterData = struct {
|
||||
pub const Self = parser.CharacterData;
|
||||
pub const prototype = *Node;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
@@ -102,79 +97,90 @@ pub const CharacterData = struct {
|
||||
pub fn _substringData(self: *parser.CharacterData, offset: u32, count: u32) ![]const u8 {
|
||||
return try parser.characterDataSubstringData(self, offset, count);
|
||||
}
|
||||
|
||||
// netsurf's CharacterData (text, comment) doesn't implement the
|
||||
// dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode.
|
||||
pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) !bool {
|
||||
if (try parser.nodeType(@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;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var get_data = [_]Case{
|
||||
.{ .src = "let link = document.getElementById('link')", .ex = "undefined" },
|
||||
.{ .src = "let cdata = link.firstChild", .ex = "undefined" },
|
||||
.{ .src = "cdata.data", .ex = "OK" },
|
||||
};
|
||||
try checkCases(js_env, &get_data);
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.CharacterData" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
var set_data = [_]Case{
|
||||
.{ .src = "cdata.data = 'OK modified'", .ex = "OK modified" },
|
||||
.{ .src = "cdata.data === 'OK modified'", .ex = "true" },
|
||||
.{ .src = "cdata.data = 'OK'", .ex = "OK" },
|
||||
};
|
||||
try checkCases(js_env, &set_data);
|
||||
try runner.testCases(&.{
|
||||
.{ "let link = document.getElementById('link')", "undefined" },
|
||||
.{ "let cdata = link.firstChild", "undefined" },
|
||||
.{ "cdata.data", "OK" },
|
||||
}, .{});
|
||||
|
||||
var get_length = [_]Case{
|
||||
.{ .src = "cdata.length === 2", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &get_length);
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.data = 'OK modified'", "OK modified" },
|
||||
.{ "cdata.data === 'OK modified'", "true" },
|
||||
.{ "cdata.data = 'OK'", "OK" },
|
||||
}, .{});
|
||||
|
||||
var get_next_elem_sibling = [_]Case{
|
||||
.{ .src = "cdata.nextElementSibling === null", .ex = "true" },
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.length === 2", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.nextElementSibling === null", "true" },
|
||||
// create a next element
|
||||
.{ .src = "let next = document.createElement('a')", .ex = "undefined" },
|
||||
.{ .src = "link.appendChild(next, cdata) !== undefined", .ex = "true" },
|
||||
.{ .src = "cdata.nextElementSibling.localName === 'a' ", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &get_next_elem_sibling);
|
||||
.{ "let next = document.createElement('a')", "undefined" },
|
||||
.{ "link.appendChild(next, cdata) !== undefined", "true" },
|
||||
.{ "cdata.nextElementSibling.localName === 'a' ", "true" },
|
||||
}, .{});
|
||||
|
||||
var get_prev_elem_sibling = [_]Case{
|
||||
.{ .src = "cdata.previousElementSibling === null", .ex = "true" },
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.previousElementSibling === null", "true" },
|
||||
// create a prev element
|
||||
.{ .src = "let prev = document.createElement('div')", .ex = "undefined" },
|
||||
.{ .src = "link.insertBefore(prev, cdata) !== undefined", .ex = "true" },
|
||||
.{ .src = "cdata.previousElementSibling.localName === 'div' ", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &get_prev_elem_sibling);
|
||||
.{ "let prev = document.createElement('div')", "undefined" },
|
||||
.{ "link.insertBefore(prev, cdata) !== undefined", "true" },
|
||||
.{ "cdata.previousElementSibling.localName === 'div' ", "true" },
|
||||
}, .{});
|
||||
|
||||
var append_data = [_]Case{
|
||||
.{ .src = "cdata.appendData(' modified')", .ex = "undefined" },
|
||||
.{ .src = "cdata.data === 'OK modified' ", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &append_data);
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.appendData(' modified')", "undefined" },
|
||||
.{ "cdata.data === 'OK modified' ", "true" },
|
||||
}, .{});
|
||||
|
||||
var delete_data = [_]Case{
|
||||
.{ .src = "cdata.deleteData('OK'.length, ' modified'.length)", .ex = "undefined" },
|
||||
.{ .src = "cdata.data == 'OK'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &delete_data);
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.deleteData('OK'.length, ' modified'.length)", "undefined" },
|
||||
.{ "cdata.data == 'OK'", "true" },
|
||||
}, .{});
|
||||
|
||||
var insert_data = [_]Case{
|
||||
.{ .src = "cdata.insertData('OK'.length-1, 'modified')", .ex = "undefined" },
|
||||
.{ .src = "cdata.data == 'OmodifiedK'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &insert_data);
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.insertData('OK'.length-1, 'modified')", "undefined" },
|
||||
.{ "cdata.data == 'OmodifiedK'", "true" },
|
||||
}, .{});
|
||||
|
||||
var replace_data = [_]Case{
|
||||
.{ .src = "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", .ex = "undefined" },
|
||||
.{ .src = "cdata.data == 'OreplacedK'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &replace_data);
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", "undefined" },
|
||||
.{ "cdata.data == 'OreplacedK'", "true" },
|
||||
}, .{});
|
||||
|
||||
var substring_data = [_]Case{
|
||||
.{ .src = "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", .ex = "true" },
|
||||
.{ .src = "cdata.substringData('OK'.length-1, 0) == ''", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &substring_data);
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
|
||||
.{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
|
||||
.{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -15,27 +15,22 @@
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const CharacterData = @import("character_data.zig").CharacterData;
|
||||
|
||||
const UserContext = @import("../user_context.zig").UserContext;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-comment
|
||||
pub const Comment = struct {
|
||||
pub const Self = parser.Comment;
|
||||
pub const prototype = *CharacterData;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Comment {
|
||||
pub fn constructor(data: ?[]const u8, state: *const SessionState) !*parser.Comment {
|
||||
return parser.documentCreateComment(
|
||||
parser.documentHTMLToDocument(userctx.document),
|
||||
parser.documentHTMLToDocument(state.document.?),
|
||||
data orelse "",
|
||||
);
|
||||
}
|
||||
@@ -44,16 +39,16 @@ pub const Comment = struct {
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var constructor = [_]Case{
|
||||
.{ .src = "let comment = new Comment('foo')", .ex = "undefined" },
|
||||
.{ .src = "comment.data", .ex = "foo" },
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Comment" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
.{ .src = "let emptycomment = new Comment()", .ex = "undefined" },
|
||||
.{ .src = "emptycomment.data", .ex = "" },
|
||||
};
|
||||
try checkCases(js_env, &constructor);
|
||||
try runner.testCases(&.{
|
||||
.{ "let comment = new Comment('foo')", "undefined" },
|
||||
.{ "comment.data", "foo" },
|
||||
|
||||
.{ "let emptycomment = new Comment()", "undefined" },
|
||||
.{ "emptycomment.data", "" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const css = @import("../css/css.zig");
|
||||
const Node = @import("../css/libdom.zig").Node;
|
||||
@@ -49,7 +49,7 @@ const MatchAll = struct {
|
||||
fn init(alloc: std.mem.Allocator) MatchAll {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.nl = NodeList.init(),
|
||||
.nl = .{},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ const MatchAll = struct {
|
||||
}
|
||||
|
||||
fn toOwnedList(m: *MatchAll) NodeList {
|
||||
defer m.nl = NodeList.init();
|
||||
// reset it.
|
||||
defer m.nl = .{};
|
||||
return m.nl;
|
||||
}
|
||||
};
|
||||
443
src/browser/dom/document.zig
Normal file
443
src/browser/dom/document.zig
Normal file
@@ -0,0 +1,443 @@
|
||||
// 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 SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
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 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(state: *const SessionState) !*parser.DocumentHTML {
|
||||
const doc = try parser.documentCreateDocument(
|
||||
try parser.documentHTMLGetTitle(state.document.?),
|
||||
);
|
||||
|
||||
// we have to work w/ document instead of html document.
|
||||
const ddoc = parser.documentHTMLToDocument(doc);
|
||||
const ccur = parser.documentHTMLToDocument(state.document.?);
|
||||
try parser.documentSetDocumentURI(ddoc, try parser.documentGetDocumentURI(ccur));
|
||||
try parser.documentSetInputEncoding(ddoc, try parser.documentGetInputEncoding(ccur));
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
pub fn get_implementation(_: *parser.Document) DOMImplementation {
|
||||
return DOMImplementation{};
|
||||
}
|
||||
|
||||
pub fn get_documentElement(self: *parser.Document) !?ElementUnion {
|
||||
const e = try parser.documentGetDocumentElement(self);
|
||||
if (e == null) return null;
|
||||
return try Element.toInterface(e.?);
|
||||
}
|
||||
|
||||
pub fn get_documentURI(self: *parser.Document) ![]const u8 {
|
||||
return try parser.documentGetDocumentURI(self);
|
||||
}
|
||||
|
||||
pub fn get_URL(self: *parser.Document) ![]const u8 {
|
||||
return try get_documentURI(self);
|
||||
}
|
||||
|
||||
// TODO implement contentType
|
||||
pub fn get_contentType(self: *parser.Document) []const u8 {
|
||||
_ = self;
|
||||
return "text/html";
|
||||
}
|
||||
|
||||
// TODO implement compactMode
|
||||
pub fn get_compatMode(self: *parser.Document) []const u8 {
|
||||
_ = self;
|
||||
return "CSS1Compat";
|
||||
}
|
||||
|
||||
pub fn get_characterSet(self: *parser.Document) ![]const u8 {
|
||||
return try parser.documentGetInputEncoding(self);
|
||||
}
|
||||
|
||||
// alias of get_characterSet
|
||||
pub fn get_charset(self: *parser.Document) ![]const u8 {
|
||||
return try get_characterSet(self);
|
||||
}
|
||||
|
||||
// alias of get_characterSet
|
||||
pub fn get_inputEncoding(self: *parser.Document) ![]const u8 {
|
||||
return try get_characterSet(self);
|
||||
}
|
||||
|
||||
pub fn get_doctype(self: *parser.Document) !?*parser.DocumentType {
|
||||
return try parser.documentGetDoctype(self);
|
||||
}
|
||||
|
||||
pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !*parser.Event {
|
||||
// TODO: for now only "Event" constructor is supported
|
||||
// see table on https://dom.spec.whatwg.org/#dom-document-createevent $2
|
||||
if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) {
|
||||
return try parser.eventCreate();
|
||||
}
|
||||
return parser.DOMError.NotSupported;
|
||||
}
|
||||
|
||||
pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion {
|
||||
const e = try parser.documentGetElementById(self, id) orelse return null;
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
|
||||
pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
|
||||
const e = try parser.documentCreateElement(self, tag_name);
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
|
||||
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {
|
||||
const e = try parser.documentCreateElementNS(self, ns, tag_name);
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
|
||||
// We can't simply use libdom dom_document_get_elements_by_tag_name here.
|
||||
// Indeed, netsurf implemented a previous dom spec when
|
||||
// getElementsByTagName returned a NodeList.
|
||||
// But since
|
||||
// https://github.com/whatwg/dom/commit/190700b7c12ecfd3b5ebdb359ab1d6ea9cbf7749
|
||||
// the spec changed to return an HTMLCollection instead.
|
||||
// That's why we reimplemented getElementsByTagName by using an
|
||||
// HTMLCollection in zig here.
|
||||
pub fn _getElementsByTagName(
|
||||
self: *parser.Document,
|
||||
tag_name: []const u8,
|
||||
state: *SessionState,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(state.arena, parser.documentToNode(self), tag_name, true);
|
||||
}
|
||||
|
||||
pub fn _getElementsByClassName(
|
||||
self: *parser.Document,
|
||||
classNames: []const u8,
|
||||
state: *SessionState,
|
||||
) !collection.HTMLCollection {
|
||||
const allocator = state.arena;
|
||||
return try collection.HTMLCollectionByClassName(allocator, 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, state: *SessionState) !?ElementUnion {
|
||||
if (selector.len == 0) return null;
|
||||
|
||||
const allocator = state.arena;
|
||||
const n = try css.querySelector(allocator, 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, state: *SessionState) !NodeList {
|
||||
const allocator = state.arena;
|
||||
return css.querySelectorAll(allocator, parser.documentToNode(self), selector);
|
||||
}
|
||||
|
||||
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
|
||||
// function must accept either node or string.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
pub fn _prepend(self: *parser.Document, nodes: []const *parser.Node) !void {
|
||||
return Node.prepend(parser.documentToNode(self), nodes);
|
||||
}
|
||||
|
||||
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
|
||||
// function must accept either node or string.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
pub fn _append(self: *parser.Document, nodes: []const *parser.Node) !void {
|
||||
return Node.append(parser.documentToNode(self), nodes);
|
||||
}
|
||||
|
||||
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
|
||||
// function must accept either node or string.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
pub fn _replaceChildren(self: *parser.Document, nodes: []const *parser.Node) !void {
|
||||
return Node.replaceChildren(parser.documentToNode(self), nodes);
|
||||
}
|
||||
|
||||
pub fn deinit(_: *parser.Document, _: std.mem.Allocator) void {}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Document" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
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",
|
||||
},
|
||||
}, .{});
|
||||
|
||||
// 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, .{});
|
||||
}
|
||||
64
src/browser/dom/document_fragment.zig
Normal file
64
src/browser/dom/document_fragment.zig
Normal file
@@ -0,0 +1,64 @@
|
||||
// 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 SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
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(state: *const SessionState) !*parser.DocumentFragment {
|
||||
return parser.documentCreateDocumentFragment(
|
||||
parser.documentHTMLToDocument(state.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;
|
||||
}
|
||||
};
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
80
src/browser/dom/document_type.zig
Normal file
80
src/browser/dom/document_type.zig
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#documenttype
|
||||
pub const DocumentType = struct {
|
||||
pub const Self = parser.DocumentType;
|
||||
pub const prototype = *Node;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_name(self: *parser.DocumentType) ![]const u8 {
|
||||
return try parser.documentTypeGetName(self);
|
||||
}
|
||||
|
||||
pub fn get_publicId(self: *parser.DocumentType) ![]const u8 {
|
||||
return try parser.documentTypeGetPublicId(self);
|
||||
}
|
||||
|
||||
pub fn get_systemId(self: *parser.DocumentType) ![]const u8 {
|
||||
return try parser.documentTypeGetSystemId(self);
|
||||
}
|
||||
|
||||
// netsurf's DocumentType doesn't implement the dom_node_get_attributes
|
||||
// and thus will crash if we try to call nodeIsEqualNode.
|
||||
pub fn _isEqualNode(self: *parser.DocumentType, other_node: *parser.Node) !bool {
|
||||
if (try parser.nodeType(other_node) != .document_type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const other: *parser.DocumentType = @ptrCast(other_node);
|
||||
if (std.mem.eql(u8, try get_name(self), try get_name(other)) == false) {
|
||||
return false;
|
||||
}
|
||||
if (std.mem.eql(u8, try get_publicId(self), try get_publicId(other)) == false) {
|
||||
return false;
|
||||
}
|
||||
if (std.mem.eql(u8, try get_systemId(self), try get_systemId(other)) == false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.DocumentType" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let dt1 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');", "undefined" },
|
||||
.{ "let dt2 = document.implementation.createDocumentType('qname2', 'pid2', 'sys2');", "undefined" },
|
||||
.{ "let dt3 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');", "undefined" },
|
||||
.{ "dt1.isEqualNode(dt1)", "true" },
|
||||
.{ "dt1.isEqualNode(dt3)", "true" },
|
||||
.{ "dt1.isEqualNode(dt2)", "false" },
|
||||
.{ "dt2.isEqualNode(dt3)", "false" },
|
||||
.{ "dt1.isEqualNode(document)", "false" },
|
||||
.{ "document.isEqualNode(dt1)", "false" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -16,25 +16,25 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
const EventTarget = @import("event_target.zig").EventTarget;
|
||||
const DOMImplementation = @import("implementation.zig").DOMImplementation;
|
||||
const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
|
||||
const DOMTokenList = @import("token_list.zig").DOMTokenList;
|
||||
const DOMTokenList = @import("token_list.zig");
|
||||
const NodeList = @import("nodelist.zig");
|
||||
const Nod = @import("node.zig");
|
||||
const Node = @import("node.zig");
|
||||
const MutationObserver = @import("mutation_observer.zig");
|
||||
const IntersectionObserver = @import("intersection_observer.zig");
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
pub const Interfaces = .{
|
||||
DOMException,
|
||||
EventTarget,
|
||||
DOMImplementation,
|
||||
NamedNodeMap,
|
||||
DOMTokenList,
|
||||
DOMTokenList.Interfaces,
|
||||
NodeList.Interfaces,
|
||||
Nod.Node,
|
||||
Nod.Interfaces,
|
||||
Node.Node,
|
||||
Node.Interfaces,
|
||||
MutationObserver.Interfaces,
|
||||
});
|
||||
IntersectionObserver.Interfaces,
|
||||
};
|
||||
@@ -18,15 +18,11 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const Variadic = jsruntime.Variadic;
|
||||
const parser = @import("../netsurf.zig");
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const collection = @import("html_collection.zig");
|
||||
const writeNode = @import("../browser/dump.zig").writeNode;
|
||||
const dump = @import("../dump.zig");
|
||||
const css = @import("css.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
@@ -35,16 +31,22 @@ const NodeList = @import("nodelist.zig").NodeList;
|
||||
const HTMLElem = @import("../html/elements.zig");
|
||||
pub const Union = @import("../html/elements.zig").Union;
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#element
|
||||
pub const Element = struct {
|
||||
pub const Self = parser.Element;
|
||||
pub const prototype = *Node;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub const DOMRect = struct {
|
||||
x: f64,
|
||||
y: f64,
|
||||
width: f64,
|
||||
height: f64,
|
||||
};
|
||||
|
||||
pub fn toInterface(e: *parser.Element) !Union {
|
||||
return try HTMLElem.toInterface(Union, e);
|
||||
// SVGElement and MathML are not supported yet.
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
@@ -95,17 +97,20 @@ pub const Element = struct {
|
||||
}
|
||||
|
||||
pub fn get_attributes(self: *parser.Element) !*parser.NamedNodeMap {
|
||||
return try parser.nodeGetAttributes(parser.elementToNode(self));
|
||||
// An element must have non-nil attributes.
|
||||
return try parser.nodeGetAttributes(parser.elementToNode(self)) orelse unreachable;
|
||||
}
|
||||
|
||||
pub fn get_innerHTML(self: *parser.Element, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
pub fn get_innerHTML(self: *parser.Element, state: *SessionState) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(state.arena);
|
||||
try dump.writeChildren(parser.elementToNode(self), buf.writer());
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
try writeNode(parser.elementToNode(self), buf.writer());
|
||||
// TODO express the caller owned the slice.
|
||||
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
|
||||
return buf.toOwnedSlice();
|
||||
pub fn get_outerHTML(self: *parser.Element, state: *SessionState) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(state.arena);
|
||||
try dump.writeNode(parser.elementToNode(self), buf.writer());
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn set_innerHTML(self: *parser.Element, str: []const u8) !void {
|
||||
@@ -214,11 +219,11 @@ pub const Element = struct {
|
||||
|
||||
pub fn _getElementsByTagName(
|
||||
self: *parser.Element,
|
||||
alloc: std.mem.Allocator,
|
||||
tag_name: []const u8,
|
||||
state: *SessionState,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(
|
||||
alloc,
|
||||
state.arena,
|
||||
parser.elementToNode(self),
|
||||
tag_name,
|
||||
false,
|
||||
@@ -227,11 +232,11 @@ pub const Element = struct {
|
||||
|
||||
pub fn _getElementsByClassName(
|
||||
self: *parser.Element,
|
||||
alloc: std.mem.Allocator,
|
||||
classNames: []const u8,
|
||||
state: *SessionState,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByClassName(
|
||||
alloc,
|
||||
state.arena,
|
||||
parser.elementToNode(self),
|
||||
classNames,
|
||||
false,
|
||||
@@ -294,180 +299,234 @@ pub const Element = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _querySelector(self: *parser.Element, alloc: std.mem.Allocator, selector: []const u8) !?Union {
|
||||
pub fn _querySelector(self: *parser.Element, selector: []const u8, state: *SessionState) !?Union {
|
||||
if (selector.len == 0) return null;
|
||||
|
||||
const n = try css.querySelector(alloc, parser.elementToNode(self), selector);
|
||||
const n = try css.querySelector(state.arena, parser.elementToNode(self), selector);
|
||||
|
||||
if (n == null) return null;
|
||||
|
||||
return try toInterface(parser.nodeToElement(n.?));
|
||||
}
|
||||
|
||||
pub fn _querySelectorAll(self: *parser.Element, alloc: std.mem.Allocator, selector: []const u8) !NodeList {
|
||||
return css.querySelectorAll(alloc, parser.elementToNode(self), selector);
|
||||
pub fn _querySelectorAll(self: *parser.Element, selector: []const u8, state: *SessionState) !NodeList {
|
||||
return css.querySelectorAll(state.arena, parser.elementToNode(self), selector);
|
||||
}
|
||||
|
||||
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
|
||||
// function must accept either node or string.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
pub fn _prepend(self: *parser.Element, nodes: ?Variadic(*parser.Node)) !void {
|
||||
pub fn _prepend(self: *parser.Element, nodes: []const *parser.Node) !void {
|
||||
return Node.prepend(parser.elementToNode(self), nodes);
|
||||
}
|
||||
|
||||
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
|
||||
// function must accept either node or string.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
pub fn _append(self: *parser.Element, nodes: ?Variadic(*parser.Node)) !void {
|
||||
pub fn _append(self: *parser.Element, nodes: []const *parser.Node) !void {
|
||||
return Node.append(parser.elementToNode(self), nodes);
|
||||
}
|
||||
|
||||
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
|
||||
// function must accept either node or string.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
pub fn _replaceChildren(self: *parser.Element, nodes: ?Variadic(*parser.Node)) !void {
|
||||
pub fn _replaceChildren(self: *parser.Element, nodes: []const *parser.Node) !void {
|
||||
return Node.replaceChildren(parser.elementToNode(self), nodes);
|
||||
}
|
||||
|
||||
pub fn deinit(_: *parser.Element, _: std.mem.Allocator) void {}
|
||||
pub fn _getBoundingClientRect(self: *parser.Element, state: *SessionState) !DOMRect {
|
||||
return state.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 just always return the element's rect.
|
||||
pub fn _getClientRects(self: *parser.Element, state: *SessionState) ![1]DOMRect {
|
||||
return [_]DOMRect{try state.renderer.getRect(self)};
|
||||
}
|
||||
|
||||
pub fn get_clientWidth(_: *parser.Element, state: *SessionState) u32 {
|
||||
return state.renderer.width();
|
||||
}
|
||||
|
||||
pub fn get_clientHeight(_: *parser.Element, state: *SessionState) u32 {
|
||||
return state.renderer.height();
|
||||
}
|
||||
|
||||
pub fn _matches(self: *parser.Element, selectors: []const u8, state: *SessionState) !bool {
|
||||
const cssParse = @import("../css/css.zig").parse;
|
||||
const CssNodeWrap = @import("../css/libdom.zig").Node;
|
||||
const s = try cssParse(state.call_arena, selectors, .{});
|
||||
return s.match(CssNodeWrap{ .node = parser.elementToNode(self) });
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var getters = [_]Case{
|
||||
.{ .src = "let g = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "g.namespaceURI", .ex = "http://www.w3.org/1999/xhtml" },
|
||||
.{ .src = "g.prefix", .ex = "null" },
|
||||
.{ .src = "g.localName", .ex = "div" },
|
||||
.{ .src = "g.tagName", .ex = "DIV" },
|
||||
};
|
||||
try checkCases(js_env, &getters);
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Element" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
var gettersetters = [_]Case{
|
||||
.{ .src = "let gs = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "gs.id", .ex = "content" },
|
||||
.{ .src = "gs.id = 'foo'", .ex = "foo" },
|
||||
.{ .src = "gs.id", .ex = "foo" },
|
||||
.{ .src = "gs.id = 'content'", .ex = "content" },
|
||||
.{ .src = "gs.className", .ex = "" },
|
||||
.{ .src = "let gs2 = document.getElementById('para-empty')", .ex = "undefined" },
|
||||
.{ .src = "gs2.className", .ex = "ok empty" },
|
||||
.{ .src = "gs2.className = 'foo bar baz'", .ex = "foo bar baz" },
|
||||
.{ .src = "gs2.className", .ex = "foo bar baz" },
|
||||
.{ .src = "gs2.className = 'ok empty'", .ex = "ok empty" },
|
||||
.{ .src = "let cl = gs2.classList", .ex = "undefined" },
|
||||
.{ .src = "cl.length", .ex = "2" },
|
||||
};
|
||||
try checkCases(js_env, &gettersetters);
|
||||
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" },
|
||||
}, .{});
|
||||
|
||||
var attribute = [_]Case{
|
||||
.{ .src = "let a = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "a.hasAttributes()", .ex = "true" },
|
||||
.{ .src = "a.attributes.length", .ex = "1" },
|
||||
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" },
|
||||
}, .{});
|
||||
|
||||
.{ .src = "a.getAttribute('id')", .ex = "content" },
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.getElementById('content')", "undefined" },
|
||||
.{ "a.hasAttributes()", "true" },
|
||||
.{ "a.attributes.length", "1" },
|
||||
|
||||
.{ .src = "a.hasAttribute('foo')", .ex = "false" },
|
||||
.{ .src = "a.getAttribute('foo')", .ex = "null" },
|
||||
.{ "a.getAttribute('id')", "content" },
|
||||
|
||||
.{ .src = "a.setAttribute('foo', 'bar')", .ex = "undefined" },
|
||||
.{ .src = "a.hasAttribute('foo')", .ex = "true" },
|
||||
.{ .src = "a.getAttribute('foo')", .ex = "bar" },
|
||||
.{ "a.hasAttribute('foo')", "false" },
|
||||
.{ "a.getAttribute('foo')", "null" },
|
||||
|
||||
.{ .src = "a.setAttribute('foo', 'baz')", .ex = "undefined" },
|
||||
.{ .src = "a.hasAttribute('foo')", .ex = "true" },
|
||||
.{ .src = "a.getAttribute('foo')", .ex = "baz" },
|
||||
.{ "a.setAttribute('foo', 'bar')", "undefined" },
|
||||
.{ "a.hasAttribute('foo')", "true" },
|
||||
.{ "a.getAttribute('foo')", "bar" },
|
||||
|
||||
.{ .src = "a.removeAttribute('foo')", .ex = "undefined" },
|
||||
.{ .src = "a.hasAttribute('foo')", .ex = "false" },
|
||||
.{ .src = "a.getAttribute('foo')", .ex = "null" },
|
||||
};
|
||||
try checkCases(js_env, &attribute);
|
||||
.{ "a.setAttribute('foo', 'baz')", "undefined" },
|
||||
.{ "a.hasAttribute('foo')", "true" },
|
||||
.{ "a.getAttribute('foo')", "baz" },
|
||||
|
||||
var toggleAttr = [_]Case{
|
||||
.{ .src = "let b = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "b.toggleAttribute('foo')", .ex = "true" },
|
||||
.{ .src = "b.hasAttribute('foo')", .ex = "true" },
|
||||
.{ .src = "b.getAttribute('foo')", .ex = "" },
|
||||
.{ "a.removeAttribute('foo')", "undefined" },
|
||||
.{ "a.hasAttribute('foo')", "false" },
|
||||
.{ "a.getAttribute('foo')", "null" },
|
||||
}, .{});
|
||||
|
||||
.{ .src = "b.toggleAttribute('foo')", .ex = "false" },
|
||||
.{ .src = "b.hasAttribute('foo')", .ex = "false" },
|
||||
};
|
||||
try checkCases(js_env, &toggleAttr);
|
||||
try runner.testCases(&.{
|
||||
.{ "let b = document.getElementById('content')", "undefined" },
|
||||
.{ "b.toggleAttribute('foo')", "true" },
|
||||
.{ "b.hasAttribute('foo')", "true" },
|
||||
.{ "b.getAttribute('foo')", "" },
|
||||
|
||||
var parentNode = [_]Case{
|
||||
.{ .src = "let c = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "c.children.length", .ex = "3" },
|
||||
.{ .src = "c.firstElementChild.nodeName", .ex = "A" },
|
||||
.{ .src = "c.lastElementChild.nodeName", .ex = "P" },
|
||||
.{ .src = "c.childElementCount", .ex = "3" },
|
||||
.{ "b.toggleAttribute('foo')", "false" },
|
||||
.{ "b.hasAttribute('foo')", "false" },
|
||||
}, .{});
|
||||
|
||||
.{ .src = "c.prepend(document.createTextNode('foo'))", .ex = "undefined" },
|
||||
.{ .src = "c.append(document.createTextNode('bar'))", .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &parentNode);
|
||||
try runner.testCases(&.{
|
||||
.{ "let c = document.getElementById('content')", "undefined" },
|
||||
.{ "c.children.length", "3" },
|
||||
.{ "c.firstElementChild.nodeName", "A" },
|
||||
.{ "c.lastElementChild.nodeName", "P" },
|
||||
.{ "c.childElementCount", "3" },
|
||||
|
||||
var elementSibling = [_]Case{
|
||||
.{ .src = "let d = document.getElementById('para')", .ex = "undefined" },
|
||||
.{ .src = "d.previousElementSibling.nodeName", .ex = "P" },
|
||||
.{ .src = "d.nextElementSibling", .ex = "null" },
|
||||
};
|
||||
try checkCases(js_env, &elementSibling);
|
||||
.{ "c.prepend(document.createTextNode('foo'))", "undefined" },
|
||||
.{ "c.append(document.createTextNode('bar'))", "undefined" },
|
||||
}, .{});
|
||||
|
||||
var querySelector = [_]Case{
|
||||
.{ .src = "let e = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "e.querySelector('foo')", .ex = "null" },
|
||||
.{ .src = "e.querySelector('#foo')", .ex = "null" },
|
||||
.{ .src = "e.querySelector('#link').id", .ex = "link" },
|
||||
.{ .src = "e.querySelector('#para').id", .ex = "para" },
|
||||
.{ .src = "e.querySelector('*').id", .ex = "link" },
|
||||
.{ .src = "e.querySelector('')", .ex = "null" },
|
||||
.{ .src = "e.querySelector('*').id", .ex = "link" },
|
||||
.{ .src = "e.querySelector('#content')", .ex = "null" },
|
||||
.{ .src = "e.querySelector('#para').id", .ex = "para" },
|
||||
.{ .src = "e.querySelector('.ok').id", .ex = "link" },
|
||||
.{ .src = "e.querySelector('a ~ p').id", .ex = "para-empty" },
|
||||
try runner.testCases(&.{
|
||||
.{ "let d = document.getElementById('para')", "undefined" },
|
||||
.{ "d.previousElementSibling.nodeName", "P" },
|
||||
.{ "d.nextElementSibling", "null" },
|
||||
}, .{});
|
||||
|
||||
.{ .src = "e.querySelectorAll('foo').length", .ex = "0" },
|
||||
.{ .src = "e.querySelectorAll('#foo').length", .ex = "0" },
|
||||
.{ .src = "e.querySelectorAll('#link').length", .ex = "1" },
|
||||
.{ .src = "e.querySelectorAll('#link').item(0).id", .ex = "link" },
|
||||
.{ .src = "e.querySelectorAll('#para').length", .ex = "1" },
|
||||
.{ .src = "e.querySelectorAll('#para').item(0).id", .ex = "para" },
|
||||
.{ .src = "e.querySelectorAll('*').length", .ex = "4" },
|
||||
.{ .src = "e.querySelectorAll('p').length", .ex = "2" },
|
||||
.{ .src = "e.querySelectorAll('.ok').item(0).id", .ex = "link" },
|
||||
};
|
||||
try checkCases(js_env, &querySelector);
|
||||
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" },
|
||||
|
||||
var attrNode = [_]Case{
|
||||
.{ .src = "let f = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "let ff = document.createAttribute('foo')", .ex = "undefined" },
|
||||
.{ .src = "f.setAttributeNode(ff)", .ex = "null" },
|
||||
.{ .src = "f.getAttributeNode('foo').name", .ex = "foo" },
|
||||
.{ .src = "f.removeAttributeNode(ff).name", .ex = "foo" },
|
||||
.{ .src = "f.getAttributeNode('bar')", .ex = "null" },
|
||||
};
|
||||
try checkCases(js_env, &attrNode);
|
||||
.{ "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" },
|
||||
}, .{});
|
||||
|
||||
var innerHTML = [_]Case{
|
||||
.{ .src = "document.getElementById('para').innerHTML", .ex = " And" },
|
||||
.{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "<span id=\"para-empty-child\"></span>" },
|
||||
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" },
|
||||
}, .{});
|
||||
|
||||
.{ .src = "let h = document.getElementById('para-empty')", .ex = "undefined" },
|
||||
.{ .src = "const prev = h.innerHTML", .ex = "undefined" },
|
||||
.{ .src = "h.innerHTML = '<p id=\"hello\">hello world</p>'", .ex = "<p id=\"hello\">hello world</p>" },
|
||||
.{ .src = "h.innerHTML", .ex = "<p id=\"hello\">hello world</p>" },
|
||||
.{ .src = "h.firstChild.nodeName", .ex = "P" },
|
||||
.{ .src = "h.firstChild.id", .ex = "hello" },
|
||||
.{ .src = "h.firstChild.textContent", .ex = "hello world" },
|
||||
.{ .src = "h.innerHTML = prev; true", .ex = "true" },
|
||||
.{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "<span id=\"para-empty-child\"></span>" },
|
||||
};
|
||||
try checkCases(js_env, &innerHTML);
|
||||
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", "1" },
|
||||
.{ "r1.y", "0" },
|
||||
.{ "r1.width", "1" },
|
||||
.{ "r1.height", "1" },
|
||||
|
||||
.{ "let r2 = document.getElementById('content').getBoundingClientRect()", "undefined" },
|
||||
.{ "r2.x", "2" },
|
||||
.{ "r2.y", "0" },
|
||||
.{ "r2.width", "1" },
|
||||
.{ "r2.height", "1" },
|
||||
|
||||
.{ "let r3 = document.getElementById('para').getBoundingClientRect()", "undefined" },
|
||||
.{ "r3.x", "1" },
|
||||
.{ "r3.y", "0" },
|
||||
.{ "r3.width", "1" },
|
||||
.{ "r3.height", "1" },
|
||||
|
||||
.{ "document.getElementById('para').clientWidth", "3" },
|
||||
.{ "document.getElementById('para').clientHeight", "1" },
|
||||
}, .{});
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
222
src/browser/dom/event_target.zig
Normal file
222
src/browser/dom/event_target.zig
Normal file
@@ -0,0 +1,222 @@
|
||||
// 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 SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
const Nod = @import("node.zig");
|
||||
|
||||
// EventTarget interfaces
|
||||
pub const Union = Nod.Union;
|
||||
|
||||
// EventTarget implementation
|
||||
pub const EventTarget = struct {
|
||||
pub const Self = parser.EventTarget;
|
||||
pub const Exception = DOMException;
|
||||
|
||||
pub fn toInterface(et: *parser.EventTarget) !Union {
|
||||
// NOTE: for now we state that all EventTarget are Nodes
|
||||
// TODO: handle other types (eg. Window)
|
||||
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
pub fn _addEventListener(
|
||||
self: *parser.EventTarget,
|
||||
typ: []const u8,
|
||||
cbk: Env.Callback,
|
||||
capture: ?bool,
|
||||
state: *SessionState,
|
||||
// TODO: hanle EventListenerOptions
|
||||
// see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
) !void {
|
||||
// check if event target has already this listener
|
||||
const lst = try parser.eventTargetHasListener(
|
||||
self,
|
||||
typ,
|
||||
capture orelse false,
|
||||
cbk.id,
|
||||
);
|
||||
if (lst != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eh = try EventHandler.init(state.arena, try cbk.withThis(self));
|
||||
|
||||
try parser.eventTargetAddEventListener(
|
||||
self,
|
||||
typ,
|
||||
&eh.node,
|
||||
capture orelse false,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _removeEventListener(
|
||||
self: *parser.EventTarget,
|
||||
typ: []const u8,
|
||||
cbk: Env.Callback,
|
||||
capture: ?bool,
|
||||
// TODO: hanle EventListenerOptions
|
||||
// see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
) !void {
|
||||
// check if event target has already this listener
|
||||
const lst = try parser.eventTargetHasListener(
|
||||
self,
|
||||
typ,
|
||||
capture orelse false,
|
||||
cbk.id,
|
||||
);
|
||||
if (lst == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove listener
|
||||
try parser.eventTargetRemoveEventListener(
|
||||
self,
|
||||
typ,
|
||||
lst.?,
|
||||
capture orelse false,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool {
|
||||
return try parser.eventTargetDispatchEvent(self, event);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *parser.EventTarget, state: *SessionState) void {
|
||||
parser.eventTargetRemoveAllEventListeners(self, state.arena) catch unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
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, 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" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -19,19 +19,13 @@
|
||||
const std = @import("std");
|
||||
const allocPrint = std.fmt.allocPrint;
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
// https://webidl.spec.whatwg.org/#idl-DOMException
|
||||
pub const DOMException = struct {
|
||||
err: parser.DOMError,
|
||||
str: []const u8,
|
||||
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub const ErrorSet = parser.DOMError;
|
||||
|
||||
// static attributes
|
||||
@@ -62,7 +56,7 @@ pub const DOMException = struct {
|
||||
pub const _DATA_CLONE_ERR = 25;
|
||||
|
||||
// TODO: deinit
|
||||
pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) anyerror!DOMException {
|
||||
pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) !DOMException {
|
||||
const errCast = @as(parser.DOMError, @errorCast(err));
|
||||
const errName = DOMException.name(errCast);
|
||||
const str = switch (errCast) {
|
||||
@@ -120,7 +114,7 @@ pub const DOMException = struct {
|
||||
|
||||
// JS properties and methods
|
||||
|
||||
pub fn get_code(self: DOMException) u8 {
|
||||
pub fn get_code(self: *const DOMException) u8 {
|
||||
return switch (self.err) {
|
||||
error.IndexSize => 1,
|
||||
error.StringSize => 2,
|
||||
@@ -157,38 +151,41 @@ pub const DOMException = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_name(self: DOMException) []const u8 {
|
||||
pub fn get_name(self: *const DOMException) []const u8 {
|
||||
return DOMException.name(self.err);
|
||||
}
|
||||
|
||||
pub fn get_message(self: DOMException) []const u8 {
|
||||
pub fn get_message(self: *const DOMException) []const u8 {
|
||||
const errName = DOMException.name(self.err);
|
||||
return self.str[errName.len + 2 ..];
|
||||
}
|
||||
|
||||
pub fn _toString(self: DOMException) []const u8 {
|
||||
pub fn _toString(self: *const DOMException) []const u8 {
|
||||
return self.str;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Exception" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
const err = "Failed to execute 'appendChild' on 'Node': The new child element contains the parent.";
|
||||
var cases = [_]Case{
|
||||
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "let link = document.getElementById('link')", .ex = "undefined" },
|
||||
try runner.testCases(&.{
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let link = document.getElementById('link')", "undefined" },
|
||||
// HierarchyRequestError
|
||||
.{ .src = "var HierarchyRequestError; try {link.appendChild(content)} catch (error) {HierarchyRequestError = error} HierarchyRequestError.name", .ex = "HierarchyRequestError" },
|
||||
.{ .src = "HierarchyRequestError.code", .ex = "3" },
|
||||
.{ .src = "HierarchyRequestError.message", .ex = err },
|
||||
.{ .src = "HierarchyRequestError.toString()", .ex = "HierarchyRequestError: " ++ err },
|
||||
.{ .src = "HierarchyRequestError instanceof DOMException", .ex = "true" },
|
||||
.{ .src = "HierarchyRequestError instanceof Error", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &cases);
|
||||
.{
|
||||
\\ var he;
|
||||
\\ try { link.appendChild(content) } catch (error) { he = error}
|
||||
\\ he.name
|
||||
,
|
||||
"HierarchyRequestError",
|
||||
},
|
||||
.{ "he.code", "3" },
|
||||
.{ "he.message", err },
|
||||
.{ "he.toString()", "HierarchyRequestError: " ++ err },
|
||||
.{ "he instanceof DOMException", "true" },
|
||||
.{ "he instanceof Error", "true" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -17,22 +17,14 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const utils = @import("utils.z");
|
||||
const Element = @import("element.zig").Element;
|
||||
const Union = @import("element.zig").Union;
|
||||
|
||||
const JsThis = @import("../env.zig").JsThis;
|
||||
const Walker = @import("walker.zig").Walker;
|
||||
const WalkerDepthFirst = @import("walker.zig").WalkerDepthFirst;
|
||||
const WalkerChildren = @import("walker.zig").WalkerChildren;
|
||||
const WalkerNone = @import("walker.zig").WalkerNone;
|
||||
|
||||
const Matcher = union(enum) {
|
||||
matchByName: MatchByName,
|
||||
@@ -45,25 +37,11 @@ const Matcher = union(enum) {
|
||||
|
||||
pub fn match(self: Matcher, node: *parser.Node) !bool {
|
||||
switch (self) {
|
||||
inline .matchTrue => return true,
|
||||
inline .matchFalse => return false,
|
||||
inline .matchByTagName => |case| return case.match(node),
|
||||
inline .matchByClassName => |case| return case.match(node),
|
||||
inline .matchByName => |case| return case.match(node),
|
||||
inline .matchByLinks => return MatchByLinks.match(node),
|
||||
inline .matchByAnchors => return MatchByAnchors.match(node),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: Matcher, alloc: std.mem.Allocator) void {
|
||||
switch (self) {
|
||||
inline .matchTrue => return,
|
||||
inline .matchFalse => return,
|
||||
inline .matchByTagName => |case| return case.deinit(alloc),
|
||||
inline .matchByClassName => |case| return case.deinit(alloc),
|
||||
inline .matchByName => |case| return case.deinit(alloc),
|
||||
inline .matchByLinks => return,
|
||||
inline .matchByAnchors => return,
|
||||
.matchTrue => return true,
|
||||
.matchFalse => return false,
|
||||
.matchByLinks => return MatchByLinks.match(node),
|
||||
.matchByAnchors => return MatchByAnchors.match(node),
|
||||
inline else => |m| return m.match(node),
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -74,54 +52,49 @@ pub const MatchByTagName = struct {
|
||||
tag: []const u8,
|
||||
is_wildcard: bool,
|
||||
|
||||
fn init(alloc: std.mem.Allocator, tag_name: []const u8) !MatchByTagName {
|
||||
const tag_name_alloc = try alloc.alloc(u8, tag_name.len);
|
||||
@memcpy(tag_name_alloc, tag_name);
|
||||
return MatchByTagName{
|
||||
.tag = tag_name_alloc,
|
||||
.is_wildcard = std.mem.eql(u8, tag_name, "*"),
|
||||
fn init(arena: Allocator, tag_name: []const u8) !MatchByTagName {
|
||||
if (std.mem.eql(u8, tag_name, "*")) {
|
||||
return .{ .tag = "*", .is_wildcard = true };
|
||||
}
|
||||
|
||||
return .{
|
||||
.tag = try arena.dupe(u8, tag_name),
|
||||
.is_wildcard = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn match(self: MatchByTagName, node: *parser.Node) !bool {
|
||||
return self.is_wildcard or std.ascii.eqlIgnoreCase(self.tag, try parser.nodeName(node));
|
||||
}
|
||||
|
||||
fn deinit(self: MatchByTagName, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.tag);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn HTMLCollectionByTagName(
|
||||
alloc: std.mem.Allocator,
|
||||
arena: Allocator,
|
||||
root: ?*parser.Node,
|
||||
tag_name: []const u8,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByTagName = try MatchByTagName.init(alloc, tag_name),
|
||||
},
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
|
||||
pub const MatchByClassName = struct {
|
||||
classNames: []const u8,
|
||||
class_names: []const u8,
|
||||
|
||||
fn init(alloc: std.mem.Allocator, classNames: []const u8) !MatchByClassName {
|
||||
const class_names_alloc = try alloc.alloc(u8, classNames.len);
|
||||
@memcpy(class_names_alloc, classNames);
|
||||
return MatchByClassName{
|
||||
.classNames = class_names_alloc,
|
||||
fn init(arena: Allocator, class_names: []const u8) !MatchByClassName {
|
||||
return .{
|
||||
.class_names = try arena.dupe(u8, class_names),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn match(self: MatchByClassName, node: *parser.Node) !bool {
|
||||
var it = std.mem.splitAny(u8, self.classNames, " ");
|
||||
const e = parser.nodeToElement(node);
|
||||
|
||||
var it = std.mem.splitScalar(u8, self.class_names, ' ');
|
||||
while (it.next()) |c| {
|
||||
if (!try parser.elementHasClass(e, c)) {
|
||||
return false;
|
||||
@@ -130,24 +103,18 @@ pub const MatchByClassName = struct {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn deinit(self: MatchByClassName, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.classNames);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn HTMLCollectionByClassName(
|
||||
alloc: std.mem.Allocator,
|
||||
arena: Allocator,
|
||||
root: ?*parser.Node,
|
||||
classNames: []const u8,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByClassName = try MatchByClassName.init(alloc, classNames),
|
||||
},
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
@@ -155,11 +122,9 @@ pub fn HTMLCollectionByClassName(
|
||||
pub const MatchByName = struct {
|
||||
name: []const u8,
|
||||
|
||||
fn init(alloc: std.mem.Allocator, name: []const u8) !MatchByName {
|
||||
const names_alloc = try alloc.alloc(u8, name.len);
|
||||
@memcpy(names_alloc, name);
|
||||
return MatchByName{
|
||||
.name = names_alloc,
|
||||
fn init(arena: Allocator, name: []const u8) !MatchByName {
|
||||
return .{
|
||||
.name = try arena.dupe(u8, name),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -168,24 +133,18 @@ pub const MatchByName = struct {
|
||||
const nname = try parser.elementGetAttribute(e, "name") orelse return false;
|
||||
return std.mem.eql(u8, self.name, nname);
|
||||
}
|
||||
|
||||
fn deinit(self: MatchByName, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.name);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn HTMLCollectionByName(
|
||||
alloc: std.mem.Allocator,
|
||||
arena: Allocator,
|
||||
root: ?*parser.Node,
|
||||
name: []const u8,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByName = try MatchByName.init(alloc, name),
|
||||
},
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByName = try MatchByName.init(arena, name) },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
@@ -196,8 +155,8 @@ pub fn HTMLCollectionAll(
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{ .matchTrue = .{} },
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchTrue = .{} },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
@@ -208,8 +167,8 @@ pub fn HTMLCollectionChildren(
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerChildren = .{} },
|
||||
.matcher = Matcher{ .matchTrue = .{} },
|
||||
.walker = .{ .walkerChildren = .{} },
|
||||
.matcher = .{ .matchTrue = .{} },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
@@ -217,8 +176,8 @@ pub fn HTMLCollectionChildren(
|
||||
pub fn HTMLCollectionEmpty() !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = null,
|
||||
.walker = Walker{ .walkerNone = .{} },
|
||||
.matcher = Matcher{ .matchFalse = .{} },
|
||||
.walker = .{ .walkerNone = .{} },
|
||||
.matcher = .{ .matchFalse = .{} },
|
||||
.include_root = false,
|
||||
};
|
||||
}
|
||||
@@ -243,10 +202,8 @@ pub fn HTMLCollectionByLinks(
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByLinks = MatchByLinks{},
|
||||
},
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByLinks = MatchByLinks{} },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
@@ -270,17 +227,13 @@ pub fn HTMLCollectionByAnchors(
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = Walker{ .walkerDepthFirst = .{} },
|
||||
.matcher = Matcher{
|
||||
.matchByAnchors = MatchByAnchors{},
|
||||
},
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByAnchors = MatchByAnchors{} },
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
|
||||
pub const HTMLCollectionIterator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
coll: *HTMLCollection,
|
||||
index: u32 = 0,
|
||||
|
||||
@@ -311,8 +264,6 @@ pub const HTMLCollectionIterator = struct {
|
||||
// dom_html_collection expects a comparison function callback as arguement.
|
||||
// But we wanted a dynamically comparison here, according to the match tagname.
|
||||
pub const HTMLCollection = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
matcher: Matcher,
|
||||
walker: Walker,
|
||||
|
||||
@@ -324,15 +275,11 @@ pub const HTMLCollection = struct {
|
||||
include_root: bool = false,
|
||||
|
||||
// save a state for the collection to improve the _item speed.
|
||||
cur_idx: ?u32 = undefined,
|
||||
cur_node: ?*parser.Node = undefined,
|
||||
|
||||
// array_like_keys is used to keep reference to array like interface implementation.
|
||||
// the collection generates keys string which must be free on deinit.
|
||||
array_like_keys: std.ArrayListUnmanaged([]u8) = .{},
|
||||
cur_idx: ?u32 = null,
|
||||
cur_node: ?*parser.Node = null,
|
||||
|
||||
// start returns the first node to walk on.
|
||||
fn start(self: HTMLCollection) !?*parser.Node {
|
||||
fn start(self: *const HTMLCollection) !?*parser.Node {
|
||||
if (self.root == null) return null;
|
||||
|
||||
if (self.include_root) {
|
||||
@@ -412,7 +359,7 @@ pub const HTMLCollection = struct {
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
|
||||
pub fn _namedItem(self: *HTMLCollection, name: []const u8) !?Union {
|
||||
pub fn _namedItem(self: *const HTMLCollection, name: []const u8) !?Union {
|
||||
if (self.root == null) return null;
|
||||
if (name.len == 0) return null;
|
||||
|
||||
@@ -454,81 +401,67 @@ pub const HTMLCollection = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn postAttach(self: *HTMLCollection, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void {
|
||||
const ln = try self.get_length();
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
defer i += 1;
|
||||
const k = try std.fmt.allocPrint(alloc, "{d}", .{i});
|
||||
try self.array_like_keys.append(alloc, k);
|
||||
|
||||
const node = try self.item(i) orelse unreachable;
|
||||
pub fn postAttach(self: *HTMLCollection, js_this: JsThis) !void {
|
||||
const len = try self.get_length();
|
||||
for (0..len) |i| {
|
||||
const node = try self.item(@intCast(i)) orelse unreachable;
|
||||
const e = @as(*parser.Element, @ptrCast(node));
|
||||
try js_obj.set(k, e);
|
||||
try js_this.setIndex(@intCast(i), e);
|
||||
|
||||
if (try item_name(e)) |name| {
|
||||
try js_obj.set(name, e);
|
||||
try js_this.set(name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: *HTMLCollection, alloc: std.mem.Allocator) void {
|
||||
for (self.array_like_keys_) |k| alloc.free(k);
|
||||
self.array_like_keys.deinit(alloc);
|
||||
self.matcher.deinit(alloc);
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.HTMLCollection" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var getElementsByTagName = [_]Case{
|
||||
.{ .src = "let getElementsByTagName = document.getElementsByTagName('p')", .ex = "undefined" },
|
||||
.{ .src = "getElementsByTagName.length", .ex = "2" },
|
||||
.{ .src = "let getElementsByTagNameCI = document.getElementsByTagName('P')", .ex = "undefined" },
|
||||
.{ .src = "getElementsByTagNameCI.length", .ex = "2" },
|
||||
.{ .src = "getElementsByTagName.item(0).localName", .ex = "p" },
|
||||
.{ .src = "getElementsByTagName.item(1).localName", .ex = "p" },
|
||||
.{ .src = "let getElementsByTagNameAll = document.getElementsByTagName('*')", .ex = "undefined" },
|
||||
.{ .src = "getElementsByTagNameAll.length", .ex = "8" },
|
||||
.{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" },
|
||||
.{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" },
|
||||
.{ .src = "getElementsByTagNameAll.item(1).localName", .ex = "head" },
|
||||
.{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" },
|
||||
.{ .src = "getElementsByTagNameAll.item(2).localName", .ex = "body" },
|
||||
.{ .src = "getElementsByTagNameAll.item(3).localName", .ex = "div" },
|
||||
.{ .src = "getElementsByTagNameAll.item(7).localName", .ex = "p" },
|
||||
.{ .src = "getElementsByTagNameAll.namedItem('para-empty-child').localName", .ex = "span" },
|
||||
try runner.testCases(&.{
|
||||
.{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
|
||||
.{ "getElementsByTagName.length", "2" },
|
||||
.{ "let getElementsByTagNameCI = document.getElementsByTagName('P')", "undefined" },
|
||||
.{ "getElementsByTagNameCI.length", "2" },
|
||||
.{ "getElementsByTagName.item(0).localName", "p" },
|
||||
.{ "getElementsByTagName.item(1).localName", "p" },
|
||||
.{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
|
||||
.{ "getElementsByTagNameAll.length", "8" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(1).localName", "head" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(2).localName", "body" },
|
||||
.{ "getElementsByTagNameAll.item(3).localName", "div" },
|
||||
.{ "getElementsByTagNameAll.item(7).localName", "p" },
|
||||
.{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
|
||||
|
||||
// array like
|
||||
.{ .src = "getElementsByTagNameAll[0].localName", .ex = "html" },
|
||||
.{ .src = "getElementsByTagNameAll[7].localName", .ex = "p" },
|
||||
.{ .src = "getElementsByTagNameAll[8]", .ex = "undefined" },
|
||||
.{ .src = "getElementsByTagNameAll['para-empty-child'].localName", .ex = "span" },
|
||||
.{ .src = "getElementsByTagNameAll['foo']", .ex = "undefined" },
|
||||
.{ "getElementsByTagNameAll[0].localName", "html" },
|
||||
.{ "getElementsByTagNameAll[7].localName", "p" },
|
||||
.{ "getElementsByTagNameAll[8]", "undefined" },
|
||||
.{ "getElementsByTagNameAll['para-empty-child'].localName", "span" },
|
||||
.{ "getElementsByTagNameAll['foo']", "undefined" },
|
||||
|
||||
.{ .src = "document.getElementById('content').getElementsByTagName('*').length", .ex = "4" },
|
||||
.{ .src = "document.getElementById('content').getElementsByTagName('p').length", .ex = "2" },
|
||||
.{ .src = "document.getElementById('content').getElementsByTagName('div').length", .ex = "0" },
|
||||
.{ "document.getElementById('content').getElementsByTagName('*').length", "4" },
|
||||
.{ "document.getElementById('content').getElementsByTagName('p').length", "2" },
|
||||
.{ "document.getElementById('content').getElementsByTagName('div').length", "0" },
|
||||
|
||||
.{ .src = "document.children.length", .ex = "1" },
|
||||
.{ .src = "document.getElementById('content').children.length", .ex = "3" },
|
||||
.{ "document.children.length", "1" },
|
||||
.{ "document.getElementById('content').children.length", "3" },
|
||||
|
||||
// check liveness
|
||||
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "let pe = document.getElementById('para-empty')", .ex = "undefined" },
|
||||
.{ .src = "let p = document.createElement('p')", .ex = "undefined" },
|
||||
.{ .src = "p.textContent = 'OK live'", .ex = "OK live" },
|
||||
.{ .src = "getElementsByTagName.item(1).textContent", .ex = " And" },
|
||||
.{ .src = "content.appendChild(p) != undefined", .ex = "true" },
|
||||
.{ .src = "getElementsByTagName.length", .ex = "3" },
|
||||
.{ .src = "getElementsByTagName.item(2).textContent", .ex = "OK live" },
|
||||
.{ .src = "content.insertBefore(p, pe) != undefined", .ex = "true" },
|
||||
.{ .src = "getElementsByTagName.item(0).textContent", .ex = "OK live" },
|
||||
};
|
||||
try checkCases(js_env, &getElementsByTagName);
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let pe = document.getElementById('para-empty')", "undefined" },
|
||||
.{ "let p = document.createElement('p')", "undefined" },
|
||||
.{ "p.textContent = 'OK live'", "OK live" },
|
||||
.{ "getElementsByTagName.item(1).textContent", " And" },
|
||||
.{ "content.appendChild(p) != undefined", "true" },
|
||||
.{ "getElementsByTagName.length", "3" },
|
||||
.{ "getElementsByTagName.item(2).textContent", "OK live" },
|
||||
.{ "content.insertBefore(p, pe) != undefined", "true" },
|
||||
.{ "getElementsByTagName.item(0).textContent", "OK live" },
|
||||
}, .{});
|
||||
}
|
||||
69
src/browser/dom/implementation.zig
Normal file
69
src/browser/dom/implementation.zig
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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]" },
|
||||
.{ "impl.createHTMLDocument('foo');", "[object HTMLDocument]" },
|
||||
.{ "impl.createDocument(null, 'foo');", "[object Document]" },
|
||||
.{ "impl.createDocumentType('foo', 'bar', 'baz')", "[object DocumentType]" },
|
||||
.{ "impl.hasFeature()", "true" },
|
||||
}, .{});
|
||||
}
|
||||
278
src/browser/dom/intersection_observer.zig
Normal file
278
src/browser/dom/intersection_observer.zig
Normal file
@@ -0,0 +1,278 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Element = @import("element.zig").Element;
|
||||
const Document = @import("document.zig").Document;
|
||||
|
||||
pub const Interfaces = .{
|
||||
IntersectionObserver,
|
||||
IntersectionObserverEntry,
|
||||
};
|
||||
|
||||
const log = std.log.scoped(.events);
|
||||
|
||||
// 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 {
|
||||
callback: Env.Callback,
|
||||
options: IntersectionObserverOptions,
|
||||
state: *SessionState,
|
||||
|
||||
observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry),
|
||||
|
||||
// new IntersectionObserver(callback)
|
||||
// new IntersectionObserver(callback, options) [not supported yet]
|
||||
pub fn constructor(callback: Env.Callback, options_: ?IntersectionObserverOptions, state: *SessionState) !IntersectionObserver {
|
||||
var options = IntersectionObserverOptions{
|
||||
.root = parser.documentToNode(parser.documentHTMLToDocument(state.document.?)),
|
||||
.rootMargin = "0px 0px 0px 0px",
|
||||
.threshold = &.{0.0},
|
||||
};
|
||||
if (options_) |*o| {
|
||||
if (o.root) |root| {
|
||||
options.root = root;
|
||||
} // Other properties are not used due to the way we render
|
||||
}
|
||||
|
||||
return .{
|
||||
.callback = callback,
|
||||
.options = options,
|
||||
.state = state,
|
||||
.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.state.arena, .{
|
||||
.state = self.state,
|
||||
.target = target_element,
|
||||
.options = &self.options,
|
||||
});
|
||||
|
||||
var result: Env.Callback.Result = undefined;
|
||||
self.callback.tryCall(.{self.observed_entries.items}, &result) catch {
|
||||
log.err("intersection observer callback error: {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
}
|
||||
|
||||
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: ?[]const f32,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
|
||||
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
||||
pub const IntersectionObserverEntry = struct {
|
||||
state: *SessionState,
|
||||
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 self.state.renderer.getRect(self.target);
|
||||
}
|
||||
|
||||
// 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 self.state.renderer.getRect(self.target);
|
||||
}
|
||||
|
||||
// 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.state.document.?)) {
|
||||
return self.state.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 try self.state.renderer.getRect(element);
|
||||
}
|
||||
|
||||
// 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" },
|
||||
.{ "new IntersectionObserver(entries => { entry = entries[0]; }).observe(document.createElement('div'));", "undefined" },
|
||||
.{ "entry.boundingClientRect.x;", "1" },
|
||||
.{ "entry.intersectionRatio;", "1" },
|
||||
.{ "entry.intersectionRect.x;", "1" },
|
||||
.{ "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;", "2" },
|
||||
.{ "entry.rootBounds.height;", "1" },
|
||||
.{ "entry.target;", "[object HTMLDivElement]" },
|
||||
}, .{});
|
||||
|
||||
// Options
|
||||
try runner.testCases(&.{
|
||||
.{ "const new_root = document.createElement('span');", "undefined" },
|
||||
.{ "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;", "2" },
|
||||
}, .{});
|
||||
}
|
||||
397
src/browser/dom/mutation_observer.zig
Normal file
397
src/browser/dom/mutation_observer.zig
Normal file
@@ -0,0 +1,397 @@
|
||||
// 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 SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
|
||||
pub const Interfaces = .{
|
||||
MutationObserver,
|
||||
MutationRecord,
|
||||
};
|
||||
|
||||
const Walker = @import("../dom/walker.zig").WalkerChildren;
|
||||
|
||||
const log = std.log.scoped(.events);
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
||||
pub const MutationObserver = struct {
|
||||
cbk: Env.Callback,
|
||||
arena: Allocator,
|
||||
|
||||
// List of records which were observed. When the scopeEnds, we need to
|
||||
// execute our callback with it.
|
||||
observed: std.ArrayListUnmanaged(*MutationRecord),
|
||||
|
||||
pub fn constructor(cbk: Env.Callback, state: *SessionState) !MutationObserver {
|
||||
return .{
|
||||
.cbk = cbk,
|
||||
.observed = .{},
|
||||
.arena = state.arena,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?MutationObserverInit) !void {
|
||||
const options = options_ orelse MutationObserverInit{};
|
||||
|
||||
const observer = try self.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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn jsCallScopeEnd(self: *MutationObserver, _: anytype) void {
|
||||
const record = self.observed.items;
|
||||
if (record.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
defer self.observed.clearRetainingCapacity();
|
||||
|
||||
for (record) |r| {
|
||||
const records = [_]MutationRecord{r.*};
|
||||
var result: Env.Callback.Result = undefined;
|
||||
self.cbk.tryCall(.{records}, &result) catch {
|
||||
log.err("mutation observer callback error: {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _disconnect(_: *MutationObserver) !void {
|
||||
// TODO unregister listeners.
|
||||
}
|
||||
|
||||
// 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 MutationObserverInit = struct {
|
||||
childList: bool = false,
|
||||
attributes: bool = false,
|
||||
characterData: bool = false,
|
||||
subtree: bool = false,
|
||||
attributeOldValue: bool = false,
|
||||
characterDataOldValue: bool = false,
|
||||
// TODO
|
||||
// attributeFilter: [][]const u8,
|
||||
|
||||
fn attr(self: MutationObserverInit) bool {
|
||||
return self.attributes or self.attributeOldValue;
|
||||
}
|
||||
|
||||
fn cdata(self: MutationObserverInit) bool {
|
||||
return self.characterData or self.characterDataOldValue;
|
||||
}
|
||||
};
|
||||
|
||||
const Observer = struct {
|
||||
node: *parser.Node,
|
||||
options: MutationObserverInit,
|
||||
|
||||
// record of the mutation, all observed changes in 1 call are batched
|
||||
record: ?MutationRecord = null,
|
||||
|
||||
// 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(o: *const Observer, target: *parser.Node) bool {
|
||||
// mutation on any target is always ok.
|
||||
if (o.options.subtree) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if target equals node, alway ok.
|
||||
if (target == o.node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// no subtree, no same target and no childlist, always noky.
|
||||
if (!o.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(o.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);
|
||||
|
||||
var mutation_observer = self.mutation_observer;
|
||||
|
||||
const node = blk: {
|
||||
const event_target = parser.eventTarget(event) catch |e| {
|
||||
log.err("mutation observer event target: {any}", .{e});
|
||||
return;
|
||||
} orelse return;
|
||||
|
||||
break :blk parser.eventTargetToNode(event_target);
|
||||
};
|
||||
|
||||
if (self.appliesTo(node) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event_type = blk: {
|
||||
const t = parser.eventType(event) catch |e| {
|
||||
log.err("mutation observer event type: {any}", .{e});
|
||||
return;
|
||||
};
|
||||
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
|
||||
};
|
||||
|
||||
const arena = mutation_observer.arena;
|
||||
if (self.record == null) {
|
||||
self.record = .{
|
||||
.target = self.node,
|
||||
.type = event_type.recordType(),
|
||||
};
|
||||
mutation_observer.observed.append(arena, &self.record.?) catch |err| {
|
||||
log.err("mutation_observer append: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
var record = &self.record.?;
|
||||
const mutation_event = parser.eventToMutationEvent(event);
|
||||
|
||||
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| {
|
||||
record.added_nodes.append(arena, related_node) catch |e| {
|
||||
log.err("mutation event handler error: {any}", .{e});
|
||||
return;
|
||||
};
|
||||
}
|
||||
},
|
||||
.DOMNodeRemoved => {
|
||||
if (parser.mutationEventRelatedNode(mutation_event) catch null) |related_node| {
|
||||
record.removed_nodes.append(arena, related_node) catch |e| {
|
||||
log.err("mutation event handler error: {any}", .{e});
|
||||
return;
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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");
|
||||
\\ 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";
|
||||
\\ 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";
|
||||
,
|
||||
"2",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,18 +18,13 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#namednodemap
|
||||
pub const NamedNodeMap = struct {
|
||||
pub const Self = parser.NamedNodeMap;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub const Exception = DOMException;
|
||||
|
||||
@@ -75,23 +70,30 @@ pub const NamedNodeMap = struct {
|
||||
) !*parser.Attribute {
|
||||
return try parser.namedNodeMapRemoveNamedItemNS(self, namespace, localname);
|
||||
}
|
||||
|
||||
pub fn indexed_get(self: *parser.NamedNodeMap, index: u32, has_value: *bool) !*parser.Attribute {
|
||||
return (try NamedNodeMap._item(self, index)) orelse {
|
||||
has_value.* = false;
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var setItem = [_]Case{
|
||||
.{ .src = "let a = document.getElementById('content').attributes", .ex = "undefined" },
|
||||
.{ .src = "a.length", .ex = "1" },
|
||||
.{ .src = "a.item(0)", .ex = "[object Attr]" },
|
||||
.{ .src = "a.item(1)", .ex = "null" },
|
||||
.{ .src = "a.getNamedItem('id')", .ex = "[object Attr]" },
|
||||
.{ .src = "a.getNamedItem('foo')", .ex = "null" },
|
||||
.{ .src = "a.setNamedItem(a.getNamedItem('id'))", .ex = "[object Attr]" },
|
||||
};
|
||||
try checkCases(js_env, &setItem);
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NamedNodeMap" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.getElementById('content').attributes", "undefined" },
|
||||
.{ "a.length", "1" },
|
||||
.{ "a.item(0)", "[object Attr]" },
|
||||
.{ "a.item(1)", "null" },
|
||||
.{ "a.getNamedItem('id')", "[object Attr]" },
|
||||
.{ "a.getNamedItem('foo')", "null" },
|
||||
.{ "a.setNamedItem(a.getNamedItem('id'))", "[object Attr]" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,16 +18,10 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const runScript = jsruntime.test_utils.runScript;
|
||||
const Variadic = jsruntime.Variadic;
|
||||
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const generate = @import("../../runtime/generate.zig");
|
||||
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
const EventTarget = @import("event_target.zig").EventTarget;
|
||||
|
||||
// DOM
|
||||
@@ -47,7 +41,7 @@ const HTML = @import("../html/html.zig");
|
||||
const HTMLElem = @import("../html/elements.zig");
|
||||
|
||||
// Node interfaces
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
pub const Interfaces = .{
|
||||
Attr,
|
||||
CData.CharacterData,
|
||||
CData.Interfaces,
|
||||
@@ -57,18 +51,16 @@ pub const Interfaces = generate.Tuple(.{
|
||||
DocumentFragment,
|
||||
HTMLCollection,
|
||||
HTMLCollectionIterator,
|
||||
|
||||
HTML.Interfaces,
|
||||
});
|
||||
const Generated = generate.Union.compile(Interfaces);
|
||||
pub const Union = Generated._union;
|
||||
pub const Tags = Generated._enum;
|
||||
};
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
|
||||
// Node implementation
|
||||
pub const Node = struct {
|
||||
pub const Self = parser.Node;
|
||||
pub const prototype = *EventTarget;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn toInterface(node: *parser.Node) !Union {
|
||||
return switch (try parser.nodeType(node)) {
|
||||
@@ -88,6 +80,23 @@ pub const Node = struct {
|
||||
};
|
||||
}
|
||||
|
||||
// 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
|
||||
// --------
|
||||
|
||||
@@ -264,13 +273,13 @@ pub const Node = struct {
|
||||
return try parser.nodeHasChildNodes(self);
|
||||
}
|
||||
|
||||
pub fn get_childNodes(self: *parser.Node, alloc: std.mem.Allocator) !NodeList {
|
||||
var list = NodeList.init();
|
||||
errdefer list.deinit(alloc);
|
||||
pub fn get_childNodes(self: *parser.Node, state: *SessionState) !NodeList {
|
||||
const allocator = state.arena;
|
||||
var list: NodeList = .{};
|
||||
|
||||
var n = try parser.nodeFirstChild(self) orelse return list;
|
||||
while (true) {
|
||||
try list.append(alloc, n);
|
||||
try list.append(allocator, n);
|
||||
n = try parser.nodeNextSibling(n) orelse return list;
|
||||
}
|
||||
}
|
||||
@@ -279,8 +288,7 @@ pub const Node = struct {
|
||||
return try parser.nodeInsertBefore(self, new_node, ref_node);
|
||||
}
|
||||
|
||||
pub fn _isDefaultNamespace(self: *parser.Node, namespace: []const u8) !bool {
|
||||
// TODO: namespace is not an optional parameter, but can be null.
|
||||
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
|
||||
return try parser.nodeIsDefaultNamespace(self, namespace);
|
||||
}
|
||||
|
||||
@@ -329,11 +337,10 @@ pub const Node = struct {
|
||||
// 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: ?Variadic(*parser.Node)) !bool {
|
||||
if (nodes == null) return true;
|
||||
if (nodes.?.slice.len == 0) return true;
|
||||
pub fn hierarchy(self: *parser.Node, nodes: []const *parser.Node) !bool {
|
||||
if (nodes.len == 0) return true;
|
||||
|
||||
for (nodes.?.slice) |node| if (self == node) return false;
|
||||
for (nodes) |node| if (self == node) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -341,22 +348,21 @@ pub const Node = struct {
|
||||
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
|
||||
// function must accept either node or string.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
pub fn prepend(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void {
|
||||
if (nodes == null) return;
|
||||
if (nodes.?.slice.len == 0) return;
|
||||
pub fn prepend(self: *parser.Node, nodes: []const *parser.Node) !void {
|
||||
if (nodes.len == 0) return;
|
||||
|
||||
// check hierarchy
|
||||
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
|
||||
|
||||
const first = try parser.nodeFirstChild(self);
|
||||
if (first == null) {
|
||||
for (nodes.?.slice) |node| {
|
||||
for (nodes) |node| {
|
||||
_ = try parser.nodeAppendChild(self, node);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (nodes.?.slice) |node| {
|
||||
for (nodes) |node| {
|
||||
_ = try parser.nodeInsertBefore(self, node, first.?);
|
||||
}
|
||||
}
|
||||
@@ -364,14 +370,13 @@ pub const Node = struct {
|
||||
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
|
||||
// function must accept either node or string.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
pub fn append(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void {
|
||||
if (nodes == null) return;
|
||||
if (nodes.?.slice.len == 0) return;
|
||||
pub fn append(self: *parser.Node, nodes: []const *parser.Node) !void {
|
||||
if (nodes.len == 0) return;
|
||||
|
||||
// check hierarchy
|
||||
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
|
||||
|
||||
for (nodes.?.slice) |node| {
|
||||
for (nodes) |node| {
|
||||
_ = try parser.nodeAppendChild(self, node);
|
||||
}
|
||||
}
|
||||
@@ -379,9 +384,8 @@ pub const Node = struct {
|
||||
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
|
||||
// function must accept either node or string.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
pub fn replaceChildren(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void {
|
||||
if (nodes == null) return;
|
||||
if (nodes.?.slice.len == 0) return;
|
||||
pub fn replaceChildren(self: *parser.Node, nodes: []const *parser.Node) !void {
|
||||
if (nodes.len == 0) return;
|
||||
|
||||
// check hierarchy
|
||||
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
|
||||
@@ -390,7 +394,7 @@ pub const Node = struct {
|
||||
try removeChildren(self);
|
||||
|
||||
// add new children
|
||||
for (nodes.?.slice) |node| {
|
||||
for (nodes) |node| {
|
||||
_ = try parser.nodeAppendChild(self, node);
|
||||
}
|
||||
}
|
||||
@@ -413,219 +417,207 @@ pub const Node = struct {
|
||||
pub fn deinit(_: *parser.Node, _: std.mem.Allocator) void {}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.node" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
pub fn testExecFn(
|
||||
alloc: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// helper functions
|
||||
const trim_and_replace =
|
||||
\\function trimAndReplace(str) {
|
||||
\\str = str.replace(/(\r\n|\n|\r)/gm,'');
|
||||
\\str = str.replace(/\s+/g, ' ');
|
||||
\\str = str.trim();
|
||||
\\return str;
|
||||
\\}
|
||||
;
|
||||
try runScript(js_env, alloc, trim_and_replace, "proto_test");
|
||||
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" },
|
||||
}, .{});
|
||||
|
||||
var node_compare_document_position = [_]Case{
|
||||
.{ .src = "document.body.compareDocumentPosition(document.firstChild); ", .ex = "10" },
|
||||
.{ .src = "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", .ex = "10" },
|
||||
.{ .src = "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", .ex = "20" },
|
||||
.{ .src = "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", .ex = "0" },
|
||||
.{ .src = "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", .ex = "2" },
|
||||
.{ .src = "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", .ex = "4" },
|
||||
};
|
||||
try checkCases(js_env, &node_compare_document_position);
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('content').getRootNode().__proto__.constructor.name", "HTMLDocument" },
|
||||
}, .{});
|
||||
|
||||
var get_root_node = [_]Case{
|
||||
.{ .src = "document.getElementById('content').getRootNode().__proto__.constructor.name", .ex = "HTMLDocument" },
|
||||
};
|
||||
try checkCases(js_env, &get_root_node);
|
||||
|
||||
var first_child = [_]Case{
|
||||
try runner.testCases(&.{
|
||||
// for next test cases
|
||||
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "let link = document.getElementById('link')", .ex = "undefined" },
|
||||
.{ .src = "let first_child = content.firstChild.nextSibling", .ex = "undefined" }, // nextSibling because of line return \n
|
||||
.{ "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
|
||||
|
||||
.{ .src = "let body_first_child = document.body.firstChild", .ex = "undefined" },
|
||||
.{ .src = "body_first_child.localName", .ex = "div" },
|
||||
.{ .src = "body_first_child.__proto__.constructor.name", .ex = "HTMLDivElement" },
|
||||
.{ .src = "document.getElementById('para-empty').firstChild.firstChild", .ex = "null" },
|
||||
};
|
||||
try checkCases(js_env, &first_child);
|
||||
.{ "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" },
|
||||
}, .{});
|
||||
|
||||
var last_child = [_]Case{
|
||||
.{ .src = "let last_child = content.lastChild.previousSibling", .ex = "undefined" }, // previousSibling because of line return \n
|
||||
.{ .src = "last_child.__proto__.constructor.name", .ex = "Comment" },
|
||||
};
|
||||
try checkCases(js_env, &last_child);
|
||||
try runner.testCases(&.{
|
||||
.{ "let last_child = content.lastChild.previousSibling", "undefined" }, // previousSibling because of line return \n
|
||||
.{ "last_child.__proto__.constructor.name", "Comment" },
|
||||
}, .{});
|
||||
|
||||
var next_sibling = [_]Case{
|
||||
.{ .src = "let next_sibling = link.nextSibling.nextSibling", .ex = "undefined" },
|
||||
.{ .src = "next_sibling.localName", .ex = "p" },
|
||||
.{ .src = "next_sibling.__proto__.constructor.name", .ex = "HTMLParagraphElement" },
|
||||
.{ .src = "content.nextSibling.nextSibling", .ex = "null" },
|
||||
};
|
||||
try checkCases(js_env, &next_sibling);
|
||||
try runner.testCases(&.{
|
||||
.{ "let next_sibling = link.nextSibling.nextSibling", "undefined" },
|
||||
.{ "next_sibling.localName", "p" },
|
||||
.{ "next_sibling.__proto__.constructor.name", "HTMLParagraphElement" },
|
||||
.{ "content.nextSibling.nextSibling", "null" },
|
||||
}, .{});
|
||||
|
||||
var prev_sibling = [_]Case{
|
||||
.{ .src = "let prev_sibling = document.getElementById('para-empty').previousSibling.previousSibling", .ex = "undefined" },
|
||||
.{ .src = "prev_sibling.localName", .ex = "a" },
|
||||
.{ .src = "prev_sibling.__proto__.constructor.name", .ex = "HTMLAnchorElement" },
|
||||
.{ .src = "content.previousSibling", .ex = "null" },
|
||||
};
|
||||
try checkCases(js_env, &prev_sibling);
|
||||
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" },
|
||||
}, .{});
|
||||
|
||||
var parent = [_]Case{
|
||||
.{ .src = "let parent = document.getElementById('para').parentElement", .ex = "undefined" },
|
||||
.{ .src = "parent.localName", .ex = "div" },
|
||||
.{ .src = "parent.__proto__.constructor.name", .ex = "HTMLDivElement" },
|
||||
.{ .src = "let h = content.parentElement.parentElement", .ex = "undefined" },
|
||||
.{ .src = "h.parentElement", .ex = "null" },
|
||||
.{ .src = "h.parentNode.__proto__.constructor.name", .ex = "HTMLDocument" },
|
||||
};
|
||||
try checkCases(js_env, &parent);
|
||||
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" },
|
||||
}, .{});
|
||||
|
||||
var node_name = [_]Case{
|
||||
.{ .src = "first_child.nodeName === 'A'", .ex = "true" },
|
||||
.{ .src = "link.firstChild.nodeName === '#text'", .ex = "true" },
|
||||
.{ .src = "last_child.nodeName === '#comment'", .ex = "true" },
|
||||
.{ .src = "document.nodeName === '#document'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &node_name);
|
||||
try runner.testCases(&.{
|
||||
.{ "first_child.nodeName === 'A'", "true" },
|
||||
.{ "link.firstChild.nodeName === '#text'", "true" },
|
||||
.{ "last_child.nodeName === '#comment'", "true" },
|
||||
.{ "document.nodeName === '#document'", "true" },
|
||||
}, .{});
|
||||
|
||||
var node_type = [_]Case{
|
||||
.{ .src = "first_child.nodeType === 1", .ex = "true" },
|
||||
.{ .src = "link.firstChild.nodeType === 3", .ex = "true" },
|
||||
.{ .src = "last_child.nodeType === 8", .ex = "true" },
|
||||
.{ .src = "document.nodeType === 9", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &node_type);
|
||||
try runner.testCases(&.{
|
||||
.{ "first_child.nodeType === 1", "true" },
|
||||
.{ "link.firstChild.nodeType === 3", "true" },
|
||||
.{ "last_child.nodeType === 8", "true" },
|
||||
.{ "document.nodeType === 9", "true" },
|
||||
}, .{});
|
||||
|
||||
var owner = [_]Case{
|
||||
.{ .src = "let owner = content.ownerDocument", .ex = "undefined" },
|
||||
.{ .src = "owner.__proto__.constructor.name", .ex = "HTMLDocument" },
|
||||
.{ .src = "document.ownerDocument", .ex = "null" },
|
||||
.{ .src = "let owner2 = document.createElement('div').ownerDocument", .ex = "undefined" },
|
||||
.{ .src = "owner2.__proto__.constructor.name", .ex = "HTMLDocument" },
|
||||
};
|
||||
try checkCases(js_env, &owner);
|
||||
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" },
|
||||
}, .{});
|
||||
|
||||
var connected = [_]Case{
|
||||
.{ .src = "content.isConnected", .ex = "true" },
|
||||
.{ .src = "document.isConnected", .ex = "true" },
|
||||
.{ .src = "document.createElement('div').isConnected", .ex = "false" },
|
||||
};
|
||||
try checkCases(js_env, &connected);
|
||||
try runner.testCases(&.{
|
||||
.{ "content.isConnected", "true" },
|
||||
.{ "document.isConnected", "true" },
|
||||
.{ "document.createElement('div').isConnected", "false" },
|
||||
}, .{});
|
||||
|
||||
var node_value = [_]Case{
|
||||
.{ .src = "last_child.nodeValue === 'comment'", .ex = "true" },
|
||||
.{ .src = "link.nodeValue === null", .ex = "true" },
|
||||
.{ .src = "let text = link.firstChild", .ex = "undefined" },
|
||||
.{ .src = "text.nodeValue === 'OK'", .ex = "true" },
|
||||
.{ .src = "text.nodeValue = 'OK modified'", .ex = "OK modified" },
|
||||
.{ .src = "text.nodeValue === 'OK modified'", .ex = "true" },
|
||||
.{ .src = "link.nodeValue = 'nothing'", .ex = "nothing" },
|
||||
};
|
||||
try checkCases(js_env, &node_value);
|
||||
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" },
|
||||
}, .{});
|
||||
|
||||
var node_text_content = [_]Case{
|
||||
.{ .src = "text.textContent === 'OK modified'", .ex = "true" },
|
||||
.{ .src = "trimAndReplace(content.textContent) === 'OK modified And'", .ex = "true" },
|
||||
.{ .src = "text.textContent = 'OK'", .ex = "OK" },
|
||||
.{ .src = "text.textContent", .ex = "OK" },
|
||||
.{ .src = "trimAndReplace(document.getElementById('para-empty').textContent)", .ex = "" },
|
||||
.{ .src = "document.getElementById('para-empty').textContent = 'OK'", .ex = "OK" },
|
||||
.{ .src = "document.getElementById('para-empty').firstChild.nodeName === '#text'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &node_text_content);
|
||||
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" },
|
||||
}, .{});
|
||||
|
||||
var node_append_child = [_]Case{
|
||||
.{ .src = "let append = document.createElement('h1')", .ex = "undefined" },
|
||||
.{ .src = "content.appendChild(append).toString()", .ex = "[object HTMLHeadingElement]" },
|
||||
.{ .src = "content.lastChild.__proto__.constructor.name", .ex = "HTMLHeadingElement" },
|
||||
.{ .src = "content.appendChild(link).toString()", .ex = "[object HTMLAnchorElement]" },
|
||||
};
|
||||
try checkCases(js_env, &node_append_child);
|
||||
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]" },
|
||||
}, .{});
|
||||
|
||||
var node_clone = [_]Case{
|
||||
.{ .src = "let clone = link.cloneNode()", .ex = "undefined" },
|
||||
.{ .src = "clone.toString()", .ex = "[object HTMLAnchorElement]" },
|
||||
.{ .src = "clone.parentNode === null", .ex = "true" },
|
||||
.{ .src = "clone.firstChild === null", .ex = "true" },
|
||||
.{ .src = "let clone_deep = link.cloneNode(true)", .ex = "undefined" },
|
||||
.{ .src = "clone_deep.firstChild.nodeName === '#text'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &node_clone);
|
||||
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" },
|
||||
}, .{});
|
||||
|
||||
var node_contains = [_]Case{
|
||||
.{ .src = "link.contains(text)", .ex = "true" },
|
||||
.{ .src = "text.contains(link)", .ex = "false" },
|
||||
};
|
||||
try checkCases(js_env, &node_contains);
|
||||
try runner.testCases(&.{
|
||||
.{ "link.contains(text)", "true" },
|
||||
.{ "text.contains(link)", "false" },
|
||||
}, .{});
|
||||
|
||||
var node_has_child_nodes = [_]Case{
|
||||
.{ .src = "link.hasChildNodes()", .ex = "true" },
|
||||
.{ .src = "text.hasChildNodes()", .ex = "false" },
|
||||
};
|
||||
try checkCases(js_env, &node_has_child_nodes);
|
||||
try runner.testCases(&.{
|
||||
.{ "link.hasChildNodes()", "true" },
|
||||
.{ "text.hasChildNodes()", "false" },
|
||||
}, .{});
|
||||
|
||||
var node_child_nodes = [_]Case{
|
||||
.{ .src = "link.childNodes.length", .ex = "1" },
|
||||
.{ .src = "text.childNodes.length", .ex = "0" },
|
||||
};
|
||||
try checkCases(js_env, &node_child_nodes);
|
||||
try runner.testCases(&.{
|
||||
.{ "link.childNodes.length", "1" },
|
||||
.{ "text.childNodes.length", "0" },
|
||||
}, .{});
|
||||
|
||||
var node_insert_before = [_]Case{
|
||||
.{ .src = "let insertBefore = document.createElement('a')", .ex = "undefined" },
|
||||
.{ .src = "link.insertBefore(insertBefore, text) !== undefined", .ex = "true" },
|
||||
.{ .src = "link.firstChild.localName === 'a'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &node_insert_before);
|
||||
try runner.testCases(&.{
|
||||
.{ "let insertBefore = document.createElement('a')", "undefined" },
|
||||
.{ "link.insertBefore(insertBefore, text) !== undefined", "true" },
|
||||
.{ "link.firstChild.localName === 'a'", "true" },
|
||||
}, .{});
|
||||
|
||||
var node_is_default_namespace = [_]Case{
|
||||
try runner.testCases(&.{
|
||||
// TODO: does not seems to work
|
||||
// .{ .src = "link.isDefaultNamespace('')", .ex = "true" },
|
||||
.{ .src = "link.isDefaultNamespace('false')", .ex = "false" },
|
||||
};
|
||||
try checkCases(js_env, &node_is_default_namespace);
|
||||
// .{ "link.isDefaultNamespace('')", "true" },
|
||||
.{ "link.isDefaultNamespace('false')", "false" },
|
||||
}, .{});
|
||||
|
||||
var node_is_equal_node = [_]Case{
|
||||
.{ .src = "let equal1 = document.createElement('a')", .ex = "undefined" },
|
||||
.{ .src = "let equal2 = document.createElement('a')", .ex = "undefined" },
|
||||
.{ .src = "equal1.textContent = 'is equal'", .ex = "is equal" },
|
||||
.{ .src = "equal2.textContent = 'is equal'", .ex = "is equal" },
|
||||
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
|
||||
// .{ .src = "equal1.isEqualNode(equal2)", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &node_is_equal_node);
|
||||
// .{ "equal1.isEqualNode(equal2)", "true" },
|
||||
}, .{});
|
||||
|
||||
var node_is_same_node = [_]Case{
|
||||
.{ .src = "document.body.isSameNode(document.body)", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &node_is_same_node);
|
||||
try runner.testCases(&.{
|
||||
.{ "document.body.isSameNode(document.body)", "true" },
|
||||
}, .{});
|
||||
|
||||
var node_normalize = [_]Case{
|
||||
try runner.testCases(&.{
|
||||
// TODO: no test
|
||||
.{ .src = "link.normalize()", .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &node_normalize);
|
||||
.{ "link.normalize()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
var node_remove_child = [_]Case{
|
||||
.{ .src = "content.removeChild(append) !== undefined", .ex = "true" },
|
||||
.{ .src = "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &node_remove_child);
|
||||
try runner.testCases(&.{
|
||||
.{ "content.removeChild(append) !== undefined", "true" },
|
||||
.{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" },
|
||||
}, .{});
|
||||
|
||||
var node_replace_child = [_]Case{
|
||||
.{ .src = "let replace = document.createElement('div')", .ex = "undefined" },
|
||||
.{ .src = "link.replaceChild(replace, insertBefore) !== undefined", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &node_replace_child);
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,14 +18,10 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Callback = jsruntime.Callback;
|
||||
const CallbackResult = jsruntime.CallbackResult;
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
const JsThis = @import("../env.zig").JsThis;
|
||||
const Callback = @import("../env.zig").Callback;
|
||||
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
const Node = @import("node.zig").Node;
|
||||
@@ -36,14 +32,12 @@ const log = std.log.scoped(.nodelist);
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
pub const Interfaces = .{
|
||||
NodeListIterator,
|
||||
NodeList,
|
||||
});
|
||||
};
|
||||
|
||||
pub const NodeListIterator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
coll: *NodeList,
|
||||
index: u32 = 0,
|
||||
|
||||
@@ -70,8 +64,6 @@ pub const NodeListIterator = struct {
|
||||
};
|
||||
|
||||
pub const NodeListEntriesIterator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
coll: *NodeList,
|
||||
index: u32 = 0,
|
||||
|
||||
@@ -105,18 +97,10 @@ pub const NodeListEntriesIterator = struct {
|
||||
// implementation allows only static nodelist.
|
||||
// see https://dom.spec.whatwg.org/#old-style-collections
|
||||
pub const NodeList = struct {
|
||||
pub const mem_guarantied = true;
|
||||
pub const Exception = DOMException;
|
||||
|
||||
const NodesArrayList = std.ArrayListUnmanaged(*parser.Node);
|
||||
|
||||
nodes: NodesArrayList,
|
||||
|
||||
pub fn init() NodeList {
|
||||
return NodeList{
|
||||
.nodes = NodesArrayList{},
|
||||
};
|
||||
}
|
||||
nodes: NodesArrayList = .{},
|
||||
|
||||
pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void {
|
||||
// TODO unref all nodes
|
||||
@@ -131,7 +115,7 @@ pub const NodeList = struct {
|
||||
return @intCast(self.nodes.items.len);
|
||||
}
|
||||
|
||||
pub fn _item(self: *NodeList, index: u32) !?NodeUnion {
|
||||
pub fn _item(self: *const NodeList, index: u32) !?NodeUnion {
|
||||
if (index >= self.nodes.items.len) {
|
||||
return null;
|
||||
}
|
||||
@@ -140,17 +124,30 @@ pub const NodeList = struct {
|
||||
return try Node.toInterface(n);
|
||||
}
|
||||
|
||||
pub fn _forEach(self: *NodeList, alloc: std.mem.Allocator, cbk: Callback) !void { // TODO handle thisArg
|
||||
var res = CallbackResult.init(alloc);
|
||||
defer res.deinit();
|
||||
// This code works, but it's _MUCH_ slower than using postAttach. The benefit
|
||||
// of this version, is that it's "live"..but we're talking many orders of
|
||||
// magnitude slower.
|
||||
//
|
||||
// You can test it by commenting out `postAttach`, uncommenting this and
|
||||
// running:
|
||||
// zig build wpt -- tests/wpt/dom/nodes/NodeList-static-length-getter-tampered-indexOf-1.html
|
||||
//
|
||||
// I think this _is_ the right way to do it, but I must be doing something
|
||||
// wrong to make it so slow.
|
||||
// pub fn indexed_get(self: *const NodeList, index: u32, has_value: *bool) !?NodeUnion {
|
||||
// return (try self._item(index)) orelse {
|
||||
// has_value.* = false;
|
||||
// return null;
|
||||
// };
|
||||
// }
|
||||
|
||||
pub fn _forEach(self: *NodeList, cbk: Callback) !void { // TODO handle thisArg
|
||||
for (self.nodes.items, 0..) |n, i| {
|
||||
const ii: u32 = @intCast(i);
|
||||
cbk.trycall(.{ n, ii, self }, &res) catch |e| {
|
||||
log.err("callback error: {s}", .{res.result orelse "unknown"});
|
||||
log.debug("{s}", .{res.stack orelse "no stack trace"});
|
||||
|
||||
return e;
|
||||
var result: Callback.Result = undefined;
|
||||
cbk.tryCall(.{ n, ii, self }, &result) catch {
|
||||
log.err("callback error: {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -172,38 +169,32 @@ pub const NodeList = struct {
|
||||
}
|
||||
|
||||
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
|
||||
|
||||
pub fn postAttach(self: *NodeList, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void {
|
||||
const ln = self.get_length();
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
defer i += 1;
|
||||
const k = try std.fmt.allocPrint(alloc, "{d}", .{i});
|
||||
|
||||
const node = try self._item(i) orelse unreachable;
|
||||
try js_obj.set(k, node);
|
||||
pub fn postAttach(self: *NodeList, js_this: JsThis) !void {
|
||||
const len = self.get_length();
|
||||
for (0..len) |i| {
|
||||
const node = try self._item(@intCast(i)) orelse unreachable;
|
||||
try js_this.setIndex(i, node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NodeList" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var childnodes = [_]Case{
|
||||
.{ .src = "let list = document.getElementById('content').childNodes", .ex = "undefined" },
|
||||
.{ .src = "list.length", .ex = "9" },
|
||||
.{ .src = "list[0].__proto__.constructor.name", .ex = "Text" },
|
||||
.{ .src =
|
||||
\\let i = 0;
|
||||
\\list.forEach(function (n, idx) {
|
||||
\\ i += idx;
|
||||
\\});
|
||||
\\i;
|
||||
, .ex = "36" },
|
||||
};
|
||||
try checkCases(js_env, &childnodes);
|
||||
try runner.testCases(&.{
|
||||
.{ "let list = document.getElementById('content').childNodes", "undefined" },
|
||||
.{ "list.length", "9" },
|
||||
.{ "list[0].__proto__.constructor.name", "Text" },
|
||||
.{
|
||||
\\ let i = 0;
|
||||
\\ list.forEach(function (n, idx) {
|
||||
\\ i += idx;
|
||||
\\ });
|
||||
\\ i;
|
||||
,
|
||||
"36",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
116
src/browser/dom/processing_instruction.zig
Normal file
116
src/browser/dom/processing_instruction.zig
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Node = @import("node.zig").Node;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
// 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, state: *SessionState) !*parser.ProcessingInstruction {
|
||||
return try parser.documentCreateProcessingInstruction(
|
||||
@ptrCast(state.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" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -16,33 +16,25 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const CharacterData = @import("character_data.zig").CharacterData;
|
||||
const CDATASection = @import("cdata_section.zig").CDATASection;
|
||||
|
||||
const UserContext = @import("../user_context.zig").UserContext;
|
||||
|
||||
// Text interfaces
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
pub const Interfaces = .{
|
||||
CDATASection,
|
||||
});
|
||||
};
|
||||
|
||||
pub const Text = struct {
|
||||
pub const Self = parser.Text;
|
||||
pub const prototype = *CharacterData;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Text {
|
||||
pub fn constructor(data: ?[]const u8, state: *const SessionState) !*parser.Text {
|
||||
return parser.documentCreateTextNode(
|
||||
parser.documentHTMLToDocument(userctx.document),
|
||||
parser.documentHTMLToDocument(state.document.?),
|
||||
data orelse "",
|
||||
);
|
||||
}
|
||||
@@ -67,30 +59,28 @@ pub const Text = struct {
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var constructor = [_]Case{
|
||||
.{ .src = "let t = new Text('foo')", .ex = "undefined" },
|
||||
.{ .src = "t.data", .ex = "foo" },
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Text" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
.{ .src = "let emptyt = new Text()", .ex = "undefined" },
|
||||
.{ .src = "emptyt.data", .ex = "" },
|
||||
};
|
||||
try checkCases(js_env, &constructor);
|
||||
try runner.testCases(&.{
|
||||
.{ "let t = new Text('foo')", "undefined" },
|
||||
.{ "t.data", "foo" },
|
||||
|
||||
var get_whole_text = [_]Case{
|
||||
.{ .src = "let text = document.getElementById('link').firstChild", .ex = "undefined" },
|
||||
.{ .src = "text.wholeText === 'OK'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &get_whole_text);
|
||||
.{ "let emptyt = new Text()", "undefined" },
|
||||
.{ "emptyt.data", "" },
|
||||
}, .{});
|
||||
|
||||
var split_text = [_]Case{
|
||||
.{ .src = "text.data = 'OK modified'", .ex = "OK modified" },
|
||||
.{ .src = "let split = text.splitText('OK'.length)", .ex = "undefined" },
|
||||
.{ .src = "split.data === ' modified'", .ex = "true" },
|
||||
.{ .src = "text.data === 'OK'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &split_text);
|
||||
try runner.testCases(&.{
|
||||
.{ "let text = document.getElementById('link').firstChild", "undefined" },
|
||||
.{ "text.wholeText === 'OK'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "text.data = 'OK modified'", "OK modified" },
|
||||
.{ "let split = text.splitText('OK'.length)", "undefined" },
|
||||
.{ "split.data === ' modified'", "true" },
|
||||
.{ "text.data === 'OK'", "true" },
|
||||
}, .{});
|
||||
}
|
||||
243
src/browser/dom/token_list.zig
Normal file
243
src/browser/dom/token_list.zig
Normal file
@@ -0,0 +1,243 @@
|
||||
// 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 iterator = @import("../iterator/iterator.zig");
|
||||
|
||||
const Callback = @import("../env.zig").Callback;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
const log = std.log.scoped(.token_list);
|
||||
|
||||
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: Callback, this_arg: JsObject) !void {
|
||||
var entries = _entries(self);
|
||||
while (try entries._next()) |entry| {
|
||||
var result: Callback.Result = undefined;
|
||||
cbk.tryCallWithThis(this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
|
||||
log.err("callback error: {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -16,9 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
pub const Walker = union(enum) {
|
||||
walkerDepthFirst: WalkerDepthFirst,
|
||||
@@ -17,90 +17,94 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const File = std.fs.File;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const Walker = @import("../dom/walker.zig").WalkerChildren;
|
||||
const parser = @import("netsurf.zig");
|
||||
const Walker = @import("dom/walker.zig").WalkerChildren;
|
||||
|
||||
// writer must be a std.io.Writer
|
||||
pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {
|
||||
try writer.writeAll("<!DOCTYPE html>\n");
|
||||
try writeNode(parser.documentToNode(doc), writer);
|
||||
try writeChildren(parser.documentToNode(doc), writer);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// write the children
|
||||
// TODO avoid recursion
|
||||
try writeChildren(node, writer);
|
||||
|
||||
// close the tag
|
||||
try writer.writeAll("</");
|
||||
try writer.writeAll(tag);
|
||||
try writer.writeAll(">");
|
||||
},
|
||||
.text => {
|
||||
const v = try parser.nodeValue(node) orelse return;
|
||||
try writeEscapedTextNode(writer, v);
|
||||
},
|
||||
.cdata_section => {
|
||||
const v = try parser.nodeValue(node) orelse return;
|
||||
try writer.writeAll("<![CDATA[");
|
||||
try writer.writeAll(v);
|
||||
try writer.writeAll("]]>");
|
||||
},
|
||||
.comment => {
|
||||
const v = try parser.nodeValue(node) orelse return;
|
||||
try writer.writeAll("<!--");
|
||||
try writer.writeAll(v);
|
||||
try writer.writeAll("-->");
|
||||
},
|
||||
// TODO handle processing instruction dump
|
||||
.processing_instruction => return,
|
||||
// document fragment is outside of the main document DOM, so we
|
||||
// don't output it.
|
||||
.document_fragment => return,
|
||||
// document will never be called, but required for completeness.
|
||||
.document => return,
|
||||
// done globally instead, but required for completeness.
|
||||
.document_type => return,
|
||||
// deprecated
|
||||
.attribute => return,
|
||||
.entity_reference => return,
|
||||
.entity => return,
|
||||
.notation => return,
|
||||
}
|
||||
}
|
||||
|
||||
// writer must be a std.io.Writer
|
||||
pub fn writeNode(root: *parser.Node, writer: anytype) !void {
|
||||
pub fn writeChildren(root: *parser.Node, writer: anytype) !void {
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try walker.get_next(root, next) orelse break;
|
||||
switch (try parser.nodeType(next.?)) {
|
||||
.element => {
|
||||
// open the tag
|
||||
const tag = try parser.nodeLocalName(next.?);
|
||||
try writer.writeAll("<");
|
||||
try writer.writeAll(tag);
|
||||
|
||||
// write the attributes
|
||||
const map = try parser.nodeGetAttributes(next.?);
|
||||
const ln = try parser.namedNodeMapGetLength(map);
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
const attr = try parser.namedNodeMapItem(map, i) orelse break;
|
||||
try writer.writeAll(" ");
|
||||
try writer.writeAll(try parser.attributeGetName(attr));
|
||||
try writer.writeAll("=\"");
|
||||
try writer.writeAll(try parser.attributeGetValue(attr) orelse "");
|
||||
try writer.writeAll("\"");
|
||||
i += 1;
|
||||
}
|
||||
|
||||
try writer.writeAll(">");
|
||||
|
||||
// void elements can't have any content.
|
||||
if (try isVoid(parser.nodeToElement(next.?))) continue;
|
||||
|
||||
// write the children
|
||||
// TODO avoid recursion
|
||||
try writeNode(next.?, writer);
|
||||
|
||||
// close the tag
|
||||
try writer.writeAll("</");
|
||||
try writer.writeAll(tag);
|
||||
try writer.writeAll(">");
|
||||
},
|
||||
.text => {
|
||||
const v = try parser.nodeValue(next.?) orelse continue;
|
||||
try writer.writeAll(v);
|
||||
},
|
||||
.cdata_section => {
|
||||
const v = try parser.nodeValue(next.?) orelse continue;
|
||||
try writer.writeAll("<![CDATA[");
|
||||
try writer.writeAll(v);
|
||||
try writer.writeAll("]]>");
|
||||
},
|
||||
.comment => {
|
||||
const v = try parser.nodeValue(next.?) orelse continue;
|
||||
try writer.writeAll("<!--");
|
||||
try writer.writeAll(v);
|
||||
try writer.writeAll("-->");
|
||||
},
|
||||
// TODO handle processing instruction dump
|
||||
.processing_instruction => continue,
|
||||
// document fragment is outside of the main document DOM, so we
|
||||
// don't output it.
|
||||
.document_fragment => continue,
|
||||
// document will never be called, but required for completeness.
|
||||
.document => continue,
|
||||
// done globally instead, but required for completeness.
|
||||
.document_type => continue,
|
||||
// deprecated
|
||||
.attribute => continue,
|
||||
.entity_reference => continue,
|
||||
.entity => continue,
|
||||
.notation => continue,
|
||||
}
|
||||
try writeNode(next.?, writer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,18 +119,87 @@ fn isVoid(elem: *parser.Element) !bool {
|
||||
};
|
||||
}
|
||||
|
||||
fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
|
||||
var v = value;
|
||||
while (v.len > 0) {
|
||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
|
||||
return writer.writeAll(v);
|
||||
};
|
||||
try writer.writeAll(v[0..index]);
|
||||
switch (v[index]) {
|
||||
'&' => try writer.writeAll("&"),
|
||||
'<' => try writer.writeAll("<"),
|
||||
'>' => try writer.writeAll(">"),
|
||||
else => unreachable,
|
||||
}
|
||||
v = v[index + 1 ..];
|
||||
}
|
||||
}
|
||||
|
||||
fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
|
||||
var v = value;
|
||||
while (v.len > 0) {
|
||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
|
||||
return writer.writeAll(v);
|
||||
};
|
||||
try writer.writeAll(v[0..index]);
|
||||
switch (v[index]) {
|
||||
'&' => try writer.writeAll("&"),
|
||||
'<' => try writer.writeAll("<"),
|
||||
'>' => try writer.writeAll(">"),
|
||||
'"' => try writer.writeAll("""),
|
||||
else => unreachable,
|
||||
}
|
||||
v = v[index + 1 ..];
|
||||
}
|
||||
}
|
||||
|
||||
const testing = std.testing;
|
||||
test "dump.writeHTML" {
|
||||
const out = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only });
|
||||
defer out.close();
|
||||
try testWriteHTML(
|
||||
"<div id=\"content\">Over 9000!</div>",
|
||||
"<div id=\"content\">Over 9000!</div>",
|
||||
);
|
||||
|
||||
const file = try std.fs.cwd().openFile("test.html", .{});
|
||||
defer file.close();
|
||||
try testWriteHTML(
|
||||
"<root><!-- a comment --></root>",
|
||||
"<root><!-- a comment --></root>",
|
||||
);
|
||||
|
||||
const doc_html = try parser.documentHTMLParse(file.reader(), "UTF-8");
|
||||
// ignore close error
|
||||
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");
|
||||
}
|
||||
|
||||
fn testWriteHTML(comptime expected_body: []const u8, src: []const u8) !void {
|
||||
const expected =
|
||||
"<!DOCTYPE html>\n<html><head></head><body>" ++
|
||||
expected_body ++
|
||||
"</body></html>\n";
|
||||
return testWriteFullHTML(expected, src);
|
||||
}
|
||||
|
||||
fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
|
||||
var buf = std.ArrayListUnmanaged(u8){};
|
||||
defer buf.deinit(testing.allocator);
|
||||
|
||||
const doc_html = try parser.documentHTMLParseFromStr(src);
|
||||
defer parser.documentHTMLClose(doc_html) catch {};
|
||||
|
||||
const doc = parser.documentHTMLToDocument(doc_html);
|
||||
|
||||
try writeHTML(doc, out);
|
||||
try writeHTML(doc, buf.writer(testing.allocator));
|
||||
try testing.expectEqualStrings(expected, buf.items);
|
||||
}
|
||||
|
||||
60
src/browser/env.zig
Normal file
60
src/browser/env.zig
Normal file
@@ -0,0 +1,60 @@
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf.zig");
|
||||
const URL = @import("../url.zig").URL;
|
||||
const js = @import("../runtime/js.zig");
|
||||
const storage = @import("storage/storage.zig");
|
||||
const generate = @import("../runtime/generate.zig");
|
||||
const Loop = @import("../runtime/loop.zig").Loop;
|
||||
const HttpClient = @import("../http/client.zig").Client;
|
||||
const Renderer = @import("browser.zig").Renderer;
|
||||
|
||||
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(*SessionState, WebApis);
|
||||
//
|
||||
// If there's a compile time error witht he Env, it's type will be readable,
|
||||
// i.e.: runtime.js.Env(*browser.env.SessionState, 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(*SessionState, Interfaces);
|
||||
//
|
||||
// Because Interfaces is an anynoumous type, it doesn't have a friendly name
|
||||
// and errors would be something like:
|
||||
// runtime.js.Env(*browser.env.SessionState, .{...A HUNDRED TYPES...})
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
@import("crypto/crypto.zig").Crypto,
|
||||
@import("console/console.zig").Console,
|
||||
@import("dom/dom.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("xmlserializer/xmlserializer.zig").Interfaces,
|
||||
});
|
||||
};
|
||||
|
||||
pub const JsThis = Env.JsThis;
|
||||
pub const JsObject = Env.JsObject;
|
||||
pub const Callback = Env.Callback;
|
||||
pub const Env = js.Env(*SessionState, WebApis);
|
||||
pub const Global = @import("html/window.zig").Window;
|
||||
|
||||
pub const SessionState = struct {
|
||||
loop: *Loop,
|
||||
url: *const URL,
|
||||
renderer: *Renderer,
|
||||
arena: std.mem.Allocator,
|
||||
http_client: *HttpClient,
|
||||
cookie_jar: *storage.CookieJar,
|
||||
document: ?*parser.DocumentHTML,
|
||||
|
||||
// dangerous, but set by the JS framework
|
||||
// shorter-lived than the arena above, which
|
||||
// exists for the entire rendering of the page
|
||||
call_arena: std.mem.Allocator = undefined,
|
||||
};
|
||||
79
src/browser/events/custom_event.zig
Normal file
79
src/browser/events/custom_event.zig
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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;
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
270
src/browser/events/event.zig
Normal file
270
src/browser/events/event.zig
Normal file
@@ -0,0 +1,270 @@
|
||||
// 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 Callback = @import("../env.zig").Callback;
|
||||
const generate = @import("../../runtime/generate.zig");
|
||||
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
||||
|
||||
const CustomEvent = @import("custom_event.zig").CustomEvent;
|
||||
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
||||
|
||||
const log = std.log.scoped(.events);
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = .{
|
||||
Event,
|
||||
CustomEvent,
|
||||
ProgressEvent,
|
||||
};
|
||||
|
||||
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 => .{ .Event = evt },
|
||||
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
|
||||
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @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) !?EventTargetUnion {
|
||||
const et = try parser.eventTarget(self);
|
||||
if (et == null) return null;
|
||||
return try EventTarget.toInterface(et.?);
|
||||
}
|
||||
|
||||
pub fn get_currentTarget(self: *parser.Event) !?EventTargetUnion {
|
||||
const et = try parser.eventCurrentTarget(self);
|
||||
if (et == null) return null;
|
||||
return try EventTarget.toInterface(et.?);
|
||||
}
|
||||
|
||||
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 {
|
||||
callback: Callback,
|
||||
node: parser.EventNode,
|
||||
|
||||
pub fn init(allocator: Allocator, callback: Callback) !*EventHandler {
|
||||
const eh = try allocator.create(EventHandler);
|
||||
eh.* = .{
|
||||
.callback = callback,
|
||||
.node = .{
|
||||
.id = callback.id,
|
||||
.func = handle,
|
||||
},
|
||||
};
|
||||
return eh;
|
||||
}
|
||||
|
||||
fn handle(node: *parser.EventNode, event: *parser.Event) void {
|
||||
const ievent = Event.toInterface(event) catch |err| {
|
||||
log.err("Event.toInterface: {}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
const self: *EventHandler = @fieldParentPtr("node", node);
|
||||
var result: Callback.Result = undefined;
|
||||
self.callback.tryCall(.{ievent}, &result) catch {
|
||||
log.err("event handler error: {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,25 +18,22 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const Document = @import("../dom/document.zig").Document;
|
||||
const NodeList = @import("../dom/nodelist.zig").NodeList;
|
||||
const HTMLElem = @import("elements.zig");
|
||||
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 mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
@@ -78,14 +75,19 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: not implemented by libdom
|
||||
pub fn get_cookie(_: *parser.DocumentHTML) ![]const u8 {
|
||||
return error.NotImplemented;
|
||||
pub fn get_cookie(_: *parser.DocumentHTML, state: *SessionState) ![]const u8 {
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
try state.cookie_jar.forRequest(&state.url.uri, buf.writer(state.arena), .{ .navigation = true });
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
// TODO: not implemented by libdom
|
||||
pub fn set_cookie(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
|
||||
return error.NotImplemented;
|
||||
pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, state: *SessionState) ![]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(state.cookie_jar.allocator, &state.url.uri, cookie_str);
|
||||
errdefer c.deinit();
|
||||
try state.cookie_jar.add(c, std.time.timestamp());
|
||||
return cookie_str;
|
||||
}
|
||||
|
||||
pub fn get_title(self: *parser.DocumentHTML) ![]const u8 {
|
||||
@@ -97,44 +99,44 @@ pub const HTMLDocument = struct {
|
||||
return v;
|
||||
}
|
||||
|
||||
pub fn _getElementsByName(self: *parser.DocumentHTML, alloc: std.mem.Allocator, name: []const u8) !NodeList {
|
||||
var list = NodeList.init();
|
||||
errdefer list.deinit(alloc);
|
||||
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, state: *SessionState) !NodeList {
|
||||
const arena = state.arena;
|
||||
var list: NodeList = .{};
|
||||
|
||||
if (name.len == 0) return list;
|
||||
|
||||
const root = parser.documentHTMLToNode(self);
|
||||
var c = try collection.HTMLCollectionByName(alloc, root, name, false);
|
||||
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(alloc, n);
|
||||
try list.append(arena, n);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
pub fn get_images(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "img", false);
|
||||
pub fn get_images(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "img", false);
|
||||
}
|
||||
|
||||
pub fn get_embeds(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "embed", false);
|
||||
pub fn get_embeds(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "embed", false);
|
||||
}
|
||||
|
||||
pub fn get_plugins(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection {
|
||||
return get_embeds(self, alloc);
|
||||
pub fn get_plugins(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
|
||||
return get_embeds(self, state);
|
||||
}
|
||||
|
||||
pub fn get_forms(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "form", false);
|
||||
pub fn get_forms(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "form", false);
|
||||
}
|
||||
|
||||
pub fn get_scripts(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "script", false);
|
||||
pub fn get_scripts(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "script", false);
|
||||
}
|
||||
|
||||
pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection {
|
||||
@@ -157,6 +159,10 @@ pub const HTMLDocument = struct {
|
||||
return try parser.documentHTMLGetCurrentScript(self);
|
||||
}
|
||||
|
||||
pub fn get_location(self: *parser.DocumentHTML) !?*Location {
|
||||
return try parser.documentHTMLGetLocation(Location, self);
|
||||
}
|
||||
|
||||
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
|
||||
return "off";
|
||||
}
|
||||
@@ -201,54 +207,57 @@ pub const HTMLDocument = struct {
|
||||
pub fn set_bgColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn deinit(_: *parser.DocumentHTML, _: std.mem.Allocator) void {}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var constructor = [_]Case{
|
||||
.{ .src = "document.__proto__.constructor.name", .ex = "HTMLDocument" },
|
||||
.{ .src = "document.__proto__.__proto__.constructor.name", .ex = "Document" },
|
||||
.{ .src = "document.body.localName == 'body'", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &constructor);
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
var getters = [_]Case{
|
||||
.{ .src = "document.domain", .ex = "" },
|
||||
.{ .src = "document.referrer", .ex = "" },
|
||||
.{ .src = "document.title", .ex = "" },
|
||||
.{ .src = "document.body.localName", .ex = "body" },
|
||||
.{ .src = "document.head.localName", .ex = "head" },
|
||||
.{ .src = "document.images.length", .ex = "0" },
|
||||
.{ .src = "document.embeds.length", .ex = "0" },
|
||||
.{ .src = "document.plugins.length", .ex = "0" },
|
||||
.{ .src = "document.scripts.length", .ex = "0" },
|
||||
.{ .src = "document.forms.length", .ex = "0" },
|
||||
.{ .src = "document.links.length", .ex = "1" },
|
||||
.{ .src = "document.applets.length", .ex = "0" },
|
||||
.{ .src = "document.anchors.length", .ex = "0" },
|
||||
.{ .src = "document.all.length", .ex = "8" },
|
||||
.{ .src = "document.currentScript", .ex = "null" },
|
||||
};
|
||||
try checkCases(js_env, &getters);
|
||||
test "Browser.HTML.Document" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
var titles = [_]Case{
|
||||
.{ .src = "document.title = 'foo'", .ex = "foo" },
|
||||
.{ .src = "document.title", .ex = "foo" },
|
||||
.{ .src = "document.title = ''", .ex = "" },
|
||||
};
|
||||
try checkCases(js_env, &titles);
|
||||
try runner.testCases(&.{
|
||||
.{ "document.__proto__.constructor.name", "HTMLDocument" },
|
||||
.{ "document.__proto__.__proto__.constructor.name", "Document" },
|
||||
.{ "document.body.localName == 'body'", "true" },
|
||||
}, .{});
|
||||
|
||||
var getElementsByName = [_]Case{
|
||||
.{ .src = "document.getElementById('link').setAttribute('name', 'foo')", .ex = "undefined" },
|
||||
.{ .src = "let list = document.getElementsByName('foo')", .ex = "undefined" },
|
||||
.{ .src = "list.length", .ex = "1" },
|
||||
};
|
||||
try checkCases(js_env, &getElementsByName);
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -17,16 +17,13 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const generate = @import("../generate.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const generate = @import("../../runtime/generate.zig");
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const Element = @import("../dom/element.zig").Element;
|
||||
const URL = @import("../url/url.zig").URL;
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const Element = @import("../dom/element.zig").Element;
|
||||
|
||||
// HTMLElement interfaces
|
||||
pub const Interfaces = .{
|
||||
@@ -99,21 +96,18 @@ pub const Interfaces = .{
|
||||
HTMLVideoElement,
|
||||
CSSProperties,
|
||||
};
|
||||
const Generated = generate.Union.compile(Interfaces);
|
||||
pub const Union = Generated._union;
|
||||
pub const Tags = Generated._enum;
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
|
||||
// Abstract class
|
||||
// --------------
|
||||
|
||||
const CSSProperties = struct {
|
||||
pub const mem_guarantied = true;
|
||||
};
|
||||
const CSSProperties = struct {};
|
||||
|
||||
pub const HTMLElement = struct {
|
||||
pub const Self = parser.ElementHTML;
|
||||
pub const prototype = *Element;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_style(_: *parser.ElementHTML) CSSProperties {
|
||||
return .{};
|
||||
@@ -137,6 +131,18 @@ pub const HTMLElement = struct {
|
||||
// attach the text node.
|
||||
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @ptrCast(t)));
|
||||
}
|
||||
|
||||
pub fn _click(e: *parser.ElementHTML) !void {
|
||||
const event = try parser.mouseEventCreate();
|
||||
defer parser.mouseEventDestroy(event);
|
||||
try parser.mouseEventInit(event, "click", .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
});
|
||||
_ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event));
|
||||
}
|
||||
};
|
||||
|
||||
// Deprecated HTMLElements in Chrome (2023/03/15)
|
||||
@@ -149,7 +155,7 @@ pub const HTMLElement = struct {
|
||||
pub const HTMLMediaElement = struct {
|
||||
pub const Self = parser.MediaElement;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
// HTML elements
|
||||
@@ -158,14 +164,14 @@ pub const HTMLMediaElement = struct {
|
||||
pub const HTMLUnknownElement = struct {
|
||||
pub const Self = parser.Unknown;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
// https://html.spec.whatwg.org/#the-a-element
|
||||
pub const HTMLAnchorElement = struct {
|
||||
pub const Self = parser.Anchor;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_target(self: *parser.Anchor) ![]const u8 {
|
||||
return try parser.anchorGetTarget(self);
|
||||
@@ -175,7 +181,7 @@ pub const HTMLAnchorElement = struct {
|
||||
return try parser.anchorSetTarget(self, href);
|
||||
}
|
||||
|
||||
pub fn get_download(_: *parser.Anchor) ![]const u8 {
|
||||
pub fn get_download(_: *const parser.Anchor) ![]const u8 {
|
||||
return ""; // TODO
|
||||
}
|
||||
|
||||
@@ -219,47 +225,39 @@ pub const HTMLAnchorElement = struct {
|
||||
return try parser.nodeSetTextContent(parser.anchorToNode(self), v);
|
||||
}
|
||||
|
||||
inline fn url(self: *parser.Anchor, alloc: std.mem.Allocator) !URL {
|
||||
inline fn url(self: *parser.Anchor, state: *SessionState) !URL {
|
||||
const href = try parser.anchorGetHref(self);
|
||||
return URL.constructor(alloc, href, null); // TODO inject base url
|
||||
return URL.constructor(href, null, state); // TODO inject base url
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_origin(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try u.get_origin(alloc);
|
||||
pub fn get_origin(self: *parser.Anchor, state: *SessionState) ![]const u8 {
|
||||
var u = try url(self, state);
|
||||
return try u.get_origin(state);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_protocol(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return u.get_protocol(alloc);
|
||||
pub fn get_protocol(self: *parser.Anchor, state: *SessionState) ![]const u8 {
|
||||
var u = try url(self, state);
|
||||
return u.get_protocol(state);
|
||||
}
|
||||
|
||||
pub fn set_protocol(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
pub fn set_protocol(self: *parser.Anchor, v: []const u8, state: *SessionState) !void {
|
||||
const arena = state.arena;
|
||||
var u = try url(self, state);
|
||||
|
||||
u.uri.scheme = v;
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
|
||||
const href = try u.toString(arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_host(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try u.get_host(alloc);
|
||||
pub fn get_host(self: *parser.Anchor, state: *SessionState) ![]const u8 {
|
||||
var u = try url(self, state);
|
||||
return try u.get_host(state);
|
||||
}
|
||||
|
||||
pub fn set_host(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
|
||||
pub fn set_host(self: *parser.Anchor, v: []const u8, state: *SessionState) !void {
|
||||
// search : separator
|
||||
var p: ?u16 = null;
|
||||
var h: []const u8 = undefined;
|
||||
@@ -271,8 +269,8 @@ pub const HTMLAnchorElement = struct {
|
||||
}
|
||||
}
|
||||
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
const arena = state.arena;
|
||||
var u = try url(self, state);
|
||||
|
||||
if (p) |pp| {
|
||||
u.uri.host = .{ .raw = h };
|
||||
@@ -282,40 +280,33 @@ pub const HTMLAnchorElement = struct {
|
||||
u.uri.port = null;
|
||||
}
|
||||
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
|
||||
const href = try u.toString(arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_hostname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try alloc.dupe(u8, u.get_hostname());
|
||||
pub fn get_hostname(self: *parser.Anchor, state: *SessionState) ![]const u8 {
|
||||
var u = try url(self, state);
|
||||
return try state.arena.dupe(u8, u.get_hostname());
|
||||
}
|
||||
|
||||
pub fn set_hostname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
pub fn set_hostname(self: *parser.Anchor, v: []const u8, state: *SessionState) !void {
|
||||
const arena = state.arena;
|
||||
var u = try url(self, state);
|
||||
u.uri.host = .{ .raw = v };
|
||||
const href = try u.format(alloc);
|
||||
const href = try u.toString(arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_port(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try u.get_port(alloc);
|
||||
pub fn get_port(self: *parser.Anchor, state: *SessionState) ![]const u8 {
|
||||
var u = try url(self, state);
|
||||
return try u.get_port(state);
|
||||
}
|
||||
|
||||
pub fn set_port(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
pub fn set_port(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
|
||||
const arena = state.arena;
|
||||
var u = try url(self, state);
|
||||
|
||||
if (v != null and v.?.len > 0) {
|
||||
u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
|
||||
@@ -323,407 +314,387 @@ pub const HTMLAnchorElement = struct {
|
||||
u.uri.port = null;
|
||||
}
|
||||
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
|
||||
const href = try u.toString(arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_username(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try alloc.dupe(u8, u.get_username());
|
||||
pub fn get_username(self: *parser.Anchor, state: *SessionState) ![]const u8 {
|
||||
var u = try url(self, state);
|
||||
return try state.arena.dupe(u8, u.get_username());
|
||||
}
|
||||
|
||||
pub fn set_username(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
pub fn set_username(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
|
||||
const arena = state.arena;
|
||||
var u = try url(self, state);
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.user = .{ .raw = vv };
|
||||
} else {
|
||||
u.uri.user = null;
|
||||
}
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
const href = try u.toString(arena);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_password(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try alloc.dupe(u8, u.get_password());
|
||||
pub fn get_password(self: *parser.Anchor, state: *SessionState) ![]const u8 {
|
||||
var u = try url(self, state);
|
||||
return try state.arena.dupe(u8, u.get_password());
|
||||
}
|
||||
|
||||
pub fn set_password(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
|
||||
const arena = state.arena;
|
||||
var u = try url(self, state);
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.password = .{ .raw = vv };
|
||||
} else {
|
||||
u.uri.password = null;
|
||||
}
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
const href = try u.toString(arena);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_pathname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try alloc.dupe(u8, u.get_pathname());
|
||||
pub fn get_pathname(self: *parser.Anchor, state: *SessionState) ![]const u8 {
|
||||
var u = try url(self, state);
|
||||
return try state.arena.dupe(u8, u.get_pathname());
|
||||
}
|
||||
|
||||
pub fn set_pathname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
pub fn set_pathname(self: *parser.Anchor, v: []const u8, state: *SessionState) !void {
|
||||
const arena = state.arena;
|
||||
var u = try url(self, state);
|
||||
u.uri.path = .{ .raw = v };
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
const href = try u.toString(arena);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_search(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try u.get_search(alloc);
|
||||
pub fn get_search(self: *parser.Anchor, state: *SessionState) ![]const u8 {
|
||||
var u = try url(self, state);
|
||||
return try u.get_search(state);
|
||||
}
|
||||
|
||||
pub fn set_search(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
|
||||
const arena = state.arena;
|
||||
var u = try url(self, state);
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.query = .{ .raw = vv };
|
||||
} else {
|
||||
u.uri.query = null;
|
||||
}
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
const href = try u.toString(arena);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_hash(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try u.get_hash(alloc);
|
||||
pub fn get_hash(self: *parser.Anchor, state: *SessionState) ![]const u8 {
|
||||
var u = try url(self, state);
|
||||
return try u.get_hash(state);
|
||||
}
|
||||
|
||||
pub fn set_hash(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
|
||||
const arena = state.arena;
|
||||
var u = try url(self, state);
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.fragment = .{ .raw = vv };
|
||||
} else {
|
||||
u.uri.fragment = null;
|
||||
}
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
const href = try u.toString(arena);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
pub fn deinit(_: *parser.Anchor, _: std.mem.Allocator) void {}
|
||||
};
|
||||
|
||||
pub const HTMLAppletElement = struct {
|
||||
pub const Self = parser.Applet;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLAreaElement = struct {
|
||||
pub const Self = parser.Area;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLAudioElement = struct {
|
||||
pub const Self = parser.Audio;
|
||||
pub const prototype = *HTMLMediaElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLBRElement = struct {
|
||||
pub const Self = parser.BR;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLBaseElement = struct {
|
||||
pub const Self = parser.Base;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLBodyElement = struct {
|
||||
pub const Self = parser.Body;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLButtonElement = struct {
|
||||
pub const Self = parser.Button;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLCanvasElement = struct {
|
||||
pub const Self = parser.Canvas;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLDListElement = struct {
|
||||
pub const Self = parser.DList;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLDataElement = struct {
|
||||
pub const Self = parser.Data;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLDataListElement = struct {
|
||||
pub const Self = parser.DataList;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLDialogElement = struct {
|
||||
pub const Self = parser.Dialog;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLDirectoryElement = struct {
|
||||
pub const Self = parser.Directory;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLDivElement = struct {
|
||||
pub const Self = parser.Div;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLEmbedElement = struct {
|
||||
pub const Self = parser.Embed;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLFieldSetElement = struct {
|
||||
pub const Self = parser.FieldSet;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLFontElement = struct {
|
||||
pub const Self = parser.Font;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLFormElement = struct {
|
||||
pub const Self = parser.Form;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLFrameElement = struct {
|
||||
pub const Self = parser.Frame;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLFrameSetElement = struct {
|
||||
pub const Self = parser.FrameSet;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLHRElement = struct {
|
||||
pub const Self = parser.HR;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLHeadElement = struct {
|
||||
pub const Self = parser.Head;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLHeadingElement = struct {
|
||||
pub const Self = parser.Heading;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLHtmlElement = struct {
|
||||
pub const Self = parser.Html;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLIFrameElement = struct {
|
||||
pub const Self = parser.IFrame;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLImageElement = struct {
|
||||
pub const Self = parser.Image;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLInputElement = struct {
|
||||
pub const Self = parser.Input;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLLIElement = struct {
|
||||
pub const Self = parser.LI;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLLabelElement = struct {
|
||||
pub const Self = parser.Label;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLLegendElement = struct {
|
||||
pub const Self = parser.Legend;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLLinkElement = struct {
|
||||
pub const Self = parser.Link;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLMapElement = struct {
|
||||
pub const Self = parser.Map;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLMetaElement = struct {
|
||||
pub const Self = parser.Meta;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLMeterElement = struct {
|
||||
pub const Self = parser.Meter;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLModElement = struct {
|
||||
pub const Self = parser.Mod;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLOListElement = struct {
|
||||
pub const Self = parser.OList;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLObjectElement = struct {
|
||||
pub const Self = parser.Object;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLOptGroupElement = struct {
|
||||
pub const Self = parser.OptGroup;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLOptionElement = struct {
|
||||
pub const Self = parser.Option;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLOutputElement = struct {
|
||||
pub const Self = parser.Output;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLParagraphElement = struct {
|
||||
pub const Self = parser.Paragraph;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLParamElement = struct {
|
||||
pub const Self = parser.Param;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLPictureElement = struct {
|
||||
pub const Self = parser.Picture;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLPreElement = struct {
|
||||
pub const Self = parser.Pre;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLProgressElement = struct {
|
||||
pub const Self = parser.Progress;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLQuoteElement = struct {
|
||||
pub const Self = parser.Quote;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
// https://html.spec.whatwg.org/#the-script-element
|
||||
pub const HTMLScriptElement = struct {
|
||||
pub const Self = parser.Script;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_src(self: *parser.Script) !?[]const u8 {
|
||||
return try parser.elementGetAttribute(
|
||||
@@ -838,103 +809,103 @@ pub const HTMLScriptElement = struct {
|
||||
pub const HTMLSelectElement = struct {
|
||||
pub const Self = parser.Select;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLSourceElement = struct {
|
||||
pub const Self = parser.Source;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLSpanElement = struct {
|
||||
pub const Self = parser.Span;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLStyleElement = struct {
|
||||
pub const Self = parser.Style;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLTableElement = struct {
|
||||
pub const Self = parser.Table;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLTableCaptionElement = struct {
|
||||
pub const Self = parser.TableCaption;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLTableCellElement = struct {
|
||||
pub const Self = parser.TableCell;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLTableColElement = struct {
|
||||
pub const Self = parser.TableCol;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLTableRowElement = struct {
|
||||
pub const Self = parser.TableRow;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLTableSectionElement = struct {
|
||||
pub const Self = parser.TableSection;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLTemplateElement = struct {
|
||||
pub const Self = parser.Template;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLTextAreaElement = struct {
|
||||
pub const Self = parser.TextArea;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLTimeElement = struct {
|
||||
pub const Self = parser.Time;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLTitleElement = struct {
|
||||
pub const Self = parser.Title;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLTrackElement = struct {
|
||||
pub const Self = parser.Track;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLUListElement = struct {
|
||||
pub const Self = parser.UList;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLVideoElement = struct {
|
||||
pub const Self = parser.Video;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub fn toInterface(comptime T: type, e: *parser.Element) !T {
|
||||
@@ -1011,89 +982,92 @@ pub fn toInterface(comptime T: type, e: *parser.Element) !T {
|
||||
};
|
||||
}
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Element" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var anchor = [_]Case{
|
||||
.{ .src = "let a = document.getElementById('link')", .ex = "undefined" },
|
||||
.{ .src = "a.target", .ex = "" },
|
||||
.{ .src = "a.target = '_blank'", .ex = "_blank" },
|
||||
.{ .src = "a.target", .ex = "_blank" },
|
||||
.{ .src = "a.target = ''", .ex = "" },
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.getElementById('link')", "undefined" },
|
||||
.{ "a.target", "" },
|
||||
.{ "a.target = '_blank'", "_blank" },
|
||||
.{ "a.target", "_blank" },
|
||||
.{ "a.target = ''", "" },
|
||||
|
||||
.{ .src = "a.href", .ex = "foo" },
|
||||
.{ .src = "a.href = 'https://lightpanda.io/'", .ex = "https://lightpanda.io/" },
|
||||
.{ .src = "a.href", .ex = "https://lightpanda.io/" },
|
||||
.{ "a.href", "foo" },
|
||||
.{ "a.href = 'https://lightpanda.io/'", "https://lightpanda.io/" },
|
||||
.{ "a.href", "https://lightpanda.io/" },
|
||||
|
||||
.{ .src = "a.origin", .ex = "https://lightpanda.io" },
|
||||
.{ "a.origin", "https://lightpanda.io" },
|
||||
|
||||
.{ .src = "a.host = 'lightpanda.io:443'", .ex = "lightpanda.io:443" },
|
||||
.{ .src = "a.host", .ex = "lightpanda.io:443" },
|
||||
.{ .src = "a.port", .ex = "443" },
|
||||
.{ .src = "a.hostname", .ex = "lightpanda.io" },
|
||||
.{ "a.host = 'lightpanda.io:443'", "lightpanda.io:443" },
|
||||
.{ "a.host", "lightpanda.io:443" },
|
||||
.{ "a.port", "443" },
|
||||
.{ "a.hostname", "lightpanda.io" },
|
||||
|
||||
.{ .src = "a.host = 'lightpanda.io'", .ex = "lightpanda.io" },
|
||||
.{ .src = "a.host", .ex = "lightpanda.io" },
|
||||
.{ .src = "a.port", .ex = "" },
|
||||
.{ .src = "a.hostname", .ex = "lightpanda.io" },
|
||||
.{ "a.host = 'lightpanda.io'", "lightpanda.io" },
|
||||
.{ "a.host", "lightpanda.io" },
|
||||
.{ "a.port", "" },
|
||||
.{ "a.hostname", "lightpanda.io" },
|
||||
|
||||
.{ .src = "a.host", .ex = "lightpanda.io" },
|
||||
.{ .src = "a.hostname", .ex = "lightpanda.io" },
|
||||
.{ .src = "a.hostname = 'foo.bar'", .ex = "foo.bar" },
|
||||
.{ .src = "a.href", .ex = "https://foo.bar/" },
|
||||
.{ "a.host", "lightpanda.io" },
|
||||
.{ "a.hostname", "lightpanda.io" },
|
||||
.{ "a.hostname = 'foo.bar'", "foo.bar" },
|
||||
.{ "a.href", "https://foo.bar/" },
|
||||
|
||||
.{ .src = "a.search", .ex = "" },
|
||||
.{ .src = "a.search = 'q=bar'", .ex = "q=bar" },
|
||||
.{ .src = "a.search", .ex = "?q=bar" },
|
||||
.{ .src = "a.href", .ex = "https://foo.bar/?q=bar" },
|
||||
.{ "a.search", "" },
|
||||
.{ "a.search = 'q=bar'", "q=bar" },
|
||||
.{ "a.search", "?q=bar" },
|
||||
.{ "a.href", "https://foo.bar/?q=bar" },
|
||||
|
||||
.{ .src = "a.hash", .ex = "" },
|
||||
.{ .src = "a.hash = 'frag'", .ex = "frag" },
|
||||
.{ .src = "a.hash", .ex = "#frag" },
|
||||
.{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" },
|
||||
.{ "a.hash", "" },
|
||||
.{ "a.hash = 'frag'", "frag" },
|
||||
.{ "a.hash", "#frag" },
|
||||
.{ "a.href", "https://foo.bar/?q=bar#frag" },
|
||||
|
||||
.{ .src = "a.port", .ex = "" },
|
||||
.{ .src = "a.port = '443'", .ex = "443" },
|
||||
.{ .src = "a.host", .ex = "foo.bar:443" },
|
||||
.{ .src = "a.hostname", .ex = "foo.bar" },
|
||||
.{ .src = "a.href", .ex = "https://foo.bar:443/?q=bar#frag" },
|
||||
.{ .src = "a.port = null", .ex = "null" },
|
||||
.{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" },
|
||||
.{ "a.port", "" },
|
||||
.{ "a.port = '443'", "443" },
|
||||
.{ "a.host", "foo.bar:443" },
|
||||
.{ "a.hostname", "foo.bar" },
|
||||
.{ "a.href", "https://foo.bar:443/?q=bar#frag" },
|
||||
.{ "a.port = null", "null" },
|
||||
.{ "a.href", "https://foo.bar/?q=bar#frag" },
|
||||
|
||||
.{ .src = "a.href = 'foo'", .ex = "foo" },
|
||||
.{ "a.href = 'foo'", "foo" },
|
||||
|
||||
.{ .src = "a.type", .ex = "" },
|
||||
.{ .src = "a.type = 'text/html'", .ex = "text/html" },
|
||||
.{ .src = "a.type", .ex = "text/html" },
|
||||
.{ .src = "a.type = ''", .ex = "" },
|
||||
.{ "a.type", "" },
|
||||
.{ "a.type = 'text/html'", "text/html" },
|
||||
.{ "a.type", "text/html" },
|
||||
.{ "a.type = ''", "" },
|
||||
|
||||
.{ .src = "a.text", .ex = "OK" },
|
||||
.{ .src = "a.text = 'foo'", .ex = "foo" },
|
||||
.{ .src = "a.text", .ex = "foo" },
|
||||
.{ .src = "a.text = 'OK'", .ex = "OK" },
|
||||
};
|
||||
try checkCases(js_env, &anchor);
|
||||
.{ "a.text", "OK" },
|
||||
.{ "a.text = 'foo'", "foo" },
|
||||
.{ "a.text", "foo" },
|
||||
.{ "a.text = 'OK'", "OK" },
|
||||
}, .{});
|
||||
|
||||
var script = [_]Case{
|
||||
.{ .src = "let script = document.createElement('script')", .ex = "undefined" },
|
||||
.{ .src = "script.src = 'foo.bar'", .ex = "foo.bar" },
|
||||
try runner.testCases(&.{
|
||||
.{ "let script = document.createElement('script')", "undefined" },
|
||||
.{ "script.src = 'foo.bar'", "foo.bar" },
|
||||
|
||||
.{ .src = "script.async = true", .ex = "true" },
|
||||
.{ .src = "script.async", .ex = "true" },
|
||||
.{ .src = "script.async = false", .ex = "false" },
|
||||
.{ .src = "script.async", .ex = "false" },
|
||||
};
|
||||
try checkCases(js_env, &script);
|
||||
.{ "script.async = true", "true" },
|
||||
.{ "script.async", "true" },
|
||||
.{ "script.async = false", "false" },
|
||||
.{ "script.async", "false" },
|
||||
}, .{});
|
||||
|
||||
var innertext = [_]Case{
|
||||
.{ .src = "const backup = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "document.getElementById('content').innerText = 'foo';", .ex = "foo" },
|
||||
.{ .src = "document.getElementById('content').innerText", .ex = "foo" },
|
||||
.{ .src = "document.getElementById('content').innerHTML = backup; true;", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &innertext);
|
||||
try runner.testCases(&.{
|
||||
.{ "const backup = document.getElementById('content')", "undefined" },
|
||||
.{ "document.getElementById('content').innerText = 'foo';", "foo" },
|
||||
.{ "document.getElementById('content').innerText", "foo" },
|
||||
.{ "document.getElementById('content').innerHTML = backup; true;", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let click_count = 0;", "undefined" },
|
||||
.{ "let clickCbk = function() { click_count++ }", "undefined" },
|
||||
.{ "document.getElementById('content').addEventListener('click', clickCbk);", "undefined" },
|
||||
.{ "document.getElementById('content').click()", "undefined" },
|
||||
.{ "click_count", "1" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,16 +18,8 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const jsruntime = @import("jsruntime");
|
||||
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
||||
pub const History = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
const ScrollRestorationMode = enum {
|
||||
auto,
|
||||
manual,
|
||||
@@ -98,31 +90,31 @@ pub const History = struct {
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var history = [_]Case{
|
||||
.{ .src = "history.scrollRestoration", .ex = "auto" },
|
||||
.{ .src = "history.scrollRestoration = 'manual'", .ex = "manual" },
|
||||
.{ .src = "history.scrollRestoration = 'foo'", .ex = "foo" },
|
||||
.{ .src = "history.scrollRestoration", .ex = "manual" },
|
||||
.{ .src = "history.scrollRestoration = 'auto'", .ex = "auto" },
|
||||
.{ .src = "history.scrollRestoration", .ex = "auto" },
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.History" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
.{ .src = "history.state", .ex = "null" },
|
||||
try runner.testCases(&.{
|
||||
.{ "history.scrollRestoration", "auto" },
|
||||
.{ "history.scrollRestoration = 'manual'", "manual" },
|
||||
.{ "history.scrollRestoration = 'foo'", "foo" },
|
||||
.{ "history.scrollRestoration", "manual" },
|
||||
.{ "history.scrollRestoration = 'auto'", "auto" },
|
||||
.{ "history.scrollRestoration", "auto" },
|
||||
|
||||
.{ .src = "history.pushState({}, null, '')", .ex = "undefined" },
|
||||
.{ "history.state", "null" },
|
||||
|
||||
.{ .src = "history.replaceState({}, null, '')", .ex = "undefined" },
|
||||
.{ "history.pushState({}, null, '')", "undefined" },
|
||||
|
||||
.{ .src = "history.go()", .ex = "undefined" },
|
||||
.{ .src = "history.go(1)", .ex = "undefined" },
|
||||
.{ .src = "history.go(-1)", .ex = "undefined" },
|
||||
.{ "history.replaceState({}, null, '')", "undefined" },
|
||||
|
||||
.{ .src = "history.forward()", .ex = "undefined" },
|
||||
.{ "history.go()", "undefined" },
|
||||
.{ "history.go(1)", "undefined" },
|
||||
.{ "history.go(-1)", "undefined" },
|
||||
|
||||
.{ .src = "history.back()", .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &history);
|
||||
.{ "history.forward()", "undefined" },
|
||||
|
||||
.{ "history.back()", "undefined" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -16,20 +16,22 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const HTMLDocument = @import("document.zig").HTMLDocument;
|
||||
const HTMLElem = @import("elements.zig");
|
||||
const SVGElem = @import("svg_elements.zig");
|
||||
const Window = @import("window.zig").Window;
|
||||
const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
const Location = @import("location.zig").Location;
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
pub const Interfaces = .{
|
||||
HTMLDocument,
|
||||
HTMLElem.HTMLElement,
|
||||
HTMLElem.HTMLMediaElement,
|
||||
HTMLElem.Interfaces,
|
||||
SVGElem.SVGElement,
|
||||
Window,
|
||||
Navigator,
|
||||
History,
|
||||
});
|
||||
Location,
|
||||
};
|
||||
107
src/browser/html/location.zig
Normal file
107
src/browser/html/location.zig
Normal file
@@ -0,0 +1,107 @@
|
||||
// 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 SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
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, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_href(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_protocol(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_protocol(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_host(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_host(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_hostname(self: *Location) []const u8 {
|
||||
if (self.url) |*u| return u.get_hostname();
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_port(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_port(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_pathname(self: *Location) []const u8 {
|
||||
if (self.url) |*u| return u.get_pathname();
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_search(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_search(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_hash(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_hash(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_origin(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_origin(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _assign(_: *Location, url: []const u8) !void {
|
||||
_ = url;
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _replace(_: *Location, url: []const u8) !void {
|
||||
_ = url;
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _reload(_: *Location) !void {}
|
||||
|
||||
pub fn _toString(self: *Location, state: *SessionState) ![]const u8 {
|
||||
return try self.get_href(state);
|
||||
}
|
||||
};
|
||||
|
||||
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", "" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -19,15 +19,9 @@
|
||||
const std = @import("std");
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const jsruntime = @import("jsruntime");
|
||||
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/system-state.html#navigator
|
||||
pub const Navigator = struct {
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
agent: []const u8 = "Lightpanda/1.0",
|
||||
version: []const u8 = "1.0",
|
||||
vendor: []const u8 = "",
|
||||
@@ -89,14 +83,14 @@ pub const Navigator = struct {
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var navigator = [_]Case{
|
||||
.{ .src = "navigator.userAgent", .ex = "Lightpanda/1.0" },
|
||||
.{ .src = "navigator.appVersion", .ex = "1.0" },
|
||||
.{ .src = "navigator.language", .ex = "en-US" },
|
||||
};
|
||||
try checkCases(js_env, &navigator);
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Navigator" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "navigator.userAgent", "Lightpanda/1.0" },
|
||||
.{ "navigator.appVersion", "1.0" },
|
||||
.{ "navigator.language", "en-US" },
|
||||
}, .{});
|
||||
}
|
||||
41
src/browser/html/svg_elements.zig
Normal file
41
src/browser/html/svg_elements.zig
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const Element = @import("../dom/element.zig").Element;
|
||||
|
||||
// Support for SVGElements is very limited, this is a dummy implementation.
|
||||
// This is here no to be able to support `element instanceof SVGElement;` in JavaScript.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/SVGElement
|
||||
pub const SVGElement = struct {
|
||||
// Currently the prototype chain is not implemented (will not be returned by toInterface())
|
||||
// For that we need parser.SvgElement and the derived types with tags in the v-table.
|
||||
pub const prototype = *Element;
|
||||
// While this is a Node, could consider not exposing the subtype untill we have
|
||||
// a Self type to cast to.
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.SVGElement" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "'AString' instanceof SVGElement", "false" },
|
||||
}, .{});
|
||||
}
|
||||
223
src/browser/html/window.zig
Normal file
223
src/browser/html/window.zig
Normal file
@@ -0,0 +1,223 @@
|
||||
// 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 Callback = @import("../env.zig").Callback;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
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 storage = @import("../storage/storage.zig");
|
||||
|
||||
const log = std.log.scoped(.window);
|
||||
|
||||
// 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 = null,
|
||||
target: []const u8 = "",
|
||||
history: History = .{},
|
||||
location: Location = .{},
|
||||
storage_shelf: ?*storage.Shelf = null,
|
||||
|
||||
// counter for having unique timer ids
|
||||
timer_id: u31 = 0,
|
||||
timers: std.AutoHashMapUnmanaged(u32, *TimerCallback) = .{},
|
||||
|
||||
crypto: Crypto = .{},
|
||||
console: Console = .{},
|
||||
navigator: Navigator = .{},
|
||||
|
||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
|
||||
return .{
|
||||
.target = target orelse "",
|
||||
.navigator = navigator orelse .{},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn replaceLocation(self: *Window, loc: Location) !void {
|
||||
self.location = loc;
|
||||
if (self.document) |doc| {
|
||||
try parser.documentHTMLSetLocation(Location, doc, &self.location);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
|
||||
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 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;
|
||||
}
|
||||
|
||||
pub fn get_document(self: *Window) ?*parser.DocumentHTML {
|
||||
return self.document;
|
||||
}
|
||||
|
||||
pub fn get_history(self: *Window) *History {
|
||||
return &self.history;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO handle callback arguments.
|
||||
pub fn _setTimeout(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 {
|
||||
return self.createTimeout(cbk, delay, state, false);
|
||||
}
|
||||
|
||||
// TODO handle callback arguments.
|
||||
pub fn _setInterval(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 {
|
||||
return self.createTimeout(cbk, delay, state, true);
|
||||
}
|
||||
|
||||
pub fn _clearTimeout(self: *Window, id: u32, state: *SessionState) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
try state.loop.cancel(kv.value.loop_id);
|
||||
}
|
||||
|
||||
pub fn _clearInterval(self: *Window, id: u32, state: *SessionState) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
try state.loop.cancel(kv.value.loop_id);
|
||||
}
|
||||
|
||||
pub fn createTimeout(self: *Window, cbk: Callback, delay_: ?u32, state: *SessionState, comptime repeat: bool) !u32 {
|
||||
if (self.timers.count() > 512) {
|
||||
return error.TooManyTimeout;
|
||||
}
|
||||
const timer_id = self.timer_id +% 1;
|
||||
self.timer_id = timer_id;
|
||||
|
||||
const arena = state.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: u63 = @as(u63, (delay_ orelse 0)) * 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 (repeat) delay else null,
|
||||
};
|
||||
callback.loop_id = try state.loop.timeout(delay, &callback.node);
|
||||
|
||||
gop.value_ptr.* = callback;
|
||||
return timer_id;
|
||||
}
|
||||
};
|
||||
|
||||
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: Callback,
|
||||
|
||||
// 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,
|
||||
|
||||
window: *Window,
|
||||
|
||||
fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
const self: *TimerCallback = @fieldParentPtr("node", node);
|
||||
|
||||
var result: Callback.Result = undefined;
|
||||
self.cbk.tryCall(.{}, &result) catch {
|
||||
log.err("timeout callback error: {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
|
||||
if (self.repeat) |r| {
|
||||
// setInterval
|
||||
repeat_delay.* = r;
|
||||
return;
|
||||
}
|
||||
|
||||
// setTimeout
|
||||
_ = self.window.timers.remove(self.timer_id);
|
||||
}
|
||||
};
|
||||
226
src/browser/iterator/iterator.zig
Normal file
226
src/browser/iterator/iterator.zig
Normal file
@@ -0,0 +1,226 @@
|
||||
pub const Interfaces = .{
|
||||
U32Iterator,
|
||||
};
|
||||
|
||||
pub const U32Iterator = struct {
|
||||
length: u32,
|
||||
index: u32 = 0,
|
||||
|
||||
pub const Return = struct {
|
||||
value: u32,
|
||||
done: bool,
|
||||
};
|
||||
|
||||
pub fn _next(self: *U32Iterator) Return {
|
||||
const i = self.index;
|
||||
if (i >= self.length) {
|
||||
return .{
|
||||
.value = 0,
|
||||
.done = true,
|
||||
};
|
||||
}
|
||||
|
||||
self.index = i + 1;
|
||||
return .{
|
||||
.value = i,
|
||||
.done = false,
|
||||
};
|
||||
}
|
||||
|
||||
// Iterators should be iterable. There's a [JS] example on MDN that
|
||||
// suggests this is the correct approach:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol
|
||||
pub fn _symbol_iterator(self: *U32Iterator) *U32Iterator {
|
||||
return self;
|
||||
}
|
||||
};
|
||||
|
||||
// A wrapper around an iterator that emits an Iterable result
|
||||
// An iterable has a next() which emits a {done: bool, value: T} result
|
||||
pub fn Iterable(comptime T: type, comptime JsName: []const u8) type {
|
||||
// The inner iterator's return type.
|
||||
// Maybe an error union.
|
||||
// Definitely an optional
|
||||
const RawValue = @typeInfo(@TypeOf(T._next)).@"fn".return_type.?;
|
||||
const CanError = @typeInfo(RawValue) == .error_union;
|
||||
|
||||
const Value = blk: {
|
||||
// Unwrap the RawValue
|
||||
var V = RawValue;
|
||||
if (CanError) {
|
||||
V = @typeInfo(V).error_union.payload;
|
||||
}
|
||||
break :blk @typeInfo(V).optional.child;
|
||||
};
|
||||
|
||||
const Result = struct {
|
||||
done: bool,
|
||||
// todo, technically, we should return undefined when done = true
|
||||
// or even omit the value;
|
||||
value: ?Value,
|
||||
};
|
||||
|
||||
const ReturnType = if (CanError) T.Error!Result else Result;
|
||||
|
||||
return struct {
|
||||
// the inner value iterator
|
||||
inner: T,
|
||||
|
||||
// Generics don't generate clean names. Can't just take the resulting
|
||||
// type name and use that as a the JS class name. So we always ask for
|
||||
// an explicit JS class name
|
||||
pub const js_name = JsName;
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(inner: T) Self {
|
||||
return .{ .inner = inner };
|
||||
}
|
||||
|
||||
pub fn _next(self: *Self) ReturnType {
|
||||
const value = if (comptime CanError) try self.inner._next() else self.inner._next();
|
||||
return .{ .done = value == null, .value = value };
|
||||
}
|
||||
|
||||
pub fn _symbol_iterator(self: *Self) *Self {
|
||||
return self;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// A wrapper around an iterator that emits integer/index keyed entries.
|
||||
pub fn NumericEntries(comptime T: type, comptime JsName: []const u8) type {
|
||||
// The inner iterator's return type.
|
||||
// Maybe an error union.
|
||||
// Definitely an optional
|
||||
const RawValue = @typeInfo(@TypeOf(T._next)).@"fn".return_type.?;
|
||||
const CanError = @typeInfo(RawValue) == .error_union;
|
||||
|
||||
const Value = blk: {
|
||||
// Unwrap the RawValue
|
||||
var V = RawValue;
|
||||
if (CanError) {
|
||||
V = @typeInfo(V).error_union.payload;
|
||||
}
|
||||
break :blk @typeInfo(V).optional.child;
|
||||
};
|
||||
|
||||
const ReturnType = if (CanError) T.Error!?struct { u32, Value } else ?struct { u32, Value };
|
||||
|
||||
// Avoid ambiguity. We want to expose a NumericEntries(T).Iterable, so we
|
||||
// need a declartion inside here for an "Iterable", but that will conflict
|
||||
// with the above Iterable generic function we have.
|
||||
const BaseIterable = Iterable;
|
||||
|
||||
return struct {
|
||||
// the inner value iterator
|
||||
inner: T,
|
||||
index: u32,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
// Generics don't generate clean names. Can't just take the resulting
|
||||
// type name and use that as a the JS class name. So we always ask for
|
||||
// an explicit JS class name
|
||||
pub const js_name = JsName;
|
||||
|
||||
// re-exposed for when/if we compose this type into an Iterable
|
||||
pub const Error = T.Error;
|
||||
|
||||
// This iterator as an iterable
|
||||
pub const Iterable = BaseIterable(Self, JsName ++ "Iterable");
|
||||
|
||||
pub fn init(inner: T) Self {
|
||||
return .{ .inner = inner, .index = 0 };
|
||||
}
|
||||
|
||||
pub fn _next(self: *Self) ReturnType {
|
||||
const value_ = if (comptime CanError) try self.inner._next() else self.inner._next();
|
||||
const value = value_ orelse return null;
|
||||
|
||||
const index = self.index;
|
||||
self.index = index + 1;
|
||||
return .{ index, value };
|
||||
}
|
||||
|
||||
// make the iterator, iterable
|
||||
pub fn _symbol_iterator(self: *Self) Self.Iterable {
|
||||
return Self.Iterable.init(self.*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "U32Iterator" {
|
||||
{
|
||||
var it = U32Iterator{ .length = 0 };
|
||||
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
|
||||
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
|
||||
}
|
||||
|
||||
{
|
||||
var it = U32Iterator{ .length = 3 };
|
||||
try testing.expectEqual(.{ .value = 0, .done = false }, it._next());
|
||||
try testing.expectEqual(.{ .value = 1, .done = false }, it._next());
|
||||
try testing.expectEqual(.{ .value = 2, .done = false }, it._next());
|
||||
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
|
||||
try testing.expectEqual(.{ .value = 0, .done = true }, it._next());
|
||||
}
|
||||
}
|
||||
|
||||
test "NumericEntries" {
|
||||
const it = DummyIterator{};
|
||||
var entries = NumericEntries(DummyIterator, "DummyIterator").init(it);
|
||||
|
||||
const v1 = entries._next().?;
|
||||
try testing.expectEqual(0, v1.@"0");
|
||||
try testing.expectEqual("it's", v1.@"1");
|
||||
|
||||
const v2 = entries._next().?;
|
||||
try testing.expectEqual(1, v2.@"0");
|
||||
try testing.expectEqual("over", v2.@"1");
|
||||
|
||||
const v3 = entries._next().?;
|
||||
try testing.expectEqual(2, v3.@"0");
|
||||
try testing.expectEqual("9000!!", v3.@"1");
|
||||
|
||||
try testing.expectEqual(null, entries._next());
|
||||
try testing.expectEqual(null, entries._next());
|
||||
try testing.expectEqual(null, entries._next());
|
||||
}
|
||||
|
||||
test "Iterable" {
|
||||
const it = DummyIterator{};
|
||||
var entries = Iterable(DummyIterator, "DummyIterator").init(it);
|
||||
|
||||
const v1 = entries._next();
|
||||
try testing.expectEqual(false, v1.done);
|
||||
try testing.expectEqual("it's", v1.value.?);
|
||||
|
||||
const v2 = entries._next();
|
||||
try testing.expectEqual(false, v2.done);
|
||||
try testing.expectEqual("over", v2.value.?);
|
||||
|
||||
const v3 = entries._next();
|
||||
try testing.expectEqual(false, v3.done);
|
||||
try testing.expectEqual("9000!!", v3.value.?);
|
||||
|
||||
try testing.expectEqual(true, entries._next().done);
|
||||
try testing.expectEqual(true, entries._next().done);
|
||||
try testing.expectEqual(true, entries._next().done);
|
||||
}
|
||||
|
||||
const DummyIterator = struct {
|
||||
index: u32 = 0,
|
||||
|
||||
pub fn _next(self: *DummyIterator) ?[]const u8 {
|
||||
const index = self.index;
|
||||
self.index = index + 1;
|
||||
return switch (index) {
|
||||
0 => "it's",
|
||||
1 => "over",
|
||||
2 => "9000!!",
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Client = @import("../http/Client.zig");
|
||||
|
||||
const user_agent = @import("browser.zig").user_agent;
|
||||
|
||||
pub const Loader = struct {
|
||||
client: Client,
|
||||
// use 64KB for headers buffer size.
|
||||
server_header_buffer: [1024 * 64]u8 = undefined,
|
||||
|
||||
pub const Response = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
req: *Client.Request,
|
||||
|
||||
pub fn deinit(self: *Response) void {
|
||||
self.req.deinit();
|
||||
self.alloc.destroy(self.req);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) Loader {
|
||||
return Loader{
|
||||
.client = Client{
|
||||
.allocator = alloc,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Loader) void {
|
||||
self.client.deinit();
|
||||
}
|
||||
|
||||
// see
|
||||
// https://ziglang.org/documentation/master/std/#A;std:http.Client.fetch
|
||||
// for reference.
|
||||
// The caller is responsible for calling `deinit()` on the `Response`.
|
||||
pub fn get(self: *Loader, alloc: std.mem.Allocator, uri: std.Uri) !Response {
|
||||
var resp = Response{
|
||||
.alloc = alloc,
|
||||
.req = try alloc.create(Client.Request),
|
||||
};
|
||||
errdefer alloc.destroy(resp.req);
|
||||
|
||||
resp.req.* = try self.client.open(.GET, uri, .{
|
||||
.headers = .{
|
||||
.user_agent = .{ .override = user_agent },
|
||||
},
|
||||
.extra_headers = &.{
|
||||
.{ .name = "Accept", .value = "*/*" },
|
||||
.{ .name = "Accept-Language", .value = "en-US,en;q=0.5" },
|
||||
},
|
||||
.server_header_buffer = &self.server_header_buffer,
|
||||
});
|
||||
errdefer resp.req.deinit();
|
||||
|
||||
try resp.req.send();
|
||||
try resp.req.finish();
|
||||
try resp.req.wait();
|
||||
|
||||
return resp;
|
||||
}
|
||||
};
|
||||
|
||||
test "basic url get" {
|
||||
const alloc = std.testing.allocator;
|
||||
var loader = Loader.init(alloc);
|
||||
defer loader.deinit();
|
||||
|
||||
var result = try loader.get(alloc, "https://en.wikipedia.org/wiki/Main_Page");
|
||||
defer result.deinit();
|
||||
|
||||
try std.testing.expect(result.req.response.status == std.http.Status.ok);
|
||||
}
|
||||
@@ -21,7 +21,6 @@
|
||||
// We replace the libdom default usage of allocations with mimalloc heap
|
||||
// allocation to be able to free all memory used at once, like an arena usage.
|
||||
|
||||
const std = @import("std");
|
||||
const c = @cImport({
|
||||
@cInclude("mimalloc.h");
|
||||
});
|
||||
@@ -17,141 +17,491 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const strparser = @import("../str/parser.zig");
|
||||
const Reader = strparser.Reader;
|
||||
const trim = strparser.trim;
|
||||
pub const Mime = struct {
|
||||
content_type: ContentType,
|
||||
params: []const u8 = "",
|
||||
charset: ?[]const u8 = null,
|
||||
|
||||
const Self = @This();
|
||||
pub const unknown = Mime{
|
||||
.params = "",
|
||||
.charset = "",
|
||||
.content_type = .{ .unknown = {} },
|
||||
};
|
||||
|
||||
const MimeError = error{
|
||||
Empty,
|
||||
TooBig,
|
||||
Invalid,
|
||||
InvalidChar,
|
||||
pub const ContentTypeEnum = enum {
|
||||
text_xml,
|
||||
text_html,
|
||||
text_javascript,
|
||||
text_plain,
|
||||
unknown,
|
||||
other,
|
||||
};
|
||||
|
||||
pub const ContentType = union(ContentTypeEnum) {
|
||||
text_xml: void,
|
||||
text_html: void,
|
||||
text_javascript: void,
|
||||
text_plain: void,
|
||||
unknown: void,
|
||||
other: struct { type: []const u8, sub_type: []const u8 },
|
||||
};
|
||||
|
||||
pub fn parse(arena: Allocator, 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: ?[]const u8 = null;
|
||||
|
||||
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 => charset = try parseAttributeValue(arena, value),
|
||||
}
|
||||
}
|
||||
|
||||
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/javascript",
|
||||
@"application/javascript",
|
||||
@"application/x-javascript",
|
||||
|
||||
@"text/plain",
|
||||
}, 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 = {} },
|
||||
};
|
||||
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;
|
||||
};
|
||||
|
||||
fn parseAttributeValue(arena: Allocator, value: []const u8) ![]const u8 {
|
||||
if (value[0] != '"') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 1 to skip the opening quote
|
||||
var value_pos: usize = 1;
|
||||
var unescaped_len: usize = 0;
|
||||
const last = value.len - 1;
|
||||
|
||||
while (value_pos < value.len) {
|
||||
switch (value[value_pos]) {
|
||||
'"' => break,
|
||||
'\\' => {
|
||||
if (value_pos == last) {
|
||||
return error.Invalid;
|
||||
}
|
||||
const next = value[value_pos + 1];
|
||||
if (T_SPECIAL[next] == false) {
|
||||
return error.Invalid;
|
||||
}
|
||||
value_pos += 2;
|
||||
},
|
||||
else => value_pos += 1,
|
||||
}
|
||||
unescaped_len += 1;
|
||||
}
|
||||
|
||||
if (unescaped_len == 0) {
|
||||
return error.Invalid;
|
||||
}
|
||||
|
||||
value_pos = 1;
|
||||
const owned = try arena.alloc(u8, unescaped_len);
|
||||
for (0..unescaped_len) |i| {
|
||||
switch (value[value_pos]) {
|
||||
'"' => break,
|
||||
'\\' => {
|
||||
owned[i] = value[value_pos + 1];
|
||||
value_pos += 2;
|
||||
},
|
||||
else => |c| {
|
||||
owned[i] = c;
|
||||
value_pos += 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
return owned;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
mtype: []const u8,
|
||||
msubtype: []const u8,
|
||||
params: []const u8 = "",
|
||||
const testing = @import("../testing.zig");
|
||||
test "Mime: invalid " {
|
||||
defer testing.reset();
|
||||
|
||||
charset: ?[]const u8 = null,
|
||||
boundary: ?[]const u8 = null,
|
||||
|
||||
pub const Empty = Self{ .mtype = "", .msubtype = "" };
|
||||
pub const HTML = Self{ .mtype = "text", .msubtype = "html" };
|
||||
pub const Javascript = Self{ .mtype = "application", .msubtype = "javascript" };
|
||||
|
||||
// https://mimesniff.spec.whatwg.org/#http-token-code-point
|
||||
fn isHTTPCodePoint(c: u8) bool {
|
||||
return switch (c) {
|
||||
'!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^' => return true,
|
||||
'_', '`', '|', '~' => return true,
|
||||
else => std.ascii.isAlphanumeric(c),
|
||||
};
|
||||
}
|
||||
|
||||
fn valid(s: []const u8) bool {
|
||||
const ln = s.len;
|
||||
var i: usize = 0;
|
||||
while (i < ln) {
|
||||
if (!isHTTPCodePoint(s[i])) return false;
|
||||
i += 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// https://mimesniff.spec.whatwg.org/#parsing-a-mime-type
|
||||
pub fn parse(s: []const u8) Self.MimeError!Self {
|
||||
const ln = s.len;
|
||||
if (ln == 0) return MimeError.Empty;
|
||||
// limit input size
|
||||
if (ln > 255) return MimeError.TooBig;
|
||||
|
||||
var res = Self{ .mtype = "", .msubtype = "" };
|
||||
var r = Reader{ .s = s };
|
||||
|
||||
res.mtype = trim(r.until('/'));
|
||||
if (res.mtype.len == 0) return MimeError.Invalid;
|
||||
if (!valid(res.mtype)) return MimeError.InvalidChar;
|
||||
|
||||
if (!r.skip()) return MimeError.Invalid;
|
||||
res.msubtype = trim(r.until(';'));
|
||||
if (res.msubtype.len == 0) return MimeError.Invalid;
|
||||
if (!valid(res.msubtype)) return MimeError.InvalidChar;
|
||||
|
||||
if (!r.skip()) return res;
|
||||
res.params = trim(r.tail());
|
||||
if (res.params.len == 0) return MimeError.Invalid;
|
||||
|
||||
// parse well known parameters.
|
||||
// don't check invalid parameter format.
|
||||
var rp = Reader{ .s = res.params };
|
||||
while (true) {
|
||||
const name = trim(rp.until('='));
|
||||
if (!rp.skip()) return res;
|
||||
const value = trim(rp.until(';'));
|
||||
|
||||
if (std.ascii.eqlIgnoreCase(name, "charset")) {
|
||||
res.charset = value;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(name, "boundary")) {
|
||||
res.boundary = value;
|
||||
}
|
||||
|
||||
if (!rp.skip()) return res;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
test "parse valid" {
|
||||
for ([_][]const u8{
|
||||
"text/html",
|
||||
" \ttext/html",
|
||||
"text \t/html",
|
||||
"text/ \thtml",
|
||||
"text/html \t",
|
||||
}) |tc| {
|
||||
const m = try Self.parse(tc);
|
||||
try testing.expectEqualStrings("text", m.mtype);
|
||||
try testing.expectEqualStrings("html", m.msubtype);
|
||||
}
|
||||
const m2 = try Self.parse("text/javascript1.5");
|
||||
try testing.expectEqualStrings("text", m2.mtype);
|
||||
try testing.expectEqualStrings("javascript1.5", m2.msubtype);
|
||||
|
||||
const m3 = try Self.parse("text/html; charset=utf-8");
|
||||
try testing.expectEqualStrings("text", m3.mtype);
|
||||
try testing.expectEqualStrings("html", m3.msubtype);
|
||||
try testing.expectEqualStrings("charset=utf-8", m3.params);
|
||||
try testing.expectEqualStrings("utf-8", m3.charset.?);
|
||||
|
||||
const m4 = try Self.parse("text/html; boundary=----");
|
||||
try testing.expectEqualStrings("text", m4.mtype);
|
||||
try testing.expectEqualStrings("html", m4.msubtype);
|
||||
try testing.expectEqualStrings("boundary=----", m4.params);
|
||||
try testing.expectEqualStrings("----", m4.boundary.?);
|
||||
}
|
||||
|
||||
test "parse invalid" {
|
||||
for ([_][]const u8{
|
||||
const invalids = [_][]const u8{
|
||||
"",
|
||||
"te xt/html;",
|
||||
"te@xt/html;",
|
||||
"text/ht@ml;",
|
||||
"text/html;",
|
||||
"/text/html",
|
||||
"/html",
|
||||
}) |tc| {
|
||||
_ = Self.parse(tc) catch continue;
|
||||
try testing.expect(false);
|
||||
"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=\"\\",
|
||||
"text/html; charset=\"\\a\"", // invalid to escape non special characters
|
||||
};
|
||||
|
||||
for (invalids) |invalid| {
|
||||
const mutable_input = try testing.arena_allocator.dupe(u8, invalid);
|
||||
try testing.expectError(error.Invalid, Mime.parse(undefined, mutable_input));
|
||||
}
|
||||
}
|
||||
|
||||
// Compare type and subtype.
|
||||
pub fn eql(self: Self, b: Self) bool {
|
||||
if (!std.mem.eql(u8, self.mtype, b.mtype)) return false;
|
||||
return std.mem.eql(u8, self.msubtype, b.msubtype);
|
||||
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");
|
||||
}
|
||||
|
||||
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_xml = {} },
|
||||
.charset = "\\ \" ",
|
||||
.params = "charset=\"\\\\ \\\" \"",
|
||||
}, "text/xml;charset=\"\\\\ \\\" \" ");
|
||||
}
|
||||
|
||||
test "Mime: isHTML" {
|
||||
defer testing.reset();
|
||||
|
||||
const isHTML = struct {
|
||||
fn isHTML(expected: bool, input: []const u8) !void {
|
||||
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||
var mime = try Mime.parse(testing.arena_allocator, mutable_input);
|
||||
try testing.expectEqual(expected, mime.isHTML());
|
||||
}
|
||||
}.isHTML;
|
||||
try isHTML(true, "text/html");
|
||||
try isHTML(true, "text/html;");
|
||||
try isHTML(true, "text/html; charset=utf-8");
|
||||
try isHTML(false, "text/htm"); // htm not html
|
||||
try isHTML(false, "text/plain");
|
||||
try isHTML(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(testing.arena_allocator, 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| {
|
||||
try testing.expectEqual(ec, actual.charset.?);
|
||||
} else {
|
||||
try testing.expectEqual(null, actual.charset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,10 @@ const c = @cImport({
|
||||
@cInclude("dom/bindings/hubbub/parser.h");
|
||||
@cInclude("events/event_target.h");
|
||||
@cInclude("events/event.h");
|
||||
@cInclude("events/mouse_event.h");
|
||||
});
|
||||
|
||||
const mimalloc = @import("mimalloc");
|
||||
|
||||
const Callback = @import("jsruntime").Callback;
|
||||
const mimalloc = @import("mimalloc.zig");
|
||||
|
||||
// init initializes netsurf lib.
|
||||
// init starts a mimalloc heap arena for the netsurf session. The caller must
|
||||
@@ -260,7 +259,7 @@ pub const Tag = enum(u8) {
|
||||
|
||||
pub fn all() []Tag {
|
||||
comptime {
|
||||
const info = @typeInfo(Tag).Enum;
|
||||
const info = @typeInfo(Tag).@"enum";
|
||||
var l: [info.fields.len]Tag = undefined;
|
||||
for (info.fields, 0..) |field, i| {
|
||||
l[i] = @as(Tag, @enumFromInt(field.value));
|
||||
@@ -520,6 +519,7 @@ pub fn eventSetInternalType(evt: *Event, internal_type: EventType) !void {
|
||||
pub const EventType = enum(u8) {
|
||||
event = 0,
|
||||
progress_event = 1,
|
||||
custom_event = 2,
|
||||
};
|
||||
|
||||
pub const MutationEvent = c.dom_mutation_event;
|
||||
@@ -560,11 +560,6 @@ fn eventListenerGetData(lst: *EventListener) ?*anyopaque {
|
||||
}
|
||||
|
||||
// EventTarget
|
||||
pub const EventTargetType = enum(u4) {
|
||||
node = c.DOM_EVENT_TARGET_NODE,
|
||||
window = 2,
|
||||
};
|
||||
|
||||
pub const EventTarget = c.dom_event_target;
|
||||
|
||||
pub fn eventTargetToNode(et: *EventTarget) *Node {
|
||||
@@ -590,11 +585,61 @@ pub inline fn toEventTarget(comptime T: type, v: *T) *EventTarget {
|
||||
return @as(*EventTarget, @ptrCast(et_aligned));
|
||||
}
|
||||
|
||||
// The way we implement events is a lot like how Zig implements linked lists.
|
||||
// A Zig struct contains an `EventNode` field, i.e.:
|
||||
// node: parser.EventNode,
|
||||
//
|
||||
// When eventTargetAddEventListener is called, we pass in `&self.node`.
|
||||
// This is the pointer that's stored in the netsurf listener and it's the data
|
||||
// we can get back from the listener. We can call the node's `func` function,
|
||||
// passing the node itself, and the receiving function will know how to turn
|
||||
// that node into the our "self", i..e by using @fieldParentPtr.
|
||||
// https://www.openmymind.net/Zigs-New-LinkedList-API/
|
||||
pub const EventNode = struct {
|
||||
// Event id, used for removing. Internal Zig events won't have an id.
|
||||
// This is normally set to the callback.id for a JavaScript event.
|
||||
id: ?usize = null,
|
||||
|
||||
func: *const fn (node: *EventNode, event: *Event) void,
|
||||
|
||||
fn idFromListener(lst: *EventListener) ?usize {
|
||||
const ctx = eventListenerGetData(lst) orelse return null;
|
||||
const node: *EventNode = @alignCast(@ptrCast(ctx));
|
||||
return node.id;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn eventTargetAddEventListener(
|
||||
et: *EventTarget,
|
||||
typ: []const u8,
|
||||
node: *EventNode,
|
||||
capture: bool,
|
||||
) !void {
|
||||
const event_handler = struct {
|
||||
fn handle(event_: ?*Event, ptr_: ?*anyopaque) callconv(.C) void {
|
||||
const ptr = ptr_ orelse return;
|
||||
const event = event_ orelse return;
|
||||
|
||||
const node_: *EventNode = @alignCast(@ptrCast(ptr));
|
||||
node_.func(node_, event);
|
||||
}
|
||||
}.handle;
|
||||
|
||||
var listener: ?*EventListener = undefined;
|
||||
const errLst = c.dom_event_listener_create(event_handler, node, &listener);
|
||||
try DOMErr(errLst);
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
|
||||
const s = try strFromData(typ);
|
||||
const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn eventTargetHasListener(
|
||||
et: *EventTarget,
|
||||
typ: []const u8,
|
||||
capture: bool,
|
||||
cbk_id: usize,
|
||||
id: usize,
|
||||
) !?*EventListener {
|
||||
const str = try strFromData(typ);
|
||||
|
||||
@@ -619,9 +664,8 @@ pub fn eventTargetHasListener(
|
||||
// and capture property,
|
||||
// let's check if the callback handler is the same
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
const ehd = EventHandlerDataInternal.fromListener(listener);
|
||||
if (ehd) |d| {
|
||||
if (cbk_id == d.data.cbk.id()) {
|
||||
if (EventNode.idFromListener(listener)) |node_id| {
|
||||
if (node_id == id) {
|
||||
return lst;
|
||||
}
|
||||
}
|
||||
@@ -639,127 +683,18 @@ pub fn eventTargetHasListener(
|
||||
return null;
|
||||
}
|
||||
|
||||
// EventHandlerFunc is a zig function called when the event is dispatched to a
|
||||
// listener.
|
||||
// The EventHandlerFunc is responsible to call the callback included into the
|
||||
// EventHandlerData.
|
||||
pub const EventHandlerFunc = *const fn (event: ?*Event, data: EventHandlerData) void;
|
||||
|
||||
// EventHandler implements the function exposed in C and called by libdom.
|
||||
// It retrieves the EventHandlerInternalData and call the EventHandlerFunc with
|
||||
// the EventHandlerData in parameter.
|
||||
const EventHandler = struct {
|
||||
fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void {
|
||||
if (data) |d| {
|
||||
const ehd = EventHandlerDataInternal.get(d);
|
||||
ehd.handler(event, ehd.data);
|
||||
|
||||
// NOTE: we can not call func.deinit here
|
||||
// b/c the handler can be called several times
|
||||
// either on this dispatch event or in anoter one
|
||||
}
|
||||
}
|
||||
}.handle;
|
||||
|
||||
// EventHandlerData contains a JS callback and the data associated to the
|
||||
// handler.
|
||||
// If given, deinitFunc is called with the data pointer to allow the creator to
|
||||
// clean memory.
|
||||
// The callback is deinit by EventHandlerDataInternal. It must NOT be deinit
|
||||
// into deinitFunc.
|
||||
pub const EventHandlerData = struct {
|
||||
cbk: Callback,
|
||||
data: ?*anyopaque = null,
|
||||
// deinitFunc implements the data deinitialization.
|
||||
deinitFunc: ?DeinitFunc = null,
|
||||
|
||||
pub const DeinitFunc = *const fn (data: ?*anyopaque, alloc: std.mem.Allocator) void;
|
||||
};
|
||||
|
||||
// EventHandlerDataInternal groups the EventHandlerFunc and the EventHandlerData.
|
||||
const EventHandlerDataInternal = struct {
|
||||
data: EventHandlerData,
|
||||
handler: EventHandlerFunc,
|
||||
|
||||
fn init(alloc: std.mem.Allocator, handler: EventHandlerFunc, data: EventHandlerData) !*EventHandlerDataInternal {
|
||||
const ptr = try alloc.create(EventHandlerDataInternal);
|
||||
ptr.* = .{
|
||||
.data = data,
|
||||
.handler = handler,
|
||||
};
|
||||
return ptr;
|
||||
}
|
||||
|
||||
fn deinit(self: *EventHandlerDataInternal, alloc: std.mem.Allocator) void {
|
||||
if (self.data.deinitFunc) |d| d(self.data.data, alloc);
|
||||
self.data.cbk.deinit(alloc);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
fn get(data: *anyopaque) *EventHandlerDataInternal {
|
||||
const ptr: *align(@alignOf(*EventHandlerDataInternal)) anyopaque = @alignCast(data);
|
||||
return @as(*EventHandlerDataInternal, @ptrCast(ptr));
|
||||
}
|
||||
|
||||
// retrieve a EventHandlerDataInternal from a listener.
|
||||
fn fromListener(lst: *EventListener) ?*EventHandlerDataInternal {
|
||||
const data = eventListenerGetData(lst);
|
||||
// free cbk allocation made on eventTargetAddEventListener
|
||||
if (data == null) return null;
|
||||
|
||||
return get(data.?);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn eventTargetAddEventListener(
|
||||
et: *EventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
typ: []const u8,
|
||||
handlerFunc: EventHandlerFunc,
|
||||
data: EventHandlerData,
|
||||
capture: bool,
|
||||
) !void {
|
||||
// this allocation will be removed either on
|
||||
// eventTargetRemoveEventListener or eventTargetRemoveAllEventListeners
|
||||
const ehd = try EventHandlerDataInternal.init(alloc, handlerFunc, data);
|
||||
errdefer ehd.deinit(alloc);
|
||||
|
||||
// When a function is used as an event handler, its this parameter is bound
|
||||
// to the DOM element on which the listener is placed.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#this_in_dom_event_handlers
|
||||
try ehd.data.cbk.setThisArg(et);
|
||||
|
||||
const ctx = @as(*anyopaque, @ptrCast(ehd));
|
||||
var listener: ?*EventListener = undefined;
|
||||
const errLst = c.dom_event_listener_create(EventHandler, ctx, &listener);
|
||||
try DOMErr(errLst);
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
|
||||
const s = try strFromData(typ);
|
||||
const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn eventTargetRemoveEventListener(
|
||||
et: *EventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
typ: []const u8,
|
||||
lst: *EventListener,
|
||||
capture: bool,
|
||||
) !void {
|
||||
// free data allocation made on eventTargetAddEventListener
|
||||
const ehd = EventHandlerDataInternal.fromListener(lst);
|
||||
if (ehd) |d| d.deinit(alloc);
|
||||
|
||||
const s = try strFromData(typ);
|
||||
const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn eventTargetRemoveAllEventListeners(
|
||||
et: *EventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
) !void {
|
||||
pub fn eventTargetRemoveAllEventListeners(et: *EventTarget) !void {
|
||||
var next: ?*EventListenerEntry = undefined;
|
||||
var lst: ?*EventListener = undefined;
|
||||
|
||||
@@ -776,18 +711,16 @@ pub fn eventTargetRemoveAllEventListeners(
|
||||
try DOMErr(errIter);
|
||||
|
||||
if (lst) |listener| {
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
|
||||
const ehd = EventHandlerDataInternal.fromListener(listener);
|
||||
if (ehd) |d| d.deinit(alloc);
|
||||
|
||||
const err = eventTargetVtable(et).remove_event_listener.?(
|
||||
et,
|
||||
null,
|
||||
lst,
|
||||
false,
|
||||
);
|
||||
try DOMErr(err);
|
||||
if (EventNode.idFromListener(listener) != null) {
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
const err = eventTargetVtable(et).remove_event_listener.?(
|
||||
et,
|
||||
null,
|
||||
lst,
|
||||
false,
|
||||
);
|
||||
try DOMErr(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (next == null) {
|
||||
@@ -806,18 +739,15 @@ pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool {
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn eventTargetGetType(et: *EventTarget) !EventTargetType {
|
||||
var res: c.dom_event_target_type = undefined;
|
||||
const err = eventTargetVtable(et).dom_event_target_get_type.?(et, &res);
|
||||
try DOMErr(err);
|
||||
|
||||
return @enumFromInt(res);
|
||||
pub fn elementDispatchEvent(element: *Element, event: *Event) !bool {
|
||||
const et: *EventTarget = toEventTarget(Element, element);
|
||||
return eventTargetDispatchEvent(et, @ptrCast(event));
|
||||
}
|
||||
|
||||
pub fn eventTargetTBaseFieldName(comptime T: type) ?[]const u8 {
|
||||
std.debug.assert(@inComptime());
|
||||
switch (@typeInfo(T)) {
|
||||
.Struct => |ti| {
|
||||
.@"struct" => |ti| {
|
||||
for (ti.fields) |f| {
|
||||
if (f.type == EventTargetTBase) return f.name;
|
||||
}
|
||||
@@ -837,10 +767,8 @@ pub const EventTargetTBase = extern struct {
|
||||
.remove_event_listener = remove_event_listener,
|
||||
.add_event_listener = add_event_listener,
|
||||
.iter_event_listener = iter_event_listener,
|
||||
.dom_event_target_get_type = dom_event_target_get_type,
|
||||
},
|
||||
eti: c.dom_event_target_internal = c.dom_event_target_internal{ .listeners = null },
|
||||
et_type: c.dom_event_target_type = @intFromEnum(EventTargetType.node),
|
||||
|
||||
pub fn add_event_listener(et: [*c]c.dom_event_target, t: [*c]c.dom_string, l: ?*c.struct_dom_event_listener, capture: bool) callconv(.C) c.dom_exception {
|
||||
const self = @as(*Self, @ptrCast(et));
|
||||
@@ -873,19 +801,63 @@ pub const EventTargetTBase = extern struct {
|
||||
const self = @as(*Self, @ptrCast(et));
|
||||
return c._dom_event_target_iter_event_listener(self.eti, t, capture, cur, next, l);
|
||||
}
|
||||
|
||||
pub fn dom_event_target_get_type(
|
||||
et: [*c]c.dom_event_target,
|
||||
res: [*c]c.dom_event_target_type,
|
||||
) callconv(.C) c.dom_exception {
|
||||
const self = @as(*Self, @ptrCast(et));
|
||||
|
||||
res.* = self.et_type;
|
||||
|
||||
return c.DOM_NO_ERR;
|
||||
}
|
||||
};
|
||||
|
||||
// MouseEvent
|
||||
|
||||
pub const MouseEvent = c.dom_mouse_event;
|
||||
|
||||
pub fn mouseEventCreate() !*MouseEvent {
|
||||
var evt: ?*MouseEvent = undefined;
|
||||
const err = c._dom_mouse_event_create(&evt);
|
||||
try DOMErr(err);
|
||||
return evt.?;
|
||||
}
|
||||
|
||||
pub fn mouseEventDestroy(evt: *MouseEvent) void {
|
||||
c._dom_mouse_event_destroy(evt);
|
||||
}
|
||||
|
||||
const MouseEventOpts = struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
bubbles: bool = false,
|
||||
cancelable: bool = false,
|
||||
ctrl: bool = false,
|
||||
alt: bool = false,
|
||||
shift: bool = false,
|
||||
meta: bool = false,
|
||||
button: u16 = 0,
|
||||
click_count: u16 = 1,
|
||||
};
|
||||
|
||||
pub fn mouseEventInit(evt: *MouseEvent, typ: []const u8, opts: MouseEventOpts) !void {
|
||||
const s = try strFromData(typ);
|
||||
const err = c._dom_mouse_event_init(
|
||||
evt,
|
||||
s,
|
||||
opts.bubbles,
|
||||
opts.cancelable,
|
||||
null, // dom_abstract_view* ?
|
||||
opts.click_count, // details
|
||||
opts.x, // screen_x
|
||||
opts.y, // screen_y
|
||||
opts.x, // client_x
|
||||
opts.y, // client_y
|
||||
opts.ctrl,
|
||||
opts.alt,
|
||||
opts.shift,
|
||||
opts.meta,
|
||||
opts.button,
|
||||
null, // related target
|
||||
);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn mouseEventDefaultPrevented(evt: *MouseEvent) !bool {
|
||||
return eventDefaultPrevented(@ptrCast(evt));
|
||||
}
|
||||
|
||||
// NodeType
|
||||
|
||||
pub const NodeType = enum(u4) {
|
||||
@@ -1034,6 +1006,7 @@ pub fn nodeLocalName(node: *Node) ![]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
const err = nodeVtable(node).dom_node_get_local_name.?(node, &s);
|
||||
try DOMErr(err);
|
||||
if (s == null) return "";
|
||||
var s_lower: ?*String = undefined;
|
||||
const errStr = c.dom_string_tolower(s, true, &s_lower);
|
||||
try DOMErr(errStr);
|
||||
@@ -1124,6 +1097,7 @@ pub fn nodeName(node: *Node) ![]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
const err = nodeVtable(node).dom_node_get_node_name.?(node, &s);
|
||||
try DOMErr(err);
|
||||
if (s == null) return "";
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
@@ -1211,8 +1185,8 @@ pub fn nodeInsertBefore(node: *Node, new_node: *Node, ref_node: *Node) !*Node {
|
||||
return res.?;
|
||||
}
|
||||
|
||||
pub fn nodeIsDefaultNamespace(node: *Node, namespace: []const u8) !bool {
|
||||
const s = try strFromData(namespace);
|
||||
pub fn nodeIsDefaultNamespace(node: *Node, namespace_: ?[]const u8) !bool {
|
||||
const s = if (namespace_) |n| try strFromData(n) else null;
|
||||
var res: bool = undefined;
|
||||
const err = nodeVtable(node).dom_node_is_default_namespace.?(node, s, &res);
|
||||
try DOMErr(err);
|
||||
@@ -1241,9 +1215,10 @@ pub fn nodeLookupPrefix(node: *Node, namespace: []const u8) !?[]const u8 {
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
pub fn nodeLookupNamespaceURI(node: *Node, prefix: ?[]const u8) !?[]const u8 {
|
||||
pub fn nodeLookupNamespaceURI(node: *Node, prefix_: ?[]const u8) !?[]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
const err = nodeVtable(node).dom_node_lookup_namespace.?(node, try strFromData(prefix.?), &s);
|
||||
const prefix: ?*String = if (prefix_) |p| try strFromData(p) else null;
|
||||
const err = nodeVtable(node).dom_node_lookup_namespace.?(node, prefix, &s);
|
||||
try DOMErr(err);
|
||||
if (s == null) return null;
|
||||
return strToData(s.?);
|
||||
@@ -1275,11 +1250,11 @@ pub fn nodeHasAttributes(node: *Node) !bool {
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn nodeGetAttributes(node: *Node) !*NamedNodeMap {
|
||||
pub fn nodeGetAttributes(node: *Node) !?*NamedNodeMap {
|
||||
var res: ?*NamedNodeMap = undefined;
|
||||
const err = nodeVtable(node).dom_node_get_attributes.?(node, &res);
|
||||
try DOMErr(err);
|
||||
return res.?;
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn nodeGetNamespace(node: *Node) !?[]const u8 {
|
||||
@@ -1303,6 +1278,11 @@ pub inline fn nodeToElement(node: *Node) *Element {
|
||||
return @as(*Element, @ptrCast(node));
|
||||
}
|
||||
|
||||
// nodeToDocument is an helper to convert a node to an document.
|
||||
pub inline fn nodeToDocument(node: *Node) *Document {
|
||||
return @as(*Document, @ptrCast(node));
|
||||
}
|
||||
|
||||
// CharacterData
|
||||
pub const CharacterData = c.dom_characterdata;
|
||||
|
||||
@@ -1635,6 +1615,11 @@ pub fn tokenListGetValue(l: *TokenList) !?[]const u8 {
|
||||
return strToData(res.?);
|
||||
}
|
||||
|
||||
pub fn tokenListSetValue(l: *TokenList, value: []const u8) !void {
|
||||
const err = c.dom_tokenlist_set_value(l, try strFromData(value));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
// ElementHTML
|
||||
pub const ElementHTML = c.dom_html_element;
|
||||
|
||||
@@ -1646,6 +1631,14 @@ pub fn elementHTMLGetTagType(elem_html: *ElementHTML) !Tag {
|
||||
var tag_type: c.dom_html_element_type = undefined;
|
||||
const err = elementHTMLVtable(elem_html).dom_html_element_get_tag_type.?(elem_html, &tag_type);
|
||||
try DOMErr(err);
|
||||
|
||||
if (tag_type >= 255) {
|
||||
// This is questionable, but std.meta.intToEnum has more overhead
|
||||
// Added this because this WPT test started to fail once we
|
||||
// introduced an SVGElement:
|
||||
// html/dom/documents/dom-tree-accessors/document.title-09.html
|
||||
return Tag.undef;
|
||||
}
|
||||
return @as(Tag, @enumFromInt(tag_type));
|
||||
}
|
||||
|
||||
@@ -2205,21 +2198,28 @@ fn parseParams(enc: ?[:0]const u8) c.dom_hubbub_parser_params {
|
||||
|
||||
fn parseData(parser: *c.dom_hubbub_parser, reader: anytype) !void {
|
||||
var err: c.hubbub_error = undefined;
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var ln = buffer.len;
|
||||
while (ln > 0) {
|
||||
ln = try reader.read(&buffer);
|
||||
err = c.dom_hubbub_parser_parse_chunk(parser, &buffer, ln);
|
||||
// TODO handle encoding change error return.
|
||||
// When the HTML contains a META tag with a different encoding than the
|
||||
// original one, a c.DOM_HUBBUB_HUBBUB_ERR_ENCODINGCHANGE error is
|
||||
// returned.
|
||||
// In this case, we must restart the parsing with the new detected
|
||||
// encoding. The detected encoding is stored in the document and we can
|
||||
// get it with documentGetInputEncoding().
|
||||
try parserErr(err);
|
||||
const TI = @typeInfo(@TypeOf(reader));
|
||||
if (TI == .pointer and @hasDecl(TI.pointer.child, "next")) {
|
||||
while (try reader.next()) |data| {
|
||||
err = c.dom_hubbub_parser_parse_chunk(parser, data.ptr, data.len);
|
||||
try parserErr(err);
|
||||
}
|
||||
} else {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var ln = buffer.len;
|
||||
while (ln > 0) {
|
||||
ln = try reader.read(&buffer);
|
||||
err = c.dom_hubbub_parser_parse_chunk(parser, &buffer, ln);
|
||||
// TODO handle encoding change error return.
|
||||
// When the HTML contains a META tag with a different encoding than the
|
||||
// original one, a c.DOM_HUBBUB_HUBBUB_ERR_ENCODINGCHANGE error is
|
||||
// returned.
|
||||
// In this case, we must restart the parsing with the new detected
|
||||
// encoding. The detected encoding is stored in the document and we can
|
||||
// get it with documentGetInputEncoding().
|
||||
try parserErr(err);
|
||||
}
|
||||
}
|
||||
|
||||
err = c.dom_hubbub_parser_completed(parser);
|
||||
try parserErr(err);
|
||||
}
|
||||
@@ -2290,3 +2290,20 @@ pub fn documentHTMLGetCurrentScript(doc: *DocumentHTML) !?*Script {
|
||||
if (elem == null) return null;
|
||||
return @ptrCast(elem.?);
|
||||
}
|
||||
|
||||
pub fn documentHTMLSetLocation(T: type, doc: *DocumentHTML, location: *T) !void {
|
||||
const l = @as(*anyopaque, @ptrCast(location));
|
||||
const err = documentHTMLVtable(doc).set_location.?(doc, l);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn documentHTMLGetLocation(T: type, doc: *DocumentHTML) !?*T {
|
||||
var l: ?*anyopaque = undefined;
|
||||
const err = documentHTMLVtable(doc).get_location.?(doc, &l);
|
||||
try DOMErr(err);
|
||||
|
||||
if (l == null) return null;
|
||||
|
||||
const ptr: *align(@alignOf(*T)) anyopaque = @alignCast(l.?);
|
||||
return @as(*T, @ptrCast(ptr));
|
||||
}
|
||||
46
src/browser/polyfill/fetch.zig
Normal file
46
src/browser/polyfill/fetch.zig
Normal file
@@ -0,0 +1,46 @@
|
||||
// fetch.js code comes from
|
||||
// https://github.com/JakeChampion/fetch/blob/main/fetch.js
|
||||
//
|
||||
// The original code source is available in MIT license.
|
||||
//
|
||||
// The script comes from the built version from npm.
|
||||
// You can get the package with the command:
|
||||
//
|
||||
// wget $(npm view whatwg-fetch dist.tarball)
|
||||
//
|
||||
// The source is the content of `package/dist/fetch.umd.js` file.
|
||||
pub const source = @embedFile("fetch.js");
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.fetch" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try @import("polyfill.zig").load(testing.allocator, runner.scope);
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var ok = false;
|
||||
\\ const request = new Request("http://127.0.0.1:9582/loader");
|
||||
\\ fetch(request).then((response) => { ok = response.ok; });
|
||||
\\ false;
|
||||
,
|
||||
"false",
|
||||
},
|
||||
// all events have been resolved.
|
||||
.{ "ok", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var ok2 = false;
|
||||
\\ const request2 = new Request("http://127.0.0.1:9582/loader");
|
||||
\\ (async function () { resp = await fetch(request2); ok2 = resp.ok; }());
|
||||
\\ false;
|
||||
,
|
||||
"false",
|
||||
},
|
||||
// all events have been resolved.
|
||||
.{ "ok2", "true" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -19,10 +19,8 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Env = jsruntime.Env;
|
||||
|
||||
const fetch = @import("fetch.zig").fetch_polyfill;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
const log = std.log.scoped(.polyfill);
|
||||
|
||||
@@ -33,23 +31,23 @@ const modules = [_]struct {
|
||||
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
|
||||
};
|
||||
|
||||
pub fn load(alloc: std.mem.Allocator, env: Env) !void {
|
||||
var try_catch: jsruntime.TryCatch = undefined;
|
||||
try_catch.init(env);
|
||||
pub fn load(allocator: Allocator, scope: *Env.Scope) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(scope);
|
||||
defer try_catch.deinit();
|
||||
|
||||
for (modules) |m| {
|
||||
const res = env.exec(m.source, m.name) catch {
|
||||
if (try try_catch.err(alloc, env)) |msg| {
|
||||
defer alloc.free(msg);
|
||||
const res = scope.exec(m.source, m.name) catch |err| {
|
||||
if (try try_catch.err(allocator)) |msg| {
|
||||
defer allocator.free(msg);
|
||||
log.err("load {s}: {s}", .{ m.name, msg });
|
||||
}
|
||||
return;
|
||||
return err;
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(alloc, env);
|
||||
defer alloc.free(msg);
|
||||
const msg = try res.toString(allocator);
|
||||
defer allocator.free(msg);
|
||||
log.debug("load {s}: {s}", .{ m.name, msg });
|
||||
}
|
||||
}
|
||||
906
src/browser/storage/cookie.zig
Normal file
906
src/browser/storage/cookie.zig
Normal file
@@ -0,0 +1,906 @@
|
||||
const std = @import("std");
|
||||
const Uri = std.Uri;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const http = @import("../../http/client.zig");
|
||||
const DateTime = @import("../../datetime.zig").DateTime;
|
||||
const public_suffix_list = @import("../../data/public_suffix_list.zig").lookup;
|
||||
|
||||
const log = std.log.scoped(.cookie);
|
||||
|
||||
pub const LookupOpts = struct {
|
||||
request_time: ?i64 = null,
|
||||
origin_uri: ?*const Uri = null,
|
||||
navigation: bool = true,
|
||||
};
|
||||
|
||||
pub const Jar = struct {
|
||||
allocator: Allocator,
|
||||
cookies: std.ArrayListUnmanaged(Cookie),
|
||||
|
||||
pub fn init(allocator: Allocator) Jar {
|
||||
return .{
|
||||
.cookies = .{},
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Jar) void {
|
||||
for (self.cookies.items) |c| {
|
||||
c.deinit();
|
||||
}
|
||||
self.cookies.deinit(self.allocator);
|
||||
}
|
||||
|
||||
pub fn add(
|
||||
self: *Jar,
|
||||
cookie: Cookie,
|
||||
request_time: i64,
|
||||
) !void {
|
||||
const is_expired = isCookieExpired(&cookie, request_time);
|
||||
defer if (is_expired) {
|
||||
cookie.deinit();
|
||||
};
|
||||
|
||||
for (self.cookies.items, 0..) |*c, i| {
|
||||
if (areCookiesEqual(&cookie, c)) {
|
||||
c.deinit();
|
||||
if (is_expired) {
|
||||
_ = self.cookies.swapRemove(i);
|
||||
} else {
|
||||
self.cookies.items[i] = cookie;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_expired) {
|
||||
try self.cookies.append(self.allocator, cookie);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forRequest(self: *Jar, target_uri: *const Uri, writer: anytype, opts: LookupOpts) !void {
|
||||
const target_path = target_uri.path.percent_encoded;
|
||||
const target_host = (target_uri.host orelse return error.InvalidURI).percent_encoded;
|
||||
|
||||
const same_site = try areSameSite(opts.origin_uri, target_host);
|
||||
const is_secure = std.mem.eql(u8, target_uri.scheme, "https");
|
||||
|
||||
var i: usize = 0;
|
||||
var cookies = self.cookies.items;
|
||||
const navigation = opts.navigation;
|
||||
const request_time = opts.request_time orelse std.time.timestamp();
|
||||
|
||||
var first = true;
|
||||
while (i < cookies.len) {
|
||||
const cookie = &cookies[i];
|
||||
|
||||
if (isCookieExpired(cookie, request_time)) {
|
||||
cookie.deinit();
|
||||
_ = self.cookies.swapRemove(i);
|
||||
// don't increment i !
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
|
||||
if (is_secure == false and cookie.secure) {
|
||||
// secure cookie can only be sent over HTTPs
|
||||
continue;
|
||||
}
|
||||
|
||||
if (same_site == false) {
|
||||
// If we aren't on the "same site" (matching 2nd level domain
|
||||
// taking into account public suffix list), then the cookie
|
||||
// can only be sent if cookie.same_site == .none, or if
|
||||
// we're navigating to (as opposed to, say, loading an image)
|
||||
// and cookie.same_site == .lax
|
||||
switch (cookie.same_site) {
|
||||
.strict => continue,
|
||||
.lax => if (navigation == false) continue,
|
||||
.none => {},
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const domain = cookie.domain;
|
||||
if (domain[0] == '.') {
|
||||
// When a Set-Cookie header has a Domain attribute
|
||||
// Then we will _always_ prefix it with a dot, extending its
|
||||
// availability to all subdomains (yes, setting the Domain
|
||||
// attributes EXPANDS the domains which the cookie will be
|
||||
// sent to, to always include all subdomains).
|
||||
if (std.mem.eql(u8, target_host, domain[1..]) == false and std.mem.endsWith(u8, target_host, domain) == false) {
|
||||
continue;
|
||||
}
|
||||
} else if (std.mem.eql(u8, target_host, domain) == false) {
|
||||
// When the Domain attribute isn't specific, then the cookie
|
||||
// is only sent on an exact match.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const path = cookie.path;
|
||||
if (path[path.len - 1] == '/') {
|
||||
// If our cookie has a trailing slash, we can only match is
|
||||
// the target path is a perfix. I.e., if our path is
|
||||
// /doc/ we can only match /doc/*
|
||||
if (std.mem.startsWith(u8, target_path, path) == false) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Our cookie path is something like /hello
|
||||
if (std.mem.startsWith(u8, target_path, path) == false) {
|
||||
// The target path has to either be /hello (it isn't)
|
||||
continue;
|
||||
} else if (target_path.len < path.len or (target_path.len > path.len and target_path[path.len] != '/')) {
|
||||
// Or it has to be something like /hello/* (it isn't)
|
||||
// it isn't!
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// we have a match!
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
try writer.writeAll("; ");
|
||||
}
|
||||
try writeCookie(cookie, writer);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn populateFromResponse(self: *Jar, uri: *const Uri, header: *const http.ResponseHeader) !void {
|
||||
const now = std.time.timestamp();
|
||||
var it = header.iterate("set-cookie");
|
||||
while (it.next()) |set_cookie| {
|
||||
const c = Cookie.parse(self.allocator, uri, set_cookie) catch |err| {
|
||||
log.warn("Couldn't parse cookie '{s}': {}\n", .{ set_cookie, err });
|
||||
continue;
|
||||
};
|
||||
try self.add(c, now);
|
||||
}
|
||||
}
|
||||
|
||||
fn writeCookie(cookie: *const Cookie, writer: anytype) !void {
|
||||
if (cookie.name.len > 0) {
|
||||
try writer.writeAll(cookie.name);
|
||||
try writer.writeByte('=');
|
||||
}
|
||||
if (cookie.value.len > 0) {
|
||||
try writer.writeAll(cookie.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const CookieList = struct {
|
||||
_cookies: std.ArrayListUnmanaged(*const Cookie) = .{},
|
||||
|
||||
pub fn deinit(self: *CookieList, allocator: Allocator) void {
|
||||
self._cookies.deinit(allocator);
|
||||
}
|
||||
|
||||
pub fn cookies(self: *const CookieList) []*const Cookie {
|
||||
return self._cookies.items;
|
||||
}
|
||||
|
||||
pub fn len(self: *const CookieList) usize {
|
||||
return self._cookies.items.len;
|
||||
}
|
||||
|
||||
pub fn write(self: *const CookieList, writer: anytype) !void {
|
||||
const all = self._cookies.items;
|
||||
if (all.len == 0) {
|
||||
return;
|
||||
}
|
||||
try writeCookie(all[0], writer);
|
||||
for (all[1..]) |cookie| {
|
||||
try writer.writeAll("; ");
|
||||
try writeCookie(cookie, writer);
|
||||
}
|
||||
}
|
||||
|
||||
fn writeCookie(cookie: *const Cookie, writer: anytype) !void {
|
||||
if (cookie.name.len > 0) {
|
||||
try writer.writeAll(cookie.name);
|
||||
try writer.writeByte('=');
|
||||
}
|
||||
if (cookie.value.len > 0) {
|
||||
try writer.writeAll(cookie.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn isCookieExpired(cookie: *const Cookie, now: i64) bool {
|
||||
const ce = cookie.expires orelse return false;
|
||||
return ce <= now;
|
||||
}
|
||||
|
||||
fn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool {
|
||||
if (std.mem.eql(u8, a.name, b.name) == false) {
|
||||
return false;
|
||||
}
|
||||
if (std.mem.eql(u8, a.domain, b.domain) == false) {
|
||||
return false;
|
||||
}
|
||||
if (std.mem.eql(u8, a.path, b.path) == false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn areSameSite(origin_uri_: ?*const std.Uri, target_host: []const u8) !bool {
|
||||
const origin_uri = origin_uri_ orelse return true;
|
||||
const origin_host = (origin_uri.host orelse return error.InvalidURI).percent_encoded;
|
||||
|
||||
// common case
|
||||
if (std.mem.eql(u8, target_host, origin_host)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return std.mem.eql(u8, findSecondLevelDomain(target_host), findSecondLevelDomain(origin_host));
|
||||
}
|
||||
|
||||
fn findSecondLevelDomain(host: []const u8) []const u8 {
|
||||
var i = std.mem.lastIndexOfScalar(u8, host, '.') orelse return host;
|
||||
while (true) {
|
||||
i = std.mem.lastIndexOfScalar(u8, host[0..i], '.') orelse return host;
|
||||
const strip = i + 1;
|
||||
if (public_suffix_list(host[strip..]) == false) {
|
||||
return host[strip..];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const Cookie = struct {
|
||||
arena: ArenaAllocator,
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
path: []const u8,
|
||||
domain: []const u8,
|
||||
expires: ?i64,
|
||||
secure: bool,
|
||||
http_only: bool,
|
||||
same_site: SameSite,
|
||||
|
||||
const SameSite = enum {
|
||||
strict,
|
||||
lax,
|
||||
none,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *const Cookie) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
// There's https://datatracker.ietf.org/doc/html/rfc6265 but browsers are
|
||||
// far less strict. I only found 2 cases where browsers will reject a cookie:
|
||||
// - a byte 0...32 and 127..255 anywhere in the cookie (the HTTP header
|
||||
// parser might take care of this already)
|
||||
// - any shenanigans with the domain attribute - it has to be the current
|
||||
// domain or one of higher order, exluding TLD.
|
||||
// Anything else, will turn into a cookie.
|
||||
// Single value? That's a cookie with an emtpy name and a value
|
||||
// Key or Values with characters the RFC says aren't allowed? Allowed! (
|
||||
// (as long as the characters are 32...126)
|
||||
// Invalid attributes? Ignored.
|
||||
// Invalid attribute values? Ignore.
|
||||
// Duplicate attributes - use the last valid
|
||||
// Value-less attributes with a value? Ignore the value
|
||||
pub fn parse(allocator: Allocator, uri: *const std.Uri, str: []const u8) !Cookie {
|
||||
if (str.len == 0) {
|
||||
// this check is necessary, `std.mem.minMax` asserts len > 0
|
||||
return error.Empty;
|
||||
}
|
||||
|
||||
const host = (uri.host orelse return error.InvalidURI).percent_encoded;
|
||||
|
||||
{
|
||||
const min, const max = std.mem.minMax(u8, str);
|
||||
if (min < 32 or max > 126) {
|
||||
return error.InvalidByteSequence;
|
||||
}
|
||||
}
|
||||
|
||||
const cookie_name, const cookie_value, const rest = parseNameValue(str) catch {
|
||||
return error.InvalidNameValue;
|
||||
};
|
||||
|
||||
var scrap: [8]u8 = undefined;
|
||||
|
||||
var path: ?[]const u8 = null;
|
||||
var domain: ?[]const u8 = null;
|
||||
var secure: ?bool = null;
|
||||
var max_age: ?i64 = null;
|
||||
var http_only: ?bool = null;
|
||||
var expires: ?DateTime = null;
|
||||
var same_site: ?Cookie.SameSite = null;
|
||||
|
||||
var it = std.mem.splitScalar(u8, rest, ';');
|
||||
while (it.next()) |attribute| {
|
||||
const sep = std.mem.indexOfScalarPos(u8, attribute, 0, '=') orelse attribute.len;
|
||||
const key_string = trim(attribute[0..sep]);
|
||||
|
||||
if (key_string.len > 8) {
|
||||
// not valid, ignore
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make sure no one changes our max length without also expanding the size of scrap
|
||||
std.debug.assert(key_string.len <= 8);
|
||||
|
||||
const key = std.meta.stringToEnum(enum {
|
||||
path,
|
||||
domain,
|
||||
secure,
|
||||
@"max-age",
|
||||
expires,
|
||||
httponly,
|
||||
samesite,
|
||||
}, std.ascii.lowerString(&scrap, key_string)) orelse continue;
|
||||
|
||||
var value = if (sep == attribute.len) "" else trim(attribute[sep + 1 ..]);
|
||||
switch (key) {
|
||||
.path => {
|
||||
// path attribute value either begins with a '/' or we
|
||||
// ignore it and use the "default-path" algorithm
|
||||
if (value.len > 0 and value[0] == '/') {
|
||||
path = value;
|
||||
}
|
||||
},
|
||||
.domain => {
|
||||
if (value.len == 0) {
|
||||
continue;
|
||||
}
|
||||
if (value[0] == '.') {
|
||||
// leading dot is ignored
|
||||
value = value[1..];
|
||||
}
|
||||
|
||||
if (std.mem.indexOfScalarPos(u8, value, 0, '.') == null) {
|
||||
// can't set a cookie for a TLD
|
||||
return error.InvalidDomain;
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, host, value) == false) {
|
||||
return error.InvalidDomain;
|
||||
}
|
||||
domain = value;
|
||||
},
|
||||
.secure => secure = true,
|
||||
.@"max-age" => max_age = std.fmt.parseInt(i64, value, 10) catch continue,
|
||||
.expires => expires = DateTime.parse(value, .rfc822) catch continue,
|
||||
.httponly => http_only = true,
|
||||
.samesite => {
|
||||
same_site = std.meta.stringToEnum(Cookie.SameSite, std.ascii.lowerString(&scrap, value)) orelse continue;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (same_site == .none and secure == null) {
|
||||
return error.InsecureSameSite;
|
||||
}
|
||||
|
||||
var arena = ArenaAllocator.init(allocator);
|
||||
errdefer arena.deinit();
|
||||
const aa = arena.allocator();
|
||||
const owned_name = try aa.dupe(u8, cookie_name);
|
||||
const owned_value = try aa.dupe(u8, cookie_value);
|
||||
const owned_path = if (path) |p|
|
||||
try aa.dupe(u8, p)
|
||||
else
|
||||
try defaultPath(aa, uri.path.percent_encoded);
|
||||
|
||||
const owned_domain = if (domain) |d| blk: {
|
||||
const s = try aa.alloc(u8, d.len + 1);
|
||||
s[0] = '.';
|
||||
@memcpy(s[1..], d);
|
||||
break :blk s;
|
||||
} else blk: {
|
||||
break :blk try aa.dupe(u8, host);
|
||||
};
|
||||
|
||||
var normalized_expires: ?i64 = null;
|
||||
if (max_age) |ma| {
|
||||
normalized_expires = std.time.timestamp() + ma;
|
||||
} else {
|
||||
// max age takes priority over expires
|
||||
if (expires) |e| {
|
||||
normalized_expires = e.sub(DateTime.now(), .seconds);
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.arena = arena,
|
||||
.name = owned_name,
|
||||
.value = owned_value,
|
||||
.path = owned_path,
|
||||
.same_site = same_site orelse .lax,
|
||||
.secure = secure orelse false,
|
||||
.http_only = http_only orelse false,
|
||||
.domain = owned_domain,
|
||||
.expires = normalized_expires,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseNameValue(str: []const u8) !struct { []const u8, []const u8, []const u8 } {
|
||||
const key_value_end = std.mem.indexOfScalarPos(u8, str, 0, ';') orelse str.len;
|
||||
const rest = if (key_value_end == str.len) "" else str[key_value_end + 1 ..];
|
||||
|
||||
const sep = std.mem.indexOfScalarPos(u8, str[0..key_value_end], 0, '=') orelse {
|
||||
const value = trim(str[0..key_value_end]);
|
||||
if (value.len == 0) {
|
||||
return error.Empty;
|
||||
}
|
||||
return .{ "", value, rest };
|
||||
};
|
||||
|
||||
const name = trim(str[0..sep]);
|
||||
const value = trim(str[sep + 1 .. key_value_end]);
|
||||
return .{ name, value, rest };
|
||||
}
|
||||
};
|
||||
|
||||
fn defaultPath(allocator: Allocator, document_path: []const u8) ![]const u8 {
|
||||
if (document_path.len == 0 or (document_path.len == 1 and document_path[0] == '/')) {
|
||||
return "/";
|
||||
}
|
||||
const last = std.mem.lastIndexOfScalar(u8, document_path[1..], '/') orelse {
|
||||
return "/";
|
||||
};
|
||||
return try allocator.dupe(u8, document_path[0 .. last + 1]);
|
||||
}
|
||||
|
||||
fn trim(str: []const u8) []const u8 {
|
||||
return std.mem.trim(u8, str, &std.ascii.whitespace);
|
||||
}
|
||||
|
||||
fn trimLeft(str: []const u8) []const u8 {
|
||||
return std.mem.trimLeft(u8, str, &std.ascii.whitespace);
|
||||
}
|
||||
|
||||
fn trimRight(str: []const u8) []const u8 {
|
||||
return std.mem.trimLeft(u8, str, &std.ascii.whitespace);
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "cookie: findSecondLevelDomain" {
|
||||
const cases = [_]struct { []const u8, []const u8 }{
|
||||
.{ "", "" },
|
||||
.{ "com", "com" },
|
||||
.{ "lightpanda.io", "lightpanda.io" },
|
||||
.{ "lightpanda.io", "test.lightpanda.io" },
|
||||
.{ "lightpanda.io", "first.test.lightpanda.io" },
|
||||
.{ "www.gov.uk", "www.gov.uk" },
|
||||
.{ "stats.gov.uk", "www.stats.gov.uk" },
|
||||
.{ "api.gov.uk", "api.gov.uk" },
|
||||
.{ "dev.api.gov.uk", "dev.api.gov.uk" },
|
||||
.{ "dev.api.gov.uk", "1.dev.api.gov.uk" },
|
||||
};
|
||||
for (cases) |c| {
|
||||
try testing.expectEqual(c.@"0", findSecondLevelDomain(c.@"1"));
|
||||
}
|
||||
}
|
||||
|
||||
test "Jar: add" {
|
||||
const expectCookies = struct {
|
||||
fn expect(expected: []const struct { []const u8, []const u8 }, jar: Jar) !void {
|
||||
try testing.expectEqual(expected.len, jar.cookies.items.len);
|
||||
LOOP: for (expected) |e| {
|
||||
for (jar.cookies.items) |c| {
|
||||
if (std.mem.eql(u8, e.@"0", c.name) and std.mem.eql(u8, e.@"1", c.value)) {
|
||||
continue :LOOP;
|
||||
}
|
||||
}
|
||||
std.debug.print("Cookie ({s}={s}) not found", .{ e.@"0", e.@"1" });
|
||||
return error.CookieNotFound;
|
||||
}
|
||||
}
|
||||
}.expect;
|
||||
|
||||
const now = std.time.timestamp();
|
||||
|
||||
var jar = Jar.init(testing.allocator);
|
||||
defer jar.deinit();
|
||||
try expectCookies(&.{}, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9000;Max-Age=0"), now);
|
||||
try expectCookies(&.{}, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9000"), now);
|
||||
try expectCookies(&.{.{ "over", "9000" }}, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9000!!"), now);
|
||||
try expectCookies(&.{.{ "over", "9000!!" }}, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "spice=flow"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flow" } }, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "spice=flows;Path=/"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" } }, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9001;Path=/other"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9001" } }, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9002;Path=/;Domain=lightpanda.io"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9001" }, .{ "over", "9002" } }, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=x;Path=/other;Max-Age=-200"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9002" } }, jar);
|
||||
}
|
||||
|
||||
test "Jar: forRequest" {
|
||||
const expectCookies = struct {
|
||||
fn expect(expected: []const u8, jar: *Jar, target_uri: Uri, opts: LookupOpts) !void {
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer arr.deinit(testing.allocator);
|
||||
try jar.forRequest(&target_uri, arr.writer(testing.allocator), opts);
|
||||
try testing.expectEqual(expected, arr.items);
|
||||
}
|
||||
}.expect;
|
||||
|
||||
const now = std.time.timestamp();
|
||||
|
||||
var jar = Jar.init(testing.allocator);
|
||||
defer jar.deinit();
|
||||
|
||||
const test_uri_2 = Uri.parse("http://test.lightpanda.io/") catch unreachable;
|
||||
|
||||
{
|
||||
// test with no cookies
|
||||
try expectCookies("", &jar, test_uri, .{});
|
||||
}
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global1=1"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global2=2;Max-Age=30;domain=lightpanda.io"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "path1=3;Path=/about"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "path2=4;Path=/docs/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "secure=5;Secure"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "sitenone=6;SameSite=None;Path=/x/;Secure"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "sitelax=7;SameSite=Lax;Path=/x/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "sitestrict=8;SameSite=Strict;Path=/x/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri_2, "domain1=9;domain=test.lightpanda.io"), now);
|
||||
|
||||
// nothing fancy here
|
||||
try expectCookies("global1=1; global2=2", &jar, test_uri, .{});
|
||||
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .navigation = false });
|
||||
|
||||
// We have a cookie where Domain=lightpanda.io
|
||||
// This should _not_ match xyxlightpanda.io
|
||||
try expectCookies("", &jar, try std.Uri.parse("http://anothersitelightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
});
|
||||
|
||||
// matching path without trailing /
|
||||
try expectCookies("global1=1; global2=2; path1=3", &jar, try std.Uri.parse("http://lightpanda.io/about"), .{
|
||||
.origin_uri = &test_uri,
|
||||
});
|
||||
|
||||
// incomplete prefix path
|
||||
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/abou"), .{
|
||||
.origin_uri = &test_uri,
|
||||
});
|
||||
|
||||
// path doesn't match
|
||||
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/aboutus"), .{
|
||||
.origin_uri = &test_uri,
|
||||
});
|
||||
|
||||
// path doesn't match cookie directory
|
||||
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/docs"), .{
|
||||
.origin_uri = &test_uri,
|
||||
});
|
||||
|
||||
// exact directory match
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
});
|
||||
|
||||
// sub directory match
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/more"), .{
|
||||
.origin_uri = &test_uri,
|
||||
});
|
||||
|
||||
// secure
|
||||
try expectCookies("global1=1; global2=2; secure=5", &jar, try std.Uri.parse("https://lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
});
|
||||
|
||||
// navigational cross domain, secure
|
||||
try expectCookies("global1=1; global2=2; secure=5; sitenone=6; sitelax=7", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
});
|
||||
|
||||
// navigational cross domain, insecure
|
||||
try expectCookies("global1=1; global2=2; sitelax=7", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
});
|
||||
|
||||
// non-navigational cross domain, insecure
|
||||
try expectCookies("", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
.navigation = false,
|
||||
});
|
||||
|
||||
// non-navigational cross domain, secure
|
||||
try expectCookies("sitenone=6", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
.navigation = false,
|
||||
});
|
||||
|
||||
// non-navigational same origin
|
||||
try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://lightpanda.io/")),
|
||||
.navigation = false,
|
||||
});
|
||||
|
||||
// exact domain match + suffix
|
||||
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://test.lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
});
|
||||
|
||||
// domain suffix match + suffix
|
||||
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://1.test.lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
});
|
||||
|
||||
// non-matching domain
|
||||
try expectCookies("global2=2", &jar, try std.Uri.parse("http://other.lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
});
|
||||
|
||||
const l = jar.cookies.items.len;
|
||||
try expectCookies("global1=1", &jar, test_uri, .{
|
||||
.request_time = now + 100,
|
||||
.origin_uri = &test_uri,
|
||||
});
|
||||
try testing.expectEqual(l - 1, jar.cookies.items.len);
|
||||
|
||||
// If you add more cases after this point, note that the above test removes
|
||||
// the 'global2' cookie
|
||||
}
|
||||
|
||||
test "CookieList: write" {
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer arr.deinit(testing.allocator);
|
||||
|
||||
var cookie_list = CookieList{};
|
||||
defer cookie_list.deinit(testing.allocator);
|
||||
|
||||
const c1 = try Cookie.parse(testing.allocator, &test_uri, "cookie_name=cookie_value");
|
||||
defer c1.deinit();
|
||||
{
|
||||
try cookie_list._cookies.append(testing.allocator, &c1);
|
||||
try cookie_list.write(arr.writer(testing.allocator));
|
||||
try testing.expectEqual("cookie_name=cookie_value", arr.items);
|
||||
}
|
||||
|
||||
const c2 = try Cookie.parse(testing.allocator, &test_uri, "x84");
|
||||
defer c2.deinit();
|
||||
{
|
||||
arr.clearRetainingCapacity();
|
||||
try cookie_list._cookies.append(testing.allocator, &c2);
|
||||
try cookie_list.write(arr.writer(testing.allocator));
|
||||
try testing.expectEqual("cookie_name=cookie_value; x84", arr.items);
|
||||
}
|
||||
|
||||
const c3 = try Cookie.parse(testing.allocator, &test_uri, "nope=");
|
||||
defer c3.deinit();
|
||||
{
|
||||
arr.clearRetainingCapacity();
|
||||
try cookie_list._cookies.append(testing.allocator, &c3);
|
||||
try cookie_list.write(arr.writer(testing.allocator));
|
||||
try testing.expectEqual("cookie_name=cookie_value; x84; nope=", arr.items);
|
||||
}
|
||||
}
|
||||
|
||||
test "Cookie: parse key=value" {
|
||||
try expectError(error.Empty, null, "");
|
||||
try expectError(error.InvalidByteSequence, null, &.{ 'a', 30, '=', 'b' });
|
||||
try expectError(error.InvalidByteSequence, null, &.{ 'a', 127, '=', 'b' });
|
||||
try expectError(error.InvalidByteSequence, null, &.{ 'a', '=', 'b', 20 });
|
||||
try expectError(error.InvalidByteSequence, null, &.{ 'a', '=', 'b', 128 });
|
||||
|
||||
try expectAttribute(.{ .name = "", .value = "a" }, null, "a");
|
||||
try expectAttribute(.{ .name = "", .value = "a" }, null, "a;");
|
||||
try expectAttribute(.{ .name = "", .value = "a b" }, null, "a b");
|
||||
try expectAttribute(.{ .name = "a b", .value = "b" }, null, "a b=b");
|
||||
try expectAttribute(.{ .name = "a,", .value = "b" }, null, "a,=b");
|
||||
try expectAttribute(.{ .name = ":a>", .value = "b>><" }, null, ":a>=b>><");
|
||||
|
||||
try expectAttribute(.{ .name = "abc", .value = "" }, null, "abc=");
|
||||
try expectAttribute(.{ .name = "abc", .value = "" }, null, "abc=;");
|
||||
|
||||
try expectAttribute(.{ .name = "a", .value = "b" }, null, "a=b");
|
||||
try expectAttribute(.{ .name = "a", .value = "b" }, null, "a=b;");
|
||||
|
||||
try expectAttribute(.{ .name = "abc", .value = "fe f" }, null, "abc= fe f");
|
||||
try expectAttribute(.{ .name = "abc", .value = "fe f" }, null, "abc= fe f ");
|
||||
try expectAttribute(.{ .name = "abc", .value = "fe f" }, null, "abc= fe f;");
|
||||
try expectAttribute(.{ .name = "abc", .value = "fe f" }, null, "abc= fe f ;");
|
||||
try expectAttribute(.{ .name = "abc", .value = "\" fe f\"" }, null, "abc=\" fe f\"");
|
||||
try expectAttribute(.{ .name = "abc", .value = "\" fe f \"" }, null, "abc=\" fe f \"");
|
||||
try expectAttribute(.{ .name = "ab4344c", .value = "1ads23" }, null, " ab4344c=1ads23 ");
|
||||
|
||||
try expectAttribute(.{ .name = "ab4344c", .value = "1ads23" }, null, " ab4344c = 1ads23 ;");
|
||||
}
|
||||
|
||||
test "Cookie: parse path" {
|
||||
try expectAttribute(.{ .path = "/" }, "http://a/", "b");
|
||||
try expectAttribute(.{ .path = "/" }, "http://a/", "b;path");
|
||||
try expectAttribute(.{ .path = "/" }, "http://a/", "b;Path=");
|
||||
try expectAttribute(.{ .path = "/" }, "http://a/", "b;Path=;");
|
||||
try expectAttribute(.{ .path = "/" }, "http://a/", "b; Path=other");
|
||||
try expectAttribute(.{ .path = "/" }, "http://a/23", "b; path=other ");
|
||||
|
||||
try expectAttribute(.{ .path = "/" }, "http://a/abc", "b");
|
||||
try expectAttribute(.{ .path = "/abc" }, "http://a/abc/", "b");
|
||||
try expectAttribute(.{ .path = "/abc" }, "http://a/abc/123", "b");
|
||||
try expectAttribute(.{ .path = "/abc/123" }, "http://a/abc/123/", "b");
|
||||
|
||||
try expectAttribute(.{ .path = "/a" }, "http://a/", "b;Path=/a");
|
||||
try expectAttribute(.{ .path = "/aa" }, "http://a/", "b;path=/aa;");
|
||||
try expectAttribute(.{ .path = "/aabc/" }, "http://a/", "b; path= /aabc/ ;");
|
||||
|
||||
try expectAttribute(.{ .path = "/bbb/" }, "http://a/", "b; path=/a/; path=/bbb/");
|
||||
try expectAttribute(.{ .path = "/cc" }, "http://a/", "b; path=/a/; path=/bbb/; path = /cc");
|
||||
}
|
||||
|
||||
test "Cookie: parse secure" {
|
||||
try expectAttribute(.{ .secure = false }, null, "b");
|
||||
try expectAttribute(.{ .secure = false }, null, "b;secured");
|
||||
try expectAttribute(.{ .secure = false }, null, "b;security");
|
||||
try expectAttribute(.{ .secure = false }, null, "b;SecureX");
|
||||
try expectAttribute(.{ .secure = true }, null, "b; Secure");
|
||||
try expectAttribute(.{ .secure = true }, null, "b; Secure ");
|
||||
try expectAttribute(.{ .secure = true }, null, "b; Secure=on ");
|
||||
try expectAttribute(.{ .secure = true }, null, "b; Secure=Off ");
|
||||
try expectAttribute(.{ .secure = true }, null, "b; secure=Off ");
|
||||
try expectAttribute(.{ .secure = true }, null, "b; seCUre=Off ");
|
||||
}
|
||||
|
||||
test "Cookie: parse HttpOnly" {
|
||||
try expectAttribute(.{ .http_only = false }, null, "b");
|
||||
try expectAttribute(.{ .http_only = false }, null, "b;HttpOnly0");
|
||||
try expectAttribute(.{ .http_only = false }, null, "b;H ttpOnly");
|
||||
try expectAttribute(.{ .http_only = true }, null, "b; HttpOnly");
|
||||
try expectAttribute(.{ .http_only = true }, null, "b; Httponly ");
|
||||
try expectAttribute(.{ .http_only = true }, null, "b; Httponly=on ");
|
||||
try expectAttribute(.{ .http_only = true }, null, "b; httpOnly=Off ");
|
||||
try expectAttribute(.{ .http_only = true }, null, "b; httpOnly=Off ");
|
||||
try expectAttribute(.{ .http_only = true }, null, "b; HttpOnly=Off ");
|
||||
}
|
||||
|
||||
test "Cookie: parse SameSite" {
|
||||
try expectAttribute(.{ .same_site = .lax }, null, "b;samesite");
|
||||
try expectAttribute(.{ .same_site = .lax }, null, "b;samesite=lax");
|
||||
try expectAttribute(.{ .same_site = .lax }, null, "b; SameSite=Lax ");
|
||||
try expectAttribute(.{ .same_site = .lax }, null, "b; SameSite=Other ");
|
||||
try expectAttribute(.{ .same_site = .lax }, null, "b; SameSite=Nope ");
|
||||
|
||||
// SameSite=none is only valid when Secure is set. The whole cookie is
|
||||
// rejected otherwise
|
||||
try expectError(error.InsecureSameSite, null, "b;samesite=none");
|
||||
try expectError(error.InsecureSameSite, null, "b;SameSite=None");
|
||||
try expectAttribute(.{ .same_site = .none }, null, "b; samesite=none; secure ");
|
||||
try expectAttribute(.{ .same_site = .none }, null, "b; SameSite=None ; SECURE");
|
||||
try expectAttribute(.{ .same_site = .none }, null, "b;Secure; SameSite=None");
|
||||
try expectAttribute(.{ .same_site = .none }, null, "b; SameSite=None; Secure");
|
||||
|
||||
try expectAttribute(.{ .same_site = .strict }, null, "b; samesite=Strict ");
|
||||
try expectAttribute(.{ .same_site = .strict }, null, "b; SameSite= STRICT ");
|
||||
try expectAttribute(.{ .same_site = .strict }, null, "b; SameSITE=strict;");
|
||||
try expectAttribute(.{ .same_site = .strict }, null, "b; SameSite=Strict");
|
||||
|
||||
try expectAttribute(.{ .same_site = .strict }, null, "b; SameSite=None; SameSite=lax; SameSite=Strict");
|
||||
}
|
||||
|
||||
test "Cookie: parse max-age" {
|
||||
try expectAttribute(.{ .expires = null }, null, "b;max-age");
|
||||
try expectAttribute(.{ .expires = null }, null, "b;max-age=abc");
|
||||
try expectAttribute(.{ .expires = null }, null, "b;max-age=13.22");
|
||||
try expectAttribute(.{ .expires = null }, null, "b;max-age=13abc");
|
||||
|
||||
try expectAttribute(.{ .expires = std.time.timestamp() + 13 }, null, "b;max-age=13");
|
||||
try expectAttribute(.{ .expires = std.time.timestamp() + -22 }, null, "b;max-age=-22");
|
||||
try expectAttribute(.{ .expires = std.time.timestamp() + 4294967296 }, null, "b;max-age=4294967296");
|
||||
try expectAttribute(.{ .expires = std.time.timestamp() + -4294967296 }, null, "b;Max-Age= -4294967296");
|
||||
try expectAttribute(.{ .expires = std.time.timestamp() + 0 }, null, "b; Max-Age=0");
|
||||
try expectAttribute(.{ .expires = std.time.timestamp() + 500 }, null, "b; Max-Age = 500 ; Max-Age=invalid");
|
||||
try expectAttribute(.{ .expires = std.time.timestamp() + 1000 }, null, "b;max-age=600;max-age=0;max-age = 1000");
|
||||
}
|
||||
|
||||
test "Cookie: parse expires" {
|
||||
try expectAttribute(.{ .expires = null }, null, "b;expires=");
|
||||
try expectAttribute(.{ .expires = null }, null, "b;expires=abc");
|
||||
try expectAttribute(.{ .expires = null }, null, "b;expires=13.22");
|
||||
try expectAttribute(.{ .expires = null }, null, "b;expires=33");
|
||||
|
||||
try expectAttribute(.{ .expires = 1918798080 - std.time.timestamp() }, null, "b;expires=Wed, 21 Oct 2030 07:28:00 GMT");
|
||||
// max-age has priority over expires
|
||||
try expectAttribute(.{ .expires = std.time.timestamp() + 10 }, null, "b;Max-Age=10; expires=Wed, 21 Oct 2030 07:28:00 GMT");
|
||||
}
|
||||
|
||||
test "Cookie: parse all" {
|
||||
try expectCookie(.{
|
||||
.name = "user-id",
|
||||
.value = "9000",
|
||||
.path = "/cms",
|
||||
.domain = "lightpanda.io",
|
||||
}, "https://lightpanda.io/cms/users", "user-id=9000");
|
||||
|
||||
try expectCookie(.{
|
||||
.name = "user-id",
|
||||
.value = "9000",
|
||||
.path = "/",
|
||||
.http_only = true,
|
||||
.secure = true,
|
||||
.domain = ".lightpanda.io",
|
||||
.expires = std.time.timestamp() + 30,
|
||||
}, "https://lightpanda.io/cms/users", "user-id=9000; HttpOnly; Max-Age=30; Secure; path=/; Domain=lightpanda.io");
|
||||
}
|
||||
|
||||
test "Cookie: parse domain" {
|
||||
try expectAttribute(.{ .domain = "lightpanda.io" }, "http://lightpanda.io/", "b");
|
||||
try expectAttribute(.{ .domain = "dev.lightpanda.io" }, "http://dev.lightpanda.io/", "b");
|
||||
try expectAttribute(.{ .domain = ".lightpanda.io" }, "http://lightpanda.io/", "b;domain=lightpanda.io");
|
||||
try expectAttribute(.{ .domain = ".lightpanda.io" }, "http://lightpanda.io/", "b;domain=.lightpanda.io");
|
||||
try expectAttribute(.{ .domain = ".dev.lightpanda.io" }, "http://dev.lightpanda.io/", "b;domain=dev.lightpanda.io");
|
||||
try expectAttribute(.{ .domain = ".lightpanda.io" }, "http://dev.lightpanda.io/", "b;domain=lightpanda.io");
|
||||
try expectAttribute(.{ .domain = ".lightpanda.io" }, "http://dev.lightpanda.io/", "b;domain=.lightpanda.io");
|
||||
|
||||
try expectError(error.InvalidDomain, "http://lightpanda.io/", "b;domain=io");
|
||||
try expectError(error.InvalidDomain, "http://lightpanda.io/", "b;domain=.io");
|
||||
try expectError(error.InvalidDomain, "http://lightpanda.io/", "b;domain=other.lightpanda.io");
|
||||
try expectError(error.InvalidDomain, "http://lightpanda.io/", "b;domain=other.lightpanda.com");
|
||||
try expectError(error.InvalidDomain, "http://lightpanda.io/", "b;domain=other.example.com");
|
||||
}
|
||||
|
||||
const ExpectedCookie = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
path: []const u8,
|
||||
domain: []const u8,
|
||||
expires: ?i64 = null,
|
||||
secure: bool = false,
|
||||
http_only: bool = false,
|
||||
same_site: Cookie.SameSite = .lax,
|
||||
};
|
||||
|
||||
fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u8) !void {
|
||||
const uri = try Uri.parse(url);
|
||||
var cookie = try Cookie.parse(testing.allocator, &uri, set_cookie);
|
||||
defer cookie.deinit();
|
||||
|
||||
try testing.expectEqual(expected.name, cookie.name);
|
||||
try testing.expectEqual(expected.value, cookie.value);
|
||||
try testing.expectEqual(expected.secure, cookie.secure);
|
||||
try testing.expectEqual(expected.http_only, cookie.http_only);
|
||||
try testing.expectEqual(expected.same_site, cookie.same_site);
|
||||
try testing.expectEqual(expected.path, cookie.path);
|
||||
try testing.expectEqual(expected.domain, cookie.domain);
|
||||
|
||||
try testing.expectDelta(expected.expires, cookie.expires, 2);
|
||||
}
|
||||
|
||||
fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) !void {
|
||||
const uri = if (url) |u| try Uri.parse(u) else test_uri;
|
||||
var cookie = try Cookie.parse(testing.allocator, &uri, set_cookie);
|
||||
defer cookie.deinit();
|
||||
|
||||
inline for (@typeInfo(@TypeOf(expected)).@"struct".fields) |f| {
|
||||
if (comptime std.mem.eql(u8, f.name, "expires")) {
|
||||
try testing.expectDelta(expected.expires, cookie.expires, 1);
|
||||
} else {
|
||||
try testing.expectEqual(@field(expected, f.name), @field(cookie, f.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn expectError(expected: anyerror, url: ?[]const u8, set_cookie: []const u8) !void {
|
||||
const uri = if (url) |u| try Uri.parse(u) else test_uri;
|
||||
try testing.expectError(expected, Cookie.parse(testing.allocator, &uri, set_cookie));
|
||||
}
|
||||
|
||||
const test_uri = Uri.parse("http://lightpanda.io/") catch unreachable;
|
||||
@@ -18,18 +18,17 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const DOMError = @import("netsurf").DOMError;
|
||||
const DOMError = @import("../netsurf.zig").DOMError;
|
||||
|
||||
const log = std.log.scoped(.storage);
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
pub const cookie = @import("cookie.zig");
|
||||
pub const Cookie = cookie.Cookie;
|
||||
pub const CookieJar = cookie.Jar;
|
||||
|
||||
pub const Interfaces = .{
|
||||
Bottle,
|
||||
});
|
||||
};
|
||||
|
||||
// See https://storage.spec.whatwg.org/#model for storage hierarchy.
|
||||
// A Shed contains map of Shelves. The key is the document's origin.
|
||||
@@ -101,7 +100,6 @@ pub const Bucket = struct {
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface
|
||||
pub const Bottle = struct {
|
||||
pub const mem_guarantied = true;
|
||||
const Map = std.StringHashMapUnmanaged([]const u8);
|
||||
|
||||
// allocator is stored. we don't use the JS env allocator b/c the storage
|
||||
@@ -151,20 +149,22 @@ pub const Bottle = struct {
|
||||
}
|
||||
|
||||
pub fn _setItem(self: *Bottle, k: []const u8, v: []const u8) !void {
|
||||
const old = self.map.get(k);
|
||||
if (old != null and std.mem.eql(u8, v, old.?)) return;
|
||||
|
||||
// owns k and v by copying them.
|
||||
const kk = try self.alloc.dupe(u8, k);
|
||||
errdefer self.alloc.free(kk);
|
||||
const vv = try self.alloc.dupe(u8, v);
|
||||
errdefer self.alloc.free(vv);
|
||||
|
||||
self.map.put(self.alloc, kk, vv) catch |e| {
|
||||
const gop = self.map.getOrPut(self.alloc, k) catch |e| {
|
||||
log.debug("set item: {any}", .{e});
|
||||
return DOMError.QuotaExceeded;
|
||||
};
|
||||
|
||||
if (gop.found_existing == false) {
|
||||
gop.key_ptr.* = try self.alloc.dupe(u8, k);
|
||||
gop.value_ptr.* = try self.alloc.dupe(u8, v);
|
||||
return;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, v, gop.value_ptr.*) == false) {
|
||||
self.alloc.free(gop.value_ptr.*);
|
||||
gop.value_ptr.* = try self.alloc.dupe(u8, v);
|
||||
}
|
||||
|
||||
// > Broadcast this with key, oldValue, and value.
|
||||
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
|
||||
//
|
||||
@@ -177,8 +177,10 @@ pub const Bottle = struct {
|
||||
}
|
||||
|
||||
pub fn _removeItem(self: *Bottle, k: []const u8) !void {
|
||||
const old = self.map.fetchRemove(k);
|
||||
if (old == null) return;
|
||||
if (self.map.fetchRemove(k)) |kv| {
|
||||
self.alloc.free(kv.key);
|
||||
self.alloc.free(kv.value);
|
||||
}
|
||||
|
||||
// > Broadcast this with key, oldValue, and null.
|
||||
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
|
||||
@@ -210,41 +212,44 @@ pub const Bottle = struct {
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var storage = [_]Case{
|
||||
.{ .src = "localStorage.length", .ex = "0" },
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Storage.LocalStorage" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
.{ .src = "localStorage.setItem('foo', 'bar')", .ex = "undefined" },
|
||||
.{ .src = "localStorage.length", .ex = "1" },
|
||||
.{ .src = "localStorage.getItem('foo')", .ex = "bar" },
|
||||
.{ .src = "localStorage.removeItem('foo')", .ex = "undefined" },
|
||||
.{ .src = "localStorage.length", .ex = "0" },
|
||||
try runner.testCases(&.{
|
||||
.{ "localStorage.length", "0" },
|
||||
|
||||
// .{ .src = "localStorage['foo'] = 'bar'", .ex = "undefined" },
|
||||
// .{ .src = "localStorage['foo']", .ex = "bar" },
|
||||
// .{ .src = "localStorage.length", .ex = "1" },
|
||||
.{ "localStorage.setItem('foo', 'bar')", "undefined" },
|
||||
.{ "localStorage.length", "1" },
|
||||
.{ "localStorage.getItem('foo')", "bar" },
|
||||
.{ "localStorage.removeItem('foo')", "undefined" },
|
||||
.{ "localStorage.length", "0" },
|
||||
|
||||
.{ .src = "localStorage.clear()", .ex = "undefined" },
|
||||
.{ .src = "localStorage.length", .ex = "0" },
|
||||
};
|
||||
try checkCases(js_env, &storage);
|
||||
// .{ "localStorage['foo'] = 'bar'", "undefined" },
|
||||
// .{ "localStorage['foo']", "bar" },
|
||||
// .{ "localStorage.length", "1" },
|
||||
|
||||
.{ "localStorage.clear()", "undefined" },
|
||||
.{ "localStorage.length", "0" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
test "storage bottle" {
|
||||
var bottle = Bottle.init(std.testing.allocator);
|
||||
defer bottle.deinit();
|
||||
|
||||
try std.testing.expect(0 == bottle.get_length());
|
||||
try std.testing.expect(null == bottle._getItem("foo"));
|
||||
try std.testing.expectEqual(0, bottle.get_length());
|
||||
try std.testing.expectEqual(null, bottle._getItem("foo"));
|
||||
|
||||
try bottle._setItem("foo", "bar");
|
||||
try std.testing.expect(std.mem.eql(u8, "bar", bottle._getItem("foo").?));
|
||||
try std.testing.expectEqualStrings("bar", bottle._getItem("foo").?);
|
||||
|
||||
try bottle._setItem("foo", "other");
|
||||
try std.testing.expectEqualStrings("other", bottle._getItem("foo").?);
|
||||
|
||||
try bottle._removeItem("foo");
|
||||
|
||||
try std.testing.expect(0 == bottle.get_length());
|
||||
try std.testing.expect(null == bottle._getItem("foo"));
|
||||
try std.testing.expectEqual(0, bottle.get_length());
|
||||
try std.testing.expectEqual(null, bottle._getItem("foo"));
|
||||
}
|
||||
433
src/browser/url/query.zig
Normal file
433
src/browser/url/query.zig
Normal file
@@ -0,0 +1,433 @@
|
||||
// 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 Reader = @import("../../str/parser.zig").Reader;
|
||||
const asUint = @import("../../str/parser.zig").asUint;
|
||||
|
||||
// Values is a map with string key of string values.
|
||||
pub const Values = struct {
|
||||
arena: std.heap.ArenaAllocator,
|
||||
map: std.StringArrayHashMapUnmanaged(List),
|
||||
|
||||
const List = std.ArrayListUnmanaged([]const u8);
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Values {
|
||||
return .{
|
||||
.map = .{},
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Values) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
// add the key value couple to the values.
|
||||
// the key and the value are duplicated.
|
||||
pub fn append(self: *Values, k: []const u8, v: []const u8) !void {
|
||||
const allocator = self.arena.allocator();
|
||||
const owned_value = try allocator.dupe(u8, v);
|
||||
|
||||
var gop = try self.map.getOrPut(allocator, k);
|
||||
if (gop.found_existing) {
|
||||
return gop.value_ptr.append(allocator, owned_value);
|
||||
}
|
||||
|
||||
gop.key_ptr.* = try allocator.dupe(u8, k);
|
||||
|
||||
var list = List{};
|
||||
try list.append(allocator, owned_value);
|
||||
gop.value_ptr.* = list;
|
||||
}
|
||||
|
||||
// append by taking the ownership of the key and the value
|
||||
fn appendOwned(self: *Values, k: []const u8, v: []const u8) !void {
|
||||
const allocator = self.arena.allocator();
|
||||
var gop = try self.map.getOrPut(allocator, k);
|
||||
if (gop.found_existing) {
|
||||
return gop.value_ptr.append(allocator, v);
|
||||
}
|
||||
|
||||
var list = List{};
|
||||
try list.append(allocator, v);
|
||||
gop.value_ptr.* = list;
|
||||
}
|
||||
|
||||
pub fn get(self: *const Values, k: []const u8) []const []const u8 {
|
||||
if (self.map.get(k)) |list| {
|
||||
return list.items;
|
||||
}
|
||||
|
||||
return &[_][]const u8{};
|
||||
}
|
||||
|
||||
pub fn first(self: *const Values, k: []const u8) []const u8 {
|
||||
if (self.map.getPtr(k)) |list| {
|
||||
if (list.items.len == 0) return "";
|
||||
return list.items[0];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn delete(self: *Values, k: []const u8) void {
|
||||
_ = self.map.fetchSwapRemove(k);
|
||||
}
|
||||
|
||||
pub fn deleteValue(self: *Values, k: []const u8, v: []const u8) void {
|
||||
const list = self.map.getPtr(k) orelse return;
|
||||
|
||||
for (list.items, 0..) |vv, i| {
|
||||
if (std.mem.eql(u8, v, vv)) {
|
||||
_ = list.swapRemove(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count(self: *const Values) usize {
|
||||
return self.map.count();
|
||||
}
|
||||
|
||||
pub fn encode(self: *const Values, writer: anytype) !void {
|
||||
var it = self.map.iterator();
|
||||
|
||||
const first_entry = it.next() orelse return;
|
||||
try encodeKeyValues(first_entry, writer);
|
||||
|
||||
while (it.next()) |entry| {
|
||||
try writer.writeByte('&');
|
||||
try encodeKeyValues(entry, writer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn encodeKeyValues(entry: anytype, writer: anytype) !void {
|
||||
const key = entry.key_ptr.*;
|
||||
|
||||
try escape(key, writer);
|
||||
const values = entry.value_ptr.items;
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (values[0].len > 0) {
|
||||
try writer.writeByte('=');
|
||||
try escape(values[0], writer);
|
||||
}
|
||||
|
||||
for (values[1..]) |value| {
|
||||
try writer.writeByte('&');
|
||||
try escape(key, writer);
|
||||
if (value.len > 0) {
|
||||
try writer.writeByte('=');
|
||||
try escape(value, writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escape(raw: []const u8, writer: anytype) !void {
|
||||
var start: usize = 0;
|
||||
for (raw, 0..) |char, index| {
|
||||
if ('a' <= char and char <= 'z' or 'A' <= char and char <= 'Z' or '0' <= char and char <= '9') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try writer.print("{s}%{X:0>2}", .{ raw[start..index], char });
|
||||
start = index + 1;
|
||||
}
|
||||
try writer.writeAll(raw[start..]);
|
||||
}
|
||||
|
||||
// Parse the given query.
|
||||
pub fn parseQuery(alloc: std.mem.Allocator, s: []const u8) !Values {
|
||||
var values = Values.init(alloc);
|
||||
errdefer values.deinit();
|
||||
|
||||
const arena = values.arena.allocator();
|
||||
|
||||
const ln = s.len;
|
||||
if (ln == 0) return values;
|
||||
|
||||
var r = Reader{ .data = s };
|
||||
while (true) {
|
||||
const param = r.until('&');
|
||||
if (param.len == 0) break;
|
||||
|
||||
var rr = Reader{ .data = param };
|
||||
const k = rr.until('=');
|
||||
if (k.len == 0) continue;
|
||||
|
||||
_ = rr.skip();
|
||||
const v = rr.tail();
|
||||
|
||||
// decode k and v
|
||||
const kk = try unescape(arena, k);
|
||||
const vv = try unescape(arena, v);
|
||||
|
||||
try values.appendOwned(kk, vv);
|
||||
|
||||
if (!r.skip()) break;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
// The return'd string may or may not be allocated. Callers should use arenas
|
||||
fn unescape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
|
||||
const HEX_CHAR = comptime blk: {
|
||||
var all = std.mem.zeroes([256]bool);
|
||||
for ('a'..('f' + 1)) |b| all[b] = true;
|
||||
for ('A'..('F' + 1)) |b| all[b] = true;
|
||||
for ('0'..('9' + 1)) |b| all[b] = true;
|
||||
break :blk all;
|
||||
};
|
||||
|
||||
const HEX_DECODE = comptime blk: {
|
||||
var all = std.mem.zeroes([256]u8);
|
||||
for ('a'..('z' + 1)) |b| all[b] = b - 'a' + 10;
|
||||
for ('A'..('Z' + 1)) |b| all[b] = b - 'A' + 10;
|
||||
for ('0'..('9' + 1)) |b| all[b] = b - '0';
|
||||
break :blk all;
|
||||
};
|
||||
|
||||
var has_plus = false;
|
||||
var unescaped_len = input.len;
|
||||
|
||||
{
|
||||
// Figure out if we have any spaces and what the final unescaped length
|
||||
// will be (which will let us know if we have anything to unescape in
|
||||
// the first place)
|
||||
var i: usize = 0;
|
||||
while (i < input.len) {
|
||||
const c = input[i];
|
||||
if (c == '%') {
|
||||
if (i + 2 >= input.len or !HEX_CHAR[input[i + 1]] or !HEX_CHAR[input[i + 2]]) {
|
||||
return error.EscapeError;
|
||||
}
|
||||
i += 3;
|
||||
unescaped_len -= 2;
|
||||
} else if (c == '+') {
|
||||
has_plus = true;
|
||||
i += 1;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no encoding, and no plus. nothing to unescape
|
||||
if (unescaped_len == input.len and has_plus == false) {
|
||||
return input;
|
||||
}
|
||||
|
||||
var unescaped = try allocator.alloc(u8, unescaped_len);
|
||||
errdefer allocator.free(unescaped);
|
||||
|
||||
var input_pos: usize = 0;
|
||||
for (0..unescaped_len) |unescaped_pos| {
|
||||
switch (input[input_pos]) {
|
||||
'+' => {
|
||||
unescaped[unescaped_pos] = ' ';
|
||||
input_pos += 1;
|
||||
},
|
||||
'%' => {
|
||||
const encoded = input[input_pos + 1 .. input_pos + 3];
|
||||
const encoded_as_uint = @as(u16, @bitCast(encoded[0..2].*));
|
||||
unescaped[unescaped_pos] = switch (encoded_as_uint) {
|
||||
asUint("20") => ' ',
|
||||
asUint("21") => '!',
|
||||
asUint("22") => '"',
|
||||
asUint("23") => '#',
|
||||
asUint("24") => '$',
|
||||
asUint("25") => '%',
|
||||
asUint("26") => '&',
|
||||
asUint("27") => '\'',
|
||||
asUint("28") => '(',
|
||||
asUint("29") => ')',
|
||||
asUint("2A") => '*',
|
||||
asUint("2B") => '+',
|
||||
asUint("2C") => ',',
|
||||
asUint("2F") => '/',
|
||||
asUint("3A") => ':',
|
||||
asUint("3B") => ';',
|
||||
asUint("3D") => '=',
|
||||
asUint("3F") => '?',
|
||||
asUint("40") => '@',
|
||||
asUint("5B") => '[',
|
||||
asUint("5D") => ']',
|
||||
else => HEX_DECODE[encoded[0]] << 4 | HEX_DECODE[encoded[1]],
|
||||
};
|
||||
input_pos += 3;
|
||||
},
|
||||
else => |c| {
|
||||
unescaped[unescaped_pos] = c;
|
||||
input_pos += 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
return unescaped;
|
||||
}
|
||||
|
||||
const testing = std.testing;
|
||||
test "url.Query: unescape" {
|
||||
const allocator = testing.allocator;
|
||||
const cases = [_]struct { expected: []const u8, input: []const u8, free: bool }{
|
||||
.{ .expected = "", .input = "", .free = false },
|
||||
.{ .expected = "over", .input = "over", .free = false },
|
||||
.{ .expected = "Hello World", .input = "Hello World", .free = false },
|
||||
.{ .expected = "~", .input = "%7E", .free = true },
|
||||
.{ .expected = "~", .input = "%7e", .free = true },
|
||||
.{ .expected = "Hello~World", .input = "Hello%7eWorld", .free = true },
|
||||
.{ .expected = "Hello World", .input = "Hello++World", .free = true },
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
const value = try unescape(allocator, case.input);
|
||||
defer if (case.free) {
|
||||
allocator.free(value);
|
||||
};
|
||||
try testing.expectEqualStrings(case.expected, value);
|
||||
}
|
||||
|
||||
try testing.expectError(error.EscapeError, unescape(undefined, "%"));
|
||||
try testing.expectError(error.EscapeError, unescape(undefined, "%a"));
|
||||
try testing.expectError(error.EscapeError, unescape(undefined, "%1"));
|
||||
try testing.expectError(error.EscapeError, unescape(undefined, "123%45%6"));
|
||||
try testing.expectError(error.EscapeError, unescape(undefined, "%zzzzz"));
|
||||
try testing.expectError(error.EscapeError, unescape(undefined, "%0\xff"));
|
||||
}
|
||||
|
||||
test "url.Query: parseQuery" {
|
||||
try testParseQuery(.{}, "");
|
||||
|
||||
try testParseQuery(.{}, "&");
|
||||
|
||||
try testParseQuery(.{ .a = [_][]const u8{"b"} }, "a=b");
|
||||
|
||||
try testParseQuery(.{ .hello = [_][]const u8{"world"} }, "hello=world");
|
||||
|
||||
try testParseQuery(.{ .hello = [_][]const u8{ "world", "all" } }, "hello=world&hello=all");
|
||||
|
||||
try testParseQuery(.{
|
||||
.a = [_][]const u8{"b"},
|
||||
.b = [_][]const u8{"c"},
|
||||
}, "a=b&b=c");
|
||||
|
||||
try testParseQuery(.{ .a = [_][]const u8{""} }, "a");
|
||||
try testParseQuery(.{ .a = [_][]const u8{ "", "", "" } }, "a&a&a");
|
||||
|
||||
try testParseQuery(.{ .abc = [_][]const u8{""} }, "abc");
|
||||
try testParseQuery(.{
|
||||
.abc = [_][]const u8{""},
|
||||
.dde = [_][]const u8{ "", "" },
|
||||
}, "abc&dde&dde");
|
||||
|
||||
try testParseQuery(.{
|
||||
.@"power is >" = [_][]const u8{"9,000?"},
|
||||
}, "power%20is%20%3E=9%2C000%3F");
|
||||
}
|
||||
|
||||
test "url.Query.Values: get/first/count" {
|
||||
var values = Values.init(testing.allocator);
|
||||
defer values.deinit();
|
||||
|
||||
{
|
||||
// empty
|
||||
try testing.expectEqual(0, values.count());
|
||||
try testing.expectEqual(0, values.get("").len);
|
||||
try testing.expectEqualStrings("", values.first(""));
|
||||
try testing.expectEqual(0, values.get("key").len);
|
||||
try testing.expectEqualStrings("", values.first("key"));
|
||||
}
|
||||
|
||||
{
|
||||
// add 1 value => key
|
||||
try values.appendOwned("key", "value");
|
||||
try testing.expectEqual(1, values.count());
|
||||
try testing.expectEqual(1, values.get("key").len);
|
||||
try testing.expectEqualSlices(
|
||||
[]const u8,
|
||||
&.{"value"},
|
||||
values.get("key"),
|
||||
);
|
||||
try testing.expectEqualStrings("value", values.first("key"));
|
||||
}
|
||||
|
||||
{
|
||||
// add another value for the same key
|
||||
try values.appendOwned("key", "another");
|
||||
try testing.expectEqual(1, values.count());
|
||||
try testing.expectEqual(2, values.get("key").len);
|
||||
try testing.expectEqualSlices(
|
||||
[]const u8,
|
||||
&.{ "value", "another" },
|
||||
values.get("key"),
|
||||
);
|
||||
try testing.expectEqualStrings("value", values.first("key"));
|
||||
}
|
||||
|
||||
{
|
||||
// add a new key (and value)
|
||||
try values.appendOwned("over", "9000!");
|
||||
try testing.expectEqual(2, values.count());
|
||||
try testing.expectEqual(2, values.get("key").len);
|
||||
try testing.expectEqual(1, values.get("over").len);
|
||||
try testing.expectEqualSlices(
|
||||
[]const u8,
|
||||
&.{"9000!"},
|
||||
values.get("over"),
|
||||
);
|
||||
try testing.expectEqualStrings("9000!", values.first("over"));
|
||||
}
|
||||
}
|
||||
|
||||
test "url.Query.Values: encode" {
|
||||
var values = try parseQuery(
|
||||
testing.allocator,
|
||||
"hello=world&i%20will%20not%20fear=%3E%3E&a=b&a=c",
|
||||
);
|
||||
defer values.deinit();
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer buf.deinit(testing.allocator);
|
||||
try values.encode(buf.writer(testing.allocator));
|
||||
try testing.expectEqualStrings(
|
||||
"hello=world&i%20will%20not%20fear=%3E%3E&a=b&a=c",
|
||||
buf.items,
|
||||
);
|
||||
}
|
||||
|
||||
fn testParseQuery(expected: anytype, query: []const u8) !void {
|
||||
var values = try parseQuery(testing.allocator, query);
|
||||
defer values.deinit();
|
||||
|
||||
var count: usize = 0;
|
||||
inline for (@typeInfo(@TypeOf(expected)).@"struct".fields) |f| {
|
||||
const actual = values.get(f.name);
|
||||
const expect = @field(expected, f.name);
|
||||
try testing.expectEqual(expect.len, actual.len);
|
||||
for (expect, actual) |e, a| {
|
||||
try testing.expectEqualStrings(e, a);
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
try testing.expectEqual(count, values.count());
|
||||
}
|
||||
279
src/browser/url/url.zig
Normal file
279
src/browser/url/url.zig
Normal file
@@ -0,0 +1,279 @@
|
||||
// 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 SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const query = @import("query.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
URL,
|
||||
URLSearchParams,
|
||||
};
|
||||
|
||||
// https://url.spec.whatwg.org/#url
|
||||
//
|
||||
// TODO we could avoid many of these getter string allocatoration in two differents
|
||||
// way:
|
||||
//
|
||||
// 1. We can eventually get the slice of scheme *with* the following char in
|
||||
// the underlying string. But I don't know if it's possible and how to do that.
|
||||
// I mean, if the rawuri contains `https://foo.bar`, uri.scheme is a slice
|
||||
// containing only `https`. I want `https:` so, in theory, I don't need to
|
||||
// allocatorate data, I should be able to retrieve the scheme + the following `:`
|
||||
// from rawuri.
|
||||
//
|
||||
// 2. The other way would bu to copy the `std.Uri` code to ahve a dedicated
|
||||
// parser including the characters we want for the web API.
|
||||
pub const URL = struct {
|
||||
uri: std.Uri,
|
||||
search_params: URLSearchParams,
|
||||
|
||||
pub fn constructor(
|
||||
url: []const u8,
|
||||
base: ?[]const u8,
|
||||
state: *SessionState,
|
||||
) !URL {
|
||||
const arena = state.arena;
|
||||
const raw = try std.mem.concat(arena, u8, &[_][]const u8{ url, base orelse "" });
|
||||
|
||||
const uri = std.Uri.parse(raw) catch return error.TypeError;
|
||||
return init(arena, uri);
|
||||
}
|
||||
|
||||
pub fn init(arena: std.mem.Allocator, uri: std.Uri) !URL {
|
||||
return .{
|
||||
.uri = uri,
|
||||
.search_params = try URLSearchParams.init(
|
||||
arena,
|
||||
uriComponentNullStr(uri.query),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_origin(self: *URL, state: *SessionState) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(state.arena);
|
||||
try self.uri.writeToStream(.{
|
||||
.scheme = true,
|
||||
.authentication = false,
|
||||
.authority = true,
|
||||
.path = false,
|
||||
.query = false,
|
||||
.fragment = false,
|
||||
}, buf.writer());
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
// get_href returns the URL by writing all its components.
|
||||
// The query is replaced by a dump of search params.
|
||||
//
|
||||
pub fn get_href(self: *URL, state: *SessionState) ![]const u8 {
|
||||
const arena = state.arena;
|
||||
// retrieve the query search from search_params.
|
||||
const cur = self.uri.query;
|
||||
defer self.uri.query = cur;
|
||||
var q = std.ArrayList(u8).init(arena);
|
||||
try self.search_params.values.encode(q.writer());
|
||||
self.uri.query = .{ .percent_encoded = q.items };
|
||||
|
||||
return try self.toString(arena);
|
||||
}
|
||||
|
||||
// format the url with all its components.
|
||||
pub fn toString(self: *URL, arena: std.mem.Allocator) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(arena);
|
||||
|
||||
try self.uri.writeToStream(.{
|
||||
.scheme = true,
|
||||
.authentication = true,
|
||||
.authority = true,
|
||||
.path = uriComponentNullStr(self.uri.path).len > 0,
|
||||
.query = uriComponentNullStr(self.uri.query).len > 0,
|
||||
.fragment = uriComponentNullStr(self.uri.fragment).len > 0,
|
||||
}, buf.writer());
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn get_protocol(self: *URL, state: *SessionState) ![]const u8 {
|
||||
return try std.mem.concat(state.arena, u8, &[_][]const u8{ self.uri.scheme, ":" });
|
||||
}
|
||||
|
||||
pub fn get_username(self: *URL) []const u8 {
|
||||
return uriComponentNullStr(self.uri.user);
|
||||
}
|
||||
|
||||
pub fn get_password(self: *URL) []const u8 {
|
||||
return uriComponentNullStr(self.uri.password);
|
||||
}
|
||||
|
||||
pub fn get_host(self: *URL, state: *SessionState) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(state.arena);
|
||||
|
||||
try self.uri.writeToStream(.{
|
||||
.scheme = false,
|
||||
.authentication = false,
|
||||
.authority = true,
|
||||
.path = false,
|
||||
.query = false,
|
||||
.fragment = false,
|
||||
}, buf.writer());
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn get_hostname(self: *URL) []const u8 {
|
||||
return uriComponentNullStr(self.uri.host);
|
||||
}
|
||||
|
||||
pub fn get_port(self: *URL, state: *SessionState) ![]const u8 {
|
||||
const arena = state.arena;
|
||||
if (self.uri.port == null) return try arena.dupe(u8, "");
|
||||
|
||||
var buf = std.ArrayList(u8).init(arena);
|
||||
try std.fmt.formatInt(self.uri.port.?, 10, .lower, .{}, buf.writer());
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn get_pathname(self: *URL) []const u8 {
|
||||
if (uriComponentStr(self.uri.path).len == 0) return "/";
|
||||
return uriComponentStr(self.uri.path);
|
||||
}
|
||||
|
||||
pub fn get_search(self: *URL, state: *SessionState) ![]const u8 {
|
||||
const arena = state.arena;
|
||||
if (self.search_params.get_size() == 0) return try arena.dupe(u8, "");
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
|
||||
try buf.append(arena, '?');
|
||||
try self.search_params.values.encode(buf.writer(arena));
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn get_hash(self: *URL, state: *SessionState) ![]const u8 {
|
||||
const arena = state.arena;
|
||||
if (self.uri.fragment == null) return try arena.dupe(u8, "");
|
||||
|
||||
return try std.mem.concat(arena, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) });
|
||||
}
|
||||
|
||||
pub fn get_searchParams(self: *URL) *URLSearchParams {
|
||||
return &self.search_params;
|
||||
}
|
||||
|
||||
pub fn _toJSON(self: *URL, state: *SessionState) ![]const u8 {
|
||||
return try self.get_href(state);
|
||||
}
|
||||
};
|
||||
|
||||
// uriComponentNullStr converts an optional std.Uri.Component to string value.
|
||||
// The string value can be undecoded.
|
||||
fn uriComponentNullStr(c: ?std.Uri.Component) []const u8 {
|
||||
if (c == null) return "";
|
||||
|
||||
return uriComponentStr(c.?);
|
||||
}
|
||||
|
||||
fn uriComponentStr(c: std.Uri.Component) []const u8 {
|
||||
return switch (c) {
|
||||
.raw => |v| v,
|
||||
.percent_encoded => |v| v,
|
||||
};
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#interface-urlsearchparams
|
||||
// TODO array like
|
||||
pub const URLSearchParams = struct {
|
||||
values: query.Values,
|
||||
|
||||
pub fn constructor(qs: ?[]const u8, state: *SessionState) !URLSearchParams {
|
||||
return init(state.arena, qs);
|
||||
}
|
||||
|
||||
pub fn init(arena: std.mem.Allocator, qs: ?[]const u8) !URLSearchParams {
|
||||
return .{
|
||||
.values = try query.parseQuery(arena, qs orelse ""),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_size(self: *URLSearchParams) u32 {
|
||||
return @intCast(self.values.count());
|
||||
}
|
||||
|
||||
pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8) !void {
|
||||
try self.values.append(name, value);
|
||||
}
|
||||
|
||||
pub fn _delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8) !void {
|
||||
if (value) |v| return self.values.deleteValue(name, v);
|
||||
|
||||
self.values.delete(name);
|
||||
}
|
||||
|
||||
pub fn _get(self: *URLSearchParams, name: []const u8) ?[]const u8 {
|
||||
return self.values.first(name);
|
||||
}
|
||||
|
||||
// TODO return generates an error: caught unexpected error 'TypeLookup'
|
||||
// pub fn _getAll(self: *URLSearchParams, name: []const u8) [][]const u8 {
|
||||
// try self.values.get(name);
|
||||
// }
|
||||
|
||||
// TODO
|
||||
pub fn _sort(_: *URLSearchParams) void {}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.URL" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var url = new URL('https://foo.bar/path?query#fragment')", "undefined" },
|
||||
.{ "url.origin", "https://foo.bar" },
|
||||
.{ "url.href", "https://foo.bar/path?query#fragment" },
|
||||
.{ "url.protocol", "https:" },
|
||||
.{ "url.username", "" },
|
||||
.{ "url.password", "" },
|
||||
.{ "url.host", "foo.bar" },
|
||||
.{ "url.hostname", "foo.bar" },
|
||||
.{ "url.port", "" },
|
||||
.{ "url.pathname", "/path" },
|
||||
.{ "url.search", "?query" },
|
||||
.{ "url.hash", "#fragment" },
|
||||
.{ "url.searchParams.get('query')", "" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var url = new URL('https://foo.bar/path?a=~&b=%7E#fragment')", "undefined" },
|
||||
.{ "url.searchParams.get('a')", "~" },
|
||||
.{ "url.searchParams.get('b')", "~" },
|
||||
.{ "url.searchParams.append('c', 'foo')", "undefined" },
|
||||
.{ "url.searchParams.get('c')", "foo" },
|
||||
.{ "url.searchParams.size", "3" },
|
||||
|
||||
// search is dynamic
|
||||
.{ "url.search", "?a=%7E&b=%7E&c=foo" },
|
||||
// href is dynamic
|
||||
.{ "url.href", "https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment" },
|
||||
|
||||
.{ "url.searchParams.delete('c', 'foo')", "undefined" },
|
||||
.{ "url.searchParams.get('c')", "" },
|
||||
.{ "url.searchParams.delete('a')", "undefined" },
|
||||
.{ "url.searchParams.get('a')", "" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,19 +18,19 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Callback = jsruntime.Callback;
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Callback = Env.Callback;
|
||||
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const log = std.log.scoped(.xhr);
|
||||
|
||||
pub const XMLHttpRequestEventTarget = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{},
|
||||
@@ -48,25 +48,25 @@ pub const XMLHttpRequestEventTarget = struct {
|
||||
typ: []const u8,
|
||||
cbk: Callback,
|
||||
) !void {
|
||||
const target = @as(*parser.EventTarget, @ptrCast(self));
|
||||
const eh = try EventHandler.init(alloc, try cbk.withThis(target));
|
||||
try parser.eventTargetAddEventListener(
|
||||
@as(*parser.EventTarget, @ptrCast(self)),
|
||||
alloc,
|
||||
target,
|
||||
typ,
|
||||
EventHandler,
|
||||
.{ .cbk = cbk },
|
||||
&eh.node,
|
||||
false,
|
||||
);
|
||||
}
|
||||
fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
|
||||
fn unregister(self: *XMLHttpRequestEventTarget, typ: []const u8, cbk_id: usize) !void {
|
||||
const et = @as(*parser.EventTarget, @ptrCast(self));
|
||||
// check if event target has already this listener
|
||||
const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id());
|
||||
const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id);
|
||||
if (lst == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove listener
|
||||
try parser.eventTargetRemoveEventListener(et, alloc, typ, lst.?, false);
|
||||
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
|
||||
}
|
||||
|
||||
pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback {
|
||||
@@ -88,39 +88,40 @@ pub const XMLHttpRequestEventTarget = struct {
|
||||
return self.onloadend_cbk;
|
||||
}
|
||||
|
||||
pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
|
||||
if (self.onloadstart_cbk) |cbk| try self.unregister(alloc, "loadstart", cbk);
|
||||
try self.register(alloc, "loadstart", handler);
|
||||
pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
|
||||
if (self.onloadstart_cbk) |cbk| try self.unregister("loadstart", cbk.id);
|
||||
try self.register(state.arena, "loadstart", handler);
|
||||
self.onloadstart_cbk = handler;
|
||||
}
|
||||
pub fn set_onprogress(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
|
||||
if (self.onprogress_cbk) |cbk| try self.unregister(alloc, "progress", cbk);
|
||||
try self.register(alloc, "progress", handler);
|
||||
pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
|
||||
if (self.onprogress_cbk) |cbk| try self.unregister("progress", cbk.id);
|
||||
try self.register(state.arena, "progress", handler);
|
||||
self.onprogress_cbk = handler;
|
||||
}
|
||||
pub fn set_onabort(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
|
||||
if (self.onabort_cbk) |cbk| try self.unregister(alloc, "abort", cbk);
|
||||
try self.register(alloc, "abort", handler);
|
||||
pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
|
||||
if (self.onabort_cbk) |cbk| try self.unregister("abort", cbk.id);
|
||||
try self.register(state.arena, "abort", handler);
|
||||
self.onabort_cbk = handler;
|
||||
}
|
||||
pub fn set_onload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
|
||||
if (self.onload_cbk) |cbk| try self.unregister(alloc, "load", cbk);
|
||||
try self.register(alloc, "load", handler);
|
||||
pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
|
||||
if (self.onload_cbk) |cbk| try self.unregister("load", cbk.id);
|
||||
try self.register(state.arena, "load", handler);
|
||||
self.onload_cbk = handler;
|
||||
}
|
||||
pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
|
||||
if (self.ontimeout_cbk) |cbk| try self.unregister(alloc, "timeout", cbk);
|
||||
try self.register(alloc, "timeout", handler);
|
||||
pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
|
||||
if (self.ontimeout_cbk) |cbk| try self.unregister("timeout", cbk.id);
|
||||
try self.register(state.arena, "timeout", handler);
|
||||
self.ontimeout_cbk = handler;
|
||||
}
|
||||
pub fn set_onloadend(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
|
||||
if (self.onloadend_cbk) |cbk| try self.unregister(alloc, "loadend", cbk);
|
||||
try self.register(alloc, "loadend", handler);
|
||||
pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
|
||||
if (self.onloadend_cbk) |cbk| try self.unregister("loadend", cbk.id);
|
||||
try self.register(state.arena, "loadend", handler);
|
||||
self.onloadend_cbk = handler;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void {
|
||||
parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), alloc) catch |e| {
|
||||
pub fn deinit(self: *XMLHttpRequestEventTarget, state: *SessionState) void {
|
||||
const arena = state.arena;
|
||||
parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), arena) catch |e| {
|
||||
log.err("remove all listeners: {any}", .{e});
|
||||
};
|
||||
}
|
||||
@@ -16,13 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Event = @import("../events/event.zig").Event;
|
||||
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
@@ -30,7 +24,6 @@ const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
pub const ProgressEvent = struct {
|
||||
pub const prototype = *Event;
|
||||
pub const Exception = DOMException;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub const EventInit = struct {
|
||||
lengthComputable: bool = false,
|
||||
@@ -59,32 +52,32 @@ pub const ProgressEvent = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_lengthComputable(self: ProgressEvent) bool {
|
||||
pub fn get_lengthComputable(self: *const ProgressEvent) bool {
|
||||
return self.lengthComputable;
|
||||
}
|
||||
|
||||
pub fn get_loaded(self: ProgressEvent) u64 {
|
||||
pub fn get_loaded(self: *const ProgressEvent) u64 {
|
||||
return self.loaded;
|
||||
}
|
||||
|
||||
pub fn get_total(self: ProgressEvent) u64 {
|
||||
pub fn get_total(self: *const ProgressEvent) u64 {
|
||||
return self.total;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var progress_event = [_]Case{
|
||||
.{ .src = "let pevt = new ProgressEvent('foo');", .ex = "undefined" },
|
||||
.{ .src = "pevt.loaded", .ex = "0" },
|
||||
.{ .src = "pevt instanceof ProgressEvent", .ex = "true" },
|
||||
.{ .src = "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", .ex = "undefined" },
|
||||
.{ .src = "document.addEventListener('foo', ccbk)", .ex = "undefined" },
|
||||
.{ .src = "document.dispatchEvent(pevt)", .ex = "true" },
|
||||
.{ .src = "eevt.type", .ex = "foo" },
|
||||
.{ .src = "eevt instanceof ProgressEvent", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &progress_event);
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.XHR.ProgressEvent" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let pevt = new ProgressEvent('foo');", "undefined" },
|
||||
.{ "pevt.loaded", "0" },
|
||||
.{ "pevt instanceof ProgressEvent", "true" },
|
||||
.{ "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", "undefined" },
|
||||
.{ "document.addEventListener('foo', ccbk)", "undefined" },
|
||||
.{ "document.dispatchEvent(pevt)", "true" },
|
||||
.{ "eevt.type", "foo" },
|
||||
.{ "eevt instanceof ProgressEvent", "true" },
|
||||
}, .{});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
56
src/browser/xmlserializer/xmlserializer.zig
Normal file
56
src/browser/xmlserializer/xmlserializer.zig
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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 SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const dump = @import("../dump.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
XMLSerializer,
|
||||
};
|
||||
|
||||
// https://w3c.github.io/DOM-Parsing/#dom-xmlserializer-constructor
|
||||
pub const XMLSerializer = struct {
|
||||
pub fn constructor() !XMLSerializer {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn _serializeToString(_: *const XMLSerializer, root: *parser.Node, state: *SessionState) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(state.arena);
|
||||
if (try parser.nodeType(root) == .document) {
|
||||
try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), buf.writer());
|
||||
} else {
|
||||
try dump.writeNode(root, buf.writer());
|
||||
}
|
||||
return buf.items;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.XMLSerializer" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const s = new XMLSerializer()", "undefined" },
|
||||
.{ "s.serializeToString(document.getElementById('para'))", "<p id=\"para\"> And</p>" },
|
||||
}, .{});
|
||||
}
|
||||
499
src/cdp/Node.zig
Normal file
499
src/cdp/Node.zig
Normal file
@@ -0,0 +1,499 @@
|
||||
// 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("../browser/netsurf.zig");
|
||||
|
||||
pub const Id = u32;
|
||||
|
||||
const log = std.log.scoped(.cdp_node);
|
||||
|
||||
const Node = @This();
|
||||
|
||||
id: Id,
|
||||
_node: *parser.Node,
|
||||
set_child_nodes_event: bool,
|
||||
|
||||
// Whenever we send a node to the client, we register it here for future lookup.
|
||||
// We maintain a node -> id and id -> node lookup.
|
||||
pub const Registry = struct {
|
||||
node_id: u32,
|
||||
allocator: Allocator,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
node_pool: std.heap.MemoryPool(Node),
|
||||
lookup_by_id: std.AutoHashMapUnmanaged(Id, *Node),
|
||||
lookup_by_node: std.HashMapUnmanaged(*parser.Node, *Node, NodeContext, std.hash_map.default_max_load_percentage),
|
||||
|
||||
pub fn init(allocator: Allocator) Registry {
|
||||
return .{
|
||||
.node_id = 0,
|
||||
.lookup_by_id = .{},
|
||||
.lookup_by_node = .{},
|
||||
.allocator = allocator,
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
.node_pool = std.heap.MemoryPool(Node).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Registry) void {
|
||||
const allocator = self.allocator;
|
||||
self.lookup_by_id.deinit(allocator);
|
||||
self.lookup_by_node.deinit(allocator);
|
||||
self.node_pool.deinit();
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
pub fn reset(self: *Registry) void {
|
||||
self.lookup_by_id.clearRetainingCapacity();
|
||||
self.lookup_by_node.clearRetainingCapacity();
|
||||
_ = self.arena.reset(.{ .retain_with_limit = 1024 });
|
||||
_ = self.node_pool.reset(.{ .retain_with_limit = 1024 });
|
||||
}
|
||||
|
||||
pub fn register(self: *Registry, n: *parser.Node) !*Node {
|
||||
const node_lookup_gop = try self.lookup_by_node.getOrPut(self.allocator, n);
|
||||
if (node_lookup_gop.found_existing) {
|
||||
return node_lookup_gop.value_ptr.*;
|
||||
}
|
||||
|
||||
// on error, we're probably going to abort the entire browser context
|
||||
// but, just in case, let's try to keep things tidy.
|
||||
errdefer _ = self.lookup_by_node.remove(n);
|
||||
|
||||
const node = try self.node_pool.create();
|
||||
errdefer self.node_pool.destroy(node);
|
||||
|
||||
const id = self.node_id;
|
||||
self.node_id = id + 1;
|
||||
|
||||
node.* = .{
|
||||
._node = n,
|
||||
.id = id,
|
||||
.set_child_nodes_event = false,
|
||||
};
|
||||
|
||||
node_lookup_gop.value_ptr.* = node;
|
||||
try self.lookup_by_id.putNoClobber(self.allocator, id, node);
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
const NodeContext = struct {
|
||||
pub fn hash(_: NodeContext, n: *parser.Node) u64 {
|
||||
return std.hash.Wyhash.hash(0, std.mem.asBytes(&@intFromPtr(n)));
|
||||
}
|
||||
|
||||
pub fn eql(_: NodeContext, a: *parser.Node, b: *parser.Node) bool {
|
||||
return @intFromPtr(a) == @intFromPtr(b);
|
||||
}
|
||||
};
|
||||
|
||||
// Searches are a 3 step process:
|
||||
// 1 - Dom.performSearch
|
||||
// 2 - Dom.getSearchResults
|
||||
// 3 - Dom.discardSearchResults
|
||||
//
|
||||
// For a given browser context, we can have multiple active searches. I.e.
|
||||
// performSearch could be called multiple times without getSearchResults or
|
||||
// discardSearchResults being called. We keep these active searches in the
|
||||
// browser context's node_search_list, which is a SearchList. Since we don't
|
||||
// expect many active searches (mostly just 1), a list is fine to scan through.
|
||||
pub const Search = struct {
|
||||
name: []const u8,
|
||||
node_ids: []const Id,
|
||||
|
||||
pub const List = struct {
|
||||
registry: *Registry,
|
||||
search_id: u16 = 0,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
searches: std.ArrayListUnmanaged(Search) = .{},
|
||||
|
||||
pub fn init(allocator: Allocator, registry: *Registry) List {
|
||||
return .{
|
||||
.registry = registry,
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *List) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
pub fn reset(self: *List) void {
|
||||
self.search_id = 0;
|
||||
self.searches = .{};
|
||||
_ = self.arena.reset(.{ .retain_with_limit = 4096 });
|
||||
}
|
||||
|
||||
pub fn create(self: *List, nodes: []const *parser.Node) !Search {
|
||||
const id = self.search_id;
|
||||
defer self.search_id = id +% 1;
|
||||
|
||||
const arena = self.arena.allocator();
|
||||
|
||||
const name = switch (id) {
|
||||
0 => "0",
|
||||
1 => "1",
|
||||
2 => "2",
|
||||
3 => "3",
|
||||
4 => "4",
|
||||
5 => "5",
|
||||
6 => "6",
|
||||
7 => "7",
|
||||
8 => "8",
|
||||
9 => "9",
|
||||
else => try std.fmt.allocPrint(arena, "{d}", .{id}),
|
||||
};
|
||||
|
||||
var registry = self.registry;
|
||||
const node_ids = try arena.alloc(Id, nodes.len);
|
||||
for (nodes, node_ids) |node, *node_id| {
|
||||
node_id.* = (try registry.register(node)).id;
|
||||
}
|
||||
|
||||
const search = Search{
|
||||
.name = name,
|
||||
.node_ids = node_ids,
|
||||
};
|
||||
try self.searches.append(arena, search);
|
||||
return search;
|
||||
}
|
||||
|
||||
pub fn remove(self: *List, name: []const u8) void {
|
||||
for (self.searches.items, 0..) |search, i| {
|
||||
if (std.mem.eql(u8, name, search.name)) {
|
||||
_ = self.searches.swapRemove(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: *const List, name: []const u8) ?Search {
|
||||
for (self.searches.items) |search| {
|
||||
if (std.mem.eql(u8, name, search.name)) {
|
||||
return search;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Need a custom writer, because we can't just serialize the node as-is.
|
||||
// Sometimes we want to serializ the node without chidren, sometimes with just
|
||||
// its direct children, and sometimes the entire tree.
|
||||
// (For now, we only support direct children)
|
||||
|
||||
pub const Writer = struct {
|
||||
opts: Opts,
|
||||
node: *const Node,
|
||||
registry: *Registry,
|
||||
|
||||
pub const Opts = struct {};
|
||||
|
||||
pub fn jsonStringify(self: *const Writer, w: anytype) !void {
|
||||
self.toJSON(w) catch |err| {
|
||||
// The only error our jsonStringify method can return is
|
||||
// @TypeOf(w).Error. In other words, our code can't return its own
|
||||
// error, we can only return a writer error. Kinda sucks.
|
||||
log.err("json stringify: {}", .{err});
|
||||
return error.OutOfMemory;
|
||||
};
|
||||
}
|
||||
|
||||
fn toJSON(self: *const Writer, w: anytype) !void {
|
||||
try w.beginObject();
|
||||
try self.writeCommon(self.node, false, w);
|
||||
|
||||
{
|
||||
var registry = self.registry;
|
||||
const child_nodes = try parser.nodeGetChildNodes(self.node._node);
|
||||
const child_count = try parser.nodeListLength(child_nodes);
|
||||
|
||||
var i: usize = 0;
|
||||
try w.objectField("children");
|
||||
try w.beginArray();
|
||||
for (0..child_count) |_| {
|
||||
const child = (try parser.nodeListItem(child_nodes, @intCast(i))) orelse break;
|
||||
const child_node = try registry.register(child);
|
||||
try w.beginObject();
|
||||
try self.writeCommon(child_node, true, w);
|
||||
try w.endObject();
|
||||
i += 1;
|
||||
}
|
||||
try w.endArray();
|
||||
|
||||
try w.objectField("childNodeCount");
|
||||
try w.write(i);
|
||||
}
|
||||
|
||||
try w.endObject();
|
||||
}
|
||||
|
||||
fn writeCommon(self: *const Writer, node: *const Node, include_child_count: bool, w: anytype) !void {
|
||||
try w.objectField("nodeId");
|
||||
try w.write(node.id);
|
||||
|
||||
try w.objectField("backendNodeId");
|
||||
try w.write(node.id);
|
||||
|
||||
const n = node._node;
|
||||
|
||||
if (try parser.nodeParentNode(n)) |p| {
|
||||
const parent_node = try self.registry.register(p);
|
||||
try w.objectField("parentId");
|
||||
try w.write(parent_node.id);
|
||||
}
|
||||
|
||||
const _map = try parser.nodeGetAttributes(n);
|
||||
if (_map) |map| {
|
||||
const attr_count = try parser.namedNodeMapGetLength(map);
|
||||
try w.objectField("attributes");
|
||||
try w.beginArray();
|
||||
for (0..attr_count) |i| {
|
||||
const attr = try parser.namedNodeMapItem(map, @intCast(i)) orelse continue;
|
||||
try w.write(try parser.attributeGetName(attr));
|
||||
try w.write(try parser.attributeGetValue(attr) orelse continue);
|
||||
}
|
||||
try w.endArray();
|
||||
}
|
||||
|
||||
try w.objectField("nodeType");
|
||||
try w.write(@intFromEnum(try parser.nodeType(n)));
|
||||
|
||||
try w.objectField("nodeName");
|
||||
try w.write(try parser.nodeName(n));
|
||||
|
||||
try w.objectField("localName");
|
||||
try w.write(try parser.nodeLocalName(n));
|
||||
|
||||
try w.objectField("nodeValue");
|
||||
try w.write((try parser.nodeValue(n)) orelse "");
|
||||
|
||||
if (include_child_count) {
|
||||
try w.objectField("childNodeCount");
|
||||
const child_nodes = try parser.nodeGetChildNodes(n);
|
||||
try w.write(try parser.nodeListLength(child_nodes));
|
||||
}
|
||||
|
||||
try w.objectField("documentURL");
|
||||
try w.write(null);
|
||||
|
||||
try w.objectField("baseURL");
|
||||
try w.write(null);
|
||||
|
||||
try w.objectField("xmlVersion");
|
||||
try w.write("");
|
||||
|
||||
try w.objectField("compatibilityMode");
|
||||
try w.write("NoQuirksMode");
|
||||
|
||||
try w.objectField("isScrollable");
|
||||
try w.write(false);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "cdp Node: Registry register" {
|
||||
var registry = Registry.init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
try testing.expectEqual(0, registry.lookup_by_id.count());
|
||||
try testing.expectEqual(0, registry.lookup_by_node.count());
|
||||
|
||||
var doc = try testing.Document.init("<a id=a1>link1</a><div id=d2><p>other</p></div>");
|
||||
defer doc.deinit();
|
||||
|
||||
{
|
||||
const n = (try doc.querySelector("#a1")).?;
|
||||
const node = try registry.register(n);
|
||||
const n1b = registry.lookup_by_id.get(0).?;
|
||||
const n1c = registry.lookup_by_node.get(node._node).?;
|
||||
try testing.expectEqual(node, n1b);
|
||||
try testing.expectEqual(node, n1c);
|
||||
|
||||
try testing.expectEqual(0, node.id);
|
||||
try testing.expectEqual(n, node._node);
|
||||
}
|
||||
|
||||
{
|
||||
const n = (try doc.querySelector("p")).?;
|
||||
const node = try registry.register(n);
|
||||
const n1b = registry.lookup_by_id.get(1).?;
|
||||
const n1c = registry.lookup_by_node.get(node._node).?;
|
||||
try testing.expectEqual(node, n1b);
|
||||
try testing.expectEqual(node, n1c);
|
||||
|
||||
try testing.expectEqual(1, node.id);
|
||||
try testing.expectEqual(n, node._node);
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp Node: search list" {
|
||||
var registry = Registry.init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
var search_list = Search.List.init(testing.allocator, ®istry);
|
||||
defer search_list.deinit();
|
||||
|
||||
{
|
||||
// empty search list, noops
|
||||
search_list.remove("0");
|
||||
try testing.expectEqual(null, search_list.get("0"));
|
||||
}
|
||||
|
||||
{
|
||||
// empty nodes
|
||||
const s1 = try search_list.create(&.{});
|
||||
try testing.expectEqual("0", s1.name);
|
||||
try testing.expectEqual(0, s1.node_ids.len);
|
||||
|
||||
const s2 = search_list.get("0").?;
|
||||
try testing.expectEqual("0", s2.name);
|
||||
try testing.expectEqual(0, s2.node_ids.len);
|
||||
|
||||
search_list.remove("0");
|
||||
try testing.expectEqual(null, search_list.get("0"));
|
||||
}
|
||||
|
||||
{
|
||||
var doc = try testing.Document.init("<a id=a1></a><a id=a2></a>");
|
||||
defer doc.deinit();
|
||||
|
||||
const s1 = try search_list.create(try doc.querySelectorAll("a"));
|
||||
try testing.expectEqual("1", s1.name);
|
||||
try testing.expectEqualSlices(u32, &.{ 0, 1 }, s1.node_ids);
|
||||
|
||||
try testing.expectEqual(2, registry.lookup_by_id.count());
|
||||
try testing.expectEqual(2, registry.lookup_by_node.count());
|
||||
|
||||
const s2 = try search_list.create(try doc.querySelectorAll("#a1"));
|
||||
try testing.expectEqual("2", s2.name);
|
||||
try testing.expectEqualSlices(u32, &.{0}, s2.node_ids);
|
||||
|
||||
const s3 = try search_list.create(try doc.querySelectorAll("#a2"));
|
||||
try testing.expectEqual("3", s3.name);
|
||||
try testing.expectEqualSlices(u32, &.{1}, s3.node_ids);
|
||||
|
||||
try testing.expectEqual(2, registry.lookup_by_id.count());
|
||||
try testing.expectEqual(2, registry.lookup_by_node.count());
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp Node: Writer" {
|
||||
var registry = Registry.init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
var doc = try testing.Document.init("<a id=a1></a><a id=a2></a>");
|
||||
defer doc.deinit();
|
||||
|
||||
{
|
||||
const node = try registry.register(doc.asNode());
|
||||
const json = try std.json.stringifyAlloc(testing.allocator, Writer{
|
||||
.node = node,
|
||||
.opts = .{},
|
||||
.registry = ®istry,
|
||||
}, .{});
|
||||
defer testing.allocator.free(json);
|
||||
|
||||
try testing.expectJson(.{
|
||||
.nodeId = 0,
|
||||
.backendNodeId = 0,
|
||||
.nodeType = 9,
|
||||
.nodeName = "#document",
|
||||
.localName = "",
|
||||
.nodeValue = "",
|
||||
.documentURL = null,
|
||||
.baseURL = null,
|
||||
.xmlVersion = "",
|
||||
.isScrollable = false,
|
||||
.compatibilityMode = "NoQuirksMode",
|
||||
.childNodeCount = 1,
|
||||
.children = &.{.{
|
||||
.nodeId = 1,
|
||||
.backendNodeId = 1,
|
||||
.nodeType = 1,
|
||||
.nodeName = "HTML",
|
||||
.localName = "html",
|
||||
.nodeValue = "",
|
||||
.childNodeCount = 2,
|
||||
.documentURL = null,
|
||||
.baseURL = null,
|
||||
.xmlVersion = "",
|
||||
.compatibilityMode = "NoQuirksMode",
|
||||
.isScrollable = false,
|
||||
}},
|
||||
}, json);
|
||||
}
|
||||
|
||||
{
|
||||
const node = registry.lookup_by_id.get(1).?;
|
||||
const json = try std.json.stringifyAlloc(testing.allocator, Writer{
|
||||
.node = node,
|
||||
.opts = .{},
|
||||
.registry = ®istry,
|
||||
}, .{});
|
||||
defer testing.allocator.free(json);
|
||||
|
||||
try testing.expectJson(.{
|
||||
.nodeId = 1,
|
||||
.backendNodeId = 1,
|
||||
.nodeType = 1,
|
||||
.nodeName = "HTML",
|
||||
.localName = "html",
|
||||
.nodeValue = "",
|
||||
.childNodeCount = 2,
|
||||
.documentURL = null,
|
||||
.baseURL = null,
|
||||
.xmlVersion = "",
|
||||
.compatibilityMode = "NoQuirksMode",
|
||||
.isScrollable = false,
|
||||
.children = &.{ .{
|
||||
.nodeId = 2,
|
||||
.backendNodeId = 2,
|
||||
.nodeType = 1,
|
||||
.nodeName = "HEAD",
|
||||
.localName = "head",
|
||||
.nodeValue = "",
|
||||
.childNodeCount = 0,
|
||||
.documentURL = null,
|
||||
.baseURL = null,
|
||||
.xmlVersion = "",
|
||||
.compatibilityMode = "NoQuirksMode",
|
||||
.isScrollable = false,
|
||||
.parentId = 1,
|
||||
}, .{
|
||||
.nodeId = 3,
|
||||
.backendNodeId = 3,
|
||||
.nodeType = 1,
|
||||
.nodeName = "BODY",
|
||||
.localName = "body",
|
||||
.nodeValue = "",
|
||||
.childNodeCount = 2,
|
||||
.documentURL = null,
|
||||
.baseURL = null,
|
||||
.xmlVersion = "",
|
||||
.compatibilityMode = "NoQuirksMode",
|
||||
.isScrollable = false,
|
||||
.parentId = 1,
|
||||
} },
|
||||
}, json);
|
||||
}
|
||||
}
|
||||
@@ -1,148 +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 server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
getVersion,
|
||||
setDownloadBehavior,
|
||||
getWindowForTarget,
|
||||
setWindowBounds,
|
||||
};
|
||||
|
||||
pub fn browser(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
return switch (method) {
|
||||
.getVersion => getVersion(alloc, msg, ctx),
|
||||
.setDownloadBehavior => setDownloadBehavior(alloc, msg, ctx),
|
||||
.getWindowForTarget => getWindowForTarget(alloc, msg, ctx),
|
||||
.setWindowBounds => setWindowBounds(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: hard coded data
|
||||
const ProtocolVersion = "1.3";
|
||||
const Product = "Chrome/124.0.6367.29";
|
||||
const Revision = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4";
|
||||
const UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
||||
const JsVersion = "12.4.254.8";
|
||||
|
||||
fn getVersion(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "browser.getVersion" });
|
||||
|
||||
// ouput
|
||||
const Res = struct {
|
||||
protocolVersion: []const u8 = ProtocolVersion,
|
||||
product: []const u8 = Product,
|
||||
revision: []const u8 = Revision,
|
||||
userAgent: []const u8 = UserAgent,
|
||||
jsVersion: []const u8 = JsVersion,
|
||||
};
|
||||
return result(alloc, input.id, Res, .{}, null);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setDownloadBehavior(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
behavior: []const u8,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
downloadPath: ?[]const u8 = null,
|
||||
eventsEnabled: ?bool = null,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("REQ > id {d}, method {s}", .{ input.id, "browser.setDownloadBehavior" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, null);
|
||||
}
|
||||
|
||||
// TODO: hard coded ID
|
||||
const DevToolsWindowID = 1923710101;
|
||||
|
||||
fn getWindowForTarget(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
targetId: ?[]const u8 = null,
|
||||
};
|
||||
const input = try Input(?Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
std.debug.assert(input.sessionId != null);
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "browser.getWindowForTarget" });
|
||||
|
||||
// output
|
||||
const Resp = struct {
|
||||
windowId: u64 = DevToolsWindowID,
|
||||
bounds: struct {
|
||||
left: ?u64 = null,
|
||||
top: ?u64 = null,
|
||||
width: ?u64 = null,
|
||||
height: ?u64 = null,
|
||||
windowState: []const u8 = "normal",
|
||||
} = .{},
|
||||
};
|
||||
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setWindowBounds(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "browser.setWindowBounds" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
864
src/cdp/cdp.zig
864
src/cdp/cdp.zig
@@ -17,217 +17,721 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const json = std.json;
|
||||
|
||||
const server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const App = @import("../app.zig").App;
|
||||
const Env = @import("../browser/env.zig").Env;
|
||||
const asUint = @import("../str/parser.zig").asUint;
|
||||
const Browser = @import("../browser/browser.zig").Browser;
|
||||
const Session = @import("../browser/browser.zig").Session;
|
||||
const Page = @import("../browser/browser.zig").Page;
|
||||
const Inspector = @import("../browser/env.zig").Env.Inspector;
|
||||
const Incrementing = @import("../id.zig").Incrementing;
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
|
||||
const browser = @import("browser.zig").browser;
|
||||
const target = @import("target.zig").target;
|
||||
const page = @import("page.zig").page;
|
||||
const log = @import("log.zig").log;
|
||||
const runtime = @import("runtime.zig").runtime;
|
||||
const network = @import("network.zig").network;
|
||||
const emulation = @import("emulation.zig").emulation;
|
||||
const fetch = @import("fetch.zig").fetch;
|
||||
const performance = @import("performance.zig").performance;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
const inspector = @import("inspector.zig").inspector;
|
||||
const dom = @import("dom.zig").dom;
|
||||
const css = @import("css.zig").css;
|
||||
const security = @import("security.zig").security;
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const log_cdp = std.log.scoped(.cdp);
|
||||
pub const URL_BASE = "chrome://newtab/";
|
||||
pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C";
|
||||
|
||||
pub const Error = error{
|
||||
UnknonwDomain,
|
||||
UnknownMethod,
|
||||
NoResponse,
|
||||
RequestWithoutID,
|
||||
};
|
||||
pub const CDP = CDPT(struct {
|
||||
const Client = *@import("../server.zig").Client;
|
||||
});
|
||||
|
||||
pub fn isCdpError(err: anyerror) ?Error {
|
||||
// see https://github.com/ziglang/zig/issues/2473
|
||||
const errors = @typeInfo(Error).ErrorSet.?;
|
||||
inline for (errors) |e| {
|
||||
if (std.mem.eql(u8, e.name, @errorName(err))) {
|
||||
return @errorCast(err);
|
||||
const SessionIdGen = Incrementing(u32, "SID");
|
||||
const TargetIdGen = Incrementing(u32, "TID");
|
||||
const LoaderIdGen = Incrementing(u32, "LID");
|
||||
const BrowserContextIdGen = Incrementing(u32, "BID");
|
||||
|
||||
// Generic so that we can inject mocks into it.
|
||||
pub fn CDPT(comptime TypeProvider: type) type {
|
||||
return struct {
|
||||
// Used for sending message to the client and closing on error
|
||||
client: TypeProvider.Client,
|
||||
|
||||
allocator: Allocator,
|
||||
|
||||
// The active browser
|
||||
browser: Browser,
|
||||
|
||||
// when true, any target creation must be attached.
|
||||
target_auto_attach: bool = false,
|
||||
|
||||
target_id_gen: TargetIdGen = .{},
|
||||
loader_id_gen: LoaderIdGen = .{},
|
||||
session_id_gen: SessionIdGen = .{},
|
||||
browser_context_id_gen: BrowserContextIdGen = .{},
|
||||
|
||||
browser_context: ?BrowserContext(Self),
|
||||
|
||||
// Re-used arena for processing a message. We're assuming that we're getting
|
||||
// 1 message at a time.
|
||||
message_arena: std.heap.ArenaAllocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(app: *App, client: TypeProvider.Client) !Self {
|
||||
const allocator = app.allocator;
|
||||
const browser = try Browser.init(app);
|
||||
errdefer browser.deinit();
|
||||
|
||||
return .{
|
||||
.client = client,
|
||||
.browser = browser,
|
||||
.allocator = allocator,
|
||||
.browser_context = null,
|
||||
.message_arena = std.heap.ArenaAllocator.init(allocator),
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const Domains = enum {
|
||||
Browser,
|
||||
Target,
|
||||
Page,
|
||||
Log,
|
||||
Runtime,
|
||||
Network,
|
||||
DOM,
|
||||
CSS,
|
||||
Inspector,
|
||||
Emulation,
|
||||
Fetch,
|
||||
Performance,
|
||||
Security,
|
||||
};
|
||||
pub fn deinit(self: *Self) void {
|
||||
if (self.browser_context) |*bc| {
|
||||
bc.deinit();
|
||||
}
|
||||
self.browser.deinit();
|
||||
self.message_arena.deinit();
|
||||
}
|
||||
|
||||
// The caller is responsible for calling `free` on the returned slice.
|
||||
pub fn do(
|
||||
alloc: std.mem.Allocator,
|
||||
s: []const u8,
|
||||
ctx: *Ctx,
|
||||
) anyerror![]const u8 {
|
||||
pub fn handleMessage(self: *Self, msg: []const u8) bool {
|
||||
// if there's an error, it's already been logged
|
||||
self.processMessage(msg) catch return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// incoming message parser
|
||||
var msg = IncomingMessage.init(alloc, s);
|
||||
defer msg.deinit();
|
||||
pub fn processMessage(self: *Self, msg: []const u8) !void {
|
||||
const arena = &self.message_arena;
|
||||
defer _ = arena.reset(.{ .retain_with_limit = 1024 * 16 });
|
||||
return self.dispatch(arena.allocator(), self, msg);
|
||||
}
|
||||
|
||||
return dispatch(alloc, &msg, ctx);
|
||||
}
|
||||
// Called from above, in processMessage which handles client messages
|
||||
// but can also be called internally. For example, Target.sendMessageToTarget
|
||||
// calls back into dispatch to capture the response.
|
||||
pub fn dispatch(self: *Self, arena: Allocator, sender: anytype, str: []const u8) !void {
|
||||
const input = json.parseFromSliceLeaky(InputMessage, arena, str, .{
|
||||
.ignore_unknown_fields = true,
|
||||
}) catch return error.InvalidJSON;
|
||||
|
||||
pub fn dispatch(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
ctx: *Ctx,
|
||||
) anyerror![]const u8 {
|
||||
const method = try msg.getMethod();
|
||||
var command = Command(Self, @TypeOf(sender)){
|
||||
.input = .{
|
||||
.json = str,
|
||||
.id = input.id,
|
||||
.action = "",
|
||||
.params = input.params,
|
||||
.session_id = input.sessionId,
|
||||
},
|
||||
.cdp = self,
|
||||
.arena = arena,
|
||||
.sender = sender,
|
||||
.browser_context = if (self.browser_context) |*bc| bc else null,
|
||||
};
|
||||
|
||||
// retrieve domain from method
|
||||
var iter = std.mem.splitScalar(u8, method, '.');
|
||||
const domain = std.meta.stringToEnum(Domains, iter.first()) orelse
|
||||
return error.UnknonwDomain;
|
||||
// See dispatchStartupCommand for more info on this.
|
||||
var is_startup = false;
|
||||
if (input.sessionId) |input_session_id| {
|
||||
if (std.mem.eql(u8, input_session_id, "STARTUP")) {
|
||||
is_startup = true;
|
||||
} else if (self.isValidSessionId(input_session_id) == false) {
|
||||
return command.sendError(-32001, "Unknown sessionId");
|
||||
}
|
||||
}
|
||||
|
||||
// select corresponding domain
|
||||
const action = iter.next() orelse return error.BadMethod;
|
||||
return switch (domain) {
|
||||
.Browser => browser(alloc, msg, action, ctx),
|
||||
.Target => target(alloc, msg, action, ctx),
|
||||
.Page => page(alloc, msg, action, ctx),
|
||||
.Log => log(alloc, msg, action, ctx),
|
||||
.Runtime => runtime(alloc, msg, action, ctx),
|
||||
.Network => network(alloc, msg, action, ctx),
|
||||
.DOM => dom(alloc, msg, action, ctx),
|
||||
.CSS => css(alloc, msg, action, ctx),
|
||||
.Inspector => inspector(alloc, msg, action, ctx),
|
||||
.Emulation => emulation(alloc, msg, action, ctx),
|
||||
.Fetch => fetch(alloc, msg, action, ctx),
|
||||
.Performance => performance(alloc, msg, action, ctx),
|
||||
.Security => security(alloc, msg, action, ctx),
|
||||
if (is_startup) {
|
||||
dispatchStartupCommand(&command) catch |err| {
|
||||
command.sendError(-31999, @errorName(err)) catch {};
|
||||
return err;
|
||||
};
|
||||
} else {
|
||||
dispatchCommand(&command, input.method) catch |err| {
|
||||
command.sendError(-31998, @errorName(err)) catch {};
|
||||
return err;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// A CDP session isn't 100% fully driven by the driver. There's are
|
||||
// independent actions that the browser is expected to take. For example
|
||||
// Puppeteer expects the browser to startup a tab and thus have existing
|
||||
// targets.
|
||||
// To this end, we create a [very] dummy BrowserContext, Target and
|
||||
// Session. There isn't actually a BrowserContext, just a special id.
|
||||
// When messages are received with the "STARTUP" sessionId, we do
|
||||
// "special" handling - the bare minimum we need to do until the driver
|
||||
// switches to a real BrowserContext.
|
||||
// (I can imagine this logic will become driver-specific)
|
||||
fn dispatchStartupCommand(command: anytype) !void {
|
||||
return command.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn dispatchCommand(command: anytype, method: []const u8) !void {
|
||||
const domain = blk: {
|
||||
const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse {
|
||||
return error.InvalidMethod;
|
||||
};
|
||||
command.input.action = method[i + 1 ..];
|
||||
break :blk method[0..i];
|
||||
};
|
||||
|
||||
switch (domain.len) {
|
||||
3 => switch (@as(u24, @bitCast(domain[0..3].*))) {
|
||||
asUint("DOM") => return @import("domains/dom.zig").processMessage(command),
|
||||
asUint("Log") => return @import("domains/log.zig").processMessage(command),
|
||||
asUint("CSS") => return @import("domains/css.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
4 => switch (@as(u32, @bitCast(domain[0..4].*))) {
|
||||
asUint("Page") => return @import("domains/page.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
5 => switch (@as(u40, @bitCast(domain[0..5].*))) {
|
||||
asUint("Fetch") => return @import("domains/fetch.zig").processMessage(command),
|
||||
asUint("Input") => return @import("domains/input.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(domain[0..6].*))) {
|
||||
asUint("Target") => return @import("domains/target.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
7 => switch (@as(u56, @bitCast(domain[0..7].*))) {
|
||||
asUint("Browser") => return @import("domains/browser.zig").processMessage(command),
|
||||
asUint("Runtime") => return @import("domains/runtime.zig").processMessage(command),
|
||||
asUint("Network") => return @import("domains/network.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
8 => switch (@as(u64, @bitCast(domain[0..8].*))) {
|
||||
asUint("Security") => return @import("domains/security.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
9 => switch (@as(u72, @bitCast(domain[0..9].*))) {
|
||||
asUint("Emulation") => return @import("domains/emulation.zig").processMessage(command),
|
||||
asUint("Inspector") => return @import("domains/inspector.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
11 => switch (@as(u88, @bitCast(domain[0..11].*))) {
|
||||
asUint("Performance") => return @import("domains/performance.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
return error.UnknownDomain;
|
||||
}
|
||||
|
||||
fn isValidSessionId(self: *const Self, input_session_id: []const u8) bool {
|
||||
const browser_context = &(self.browser_context orelse return false);
|
||||
const session_id = browser_context.session_id orelse return false;
|
||||
return std.mem.eql(u8, session_id, input_session_id);
|
||||
}
|
||||
|
||||
pub fn createBrowserContext(self: *Self) ![]const u8 {
|
||||
if (self.browser_context != null) {
|
||||
return error.AlreadyExists;
|
||||
}
|
||||
const id = self.browser_context_id_gen.next();
|
||||
|
||||
self.browser_context = @as(BrowserContext(Self), undefined);
|
||||
const browser_context = &self.browser_context.?;
|
||||
|
||||
try BrowserContext(Self).init(browser_context, id, self);
|
||||
return id;
|
||||
}
|
||||
|
||||
pub fn disposeBrowserContext(self: *Self, browser_context_id: []const u8) bool {
|
||||
const bc = &(self.browser_context orelse return false);
|
||||
if (std.mem.eql(u8, bc.id, browser_context_id) == false) {
|
||||
return false;
|
||||
}
|
||||
bc.deinit();
|
||||
self.browser.closeSession();
|
||||
self.browser_context = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
const SendEventOpts = struct {
|
||||
session_id: ?[]const u8 = null,
|
||||
};
|
||||
pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: SendEventOpts) !void {
|
||||
return self.sendJSON(.{
|
||||
.method = method,
|
||||
.params = if (comptime @typeInfo(@TypeOf(p)) == .null) struct {}{} else p,
|
||||
.sessionId = opts.session_id,
|
||||
});
|
||||
}
|
||||
|
||||
fn sendJSON(self: *Self, message: anytype) !void {
|
||||
return self.client.sendJSON(message, .{
|
||||
.emit_null_optional_fields = false,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const State = struct {
|
||||
executionContextId: u32 = 0,
|
||||
contextID: ?[]const u8 = null,
|
||||
sessionID: ?[]const u8 = null,
|
||||
frameID: []const u8 = FrameID,
|
||||
url: []const u8 = URLBase,
|
||||
securityOrigin: []const u8 = URLBase,
|
||||
secureContextType: []const u8 = "Secure", // TODO: enum
|
||||
loaderID: []const u8 = LoaderID,
|
||||
pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
const Node = @import("Node.zig");
|
||||
|
||||
page_life_cycle_events: bool = false, // TODO; Target based value
|
||||
};
|
||||
return struct {
|
||||
id: []const u8,
|
||||
cdp: *CDP_T,
|
||||
|
||||
// Utils
|
||||
// -----
|
||||
// Represents the browser session. There is no equivalent in CDP. For
|
||||
// all intents and purpose, from CDP's point of view our Browser and
|
||||
// our Session more or less maps to a BrowserContext. THIS HAS ZERO
|
||||
// RELATION TO SESSION_ID
|
||||
session: *Session,
|
||||
|
||||
pub fn dumpFile(
|
||||
alloc: std.mem.Allocator,
|
||||
id: u16,
|
||||
script: []const u8,
|
||||
) !void {
|
||||
const name = try std.fmt.allocPrint(alloc, "id_{d}.js", .{id});
|
||||
defer alloc.free(name);
|
||||
var dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{});
|
||||
defer dir.close();
|
||||
const f = try dir.createFile(name, .{});
|
||||
defer f.close();
|
||||
const nb = try f.write(script);
|
||||
std.debug.assert(nb == script.len);
|
||||
const p = try dir.realpathAlloc(alloc, name);
|
||||
defer alloc.free(p);
|
||||
}
|
||||
// Points to the session arena
|
||||
arena: Allocator,
|
||||
|
||||
// caller owns the slice returned
|
||||
pub fn stringify(alloc: std.mem.Allocator, res: anytype) ![]const u8 {
|
||||
var out = std.ArrayList(u8).init(alloc);
|
||||
defer out.deinit();
|
||||
// Maps to our Page. (There are other types of targets, but we only
|
||||
// deal with "pages" for now). Since we only allow 1 open page at a
|
||||
// time, we only have 1 target_id.
|
||||
target_id: ?[]const u8,
|
||||
|
||||
// Do not emit optional null fields
|
||||
const options: std.json.StringifyOptions = .{ .emit_null_optional_fields = false };
|
||||
// The CDP session_id. After the target/page is created, the client
|
||||
// "attaches" to it (either explicitly or automatically). We return a
|
||||
// "sessionId" which identifies this link. `sessionId` is the how
|
||||
// the CDP client informs us what it's trying to manipulate. Because we
|
||||
// only support 1 BrowserContext at a time, and 1 page at a time, this
|
||||
// is all pretty straightforward, but it still needs to be enforced, i.e.
|
||||
// if we get a request with a sessionId that doesn't match the current one
|
||||
// we should reject it.
|
||||
session_id: ?[]const u8,
|
||||
|
||||
try std.json.stringify(res, options, out.writer());
|
||||
const ret = try alloc.alloc(u8, out.items.len);
|
||||
@memcpy(ret, out.items);
|
||||
return ret;
|
||||
}
|
||||
loader_id: []const u8,
|
||||
security_origin: []const u8,
|
||||
page_life_cycle_events: bool,
|
||||
secure_context_type: []const u8,
|
||||
node_registry: Node.Registry,
|
||||
node_search_list: Node.Search.List,
|
||||
|
||||
const resultNull = "{{\"id\": {d}, \"result\": {{}}}}";
|
||||
const resultNullSession = "{{\"id\": {d}, \"result\": {{}}, \"sessionId\": \"{s}\"}}";
|
||||
inspector: Inspector,
|
||||
isolated_world: ?IsolatedWorld,
|
||||
|
||||
// caller owns the slice returned
|
||||
pub fn result(
|
||||
alloc: std.mem.Allocator,
|
||||
id: u16,
|
||||
comptime T: ?type,
|
||||
res: anytype,
|
||||
sessionID: ?[]const u8,
|
||||
) ![]const u8 {
|
||||
log_cdp.debug(
|
||||
"Res > id {d}, sessionID {?s}, result {any}",
|
||||
.{ id, sessionID, res },
|
||||
);
|
||||
if (T == null) {
|
||||
// No need to stringify a custom JSON msg, just use string templates
|
||||
if (sessionID) |sID| {
|
||||
return try std.fmt.allocPrint(alloc, resultNullSession, .{ id, sID });
|
||||
const Self = @This();
|
||||
|
||||
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
|
||||
const allocator = cdp.allocator;
|
||||
|
||||
const session = try cdp.browser.newSession();
|
||||
const arena = session.arena.allocator();
|
||||
|
||||
const inspector = try cdp.browser.env.newInspector(arena, self);
|
||||
|
||||
var registry = Node.Registry.init(allocator);
|
||||
errdefer registry.deinit();
|
||||
|
||||
self.* = .{
|
||||
.id = id,
|
||||
.cdp = cdp,
|
||||
.arena = arena,
|
||||
.target_id = null,
|
||||
.session_id = null,
|
||||
.session = session,
|
||||
.security_origin = URL_BASE,
|
||||
.secure_context_type = "Secure", // TODO = enum
|
||||
.loader_id = LOADER_ID,
|
||||
.page_life_cycle_events = false, // TODO; Target based value
|
||||
.node_registry = registry,
|
||||
.node_search_list = undefined,
|
||||
.isolated_world = null,
|
||||
.inspector = inspector,
|
||||
};
|
||||
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
|
||||
errdefer self.deinit();
|
||||
|
||||
try cdp.browser.notification.register(.page_remove, self, onPageRemove);
|
||||
try cdp.browser.notification.register(.page_created, self, onPageCreated);
|
||||
try cdp.browser.notification.register(.page_navigate, self, onPageNavigate);
|
||||
try cdp.browser.notification.register(.page_navigated, self, onPageNavigated);
|
||||
}
|
||||
return try std.fmt.allocPrint(alloc, resultNull, .{id});
|
||||
}
|
||||
|
||||
const Resp = struct {
|
||||
id: u16,
|
||||
result: T.?,
|
||||
sessionId: ?[]const u8,
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.inspector.deinit();
|
||||
|
||||
// If the session has a page, we need to clear it first. The page
|
||||
// context is always nested inside of the isolated world context,
|
||||
// so we need to shutdown the page one first.
|
||||
self.cdp.browser.closeSession();
|
||||
|
||||
if (self.isolated_world) |*world| {
|
||||
world.deinit();
|
||||
}
|
||||
self.node_registry.deinit();
|
||||
self.node_search_list.deinit();
|
||||
self.cdp.browser.notification.unregisterAll(self);
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.node_registry.reset();
|
||||
self.node_search_list.reset();
|
||||
}
|
||||
|
||||
pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld {
|
||||
if (self.isolated_world != null) {
|
||||
return error.CurrentlyOnly1IsolatedWorldSupported;
|
||||
}
|
||||
|
||||
var executor = try self.cdp.browser.env.newExecutor();
|
||||
errdefer executor.deinit();
|
||||
|
||||
self.isolated_world = .{
|
||||
.name = try self.arena.dupe(u8, world_name),
|
||||
.scope = null,
|
||||
.executor = executor,
|
||||
.grant_universal_access = grant_universal_access,
|
||||
};
|
||||
return &self.isolated_world.?;
|
||||
}
|
||||
|
||||
pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer {
|
||||
return .{
|
||||
.node = node,
|
||||
.opts = opts,
|
||||
.registry = &self.node_registry,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getURL(self: *const Self) ?[]const u8 {
|
||||
const page = self.session.currentPage() orelse return null;
|
||||
const raw_url = page.url.raw;
|
||||
return if (raw_url.len == 0) null else raw_url;
|
||||
}
|
||||
|
||||
pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
|
||||
const self: *Self = @alignCast(@ptrCast(ctx));
|
||||
return @import("domains/page.zig").pageRemove(self);
|
||||
}
|
||||
|
||||
pub fn onPageCreated(ctx: *anyopaque, page: *Page) !void {
|
||||
const self: *Self = @alignCast(@ptrCast(ctx));
|
||||
return @import("domains/page.zig").pageCreated(self, page);
|
||||
}
|
||||
|
||||
pub fn onPageNavigate(ctx: *anyopaque, data: *const Notification.PageNavigate) !void {
|
||||
const self: *Self = @alignCast(@ptrCast(ctx));
|
||||
return @import("domains/page.zig").pageNavigate(self, data);
|
||||
}
|
||||
|
||||
pub fn onPageNavigated(ctx: *anyopaque, data: *const Notification.PageNavigated) !void {
|
||||
const self: *Self = @alignCast(@ptrCast(ctx));
|
||||
return @import("domains/page.zig").pageNavigated(self, data);
|
||||
}
|
||||
|
||||
pub fn callInspector(self: *const Self, msg: []const u8) void {
|
||||
self.inspector.send(msg);
|
||||
// force running micro tasks after send input to the inspector.
|
||||
self.cdp.browser.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
|
||||
if (std.log.defaultLogEnabled(.debug)) {
|
||||
// msg should be {"id":<id>,...
|
||||
std.debug.assert(std.mem.startsWith(u8, msg, "{\"id\":"));
|
||||
|
||||
const id_end = std.mem.indexOfScalar(u8, msg, ',') orelse {
|
||||
log.warn("invalid inspector response message: {s}", .{msg});
|
||||
return;
|
||||
};
|
||||
const id = msg[6..id_end];
|
||||
log.debug("Res (inspector) > id {s}", .{id});
|
||||
}
|
||||
sendInspectorMessage(@alignCast(@ptrCast(ctx)), msg) catch |err| {
|
||||
log.err("Failed to send inspector response: {any}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn onInspectorEvent(ctx: *anyopaque, msg: []const u8) void {
|
||||
if (std.log.defaultLogEnabled(.debug)) {
|
||||
// msg should be {"method":<method>,...
|
||||
std.debug.assert(std.mem.startsWith(u8, msg, "{\"method\":"));
|
||||
const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse {
|
||||
log.warn("invalid inspector event message: {s}", .{msg});
|
||||
return;
|
||||
};
|
||||
const method = msg[10..method_end];
|
||||
log.debug("Event (inspector) > method {s}", .{method});
|
||||
}
|
||||
|
||||
sendInspectorMessage(@alignCast(@ptrCast(ctx)), msg) catch |err| {
|
||||
log.err("Failed to send inspector event: {any}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
// This is hacky x 2. First, we create the JSON payload by gluing our
|
||||
// session_id onto it. Second, we're much more client/websocket aware than
|
||||
// we should be.
|
||||
fn sendInspectorMessage(self: *Self, msg: []const u8) !void {
|
||||
const session_id = self.session_id orelse {
|
||||
// We no longer have an active session. What should we do
|
||||
// in this case?
|
||||
return;
|
||||
};
|
||||
|
||||
const cdp = self.cdp;
|
||||
var arena = std.heap.ArenaAllocator.init(cdp.allocator);
|
||||
errdefer arena.deinit();
|
||||
|
||||
const field = ",\"sessionId\":\"";
|
||||
|
||||
// + 1 for the closing quote after the session id
|
||||
// + 10 for the max websocket header
|
||||
const message_len = msg.len + session_id.len + 1 + field.len + 10;
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
buf.ensureTotalCapacity(arena.allocator(), message_len) catch |err| {
|
||||
log.err("Failed to expand inspector buffer: {any}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// reserve 10 bytes for websocket header
|
||||
buf.appendSliceAssumeCapacity(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
|
||||
|
||||
// -1 because we dont' want the closing brace '}'
|
||||
buf.appendSliceAssumeCapacity(msg[0 .. msg.len - 1]);
|
||||
buf.appendSliceAssumeCapacity(field);
|
||||
buf.appendSliceAssumeCapacity(session_id);
|
||||
buf.appendSliceAssumeCapacity("\"}");
|
||||
std.debug.assert(buf.items.len == message_len);
|
||||
|
||||
try cdp.client.sendJSONRaw(arena, buf);
|
||||
}
|
||||
};
|
||||
const resp = Resp{ .id = id, .result = res, .sessionId = sessionID };
|
||||
|
||||
return stringify(alloc, resp);
|
||||
}
|
||||
|
||||
pub fn sendEvent(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: *Ctx,
|
||||
/// see: https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#world
|
||||
/// The current understanding. An isolated world lives in the same isolate, but a separated context.
|
||||
/// Clients create this to be able to create variables and run code without interfering with the
|
||||
/// normal namespace and values of the webpage. Similar to the main context we need to pretend to recreate it after
|
||||
/// a executionContextsCleared event which happens when navigating to a new page. A client can have a command be executed
|
||||
/// in the isolated world by using its Context ID or the worldName.
|
||||
/// grantUniveralAccess Indecated whether the isolated world can reference objects like the DOM or other JS Objects.
|
||||
/// An isolated world has it's own instance of globals like Window.
|
||||
/// Generally the client needs to resolve a node into the isolated world to be able to work with it.
|
||||
/// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.
|
||||
const IsolatedWorld = struct {
|
||||
name: []const u8,
|
||||
comptime T: type,
|
||||
params: T,
|
||||
sessionID: ?[]const u8,
|
||||
) !void {
|
||||
log_cdp.debug("Event > method {s}, sessionID {?s}", .{ name, sessionID });
|
||||
const Resp = struct {
|
||||
method: []const u8,
|
||||
params: T,
|
||||
sessionId: ?[]const u8,
|
||||
};
|
||||
const resp = Resp{ .method = name, .params = params, .sessionId = sessionID };
|
||||
scope: ?*Env.Scope,
|
||||
executor: Env.Executor,
|
||||
grant_universal_access: bool,
|
||||
|
||||
const event_msg = try stringify(alloc, resp);
|
||||
try ctx.send(event_msg);
|
||||
pub fn deinit(self: *IsolatedWorld) void {
|
||||
self.executor.deinit();
|
||||
self.scope = null;
|
||||
}
|
||||
pub fn removeContext(self: *IsolatedWorld) !void {
|
||||
if (self.scope == null) return error.NoIsolatedContextToRemove;
|
||||
self.executor.endScope();
|
||||
self.scope = null;
|
||||
}
|
||||
|
||||
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
|
||||
// (assuming grantUniveralAccess will be set to True!).
|
||||
// We just created the world and the page. The page's state lives in the session, but is update on navigation.
|
||||
// This also means this pointer becomes invalid after removePage untill a new page is created.
|
||||
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
|
||||
pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
|
||||
if (self.scope != null) return error.Only1IsolatedContextSupported;
|
||||
self.scope = try self.executor.startScope(&page.window, &page.state, {}, false);
|
||||
}
|
||||
};
|
||||
|
||||
// This is a generic because when we send a result we have two different
|
||||
// behaviors. Normally, we're sending the result to the client. But in some cases
|
||||
// we want to capture the result. So we want the command.sendResult to be
|
||||
// generic.
|
||||
pub fn Command(comptime CDP_T: type, comptime Sender: type) type {
|
||||
return struct {
|
||||
// A misc arena that can be used for any allocation for processing
|
||||
// the message
|
||||
arena: Allocator,
|
||||
|
||||
// reference to our CDP instance
|
||||
cdp: *CDP_T,
|
||||
|
||||
// The browser context this command targets
|
||||
browser_context: ?*BrowserContext(CDP_T),
|
||||
|
||||
// The command input (the id, optional session_id, params, ...)
|
||||
input: Input,
|
||||
|
||||
// In most cases, Sender is going to be cdp itself. We'll call
|
||||
// sender.sendJSON() and CDP will send it to the client. But some
|
||||
// comamnds are dispatched internally, in which cases the Sender will
|
||||
// be code to capture the data that we were "sending".
|
||||
sender: Sender,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn params(self: *const Self, comptime T: type) !?T {
|
||||
if (self.input.params) |p| {
|
||||
return try json.parseFromSliceLeaky(
|
||||
T,
|
||||
self.arena,
|
||||
p.raw,
|
||||
.{ .ignore_unknown_fields = true },
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) {
|
||||
_ = try self.cdp.createBrowserContext();
|
||||
self.browser_context = &(self.cdp.browser_context.?);
|
||||
return self.browser_context.?;
|
||||
}
|
||||
|
||||
const SendResultOpts = struct {
|
||||
include_session_id: bool = true,
|
||||
};
|
||||
pub fn sendResult(self: *Self, result: anytype, opts: SendResultOpts) !void {
|
||||
return self.sender.sendJSON(.{
|
||||
.id = self.input.id,
|
||||
.result = if (comptime @typeInfo(@TypeOf(result)) == .null) struct {}{} else result,
|
||||
.sessionId = if (opts.include_session_id) self.input.session_id else null,
|
||||
});
|
||||
}
|
||||
|
||||
const SendEventOpts = struct {
|
||||
session_id: ?[]const u8 = null,
|
||||
};
|
||||
pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: CDP_T.SendEventOpts) !void {
|
||||
// Events ALWAYS go to the client. self.sender should not be used
|
||||
return self.cdp.sendEvent(method, p, opts);
|
||||
}
|
||||
|
||||
pub fn sendError(self: *Self, code: i32, message: []const u8) !void {
|
||||
return self.sender.sendJSON(.{
|
||||
.id = self.input.id,
|
||||
.code = code,
|
||||
.message = message,
|
||||
});
|
||||
}
|
||||
|
||||
const Input = struct {
|
||||
// When we reply to a message, we echo back the message id
|
||||
id: ?i64,
|
||||
|
||||
// The "action" of the message.Given a method of "LOG.enable", the
|
||||
// action is "enable"
|
||||
action: []const u8,
|
||||
|
||||
// See notes in BrowserContext about session_id
|
||||
session_id: ?[]const u8,
|
||||
|
||||
// Unparsed / untyped input.params.
|
||||
params: ?InputParams,
|
||||
|
||||
// The full raw json input
|
||||
json: []const u8,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Common
|
||||
// ------
|
||||
|
||||
// TODO: hard coded IDs
|
||||
pub const BrowserSessionID = "BROWSERSESSIONID597D9875C664CAC0";
|
||||
pub const ContextSessionID = "CONTEXTSESSIONID0497A05C95417CF4";
|
||||
pub const URLBase = "chrome://newtab/";
|
||||
pub const LoaderID = "LOADERID24DD2FD56CF1EF33C965C79C";
|
||||
pub const FrameID = "FRAMEIDD8AED408A0467AC93100BCDBE";
|
||||
|
||||
pub const TimestampEvent = struct {
|
||||
timestamp: f64,
|
||||
// When we parse a JSON message from the client, this is the structure
|
||||
// we always expect
|
||||
const InputMessage = struct {
|
||||
id: ?i64 = null,
|
||||
method: []const u8,
|
||||
params: ?InputParams = null,
|
||||
sessionId: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
// The JSON "params" field changes based on the "method". Initially, we just
|
||||
// capture the raw json object (including the opening and closing braces).
|
||||
// Then, when we're processing the message, and we know what type it is, we
|
||||
// can parse it (in Disaptch(T).params).
|
||||
const InputParams = struct {
|
||||
raw: []const u8,
|
||||
|
||||
pub fn jsonParse(
|
||||
_: Allocator,
|
||||
scanner: *json.Scanner,
|
||||
_: json.ParseOptions,
|
||||
) !InputParams {
|
||||
const height = scanner.stackHeight();
|
||||
|
||||
const start = scanner.cursor;
|
||||
if (try scanner.next() != .object_begin) {
|
||||
return error.UnexpectedToken;
|
||||
}
|
||||
try scanner.skipUntilStackHeight(height);
|
||||
const end = scanner.cursor;
|
||||
|
||||
return .{ .raw = scanner.input[start..end] };
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "cdp: invalid json" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
try testing.expectError(error.InvalidJSON, ctx.processMessage("invalid"));
|
||||
|
||||
// method is required
|
||||
try testing.expectError(error.InvalidJSON, ctx.processMessage(.{}));
|
||||
|
||||
try testing.expectError(error.InvalidMethod, ctx.processMessage(.{
|
||||
.method = "Target",
|
||||
}));
|
||||
try ctx.expectSentError(-31998, "InvalidMethod", .{});
|
||||
|
||||
try testing.expectError(error.UnknownDomain, ctx.processMessage(.{
|
||||
.method = "Unknown.domain",
|
||||
}));
|
||||
|
||||
try testing.expectError(error.UnknownMethod, ctx.processMessage(.{
|
||||
.method = "Target.over9000",
|
||||
}));
|
||||
}
|
||||
|
||||
test "cdp: invalid sessionId" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
// we have no browser context
|
||||
try ctx.processMessage(.{ .method = "Hi", .sessionId = "nope" });
|
||||
try ctx.expectSentError(-32001, "Unknown sessionId", .{});
|
||||
}
|
||||
|
||||
{
|
||||
// we have a brower context but no session_id
|
||||
_ = try ctx.loadBrowserContext(.{});
|
||||
try ctx.processMessage(.{ .method = "Hi", .sessionId = "BC-Has-No-SessionId" });
|
||||
try ctx.expectSentError(-32001, "Unknown sessionId", .{});
|
||||
}
|
||||
|
||||
{
|
||||
// we have a brower context with a different session_id
|
||||
_ = try ctx.loadBrowserContext(.{ .session_id = "SESS-2" });
|
||||
try ctx.processMessage(.{ .method = "Hi", .sessionId = "SESS-1" });
|
||||
try ctx.expectSentError(-32001, "Unknown sessionId", .{});
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp: STARTUP sessionId" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
// we have no browser context
|
||||
try ctx.processMessage(.{ .id = 2, .method = "Hi", .sessionId = "STARTUP" });
|
||||
try ctx.expectSentResult(null, .{ .id = 2, .index = 0, .session_id = "STARTUP" });
|
||||
}
|
||||
|
||||
{
|
||||
// we have a brower context but no session_id
|
||||
_ = try ctx.loadBrowserContext(.{});
|
||||
try ctx.processMessage(.{ .id = 3, .method = "Hi", .sessionId = "STARTUP" });
|
||||
try ctx.expectSentResult(null, .{ .id = 3, .index = 0, .session_id = "STARTUP" });
|
||||
}
|
||||
|
||||
{
|
||||
// we have a brower context with a different session_id
|
||||
_ = try ctx.loadBrowserContext(.{ .session_id = "SESS-2" });
|
||||
try ctx.processMessage(.{ .id = 4, .method = "Hi", .sessionId = "STARTUP" });
|
||||
try ctx.expectSentResult(null, .{ .id = 4, .index = 0, .session_id = "STARTUP" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +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 server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn css(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
@@ -1,59 +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 server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
enable,
|
||||
};
|
||||
|
||||
pub fn dom(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
|
||||
return switch (method) {
|
||||
.enable => enable(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn enable(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
117
src/cdp/domains/browser.zig
Normal file
117
src/cdp/domains/browser.zig
Normal file
@@ -0,0 +1,117 @@
|
||||
// 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");
|
||||
|
||||
// TODO: hard coded data
|
||||
const PROTOCOL_VERSION = "1.3";
|
||||
const PRODUCT = "Chrome/124.0.6367.29";
|
||||
const REVISION = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4";
|
||||
const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
||||
const JS_VERSION = "12.4.254.8";
|
||||
const DEV_TOOLS_WINDOW_ID = 1923710101;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
getVersion,
|
||||
setDownloadBehavior,
|
||||
getWindowForTarget,
|
||||
setWindowBounds,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.getVersion => return getVersion(cmd),
|
||||
.setDownloadBehavior => return setDownloadBehavior(cmd),
|
||||
.getWindowForTarget => return getWindowForTarget(cmd),
|
||||
.setWindowBounds => return setWindowBounds(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
fn getVersion(cmd: anytype) !void {
|
||||
// TODO: pre-serialize?
|
||||
return cmd.sendResult(.{
|
||||
.protocolVersion = PROTOCOL_VERSION,
|
||||
.product = PRODUCT,
|
||||
.revision = REVISION,
|
||||
.userAgent = USER_AGENT,
|
||||
.jsVersion = JS_VERSION,
|
||||
}, .{ .include_session_id = false });
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setDownloadBehavior(cmd: anytype) !void {
|
||||
// const params = (try cmd.params(struct {
|
||||
// behavior: []const u8,
|
||||
// browserContextId: ?[]const u8 = null,
|
||||
// downloadPath: ?[]const u8 = null,
|
||||
// eventsEnabled: ?bool = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
return cmd.sendResult(null, .{ .include_session_id = false });
|
||||
}
|
||||
|
||||
fn getWindowForTarget(cmd: anytype) !void {
|
||||
// const params = (try cmd.params(struct {
|
||||
// targetId: ?[]const u8 = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
return cmd.sendResult(.{ .windowId = DEV_TOOLS_WINDOW_ID, .bounds = .{
|
||||
.windowState = "normal",
|
||||
} }, .{});
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setWindowBounds(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "cdp.browser: getVersion" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 32,
|
||||
.method = "Browser.getVersion",
|
||||
});
|
||||
|
||||
try ctx.expectSentCount(1);
|
||||
try ctx.expectSentResult(.{
|
||||
.protocolVersion = PROTOCOL_VERSION,
|
||||
.product = PRODUCT,
|
||||
.revision = REVISION,
|
||||
.userAgent = USER_AGENT,
|
||||
.jsVersion = JS_VERSION,
|
||||
}, .{ .id = 32, .index = 0, .session_id = null });
|
||||
}
|
||||
|
||||
test "cdp.browser: getWindowForTarget" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 33,
|
||||
.method = "Browser.getWindowForTarget",
|
||||
});
|
||||
|
||||
try ctx.expectSentCount(1);
|
||||
try ctx.expectSentResult(.{
|
||||
.windowId = DEV_TOOLS_WINDOW_ID,
|
||||
.bounds = .{ .windowState = "normal" },
|
||||
}, .{ .id = 33, .index = 0, .session_id = null });
|
||||
}
|
||||
@@ -16,9 +16,14 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
pub const html: []const u8 =
|
||||
\\<main id='content'>
|
||||
\\<a href='foo'>OK</a>
|
||||
\\<p>blah-blah-blah</p>
|
||||
\\</main>
|
||||
;
|
||||
const std = @import("std");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
}
|
||||
}
|
||||
320
src/cdp/domains/dom.zig
Normal file
320
src/cdp/domains/dom.zig
Normal file
@@ -0,0 +1,320 @@
|
||||
// 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 Node = @import("../Node.zig");
|
||||
const css = @import("../../browser/dom/css.zig");
|
||||
const parser = @import("../../browser/netsurf.zig");
|
||||
const dom_node = @import("../../browser/dom/node.zig");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
getDocument,
|
||||
performSearch,
|
||||
getSearchResults,
|
||||
discardSearchResults,
|
||||
resolveNode,
|
||||
describeNode,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
.getDocument => return getDocument(cmd),
|
||||
.performSearch => return performSearch(cmd),
|
||||
.getSearchResults => return getSearchResults(cmd),
|
||||
.discardSearchResults => return discardSearchResults(cmd),
|
||||
.resolveNode => return resolveNode(cmd),
|
||||
.describeNode => return describeNode(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument
|
||||
fn getDocument(cmd: anytype) !void {
|
||||
// const params = (try cmd.params(struct {
|
||||
// depth: ?u32 = null,
|
||||
// pierce: ?bool = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const doc = page.doc orelse return error.DocumentNotLoaded;
|
||||
|
||||
const node = try bc.node_registry.register(parser.documentToNode(doc));
|
||||
return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{}) }, .{});
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
|
||||
fn performSearch(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
query: []const u8,
|
||||
includeUserAgentShadowDOM: ?bool = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const doc = page.doc orelse return error.DocumentNotLoaded;
|
||||
|
||||
const allocator = cmd.cdp.allocator;
|
||||
var list = try css.querySelectorAll(allocator, parser.documentToNode(doc), params.query);
|
||||
defer list.deinit(allocator);
|
||||
|
||||
const search = try bc.node_search_list.create(list.nodes.items);
|
||||
|
||||
// dispatch setChildNodesEvents to inform the client of the subpart of node
|
||||
// tree covering the results.
|
||||
try dispatchSetChildNodes(cmd, list.nodes.items);
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.searchId = search.name,
|
||||
.resultCount = @as(u32, @intCast(search.node_ids.len)),
|
||||
}, .{});
|
||||
}
|
||||
|
||||
// dispatchSetChildNodes send the setChildNodes event for the whole DOM tree
|
||||
// hierarchy of each nodes.
|
||||
// We dispatch event in the reverse order: from the top level to the direct parents.
|
||||
// We should dispatch a node only if it has never been sent.
|
||||
fn dispatchSetChildNodes(cmd: anytype, nodes: []*parser.Node) !void {
|
||||
const arena = cmd.arena;
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const session_id = bc.session_id orelse return error.SessionIdNotLoaded;
|
||||
|
||||
var parents: std.ArrayListUnmanaged(*Node) = .{};
|
||||
for (nodes) |_n| {
|
||||
var n = _n;
|
||||
while (true) {
|
||||
const p = try parser.nodeParentNode(n) orelse break;
|
||||
|
||||
// Register the node.
|
||||
const node = try bc.node_registry.register(p);
|
||||
if (node.set_child_nodes_event) break;
|
||||
try parents.append(arena, node);
|
||||
n = p;
|
||||
}
|
||||
}
|
||||
|
||||
const plen = parents.items.len;
|
||||
if (plen == 0) return;
|
||||
|
||||
var i: usize = plen;
|
||||
// We're going to iterate in reverse order from how we added them.
|
||||
// This ensures that we're emitting the tree of nodes top-down.
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
const node = parents.items[i];
|
||||
// Although our above loop won't add an already-sent node to `parents`
|
||||
// this can still be true because two nodes can share the same parent node
|
||||
// so we might have just sent the node a previous iteration of this loop
|
||||
if (node.set_child_nodes_event) continue;
|
||||
|
||||
node.set_child_nodes_event = true;
|
||||
|
||||
// If the node has no parent, it's the root node.
|
||||
// We don't dispatch event for it because we assume the root node is
|
||||
// dispatched via the DOM.getDocument command.
|
||||
const p = try parser.nodeParentNode(node._node) orelse {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Retrieve the parent from the registry.
|
||||
const parent_node = try bc.node_registry.register(p);
|
||||
|
||||
try cmd.sendEvent("DOM.setChildNodes", .{
|
||||
.parentId = parent_node.id,
|
||||
.nodes = .{bc.nodeWriter(node, .{})},
|
||||
}, .{
|
||||
.session_id = session_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults
|
||||
fn discardSearchResults(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
searchId: []const u8,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
bc.node_search_list.remove(params.searchId);
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults
|
||||
fn getSearchResults(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
searchId: []const u8,
|
||||
fromIndex: u32,
|
||||
toIndex: u32,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
if (params.fromIndex >= params.toIndex) {
|
||||
return error.BadIndices;
|
||||
}
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
const search = bc.node_search_list.get(params.searchId) orelse {
|
||||
return error.SearchResultNotFound;
|
||||
};
|
||||
|
||||
const node_ids = search.node_ids;
|
||||
|
||||
if (params.fromIndex >= node_ids.len) return error.BadFromIndex;
|
||||
if (params.toIndex > node_ids.len) return error.BadToIndex;
|
||||
|
||||
return cmd.sendResult(.{ .nodeIds = node_ids[params.fromIndex..params.toIndex] }, .{});
|
||||
}
|
||||
|
||||
fn resolveNode(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
nodeId: ?Node.Id = null,
|
||||
backendNodeId: ?u32 = null,
|
||||
objectGroup: ?[]const u8 = null,
|
||||
executionContextId: ?u32 = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
|
||||
var scope = page.scope;
|
||||
if (params.executionContextId) |context_id| {
|
||||
if (scope.context.debugContextId() != context_id) {
|
||||
const isolated_world = bc.isolated_world orelse return error.ContextNotFound;
|
||||
scope = isolated_world.scope orelse return error.ContextNotFound;
|
||||
|
||||
if (scope.context.debugContextId() != context_id) return error.ContextNotFound;
|
||||
}
|
||||
}
|
||||
|
||||
const input_node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;
|
||||
const node = bc.node_registry.lookup_by_id.get(input_node_id) orelse return error.UnknownNode;
|
||||
|
||||
// node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
|
||||
// So we use the Node.Union when retrieve the value from the environment
|
||||
const remote_object = try bc.inspector.getRemoteObject(
|
||||
scope,
|
||||
params.objectGroup orelse "",
|
||||
try dom_node.Node.toInterface(node._node),
|
||||
);
|
||||
defer remote_object.deinit();
|
||||
|
||||
const arena = cmd.arena;
|
||||
return cmd.sendResult(.{ .object = .{
|
||||
.type = try remote_object.getType(arena),
|
||||
.subtype = try remote_object.getSubtype(arena),
|
||||
.className = try remote_object.getClassName(arena),
|
||||
.description = try remote_object.getDescription(arena),
|
||||
.objectId = try remote_object.getObjectId(arena),
|
||||
} }, .{});
|
||||
}
|
||||
|
||||
fn describeNode(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
nodeId: ?Node.Id = null,
|
||||
backendNodeId: ?Node.Id = null,
|
||||
objectId: ?[]const u8 = null,
|
||||
depth: u32 = 1,
|
||||
pierce: bool = false,
|
||||
})) orelse return error.InvalidParams;
|
||||
if (params.backendNodeId != null or params.depth != 1 or params.pierce) {
|
||||
return error.NotYetImplementedParams;
|
||||
}
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
if (params.nodeId != null) {
|
||||
const node = bc.node_registry.lookup_by_id.get(params.nodeId.?) orelse return error.NodeNotFound;
|
||||
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
|
||||
}
|
||||
if (params.objectId != null) {
|
||||
// Retrieve the object from which ever context it is in.
|
||||
const parser_node = try bc.inspector.getNodePtr(cmd.arena, params.objectId.?);
|
||||
const node = try bc.node_registry.register(@ptrCast(parser_node));
|
||||
return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{});
|
||||
}
|
||||
return error.MissingParams;
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
|
||||
test "cdp.dom: getSearchResults unknown search id" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{
|
||||
.id = 8,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "Nope", .fromIndex = 0, .toIndex = 10 },
|
||||
}));
|
||||
}
|
||||
|
||||
test "cdp.dom: search flow" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
_ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "<p>1</p> <p>2</p>" });
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 12,
|
||||
.method = "DOM.performSearch",
|
||||
.params = .{ .query = "p" },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 12 });
|
||||
|
||||
{
|
||||
// getSearchResults
|
||||
try ctx.processMessage(.{
|
||||
.id = 13,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 2 },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .nodeIds = &.{ 0, 1 } }, .{ .id = 13 });
|
||||
|
||||
// different fromIndex
|
||||
try ctx.processMessage(.{
|
||||
.id = 14,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "0", .fromIndex = 1, .toIndex = 2 },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .nodeIds = &.{1} }, .{ .id = 14 });
|
||||
|
||||
// different toIndex
|
||||
try ctx.processMessage(.{
|
||||
.id = 15,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .nodeIds = &.{0} }, .{ .id = 15 });
|
||||
}
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 16,
|
||||
.method = "DOM.discardSearchResults",
|
||||
.params = .{ .searchId = "0" },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 16 });
|
||||
|
||||
// make sure the delete actually did something
|
||||
try testing.expectError(error.SearchResultNotFound, ctx.processMessage(.{
|
||||
.id = 17,
|
||||
.method = "DOM.getSearchResults",
|
||||
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 },
|
||||
}));
|
||||
}
|
||||
66
src/cdp/domains/emulation.zig
Normal file
66
src/cdp/domains/emulation.zig
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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 fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
setEmulatedMedia,
|
||||
setFocusEmulationEnabled,
|
||||
setDeviceMetricsOverride,
|
||||
setTouchEmulationEnabled,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.setEmulatedMedia => return setEmulatedMedia(cmd),
|
||||
.setFocusEmulationEnabled => return setFocusEmulationEnabled(cmd),
|
||||
.setDeviceMetricsOverride => return setDeviceMetricsOverride(cmd),
|
||||
.setTouchEmulationEnabled => return setTouchEmulationEnabled(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setEmulatedMedia(cmd: anytype) !void {
|
||||
// const input = (try const incoming.params(struct {
|
||||
// media: ?[]const u8 = null,
|
||||
// features: ?[]struct{
|
||||
// name: []const u8,
|
||||
// value: [] const u8
|
||||
// } = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setFocusEmulationEnabled(cmd: anytype) !void {
|
||||
// const input = (try const incoming.params(struct {
|
||||
// enabled: bool,
|
||||
// })) orelse return error.InvalidParams;
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setDeviceMetricsOverride(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setTouchEmulationEnabled(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
@@ -18,25 +18,12 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
disable,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
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 mem_guarantied = true;
|
||||
|
||||
pub fn get_name(self: *parser.DocumentType) ![]const u8 {
|
||||
return try parser.documentTypeGetName(self);
|
||||
switch (action) {
|
||||
.disable => return cmd.sendResult(null, .{}),
|
||||
}
|
||||
|
||||
pub fn get_publicId(self: *parser.DocumentType) ![]const u8 {
|
||||
return try parser.documentTypeGetPublicId(self);
|
||||
}
|
||||
|
||||
pub fn get_systemId(self: *parser.DocumentType) ![]const u8 {
|
||||
return try parser.documentTypeGetSystemId(self);
|
||||
}
|
||||
};
|
||||
}
|
||||
97
src/cdp/domains/input.zig
Normal file
97
src/cdp/domains/input.zig
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Page = @import("../../browser/browser.zig").Page;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
dispatchMouseEvent,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
|
||||
fn dispatchMouseEvent(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
type: Type, // Type of the mouse event.
|
||||
x: f32, // X coordinate of the event relative to the main frame's viewport.
|
||||
y: f32, // Y coordinate of the event relative to the main frame's viewport. 0 refers to the top of the viewport and Y increases as it proceeds towards the bottom of the viewport.
|
||||
// Many optional parameters are not implemented yet, see documentation url.
|
||||
|
||||
const Type = enum {
|
||||
mousePressed,
|
||||
mouseReleased,
|
||||
mouseMoved,
|
||||
mouseWheel,
|
||||
};
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
try cmd.sendResult(null, .{});
|
||||
|
||||
// quickly ignore types we know we don't handle
|
||||
switch (params.type) {
|
||||
.mouseMoved, .mouseWheel => return,
|
||||
else => {},
|
||||
}
|
||||
|
||||
const bc = cmd.browser_context orelse return;
|
||||
const page = bc.session.currentPage() orelse return;
|
||||
|
||||
const mouse_event = Page.MouseEvent{
|
||||
.x = @intFromFloat(@floor(params.x)), // Decimal pixel values are not understood by netsurf or our renderer
|
||||
.y = @intFromFloat(@floor(params.y)), // So we convert them once at intake here. Using floor such that -0.5 becomes -1 and 0.5 becomes 0.
|
||||
.type = switch (params.type) {
|
||||
.mousePressed => .pressed,
|
||||
.mouseReleased => .released,
|
||||
else => unreachable,
|
||||
},
|
||||
};
|
||||
try page.mouseEvent(mouse_event);
|
||||
// result already sent
|
||||
}
|
||||
|
||||
fn clickNavigate(cmd: anytype, uri: std.Uri) !void {
|
||||
const bc = cmd.browser_context.?;
|
||||
|
||||
var url_buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
try uri.writeToStream(.{
|
||||
.scheme = true,
|
||||
.authentication = true,
|
||||
.authority = true,
|
||||
.port = true,
|
||||
.path = true,
|
||||
.query = true,
|
||||
}, url_buf.writer(cmd.arena));
|
||||
const url = url_buf.items;
|
||||
|
||||
try cmd.sendEvent("Page.frameRequestedNavigation", .{
|
||||
.url = url,
|
||||
.frameId = bc.target_id.?,
|
||||
.reason = "anchorClick",
|
||||
.disposition = "currentTab",
|
||||
}, .{ .session_id = bc.session_id.? });
|
||||
|
||||
bc.session.removePage();
|
||||
_ = try bc.session.createPage(null);
|
||||
|
||||
try @import("page.zig").navigateToUrl(cmd, url, false);
|
||||
}
|
||||
@@ -16,30 +16,14 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const generate = @import("generate.zig");
|
||||
const std = @import("std");
|
||||
|
||||
const Console = @import("jsruntime").Console;
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
const DOM = @import("dom/dom.zig");
|
||||
const HTML = @import("html/html.zig");
|
||||
const Events = @import("events/event.zig");
|
||||
const XHR = @import("xhr/xhr.zig");
|
||||
const Storage = @import("storage/storage.zig");
|
||||
const URL = @import("url/url.zig");
|
||||
const Iterators = @import("iterator/iterator.zig");
|
||||
|
||||
pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
||||
|
||||
// Interfaces
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
Console,
|
||||
DOM.Interfaces,
|
||||
Events.Interfaces,
|
||||
HTML.Interfaces,
|
||||
XHR.Interfaces,
|
||||
Storage.Interfaces,
|
||||
URL.Interfaces,
|
||||
Iterators.Interfaces,
|
||||
});
|
||||
|
||||
pub const UserContext = @import("user_context.zig").UserContext;
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
}
|
||||
}
|
||||
29
src/cdp/domains/log.zig
Normal file
29
src/cdp/domains/log.zig
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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 fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
}
|
||||
}
|
||||
31
src/cdp/domains/network.zig
Normal file
31
src/cdp/domains/network.zig
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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 fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
setCacheDisabled,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
.setCacheDisabled => return cmd.sendResult(null, .{}),
|
||||
}
|
||||
}
|
||||
374
src/cdp/domains/page.zig
Normal file
374
src/cdp/domains/page.zig
Normal file
@@ -0,0 +1,374 @@
|
||||
// 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 URL = @import("../../url.zig").URL;
|
||||
const Notification = @import("../../notification.zig").Notification;
|
||||
const Page = @import("../../browser/browser.zig").Page;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
getFrameTree,
|
||||
setLifecycleEventsEnabled,
|
||||
addScriptToEvaluateOnNewDocument,
|
||||
createIsolatedWorld,
|
||||
navigate,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
.getFrameTree => return getFrameTree(cmd),
|
||||
.setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd),
|
||||
.addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd),
|
||||
.createIsolatedWorld => return createIsolatedWorld(cmd),
|
||||
.navigate => return navigate(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
const Frame = struct {
|
||||
id: []const u8,
|
||||
loaderId: []const u8,
|
||||
url: []const u8,
|
||||
domainAndRegistry: []const u8 = "",
|
||||
securityOrigin: []const u8,
|
||||
mimeType: []const u8 = "text/html",
|
||||
adFrameStatus: struct {
|
||||
adFrameType: []const u8 = "none",
|
||||
} = .{},
|
||||
secureContextType: []const u8,
|
||||
crossOriginIsolatedContextType: []const u8 = "NotIsolated",
|
||||
gatedAPIFeatures: [][]const u8 = &[0][]const u8{},
|
||||
};
|
||||
|
||||
fn getFrameTree(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const target_id = bc.target_id orelse return error.TargetNotLoaded;
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.frameTree = .{
|
||||
.frame = Frame{
|
||||
.id = target_id,
|
||||
.loaderId = bc.loader_id,
|
||||
.securityOrigin = bc.security_origin,
|
||||
.url = bc.getURL() orelse "about:blank",
|
||||
.secureContextType = bc.secure_context_type,
|
||||
},
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
|
||||
fn setLifecycleEventsEnabled(cmd: anytype) !void {
|
||||
// const params = (try cmd.params(struct {
|
||||
// enabled: bool,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
bc.page_life_cycle_events = true;
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// TODO: hard coded method
|
||||
// With the command we receive a script we need to store and run for each new document.
|
||||
// Note that the worldName refers to the name given to the isolated world.
|
||||
fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
|
||||
// const params = (try cmd.params(struct {
|
||||
// source: []const u8,
|
||||
// worldName: ?[]const u8 = null,
|
||||
// includeCommandLineAPI: bool = false,
|
||||
// runImmediately: bool = false,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.identifier = "1",
|
||||
}, .{});
|
||||
}
|
||||
|
||||
fn createIsolatedWorld(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
frameId: []const u8,
|
||||
worldName: []const u8,
|
||||
grantUniveralAccess: bool,
|
||||
})) orelse return error.InvalidParams;
|
||||
if (!params.grantUniveralAccess) {
|
||||
std.debug.print("grantUniveralAccess == false is not yet implemented", .{});
|
||||
// When grantUniveralAccess == false and the client attempts to resolve
|
||||
// or otherwise access a DOM or other JS Object from another context that should fail.
|
||||
}
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
try pageCreated(bc, page);
|
||||
const scope = world.scope.?;
|
||||
|
||||
// Create the auxdata json for the contextCreated event
|
||||
// Calling contextCreated will assign a Id to the context and send the contextCreated event
|
||||
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{params.frameId});
|
||||
bc.inspector.contextCreated(scope, world.name, "", aux_data, false);
|
||||
|
||||
return cmd.sendResult(.{ .executionContextId = scope.context.debugContextId() }, .{});
|
||||
}
|
||||
|
||||
fn navigate(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
url: []const u8,
|
||||
// referrer: ?[]const u8 = null,
|
||||
// transitionType: ?[]const u8 = null, // TODO: enum
|
||||
// frameId: ?[]const u8 = null,
|
||||
// referrerPolicy: ?[]const u8 = null, // TODO: enum
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
// didn't create?
|
||||
const target_id = bc.target_id orelse return error.TargetIdNotLoaded;
|
||||
|
||||
// didn't attach?
|
||||
if (bc.session_id == null) {
|
||||
return error.SessionIdNotLoaded;
|
||||
}
|
||||
|
||||
const url = try URL.parse(params.url, "https");
|
||||
|
||||
var page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
bc.loader_id = bc.cdp.loader_id_gen.next();
|
||||
try cmd.sendResult(.{
|
||||
.frameId = target_id,
|
||||
.loaderId = bc.loader_id,
|
||||
}, .{});
|
||||
|
||||
try page.navigate(url, .{
|
||||
.reason = .address_bar,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void {
|
||||
// I don't think it's possible that we get these notifications and don't
|
||||
// have these things setup.
|
||||
std.debug.assert(bc.session.page != null);
|
||||
|
||||
var cdp = bc.cdp;
|
||||
const loader_id = bc.loader_id;
|
||||
const target_id = bc.target_id orelse unreachable;
|
||||
const session_id = bc.session_id orelse unreachable;
|
||||
|
||||
bc.reset();
|
||||
|
||||
if (event.reason == .anchor) {
|
||||
try cdp.sendEvent("Page.frameScheduledNavigation", .{
|
||||
.frameId = target_id,
|
||||
.delay = 0,
|
||||
.reason = "anchorClick",
|
||||
.url = event.url.raw,
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
try cdp.sendEvent("Page.frameRequestedNavigation", .{
|
||||
.frameId = target_id,
|
||||
.reason = "anchorClick",
|
||||
.url = event.url.raw,
|
||||
.disposition = "currentTab",
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
// frameStartedNavigating event
|
||||
try cdp.sendEvent("Page.frameStartedNavigating", .{
|
||||
.frameId = target_id,
|
||||
.url = event.url.raw,
|
||||
.loaderId = loader_id,
|
||||
.navigationType = "differentDocument",
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
// frameStartedLoading event
|
||||
try cdp.sendEvent("Page.frameStartedLoading", .{
|
||||
.frameId = target_id,
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
if (bc.page_life_cycle_events) {
|
||||
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
|
||||
.name = "init",
|
||||
.frameId = target_id,
|
||||
.loaderId = loader_id,
|
||||
.timestamp = event.timestamp,
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
if (event.reason == .anchor) {
|
||||
try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{
|
||||
.frameId = target_id,
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
// When we actually recreated the context we should have the inspector send this event, see: resetContextGroup
|
||||
// Sending this event will tell the client that the context ids they had are invalid and the context shouls be dropped
|
||||
// The client will expect us to send new contextCreated events, such that the client has new id's for the active contexts.
|
||||
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
|
||||
|
||||
var buffer: [512]u8 = undefined;
|
||||
{
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buffer);
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const aux_data = try std.fmt.allocPrint(fba.allocator(), "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
bc.inspector.contextCreated(
|
||||
page.scope,
|
||||
"",
|
||||
try page.origin(fba.allocator()),
|
||||
aux_data,
|
||||
true,
|
||||
);
|
||||
}
|
||||
if (bc.isolated_world) |*isolated_world| {
|
||||
const aux_json = try std.fmt.bufPrint(&buffer, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
// Calling contextCreated will assign a new Id to the context and send the contextCreated event
|
||||
bc.inspector.contextCreated(
|
||||
isolated_world.scope.?,
|
||||
isolated_world.name,
|
||||
"://",
|
||||
aux_json,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pageRemove(bc: anytype) !void {
|
||||
// The main page is going to be removed, we need to remove contexts from other worlds first.
|
||||
if (bc.isolated_world) |*isolated_world| {
|
||||
try isolated_world.removeContext();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pageCreated(bc: anytype, page: *Page) !void {
|
||||
if (bc.isolated_world) |*isolated_world| {
|
||||
// We need to recreate the isolated world context
|
||||
try isolated_world.createContext(page);
|
||||
|
||||
const polyfill = @import("../../browser/polyfill/polyfill.zig");
|
||||
try polyfill.load(bc.arena, isolated_world.scope.?);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !void {
|
||||
// I don't think it's possible that we get these notifications and don't
|
||||
// have these things setup.
|
||||
std.debug.assert(bc.session.page != null);
|
||||
|
||||
var cdp = bc.cdp;
|
||||
const timestamp = event.timestamp;
|
||||
const loader_id = bc.loader_id;
|
||||
const target_id = bc.target_id orelse unreachable;
|
||||
const session_id = bc.session_id orelse unreachable;
|
||||
|
||||
// frameNavigated event
|
||||
try cdp.sendEvent("Page.frameNavigated", .{
|
||||
.type = "Navigation",
|
||||
.frame = Frame{
|
||||
.id = target_id,
|
||||
.url = event.url.raw,
|
||||
.loaderId = bc.loader_id,
|
||||
.securityOrigin = bc.security_origin,
|
||||
.secureContextType = bc.secure_context_type,
|
||||
},
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
// The DOM.documentUpdated event must be send after the frameNavigated one.
|
||||
// chromedp client expects to receive the events is this order.
|
||||
// see https://github.com/chromedp/chromedp/issues/1558
|
||||
try cdp.sendEvent("DOM.documentUpdated", null, .{ .session_id = session_id });
|
||||
|
||||
// domContentEventFired event
|
||||
// TODO: partially hard coded
|
||||
try cdp.sendEvent(
|
||||
"Page.domContentEventFired",
|
||||
.{ .timestamp = timestamp },
|
||||
.{ .session_id = session_id },
|
||||
);
|
||||
|
||||
// lifecycle DOMContentLoaded event
|
||||
// TODO: partially hard coded
|
||||
if (bc.page_life_cycle_events) {
|
||||
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
|
||||
.timestamp = timestamp,
|
||||
.name = "DOMContentLoaded",
|
||||
.frameId = target_id,
|
||||
.loaderId = loader_id,
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
// loadEventFired event
|
||||
try cdp.sendEvent(
|
||||
"Page.loadEventFired",
|
||||
.{ .timestamp = timestamp },
|
||||
.{ .session_id = session_id },
|
||||
);
|
||||
|
||||
// lifecycle DOMContentLoaded event
|
||||
if (bc.page_life_cycle_events) {
|
||||
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
|
||||
.timestamp = timestamp,
|
||||
.name = "load",
|
||||
.frameId = target_id,
|
||||
.loaderId = loader_id,
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
// frameStoppedLoading
|
||||
return cdp.sendEvent("Page.frameStoppedLoading", .{
|
||||
.frameId = target_id,
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
const LifecycleEvent = struct {
|
||||
frameId: []const u8,
|
||||
loaderId: ?[]const u8,
|
||||
name: []const u8,
|
||||
timestamp: u32,
|
||||
};
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "cdp.page: getFrameTree" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Page.getFrameTree", .params = .{ .targetId = "X" } }));
|
||||
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9", .target_id = "TID-3" });
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Page.getFrameTree" });
|
||||
try ctx.expectSentResult(.{
|
||||
.frameTree = .{
|
||||
.frame = .{
|
||||
.id = "TID-3",
|
||||
.loaderId = bc.loader_id,
|
||||
.url = "about:blank",
|
||||
.domainAndRegistry = "",
|
||||
.securityOrigin = bc.security_origin,
|
||||
.mimeType = "text/html",
|
||||
.adFrameStatus = .{
|
||||
.adFrameType = "none",
|
||||
},
|
||||
.secureContextType = bc.secure_context_type,
|
||||
.crossOriginIsolatedContextType = "NotIsolated",
|
||||
.gatedAPIFeatures = [_][]const u8{},
|
||||
},
|
||||
},
|
||||
}, .{ .id = 11 });
|
||||
}
|
||||
}
|
||||
29
src/cdp/domains/performance.zig
Normal file
29
src/cdp/domains/performance.zig
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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 fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
}
|
||||
}
|
||||
90
src/cdp/domains/runtime.zig
Normal file
90
src/cdp/domains/runtime.zig
Normal file
@@ -0,0 +1,90 @@
|
||||
// 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");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
runIfWaitingForDebugger,
|
||||
evaluate,
|
||||
addBinding,
|
||||
callFunctionOn,
|
||||
releaseObject,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.runIfWaitingForDebugger => return cmd.sendResult(null, .{}),
|
||||
else => return sendInspector(cmd, action),
|
||||
}
|
||||
}
|
||||
|
||||
fn sendInspector(cmd: anytype, action: anytype) !void {
|
||||
// save script in file at debug mode
|
||||
if (builtin.mode == .Debug) {
|
||||
try logInspector(cmd, action);
|
||||
}
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
// the result to return is handled directly by the inspector.
|
||||
bc.callInspector(cmd.input.json);
|
||||
}
|
||||
|
||||
fn logInspector(cmd: anytype, action: anytype) !void {
|
||||
const script = switch (action) {
|
||||
.evaluate => blk: {
|
||||
const params = (try cmd.params(struct {
|
||||
expression: []const u8,
|
||||
// contextId: ?u8 = null,
|
||||
// returnByValue: ?bool = null,
|
||||
// awaitPromise: ?bool = null,
|
||||
// userGesture: ?bool = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
break :blk params.expression;
|
||||
},
|
||||
.callFunctionOn => blk: {
|
||||
const params = (try cmd.params(struct {
|
||||
functionDeclaration: []const u8,
|
||||
// objectId: ?[]const u8 = null,
|
||||
// executionContextId: ?u8 = null,
|
||||
// arguments: ?[]struct {
|
||||
// value: ?[]const u8 = null,
|
||||
// objectId: ?[]const u8 = null,
|
||||
// } = null,
|
||||
// returnByValue: ?bool = null,
|
||||
// awaitPromise: ?bool = null,
|
||||
// userGesture: ?bool = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
break :blk params.functionDeclaration;
|
||||
},
|
||||
else => return,
|
||||
};
|
||||
const id = cmd.input.id orelse return error.RequiredId;
|
||||
const name = try std.fmt.allocPrint(cmd.arena, "id_{d}.js", .{id});
|
||||
|
||||
var dir = try std.fs.cwd().makeOpenPath(".zig-cache/tmp", .{});
|
||||
defer dir.close();
|
||||
|
||||
const f = try dir.createFile(name, .{});
|
||||
defer f.close();
|
||||
try f.writeAll(script);
|
||||
}
|
||||
29
src/cdp/domains/security.zig
Normal file
29
src/cdp/domains/security.zig
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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 fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
}
|
||||
}
|
||||
682
src/cdp/domains/target.zig
Normal file
682
src/cdp/domains/target.zig
Normal file
@@ -0,0 +1,682 @@
|
||||
// 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(.cdp);
|
||||
|
||||
// TODO: hard coded IDs
|
||||
const LOADER_ID = "LOADERID42AA389647D702B4D805F49A";
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
attachToTarget,
|
||||
closeTarget,
|
||||
createBrowserContext,
|
||||
createTarget,
|
||||
detachFromTarget,
|
||||
disposeBrowserContext,
|
||||
getBrowserContexts,
|
||||
getTargetInfo,
|
||||
sendMessageToTarget,
|
||||
setAutoAttach,
|
||||
setDiscoverTargets,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.attachToTarget => return attachToTarget(cmd),
|
||||
.closeTarget => return closeTarget(cmd),
|
||||
.createBrowserContext => return createBrowserContext(cmd),
|
||||
.createTarget => return createTarget(cmd),
|
||||
.detachFromTarget => return detachFromTarget(cmd),
|
||||
.disposeBrowserContext => return disposeBrowserContext(cmd),
|
||||
.getBrowserContexts => return getBrowserContexts(cmd),
|
||||
.getTargetInfo => return getTargetInfo(cmd),
|
||||
.sendMessageToTarget => return sendMessageToTarget(cmd),
|
||||
.setAutoAttach => return setAutoAttach(cmd),
|
||||
.setDiscoverTargets => return setDiscoverTargets(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
fn getBrowserContexts(cmd: anytype) !void {
|
||||
var browser_context_ids: []const []const u8 = undefined;
|
||||
if (cmd.browser_context) |bc| {
|
||||
browser_context_ids = &.{bc.id};
|
||||
} else {
|
||||
browser_context_ids = &.{};
|
||||
}
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.browserContextIds = browser_context_ids,
|
||||
}, .{ .include_session_id = false });
|
||||
}
|
||||
|
||||
fn createBrowserContext(cmd: anytype) !void {
|
||||
const bc = cmd.createBrowserContext() catch |err| switch (err) {
|
||||
error.AlreadyExists => return cmd.sendError(-32000, "Cannot have more than one browser context at a time"),
|
||||
else => return err,
|
||||
};
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.browserContextId = bc.id,
|
||||
}, .{});
|
||||
}
|
||||
|
||||
fn disposeBrowserContext(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
browserContextId: []const u8,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
if (cmd.cdp.disposeBrowserContext(params.browserContextId) == false) {
|
||||
return cmd.sendError(-32602, "No browser context with the given id found");
|
||||
}
|
||||
try cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn createTarget(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
// url: []const u8,
|
||||
// width: ?u64 = null,
|
||||
// height: ?u64 = null,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
// enableBeginFrameControl: bool = false,
|
||||
// newWindow: bool = false,
|
||||
// background: bool = false,
|
||||
// forTab: ?bool = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse cmd.createBrowserContext() catch |err| switch (err) {
|
||||
error.AlreadyExists => unreachable,
|
||||
else => return err,
|
||||
};
|
||||
|
||||
if (bc.target_id != null) {
|
||||
return error.TargetAlreadyLoaded;
|
||||
}
|
||||
if (params.browserContextId) |param_browser_context_id| {
|
||||
if (std.mem.eql(u8, param_browser_context_id, bc.id) == false) {
|
||||
return error.UnknownBrowserContextId;
|
||||
}
|
||||
}
|
||||
|
||||
// if target_id is null, we should never have a page
|
||||
std.debug.assert(bc.session.page == null);
|
||||
|
||||
// if target_id is null, we should never have a session_id
|
||||
std.debug.assert(bc.session_id == null);
|
||||
|
||||
const target_id = cmd.cdp.target_id_gen.next();
|
||||
|
||||
bc.target_id = target_id;
|
||||
|
||||
var page = try bc.session.createPage();
|
||||
{
|
||||
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
bc.inspector.contextCreated(
|
||||
page.scope,
|
||||
"",
|
||||
try page.origin(cmd.arena),
|
||||
aux_data,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
// change CDP state
|
||||
bc.security_origin = "://";
|
||||
bc.secure_context_type = "InsecureScheme";
|
||||
bc.loader_id = LOADER_ID;
|
||||
|
||||
// send targetCreated event
|
||||
// TODO: should this only be sent when Target.setDiscoverTargets
|
||||
// has been enabled?
|
||||
try cmd.sendEvent("Target.targetCreated", .{
|
||||
.targetInfo = TargetInfo{
|
||||
.attached = false,
|
||||
.targetId = target_id,
|
||||
.title = "about:blank",
|
||||
.browserContextId = bc.id,
|
||||
.url = "about:blank",
|
||||
},
|
||||
}, .{});
|
||||
|
||||
// attach to the target only if auto attach is set.
|
||||
if (cmd.cdp.target_auto_attach) {
|
||||
try doAttachtoTarget(cmd, target_id);
|
||||
}
|
||||
|
||||
try cmd.sendResult(.{
|
||||
.targetId = target_id,
|
||||
}, .{});
|
||||
}
|
||||
|
||||
fn attachToTarget(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
targetId: []const u8,
|
||||
flatten: bool = true,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const target_id = bc.target_id orelse return error.TargetNotLoaded;
|
||||
if (std.mem.eql(u8, target_id, params.targetId) == false) {
|
||||
return error.UnknownTargetId;
|
||||
}
|
||||
|
||||
if (bc.session_id != null) {
|
||||
return error.SessionAlreadyLoaded;
|
||||
}
|
||||
|
||||
try doAttachtoTarget(cmd, target_id);
|
||||
|
||||
return cmd.sendResult(
|
||||
.{ .sessionId = bc.session_id },
|
||||
.{ .include_session_id = false },
|
||||
);
|
||||
}
|
||||
|
||||
fn closeTarget(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
targetId: []const u8,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const target_id = bc.target_id orelse return error.TargetNotLoaded;
|
||||
if (std.mem.eql(u8, target_id, params.targetId) == false) {
|
||||
return error.UnknownTargetId;
|
||||
}
|
||||
|
||||
// can't be null if we have a target_id
|
||||
std.debug.assert(bc.session.page != null);
|
||||
|
||||
try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false });
|
||||
|
||||
// could be null, created but never attached
|
||||
if (bc.session_id) |session_id| {
|
||||
// Inspector.detached event
|
||||
try cmd.sendEvent("Inspector.detached", .{
|
||||
.reason = "Render process gone.",
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
// detachedFromTarget event
|
||||
try cmd.sendEvent("Target.detachedFromTarget", .{
|
||||
.targetId = target_id,
|
||||
.sessionId = session_id,
|
||||
.reason = "Render process gone.",
|
||||
}, .{});
|
||||
|
||||
bc.session_id = null;
|
||||
}
|
||||
|
||||
bc.session.removePage();
|
||||
if (bc.isolated_world) |*world| {
|
||||
world.deinit();
|
||||
bc.isolated_world = null;
|
||||
}
|
||||
bc.target_id = null;
|
||||
}
|
||||
|
||||
fn getTargetInfo(cmd: anytype) !void {
|
||||
const Params = struct {
|
||||
targetId: ?[]const u8 = null,
|
||||
};
|
||||
const params = (try cmd.params(Params)) orelse Params{};
|
||||
|
||||
if (params.targetId) |param_target_id| {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const target_id = bc.target_id orelse return error.TargetNotLoaded;
|
||||
if (std.mem.eql(u8, target_id, param_target_id) == false) {
|
||||
return error.UnknownTargetId;
|
||||
}
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.targetInfo = TargetInfo{
|
||||
.targetId = target_id,
|
||||
.type = "page",
|
||||
.title = "",
|
||||
.url = "",
|
||||
.attached = true,
|
||||
.canAccessOpener = false,
|
||||
},
|
||||
}, .{ .include_session_id = false });
|
||||
}
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.targetInfo = TargetInfo{
|
||||
.targetId = "TID-STARTUP-B",
|
||||
.type = "browser",
|
||||
.title = "",
|
||||
.url = "",
|
||||
.attached = true,
|
||||
.canAccessOpener = false,
|
||||
},
|
||||
}, .{ .include_session_id = false });
|
||||
}
|
||||
|
||||
fn sendMessageToTarget(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
message: []const u8,
|
||||
sessionId: []const u8,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
if (bc.target_id == null) {
|
||||
return error.TargetNotLoaded;
|
||||
}
|
||||
|
||||
std.debug.assert(bc.session_id != null);
|
||||
if (std.mem.eql(u8, bc.session_id.?, params.sessionId) == false) {
|
||||
// Is this right? Is the params.sessionId meant to be the active
|
||||
// sessionId? What else could it be? We have no other session_id.
|
||||
return error.UnknownSessionId;
|
||||
}
|
||||
|
||||
const Capture = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
buf: std.ArrayListUnmanaged(u8),
|
||||
|
||||
pub fn sendJSON(self: *@This(), message: anytype) !void {
|
||||
return std.json.stringify(message, .{
|
||||
.emit_null_optional_fields = false,
|
||||
}, self.buf.writer(self.allocator));
|
||||
}
|
||||
};
|
||||
|
||||
var capture = Capture{
|
||||
.buf = .{},
|
||||
.allocator = cmd.arena,
|
||||
};
|
||||
|
||||
cmd.cdp.dispatch(cmd.arena, &capture, params.message) catch |err| {
|
||||
log.err("send message {d} ({s}): {any}", .{ cmd.input.id orelse -1, params.message, err });
|
||||
return err;
|
||||
};
|
||||
|
||||
try cmd.sendEvent("Target.receivedMessageFromTarget", .{
|
||||
.message = capture.buf.items,
|
||||
.sessionId = params.sessionId,
|
||||
}, .{});
|
||||
}
|
||||
|
||||
fn detachFromTarget(cmd: anytype) !void {
|
||||
// TODO check if sessionId/targetId match.
|
||||
// const params = (try cmd.params(struct {
|
||||
// sessionId: ?[]const u8,
|
||||
// targetId: ?[]const u8,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
if (cmd.browser_context) |bc| {
|
||||
bc.session_id = null;
|
||||
// TODO should we send a Target.detachedFromTarget event?
|
||||
}
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setDiscoverTargets(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn setAutoAttach(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
autoAttach: bool,
|
||||
waitForDebuggerOnStart: bool,
|
||||
flatten: bool = true,
|
||||
// filter: ?[]TargetFilter = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
// set a flag to send Target.attachedToTarget events
|
||||
cmd.cdp.target_auto_attach = params.autoAttach;
|
||||
|
||||
try cmd.sendResult(null, .{});
|
||||
|
||||
if (cmd.cdp.target_auto_attach == false) {
|
||||
// detach from all currently attached targets.
|
||||
if (cmd.browser_context) |bc| {
|
||||
bc.session_id = null;
|
||||
// TODO should we send a Target.detachedFromTarget event?
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// autoAttach is set to true, we must attach to all existing targets.
|
||||
if (cmd.browser_context) |bc| {
|
||||
if (bc.target_id == null) {
|
||||
// hasn't attached yet
|
||||
const target_id = cmd.cdp.target_id_gen.next();
|
||||
try doAttachtoTarget(cmd, target_id);
|
||||
bc.target_id = target_id;
|
||||
}
|
||||
// should we send something here?
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a hack. Puppeteer, and probably others, expect the Browser to
|
||||
// automatically started creating targets. Things like an empty tab, or
|
||||
// a blank page. And they block until this happens. So we send an event
|
||||
// telling them that they've been attached to our Broswer. Hopefully, the
|
||||
// first thing they'll do is create a real BrowserContext and progress from
|
||||
// there.
|
||||
// This hack requires the main cdp dispatch handler to special case
|
||||
// messages from this "STARTUP" session.
|
||||
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
|
||||
.sessionId = "STARTUP",
|
||||
.targetInfo = TargetInfo{
|
||||
.type = "page",
|
||||
.targetId = "TID-STARTUP-P",
|
||||
.title = "New Private Tab",
|
||||
.url = "chrome://newtab/",
|
||||
.browserContextId = "BID-STARTUP",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
|
||||
fn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void {
|
||||
const bc = cmd.browser_context.?;
|
||||
std.debug.assert(bc.session_id == null);
|
||||
const session_id = cmd.cdp.session_id_gen.next();
|
||||
|
||||
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
|
||||
.sessionId = session_id,
|
||||
.targetInfo = TargetInfo{
|
||||
.targetId = target_id,
|
||||
.title = "about:blank",
|
||||
.url = "chrome://newtab/",
|
||||
.browserContextId = bc.id,
|
||||
},
|
||||
}, .{});
|
||||
|
||||
bc.session_id = session_id;
|
||||
}
|
||||
|
||||
const AttachToTarget = struct {
|
||||
sessionId: []const u8,
|
||||
targetInfo: TargetInfo,
|
||||
waitingForDebugger: bool = false,
|
||||
};
|
||||
|
||||
const TargetInfo = struct {
|
||||
url: []const u8,
|
||||
title: []const u8,
|
||||
targetId: []const u8,
|
||||
attached: bool = true,
|
||||
type: []const u8 = "page",
|
||||
canAccessOpener: bool = false,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "cdp.target: getBrowserContexts" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
// {
|
||||
// // no browser context
|
||||
// try ctx.processMessage(.{.id = 4, .method = "Target.getBrowserContexts"});
|
||||
|
||||
// try ctx.expectSentResult(.{
|
||||
// .browserContextIds = &.{},
|
||||
// }, .{ .id = 4, .session_id = null });
|
||||
// }
|
||||
|
||||
{
|
||||
// with a browser context
|
||||
_ = try ctx.loadBrowserContext(.{ .id = "BID-X" });
|
||||
try ctx.processMessage(.{ .id = 5, .method = "Target.getBrowserContexts" });
|
||||
|
||||
try ctx.expectSentResult(.{
|
||||
.browserContextIds = &.{"BID-X"},
|
||||
}, .{ .id = 5, .session_id = null });
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: createBrowserContext" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 4, .method = "Target.createBrowserContext" });
|
||||
try ctx.expectSentResult(.{
|
||||
.browserContextId = ctx.cdp().browser_context.?.id,
|
||||
}, .{ .id = 4, .session_id = null });
|
||||
}
|
||||
|
||||
{
|
||||
// we already have one now
|
||||
try ctx.processMessage(.{ .id = 5, .method = "Target.createBrowserContext" });
|
||||
try ctx.expectSentError(-32000, "Cannot have more than one browser context at a time", .{ .id = 5 });
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: disposeBrowserContext" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try testing.expectError(error.InvalidParams, ctx.processMessage(.{ .id = 7, .method = "Target.disposeBrowserContext" }));
|
||||
try ctx.expectSentError(-31998, "InvalidParams", .{ .id = 7 });
|
||||
}
|
||||
|
||||
{
|
||||
try ctx.processMessage(.{
|
||||
.id = 8,
|
||||
.method = "Target.disposeBrowserContext",
|
||||
.params = .{ .browserContextId = "BID-10" },
|
||||
});
|
||||
try ctx.expectSentError(-32602, "No browser context with the given id found", .{ .id = 8 });
|
||||
}
|
||||
|
||||
{
|
||||
_ = try ctx.loadBrowserContext(.{ .id = "BID-20" });
|
||||
try ctx.processMessage(.{
|
||||
.id = 9,
|
||||
.method = "Target.disposeBrowserContext",
|
||||
.params = .{ .browserContextId = "BID-20" },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 9 });
|
||||
try testing.expectEqual(null, ctx.cdp().browser_context);
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: createTarget" {
|
||||
{
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about/blank" } });
|
||||
|
||||
// should create a browser context
|
||||
const bc = ctx.cdp().browser_context.?;
|
||||
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});
|
||||
}
|
||||
|
||||
{
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
// active auto attach to get the Target.attachedToTarget event.
|
||||
try ctx.processMessage(.{ .id = 9, .method = "Target.setAutoAttach", .params = .{ .autoAttach = true, .waitForDebuggerOnStart = false } });
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about/blank" } });
|
||||
|
||||
// should create a browser context
|
||||
const bc = ctx.cdp().browser_context.?;
|
||||
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});
|
||||
try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = bc.session_id.?, .targetInfo = .{ .url = "chrome://newtab/", .title = "about:blank", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});
|
||||
}
|
||||
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try testing.expectError(error.UnknownBrowserContextId, ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-8" } }));
|
||||
try ctx.expectSentError(-31998, "UnknownBrowserContextId", .{ .id = 10 });
|
||||
}
|
||||
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
||||
try testing.expectEqual(true, bc.target_id != null);
|
||||
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
|
||||
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: closeTarget" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "X" } }));
|
||||
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
// pretend we createdTarget first
|
||||
_ = try bc.session.createPage();
|
||||
bc.target_id = "TID-A";
|
||||
{
|
||||
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 });
|
||||
}
|
||||
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Target.closeTarget", .params = .{ .targetId = "TID-A" } });
|
||||
try ctx.expectSentResult(.{ .success = true }, .{ .id = 11 });
|
||||
try testing.expectEqual(null, bc.session.page);
|
||||
try testing.expectEqual(null, bc.target_id);
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: attachToTarget" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "X" } }));
|
||||
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
// pretend we createdTarget first
|
||||
_ = try bc.session.createPage();
|
||||
bc.target_id = "TID-B";
|
||||
{
|
||||
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 });
|
||||
}
|
||||
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-B" } });
|
||||
const session_id = bc.session_id.?;
|
||||
try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 });
|
||||
try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = session_id, .targetInfo = .{ .url = "chrome://newtab/", .title = "about:blank", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: getTargetInfo" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 9, .method = "Target.getTargetInfo" });
|
||||
try ctx.expectSentResult(.{
|
||||
.targetInfo = .{
|
||||
.type = "browser",
|
||||
.title = "",
|
||||
.url = "",
|
||||
.attached = true,
|
||||
.canAccessOpener = false,
|
||||
},
|
||||
}, .{ .id = 9 });
|
||||
}
|
||||
|
||||
{
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "X" } }));
|
||||
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
// pretend we createdTarget first
|
||||
_ = try bc.session.createPage();
|
||||
bc.target_id = "TID-A";
|
||||
{
|
||||
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 });
|
||||
}
|
||||
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-A" } });
|
||||
try ctx.expectSentResult(.{
|
||||
.targetInfo = .{
|
||||
.targetId = "TID-A",
|
||||
.type = "page",
|
||||
.title = "",
|
||||
.url = "",
|
||||
.attached = true,
|
||||
.canAccessOpener = false,
|
||||
},
|
||||
}, .{ .id = 11 });
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: issue#474: attach to just created target" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
||||
try testing.expectEqual(true, bc.target_id != null);
|
||||
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
|
||||
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = bc.target_id.? } });
|
||||
const session_id = bc.session_id.?;
|
||||
try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 });
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: detachFromTarget" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
||||
try testing.expectEqual(true, bc.target_id != null);
|
||||
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
|
||||
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = bc.target_id.? } });
|
||||
try ctx.expectSentResult(.{ .sessionId = bc.session_id.? }, .{ .id = 11 });
|
||||
|
||||
try ctx.processMessage(.{ .id = 12, .method = "Target.detachFromTarget", .params = .{ .targetId = bc.target_id.? } });
|
||||
try testing.expectEqual(null, bc.session_id);
|
||||
try ctx.expectSentResult(null, .{ .id = 12 });
|
||||
|
||||
try ctx.processMessage(.{ .id = 13, .method = "Target.attachToTarget", .params = .{ .targetId = bc.target_id.? } });
|
||||
try ctx.expectSentResult(.{ .sessionId = bc.session_id.? }, .{ .id = 13 });
|
||||
}
|
||||
}
|
||||
@@ -1,123 +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 server = @import("../server.zig");
|
||||
const Ctx = server.Ctx;
|
||||
const cdp = @import("cdp.zig");
|
||||
const result = cdp.result;
|
||||
const stringify = cdp.stringify;
|
||||
const IncomingMessage = @import("msg.zig").IncomingMessage;
|
||||
const Input = @import("msg.zig").Input;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
const Methods = enum {
|
||||
setEmulatedMedia,
|
||||
setFocusEmulationEnabled,
|
||||
setDeviceMetricsOverride,
|
||||
setTouchEmulationEnabled,
|
||||
};
|
||||
|
||||
pub fn emulation(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
action: []const u8,
|
||||
ctx: *Ctx,
|
||||
) ![]const u8 {
|
||||
const method = std.meta.stringToEnum(Methods, action) orelse
|
||||
return error.UnknownMethod;
|
||||
return switch (method) {
|
||||
.setEmulatedMedia => setEmulatedMedia(alloc, msg, ctx),
|
||||
.setFocusEmulationEnabled => setFocusEmulationEnabled(alloc, msg, ctx),
|
||||
.setDeviceMetricsOverride => setDeviceMetricsOverride(alloc, msg, ctx),
|
||||
.setTouchEmulationEnabled => setTouchEmulationEnabled(alloc, msg, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
const MediaFeature = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
};
|
||||
|
||||
// TODO: noop method
|
||||
fn setEmulatedMedia(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
|
||||
// input
|
||||
const Params = struct {
|
||||
media: ?[]const u8 = null,
|
||||
features: ?[]MediaFeature = null,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setEmulatedMedia" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setFocusEmulationEnabled(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const Params = struct {
|
||||
enabled: bool,
|
||||
};
|
||||
const input = try Input(Params).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setFocusEmulationEnabled" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setDeviceMetricsOverride(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
// input
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setDeviceMetricsOverride" });
|
||||
|
||||
// output
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setTouchEmulationEnabled(
|
||||
alloc: std.mem.Allocator,
|
||||
msg: *IncomingMessage,
|
||||
_: *Ctx,
|
||||
) ![]const u8 {
|
||||
const input = try Input(void).get(alloc, msg);
|
||||
defer input.deinit();
|
||||
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setTouchEmulationEnabled" });
|
||||
|
||||
return result(alloc, input.id, null, null, input.sessionId);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user