mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-29 16:10:04 +00:00
Compare commits
1756 Commits
v0.2.0
...
mcp-protoc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81d4bdb157 | ||
|
|
cf5e4d7d1e | ||
|
|
03ed45637a | ||
|
|
9068fe718e | ||
|
|
5369d25213 | ||
|
|
649d8d1024 | ||
|
|
15d60d845a | ||
|
|
c4b837b598 | ||
|
|
54391238c9 | ||
|
|
d33edc5697 | ||
|
|
16ca8d4b14 | ||
|
|
707ffb4893 | ||
|
|
4782b37216 | ||
|
|
ce197256dd | ||
|
|
e6d644998a | ||
|
|
67bd555e75 | ||
|
|
a10e533701 | ||
|
|
0065677273 | ||
|
|
226d9bfc6f | ||
|
|
2e65ae632e | ||
|
|
ea422075c7 | ||
|
|
1d54e6944b | ||
|
|
de32e5cf34 | ||
|
|
c8d8ca5e94 | ||
|
|
7f2139f612 | ||
|
|
da0828620f | ||
|
|
cdd33621e3 | ||
|
|
8001709506 | ||
|
|
a0ae6b4c92 | ||
|
|
fdf7f5267a | ||
|
|
88e0b39d6b | ||
|
|
f95396a487 | ||
|
|
d02d05b246 | ||
|
|
7b2d817d0e | ||
|
|
7e778a17d6 | ||
|
|
a0dd14aaad | ||
|
|
d447d1e3c7 | ||
|
|
8684d35394 | ||
|
|
e243f96988 | ||
|
|
7ea8f3f766 | ||
|
|
5e6082b5e9 | ||
|
|
1befd9a5e8 | ||
|
|
e103ce0f39 | ||
|
|
14fa2da2ad | ||
|
|
28cc60adb0 | ||
|
|
96d24b5dc6 | ||
|
|
c14a9ad986 | ||
|
|
679f2104f4 | ||
|
|
c6b0c75106 | ||
|
|
93485c1ef3 | ||
|
|
0324d5c232 | ||
|
|
0588cc374d | ||
|
|
a75c0cf08d | ||
|
|
2812b8f07c | ||
|
|
e2afbec29d | ||
|
|
a45f9cb810 | ||
|
|
cf641ed458 | ||
|
|
0fc959dcc5 | ||
|
|
077376ea04 | ||
|
|
6ed8d1d201 | ||
|
|
5207bd4202 | ||
|
|
11ed95290b | ||
|
|
a876275828 | ||
|
|
e83b8aa36d | ||
|
|
179f9c1169 | ||
|
|
ca41bb5fa2 | ||
|
|
9c37961042 | ||
|
|
0dd0495ab8 | ||
|
|
c9fa76da0c | ||
|
|
7718184e22 | ||
|
|
b81b41cbf0 | ||
|
|
3a0cead03a | ||
|
|
92ce6a916a | ||
|
|
130bf7ba11 | ||
|
|
2e40354a3a | ||
|
|
3074bde2f3 | ||
|
|
ed9f5aae2e | ||
|
|
8e315e551a | ||
|
|
bad690da65 | ||
|
|
ae080f32eb | ||
|
|
c5c1d1f2f8 | ||
|
|
eb18dc89f6 | ||
|
|
afb0c29243 | ||
|
|
267eee9693 | ||
|
|
39352a6bda | ||
|
|
0838b510f8 | ||
|
|
b19f30d865 | ||
|
|
35be9f897f | ||
|
|
d517488158 | ||
|
|
fee8fe7830 | ||
|
|
428190aecc | ||
|
|
61dabdedec | ||
|
|
dfd9f216bd | ||
|
|
567cd97312 | ||
|
|
0bfe00bbb7 | ||
|
|
260768463b | ||
|
|
fd96cd6eb9 | ||
|
|
25a7b5b778 | ||
|
|
d4bcfa974f | ||
|
|
c91eac17d0 | ||
|
|
5c79961bb7 | ||
|
|
a0c200bc49 | ||
|
|
9ea39e1c34 | ||
|
|
f7125d2bf3 | ||
|
|
b163d9709b | ||
|
|
5453630955 | ||
|
|
8ada67637f | ||
|
|
5972630e95 | ||
|
|
58c18114a5 | ||
|
|
a94b0bec93 | ||
|
|
ff0fbb6b41 | ||
|
|
797cae2ef8 | ||
|
|
433c03c709 | ||
|
|
4d3e9feaf4 | ||
|
|
5700e214bf | ||
|
|
88d40a7dcd | ||
|
|
ff209f5adf | ||
|
|
8ad092a960 | ||
|
|
0fcdc1d194 | ||
|
|
60c2359fdd | ||
|
|
08c8ba72f5 | ||
|
|
cfa4201532 | ||
|
|
cb02eb000e | ||
|
|
23334edc05 | ||
|
|
8dbe22a01a | ||
|
|
80235e2ddd | ||
|
|
2abed9fe75 | ||
|
|
35551ac84e | ||
|
|
c3a2318eca | ||
|
|
a6e801be59 | ||
|
|
0bbe25ab5e | ||
|
|
c37286f845 | ||
|
|
34079913a3 | ||
|
|
4f1b499d0f | ||
|
|
c9bc370d6a | ||
|
|
4b29823a5b | ||
|
|
a69a22ccd7 | ||
|
|
a6d2ec7610 | ||
|
|
ad83c6e70b | ||
|
|
c2a0d4c0b2 | ||
|
|
9e7f0b4776 | ||
|
|
e3085cb0f1 | ||
|
|
4e2e895cd9 | ||
|
|
c1fc2b1301 | ||
|
|
324e5eb152 | ||
|
|
df4df64066 | ||
|
|
c557a0fd87 | ||
|
|
a869f92e9a | ||
|
|
4d28265839 | ||
|
|
78c6def2b1 | ||
|
|
87a0690776 | ||
|
|
fbc71d6ff7 | ||
|
|
e10ccd846d | ||
|
|
384b2f7614 | ||
|
|
fdc79af55c | ||
|
|
e9bed18cd8 | ||
|
|
30f387d361 | ||
|
|
e7d272eaf6 | ||
|
|
00d06dbe8c | ||
|
|
7b104789aa | ||
|
|
2107ade3a5 | ||
|
|
e60424a402 | ||
|
|
107da49f81 | ||
|
|
3e309da69f | ||
|
|
370ae2b85c | ||
|
|
6008187c78 | ||
|
|
598fa254cf | ||
|
|
8526770e9f | ||
|
|
21325ca9be | ||
|
|
b5b012bd5d | ||
|
|
b4b7a7d58a | ||
|
|
a5378feb1d | ||
|
|
b5d3d37f16 | ||
|
|
9b02e4963b | ||
|
|
a865b86fa5 | ||
|
|
de28d14aff | ||
|
|
2d91acbd14 | ||
|
|
88681b1fdb | ||
|
|
1feb121ba7 | ||
|
|
35cdc3c348 | ||
|
|
1353f76bf1 | ||
|
|
3e2be5b317 | ||
|
|
448eca0c32 | ||
|
|
5404ca723c | ||
|
|
e56ffe4b60 | ||
|
|
02d05ae464 | ||
|
|
a74e97854d | ||
|
|
6925fc3f70 | ||
|
|
84557cb4e6 | ||
|
|
4cdc24326a | ||
|
|
cf46f0097a | ||
|
|
f1293b7346 | ||
|
|
d94fd2a43b | ||
|
|
8c5e737669 | ||
|
|
fb29a1c5bf | ||
|
|
94190f93af | ||
|
|
93e239f682 | ||
|
|
a4cb5031d1 | ||
|
|
a2e59af44c | ||
|
|
00c962bdd8 | ||
|
|
1fa87442b8 | ||
|
|
ac5400696a | ||
|
|
5062273b7a | ||
|
|
9c2393351d | ||
|
|
f0cfe3ffc8 | ||
|
|
f70865e174 | ||
|
|
615fcffb99 | ||
|
|
13b746f9e4 | ||
|
|
e90fce4c55 | ||
|
|
59175437b5 | ||
|
|
e950384b9b | ||
|
|
78440350dc | ||
|
|
f435297949 | ||
|
|
54d1563cf3 | ||
|
|
38e9f86088 | ||
|
|
d9c5f56500 | ||
|
|
6c5733bba3 | ||
|
|
b8f1622b52 | ||
|
|
f36499b806 | ||
|
|
fa1dd5237d | ||
|
|
2b9d5fd4d9 | ||
|
|
2dbd32d120 | ||
|
|
1695ea81d2 | ||
|
|
b7bf86fd85 | ||
|
|
94d8f90a96 | ||
|
|
964fa0a8aa | ||
|
|
db01158d2d | ||
|
|
e997f8317e | ||
|
|
b9bef22bbf | ||
|
|
b2a996e5c7 | ||
|
|
a88c21cdb5 | ||
|
|
e2be8525c4 | ||
|
|
c15afa23ca | ||
|
|
7a7c4b9f49 | ||
|
|
f594b033bf | ||
|
|
10e379e4fb | ||
|
|
c1bb27c450 | ||
|
|
dda5e2c542 | ||
|
|
edd0c5c83f | ||
|
|
c6861829c3 | ||
|
|
e14c8b3025 | ||
|
|
5bc00c595c | ||
|
|
db5fb40de0 | ||
|
|
4e6a357e6e | ||
|
|
6cf515151d | ||
|
|
bf6e4cf3a6 | ||
|
|
60936baa96 | ||
|
|
c29f72a7e8 | ||
|
|
d4427e4370 | ||
|
|
b85ec04175 | ||
|
|
da05ba0eb7 | ||
|
|
414a68abeb | ||
|
|
52455b732b | ||
|
|
ba71268eb3 | ||
|
|
694aac5ce8 | ||
|
|
cbab0b712a | ||
|
|
1aee3db521 | ||
|
|
e29778d72b | ||
|
|
f634c9843d | ||
|
|
e1e45d1c5d | ||
|
|
09327c3897 | ||
|
|
ff288c8aa2 | ||
|
|
e1b14a6833 | ||
|
|
015edc3848 | ||
|
|
bd2406f803 | ||
|
|
3c29e7dbd4 | ||
|
|
586413357e | ||
|
|
9a055a61a6 | ||
|
|
5fb561dc9c | ||
|
|
b14ae02548 | ||
|
|
51fb08e6aa | ||
|
|
a6d699ad5d | ||
|
|
8372b45cc5 | ||
|
|
1739ae6b9a | ||
|
|
ba62150f7a | ||
|
|
8143a61955 | ||
|
|
e16c479781 | ||
|
|
c0c4e26d63 | ||
|
|
b252aa71d0 | ||
|
|
9ef8d9c189 | ||
|
|
9f27416603 | ||
|
|
0729f4a03a | ||
|
|
21f7b95db9 | ||
|
|
4125a5aa1e | ||
|
|
6d0dc6cb1e | ||
|
|
0675c23217 | ||
|
|
d0e6a1f5bb | ||
|
|
91afe08235 | ||
|
|
041d9d41fb | ||
|
|
7009fb5899 | ||
|
|
d2003c7c9a | ||
|
|
ce002b999c | ||
|
|
5b1056862a | ||
|
|
cc4ac99b4a | ||
|
|
46df341506 | ||
|
|
b698e2d078 | ||
|
|
5cc5e513dd | ||
|
|
e048b0372f | ||
|
|
d7aaa1c870 | ||
|
|
463aac9b59 | ||
|
|
d9cdd78138 | ||
|
|
44a83c0e1c | ||
|
|
96f24a2662 | ||
|
|
5d2801c652 | ||
|
|
deb08b7880 | ||
|
|
96e5054ffc | ||
|
|
c9753a690d | ||
|
|
27aaf46630 | ||
|
|
84190e1e06 | ||
|
|
b0b1f755ea | ||
|
|
fcf1d30c77 | ||
|
|
3c532e5aef | ||
|
|
3efcb2705d | ||
|
|
c25f389e91 | ||
|
|
533f4075a3 | ||
|
|
f508d37426 | ||
|
|
548c6eeb7a | ||
|
|
c8265f4807 | ||
|
|
a74e46debf | ||
|
|
1ceaabe69f | ||
|
|
91a2441ed8 | ||
|
|
2ecbc833a9 | ||
|
|
dac456d98c | ||
|
|
422320d9ac | ||
|
|
18b635936c | ||
|
|
7b2895ef08 | ||
|
|
b09e9f7398 | ||
|
|
ac651328c3 | ||
|
|
0380df1cb4 | ||
|
|
21421d5b53 | ||
|
|
80c309aa69 | ||
|
|
f5bc7310b1 | ||
|
|
21e9967a8a | ||
|
|
32f450f803 | ||
|
|
1972142703 | ||
|
|
b10d866e4b | ||
|
|
b373fb4a42 | ||
|
|
43a70272c5 | ||
|
|
ddd34dc57b | ||
|
|
265c5aba2e | ||
|
|
21fc6d1cf6 | ||
|
|
1a7fe6129c | ||
|
|
37462a16c5 | ||
|
|
323ec0046c | ||
|
|
dc7c6984fb | ||
|
|
92f7248a16 | ||
|
|
1ec3e156fb | ||
|
|
1121bed49b | ||
|
|
0eb43fb530 | ||
|
|
1f50dc38c3 | ||
|
|
a9d044ec10 | ||
|
|
1bdf464ef2 | ||
|
|
a70da0d176 | ||
|
|
8c52b8357c | ||
|
|
0243c6b450 | ||
|
|
f7071447cb | ||
|
|
c038bfafa1 | ||
|
|
4d60f56e66 | ||
|
|
56d3cf51e8 | ||
|
|
3013e3a9e6 | ||
|
|
fe9b2e672b | ||
|
|
3e9fa4ca47 | ||
|
|
a2e66f85a1 | ||
|
|
a9b9cf14c3 | ||
|
|
d4b941cf30 | ||
|
|
4b6bf29b83 | ||
|
|
a8b147dfc0 | ||
|
|
65627c1296 | ||
|
|
3dcdaa0a9b | ||
|
|
5bc00045c7 | ||
|
|
93ea95af24 | ||
|
|
f754773bf6 | ||
|
|
f0c9c262ca | ||
|
|
42bb2f3c58 | ||
|
|
3fde349b9f | ||
|
|
55a9976d46 | ||
|
|
66a86541d1 | ||
|
|
bc19079dad | ||
|
|
351e44343d | ||
|
|
e362a9cbc3 | ||
|
|
e2563e57f2 | ||
|
|
df5e978247 | ||
|
|
68337a6989 | ||
|
|
bf6dbedbe4 | ||
|
|
a204f40968 | ||
|
|
fe3faa0a5a | ||
|
|
39d5a25258 | ||
|
|
f4044230fd | ||
|
|
4d6d8d9a83 | ||
|
|
c4176a282f | ||
|
|
1352839472 | ||
|
|
535128da71 | ||
|
|
099550dddc | ||
|
|
7fe26bc966 | ||
|
|
cc6587d6e5 | ||
|
|
8b310ce993 | ||
|
|
be8ba53263 | ||
|
|
043d48d1c7 | ||
|
|
e8fe80189b | ||
|
|
0e48f317cb | ||
|
|
867745c71d | ||
|
|
a1a7919f74 | ||
|
|
c3de47de90 | ||
|
|
dd35bdfeb4 | ||
|
|
07c3aec34f | ||
|
|
bce3e8f7c6 | ||
|
|
ba9777e754 | ||
|
|
7040801dfa | ||
|
|
f37862a25d | ||
|
|
84d76cf90d | ||
|
|
e12f28fb70 | ||
|
|
4f8a6b62b8 | ||
|
|
d3dad772cf | ||
|
|
944b672fea | ||
|
|
b1c54aa92d | ||
|
|
4ca6f43aeb | ||
|
|
f09e66e1cc | ||
|
|
8b7a4ceaaa | ||
|
|
51e90f5971 | ||
|
|
8db64772b7 | ||
|
|
bf0be60b89 | ||
|
|
172481dd72 | ||
|
|
c6c0492c33 | ||
|
|
fca29a8be2 | ||
|
|
d365240f91 | ||
|
|
1ed61d4783 | ||
|
|
a1fb11ae33 | ||
|
|
9971816711 | ||
|
|
c38d9a3098 | ||
|
|
02198de455 | ||
|
|
6cd8202310 | ||
|
|
4d7b7d1d42 | ||
|
|
dfe04960c0 | ||
|
|
de2b1cc6fe | ||
|
|
2aef4ab677 | ||
|
|
798f68d0ce | ||
|
|
e0343a3f6d | ||
|
|
d918ec694b | ||
|
|
b2b609a309 | ||
|
|
48dd80867b | ||
|
|
f58f6e8d65 | ||
|
|
e4e21f52b5 | ||
|
|
84e1cd08b6 | ||
|
|
7796753e7a | ||
|
|
880205e874 | ||
|
|
1b96087b08 | ||
|
|
aa246c9e9f | ||
|
|
ee034943b6 | ||
|
|
f1d311d232 | ||
|
|
e4f7fca10d | ||
|
|
3d6d669a50 | ||
|
|
c4097e2b7e | ||
|
|
619d27c773 | ||
|
|
1522c90294 | ||
|
|
794e15ce21 | ||
|
|
34771b835e | ||
|
|
8df51b232a | ||
|
|
13b8ce18b2 | ||
|
|
448386e52b | ||
|
|
bf07659dd5 | ||
|
|
16dfad0895 | ||
|
|
f61449c31c | ||
|
|
60699229ca | ||
|
|
e1dd26b307 | ||
|
|
7d835ef99d | ||
|
|
0971df4dfc | ||
|
|
9fb57fbac0 | ||
|
|
48ead90850 | ||
|
|
cc88bb7feb | ||
|
|
a725e2aa6a | ||
|
|
ee637c3662 | ||
|
|
65d7a39554 | ||
|
|
37735b1caa | ||
|
|
c8f8d79f45 | ||
|
|
1866e7141e | ||
|
|
feccc9f5ce | ||
|
|
af803da5c8 | ||
|
|
25c89c9940 | ||
|
|
697a2834c2 | ||
|
|
056b8bb536 | ||
|
|
625d424199 | ||
|
|
5329d05005 | ||
|
|
d2c55da6c9 | ||
|
|
2e6dd3edfe | ||
|
|
a95b4ea7b9 | ||
|
|
c891eff664 | ||
|
|
68564ca714 | ||
|
|
ca931a11be | ||
|
|
6c7272061c | ||
|
|
4f262e5bed | ||
|
|
ff26b0d5a4 | ||
|
|
a6ccc72d15 | ||
|
|
487ee18358 | ||
|
|
dc3d2e9790 | ||
|
|
f6d0e484b0 | ||
|
|
4cea9aba3c | ||
|
|
7348a68c84 | ||
|
|
7d90c3f582 | ||
|
|
2a103fc94a | ||
|
|
753391b7e2 | ||
|
|
94ce5edd20 | ||
|
|
3626f70d3e | ||
|
|
24cc24ed50 | ||
|
|
dd29ba4664 | ||
|
|
7927ad8fcf | ||
|
|
d23453ce45 | ||
|
|
a22040efa9 | ||
|
|
ba3da32ce6 | ||
|
|
9d2ba52160 | ||
|
|
e610506df4 | ||
|
|
dd91d28bfa | ||
|
|
1ebf7460fe | ||
|
|
8c930e5c33 | ||
|
|
4fb2f7474c | ||
|
|
5301f79989 | ||
|
|
6a7f7fdf15 | ||
|
|
11fb5f990e | ||
|
|
d1ee0442ea | ||
|
|
62f31ea24a | ||
|
|
f4ca5313e6 | ||
|
|
064e7b404b | ||
|
|
dfd90bd564 | ||
|
|
55508eb418 | ||
|
|
2a4fa4ed6f | ||
|
|
cf7c9f6372 | ||
|
|
ec68c3207d | ||
|
|
ecf140f3d6 | ||
|
|
13f73b7b87 | ||
|
|
12c5bcd24f | ||
|
|
56f47ee574 | ||
|
|
74f0436ac7 | ||
|
|
22d31b1527 | ||
|
|
9f3bca771a | ||
|
|
4e16d90a81 | ||
|
|
d669d5c153 | ||
|
|
343d985e96 | ||
|
|
dc3958356d | ||
|
|
c4e85c3277 | ||
|
|
89e46376dc | ||
|
|
8eeb34dba8 | ||
|
|
7171305972 | ||
|
|
2b0c223425 | ||
|
|
8f960ab0f7 | ||
|
|
60350efa10 | ||
|
|
687f577562 | ||
|
|
8e59ce9e9f | ||
|
|
33d75354a2 | ||
|
|
a318c6263d | ||
|
|
0e4a65efb7 | ||
|
|
b88134cf04 | ||
|
|
2aaa212dbc | ||
|
|
1e37990938 | ||
|
|
a417c73bf7 | ||
|
|
37c34351ee | ||
|
|
8672232ee2 | ||
|
|
83ba974f94 | ||
|
|
85ebbe8759 | ||
|
|
61cba3f6eb | ||
|
|
3ad10ff8d0 | ||
|
|
183643547b | ||
|
|
5568340b9a | ||
|
|
1399bd3065 | ||
|
|
9172e16e80 | ||
|
|
3e5f602396 | ||
|
|
3c97332fd8 | ||
|
|
379a3f27b8 | ||
|
|
ecec932a47 | ||
|
|
e239f69f69 | ||
|
|
c77cb317c4 | ||
|
|
034b089433 | ||
|
|
c0db96482c | ||
|
|
ffa8fa0a6f | ||
|
|
7e1d459a2d | ||
|
|
71c4fce87f | ||
|
|
e91da78ebb | ||
|
|
8adad6fa61 | ||
|
|
b47004bb7c | ||
|
|
08a7fb4de0 | ||
|
|
c17a9b11cc | ||
|
|
245a92a644 | ||
|
|
6b313946fe | ||
|
|
4586fb1d13 | ||
|
|
aa051434cb | ||
|
|
c3a53752e7 | ||
|
|
f3e1204fa1 | ||
|
|
0a5eb93565 | ||
|
|
b8a3135835 | ||
|
|
330dfccb89 | ||
|
|
d80e926015 | ||
|
|
2a2b067633 | ||
|
|
be73c14395 | ||
|
|
9cd5afe5b6 | ||
|
|
1cb5d26344 | ||
|
|
ec9a2d8155 | ||
|
|
4ba40f2295 | ||
|
|
b674c2e448 | ||
|
|
0227afffc8 | ||
|
|
b8139a6e83 | ||
|
|
bde5fc9264 | ||
|
|
6a421a1d96 | ||
|
|
4f55a0f1d0 | ||
|
|
3de55899fa | ||
|
|
ae4ad713ec | ||
|
|
21313adf9c | ||
|
|
9c1293ca45 | ||
|
|
1cb1e6b680 | ||
|
|
ed6ddeaa4c | ||
|
|
de08a89e6b | ||
|
|
dd42ef1920 | ||
|
|
dd192be689 | ||
|
|
52250ed10e | ||
|
|
4a26cd8d68 | ||
|
|
2ca972c3c8 | ||
|
|
74c0d55a6c | ||
|
|
3271e1464e | ||
|
|
cabd62b48f | ||
|
|
58c2355c8b | ||
|
|
bfe2065b9f | ||
|
|
9332b1355e | ||
|
|
45705a3e29 | ||
|
|
e0f0b9f210 | ||
|
|
f2832447d4 | ||
|
|
471ba5baf6 | ||
|
|
248851701f | ||
|
|
0f46277b1f | ||
|
|
679e703754 | ||
|
|
7322f90af4 | ||
|
|
e869df98c9 | ||
|
|
e499d36126 | ||
|
|
cac66d7fad | ||
|
|
320aaf0e33 | ||
|
|
178a175e99 | ||
|
|
5fdf1cb2d1 | ||
|
|
c64500dd85 | ||
|
|
812ad3f49e | ||
|
|
8e8a1a7541 | ||
|
|
4863b3df6e | ||
|
|
768c3a533b | ||
|
|
3dea554e9e | ||
|
|
16d4f6e4e1 | ||
|
|
9c7ecf221e | ||
|
|
26db481d46 | ||
|
|
3256a57230 | ||
|
|
cbc30587ff | ||
|
|
a27de38c03 | ||
|
|
e2f1609116 | ||
|
|
ea66a91a95 | ||
|
|
0d87c352b2 | ||
|
|
918f6ce0e6 | ||
|
|
6c5efe6ce0 | ||
|
|
f0be6675e7 | ||
|
|
6a8174a15c | ||
|
|
40c3f1b618 | ||
|
|
6dd2dac049 | ||
|
|
b39bbb557f | ||
|
|
f7682cba67 | ||
|
|
f94c07160a | ||
|
|
bbe6692580 | ||
|
|
9266a1c4d9 | ||
|
|
220d80f05f | ||
|
|
9144c909dd | ||
|
|
7981fcec84 | ||
|
|
71264c56fc | ||
|
|
ca0f77bdee | ||
|
|
fc8b1b8549 | ||
|
|
bc8c44f62f | ||
|
|
01fab5c92a | ||
|
|
1c07d786a0 | ||
|
|
6f0cd87d1c | ||
|
|
e44308cba2 | ||
|
|
50245c5157 | ||
|
|
9ca5188e12 | ||
|
|
e25c33eaa6 | ||
|
|
56cc881ac0 | ||
|
|
7bddc0a89c | ||
|
|
50896bdc9d | ||
|
|
8dd4567828 | ||
|
|
06ef6d3e6a | ||
|
|
14b58e8062 | ||
|
|
eee232c12c | ||
|
|
febe321aef | ||
|
|
28777ac717 | ||
|
|
13b008b56c | ||
|
|
403f42bf38 | ||
|
|
523efbd85a | ||
|
|
fcacc8bfc6 | ||
|
|
b2e301418f | ||
|
|
334a2e44a1 | ||
|
|
252b3c3bf6 | ||
|
|
c9121a03d2 | ||
|
|
cc93180d57 | ||
|
|
24221748e1 | ||
|
|
4062a425cb | ||
|
|
cce533ebb6 | ||
|
|
48df38cbfe | ||
|
|
f982f073df | ||
|
|
34999f12ca | ||
|
|
c8d5665653 | ||
|
|
ddebaf87d0 | ||
|
|
6b80cd6109 | ||
|
|
7635d8d2a5 | ||
|
|
141ae053db | ||
|
|
10ec4ff814 | ||
|
|
d2da0b7c0e | ||
|
|
7d0548406e | ||
|
|
634e3e35a0 | ||
|
|
da3dc58199 | ||
|
|
4f99df694b | ||
|
|
c121dbbd67 | ||
|
|
c1c0a7d494 | ||
|
|
0749f60702 | ||
|
|
982b8e2d72 | ||
|
|
6e7c8d7ae2 | ||
|
|
3c858f522b | ||
|
|
f2a30f8cdd | ||
|
|
43785bfab4 | ||
|
|
78edf6d324 | ||
|
|
73565c4493 | ||
|
|
ca0ef18bdf | ||
|
|
6ed011e2f8 | ||
|
|
23d322452a | ||
|
|
5d3b965d28 | ||
|
|
d9794d72c7 | ||
|
|
524b5be937 | ||
|
|
ac2e276a6a | ||
|
|
4f4dbc0c22 | ||
|
|
8c37cac957 | ||
|
|
eceab76b6f | ||
|
|
1f81b6ddc4 | ||
|
|
52c3aadd24 | ||
|
|
ad87573d09 | ||
|
|
20fbfc8544 | ||
|
|
7695c8403f | ||
|
|
421983d06e | ||
|
|
328c681a8f | ||
|
|
48d94d0f68 | ||
|
|
10ad5d763e | ||
|
|
2a78c946e4 | ||
|
|
a7872aa054 | ||
|
|
5c228ae0a1 | ||
|
|
ce73f7ac5a | ||
|
|
64107f5957 | ||
|
|
8a1795d56f | ||
|
|
b104c3bfe8 | ||
|
|
82e3f126ff | ||
|
|
175488563e | ||
|
|
da51cdd11d | ||
|
|
a8a47b138f | ||
|
|
b63d4cf675 | ||
|
|
03b999c592 | ||
|
|
a91afab038 | ||
|
|
d4747b5386 | ||
|
|
41b81c8b05 | ||
|
|
552831364d | ||
|
|
42b5e32473 | ||
|
|
e9c36fd6f8 | ||
|
|
952dfbef36 | ||
|
|
254984b600 | ||
|
|
8cbc58d257 | ||
|
|
e6cc3e8c34 | ||
|
|
516335e0ed | ||
|
|
01798ed7f8 | ||
|
|
fcad67a854 | ||
|
|
e359ffead0 | ||
|
|
eb09041859 | ||
|
|
b3d52c966d | ||
|
|
3fb8a14348 | ||
|
|
84a949e7c7 | ||
|
|
eaf1cb26b2 | ||
|
|
f37962d3de | ||
|
|
511e957d4b | ||
|
|
71df03b729 | ||
|
|
839052f4b8 | ||
|
|
7c18d857f0 | ||
|
|
947e672d18 | ||
|
|
96942960a9 | ||
|
|
8b0118e2c8 | ||
|
|
5f9a7a5381 | ||
|
|
6897d72c3e | ||
|
|
aae9a505e0 | ||
|
|
45196e022b | ||
|
|
b9e4c44d63 | ||
|
|
0a9e5b66ee | ||
|
|
8b99e82743 | ||
|
|
059fb85e22 | ||
|
|
8997df861a | ||
|
|
e65667963f | ||
|
|
3d51667fc8 | ||
|
|
7fc6e97cd8 | ||
|
|
1473e58a41 | ||
|
|
2394b2f44f | ||
|
|
516bd98198 | ||
|
|
7d8688a130 | ||
|
|
631ec70058 | ||
|
|
6fd51cfdc0 | ||
|
|
6857b74623 | ||
|
|
5ec4305a9f | ||
|
|
88baff96d0 | ||
|
|
e871f0002b | ||
|
|
7358d48e35 | ||
|
|
178fbf0fca | ||
|
|
a50597ff27 | ||
|
|
e4cb78abee | ||
|
|
732884a3b2 | ||
|
|
80f2c42c69 | ||
|
|
49a5a39659 | ||
|
|
a4a7040b98 | ||
|
|
de5a7d5b99 | ||
|
|
3f92e388be | ||
|
|
25c941b847 | ||
|
|
24b6934d3b | ||
|
|
d286ab406c | ||
|
|
ef6a7a6904 | ||
|
|
c61eda0d24 | ||
|
|
ad226b6fb1 | ||
|
|
24491f0dfe | ||
|
|
870fd1654d | ||
|
|
38bc912e4e | ||
|
|
315c9a2d92 | ||
|
|
a14ad6f700 | ||
|
|
d56e63a91b | ||
|
|
76dcdfb98c | ||
|
|
99c09ba8a1 | ||
|
|
0f18b76813 | ||
|
|
8504e4cd22 | ||
|
|
ebe793e0e7 | ||
|
|
965c6cf4d9 | ||
|
|
2b1ab3184e | ||
|
|
e7d21c2dbe | ||
|
|
11906d9d71 | ||
|
|
ac5a64d77a | ||
|
|
c86c851c60 | ||
|
|
721cf98486 | ||
|
|
84bbb6efd4 | ||
|
|
f897cda6cd | ||
|
|
2da8b25b09 | ||
|
|
3f94fd90dd | ||
|
|
bc6be22cb4 | ||
|
|
e23604e08d | ||
|
|
be858ac9ce | ||
|
|
137ab4a557 | ||
|
|
bad0fc386d | ||
|
|
641c7b2c89 | ||
|
|
21be3db51f | ||
|
|
e978857820 | ||
|
|
3bf596c54c | ||
|
|
aedb823b4d | ||
|
|
7a417435cc | ||
|
|
497d6e80f7 | ||
|
|
ae6ab34e72 | ||
|
|
4c26161728 | ||
|
|
1731dca5dd | ||
|
|
ee2caff46e | ||
|
|
db8fb8b05d | ||
|
|
bec7e141dc | ||
|
|
ab85b4b129 | ||
|
|
b030049b40 | ||
|
|
1338a3d89d | ||
|
|
181178296f | ||
|
|
df7888d6fb | ||
|
|
dd15f5e052 | ||
|
|
f348d85b11 | ||
|
|
8c8a05b8c1 | ||
|
|
34d2fc1503 | ||
|
|
9b3fa809bf | ||
|
|
59535c112e | ||
|
|
04e5a6425a | ||
|
|
424dddf67b | ||
|
|
f0d6ae2a00 | ||
|
|
25298a32fa | ||
|
|
ba28bf01b7 | ||
|
|
d15c29b1a3 | ||
|
|
b083910a51 | ||
|
|
235aad32a6 | ||
|
|
a818560344 | ||
|
|
8f179becf7 | ||
|
|
e1695a0874 | ||
|
|
af7498d283 | ||
|
|
3e2a4d8053 | ||
|
|
29982e2caf | ||
|
|
5fea1df42b | ||
|
|
a041162b32 | ||
|
|
32cd3981d8 | ||
|
|
ca5af87196 | ||
|
|
a8164f612f | ||
|
|
d3bb0b6ff0 | ||
|
|
0ef10c1e13 | ||
|
|
4017911373 | ||
|
|
048034d4b1 | ||
|
|
fcb3f08bcb | ||
|
|
d2a05bb622 | ||
|
|
f7254ee169 | ||
|
|
a0e5c9d570 | ||
|
|
8291e4ba73 | ||
|
|
b324be3b0b | ||
|
|
6ba0ba7126 | ||
|
|
1d8e0629af | ||
|
|
42df54869f | ||
|
|
7b758b85ec | ||
|
|
82987ec401 | ||
|
|
71707b5aa7 | ||
|
|
ca2df83928 | ||
|
|
085771c2f0 | ||
|
|
607a638858 | ||
|
|
5f6d06d05d | ||
|
|
19ecb87b07 | ||
|
|
2a332c0883 | ||
|
|
bb773c6c13 | ||
|
|
238de489c1 | ||
|
|
6b4db330d8 | ||
|
|
ea5d7c0dee | ||
|
|
0f189f1af3 | ||
|
|
0f1b8dd51a | ||
|
|
d7e6946a78 | ||
|
|
255b7b1a54 | ||
|
|
79e1c751a1 | ||
|
|
fc745b9614 | ||
|
|
95b1baebd2 | ||
|
|
56fe1ceb97 | ||
|
|
863a51e556 | ||
|
|
69b3064b45 | ||
|
|
fb3eab1aa8 | ||
|
|
32c7399f26 | ||
|
|
955351b5bd | ||
|
|
75f6c67b6e | ||
|
|
700a3e6ed9 | ||
|
|
00702448c7 | ||
|
|
5074827d51 | ||
|
|
ceb0711e42 | ||
|
|
ddb5824b58 | ||
|
|
39f9209374 | ||
|
|
5fea4cf760 | ||
|
|
0e5ec86ca9 | ||
|
|
8b95211055 | ||
|
|
a27339b954 | ||
|
|
028b728760 | ||
|
|
18e63df01e | ||
|
|
5f459c0901 | ||
|
|
a90bcde38c | ||
|
|
603e7d922e | ||
|
|
861126f810 | ||
|
|
eb9b706ebc | ||
|
|
de9cbae0b2 | ||
|
|
25e890986f | ||
|
|
f66627dd04 | ||
|
|
924eb33b3f | ||
|
|
1b288c541a | ||
|
|
2612b8c86f | ||
|
|
3e2796d456 | ||
|
|
7092913863 | ||
|
|
67625fc347 | ||
|
|
eb55030b06 | ||
|
|
6e1b2d50f2 | ||
|
|
c6f72c44b8 | ||
|
|
d38ded0f26 | ||
|
|
ec20b7bd3a | ||
|
|
0766cf464a | ||
|
|
867f00e091 | ||
|
|
c823b8d7ae | ||
|
|
393d4d336c | ||
|
|
2cb3f2d03d | ||
|
|
279f2dd633 | ||
|
|
dec051a6e0 | ||
|
|
790fdd320c | ||
|
|
feb4a364a7 | ||
|
|
1140149e1e | ||
|
|
2ee9599b6e | ||
|
|
188d45e002 | ||
|
|
7c4c2f7860 | ||
|
|
90b7f2ff3b | ||
|
|
d3f0041e93 | ||
|
|
9d60142828 | ||
|
|
68d5edca60 | ||
|
|
1b369489df | ||
|
|
600ddfbf2d | ||
|
|
415d4dde2a | ||
|
|
1867245ed3 | ||
|
|
71d34592d9 | ||
|
|
db2927eea7 | ||
|
|
bb01a5cb31 | ||
|
|
815319140f | ||
|
|
6e6082119f | ||
|
|
da48ffe05c | ||
|
|
081979be3b | ||
|
|
3673956c1c | ||
|
|
bdd3c274ed | ||
|
|
423034d5c4 | ||
|
|
19fd2b12c0 | ||
|
|
21cd17873f | ||
|
|
9870fa9e34 | ||
|
|
938cd5e136 | ||
|
|
e8025ad4b3 | ||
|
|
07fa141aaa | ||
|
|
18bdf1e8b3 | ||
|
|
5be977005e | ||
|
|
282b64278e | ||
|
|
7263d484de | ||
|
|
bdb059b6c9 | ||
|
|
de3f5011bc | ||
|
|
de9faffa33 | ||
|
|
f67ca69e05 | ||
|
|
dd19e880c5 | ||
|
|
b5e8fa007c | ||
|
|
c3555bfcab | ||
|
|
0383db8788 | ||
|
|
d7af122c18 | ||
|
|
e15b8145b1 | ||
|
|
d75f5f9231 | ||
|
|
9939797792 | ||
|
|
5248b9fc6f | ||
|
|
e15295bdac | ||
|
|
4e1f96e09c | ||
|
|
96cfdebced | ||
|
|
944f34b833 | ||
|
|
1023b2ca9c | ||
|
|
16318bb9f6 | ||
|
|
350586335d | ||
|
|
9d809499a5 | ||
|
|
fdd52c17d7 | ||
|
|
1461d029db | ||
|
|
07cefd71df | ||
|
|
abab10b2cc | ||
|
|
e37d4a6756 | ||
|
|
e2a1ce623c | ||
|
|
0ff243266c | ||
|
|
645da2e307 | ||
|
|
5fd95788f9 | ||
|
|
bd29f168e0 | ||
|
|
dc97e33cd6 | ||
|
|
caf7cb07cd | ||
|
|
ad5df53ee7 | ||
|
|
95920bf207 | ||
|
|
6700166841 | ||
|
|
b8196cd06e | ||
|
|
c28afbf193 | ||
|
|
84ffffb3f3 | ||
|
|
b2c030140c | ||
|
|
90138ed574 | ||
|
|
92f131bbe4 | ||
|
|
338580087e | ||
|
|
deda53a842 | ||
|
|
5391854c82 | ||
|
|
e288bfbec4 | ||
|
|
377fe5bc40 | ||
|
|
d264ff2801 | ||
|
|
a21bb6b02d | ||
|
|
37ecd5cdef | ||
|
|
07a87dfba7 | ||
|
|
9e4db89521 | ||
|
|
536d394e41 | ||
|
|
c0580c7ad0 | ||
|
|
488e72ef4e | ||
|
|
01c224d301 | ||
|
|
eaf95a85a8 | ||
|
|
ba1d084660 | ||
|
|
2e64c461c3 | ||
|
|
ce5dad722f | ||
|
|
7675feca91 | ||
|
|
c66d74e135 | ||
|
|
54d6eed740 | ||
|
|
dc4b75070d | ||
|
|
830eb74725 | ||
|
|
4f21d8d7a8 | ||
|
|
424deb8faf | ||
|
|
b4a40f1257 | ||
|
|
9296c10ca4 | ||
|
|
fbe65cd542 | ||
|
|
ccbb6e4789 | ||
|
|
d70f436304 | ||
|
|
16aaa8201c | ||
|
|
acc1f2f3d7 | ||
|
|
433d254c70 | ||
|
|
ea4eebd2d6 | ||
|
|
3c00a527dd | ||
|
|
f72a354066 | ||
|
|
7c92e0e9ce | ||
|
|
4f6868728d | ||
|
|
0ec4522f9e | ||
|
|
c6e0c6d096 | ||
|
|
dc0fb9ed8a | ||
|
|
66d9eaee78 | ||
|
|
3797272faf | ||
|
|
682b302d04 | ||
|
|
1de10f9b05 | ||
|
|
c4391ff058 | ||
|
|
3822e3f8d9 | ||
|
|
f8f99f3878 | ||
|
|
e77e4acea9 | ||
|
|
c6de444d0b | ||
|
|
89e38c34b8 | ||
|
|
246d17972c | ||
|
|
55a8b37ef8 | ||
|
|
445183001b | ||
|
|
ca9e2200da | ||
|
|
eba3f84c04 | ||
|
|
867e6a8f4b | ||
|
|
df9779ec59 | ||
|
|
1b71d1e46d | ||
|
|
0a58918f47 | ||
|
|
afbd927fc0 | ||
|
|
2aa09ae18d | ||
|
|
09789b0b72 | ||
|
|
2426abd17a | ||
|
|
db4a97743f | ||
|
|
7ca98ed344 | ||
|
|
c9d3d17999 | ||
|
|
628049cfd7 | ||
|
|
ae9a11da53 | ||
|
|
7e097482bc | ||
|
|
df1b151587 | ||
|
|
45eb59a5aa | ||
|
|
687c17bbe2 | ||
|
|
7505aec706 | ||
|
|
c7b414492d | ||
|
|
14b0095822 | ||
|
|
a1256b46c8 | ||
|
|
094270dff7 | ||
|
|
d4e24dabc2 | ||
|
|
842df0d112 | ||
|
|
cfa9427d7c | ||
|
|
3c01e24f02 | ||
|
|
22dbf63ff9 | ||
|
|
814f7394a0 | ||
|
|
9a4cebaa1b | ||
|
|
c30207ac63 | ||
|
|
77afbddb91 | ||
|
|
18feeabe15 | ||
|
|
c3811d3a14 | ||
|
|
f20d6b551d | ||
|
|
311bcadacb | ||
|
|
2189c8cd82 | ||
|
|
6553bb8147 | ||
|
|
dea492fd64 | ||
|
|
00ab7f04fa | ||
|
|
d3ba714aba | ||
|
|
748b37f1d6 | ||
|
|
b83b188aff | ||
|
|
cfefa32603 | ||
|
|
85d8db3ef9 | ||
|
|
3c14dbe382 | ||
|
|
b49b2af11f | ||
|
|
425a36aa51 | ||
|
|
ec0b9de713 | ||
|
|
9f13b14f6d | ||
|
|
01e83b45b5 | ||
|
|
f80566e0cb | ||
|
|
42afacf0af | ||
|
|
2e61e7e682 | ||
|
|
3de9267ea7 | ||
|
|
8c99d4fcd2 | ||
|
|
be4e6e5ba5 | ||
|
|
1b5efea6eb | ||
|
|
6554f80fad | ||
|
|
2e8a9f809e | ||
|
|
dc66032720 | ||
|
|
c9433782d8 | ||
|
|
fef5586ff5 | ||
|
|
1f4a2fd654 | ||
|
|
8243385af6 | ||
|
|
26ce9b2d4a | ||
|
|
119f3169e2 | ||
|
|
16bd22ee01 | ||
|
|
f4a5f73ab2 | ||
|
|
e61a4564ea | ||
|
|
e72edee1f2 | ||
|
|
e8c150fcac | ||
|
|
52418932b1 | ||
|
|
4f81cb9333 | ||
|
|
db46f47b96 | ||
|
|
edfe5594ba | ||
|
|
f25e972594 | ||
|
|
d5488bdd42 | ||
|
|
bbff64bc96 | ||
|
|
635afefdeb | ||
|
|
fd3e67a0b4 | ||
|
|
729a6021ee | ||
|
|
309f254c2c | ||
|
|
5c37f04d64 | ||
|
|
7c3dd8e852 | ||
|
|
66ddedbaf3 | ||
|
|
7981b17897 | ||
|
|
62137d47c8 | ||
|
|
e3b5437f61 | ||
|
|
934693924e | ||
|
|
308fd92a46 | ||
|
|
da1eb71ad0 | ||
|
|
576dbb7ce6 | ||
|
|
d0c381b3df | ||
|
|
55178a81c6 | ||
|
|
249308380b | ||
|
|
d91bec08c3 | ||
|
|
e23ef4b0be | ||
|
|
6037521c49 | ||
|
|
a27fac3677 | ||
|
|
21f2eb664e | ||
|
|
81546ef4b0 | ||
|
|
4b90c8fd45 | ||
|
|
c643fb8aac | ||
|
|
0cae6ceca3 | ||
|
|
5cde59b53c | ||
|
|
7df67630af | ||
|
|
0c89dca261 | ||
|
|
6b953b8793 | ||
|
|
0d1defcf27 | ||
|
|
c1db9c19b3 | ||
|
|
95487755ed | ||
|
|
4813469659 | ||
|
|
4dfd357c0b | ||
|
|
4ca0486518 | ||
|
|
b139c05960 | ||
|
|
3d32759030 | ||
|
|
badfe39a3d | ||
|
|
060e2db351 | ||
|
|
ed802c0404 | ||
|
|
5d8739bfb2 | ||
|
|
086faf44fc | ||
|
|
e5eaa90c61 | ||
|
|
b24807ea29 | ||
|
|
d68bae9bc2 | ||
|
|
b891fb4502 | ||
|
|
ea69b3b4e3 | ||
|
|
23c8616ba5 | ||
|
|
b25c91affd | ||
|
|
151cefe0ec | ||
|
|
3412ff94bc | ||
|
|
77aa2241dc | ||
|
|
0766d08479 | ||
|
|
14112ed294 | ||
|
|
f6ed0d43a2 | ||
|
|
c8413cb029 | ||
|
|
97d53b81a7 | ||
|
|
ab888f5cd0 | ||
|
|
f54246eac1 | ||
|
|
7de9422b75 | ||
|
|
f02a37d3f0 | ||
|
|
28815a0ae6 | ||
|
|
70c7dfd0f4 | ||
|
|
9c2ebd308b | ||
|
|
11d8412591 | ||
|
|
32ca170c4d | ||
|
|
388ed08b0e | ||
|
|
3e1909b645 | ||
|
|
b408f88b8c | ||
|
|
09087401b4 | ||
|
|
c68692d78e | ||
|
|
ee2a4d0a5d | ||
|
|
a15885fe80 | ||
|
|
24111570cf | ||
|
|
ded203b1c1 | ||
|
|
e43fc98c0d | ||
|
|
1efd13545e | ||
|
|
1193ee1ab9 | ||
|
|
a6ba801738 | ||
|
|
e7958f2910 | ||
|
|
cbac9a7703 | ||
|
|
60d8f2323e | ||
|
|
70ae6b8d72 | ||
|
|
a4b1fbd6ee | ||
|
|
e1850440b0 | ||
|
|
d5c2aaeea3 | ||
|
|
a06b7acc85 | ||
|
|
615168423a | ||
|
|
73abf7d20e | ||
|
|
fcea42e91e | ||
|
|
14f7574c41 | ||
|
|
8f15ded650 | ||
|
|
4aec4ef80a | ||
|
|
ecb8f1de30 | ||
|
|
4c28180125 | ||
|
|
4138180f43 | ||
|
|
0d508a88f6 | ||
|
|
7c8fcf73f6 | ||
|
|
5904d72776 | ||
|
|
5e32ccbf12 | ||
|
|
6ce136bede | ||
|
|
b9f8ce5729 | ||
|
|
115530a104 | ||
|
|
65c9b2a5f7 | ||
|
|
46c73a05a9 | ||
|
|
c5f7e72ca8 | ||
|
|
e6fb63ddba | ||
|
|
e2645e4126 | ||
|
|
36d267ca40 | ||
|
|
2e5d04389b | ||
|
|
6130ed17a6 | ||
|
|
c4e82407ec | ||
|
|
2e28e68c48 | ||
|
|
a7882fa32b | ||
|
|
82f9e70406 | ||
|
|
3fa27c7ffa | ||
|
|
1aa50dee20 | ||
|
|
f58f7257be | ||
|
|
e17f1269a2 | ||
|
|
82f48b84b3 | ||
|
|
926bd20281 | ||
|
|
a6cd019118 | ||
|
|
bbfc476d7e | ||
|
|
8d49515a3c | ||
|
|
0a410a5544 | ||
|
|
eef203633b | ||
|
|
122255058e | ||
|
|
b1681b2213 | ||
|
|
73d79f55d8 | ||
|
|
a02fcd97d6 | ||
|
|
016708b338 | ||
|
|
9f26fc28c4 | ||
|
|
7c1b354fc3 | ||
|
|
abeda1935d | ||
|
|
403ee9ff9e | ||
|
|
cccb45fe13 | ||
|
|
8d43becb27 | ||
|
|
ee22e07fff | ||
|
|
37464e2d95 | ||
|
|
5abcecbc9b | ||
|
|
cecdf0d511 | ||
|
|
a451fe4248 | ||
|
|
d6f801f764 | ||
|
|
a35e772a6b | ||
|
|
aca3fae6b1 | ||
|
|
17891f0209 | ||
|
|
aea2b3c8e5 | ||
|
|
6d2ef9be5d | ||
|
|
57fb167a9c | ||
|
|
0406bba384 | ||
|
|
7c4c80fe4a | ||
|
|
bfb267e164 | ||
|
|
a0720948a1 | ||
|
|
9f00159a84 | ||
|
|
34067a1d70 | ||
|
|
3f6917fdcb | ||
|
|
c04a6e501e | ||
|
|
661b564399 | ||
|
|
761c103373 | ||
|
|
f4bd9e3d24 | ||
|
|
b9ddac878c | ||
|
|
f304ce5ccf | ||
|
|
828401f057 | ||
|
|
445d77a220 | ||
|
|
4d768bb5eb | ||
|
|
4e3b87d338 | ||
|
|
00740b6117 | ||
|
|
7775f203fc | ||
|
|
945af879ec | ||
|
|
b2506f0afe | ||
|
|
2eab4b84c9 | ||
|
|
7746d9968d | ||
|
|
da49d918d6 | ||
|
|
804ed758c9 | ||
|
|
17aac58e08 | ||
|
|
a7095d7dec | ||
|
|
3afbb6fcc2 | ||
|
|
8ecbd8e71c | ||
|
|
988f499723 | ||
|
|
50aeb9ff21 | ||
|
|
e620c28a1c | ||
|
|
29ee7d41f5 | ||
|
|
f9104c71f6 | ||
|
|
b6af5884b1 | ||
|
|
e4f250435d | ||
|
|
1a246f2e38 | ||
|
|
48ebc46c5f | ||
|
|
e27803038c | ||
|
|
babf8ba3e7 | ||
|
|
6ccd3f277b | ||
|
|
9d6f9aae9a | ||
|
|
95a000c279 | ||
|
|
b19debff14 | ||
|
|
39c9024747 | ||
|
|
3c660f2cb0 | ||
|
|
13dbdc7dc7 | ||
|
|
f903e4b2de | ||
|
|
b96cb2142b | ||
|
|
cc51cd4476 | ||
|
|
8a995fc515 | ||
|
|
078eccea2d | ||
|
|
190119bcd4 | ||
|
|
7672b42fbc | ||
|
|
c590658f16 | ||
|
|
017d4e792b | ||
|
|
0671be870d | ||
|
|
2f9ed37db2 | ||
|
|
2cf2db3eef | ||
|
|
11ad025e5d | ||
|
|
630cf05b2f | ||
|
|
a72782f91e | ||
|
|
fbd554a15f | ||
|
|
f71aa1cad2 | ||
|
|
6d9517f6ea | ||
|
|
fd8c488dbd | ||
|
|
dbf18b90a7 | ||
|
|
d318fe24b8 | ||
|
|
1352315441 | ||
|
|
3c635532c4 | ||
|
|
f8703bf884 | ||
|
|
eea3aa7a27 | ||
|
|
6eff448508 | ||
|
|
eb8cac5980 | ||
|
|
1a4086c98c | ||
|
|
5c91076660 | ||
|
|
5467b8dd0d | ||
|
|
d46a9d6286 | ||
|
|
2fa7810128 | ||
|
|
8249725ae7 | ||
|
|
c07b83335b | ||
|
|
7e575c501a | ||
|
|
933e2fb0ef | ||
|
|
8d51383fb2 | ||
|
|
80f4c83b83 | ||
|
|
0d739e4af7 | ||
|
|
58f9027002 | ||
|
|
990f2e2892 | ||
|
|
ce7989c171 | ||
|
|
4efb0229d4 | ||
|
|
5dd6dc2d69 | ||
|
|
20931eb9d6 | ||
|
|
c11fa122af | ||
|
|
e9141c8300 | ||
|
|
1d03b688d9 | ||
|
|
176d42f625 | ||
|
|
7c98a27c53 | ||
|
|
020b30783e | ||
|
|
fafbdb0714 | ||
|
|
466cdb4ee7 | ||
|
|
fa66f0b509 | ||
|
|
12a566c07e | ||
|
|
bf7a1c6b1f | ||
|
|
55891aa5f8 | ||
|
|
7c0acd9fcb | ||
|
|
333f1e2c47 | ||
|
|
9d30cdfefc | ||
|
|
324f6fe16e | ||
|
|
5d96304332 | ||
|
|
e6e32b5fd2 | ||
|
|
181f265de5 | ||
|
|
e5fc8bb27c | ||
|
|
34dda780d9 | ||
|
|
c7cf4eeb7a | ||
|
|
a6e5d9f6dc | ||
|
|
ea1017584e | ||
|
|
6aef32d7a8 | ||
|
|
4a1d71b6b8 | ||
|
|
a18b61cb1d | ||
|
|
e31e19aeba | ||
|
|
ef6d8a6554 | ||
|
|
100764d79e | ||
|
|
75abe7da1b | ||
|
|
a19a125aec | ||
|
|
f02fc95958 | ||
|
|
175edca8c7 | ||
|
|
f1f0a66f41 | ||
|
|
496c6905af | ||
|
|
c84106570f | ||
|
|
1a05da9e55 | ||
|
|
232e7a1759 | ||
|
|
c440d41d57 | ||
|
|
dfe5c24404 | ||
|
|
eba5773d56 | ||
|
|
5d56fea2d3 | ||
|
|
946f02b7a2 | ||
|
|
8e8ffd21d5 | ||
|
|
d02d974cd0 | ||
|
|
0a68be695d | ||
|
|
335e781d0c | ||
|
|
9f5c2e4ca7 | ||
|
|
76a53bedbe | ||
|
|
b0bc84ed21 | ||
|
|
ae298fc2e6 | ||
|
|
3b809b2910 | ||
|
|
68fbc0bde3 | ||
|
|
9d8e5263a6 | ||
|
|
7eb026cc0d | ||
|
|
e51e6aa2b0 | ||
|
|
bc700d2044 | ||
|
|
30ed58ff07 | ||
|
|
066069baad | ||
|
|
068ec68917 | ||
|
|
560f028bda | ||
|
|
fd1e77df8f | ||
|
|
864ac08f16 | ||
|
|
6ad1a11593 | ||
|
|
89174ba0b6 | ||
|
|
fc5496e570 | ||
|
|
fd21d952ac | ||
|
|
073fea2bde | ||
|
|
e548712f5e | ||
|
|
c3ba83ff93 | ||
|
|
451dd0fd64 | ||
|
|
aa805c2428 | ||
|
|
58a7590aff | ||
|
|
563ab30564 | ||
|
|
5050b34361 | ||
|
|
3bb86f196b | ||
|
|
51dca3be11 | ||
|
|
adeda6cd75 | ||
|
|
09665c3a4a | ||
|
|
8f5f6212d2 | ||
|
|
a11ae912b4 | ||
|
|
3b12240615 | ||
|
|
862520e4b1 | ||
|
|
a3d2dd8366 | ||
|
|
16ef487871 | ||
|
|
54c45a0cfd | ||
|
|
1f14eb62d4 | ||
|
|
0db86a8b3d | ||
|
|
c63c85071a | ||
|
|
b63d93e325 | ||
|
|
12c6e50e16 | ||
|
|
53ccc2e04c | ||
|
|
2d3234b54d | ||
|
|
9a57c2a0d4 | ||
|
|
fc64abee8f | ||
|
|
d5f26f6d15 | ||
|
|
97f9c2991b | ||
|
|
81378d4353 | ||
|
|
9f0c902030 | ||
|
|
3c0c75be10 | ||
|
|
90d23abe18 | ||
|
|
82eccf36d4 | ||
|
|
342cb52887 | ||
|
|
cafa4f5173 | ||
|
|
67cff5af8b | ||
|
|
6d23d91aa5 | ||
|
|
3a0699fc1d | ||
|
|
027e569087 | ||
|
|
830f759f0b | ||
|
|
969891c71c | ||
|
|
4eb5c3e907 | ||
|
|
23303a759b | ||
|
|
d1e7f46994 | ||
|
|
65ea70ae90 | ||
|
|
7522b71c86 | ||
|
|
70625c86c3 | ||
|
|
74354d2027 | ||
|
|
f6397e2731 | ||
|
|
065ca39d60 | ||
|
|
b4759ae261 | ||
|
|
c095950ef9 | ||
|
|
24b7035b1b | ||
|
|
7b1f157cf8 | ||
|
|
8b8bee4e9c | ||
|
|
c27ab35600 | ||
|
|
446b4dc461 | ||
|
|
ff8ed24622 | ||
|
|
ae2d6a122b | ||
|
|
3cac375f21 | ||
|
|
7d806dd161 | ||
|
|
db037c704e | ||
|
|
954184f742 | ||
|
|
7650e0b61a | ||
|
|
4a5c93988f | ||
|
|
8ceaf0ac66 | ||
|
|
ca60aa1cc6 | ||
|
|
596d5906a0 | ||
|
|
c02db94522 | ||
|
|
3970803575 | ||
|
|
43805ad698 | ||
|
|
2498e12f19 | ||
|
|
6f3cb4b48e | ||
|
|
fbd047599e | ||
|
|
da00117622 | ||
|
|
e44c73bdf6 | ||
|
|
e3cb7bd9f0 | ||
|
|
08f5889ee5 | ||
|
|
d5bfe74e1a | ||
|
|
d7015fa3b6 | ||
|
|
9092651b5b | ||
|
|
2c53b48e0a | ||
|
|
319a1c3367 | ||
|
|
80dd590e8f | ||
|
|
992a8e8774 | ||
|
|
f56d3bd193 | ||
|
|
4ecc59d0c0 | ||
|
|
5ebf82874b | ||
|
|
12670a3153 | ||
|
|
fa3a23134e | ||
|
|
8291044abc | ||
|
|
505e0799da | ||
|
|
be1d463775 | ||
|
|
a6fc5aa345 | ||
|
|
0e6e4db08b | ||
|
|
a84708e99d | ||
|
|
6b6c0e930e | ||
|
|
926892be01 | ||
|
|
2894bef9ef | ||
|
|
a6e7ecd9e5 | ||
|
|
9b000a002e | ||
|
|
0f9c9e2089 | ||
|
|
0edc1fcec7 | ||
|
|
b46d3b22e2 | ||
|
|
412c881cd4 | ||
|
|
48f07a110f | ||
|
|
5c1b7935e2 | ||
|
|
62aa564df1 | ||
|
|
798ee4a4d5 | ||
|
|
7d87fb80ec | ||
|
|
393227a786 | ||
|
|
c5870353e3 | ||
|
|
7c9941c629 | ||
|
|
c7dbb6792d | ||
|
|
728b2b7089 | ||
|
|
5def997bed | ||
|
|
a30c65966b | ||
|
|
cd67ed8a27 | ||
|
|
5400dc783e | ||
|
|
2880e9867d | ||
|
|
58f9469a6f | ||
|
|
30d052db99 | ||
|
|
744311f107 | ||
|
|
656674a477 | ||
|
|
0e4aa38aaa | ||
|
|
fdc267fa1f | ||
|
|
4325b80d64 | ||
|
|
fbe07836f9 | ||
|
|
304681bd21 | ||
|
|
05a01bb7c4 | ||
|
|
cbc028b040 | ||
|
|
2074c0149f | ||
|
|
61ed97dd45 | ||
|
|
a358c46b9f | ||
|
|
50c1e2472b | ||
|
|
ea2fc76d3c | ||
|
|
58634b54ec | ||
|
|
4b4bc1a4d3 | ||
|
|
0549e07a90 | ||
|
|
42666b1d30 | ||
|
|
0a8be77233 | ||
|
|
b26fb0e6c7 | ||
|
|
1699a92822 | ||
|
|
7ae3e8cb47 | ||
|
|
fd26ae4b5b | ||
|
|
9945a5f9cc | ||
|
|
d5e9ae23ef | ||
|
|
d50e056114 | ||
|
|
d7d956d966 | ||
|
|
bd3966bf8d | ||
|
|
74578ba274 | ||
|
|
cb89742d2f | ||
|
|
6d0f991c17 | ||
|
|
d126d2a0f9 | ||
|
|
b51cca5617 | ||
|
|
dc54dad290 | ||
|
|
7d6ab5a708 | ||
|
|
07acb9308d | ||
|
|
ef315a46bc | ||
|
|
eb45bd051c | ||
|
|
65102edc98 | ||
|
|
04eda96416 | ||
|
|
f5036bdf5e | ||
|
|
b6df85da7a | ||
|
|
9775b39a8d | ||
|
|
d6d74c5024 | ||
|
|
e09d15b12a | ||
|
|
6d33d23935 | ||
|
|
47760e00f7 | ||
|
|
72e8421099 | ||
|
|
844b0ed457 | ||
|
|
7e37db796f | ||
|
|
3e5b506675 | ||
|
|
d356dbfc06 | ||
|
|
f5aee1f4c0 | ||
|
|
de4926d87d | ||
|
|
56a39e2cc7 | ||
|
|
8e14dacc32 | ||
|
|
05102c673a | ||
|
|
db2ecfe159 | ||
|
|
640cb0d489 | ||
|
|
223a6170d5 | ||
|
|
63f1c85964 | ||
|
|
c252c8e870 | ||
|
|
801c019150 | ||
|
|
d77a6620f3 | ||
|
|
4e4a615df8 | ||
|
|
1b0ea44519 | ||
|
|
86f4ea108d | ||
|
|
2322cb9b83 | ||
|
|
4720268426 | ||
|
|
b4f134bff6 | ||
|
|
f2a9125b99 | ||
|
|
8438b7d561 | ||
|
|
18c846757b | ||
|
|
bc11a48e6b | ||
|
|
01ecd725b8 | ||
|
|
e6af7d1bd0 | ||
|
|
701de08e8a | ||
|
|
363b95bdef | ||
|
|
ca5a385b51 | ||
|
|
93f0d24673 | ||
|
|
a5038893fe | ||
|
|
3442f99a49 | ||
|
|
6ecf52cc03 | ||
|
|
8aaef674fe | ||
|
|
3b1cd06615 | ||
|
|
4841f8cc8f | ||
|
|
d9d8f68bf8 | ||
|
|
cf726d9813 | ||
|
|
92be2c45d6 | ||
|
|
914092b538 | ||
|
|
a8cd5fc266 | ||
|
|
643f07fa10 | ||
|
|
0d77ff661b | ||
|
|
70d84b2f72 | ||
|
|
41905ef735 | ||
|
|
2a468cc750 | ||
|
|
32520000c6 | ||
|
|
14db7a8eb3 | ||
|
|
8460e9a385 | ||
|
|
933a93a703 | ||
|
|
c2e09d3084 | ||
|
|
98397401b8 | ||
|
|
e042b1105a | ||
|
|
ee4775eb1a | ||
|
|
6ff6232316 | ||
|
|
10035ab2f4 | ||
|
|
2679175ae9 | ||
|
|
8d3aa1f3fa | ||
|
|
75e78795ec | ||
|
|
05f0f8901e | ||
|
|
6917aeb47b | ||
|
|
516a86e33f | ||
|
|
7184a91c95 | ||
|
|
83e9d705cf | ||
|
|
bb907f5adb | ||
|
|
f1b60453bd | ||
|
|
0ef339f12a | ||
|
|
5c0169ee05 | ||
|
|
daf959ee90 | ||
|
|
89b43b6102 | ||
|
|
d3b05201b9 | ||
|
|
127e53cf3a | ||
|
|
29281fe3ec | ||
|
|
a0fb55802f | ||
|
|
90ec068367 | ||
|
|
f57cf1be75 | ||
|
|
3f44dee367 | ||
|
|
82161ce94c | ||
|
|
27b8e2a38c | ||
|
|
e5f2fbdcb2 | ||
|
|
cdf0cdd0ea | ||
|
|
f12ff2c7bd | ||
|
|
6c7c507d32 | ||
|
|
0c97b8238b | ||
|
|
967a2030e6 | ||
|
|
78ebd5faf8 | ||
|
|
9d498fa069 | ||
|
|
0db1ceaea7 | ||
|
|
df27aeef6c | ||
|
|
5ae0df53bb | ||
|
|
48df6ae159 | ||
|
|
6cae2fcea7 | ||
|
|
d1d4d4894d | ||
|
|
adfcf7bb2c | ||
|
|
c8f75cd266 | ||
|
|
282a9bbf65 | ||
|
|
d4c8af2a61 | ||
|
|
3930524bbf | ||
|
|
622ca3121f |
16
.github/actions/install/action.yml
vendored
16
.github/actions/install/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
|||||||
zig-v8:
|
zig-v8:
|
||||||
description: 'zig v8 version to install'
|
description: 'zig v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
default: 'v0.2.2'
|
default: 'v0.3.7'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
@@ -22,6 +22,10 @@ inputs:
|
|||||||
description: 'cache dir to use'
|
description: 'cache dir to use'
|
||||||
required: false
|
required: false
|
||||||
default: '~/.cache'
|
default: '~/.cache'
|
||||||
|
debug:
|
||||||
|
description: 'enable v8 pre-built debug version, only available for linux x86_64'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
@@ -32,7 +36,7 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
|
sudo apt-get install -y wget xz-utils ca-certificates clang make git
|
||||||
|
|
||||||
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
||||||
- uses: mlugg/setup-zig@v2
|
- uses: mlugg/setup-zig@v2
|
||||||
@@ -42,22 +46,22 @@ runs:
|
|||||||
|
|
||||||
- name: Cache v8
|
- name: Cache v8
|
||||||
id: cache-v8
|
id: cache-v8
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
env:
|
env:
|
||||||
cache-name: cache-v8
|
cache-name: cache-v8
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.cache-dir }}/v8
|
path: ${{ inputs.cache-dir }}/v8
|
||||||
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}.a
|
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||||
|
|
||||||
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
|
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ${{ inputs.cache-dir }}/v8
|
mkdir -p ${{ inputs.cache-dir }}/v8
|
||||||
|
|
||||||
wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a
|
wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||||
|
|
||||||
- name: install v8
|
- name: install v8
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p v8
|
mkdir -p v8
|
||||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8.a
|
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||||
|
|||||||
14
.github/workflows/e2e-integration-test.yml
vendored
14
.github/workflows/e2e-integration-test.yml
vendored
@@ -20,19 +20,17 @@ jobs:
|
|||||||
if: github.event.pull_request.draft == false
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: zig build release
|
- name: zig build release
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
path: |
|
path: |
|
||||||
@@ -47,7 +45,7 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -55,7 +53,7 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
@@ -63,6 +61,6 @@ jobs:
|
|||||||
|
|
||||||
- name: run end to end integration tests
|
- name: run end to end integration tests
|
||||||
run: |
|
run: |
|
||||||
./lightpanda serve & echo $! > LPD.pid
|
./lightpanda serve --log-level error & echo $! > LPD.pid
|
||||||
go run integration/main.go
|
go run integration/main.go
|
||||||
kill `cat LPD.pid`
|
kill `cat LPD.pid`
|
||||||
|
|||||||
234
.github/workflows/e2e-test.yml
vendored
234
.github/workflows/e2e-test.yml
vendored
@@ -9,15 +9,13 @@ env:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [main]
|
||||||
- main
|
|
||||||
paths:
|
paths:
|
||||||
- "build.zig"
|
|
||||||
- "src/**/*.zig"
|
|
||||||
- "src/*.zig"
|
|
||||||
- "vendor/zig-js-runtime"
|
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
- "vendor/**"
|
- "src/**"
|
||||||
|
- "build.zig"
|
||||||
|
- "build.zig.zon"
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
# By default GH trigger on types opened, synchronize and reopened.
|
# By default GH trigger on types opened, synchronize and reopened.
|
||||||
@@ -29,12 +27,10 @@ on:
|
|||||||
|
|
||||||
paths:
|
paths:
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
|
- "src/**"
|
||||||
- "build.zig"
|
- "build.zig"
|
||||||
- "src/**/*.zig"
|
- "build.zig.zon"
|
||||||
- "src/*.zig"
|
|
||||||
- "vendor/**"
|
|
||||||
- ".github/**"
|
|
||||||
- "vendor/**"
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -52,18 +48,14 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: zig build release
|
- name: zig build release
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
path: |
|
path: |
|
||||||
@@ -78,7 +70,7 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -86,7 +78,7 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
@@ -106,7 +98,7 @@ jobs:
|
|||||||
- name: run end to end tests through proxy
|
- name: run end to end tests through proxy
|
||||||
run: |
|
run: |
|
||||||
./proxy/proxy & echo $! > PROXY.id
|
./proxy/proxy & echo $! > PROXY.id
|
||||||
./lightpanda serve --http_proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid
|
./lightpanda serve --http-proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid
|
||||||
go run runner/main.go
|
go run runner/main.go
|
||||||
kill `cat LPD.pid` `cat PROXY.id`
|
kill `cat LPD.pid` `cat PROXY.id`
|
||||||
|
|
||||||
@@ -119,21 +111,16 @@ jobs:
|
|||||||
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
|
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
|
||||||
kill `cat LPD.pid` `cat PROXY.id`
|
kill `cat LPD.pid` `cat PROXY.id`
|
||||||
|
|
||||||
cdp-and-hyperfine-bench:
|
# e2e tests w/ web-bot-auth configuration on.
|
||||||
name: cdp-and-hyperfine-bench
|
wba-demo-scripts:
|
||||||
|
name: wba-demo-scripts
|
||||||
needs: zig-build-release
|
needs: zig-build-release
|
||||||
|
|
||||||
env:
|
runs-on: ubuntu-latest
|
||||||
MAX_MEMORY: 28000
|
|
||||||
MAX_AVG_DURATION: 23
|
|
||||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
|
||||||
|
|
||||||
# use a self host runner.
|
|
||||||
runs-on: lpd-bench-hetzner
|
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -141,7 +128,124 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
- run: echo "${{ secrets.WBA_PRIVATE_KEY_PEM }}" > private_key.pem
|
||||||
|
|
||||||
|
- name: run end to end tests
|
||||||
|
run: |
|
||||||
|
./lightpanda serve \
|
||||||
|
--web-bot-auth-key-file private_key.pem \
|
||||||
|
--web-bot-auth-keyid ${{ vars.WBA_KEY_ID }} \
|
||||||
|
--web-bot-auth-domain ${{ vars.WBA_DOMAIN }} \
|
||||||
|
& echo $! > LPD.pid
|
||||||
|
go run runner/main.go
|
||||||
|
kill `cat LPD.pid`
|
||||||
|
|
||||||
|
- name: build proxy
|
||||||
|
run: |
|
||||||
|
cd proxy
|
||||||
|
go build
|
||||||
|
|
||||||
|
- name: run end to end tests through proxy
|
||||||
|
run: |
|
||||||
|
./proxy/proxy & echo $! > PROXY.id
|
||||||
|
./lightpanda serve \
|
||||||
|
--web-bot-auth-key-file private_key.pem \
|
||||||
|
--web-bot-auth-keyid ${{ vars.WBA_KEY_ID }} \
|
||||||
|
--web-bot-auth-domain ${{ vars.WBA_DOMAIN }} \
|
||||||
|
--http-proxy 'http://127.0.0.1:3000' \
|
||||||
|
& echo $! > LPD.pid
|
||||||
|
go run runner/main.go
|
||||||
|
kill `cat LPD.pid` `cat PROXY.id`
|
||||||
|
|
||||||
|
- name: run request interception through proxy
|
||||||
|
run: |
|
||||||
|
export PROXY_USERNAME=username PROXY_PASSWORD=password
|
||||||
|
./proxy/proxy & echo $! > PROXY.id
|
||||||
|
./lightpanda serve & echo $! > LPD.pid
|
||||||
|
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
|
||||||
|
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
|
||||||
|
kill `cat LPD.pid` `cat PROXY.id`
|
||||||
|
|
||||||
|
wba-test:
|
||||||
|
name: wba-test
|
||||||
|
needs: zig-build-release
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
|
||||||
|
# Don't execute on PR
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
repository: 'lightpanda-io/demo'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: download artifact
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
# force a wakup of the auth server before requesting it w/ the test itself
|
||||||
|
- run: curl https://${{ vars.WBA_DOMAIN }}
|
||||||
|
|
||||||
|
- name: run wba test
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
node webbotauth/validator.js &
|
||||||
|
VALIDATOR_PID=$!
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
exec 3<<< "${{ secrets.WBA_PRIVATE_KEY_PEM }}"
|
||||||
|
|
||||||
|
./lightpanda fetch --dump http://127.0.0.1:8989/ \
|
||||||
|
--web-bot-auth-key-file /proc/self/fd/3 \
|
||||||
|
--web-bot-auth-keyid ${{ vars.WBA_KEY_ID }} \
|
||||||
|
--web-bot-auth-domain ${{ vars.WBA_DOMAIN }}
|
||||||
|
|
||||||
|
wait $VALIDATOR_PID
|
||||||
|
exec 3>&-
|
||||||
|
|
||||||
|
cdp-and-hyperfine-bench:
|
||||||
|
name: cdp-and-hyperfine-bench
|
||||||
|
needs: zig-build-release
|
||||||
|
|
||||||
|
env:
|
||||||
|
MAX_VmHWM: 28000 # 28MB (KB)
|
||||||
|
MAX_CG_PEAK: 8000 # 8MB (KB)
|
||||||
|
MAX_AVG_DURATION: 17
|
||||||
|
|
||||||
|
# How to give cgroups access to the user actions-runner on the host:
|
||||||
|
# $ sudo apt install cgroup-tools
|
||||||
|
# $ sudo chmod o+w /sys/fs/cgroup/cgroup.procs
|
||||||
|
# $ sudo mkdir -p /sys/fs/cgroup/actions-runner
|
||||||
|
# $ sudo chown -R actions-runner:actions-runner /sys/fs/cgroup/actions-runner
|
||||||
|
CG_ROOT: /sys/fs/cgroup
|
||||||
|
CG: actions-runner/lpd_${{ github.run_id }}_${{ github.run_attempt }}
|
||||||
|
|
||||||
|
# use a self host runner.
|
||||||
|
runs-on: lpd-bench-hetzner
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
repository: 'lightpanda-io/demo'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- run: npm install
|
||||||
|
|
||||||
|
- name: download artifact
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
@@ -152,22 +256,53 @@ jobs:
|
|||||||
go run ws/main.go & echo $! > WS.pid
|
go run ws/main.go & echo $! > WS.pid
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
|
- name: run lightpanda in cgroup
|
||||||
|
run: |
|
||||||
|
if [ ! -f /sys/fs/cgroup/cgroup.controllers ]; then
|
||||||
|
echo "cgroup v2 not available: /sys/fs/cgroup/cgroup.controllers missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p $CG_ROOT/$CG
|
||||||
|
cgexec -g memory:$CG ./lightpanda serve & echo $! > LPD.pid
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
- name: run puppeteer
|
- name: run puppeteer
|
||||||
run: |
|
run: |
|
||||||
./lightpanda serve & echo $! > LPD.pid
|
|
||||||
sleep 2
|
|
||||||
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
|
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
|
||||||
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
|
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
|
||||||
kill `cat LPD.pid`
|
kill `cat LPD.pid`
|
||||||
|
|
||||||
|
PID=$(cat LPD.pid)
|
||||||
|
while kill -0 $PID 2>/dev/null; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ ! -f $CG_ROOT/$CG/memory.peak ]; then
|
||||||
|
echo "memory.peak not available in $CG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cat $CG_ROOT/$CG/memory.peak > LPD.cg_mem_peak
|
||||||
|
|
||||||
- name: puppeteer result
|
- name: puppeteer result
|
||||||
run: cat puppeteer.out
|
run: cat puppeteer.out
|
||||||
|
|
||||||
- name: memory regression
|
- name: cgroup memory regression
|
||||||
|
run: |
|
||||||
|
PEAK_BYTES=$(cat LPD.cg_mem_peak)
|
||||||
|
PEAK_KB=$((PEAK_BYTES / 1024))
|
||||||
|
echo "memory.peak_bytes=$PEAK_BYTES"
|
||||||
|
echo "memory.peak_kb=$PEAK_KB"
|
||||||
|
test "$PEAK_KB" -le "$MAX_CG_PEAK"
|
||||||
|
|
||||||
|
- name: virtual memory regression
|
||||||
run: |
|
run: |
|
||||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||||
echo "Peak resident set size: $LPD_VmHWM"
|
echo "Peak resident set size: $LPD_VmHWM"
|
||||||
test "$LPD_VmHWM" -le "$MAX_MEMORY"
|
test "$LPD_VmHWM" -le "$MAX_VmHWM"
|
||||||
|
|
||||||
|
- name: cleanup cgroup
|
||||||
|
run: rmdir $CG_ROOT/$CG
|
||||||
|
|
||||||
- name: duration regression
|
- name: duration regression
|
||||||
run: |
|
run: |
|
||||||
@@ -180,7 +315,8 @@ jobs:
|
|||||||
export AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
export AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
||||||
export TOTAL_DURATION=`cat puppeteer.out|grep 'total duration'|sed 's/total duration (ms) //'`
|
export TOTAL_DURATION=`cat puppeteer.out|grep 'total duration'|sed 's/total duration (ms) //'`
|
||||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||||
echo "{\"duration_total\":${TOTAL_DURATION},\"duration_avg\":${AVG_DURATION},\"mem_peak\":${LPD_VmHWM}}" > bench.json
|
export LPD_CG_PEAK_KB=$(( $(cat LPD.cg_mem_peak) / 1024 ))
|
||||||
|
echo "{\"duration_total\":${TOTAL_DURATION},\"duration_avg\":${AVG_DURATION},\"mem_peak\":${LPD_VmHWM},\"cg_mem_peak\":${LPD_CG_PEAK_KB}}" > bench.json
|
||||||
cat bench.json
|
cat bench.json
|
||||||
|
|
||||||
- name: run hyperfine
|
- name: run hyperfine
|
||||||
@@ -195,7 +331,7 @@ jobs:
|
|||||||
echo "${{github.sha}}" > commit.txt
|
echo "${{github.sha}}" > commit.txt
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
path: |
|
path: |
|
||||||
@@ -218,12 +354,12 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
||||||
credentials:
|
credentials:
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
|
|
||||||
@@ -232,3 +368,19 @@ jobs:
|
|||||||
|
|
||||||
- name: format and send json result
|
- name: format and send json result
|
||||||
run: /perf-fmt hyperfine ${{ github.sha }} hyperfine.json
|
run: /perf-fmt hyperfine ${{ github.sha }} hyperfine.json
|
||||||
|
|
||||||
|
browser-fetch:
|
||||||
|
name: browser fetch
|
||||||
|
needs: zig-build-release
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: download artifact
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
- run: ./lightpanda fetch https://demo-browser.lightpanda.io/campfire-commerce/
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ env:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
|
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
|
||||||
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
||||||
|
|
||||||
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
|
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
|
||||||
|
VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dversion={0}', github.ref_name) || '-Dversion=nightly' }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -27,26 +29,23 @@ jobs:
|
|||||||
OS: linux
|
OS: linux
|
||||||
|
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
timeout-minutes: 15
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: v8 snapshot
|
- name: v8 snapshot
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 ${{ env.VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -62,6 +61,7 @@ jobs:
|
|||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: ${{ env.RELEASE }}
|
tag: ${{ env.RELEASE }}
|
||||||
|
makeLatest: true
|
||||||
|
|
||||||
build-linux-aarch64:
|
build-linux-aarch64:
|
||||||
env:
|
env:
|
||||||
@@ -69,26 +69,23 @@ jobs:
|
|||||||
OS: linux
|
OS: linux
|
||||||
|
|
||||||
runs-on: ubuntu-22.04-arm
|
runs-on: ubuntu-22.04-arm
|
||||||
timeout-minutes: 15
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: v8 snapshot
|
- name: v8 snapshot
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic ${{ env.VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -104,6 +101,7 @@ jobs:
|
|||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: ${{ env.RELEASE }}
|
tag: ${{ env.RELEASE }}
|
||||||
|
makeLatest: true
|
||||||
|
|
||||||
build-macos-aarch64:
|
build-macos-aarch64:
|
||||||
env:
|
env:
|
||||||
@@ -113,26 +111,23 @@ jobs:
|
|||||||
# macos-14 runs on arm CPU. see
|
# macos-14 runs on arm CPU. see
|
||||||
# https://github.com/actions/runner-images?tab=readme-ov-file
|
# https://github.com/actions/runner-images?tab=readme-ov-file
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
timeout-minutes: 15
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: v8 snapshot
|
- name: v8 snapshot
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast ${{ env.VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -148,6 +143,7 @@ jobs:
|
|||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: ${{ env.RELEASE }}
|
tag: ${{ env.RELEASE }}
|
||||||
|
makeLatest: true
|
||||||
|
|
||||||
build-macos-x86_64:
|
build-macos-x86_64:
|
||||||
env:
|
env:
|
||||||
@@ -155,26 +151,23 @@ jobs:
|
|||||||
OS: macos
|
OS: macos
|
||||||
|
|
||||||
runs-on: macos-14-large
|
runs-on: macos-14-large
|
||||||
timeout-minutes: 15
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
os: ${{env.OS}}
|
os: ${{env.OS}}
|
||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
mode: 'release'
|
|
||||||
|
|
||||||
- name: v8 snapshot
|
- name: v8 snapshot
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast ${{ env.VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -190,3 +183,4 @@ jobs:
|
|||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: ${{ env.RELEASE }}
|
tag: ${{ env.RELEASE }}
|
||||||
|
makeLatest: true
|
||||||
110
.github/workflows/wpt.yml
vendored
110
.github/workflows/wpt.yml
vendored
@@ -5,40 +5,126 @@ env:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
||||||
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
||||||
|
AWS_CF_DISTRIBUTION: ${{ vars.AWS_CF_DISTRIBUTION }}
|
||||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "23 2 * * *"
|
- cron: "21 2 * * *"
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
wpt:
|
wpt-build-release:
|
||||||
name: web platform tests json output
|
name: zig build release
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
env:
|
||||||
timeout-minutes: 90
|
ARCH: aarch64
|
||||||
|
OS: linux
|
||||||
|
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
with:
|
||||||
|
os: ${{env.OS}}
|
||||||
|
arch: ${{env.ARCH}}
|
||||||
|
|
||||||
- name: json output
|
- name: v8 snapshot
|
||||||
run: zig build wpt -- --json > wpt.json
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
|
- name: zig build release
|
||||||
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic
|
||||||
|
|
||||||
|
- name: upload artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
path: |
|
||||||
|
zig-out/bin/lightpanda
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
wpt-build-runner:
|
||||||
|
name: build wpt runner
|
||||||
|
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
repository: 'lightpanda-io/demo'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
cd ./wptrunner
|
||||||
|
CGO_ENABLED=0 go build
|
||||||
|
|
||||||
|
- name: upload artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: wptrunner
|
||||||
|
path: |
|
||||||
|
wptrunner/wptrunner
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
run-wpt:
|
||||||
|
name: web platform tests json output
|
||||||
|
needs:
|
||||||
|
- wpt-build-release
|
||||||
|
- wpt-build-runner
|
||||||
|
|
||||||
|
# use a self host runner.
|
||||||
|
runs-on: lpd-wpt-aws
|
||||||
|
timeout-minutes: 600
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: fork
|
||||||
|
repository: 'lightpanda-io/wpt'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# The hosts are configured manually on the self host runner.
|
||||||
|
# - name: create custom hosts
|
||||||
|
# run: ./wpt make-hosts-file | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
|
- name: generate manifest
|
||||||
|
run: ./wpt manifest
|
||||||
|
|
||||||
|
- name: download lightpanda release
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
- name: download wptrunner
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: wptrunner
|
||||||
|
|
||||||
|
- run: chmod a+x ./wptrunner
|
||||||
|
|
||||||
|
- name: run test with json output
|
||||||
|
run: |
|
||||||
|
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
||||||
|
sleep 20s
|
||||||
|
./wptrunner -lpd-path ./lightpanda -json -concurrency 5 -pool 5 --mem-limit 400 > wpt.json
|
||||||
|
kill `cat WPT.pid`
|
||||||
|
|
||||||
- name: write commit
|
- name: write commit
|
||||||
run: |
|
run: |
|
||||||
echo "${{github.sha}}" > commit.txt
|
echo "${{github.sha}}" > commit.txt
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: wpt-results
|
name: wpt-results
|
||||||
path: |
|
path: |
|
||||||
@@ -48,7 +134,7 @@ jobs:
|
|||||||
|
|
||||||
perf-fmt:
|
perf-fmt:
|
||||||
name: perf-fmt
|
name: perf-fmt
|
||||||
needs: wpt
|
needs: run-wpt
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
@@ -61,7 +147,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: wpt-results
|
name: wpt-results
|
||||||
|
|
||||||
|
|||||||
60
.github/workflows/zig-fmt.yml
vendored
60
.github/workflows/zig-fmt.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: zig-fmt
|
|
||||||
|
|
||||||
on:
|
|
||||||
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"
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
zig-fmt:
|
|
||||||
name: zig fmt
|
|
||||||
|
|
||||||
# Don't run the CI with draft PR.
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
|
||||||
- uses: mlugg/setup-zig@v2
|
|
||||||
|
|
||||||
- name: Run zig fmt
|
|
||||||
id: fmt
|
|
||||||
run: |
|
|
||||||
zig fmt --check ./*.zig ./**/*.zig 2> zig-fmt.err > zig-fmt.err2 || echo "Failed"
|
|
||||||
delimiter="$(openssl rand -hex 8)"
|
|
||||||
echo "zig_fmt_errs<<${delimiter}" >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
if [ -s zig-fmt.err ]; then
|
|
||||||
echo "// The following errors occurred:" >> "${GITHUB_OUTPUT}"
|
|
||||||
cat zig-fmt.err >> "${GITHUB_OUTPUT}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -s zig-fmt.err2 ]; then
|
|
||||||
echo "// The following files were not formatted:" >> "${GITHUB_OUTPUT}"
|
|
||||||
cat zig-fmt.err2 >> "${GITHUB_OUTPUT}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "${delimiter}" >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
- name: Fail the job
|
|
||||||
if: steps.fmt.outputs.zig_fmt_errs != ''
|
|
||||||
run: exit 1
|
|
||||||
127
.github/workflows/zig-test.yml
vendored
127
.github/workflows/zig-test.yml
vendored
@@ -5,20 +5,18 @@ env:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
||||||
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
||||||
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [main]
|
||||||
- main
|
|
||||||
paths:
|
paths:
|
||||||
- "build.zig"
|
|
||||||
- "src/**/*.zig"
|
|
||||||
- "src/*.zig"
|
|
||||||
- "vendor/zig-js-runtime"
|
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
- "vendor/**"
|
- "src/**"
|
||||||
pull_request:
|
- "build.zig"
|
||||||
|
- "build.zig.zon"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
# By default GH trigger on types opened, synchronize and reopened.
|
# 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
|
# 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
|
# Since we skip the job when the PR is in draft state, we want to force CI
|
||||||
@@ -28,89 +26,95 @@ on:
|
|||||||
|
|
||||||
paths:
|
paths:
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
|
- "src/**"
|
||||||
- "build.zig"
|
- "build.zig"
|
||||||
- "src/**/*.zig"
|
- "build.zig.zon"
|
||||||
- "src/*.zig"
|
|
||||||
- "vendor/**"
|
|
||||||
- ".github/**"
|
|
||||||
- "vendor/**"
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
zig-build-dev:
|
zig-fmt:
|
||||||
name: zig build dev
|
name: zig fmt
|
||||||
|
|
||||||
# Don't run the CI with draft PR.
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
||||||
|
- uses: mlugg/setup-zig@v2
|
||||||
|
|
||||||
|
- name: Run zig fmt
|
||||||
|
id: fmt
|
||||||
|
run: |
|
||||||
|
zig fmt --check ./*.zig ./**/*.zig 2> zig-fmt.err > zig-fmt.err2 || echo "Failed"
|
||||||
|
delimiter="$(openssl rand -hex 8)"
|
||||||
|
echo "zig_fmt_errs<<${delimiter}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
if [ -s zig-fmt.err ]; then
|
||||||
|
echo "// The following errors occurred:" >> "${GITHUB_OUTPUT}"
|
||||||
|
cat zig-fmt.err >> "${GITHUB_OUTPUT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -s zig-fmt.err2 ]; then
|
||||||
|
echo "// The following files were not formatted:" >> "${GITHUB_OUTPUT}"
|
||||||
|
cat zig-fmt.err2 >> "${GITHUB_OUTPUT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${delimiter}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Fail the job
|
||||||
|
if: steps.fmt.outputs.zig_fmt_errs != ''
|
||||||
|
run: exit 1
|
||||||
|
|
||||||
|
zig-test-debug:
|
||||||
|
name: zig test using v8 in debug mode
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: zig build debug
|
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a
|
|
||||||
|
|
||||||
- name: upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-dev
|
debug: true
|
||||||
path: |
|
|
||||||
zig-out/bin/lightpanda
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
browser-fetch:
|
- name: zig build test
|
||||||
name: browser fetch
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8_debug.a -Dtsan=true test
|
||||||
needs: zig-build-dev
|
|
||||||
|
|
||||||
# Don't run the CI with draft PR.
|
zig-test-release:
|
||||||
if: github.event.pull_request.draft == false
|
name: zig test
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: download artifact
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: lightpanda-build-dev
|
|
||||||
|
|
||||||
- run: chmod a+x ./lightpanda
|
|
||||||
|
|
||||||
- run: ./lightpanda fetch https://httpbin.io/xhr/get
|
|
||||||
|
|
||||||
zig-test:
|
|
||||||
name: zig test
|
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
# Don't run the CI with draft PR.
|
|
||||||
if: github.event.pull_request.draft == false
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: zig build test
|
- name: zig build test
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a test -- --json > bench.json
|
run: METRICS=true zig build -Dprebuilt_v8_path=v8/libc_v8.a test > bench.json
|
||||||
|
|
||||||
- name: write commit
|
- name: write commit
|
||||||
run: |
|
run: |
|
||||||
echo "${{github.sha}}" > commit.txt
|
echo "${{github.sha}}" > commit.txt
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
path: |
|
path: |
|
||||||
@@ -120,23 +124,22 @@ jobs:
|
|||||||
|
|
||||||
bench-fmt:
|
bench-fmt:
|
||||||
name: perf-fmt
|
name: perf-fmt
|
||||||
needs: zig-test
|
needs: zig-test-release
|
||||||
|
|
||||||
# Don't execute on PR
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
||||||
credentials:
|
credentials:
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
|
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,11 +1,6 @@
|
|||||||
zig-cache
|
|
||||||
/.zig-cache/
|
/.zig-cache/
|
||||||
/.lp-cache/
|
/.lp-cache/
|
||||||
zig-out
|
zig-out
|
||||||
/vendor/netsurf/out
|
|
||||||
/vendor/libiconv/
|
|
||||||
lightpanda.id
|
lightpanda.id
|
||||||
/v8/
|
|
||||||
/build/
|
|
||||||
/src/html5ever/target/
|
/src/html5ever/target/
|
||||||
src/snapshot.bin
|
src/snapshot.bin
|
||||||
|
|||||||
15
.gitmodules
vendored
15
.gitmodules
vendored
@@ -1,15 +0,0 @@
|
|||||||
[submodule "tests/wpt"]
|
|
||||||
path = tests/wpt
|
|
||||||
url = https://github.com/lightpanda-io/wpt
|
|
||||||
[submodule "vendor/nghttp2"]
|
|
||||||
path = vendor/nghttp2
|
|
||||||
url = https://github.com/nghttp2/nghttp2.git
|
|
||||||
[submodule "vendor/zlib"]
|
|
||||||
path = vendor/zlib
|
|
||||||
url = https://github.com/madler/zlib.git
|
|
||||||
[submodule "vendor/curl"]
|
|
||||||
path = vendor/curl
|
|
||||||
url = https://github.com/curl/curl.git
|
|
||||||
[submodule "vendor/brotli"]
|
|
||||||
path = vendor/brotli
|
|
||||||
url = https://github.com/google/brotli
|
|
||||||
12
Dockerfile
12
Dockerfile
@@ -3,11 +3,12 @@ FROM debian:stable-slim
|
|||||||
ARG MINISIG=0.12
|
ARG MINISIG=0.12
|
||||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||||
ARG V8=14.0.365.4
|
ARG V8=14.0.365.4
|
||||||
ARG ZIG_V8=v0.2.2
|
ARG ZIG_V8=v0.3.7
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update -yq && \
|
RUN apt-get update -yq && \
|
||||||
apt-get install -yq xz-utils ca-certificates \
|
apt-get install -yq xz-utils ca-certificates \
|
||||||
|
pkg-config libglib2.0-dev \
|
||||||
clang make curl git
|
clang make curl git
|
||||||
|
|
||||||
# Get Rust
|
# Get Rust
|
||||||
@@ -35,10 +36,6 @@ RUN ZIG=$(grep '\.minimum_zig_version = "' "build.zig.zon" | cut -d'"' -f2) && \
|
|||||||
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
|
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
|
||||||
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
|
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
|
||||||
|
|
||||||
# install deps
|
|
||||||
RUN git submodule init && \
|
|
||||||
git submodule update --recursive
|
|
||||||
|
|
||||||
# download and install v8
|
# download and install v8
|
||||||
RUN case $TARGETPLATFORM in \
|
RUN case $TARGETPLATFORM in \
|
||||||
"linux/arm64") ARCH="aarch64" ;; \
|
"linux/arm64") ARCH="aarch64" ;; \
|
||||||
@@ -56,8 +53,7 @@ RUN zig build -Doptimize=ReleaseFast \
|
|||||||
# build release
|
# build release
|
||||||
RUN zig build -Doptimize=ReleaseFast \
|
RUN zig build -Doptimize=ReleaseFast \
|
||||||
-Dsnapshot_path=../../snapshot.bin \
|
-Dsnapshot_path=../../snapshot.bin \
|
||||||
-Dprebuilt_v8_path=v8/libc_v8.a \
|
-Dprebuilt_v8_path=v8/libc_v8.a
|
||||||
-Dgit_commit=$(git rev-parse --short HEAD)
|
|
||||||
|
|
||||||
FROM debian:stable-slim
|
FROM debian:stable-slim
|
||||||
|
|
||||||
@@ -78,4 +74,4 @@ EXPOSE 9222/tcp
|
|||||||
# Using "tini" as PID1 ensures that signals work as expected, so e.g. "docker stop" will not hang.
|
# Using "tini" as PID1 ensures that signals work as expected, so e.g. "docker stop" will not hang.
|
||||||
# (See https://github.com/krallin/tini#why-tini).
|
# (See https://github.com/krallin/tini#why-tini).
|
||||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222", "--log_level", "info"]
|
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222", "--log-level", "info"]
|
||||||
|
|||||||
@@ -4,11 +4,3 @@ License names used in this document are as per [SPDX License
|
|||||||
List](https://spdx.org/licenses/).
|
List](https://spdx.org/licenses/).
|
||||||
|
|
||||||
The default license for this project is [AGPL-3.0-only](LICENSE).
|
The default license for this project is [AGPL-3.0-only](LICENSE).
|
||||||
|
|
||||||
The following directories and their subdirectories are licensed under their
|
|
||||||
original upstream licenses:
|
|
||||||
|
|
||||||
```
|
|
||||||
vendor/
|
|
||||||
tests/wpt/
|
|
||||||
```
|
|
||||||
|
|||||||
30
Makefile
30
Makefile
@@ -47,7 +47,7 @@ help:
|
|||||||
|
|
||||||
# $(ZIG) commands
|
# $(ZIG) commands
|
||||||
# ------------
|
# ------------
|
||||||
.PHONY: build build-v8-snapshot build-dev run run-release shell test bench wpt data end2end
|
.PHONY: build build-v8-snapshot build-dev run run-release test bench data end2end
|
||||||
|
|
||||||
## Build v8 snapshot
|
## Build v8 snapshot
|
||||||
build-v8-snapshot:
|
build-v8-snapshot:
|
||||||
@@ -57,14 +57,14 @@ build-v8-snapshot:
|
|||||||
|
|
||||||
## Build in release-fast mode
|
## Build in release-fast mode
|
||||||
build: build-v8-snapshot
|
build: build-v8-snapshot
|
||||||
@printf "\033[36mBuilding (release safe)...\033[0m\n"
|
@printf "\033[36mBuilding (release fast)...\033[0m\n"
|
||||||
@$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
@$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
@printf "\033[33mBuild OK\033[0m\n"
|
@printf "\033[33mBuild OK\033[0m\n"
|
||||||
|
|
||||||
## Build in debug mode
|
## Build in debug mode
|
||||||
build-dev:
|
build-dev:
|
||||||
@printf "\033[36mBuilding (debug)...\033[0m\n"
|
@printf "\033[36mBuilding (debug)...\033[0m\n"
|
||||||
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
@$(ZIG) build || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
@printf "\033[33mBuild OK\033[0m\n"
|
@printf "\033[33mBuild OK\033[0m\n"
|
||||||
|
|
||||||
## Run the server in release mode
|
## Run the server in release mode
|
||||||
@@ -77,20 +77,6 @@ run-debug: build-dev
|
|||||||
@printf "\033[36mRunning...\033[0m\n"
|
@printf "\033[36mRunning...\033[0m\n"
|
||||||
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
|
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
|
||||||
|
|
||||||
## Run a JS shell in debug mode
|
|
||||||
shell:
|
|
||||||
@printf "\033[36mBuilding shell...\033[0m\n"
|
|
||||||
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
|
||||||
|
|
||||||
## Run WPT tests
|
|
||||||
wpt:
|
|
||||||
@printf "\033[36mBuilding wpt...\033[0m\n"
|
|
||||||
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
|
||||||
|
|
||||||
wpt-summary:
|
|
||||||
@printf "\033[36mBuilding wpt...\033[0m\n"
|
|
||||||
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
|
||||||
|
|
||||||
## Test - `grep` is used to filter out the huge compile command on build
|
## Test - `grep` is used to filter out the huge compile command on build
|
||||||
ifeq ($(OS), macos)
|
ifeq ($(OS), macos)
|
||||||
test:
|
test:
|
||||||
@@ -111,13 +97,7 @@ end2end:
|
|||||||
# ------------
|
# ------------
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
|
|
||||||
## Install and build dependencies for release
|
install: build
|
||||||
install: install-submodule
|
|
||||||
|
|
||||||
data:
|
data:
|
||||||
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
||||||
|
|
||||||
## Init and update git submodule
|
|
||||||
install-submodule:
|
|
||||||
@git submodule init && \
|
|
||||||
git submodule update
|
|
||||||
|
|||||||
157
README.md
157
README.md
@@ -1,18 +1,32 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://lightpanda.io"><img src="https://cdn.lightpanda.io/assets/images/logo/lpd-logo.png" alt="Logo" height=170></a>
|
<a href="https://lightpanda.io"><img src="https://cdn.lightpanda.io/assets/images/logo/lpd-logo.png" alt="Logo" height=170></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">Lightpanda Browser</h1>
|
<h1 align="center">Lightpanda Browser</h1>
|
||||||
|
<p align="center">
|
||||||
|
<strong>The headless browser built from scratch for AI agents and automation.</strong><br>
|
||||||
|
Not a Chromium fork. Not a WebKit patch. A new browser, written in Zig.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p align="center"><a href="https://lightpanda.io/">lightpanda.io</a></p>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://github.com/lightpanda-io/browser/blob/main/LICENSE)
|
[](https://github.com/lightpanda-io/browser/blob/main/LICENSE)
|
||||||
[](https://twitter.com/lightpanda_io)
|
[](https://twitter.com/lightpanda_io)
|
||||||
[](https://github.com/lightpanda-io/browser)
|
[](https://github.com/lightpanda-io/browser)
|
||||||
|
[](https://discord.gg/K63XeymfB5)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/execution-time-v2.svg">
|
||||||
|
](https://github.com/lightpanda-io/demo)
|
||||||
|
 
|
||||||
|
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/memory-frame-v2.svg">
|
||||||
|
](https://github.com/lightpanda-io/demo)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
_chromedp requesting 933 real web pages over the network on a AWS EC2 m5.large instance.
|
||||||
|
See [benchmark details](https://github.com/lightpanda-io/demo/blob/main/BENCHMARKS.md#crawler-benchmark)._
|
||||||
|
|
||||||
Lightpanda is the open-source browser made for headless usage:
|
Lightpanda is the open-source browser made for headless usage:
|
||||||
|
|
||||||
@@ -26,16 +40,6 @@ Fast web automation for AI agents, LLM training, scraping and testing:
|
|||||||
- Exceptionally fast execution (11x faster than Chrome)
|
- Exceptionally fast execution (11x faster than Chrome)
|
||||||
- Instant startup
|
- Instant startup
|
||||||
|
|
||||||
[<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>
|
|
||||||
|
|
||||||
_Puppeteer requesting 100 pages from a local website on a AWS EC2 m5.large instance.
|
|
||||||
See [benchmark details](https://github.com/lightpanda-io/demo)._
|
|
||||||
|
|
||||||
[^1]: **Playwright support disclaimer:**
|
[^1]: **Playwright support disclaimer:**
|
||||||
Due to the nature of Playwright, a script that works with the current version of the browser may not function correctly with a future version. Playwright uses an intermediate JavaScript layer that selects an execution strategy based on the browser's available features. If Lightpanda adds a new [Web API](https://developer.mozilla.org/en-US/docs/Web/API), Playwright may choose to execute different code for the same script. This new code path could attempt to use features that are not yet implemented. Lightpanda makes an effort to add compatibility tests, but we can't cover all scenarios. If you encounter an issue, please create a [GitHub issue](https://github.com/lightpanda-io/browser/issues) and include the last known working version of the script.
|
Due to the nature of Playwright, a script that works with the current version of the browser may not function correctly with a future version. Playwright uses an intermediate JavaScript layer that selects an execution strategy based on the browser's available features. If Lightpanda adds a new [Web API](https://developer.mozilla.org/en-US/docs/Web/API), Playwright may choose to execute different code for the same script. This new code path could attempt to use features that are not yet implemented. Lightpanda makes an effort to add compatibility tests, but we can't cover all scenarios. If you encounter an issue, please create a [GitHub issue](https://github.com/lightpanda-io/browser/issues) and include the last known working version of the script.
|
||||||
|
|
||||||
@@ -78,23 +82,49 @@ docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
|
|||||||
### Dump a URL
|
### Dump a URL
|
||||||
|
|
||||||
```console
|
```console
|
||||||
./lightpanda fetch --dump https://lightpanda.io
|
./lightpanda fetch --obey-robots --log-format pretty --log-level info https://demo-browser.lightpanda.io/campfire-commerce/
|
||||||
```
|
```
|
||||||
```console
|
```console
|
||||||
info(browser): GET https://lightpanda.io/ http.Status.ok
|
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||||
info(browser): fetch script https://api.website.lightpanda.io/js/script.js: http.Status.ok
|
disabled = false
|
||||||
info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeError: Cannot read properties of undefined (reading 'pushState')
|
|
||||||
|
INFO page : navigate . . . . . . . . . . . . . . . . . . . . [+6ms]
|
||||||
|
url = https://demo-browser.lightpanda.io/campfire-commerce/
|
||||||
|
method = GET
|
||||||
|
reason = address_bar
|
||||||
|
body = false
|
||||||
|
req_id = 1
|
||||||
|
|
||||||
|
INFO browser : executing script . . . . . . . . . . . . . . [+118ms]
|
||||||
|
src = https://demo-browser.lightpanda.io/campfire-commerce/script.js
|
||||||
|
kind = javascript
|
||||||
|
cacheable = true
|
||||||
|
|
||||||
|
INFO http : request complete . . . . . . . . . . . . . . . . [+140ms]
|
||||||
|
source = xhr
|
||||||
|
url = https://demo-browser.lightpanda.io/campfire-commerce/json/product.json
|
||||||
|
status = 200
|
||||||
|
len = 4770
|
||||||
|
|
||||||
|
INFO http : request complete . . . . . . . . . . . . . . . . [+141ms]
|
||||||
|
source = fetch
|
||||||
|
url = https://demo-browser.lightpanda.io/campfire-commerce/json/reviews.json
|
||||||
|
status = 200
|
||||||
|
len = 1615
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start a CDP server
|
### Start a CDP server
|
||||||
|
|
||||||
```console
|
```console
|
||||||
./lightpanda serve --host 127.0.0.1 --port 9222
|
./lightpanda serve --obey-robots --log-format pretty --log-level info --host 127.0.0.1 --port 9222
|
||||||
```
|
```
|
||||||
```console
|
```console
|
||||||
info(websocket): starting blocking worker to listen on 127.0.0.1:9222
|
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||||
info(server): accepting new conn...
|
disabled = false
|
||||||
|
|
||||||
|
INFO app : server running . . . . . . . . . . . . . . . . . [+0ms]
|
||||||
|
address = 127.0.0.1:9222
|
||||||
```
|
```
|
||||||
|
|
||||||
Once the CDP server started, you can run a Puppeteer script by configuring the
|
Once the CDP server started, you can run a Puppeteer script by configuring the
|
||||||
@@ -115,7 +145,7 @@ const context = await browser.createBrowserContext();
|
|||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
// Dump all the links from the page.
|
// Dump all the links from the page.
|
||||||
await page.goto('https://wikipedia.com/');
|
await page.goto('https://demo-browser.lightpanda.io/amiibo/', {waitUntil: "networkidle0"});
|
||||||
|
|
||||||
const links = await page.evaluate(() => {
|
const links = await page.evaluate(() => {
|
||||||
return Array.from(document.querySelectorAll('a')).map(row => {
|
return Array.from(document.querySelectorAll('a')).map(row => {
|
||||||
@@ -140,6 +170,7 @@ You may still encounter errors or crashes. Please open an issue with specifics i
|
|||||||
|
|
||||||
Here are the key features we have implemented:
|
Here are the key features we have implemented:
|
||||||
|
|
||||||
|
- [ ] CORS [#2015](https://github.com/lightpanda-io/browser/issues/2015)
|
||||||
- [x] HTTP loader ([Libcurl](https://curl.se/libcurl/))
|
- [x] HTTP loader ([Libcurl](https://curl.se/libcurl/))
|
||||||
- [x] HTML parser ([html5ever](https://github.com/servo/html5ever))
|
- [x] HTML parser ([html5ever](https://github.com/servo/html5ever))
|
||||||
- [x] DOM tree
|
- [x] DOM tree
|
||||||
@@ -156,11 +187,10 @@ Here are the key features we have implemented:
|
|||||||
- [x] Custom HTTP headers
|
- [x] Custom HTTP headers
|
||||||
- [x] Proxy support
|
- [x] Proxy support
|
||||||
- [x] Network interception
|
- [x] Network interception
|
||||||
|
- [x] Respect `robots.txt` with option `--obey-robots`
|
||||||
|
|
||||||
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||||
|
|
||||||
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
|
|
||||||
|
|
||||||
## Build from sources
|
## Build from sources
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
@@ -169,15 +199,16 @@ Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to
|
|||||||
install it with the right version in order to build the project.
|
install it with the right version in order to build the project.
|
||||||
|
|
||||||
Lightpanda also depends on
|
Lightpanda also depends on
|
||||||
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
|
[v8](https://chromium.googlesource.com/v8/v8.git),
|
||||||
[Libcurl](https://curl.se/libcurl/) and [html5ever](https://github.com/servo/html5ever).
|
[Libcurl](https://curl.se/libcurl/) and [html5ever](https://github.com/servo/html5ever).
|
||||||
|
|
||||||
To be able to build the v8 engine for zig-js-runtime, you have to install some libs:
|
To be able to build the v8 engine, you have to install some libs:
|
||||||
|
|
||||||
For **Debian/Ubuntu based Linux**:
|
For **Debian/Ubuntu based Linux**:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt install xz-utils ca-certificates \
|
sudo apt install xz-utils ca-certificates \
|
||||||
|
pkg-config libglib2.0-dev \
|
||||||
clang make curl git
|
clang make curl git
|
||||||
```
|
```
|
||||||
You also need to [install Rust](https://rust-lang.org/tools/install/).
|
You also need to [install Rust](https://rust-lang.org/tools/install/).
|
||||||
@@ -192,18 +223,6 @@ For **MacOS**, you need cmake and [Rust](https://rust-lang.org/tools/install/).
|
|||||||
brew install cmake
|
brew install cmake
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Git submodules
|
|
||||||
|
|
||||||
The project uses git submodules for dependencies.
|
|
||||||
|
|
||||||
To init or update the submodules in the `vendor/` directory:
|
|
||||||
|
|
||||||
```
|
|
||||||
make install-submodule
|
|
||||||
```
|
|
||||||
|
|
||||||
This is an alias for `git submodule init && git submodule update`.
|
|
||||||
|
|
||||||
### Build and run
|
### Build and run
|
||||||
|
|
||||||
You an build the entire browser with `make build` or `make build-dev` for debug
|
You an build the entire browser with `make build` or `make build-dev` for debug
|
||||||
@@ -253,35 +272,75 @@ make end2end
|
|||||||
Lightpanda is tested against the standardized [Web Platform
|
Lightpanda is tested against the standardized [Web Platform
|
||||||
Tests](https://web-platform-tests.org/).
|
Tests](https://web-platform-tests.org/).
|
||||||
|
|
||||||
The relevant tests cases are committed in a [dedicated repository](https://github.com/lightpanda-io/wpt) which is fetched by the `make install-submodule` command.
|
We use [a fork](https://github.com/lightpanda-io/wpt/tree/fork) including a custom
|
||||||
|
[`testharnessreport.js`](https://github.com/lightpanda-io/wpt/commit/01a3115c076a3ad0c84849dbbf77a6e3d199c56f).
|
||||||
All the tests cases executed are located in the `tests/wpt` sub-directory.
|
|
||||||
|
|
||||||
For reference, you can easily execute a WPT test case with your browser via
|
For reference, you can easily execute a WPT test case with your browser via
|
||||||
[wpt.live](https://wpt.live).
|
[wpt.live](https://wpt.live).
|
||||||
|
|
||||||
|
#### Configure WPT HTTP server
|
||||||
|
|
||||||
|
To run the test, you must clone the repository, configure the custom hosts and generate the
|
||||||
|
`MANIFEST.json` file.
|
||||||
|
|
||||||
|
Clone the repository with the `fork` branch.
|
||||||
|
```
|
||||||
|
git clone -b fork --depth=1 git@github.com:lightpanda-io/wpt.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter into the `wpt/` dir.
|
||||||
|
|
||||||
|
Install custom domains in your `/etc/hosts`
|
||||||
|
```
|
||||||
|
./wpt make-hosts-file | sudo tee -a /etc/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate `MANIFEST.json`
|
||||||
|
```
|
||||||
|
./wpt manifest
|
||||||
|
```
|
||||||
|
Use the [WPT's setup
|
||||||
|
guide](https://web-platform-tests.org/running-tests/from-local-system.html) for
|
||||||
|
details.
|
||||||
|
|
||||||
#### Run WPT test suite
|
#### Run WPT test suite
|
||||||
|
|
||||||
To run all the tests:
|
An external [Go](https://go.dev) runner is provided by
|
||||||
|
[github.com/lightpanda-io/demo/](https://github.com/lightpanda-io/demo/)
|
||||||
|
repository, located into `wptrunner/` dir.
|
||||||
|
You need to clone the project first.
|
||||||
|
|
||||||
|
First start the WPT's HTTP server from your `wpt/` clone dir.
|
||||||
|
```
|
||||||
|
./wpt serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a Lightpanda browser
|
||||||
|
|
||||||
```
|
```
|
||||||
make wpt
|
zig build run -- --insecure-disable-tls-host-verification
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can start the wptrunner from the Demo's clone dir:
|
||||||
|
```
|
||||||
|
cd wptrunner && go run .
|
||||||
```
|
```
|
||||||
|
|
||||||
Or one specific test:
|
Or one specific test:
|
||||||
|
|
||||||
```
|
```
|
||||||
make wpt Node-childNodes.html
|
cd wptrunner && go run . Node-childNodes.html
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Add a new WPT test case
|
`wptrunner` command accepts `--summary` and `--json` options modifying output.
|
||||||
|
Also `--concurrency` define the concurrency limit.
|
||||||
|
|
||||||
We add new relevant tests cases files when we implemented changes in Lightpanda.
|
:warning: Running the whole test suite will take a long time. In this case,
|
||||||
|
it's useful to build in `releaseFast` mode to make tests faster.
|
||||||
|
|
||||||
To add a new test, copy the file you want from the [WPT
|
```
|
||||||
repo](https://github.com/web-platform-tests/wpt) into the `tests/wpt` directory.
|
zig build -Doptimize=ReleaseFast run
|
||||||
|
```
|
||||||
:warning: Please keep the original directory tree structure of `tests/wpt`.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,35 @@
|
|||||||
.{
|
.{
|
||||||
.name = .browser,
|
.name = .browser,
|
||||||
.paths = .{""},
|
.version = "1.0.0-dev",
|
||||||
.version = "0.0.0",
|
|
||||||
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
|
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
|
||||||
.minimum_zig_version = "0.15.2",
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d6b5f89cfc7feece29359e8c848bb916e8ecfab6.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.7.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH6_0gBABrJc5cL6-P2wGvvweTTCgWdpmClr9r-C-s",
|
.hash = "v8-0.0.0-xddH67uBBAD95hWsPQz3Ni1PlZjdywtPXrGUAp8rSKco",
|
||||||
|
},
|
||||||
|
// .v8 = .{ .path = "../zig-v8-fork" },
|
||||||
|
.brotli = .{
|
||||||
|
// v1.2.0
|
||||||
|
.url = "https://github.com/google/brotli/archive/028fb5a23661f123017c060daa546b55cf4bde29.tar.gz",
|
||||||
|
.hash = "N-V-__8AAJudKgCQCuIiH6MJjAiIJHfg_tT_Ew-0vZwVkCo_",
|
||||||
|
},
|
||||||
|
.zlib = .{
|
||||||
|
.url = "https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz",
|
||||||
|
.hash = "N-V-__8AAJ2cNgAgfBtAw33Bxfu1IWISDeKKSr3DAqoAysIJ",
|
||||||
|
},
|
||||||
|
.nghttp2 = .{
|
||||||
|
.url = "https://github.com/nghttp2/nghttp2/releases/download/v1.68.0/nghttp2-1.68.0.tar.gz",
|
||||||
|
.hash = "N-V-__8AAL15vQCI63ZL6Zaz5hJg6JTEgYXGbLnMFSnf7FT3",
|
||||||
},
|
},
|
||||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
|
||||||
.@"boringssl-zig" = .{
|
.@"boringssl-zig" = .{
|
||||||
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
|
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
|
||||||
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
|
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
|
||||||
},
|
},
|
||||||
|
.curl = .{
|
||||||
|
.url = "https://github.com/curl/curl/releases/download/curl-8_18_0/curl-8.18.0.tar.gz",
|
||||||
|
.hash = "N-V-__8AALp9QAGn6CCHZ6fK_FfMyGtG824LSHYHHasM3w-y",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
.paths = .{""},
|
||||||
}
|
}
|
||||||
|
|||||||
24
flake.lock
generated
24
flake.lock
generated
@@ -8,11 +8,11 @@
|
|||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763016383,
|
"lastModified": 1770708269,
|
||||||
"narHash": "sha256-eYmo7FNvm3q08iROzwIi8i9dWuUbJJl3uLR3OLnSmdI=",
|
"narHash": "sha256-OnZW86app7hHJJoB5lC9GNXY5QBBIESJB+sIdwEyld0=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "0fad5c0e5c531358e7174cd666af4608f08bc3ba",
|
"rev": "6b5325a017a9a9fe7e6252ccac3680cc7181cd63",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -96,11 +96,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763043403,
|
"lastModified": 1768649915,
|
||||||
"narHash": "sha256-DgCTbHdIpzbXSlQlOZEWj8oPt2lrRMlSk03oIstvkVQ=",
|
"narHash": "sha256-jc21hKogFnxU7KXSVTRmxC7u5D4RHwm9BAvDf5/Z1Uo=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "75e04ecd084f93d4105ce68c07dac7656291fe2e",
|
"rev": "3e3f3c7f9977dc123c23ee21e8085ed63daf8c37",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -122,11 +122,11 @@
|
|||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762860488,
|
"lastModified": 1770668050,
|
||||||
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
|
"narHash": "sha256-Q05yaIZtQrBKHpyWaPmyJmDRj0lojnVf8nUFE0vydcY=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
|
"rev": "9efc1f709f3c8134c3acac5d3592a8e4c184a0c6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -175,11 +175,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762907712,
|
"lastModified": 1770598090,
|
||||||
"narHash": "sha256-VNW/+VYIg6N4b9Iq+F0YZmm22n74IdFS7hsPLblWuOY=",
|
"narHash": "sha256-k+82IDgTd9o5sxHIqGlvfwseKln3Ejx1edGtDltuPXo=",
|
||||||
"owner": "mitchellh",
|
"owner": "mitchellh",
|
||||||
"repo": "zig-overlay",
|
"repo": "zig-overlay",
|
||||||
"rev": "d16453ee78765e49527c56d23386cead799b6b53",
|
"rev": "142495696982c88edddc8e17e4da90d8164acadf",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
94
src/App.zig
94
src/App.zig
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -21,99 +21,75 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
const Http = @import("http/Http.zig");
|
const Config = @import("Config.zig");
|
||||||
const Snapshot = @import("browser/js/Snapshot.zig");
|
const Snapshot = @import("browser/js/Snapshot.zig");
|
||||||
const Platform = @import("browser/js/Platform.zig");
|
const Platform = @import("browser/js/Platform.zig");
|
||||||
|
|
||||||
const Notification = @import("Notification.zig");
|
|
||||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||||
|
|
||||||
// Container for global state / objects that various parts of the system
|
const Network = @import("network/Runtime.zig");
|
||||||
// might need.
|
pub const ArenaPool = @import("ArenaPool.zig");
|
||||||
|
|
||||||
const App = @This();
|
const App = @This();
|
||||||
|
|
||||||
http: Http,
|
network: Network,
|
||||||
config: Config,
|
config: *const Config,
|
||||||
platform: Platform,
|
platform: Platform,
|
||||||
snapshot: Snapshot,
|
snapshot: Snapshot,
|
||||||
telemetry: Telemetry,
|
telemetry: Telemetry,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
arena_pool: ArenaPool,
|
||||||
app_dir_path: ?[]const u8,
|
app_dir_path: ?[]const u8,
|
||||||
notification: *Notification,
|
|
||||||
shutdown: bool = false,
|
|
||||||
|
|
||||||
pub const RunMode = enum {
|
pub fn init(allocator: Allocator, config: *const Config) !*App {
|
||||||
help,
|
|
||||||
fetch,
|
|
||||||
serve,
|
|
||||||
version,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Config = struct {
|
|
||||||
run_mode: RunMode,
|
|
||||||
tls_verify_host: bool = true,
|
|
||||||
http_proxy: ?[:0]const u8 = null,
|
|
||||||
proxy_bearer_token: ?[:0]const u8 = null,
|
|
||||||
http_timeout_ms: ?u31 = null,
|
|
||||||
http_connect_timeout_ms: ?u31 = null,
|
|
||||||
http_max_host_open: ?u8 = null,
|
|
||||||
http_max_concurrent: ?u8 = null,
|
|
||||||
user_agent: [:0]const u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, config: Config) !*App {
|
|
||||||
const app = try allocator.create(App);
|
const app = try allocator.create(App);
|
||||||
errdefer allocator.destroy(app);
|
errdefer allocator.destroy(app);
|
||||||
|
|
||||||
app.config = config;
|
app.* = .{
|
||||||
app.allocator = allocator;
|
.config = config,
|
||||||
|
.allocator = allocator,
|
||||||
|
.network = undefined,
|
||||||
|
.platform = undefined,
|
||||||
|
.snapshot = undefined,
|
||||||
|
.app_dir_path = undefined,
|
||||||
|
.telemetry = undefined,
|
||||||
|
.arena_pool = undefined,
|
||||||
|
};
|
||||||
|
|
||||||
app.notification = try Notification.init(allocator, null);
|
app.network = try Network.init(allocator, config);
|
||||||
errdefer app.notification.deinit();
|
errdefer app.network.deinit();
|
||||||
|
|
||||||
app.http = try Http.init(allocator, .{
|
|
||||||
.max_host_open = config.http_max_host_open orelse 4,
|
|
||||||
.max_concurrent = config.http_max_concurrent orelse 10,
|
|
||||||
.timeout_ms = config.http_timeout_ms orelse 5000,
|
|
||||||
.connect_timeout_ms = config.http_connect_timeout_ms orelse 0,
|
|
||||||
.http_proxy = config.http_proxy,
|
|
||||||
.tls_verify_host = config.tls_verify_host,
|
|
||||||
.proxy_bearer_token = config.proxy_bearer_token,
|
|
||||||
.user_agent = config.user_agent,
|
|
||||||
});
|
|
||||||
errdefer app.http.deinit();
|
|
||||||
|
|
||||||
app.platform = try Platform.init();
|
app.platform = try Platform.init();
|
||||||
errdefer app.platform.deinit();
|
errdefer app.platform.deinit();
|
||||||
|
|
||||||
app.snapshot = try Snapshot.load(allocator);
|
app.snapshot = try Snapshot.load();
|
||||||
errdefer app.snapshot.deinit(allocator);
|
errdefer app.snapshot.deinit();
|
||||||
|
|
||||||
app.app_dir_path = getAndMakeAppDir(allocator);
|
app.app_dir_path = getAndMakeAppDir(allocator);
|
||||||
|
|
||||||
app.telemetry = try Telemetry.init(app, config.run_mode);
|
app.telemetry = try Telemetry.init(app, config.mode);
|
||||||
errdefer app.telemetry.deinit();
|
errdefer app.telemetry.deinit(allocator);
|
||||||
|
|
||||||
try app.telemetry.register(app.notification);
|
app.arena_pool = ArenaPool.init(allocator, 512, 1024 * 16);
|
||||||
|
errdefer app.arena_pool.deinit();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *App) void {
|
pub fn shutdown(self: *const App) bool {
|
||||||
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
|
return self.network.shutdown.load(.acquire);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
pub fn deinit(self: *App) void {
|
||||||
const allocator = self.allocator;
|
const allocator = self.allocator;
|
||||||
if (self.app_dir_path) |app_dir_path| {
|
if (self.app_dir_path) |app_dir_path| {
|
||||||
allocator.free(app_dir_path);
|
allocator.free(app_dir_path);
|
||||||
self.app_dir_path = null;
|
self.app_dir_path = null;
|
||||||
}
|
}
|
||||||
self.telemetry.deinit();
|
self.telemetry.deinit(allocator);
|
||||||
self.notification.deinit();
|
self.network.deinit();
|
||||||
self.http.deinit();
|
self.snapshot.deinit();
|
||||||
self.snapshot.deinit(allocator);
|
|
||||||
self.platform.deinit();
|
self.platform.deinit();
|
||||||
|
self.arena_pool.deinit();
|
||||||
|
|
||||||
allocator.destroy(self);
|
allocator.destroy(self);
|
||||||
}
|
}
|
||||||
|
|||||||
273
src/ArenaPool.zig
Normal file
273
src/ArenaPool.zig
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const log = @import("log.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
|
const ArenaPool = @This();
|
||||||
|
|
||||||
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
retain_bytes: usize,
|
||||||
|
free_list_len: u16 = 0,
|
||||||
|
free_list: ?*Entry = null,
|
||||||
|
free_list_max: u16,
|
||||||
|
entry_pool: std.heap.MemoryPool(Entry),
|
||||||
|
mutex: std.Thread.Mutex = .{},
|
||||||
|
// Debug mode: track acquire/release counts per debug name to detect leaks and double-frees
|
||||||
|
_leak_track: if (IS_DEBUG) std.StringHashMapUnmanaged(isize) else void = if (IS_DEBUG) .empty else {},
|
||||||
|
|
||||||
|
const Entry = struct {
|
||||||
|
next: ?*Entry,
|
||||||
|
arena: ArenaAllocator,
|
||||||
|
debug: if (IS_DEBUG) []const u8 else void = if (IS_DEBUG) "" else {},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DebugInfo = struct {
|
||||||
|
debug: []const u8 = "",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) ArenaPool {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.free_list_max = free_list_max,
|
||||||
|
.retain_bytes = retain_bytes,
|
||||||
|
.entry_pool = .init(allocator),
|
||||||
|
._leak_track = if (IS_DEBUG) .empty else {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *ArenaPool) void {
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
var has_leaks = false;
|
||||||
|
var it = self._leak_track.iterator();
|
||||||
|
while (it.next()) |kv| {
|
||||||
|
if (kv.value_ptr.* != 0) {
|
||||||
|
log.err(.bug, "ArenaPool leak", .{ .name = kv.key_ptr.*, .count = kv.value_ptr.* });
|
||||||
|
has_leaks = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (has_leaks) {
|
||||||
|
@panic("ArenaPool: leaked arenas detected");
|
||||||
|
}
|
||||||
|
self._leak_track.deinit(self.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry = self.free_list;
|
||||||
|
while (entry) |e| {
|
||||||
|
entry = e.next;
|
||||||
|
e.arena.deinit();
|
||||||
|
}
|
||||||
|
self.entry_pool.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn acquire(self: *ArenaPool, dbg: DebugInfo) !Allocator {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
if (self.free_list) |entry| {
|
||||||
|
self.free_list = entry.next;
|
||||||
|
self.free_list_len -= 1;
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
entry.debug = dbg.debug;
|
||||||
|
const gop = try self._leak_track.getOrPut(self.allocator, dbg.debug);
|
||||||
|
if (!gop.found_existing) {
|
||||||
|
gop.value_ptr.* = 0;
|
||||||
|
}
|
||||||
|
gop.value_ptr.* += 1;
|
||||||
|
}
|
||||||
|
return entry.arena.allocator();
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = try self.entry_pool.create();
|
||||||
|
entry.* = .{
|
||||||
|
.next = null,
|
||||||
|
.arena = ArenaAllocator.init(self.allocator),
|
||||||
|
.debug = if (IS_DEBUG) dbg.debug else {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
const gop = try self._leak_track.getOrPut(self.allocator, dbg.debug);
|
||||||
|
if (!gop.found_existing) {
|
||||||
|
gop.value_ptr.* = 0;
|
||||||
|
}
|
||||||
|
gop.value_ptr.* += 1;
|
||||||
|
}
|
||||||
|
return entry.arena.allocator();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
||||||
|
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||||
|
const entry: *Entry = @fieldParentPtr("arena", arena);
|
||||||
|
|
||||||
|
// Reset the arena before acquiring the lock to minimize lock hold time
|
||||||
|
_ = arena.reset(.{ .retain_with_limit = self.retain_bytes });
|
||||||
|
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
if (IS_DEBUG) {
|
||||||
|
if (self._leak_track.getPtr(entry.debug)) |count| {
|
||||||
|
count.* -= 1;
|
||||||
|
if (count.* < 0) {
|
||||||
|
log.err(.bug, "ArenaPool double-free", .{ .name = entry.debug });
|
||||||
|
@panic("ArenaPool: double-free detected");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.err(.bug, "ArenaPool release unknown", .{ .name = entry.debug });
|
||||||
|
@panic("ArenaPool: release of untracked arena");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const free_list_len = self.free_list_len;
|
||||||
|
if (free_list_len == self.free_list_max) {
|
||||||
|
arena.deinit();
|
||||||
|
self.entry_pool.destroy(entry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.next = self.free_list;
|
||||||
|
self.free_list_len = free_list_len + 1;
|
||||||
|
self.free_list = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(_: *const ArenaPool, allocator: Allocator, retain: usize) void {
|
||||||
|
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||||
|
_ = arena.reset(.{ .retain_with_limit = retain });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resetRetain(_: *const ArenaPool, allocator: Allocator) void {
|
||||||
|
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||||
|
_ = arena.reset(.retain_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
test "arena pool - basic acquire and use" {
|
||||||
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
|
defer pool.deinit();
|
||||||
|
|
||||||
|
const alloc = try pool.acquire(.{ .debug = "test" });
|
||||||
|
const buf = try alloc.alloc(u8, 64);
|
||||||
|
@memset(buf, 0xAB);
|
||||||
|
try testing.expectEqual(@as(u8, 0xAB), buf[0]);
|
||||||
|
|
||||||
|
pool.release(alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "arena pool - reuse entry after release" {
|
||||||
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
|
defer pool.deinit();
|
||||||
|
|
||||||
|
const alloc1 = try pool.acquire(.{ .debug = "test" });
|
||||||
|
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
||||||
|
|
||||||
|
pool.release(alloc1);
|
||||||
|
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
||||||
|
|
||||||
|
// The same entry should be returned from the free list.
|
||||||
|
const alloc2 = try pool.acquire(.{ .debug = "test" });
|
||||||
|
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
||||||
|
try testing.expectEqual(alloc1.ptr, alloc2.ptr);
|
||||||
|
|
||||||
|
pool.release(alloc2);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "arena pool - multiple concurrent arenas" {
|
||||||
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
|
defer pool.deinit();
|
||||||
|
|
||||||
|
const a1 = try pool.acquire(.{ .debug = "test1" });
|
||||||
|
const a2 = try pool.acquire(.{ .debug = "test2" });
|
||||||
|
const a3 = try pool.acquire(.{ .debug = "test3" });
|
||||||
|
|
||||||
|
// All three must be distinct arenas.
|
||||||
|
try testing.expect(a1.ptr != a2.ptr);
|
||||||
|
try testing.expect(a2.ptr != a3.ptr);
|
||||||
|
try testing.expect(a1.ptr != a3.ptr);
|
||||||
|
|
||||||
|
_ = try a1.alloc(u8, 16);
|
||||||
|
_ = try a2.alloc(u8, 32);
|
||||||
|
_ = try a3.alloc(u8, 48);
|
||||||
|
|
||||||
|
pool.release(a1);
|
||||||
|
pool.release(a2);
|
||||||
|
pool.release(a3);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(u16, 3), pool.free_list_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "arena pool - free list respects max limit" {
|
||||||
|
// Cap the free list at 1 so the second release discards its arena.
|
||||||
|
var pool = ArenaPool.init(testing.allocator, 1, 1024 * 16);
|
||||||
|
defer pool.deinit();
|
||||||
|
|
||||||
|
const a1 = try pool.acquire(.{ .debug = "test1" });
|
||||||
|
const a2 = try pool.acquire(.{ .debug = "test2" });
|
||||||
|
|
||||||
|
pool.release(a1);
|
||||||
|
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
||||||
|
|
||||||
|
// The free list is full; a2's arena should be destroyed, not queued.
|
||||||
|
pool.release(a2);
|
||||||
|
try testing.expectEqual(@as(u16, 1), pool.free_list_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "arena pool - reset clears memory without releasing" {
|
||||||
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
|
defer pool.deinit();
|
||||||
|
|
||||||
|
const alloc = try pool.acquire(.{ .debug = "test" });
|
||||||
|
|
||||||
|
const buf = try alloc.alloc(u8, 128);
|
||||||
|
@memset(buf, 0xFF);
|
||||||
|
|
||||||
|
// reset() frees arena memory but keeps the allocator in-flight.
|
||||||
|
pool.reset(alloc, 0);
|
||||||
|
|
||||||
|
// The free list must stay empty; the allocator was not released.
|
||||||
|
try testing.expectEqual(@as(u16, 0), pool.free_list_len);
|
||||||
|
|
||||||
|
// Allocating again through the same arena must still work.
|
||||||
|
const buf2 = try alloc.alloc(u8, 64);
|
||||||
|
@memset(buf2, 0x00);
|
||||||
|
try testing.expectEqual(@as(u8, 0x00), buf2[0]);
|
||||||
|
|
||||||
|
pool.release(alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "arena pool - deinit with entries in free list" {
|
||||||
|
// Verifies that deinit properly cleans up free-listed arenas (no leaks
|
||||||
|
// detected by the test allocator).
|
||||||
|
var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16);
|
||||||
|
|
||||||
|
const a1 = try pool.acquire(.{ .debug = "test1" });
|
||||||
|
const a2 = try pool.acquire(.{ .debug = "test2" });
|
||||||
|
_ = try a1.alloc(u8, 256);
|
||||||
|
_ = try a2.alloc(u8, 512);
|
||||||
|
pool.release(a1);
|
||||||
|
pool.release(a2);
|
||||||
|
try testing.expectEqual(@as(u16, 2), pool.free_list_len);
|
||||||
|
|
||||||
|
pool.deinit();
|
||||||
|
}
|
||||||
1023
src/Config.zig
Normal file
1023
src/Config.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,11 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const lp = @import("lightpanda");
|
||||||
|
|
||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
const Page = @import("browser/Page.zig");
|
const Page = @import("browser/Page.zig");
|
||||||
const Transfer = @import("http/Client.zig").Transfer;
|
const Transfer = @import("browser/HttpClient.zig").Transfer;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
@@ -38,10 +39,9 @@ const List = std.DoublyLinkedList;
|
|||||||
// CDP code registers for the "network_bytes_sent" event, because it needs to
|
// CDP code registers for the "network_bytes_sent" event, because it needs to
|
||||||
// send messages to the client when this happens. Our HTTP client could then
|
// send messages to the client when this happens. Our HTTP client could then
|
||||||
// emit a "network_bytes_sent" message. It would be easy, and it would work.
|
// emit a "network_bytes_sent" message. It would be easy, and it would work.
|
||||||
// That is, it would work until the Telemetry code makes an HTTP request, and
|
// That is, it would work until multiple CDP clients connect, and because
|
||||||
// because everything's just one big global, that gets picked up by the
|
// everything's just one big global, events from one CDP session would be sent
|
||||||
// registered CDP listener, and the telemetry network activity gets sent to the
|
// to all CDP clients.
|
||||||
// CDP client.
|
|
||||||
//
|
//
|
||||||
// To avoid this, one way or another, we need scoping. We could still have
|
// To avoid this, one way or another, we need scoping. We could still have
|
||||||
// a global registry but every "register" and every "emit" has some type of
|
// a global registry but every "register" and every "emit" has some type of
|
||||||
@@ -49,14 +49,10 @@ const List = std.DoublyLinkedList;
|
|||||||
// between components to share a common scope.
|
// between components to share a common scope.
|
||||||
//
|
//
|
||||||
// Instead, the approach that we take is to have a notification instance per
|
// Instead, the approach that we take is to have a notification instance per
|
||||||
// scope. This makes some things harder, but we only plan on having 2
|
// CDP connection (BrowserContext). Each CDP connection has its own notification
|
||||||
// notification instances at a given time: one in a Browser and one in the App.
|
// that is shared across all Sessions (tabs) within that connection. This ensures
|
||||||
// What about something like Telemetry, which lives outside of a Browser but
|
// proper isolation between different CDP clients while allowing a single client
|
||||||
// still cares about Browser-events (like .page_navigate)? When the Browser
|
// to receive events from all its tabs.
|
||||||
// notification is created, a `notification_created` event is raised in the
|
|
||||||
// App's notification, which Telemetry is registered for. This allows Telemetry
|
|
||||||
// to register for events in the Browser notification. See the Telemetry's
|
|
||||||
// register function.
|
|
||||||
const Notification = @This();
|
const Notification = @This();
|
||||||
// Every event type (which are hard-coded), has a list of Listeners.
|
// Every event type (which are hard-coded), has a list of Listeners.
|
||||||
// When the event happens, we dispatch to those listener.
|
// When the event happens, we dispatch to those listener.
|
||||||
@@ -65,7 +61,7 @@ event_listeners: EventListeners,
|
|||||||
// list of listeners for a specified receiver
|
// list of listeners for a specified receiver
|
||||||
// @intFromPtr(receiver) -> [listener1, listener2, ...]
|
// @intFromPtr(receiver) -> [listener1, listener2, ...]
|
||||||
// Used when `unregisterAll` is called.
|
// Used when `unregisterAll` is called.
|
||||||
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayListUnmanaged(*Listener)),
|
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayList(*Listener)),
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
mem_pool: std.heap.MemoryPool(Listener),
|
mem_pool: std.heap.MemoryPool(Listener),
|
||||||
@@ -77,6 +73,7 @@ const EventListeners = struct {
|
|||||||
page_navigated: List = .{},
|
page_navigated: List = .{},
|
||||||
page_network_idle: List = .{},
|
page_network_idle: List = .{},
|
||||||
page_network_almost_idle: List = .{},
|
page_network_almost_idle: List = .{},
|
||||||
|
page_frame_created: List = .{},
|
||||||
http_request_fail: List = .{},
|
http_request_fail: List = .{},
|
||||||
http_request_start: List = .{},
|
http_request_start: List = .{},
|
||||||
http_request_intercept: List = .{},
|
http_request_intercept: List = .{},
|
||||||
@@ -84,7 +81,6 @@ const EventListeners = struct {
|
|||||||
http_request_auth_required: List = .{},
|
http_request_auth_required: List = .{},
|
||||||
http_response_data: List = .{},
|
http_response_data: List = .{},
|
||||||
http_response_header_done: List = .{},
|
http_response_header_done: List = .{},
|
||||||
notification_created: List = .{},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Events = union(enum) {
|
const Events = union(enum) {
|
||||||
@@ -94,6 +90,7 @@ const Events = union(enum) {
|
|||||||
page_navigated: *const PageNavigated,
|
page_navigated: *const PageNavigated,
|
||||||
page_network_idle: *const PageNetworkIdle,
|
page_network_idle: *const PageNetworkIdle,
|
||||||
page_network_almost_idle: *const PageNetworkAlmostIdle,
|
page_network_almost_idle: *const PageNetworkAlmostIdle,
|
||||||
|
page_frame_created: *const PageFrameCreated,
|
||||||
http_request_fail: *const RequestFail,
|
http_request_fail: *const RequestFail,
|
||||||
http_request_start: *const RequestStart,
|
http_request_start: *const RequestStart,
|
||||||
http_request_intercept: *const RequestIntercept,
|
http_request_intercept: *const RequestIntercept,
|
||||||
@@ -101,31 +98,42 @@ const Events = union(enum) {
|
|||||||
http_request_done: *const RequestDone,
|
http_request_done: *const RequestDone,
|
||||||
http_response_data: *const ResponseData,
|
http_response_data: *const ResponseData,
|
||||||
http_response_header_done: *const ResponseHeaderDone,
|
http_response_header_done: *const ResponseHeaderDone,
|
||||||
notification_created: *Notification,
|
|
||||||
};
|
};
|
||||||
const EventType = std.meta.FieldEnum(Events);
|
const EventType = std.meta.FieldEnum(Events);
|
||||||
|
|
||||||
pub const PageRemove = struct {};
|
pub const PageRemove = struct {};
|
||||||
|
|
||||||
pub const PageNavigate = struct {
|
pub const PageNavigate = struct {
|
||||||
req_id: usize,
|
req_id: u32,
|
||||||
|
frame_id: u32,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
url: [:0]const u8,
|
url: [:0]const u8,
|
||||||
opts: Page.NavigateOpts,
|
opts: Page.NavigateOpts,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PageNavigated = struct {
|
pub const PageNavigated = struct {
|
||||||
req_id: usize,
|
req_id: u32,
|
||||||
|
frame_id: u32,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
url: [:0]const u8,
|
url: [:0]const u8,
|
||||||
opts: Page.NavigatedOpts,
|
opts: Page.NavigatedOpts,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PageNetworkIdle = struct {
|
pub const PageNetworkIdle = struct {
|
||||||
|
req_id: u32,
|
||||||
|
frame_id: u32,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PageNetworkAlmostIdle = struct {
|
pub const PageNetworkAlmostIdle = struct {
|
||||||
|
req_id: u32,
|
||||||
|
frame_id: u32,
|
||||||
|
timestamp: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PageFrameCreated = struct {
|
||||||
|
frame_id: u32,
|
||||||
|
parent_id: u32,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -161,12 +169,7 @@ pub const RequestFail = struct {
|
|||||||
err: anyerror,
|
err: anyerror,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, parent: ?*Notification) !*Notification {
|
pub fn init(allocator: Allocator) !*Notification {
|
||||||
|
|
||||||
// This is put on the heap because we want to raise a .notification_created
|
|
||||||
// event, so that, something like Telemetry, can receive the
|
|
||||||
// .page_navigate event on all notification instances. That can only work
|
|
||||||
// if we dispatch .notification_created with a *Notification.
|
|
||||||
const notification = try allocator.create(Notification);
|
const notification = try allocator.create(Notification);
|
||||||
errdefer allocator.destroy(notification);
|
errdefer allocator.destroy(notification);
|
||||||
|
|
||||||
@@ -177,10 +180,6 @@ pub fn init(allocator: Allocator, parent: ?*Notification) !*Notification {
|
|||||||
.mem_pool = std.heap.MemoryPool(Listener).init(allocator),
|
.mem_pool = std.heap.MemoryPool(Listener).init(allocator),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (parent) |pn| {
|
|
||||||
pn.dispatch(.notification_created, notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +240,7 @@ pub fn unregister(self: *Notification, comptime event: EventType, receiver: anyt
|
|||||||
if (listeners.items.len == 0) {
|
if (listeners.items.len == 0) {
|
||||||
listeners.deinit(self.allocator);
|
listeners.deinit(self.allocator);
|
||||||
const removed = self.listeners.remove(@intFromPtr(receiver));
|
const removed = self.listeners.remove(@intFromPtr(receiver));
|
||||||
std.debug.assert(removed == true);
|
lp.assert(removed == true, "Notification.unregister", .{ .type = event });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +254,9 @@ pub fn unregisterAll(self: *Notification, receiver: *anyopaque) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch(self: *Notification, comptime event: EventType, data: ArgType(event)) void {
|
pub fn dispatch(self: *Notification, comptime event: EventType, data: ArgType(event)) void {
|
||||||
|
if (self.listeners.count() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const list = &@field(self.event_listeners, @tagName(event));
|
const list = &@field(self.event_listeners, @tagName(event));
|
||||||
|
|
||||||
var node = list.first;
|
var node = list.first;
|
||||||
@@ -312,11 +314,12 @@ const Listener = struct {
|
|||||||
|
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
test "Notification" {
|
test "Notification" {
|
||||||
var notifier = try Notification.init(testing.allocator, null);
|
var notifier = try Notification.init(testing.allocator);
|
||||||
defer notifier.deinit();
|
defer notifier.deinit();
|
||||||
|
|
||||||
// noop
|
// noop
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
.req_id = 1,
|
.req_id = 1,
|
||||||
.timestamp = 4,
|
.timestamp = 4,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
@@ -327,6 +330,7 @@ test "Notification" {
|
|||||||
|
|
||||||
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
.req_id = 1,
|
.req_id = 1,
|
||||||
.timestamp = 4,
|
.timestamp = 4,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
@@ -336,6 +340,7 @@ test "Notification" {
|
|||||||
|
|
||||||
notifier.unregisterAll(&tc);
|
notifier.unregisterAll(&tc);
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
.req_id = 1,
|
.req_id = 1,
|
||||||
.timestamp = 10,
|
.timestamp = 10,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
@@ -346,23 +351,25 @@ test "Notification" {
|
|||||||
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
.req_id = 1,
|
.req_id = 1,
|
||||||
.timestamp = 10,
|
.timestamp = 10,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
.opts = .{},
|
.opts = .{},
|
||||||
});
|
});
|
||||||
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(14, tc.page_navigate);
|
try testing.expectEqual(14, tc.page_navigate);
|
||||||
try testing.expectEqual(6, tc.page_navigated);
|
try testing.expectEqual(6, tc.page_navigated);
|
||||||
|
|
||||||
notifier.unregisterAll(&tc);
|
notifier.unregisterAll(&tc);
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.frame_id = 0,
|
||||||
.req_id = 1,
|
.req_id = 1,
|
||||||
.timestamp = 100,
|
.timestamp = 100,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
.opts = .{},
|
.opts = .{},
|
||||||
});
|
});
|
||||||
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(14, tc.page_navigate);
|
try testing.expectEqual(14, tc.page_navigate);
|
||||||
try testing.expectEqual(6, tc.page_navigated);
|
try testing.expectEqual(6, tc.page_navigated);
|
||||||
|
|
||||||
@@ -370,27 +377,27 @@ test "Notification" {
|
|||||||
// unregister
|
// unregister
|
||||||
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
||||||
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(114, tc.page_navigate);
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
try testing.expectEqual(1006, tc.page_navigated);
|
try testing.expectEqual(1006, tc.page_navigated);
|
||||||
|
|
||||||
notifier.unregister(.page_navigate, &tc);
|
notifier.unregister(.page_navigate, &tc);
|
||||||
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(114, tc.page_navigate);
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
try testing.expectEqual(2006, tc.page_navigated);
|
try testing.expectEqual(2006, tc.page_navigated);
|
||||||
|
|
||||||
notifier.unregister(.page_navigated, &tc);
|
notifier.unregister(.page_navigated, &tc);
|
||||||
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(114, tc.page_navigate);
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
try testing.expectEqual(2006, tc.page_navigated);
|
try testing.expectEqual(2006, tc.page_navigated);
|
||||||
|
|
||||||
// already unregistered, try anyways
|
// already unregistered, try anyways
|
||||||
notifier.unregister(.page_navigated, &tc);
|
notifier.unregister(.page_navigated, &tc);
|
||||||
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigate, &.{ .frame_id = 0, .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigated, &.{ .frame_id = 0, .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(114, tc.page_navigate);
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
try testing.expectEqual(2006, tc.page_navigated);
|
try testing.expectEqual(2006, tc.page_navigated);
|
||||||
}
|
}
|
||||||
|
|||||||
563
src/SemanticTree.zig
Normal file
563
src/SemanticTree.zig
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. See <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const lp = @import("lightpanda");
|
||||||
|
const log = @import("log.zig");
|
||||||
|
const isAllWhitespace = @import("string.zig").isAllWhitespace;
|
||||||
|
const Page = lp.Page;
|
||||||
|
const interactive = @import("browser/interactive.zig");
|
||||||
|
|
||||||
|
const CData = @import("browser/webapi/CData.zig");
|
||||||
|
const Element = @import("browser/webapi/Element.zig");
|
||||||
|
const Node = @import("browser/webapi/Node.zig");
|
||||||
|
const AXNode = @import("cdp/AXNode.zig");
|
||||||
|
const CDPNode = @import("cdp/Node.zig");
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
dom_node: *Node,
|
||||||
|
registry: *CDPNode.Registry,
|
||||||
|
page: *Page,
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
prune: bool = true,
|
||||||
|
interactive_only: bool = false,
|
||||||
|
max_depth: u32 = std.math.maxInt(u32) - 1,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!void {
|
||||||
|
var visitor = JsonVisitor{ .jw = jw, .tree = self };
|
||||||
|
var xpath_buffer: std.ArrayList(u8) = .{};
|
||||||
|
const listener_targets = interactive.buildListenerTargetMap(self.page, self.arena) catch |err| {
|
||||||
|
log.err(.app, "listener map failed", .{ .err = err });
|
||||||
|
return error.WriteFailed;
|
||||||
|
};
|
||||||
|
var visibility_cache: Element.VisibilityCache = .empty;
|
||||||
|
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
||||||
|
var ctx: WalkContext = .{
|
||||||
|
.xpath_buffer = &xpath_buffer,
|
||||||
|
.listener_targets = listener_targets,
|
||||||
|
.visibility_cache = &visibility_cache,
|
||||||
|
.pointer_events_cache = &pointer_events_cache,
|
||||||
|
};
|
||||||
|
self.walk(&ctx, self.dom_node, null, &visitor, 1, 0) catch |err| {
|
||||||
|
log.err(.app, "semantic tree json dump failed", .{ .err = err });
|
||||||
|
return error.WriteFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!void {
|
||||||
|
var visitor = TextVisitor{ .writer = writer, .tree = self, .depth = 0 };
|
||||||
|
var xpath_buffer: std.ArrayList(u8) = .empty;
|
||||||
|
const listener_targets = interactive.buildListenerTargetMap(self.page, self.arena) catch |err| {
|
||||||
|
log.err(.app, "listener map failed", .{ .err = err });
|
||||||
|
return error.WriteFailed;
|
||||||
|
};
|
||||||
|
var visibility_cache: Element.VisibilityCache = .empty;
|
||||||
|
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
||||||
|
var ctx: WalkContext = .{
|
||||||
|
.xpath_buffer = &xpath_buffer,
|
||||||
|
.listener_targets = listener_targets,
|
||||||
|
.visibility_cache = &visibility_cache,
|
||||||
|
.pointer_events_cache = &pointer_events_cache,
|
||||||
|
};
|
||||||
|
self.walk(&ctx, self.dom_node, null, &visitor, 1, 0) catch |err| {
|
||||||
|
log.err(.app, "semantic tree text dump failed", .{ .err = err });
|
||||||
|
return error.WriteFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const OptionData = struct {
|
||||||
|
value: []const u8,
|
||||||
|
text: []const u8,
|
||||||
|
selected: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeData = struct {
|
||||||
|
id: CDPNode.Id,
|
||||||
|
axn: AXNode,
|
||||||
|
role: []const u8,
|
||||||
|
name: ?[]const u8,
|
||||||
|
value: ?[]const u8,
|
||||||
|
options: ?[]OptionData = null,
|
||||||
|
xpath: []const u8,
|
||||||
|
is_interactive: bool,
|
||||||
|
node_name: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const WalkContext = struct {
|
||||||
|
xpath_buffer: *std.ArrayList(u8),
|
||||||
|
listener_targets: interactive.ListenerTargetMap,
|
||||||
|
visibility_cache: *Element.VisibilityCache,
|
||||||
|
pointer_events_cache: *Element.PointerEventsCache,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn walk(
|
||||||
|
self: @This(),
|
||||||
|
ctx: *WalkContext,
|
||||||
|
node: *Node,
|
||||||
|
parent_name: ?[]const u8,
|
||||||
|
visitor: anytype,
|
||||||
|
index: usize,
|
||||||
|
current_depth: u32,
|
||||||
|
) !void {
|
||||||
|
if (current_depth > self.max_depth) return;
|
||||||
|
|
||||||
|
// 1. Skip non-content nodes
|
||||||
|
if (node.is(Element)) |el| {
|
||||||
|
const tag = el.getTag();
|
||||||
|
if (tag.isMetadata() or tag == .svg) return;
|
||||||
|
|
||||||
|
// We handle options/optgroups natively inside their parents, skip them in the general walk
|
||||||
|
if (tag == .datalist or tag == .option or tag == .optgroup) return;
|
||||||
|
|
||||||
|
// Check visibility using the engine's checkVisibility which handles CSS display: none
|
||||||
|
if (!el.checkVisibilityCached(ctx.visibility_cache, self.page)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.is(Element.Html)) |html_el| {
|
||||||
|
if (html_el.getHidden()) return;
|
||||||
|
}
|
||||||
|
} else if (node.is(CData.Text)) |text_node| {
|
||||||
|
const text = text_node.getWholeText();
|
||||||
|
if (isAllWhitespace(text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (node._type != .document and node._type != .document_fragment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cdp_node = try self.registry.register(node);
|
||||||
|
const axn = AXNode.fromNode(node);
|
||||||
|
const role = try axn.getRole();
|
||||||
|
|
||||||
|
var is_interactive = false;
|
||||||
|
var value: ?[]const u8 = null;
|
||||||
|
var options: ?[]OptionData = null;
|
||||||
|
var node_name: []const u8 = "text";
|
||||||
|
|
||||||
|
if (node.is(Element)) |el| {
|
||||||
|
node_name = el.getTagNameLower();
|
||||||
|
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
value = input.getValue();
|
||||||
|
if (el.getAttributeSafe(comptime lp.String.wrap("list"))) |list_id| {
|
||||||
|
options = try extractDataListOptions(list_id, self.page, self.arena);
|
||||||
|
}
|
||||||
|
} else if (el.is(Element.Html.TextArea)) |textarea| {
|
||||||
|
value = textarea.getValue();
|
||||||
|
} else if (el.is(Element.Html.Select)) |select| {
|
||||||
|
value = select.getValue(self.page);
|
||||||
|
options = try extractSelectOptions(el.asNode(), self.page, self.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.is(Element.Html)) |html_el| {
|
||||||
|
if (interactive.classifyInteractivity(self.page, el, html_el, ctx.listener_targets, ctx.pointer_events_cache) != null) {
|
||||||
|
is_interactive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node._type == .document or node._type == .document_fragment) {
|
||||||
|
node_name = "root";
|
||||||
|
}
|
||||||
|
|
||||||
|
const initial_xpath_len = ctx.xpath_buffer.items.len;
|
||||||
|
try appendXPathSegment(node, ctx.xpath_buffer.writer(self.arena), index);
|
||||||
|
const xpath = ctx.xpath_buffer.items;
|
||||||
|
|
||||||
|
var name = try axn.getName(self.page, self.arena);
|
||||||
|
|
||||||
|
const has_explicit_label = if (node.is(Element)) |el|
|
||||||
|
el.getAttributeSafe(.wrap("aria-label")) != null or el.getAttributeSafe(.wrap("title")) != null
|
||||||
|
else
|
||||||
|
false;
|
||||||
|
|
||||||
|
const structural = isStructuralRole(role);
|
||||||
|
|
||||||
|
// Filter out computed concatenated names for generic containers without explicit labels.
|
||||||
|
// This prevents token bloat and ensures their StaticText children aren't incorrectly pruned.
|
||||||
|
// We ignore interactivity because a generic wrapper with an event listener still shouldn't hoist all text.
|
||||||
|
if (name != null and structural and !has_explicit_label) {
|
||||||
|
name = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var should_visit = true;
|
||||||
|
if (self.interactive_only) {
|
||||||
|
var keep = false;
|
||||||
|
if (interactive.isInteractiveRole(role)) {
|
||||||
|
keep = true;
|
||||||
|
} else if (interactive.isContentRole(role)) {
|
||||||
|
if (name != null and name.?.len > 0) {
|
||||||
|
keep = true;
|
||||||
|
}
|
||||||
|
} else if (std.mem.eql(u8, role, "RootWebArea")) {
|
||||||
|
keep = true;
|
||||||
|
} else if (is_interactive) {
|
||||||
|
keep = true;
|
||||||
|
}
|
||||||
|
if (!keep) {
|
||||||
|
should_visit = false;
|
||||||
|
}
|
||||||
|
} else if (self.prune) {
|
||||||
|
if (structural and !is_interactive and !has_explicit_label) {
|
||||||
|
should_visit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, role, "StaticText") and node._parent != null) {
|
||||||
|
if (parent_name != null and name != null and std.mem.indexOf(u8, parent_name.?, name.?) != null) {
|
||||||
|
should_visit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var did_visit = false;
|
||||||
|
var should_walk_children = true;
|
||||||
|
var data: NodeData = .{
|
||||||
|
.id = cdp_node.id,
|
||||||
|
.axn = axn,
|
||||||
|
.role = role,
|
||||||
|
.name = name,
|
||||||
|
.value = value,
|
||||||
|
.options = options,
|
||||||
|
.xpath = xpath,
|
||||||
|
.is_interactive = is_interactive,
|
||||||
|
.node_name = node_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (should_visit) {
|
||||||
|
should_walk_children = try visitor.visit(node, &data);
|
||||||
|
did_visit = true; // Always true if should_visit was true, because visit() executed and opened structures
|
||||||
|
} else {
|
||||||
|
// If we skip the node, we must NOT tell the visitor to close it later
|
||||||
|
did_visit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (should_walk_children) {
|
||||||
|
// If we are printing this node normally OR skipping it and unrolling its children,
|
||||||
|
// we walk the children iterator.
|
||||||
|
var it = node.childrenIterator();
|
||||||
|
var tag_counts = std.StringArrayHashMap(usize).init(self.arena);
|
||||||
|
while (it.next()) |child| {
|
||||||
|
var tag: []const u8 = "text()";
|
||||||
|
if (child.is(Element)) |el| {
|
||||||
|
tag = el.getTagNameLower();
|
||||||
|
}
|
||||||
|
|
||||||
|
const gop = try tag_counts.getOrPut(tag);
|
||||||
|
if (!gop.found_existing) {
|
||||||
|
gop.value_ptr.* = 0;
|
||||||
|
}
|
||||||
|
gop.value_ptr.* += 1;
|
||||||
|
|
||||||
|
try self.walk(ctx, child, name, visitor, gop.value_ptr.*, current_depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (did_visit) {
|
||||||
|
try visitor.leave();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.xpath_buffer.shrinkRetainingCapacity(initial_xpath_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extractSelectOptions(node: *Node, page: *Page, arena: std.mem.Allocator) ![]OptionData {
|
||||||
|
var options: std.ArrayList(OptionData) = .empty;
|
||||||
|
var it = node.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
if (child.is(Element)) |el| {
|
||||||
|
if (el.getTag() == .option) {
|
||||||
|
if (el.is(Element.Html.Option)) |opt| {
|
||||||
|
const text = opt.getText(page);
|
||||||
|
const value = opt.getValue(page);
|
||||||
|
const selected = opt.getSelected();
|
||||||
|
try options.append(arena, .{ .text = text, .value = value, .selected = selected });
|
||||||
|
}
|
||||||
|
} else if (el.getTag() == .optgroup) {
|
||||||
|
var group_it = child.childrenIterator();
|
||||||
|
while (group_it.next()) |group_child| {
|
||||||
|
if (group_child.is(Element.Html.Option)) |opt| {
|
||||||
|
const text = opt.getText(page);
|
||||||
|
const value = opt.getValue(page);
|
||||||
|
const selected = opt.getSelected();
|
||||||
|
try options.append(arena, .{ .text = text, .value = value, .selected = selected });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options.toOwnedSlice(arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extractDataListOptions(list_id: []const u8, page: *Page, arena: std.mem.Allocator) !?[]OptionData {
|
||||||
|
if (page.document.getElementById(list_id, page)) |referenced_el| {
|
||||||
|
if (referenced_el.getTag() == .datalist) {
|
||||||
|
return try extractSelectOptions(referenced_el.asNode(), page, arena);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn appendXPathSegment(node: *Node, writer: anytype, index: usize) !void {
|
||||||
|
if (node.is(Element)) |el| {
|
||||||
|
const tag = el.getTagNameLower();
|
||||||
|
try std.fmt.format(writer, "/{s}[{d}]", .{ tag, index });
|
||||||
|
} else if (node.is(CData.Text)) |_| {
|
||||||
|
try std.fmt.format(writer, "/text()[{d}]", .{index});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonVisitor = struct {
|
||||||
|
jw: *std.json.Stringify,
|
||||||
|
tree: Self,
|
||||||
|
|
||||||
|
pub fn visit(self: *JsonVisitor, node: *Node, data: *NodeData) !bool {
|
||||||
|
try self.jw.beginObject();
|
||||||
|
|
||||||
|
try self.jw.objectField("nodeId");
|
||||||
|
try self.jw.write(try std.fmt.allocPrint(self.tree.arena, "{d}", .{data.id}));
|
||||||
|
|
||||||
|
try self.jw.objectField("backendDOMNodeId");
|
||||||
|
try self.jw.write(data.id);
|
||||||
|
|
||||||
|
try self.jw.objectField("nodeName");
|
||||||
|
try self.jw.write(data.node_name);
|
||||||
|
|
||||||
|
try self.jw.objectField("xpath");
|
||||||
|
try self.jw.write(data.xpath);
|
||||||
|
|
||||||
|
if (node.is(Element)) |el| {
|
||||||
|
try self.jw.objectField("nodeType");
|
||||||
|
try self.jw.write(1);
|
||||||
|
|
||||||
|
try self.jw.objectField("isInteractive");
|
||||||
|
try self.jw.write(data.is_interactive);
|
||||||
|
|
||||||
|
try self.jw.objectField("role");
|
||||||
|
try self.jw.write(data.role);
|
||||||
|
|
||||||
|
if (data.name) |name| {
|
||||||
|
if (name.len > 0) {
|
||||||
|
try self.jw.objectField("name");
|
||||||
|
try self.jw.write(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.value) |value| {
|
||||||
|
try self.jw.objectField("value");
|
||||||
|
try self.jw.write(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el._attributes) |attrs| {
|
||||||
|
try self.jw.objectField("attributes");
|
||||||
|
try self.jw.beginObject();
|
||||||
|
var iter = attrs.iterator();
|
||||||
|
while (iter.next()) |attr| {
|
||||||
|
try self.jw.objectField(attr._name.str());
|
||||||
|
try self.jw.write(attr._value.str());
|
||||||
|
}
|
||||||
|
try self.jw.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.options) |options| {
|
||||||
|
try self.jw.objectField("options");
|
||||||
|
try self.jw.beginArray();
|
||||||
|
for (options) |opt| {
|
||||||
|
try self.jw.beginObject();
|
||||||
|
try self.jw.objectField("value");
|
||||||
|
try self.jw.write(opt.value);
|
||||||
|
try self.jw.objectField("text");
|
||||||
|
try self.jw.write(opt.text);
|
||||||
|
try self.jw.objectField("selected");
|
||||||
|
try self.jw.write(opt.selected);
|
||||||
|
try self.jw.endObject();
|
||||||
|
}
|
||||||
|
try self.jw.endArray();
|
||||||
|
}
|
||||||
|
} else if (node.is(CData.Text)) |text_node| {
|
||||||
|
try self.jw.objectField("nodeType");
|
||||||
|
try self.jw.write(3);
|
||||||
|
try self.jw.objectField("nodeValue");
|
||||||
|
try self.jw.write(text_node.getWholeText());
|
||||||
|
} else {
|
||||||
|
try self.jw.objectField("nodeType");
|
||||||
|
try self.jw.write(9);
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.jw.objectField("children");
|
||||||
|
try self.jw.beginArray();
|
||||||
|
|
||||||
|
if (data.options != null) {
|
||||||
|
// Signal to not walk children, as we handled them natively
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leave(self: *JsonVisitor) !void {
|
||||||
|
try self.jw.endArray();
|
||||||
|
try self.jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn isStructuralRole(role: []const u8) bool {
|
||||||
|
const structural_roles = std.StaticStringMap(void).initComptime(.{
|
||||||
|
.{ "none", {} },
|
||||||
|
.{ "generic", {} },
|
||||||
|
.{ "InlineTextBox", {} },
|
||||||
|
.{ "banner", {} },
|
||||||
|
.{ "navigation", {} },
|
||||||
|
.{ "main", {} },
|
||||||
|
.{ "list", {} },
|
||||||
|
.{ "listitem", {} },
|
||||||
|
.{ "table", {} },
|
||||||
|
.{ "rowgroup", {} },
|
||||||
|
.{ "row", {} },
|
||||||
|
.{ "cell", {} },
|
||||||
|
.{ "region", {} },
|
||||||
|
});
|
||||||
|
return structural_roles.has(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextVisitor = struct {
|
||||||
|
writer: *std.Io.Writer,
|
||||||
|
tree: Self,
|
||||||
|
depth: usize,
|
||||||
|
|
||||||
|
pub fn visit(self: *TextVisitor, node: *Node, data: *NodeData) !bool {
|
||||||
|
for (0..self.depth) |_| {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
var name_to_print: ?[]const u8 = null;
|
||||||
|
if (data.name) |n| {
|
||||||
|
if (n.len > 0) {
|
||||||
|
name_to_print = n;
|
||||||
|
}
|
||||||
|
} else if (node.is(CData.Text)) |text_node| {
|
||||||
|
const trimmed = std.mem.trim(u8, text_node.getWholeText(), " \t\r\n");
|
||||||
|
if (trimmed.len > 0) {
|
||||||
|
name_to_print = trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_text_only = std.mem.eql(u8, data.role, "StaticText") or std.mem.eql(u8, data.role, "none") or std.mem.eql(u8, data.role, "generic");
|
||||||
|
|
||||||
|
try self.writer.print("{d}", .{data.id});
|
||||||
|
if (!is_text_only) {
|
||||||
|
try self.writer.print(" {s}", .{data.role});
|
||||||
|
}
|
||||||
|
if (name_to_print) |n| {
|
||||||
|
try self.writer.print(" '{s}'", .{n});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.value) |v| {
|
||||||
|
if (v.len > 0) {
|
||||||
|
try self.writer.print(" value='{s}'", .{v});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.options) |options| {
|
||||||
|
try self.writer.writeAll(" options=[");
|
||||||
|
for (options, 0..) |opt, i| {
|
||||||
|
if (i > 0) try self.writer.writeAll(",");
|
||||||
|
try self.writer.print("'{s}'", .{opt.value});
|
||||||
|
if (opt.selected) {
|
||||||
|
try self.writer.writeAll("*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try self.writer.writeAll("]\n");
|
||||||
|
self.depth += 1;
|
||||||
|
return false; // Native handling complete, do not walk children
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
self.depth += 1;
|
||||||
|
|
||||||
|
// If this is a leaf-like semantic node and we already have a name,
|
||||||
|
// skip children to avoid redundant StaticText or noise.
|
||||||
|
const is_leaf_semantic = std.mem.eql(u8, data.role, "link") or
|
||||||
|
std.mem.eql(u8, data.role, "button") or
|
||||||
|
std.mem.eql(u8, data.role, "heading") or
|
||||||
|
std.mem.eql(u8, data.role, "code");
|
||||||
|
if (is_leaf_semantic and data.name != null and data.name.?.len > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leave(self: *TextVisitor) !void {
|
||||||
|
if (self.depth > 0) {
|
||||||
|
self.depth -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("testing.zig");
|
||||||
|
|
||||||
|
test "SemanticTree backendDOMNodeId" {
|
||||||
|
var registry: CDPNode.Registry = .init(testing.allocator);
|
||||||
|
defer registry.deinit();
|
||||||
|
|
||||||
|
var page = try testing.pageTest("cdp/registry1.html");
|
||||||
|
defer testing.reset();
|
||||||
|
defer page._session.removePage();
|
||||||
|
|
||||||
|
const st: Self = .{
|
||||||
|
.dom_node = page.window._document.asNode(),
|
||||||
|
.registry = ®istry,
|
||||||
|
.page = page,
|
||||||
|
.arena = testing.arena_allocator,
|
||||||
|
.prune = false,
|
||||||
|
.interactive_only = false,
|
||||||
|
.max_depth = std.math.maxInt(u32) - 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const json_str = try std.json.Stringify.valueAlloc(testing.allocator, st, .{});
|
||||||
|
defer testing.allocator.free(json_str);
|
||||||
|
|
||||||
|
try testing.expect(std.mem.indexOf(u8, json_str, "\"backendDOMNodeId\":") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "SemanticTree max_depth" {
|
||||||
|
var registry: CDPNode.Registry = .init(testing.allocator);
|
||||||
|
defer registry.deinit();
|
||||||
|
|
||||||
|
var page = try testing.pageTest("cdp/registry1.html");
|
||||||
|
defer testing.reset();
|
||||||
|
defer page._session.removePage();
|
||||||
|
|
||||||
|
const st: Self = .{
|
||||||
|
.dom_node = page.window._document.asNode(),
|
||||||
|
.registry = ®istry,
|
||||||
|
.page = page,
|
||||||
|
.arena = testing.arena_allocator,
|
||||||
|
.prune = false,
|
||||||
|
.interactive_only = false,
|
||||||
|
.max_depth = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||||
|
defer aw.deinit();
|
||||||
|
|
||||||
|
try st.textStringify(&aw.writer);
|
||||||
|
const text_str = aw.written();
|
||||||
|
|
||||||
|
try testing.expect(std.mem.indexOf(u8, text_str, "other") == null);
|
||||||
|
}
|
||||||
1148
src/Server.zig
1148
src/Server.zig
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,11 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const URL = @import("browser/URL.zig");
|
||||||
|
|
||||||
const TestHTTPServer = @This();
|
const TestHTTPServer = @This();
|
||||||
|
|
||||||
shutdown: bool,
|
shutdown: std.atomic.Value(bool),
|
||||||
listener: ?std.net.Server,
|
listener: ?std.net.Server,
|
||||||
handler: Handler,
|
handler: Handler,
|
||||||
|
|
||||||
@@ -28,16 +29,23 @@ const Handler = *const fn (req: *std.http.Server.Request) anyerror!void;
|
|||||||
|
|
||||||
pub fn init(handler: Handler) TestHTTPServer {
|
pub fn init(handler: Handler) TestHTTPServer {
|
||||||
return .{
|
return .{
|
||||||
.shutdown = true,
|
.shutdown = .init(true),
|
||||||
.listener = null,
|
.listener = null,
|
||||||
.handler = handler,
|
.handler = handler,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *TestHTTPServer) void {
|
pub fn deinit(self: *TestHTTPServer) void {
|
||||||
self.shutdown = true;
|
self.listener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(self: *TestHTTPServer) void {
|
||||||
|
self.shutdown.store(true, .release);
|
||||||
if (self.listener) |*listener| {
|
if (self.listener) |*listener| {
|
||||||
listener.deinit();
|
switch (@import("builtin").target.os.tag) {
|
||||||
|
.linux => std.posix.shutdown(listener.stream.handle, .recv) catch {},
|
||||||
|
else => std.posix.close(listener.stream.handle),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,12 +54,13 @@ pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {
|
|||||||
|
|
||||||
self.listener = try address.listen(.{ .reuse_address = true });
|
self.listener = try address.listen(.{ .reuse_address = true });
|
||||||
var listener = &self.listener.?;
|
var listener = &self.listener.?;
|
||||||
|
self.shutdown.store(false, .release);
|
||||||
|
|
||||||
wg.finish();
|
wg.finish();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const conn = listener.accept() catch |err| {
|
const conn = listener.accept() catch |err| {
|
||||||
if (self.shutdown) {
|
if (self.shutdown.load(.acquire) or err == error.SocketNotListening) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
@@ -89,7 +98,10 @@ fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
|
pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
|
||||||
var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) {
|
var url_buf: [1024]u8 = undefined;
|
||||||
|
var fba = std.heap.FixedBufferAllocator.init(&url_buf);
|
||||||
|
const unescaped_file_path = try URL.unescape(fba.allocator(), file_path);
|
||||||
|
var file = std.fs.cwd().openFile(unescaped_file_path, .{}) catch |err| switch (err) {
|
||||||
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
|
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,17 +19,15 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
||||||
|
|
||||||
const js = @import("js/js.zig");
|
const js = @import("js/js.zig");
|
||||||
const log = @import("../log.zig");
|
|
||||||
const App = @import("../App.zig");
|
const App = @import("../App.zig");
|
||||||
const HttpClient = @import("../http/Client.zig");
|
const HttpClient = @import("HttpClient.zig");
|
||||||
const Notification = @import("../Notification.zig");
|
|
||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const ArenaPool = App.ArenaPool;
|
||||||
|
|
||||||
const Session = @import("Session.zig");
|
const Session = @import("Session.zig");
|
||||||
|
const Notification = @import("../Notification.zig");
|
||||||
|
|
||||||
// Browser is an instance of the browser.
|
// Browser is an instance of the browser.
|
||||||
// You can create multiple browser instances.
|
// You can create multiple browser instances.
|
||||||
@@ -40,54 +38,40 @@ env: js.Env,
|
|||||||
app: *App,
|
app: *App,
|
||||||
session: ?Session,
|
session: ?Session,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
arena_pool: *ArenaPool,
|
||||||
http_client: *HttpClient,
|
http_client: *HttpClient,
|
||||||
call_arena: ArenaAllocator,
|
|
||||||
page_arena: ArenaAllocator,
|
|
||||||
session_arena: ArenaAllocator,
|
|
||||||
transfer_arena: ArenaAllocator,
|
|
||||||
notification: *Notification,
|
|
||||||
|
|
||||||
pub fn init(app: *App) !Browser {
|
const InitOpts = struct {
|
||||||
|
env: js.Env.InitOpts = .{},
|
||||||
|
http_client: *HttpClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(app: *App, opts: InitOpts) !Browser {
|
||||||
const allocator = app.allocator;
|
const allocator = app.allocator;
|
||||||
|
|
||||||
var env = try js.Env.init(allocator, &app.platform, &app.snapshot);
|
var env = try js.Env.init(app, opts.env);
|
||||||
errdefer env.deinit();
|
errdefer env.deinit();
|
||||||
|
|
||||||
const notification = try Notification.init(allocator, app.notification);
|
|
||||||
app.http.client.notification = notification;
|
|
||||||
app.http.client.next_request_id = 0; // Should we track ids in CDP only?
|
|
||||||
errdefer notification.deinit();
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.app = app,
|
.app = app,
|
||||||
.env = env,
|
.env = env,
|
||||||
.session = null,
|
.session = null,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.notification = notification,
|
.arena_pool = &app.arena_pool,
|
||||||
.http_client = app.http.client,
|
.http_client = opts.http_client,
|
||||||
.call_arena = ArenaAllocator.init(allocator),
|
|
||||||
.page_arena = ArenaAllocator.init(allocator),
|
|
||||||
.session_arena = ArenaAllocator.init(allocator),
|
|
||||||
.transfer_arena = ArenaAllocator.init(allocator),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Browser) void {
|
pub fn deinit(self: *Browser) void {
|
||||||
self.closeSession();
|
self.closeSession();
|
||||||
self.env.deinit();
|
self.env.deinit();
|
||||||
self.call_arena.deinit();
|
|
||||||
self.page_arena.deinit();
|
|
||||||
self.session_arena.deinit();
|
|
||||||
self.transfer_arena.deinit();
|
|
||||||
self.http_client.notification = null;
|
|
||||||
self.notification.deinit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn newSession(self: *Browser) !*Session {
|
pub fn newSession(self: *Browser, notification: *Notification) !*Session {
|
||||||
self.closeSession();
|
self.closeSession();
|
||||||
self.session = @as(Session, undefined);
|
self.session = @as(Session, undefined);
|
||||||
const session = &self.session.?;
|
const session = &self.session.?;
|
||||||
try Session.init(session, self);
|
try Session.init(session, self, notification);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,20 +79,40 @@ pub fn closeSession(self: *Browser) void {
|
|||||||
if (self.session) |*session| {
|
if (self.session) |*session| {
|
||||||
session.deinit();
|
session.deinit();
|
||||||
self.session = null;
|
self.session = null;
|
||||||
_ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
self.env.memoryPressureNotification(.critical);
|
||||||
self.env.lowMemoryNotification();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMicrotasks(self: *const Browser) void {
|
pub fn runMicrotasks(self: *Browser) void {
|
||||||
self.env.runMicrotasks();
|
self.env.runMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMessageLoop(self: *const Browser) void {
|
pub fn runMacrotasks(self: *Browser) !void {
|
||||||
while (self.env.pumpMessageLoop()) {
|
const env = &self.env;
|
||||||
if (comptime IS_DEBUG) {
|
|
||||||
log.debug(.browser, "pumpMessageLoop", .{});
|
try self.env.runMacrotasks();
|
||||||
}
|
env.pumpMessageLoop();
|
||||||
}
|
|
||||||
|
// either of the above could have queued more microtasks
|
||||||
|
env.runMicrotasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hasBackgroundTasks(self: *Browser) bool {
|
||||||
|
return self.env.hasBackgroundTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn waitForBackgroundTasks(self: *Browser) void {
|
||||||
|
self.env.waitForBackgroundTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn msToNextMacrotask(self: *Browser) ?u64 {
|
||||||
|
return self.env.msToNextMacrotask();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn msTo(self: *Browser) bool {
|
||||||
|
return self.env.hasBackgroundTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runIdleTasks(self: *const Browser) void {
|
||||||
self.env.runIdleTasks();
|
self.env.runIdleTasks();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,30 +28,61 @@ const Page = @import("Page.zig");
|
|||||||
const Node = @import("webapi/Node.zig");
|
const Node = @import("webapi/Node.zig");
|
||||||
const Event = @import("webapi/Event.zig");
|
const Event = @import("webapi/Event.zig");
|
||||||
const EventTarget = @import("webapi/EventTarget.zig");
|
const EventTarget = @import("webapi/EventTarget.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
|
const EventKey = struct {
|
||||||
|
event_target: usize,
|
||||||
|
type_string: String,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventKeyContext = struct {
|
||||||
|
pub fn hash(_: @This(), key: EventKey) u64 {
|
||||||
|
var hasher = std.hash.Wyhash.init(0);
|
||||||
|
hasher.update(std.mem.asBytes(&key.event_target));
|
||||||
|
hasher.update(key.type_string.str());
|
||||||
|
return hasher.final();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(_: @This(), a: EventKey, b: EventKey) bool {
|
||||||
|
return a.event_target == b.event_target and a.type_string.eql(b.type_string);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub const EventManager = @This();
|
pub const EventManager = @This();
|
||||||
|
|
||||||
page: *Page,
|
page: *Page,
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
|
// Used as an optimization in Page._documentIsComplete. If we know there are no
|
||||||
|
// 'load' listeners in the document, we can skip dispatching the per-resource
|
||||||
|
// 'load' event (e.g. amazon product page has no listener and ~350 resources)
|
||||||
|
has_dom_load_listener: bool,
|
||||||
listener_pool: std.heap.MemoryPool(Listener),
|
listener_pool: std.heap.MemoryPool(Listener),
|
||||||
|
ignore_list: std.ArrayList(*Listener),
|
||||||
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
||||||
lookup: std.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList),
|
lookup: std.HashMapUnmanaged(
|
||||||
|
EventKey,
|
||||||
|
*std.DoublyLinkedList,
|
||||||
|
EventKeyContext,
|
||||||
|
std.hash_map.default_max_load_percentage,
|
||||||
|
),
|
||||||
dispatch_depth: usize,
|
dispatch_depth: usize,
|
||||||
deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),
|
deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),
|
||||||
|
|
||||||
pub fn init(page: *Page) EventManager {
|
pub fn init(arena: Allocator, page: *Page) EventManager {
|
||||||
return .{
|
return .{
|
||||||
.page = page,
|
.page = page,
|
||||||
.lookup = .{},
|
.lookup = .{},
|
||||||
.arena = page.arena,
|
.arena = arena,
|
||||||
.list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena),
|
.ignore_list = .{},
|
||||||
.listener_pool = std.heap.MemoryPool(Listener).init(page.arena),
|
.list_pool = .init(arena),
|
||||||
|
.listener_pool = .init(arena),
|
||||||
.dispatch_depth = 0,
|
.dispatch_depth = 0,
|
||||||
.deferred_removals = .{},
|
.deferred_removals = .{},
|
||||||
|
.has_dom_load_listener = false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +100,7 @@ pub const Callback = union(enum) {
|
|||||||
|
|
||||||
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void {
|
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once, .target = target });
|
log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once, .target = target.toString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a signal is provided and already aborted, don't register the listener
|
// If a signal is provided and already aborted, don't register the listener
|
||||||
@@ -79,20 +110,28 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const gop = try self.lookup.getOrPut(self.arena, @intFromPtr(target));
|
// Allocate the type string we'll use in both listener and key
|
||||||
|
const type_string = try String.init(self.arena, typ, .{});
|
||||||
|
|
||||||
|
if (type_string.eql(comptime .wrap("load")) and target._type == .node) {
|
||||||
|
self.has_dom_load_listener = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gop = try self.lookup.getOrPut(self.arena, .{
|
||||||
|
.type_string = type_string,
|
||||||
|
.event_target = @intFromPtr(target),
|
||||||
|
});
|
||||||
if (gop.found_existing) {
|
if (gop.found_existing) {
|
||||||
// check for duplicate callbacks already registered
|
// check for duplicate callbacks already registered
|
||||||
var node = gop.value_ptr.*.first;
|
var node = gop.value_ptr.*.first;
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
if (listener.typ.eqlSlice(typ)) {
|
const is_duplicate = switch (callback) {
|
||||||
const is_duplicate = switch (callback) {
|
.object => |obj| listener.function.eqlObject(obj),
|
||||||
.object => |obj| listener.function.eqlObject(obj),
|
.function => |func| listener.function.eqlFunction(func),
|
||||||
.function => |func| listener.function.eqlFunction(func),
|
};
|
||||||
};
|
if (is_duplicate and listener.capture == opts.capture) {
|
||||||
if (is_duplicate and listener.capture == opts.capture) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
node = n.next;
|
node = n.next;
|
||||||
}
|
}
|
||||||
@@ -102,8 +141,8 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
}
|
}
|
||||||
|
|
||||||
const func = switch (callback) {
|
const func = switch (callback) {
|
||||||
.function => |f| Function{ .value = f },
|
.function => |f| Function{ .value = try f.persist() },
|
||||||
.object => |o| Function{ .object = o },
|
.object => |o| Function{ .object = try o.persist() },
|
||||||
};
|
};
|
||||||
|
|
||||||
const listener = try self.listener_pool.create();
|
const listener = try self.listener_pool.create();
|
||||||
@@ -114,48 +153,67 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
.passive = opts.passive,
|
.passive = opts.passive,
|
||||||
.function = func,
|
.function = func,
|
||||||
.signal = opts.signal,
|
.signal = opts.signal,
|
||||||
.typ = try String.init(self.arena, typ, .{}),
|
.typ = type_string,
|
||||||
};
|
};
|
||||||
// append the listener to the list of listeners for this target
|
// append the listener to the list of listeners for this target
|
||||||
gop.value_ptr.*.append(&listener.node);
|
gop.value_ptr.*.append(&listener.node);
|
||||||
|
|
||||||
|
// Track load listeners for script execution ignore list
|
||||||
|
if (type_string.eql(comptime .wrap("load"))) {
|
||||||
|
try self.ignore_list.append(self.arena, listener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
|
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
|
||||||
const list = self.lookup.get(@intFromPtr(target)) orelse return;
|
const list = self.lookup.get(.{
|
||||||
if (findListener(list, typ, callback, use_capture)) |listener| {
|
.type_string = .wrap(typ),
|
||||||
|
.event_target = @intFromPtr(target),
|
||||||
|
}) orelse return;
|
||||||
|
if (findListener(list, callback, use_capture)) |listener| {
|
||||||
self.removeListener(list, listener);
|
self.removeListener(list, listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void {
|
pub fn clearIgnoreList(self: *EventManager) void {
|
||||||
|
self.ignore_list.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatching can be recursive from the compiler's point of view, so we need to
|
||||||
|
// give it an explicit error set so that other parts of the code can use and
|
||||||
|
// inferred error.
|
||||||
|
const DispatchError = error{
|
||||||
|
OutOfMemory,
|
||||||
|
StringTooLarge,
|
||||||
|
JSExecCallback,
|
||||||
|
CompilationError,
|
||||||
|
ExecutionError,
|
||||||
|
JsException,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DispatchOpts = struct {
|
||||||
|
// A "load" event triggered by a script (in ScriptManager) should not trigger
|
||||||
|
// a "load" listener added within that script. Therefore, any "load" listener
|
||||||
|
// that we add go into an ignore list until after the script finishes executing.
|
||||||
|
// The ignore list is only checked when apply_ignore == true, which is only
|
||||||
|
// set by the ScriptManager when raising the script's "load" event.
|
||||||
|
apply_ignore: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!void {
|
||||||
|
return self.dispatchOpts(target, event, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void {
|
||||||
|
event.acquireRef();
|
||||||
|
defer event.deinit(false, self.page._session);
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
||||||
}
|
}
|
||||||
|
|
||||||
event._target = target;
|
|
||||||
event._dispatch_target = target; // Store original target for composedPath()
|
|
||||||
var was_handled = false;
|
|
||||||
|
|
||||||
defer if (was_handled) {
|
|
||||||
self.page.js.runMicrotasks();
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (target._type) {
|
switch (target._type) {
|
||||||
.node => |node| try self.dispatchNode(node, event, &was_handled),
|
.node => |node| try self.dispatchNode(node, event, opts),
|
||||||
.xhr,
|
else => try self.dispatchDirect(target, event, null, .{ .context = "dispatch" }),
|
||||||
.window,
|
|
||||||
.abort_signal,
|
|
||||||
.media_query_list,
|
|
||||||
.message_port,
|
|
||||||
.text_track_cue,
|
|
||||||
.navigation,
|
|
||||||
.screen,
|
|
||||||
.screen_orientation,
|
|
||||||
.generic,
|
|
||||||
=> {
|
|
||||||
const list = self.lookup.get(@intFromPtr(target)) orelse return;
|
|
||||||
try self.dispatchAll(list, target, event, &was_handled);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,13 +222,28 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void
|
|||||||
// property is just a shortcut for calling addEventListener, but they are distinct.
|
// property is just a shortcut for calling addEventListener, but they are distinct.
|
||||||
// An event set via property cannot be removed by removeEventListener. If you
|
// An event set via property cannot be removed by removeEventListener. If you
|
||||||
// set both the property and add a listener, they both execute.
|
// set both the property and add a listener, they both execute.
|
||||||
const DispatchWithFunctionOptions = struct {
|
const DispatchDirectOptions = struct {
|
||||||
context: []const u8,
|
context: []const u8,
|
||||||
inject_target: bool = true,
|
inject_target: bool = true,
|
||||||
};
|
};
|
||||||
pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *Event, function_: ?js.Function, comptime opts: DispatchWithFunctionOptions) !void {
|
|
||||||
|
// Direct dispatch for non-DOM targets (Window, XHR, AbortSignal) or DOM nodes with
|
||||||
|
// property handlers. No propagation - just calls the handler and registered listeners.
|
||||||
|
// Handler can be: null, ?js.Function.Global, ?js.Function.Temp, or js.Function
|
||||||
|
pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, handler: anytype, comptime opts: DispatchDirectOptions) !void {
|
||||||
|
const page = self.page;
|
||||||
|
|
||||||
|
// Set window.event to the currently dispatching event (WHATWG spec)
|
||||||
|
const window = page.window;
|
||||||
|
const prev_event = window._current_event;
|
||||||
|
window._current_event = event;
|
||||||
|
defer window._current_event = prev_event;
|
||||||
|
|
||||||
|
event.acquireRef();
|
||||||
|
defer event.deinit(false, page._session);
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.event, "dispatchWithFunction", .{ .type = event._type_string.str(), .context = opts.context, .has_function = function_ != null });
|
log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comptime opts.inject_target) {
|
if (comptime opts.inject_target) {
|
||||||
@@ -179,11 +252,15 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
|||||||
}
|
}
|
||||||
|
|
||||||
var was_dispatched = false;
|
var was_dispatched = false;
|
||||||
defer if (was_dispatched) {
|
|
||||||
self.page.js.runMicrotasks();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (function_) |func| {
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer {
|
||||||
|
ls.local.runMicrotasks();
|
||||||
|
ls.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getFunction(handler, &ls.local)) |func| {
|
||||||
event._current_target = target;
|
event._current_target = target;
|
||||||
if (func.callWithThis(void, target, .{event})) {
|
if (func.callWithThis(void, target, .{event})) {
|
||||||
was_dispatched = true;
|
was_dispatched = true;
|
||||||
@@ -193,110 +270,15 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = self.lookup.get(@intFromPtr(target)) orelse return;
|
// listeners reigstered via addEventListener
|
||||||
try self.dispatchAll(list, target, event, &was_dispatched);
|
const list = self.lookup.get(.{
|
||||||
}
|
.event_target = @intFromPtr(target),
|
||||||
|
.type_string = event._type_string,
|
||||||
|
}) orelse return;
|
||||||
|
|
||||||
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void {
|
// This is a slightly simplified version of what you'll find in dispatchPhase
|
||||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
// It is simpler because, for direct dispatching, we know there's no ancestors
|
||||||
|
// and only the single target phase.
|
||||||
// Defer runs even on early return - ensures event phase is reset
|
|
||||||
// and default actions execute (unless prevented)
|
|
||||||
defer {
|
|
||||||
event._event_phase = .none;
|
|
||||||
|
|
||||||
// Execute default action if not prevented
|
|
||||||
if (event._prevent_default) {
|
|
||||||
// can't return in a defer (╯°□°)╯︵ ┻━┻
|
|
||||||
} else if (event._type_string.eqlSlice("click")) {
|
|
||||||
self.page.handleClick(target) catch |err| {
|
|
||||||
log.warn(.event, "page.click", .{ .err = err });
|
|
||||||
};
|
|
||||||
} else if (event._type_string.eqlSlice("keydown")) {
|
|
||||||
self.page.handleKeydown(target, event) catch |err| {
|
|
||||||
log.warn(.event, "page.keydown", .{ .err = err });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var path_len: usize = 0;
|
|
||||||
var path_buffer: [128]*EventTarget = undefined;
|
|
||||||
|
|
||||||
var node: ?*Node = target;
|
|
||||||
while (node) |n| {
|
|
||||||
if (path_len >= path_buffer.len) break;
|
|
||||||
path_buffer[path_len] = n.asEventTarget();
|
|
||||||
path_len += 1;
|
|
||||||
|
|
||||||
// Check if this node is a shadow root
|
|
||||||
if (n.is(ShadowRoot)) |shadow| {
|
|
||||||
event._needs_retargeting = true;
|
|
||||||
|
|
||||||
// If event is not composed, stop at shadow boundary
|
|
||||||
if (!event._composed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, jump to the shadow host and continue
|
|
||||||
node = shadow._host.asNode();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
node = n._parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Even though the window isn't part of the DOM, events always propagate
|
|
||||||
// through it in the capture phase (unless we stopped at a shadow boundary)
|
|
||||||
if (path_len < path_buffer.len) {
|
|
||||||
path_buffer[path_len] = self.page.window.asEventTarget();
|
|
||||||
path_len += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = path_buffer[0..path_len];
|
|
||||||
|
|
||||||
// Phase 1: Capturing phase (root → target, excluding target)
|
|
||||||
// This happens for all events, regardless of bubbling
|
|
||||||
event._event_phase = .capturing_phase;
|
|
||||||
var i: usize = path_len;
|
|
||||||
while (i > 1) {
|
|
||||||
i -= 1;
|
|
||||||
const current_target = path[i];
|
|
||||||
if (self.lookup.get(@intFromPtr(current_target))) |list| {
|
|
||||||
try self.dispatchPhase(list, current_target, event, was_handled, true);
|
|
||||||
if (event._stop_propagation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: At target
|
|
||||||
event._event_phase = .at_target;
|
|
||||||
const target_et = target.asEventTarget();
|
|
||||||
if (self.lookup.get(@intFromPtr(target_et))) |list| {
|
|
||||||
try self.dispatchPhase(list, target_et, event, was_handled, null);
|
|
||||||
if (event._stop_propagation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 3: Bubbling phase (target → root, excluding target)
|
|
||||||
// This only happens if the event bubbles
|
|
||||||
if (event._bubbles) {
|
|
||||||
event._event_phase = .bubbling_phase;
|
|
||||||
for (path[1..]) |current_target| {
|
|
||||||
if (self.lookup.get(@intFromPtr(current_target))) |list| {
|
|
||||||
try self.dispatchPhase(list, current_target, event, was_handled, false);
|
|
||||||
if (event._stop_propagation) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime capture_only: ?bool) !void {
|
|
||||||
const page = self.page;
|
|
||||||
const typ = event._type_string;
|
|
||||||
|
|
||||||
// Track dispatch depth for deferred removal
|
// Track dispatch depth for deferred removal
|
||||||
self.dispatch_depth += 1;
|
self.dispatch_depth += 1;
|
||||||
@@ -330,16 +312,6 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
is_done = (listener == last_listener);
|
is_done = (listener == last_listener);
|
||||||
node = n.next;
|
node = n.next;
|
||||||
|
|
||||||
// Skip non-matching listeners
|
|
||||||
if (!listener.typ.eql(typ)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (comptime capture_only) |capture| {
|
|
||||||
if (listener.capture != capture) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip removed listeners
|
// Skip removed listeners
|
||||||
if (listener.removed) {
|
if (listener.removed) {
|
||||||
continue;
|
continue;
|
||||||
@@ -358,6 +330,311 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
self.removeListener(list, listener);
|
self.removeListener(list, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
was_dispatched = true;
|
||||||
|
event._current_target = target;
|
||||||
|
|
||||||
|
switch (listener.function) {
|
||||||
|
.value => |value| try ls.toLocal(value).callWithThis(void, target, .{event}),
|
||||||
|
.string => |string| {
|
||||||
|
const str = try page.call_arena.dupeZ(u8, string.str());
|
||||||
|
try ls.local.eval(str, null);
|
||||||
|
},
|
||||||
|
.object => |obj_global| {
|
||||||
|
const obj = ls.toLocal(obj_global);
|
||||||
|
if (try obj.getFunction("handleEvent")) |handleEvent| {
|
||||||
|
try handleEvent.callWithThis(void, obj, .{event});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event._stop_immediate_propagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getFunction(handler: anytype, local: *const js.Local) ?js.Function {
|
||||||
|
const T = @TypeOf(handler);
|
||||||
|
const ti = @typeInfo(T);
|
||||||
|
|
||||||
|
if (ti == .null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (ti == .optional) {
|
||||||
|
return getFunction(handler orelse return null, local);
|
||||||
|
}
|
||||||
|
return switch (T) {
|
||||||
|
js.Function => handler,
|
||||||
|
js.Function.Temp => local.toLocal(handler),
|
||||||
|
js.Function.Global => local.toLocal(handler),
|
||||||
|
else => @compileError("handler must be null or \\??js.Function(\\.(Temp|Global))?"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if there are any listeners for a direct dispatch (non-DOM target).
|
||||||
|
/// Use this to avoid creating an event when there are no listeners.
|
||||||
|
pub fn hasDirectListeners(self: *EventManager, target: *EventTarget, typ: []const u8, handler: anytype) bool {
|
||||||
|
if (hasHandler(handler)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return self.lookup.get(.{
|
||||||
|
.event_target = @intFromPtr(target),
|
||||||
|
.type_string = .wrap(typ),
|
||||||
|
}) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasHandler(handler: anytype) bool {
|
||||||
|
const ti = @typeInfo(@TypeOf(handler));
|
||||||
|
if (ti == .null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ti == .optional) {
|
||||||
|
return handler != null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts: DispatchOpts) !void {
|
||||||
|
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||||
|
|
||||||
|
{
|
||||||
|
const et = target.asEventTarget();
|
||||||
|
event._target = et;
|
||||||
|
event._dispatch_target = et; // Store original target for composedPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = self.page;
|
||||||
|
|
||||||
|
// Set window.event to the currently dispatching event (WHATWG spec)
|
||||||
|
const window = page.window;
|
||||||
|
const prev_event = window._current_event;
|
||||||
|
window._current_event = event;
|
||||||
|
defer window._current_event = prev_event;
|
||||||
|
|
||||||
|
var was_handled = false;
|
||||||
|
|
||||||
|
// Create a single scope for all event handlers in this dispatch.
|
||||||
|
// This ensures function handles passed to queueMicrotask remain valid
|
||||||
|
// throughout the entire dispatch, preventing crashes when microtasks run.
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer {
|
||||||
|
if (was_handled) {
|
||||||
|
ls.local.runMicrotasks();
|
||||||
|
}
|
||||||
|
ls.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const activation_state = try ActivationState.create(event, target, page);
|
||||||
|
|
||||||
|
// Defer runs even on early return - ensures event phase is reset
|
||||||
|
// and default actions execute (unless prevented)
|
||||||
|
defer {
|
||||||
|
event._event_phase = .none;
|
||||||
|
event._stop_propagation = false;
|
||||||
|
event._stop_immediate_propagation = false;
|
||||||
|
// Handle checkbox/radio activation rollback or commit
|
||||||
|
if (activation_state) |state| {
|
||||||
|
state.restore(event, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute default action if not prevented
|
||||||
|
if (event._prevent_default) {
|
||||||
|
// can't return in a defer (╯°□°)╯︵ ┻━┻
|
||||||
|
} else if (event._type_string.eql(comptime .wrap("click"))) {
|
||||||
|
page.handleClick(target) catch |err| {
|
||||||
|
log.warn(.event, "page.click", .{ .err = err });
|
||||||
|
};
|
||||||
|
} else if (event._type_string.eql(comptime .wrap("keydown"))) {
|
||||||
|
page.handleKeydown(target, event) catch |err| {
|
||||||
|
log.warn(.event, "page.keydown", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var path_len: usize = 0;
|
||||||
|
var path_buffer: [128]*EventTarget = undefined;
|
||||||
|
|
||||||
|
var node: ?*Node = target;
|
||||||
|
while (node) |n| {
|
||||||
|
if (path_len >= path_buffer.len) break;
|
||||||
|
path_buffer[path_len] = n.asEventTarget();
|
||||||
|
path_len += 1;
|
||||||
|
|
||||||
|
// Check if this node is a shadow root
|
||||||
|
if (n.is(ShadowRoot)) |shadow| {
|
||||||
|
event._needs_retargeting = true;
|
||||||
|
|
||||||
|
// If event is not composed, stop at shadow boundary
|
||||||
|
if (!event._composed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, jump to the shadow host and continue
|
||||||
|
node = shadow._host.asNode();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = n._parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even though the window isn't part of the DOM, most events propagate
|
||||||
|
// through it in the capture phase (unless we stopped at a shadow boundary)
|
||||||
|
// The only explicit exception is "load"
|
||||||
|
if (event._type_string.eql(comptime .wrap("load")) == false) {
|
||||||
|
if (path_len < path_buffer.len) {
|
||||||
|
path_buffer[path_len] = page.window.asEventTarget();
|
||||||
|
path_len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = path_buffer[0..path_len];
|
||||||
|
|
||||||
|
// Phase 1: Capturing phase (root → target, excluding target)
|
||||||
|
// This happens for all events, regardless of bubbling
|
||||||
|
event._event_phase = .capturing_phase;
|
||||||
|
var i: usize = path_len;
|
||||||
|
while (i > 1) {
|
||||||
|
i -= 1;
|
||||||
|
if (event._stop_propagation) return;
|
||||||
|
const current_target = path[i];
|
||||||
|
if (self.lookup.get(.{
|
||||||
|
.event_target = @intFromPtr(current_target),
|
||||||
|
.type_string = event._type_string,
|
||||||
|
})) |list| {
|
||||||
|
try self.dispatchPhase(list, current_target, event, &was_handled, &ls.local, comptime .init(true, opts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: At target
|
||||||
|
if (event._stop_propagation) return;
|
||||||
|
event._event_phase = .at_target;
|
||||||
|
const target_et = target.asEventTarget();
|
||||||
|
|
||||||
|
blk: {
|
||||||
|
// Get inline handler (e.g., onclick property) for this target
|
||||||
|
if (self.getInlineHandler(target_et, event)) |inline_handler| {
|
||||||
|
was_handled = true;
|
||||||
|
event._current_target = target_et;
|
||||||
|
|
||||||
|
try ls.toLocal(inline_handler).callWithThis(void, target_et, .{event});
|
||||||
|
|
||||||
|
if (event._stop_propagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event._stop_immediate_propagation) {
|
||||||
|
break :blk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.lookup.get(.{
|
||||||
|
.type_string = event._type_string,
|
||||||
|
.event_target = @intFromPtr(target_et),
|
||||||
|
})) |list| {
|
||||||
|
try self.dispatchPhase(list, target_et, event, &was_handled, &ls.local, comptime .init(null, opts));
|
||||||
|
if (event._stop_propagation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Bubbling phase (target → root, excluding target)
|
||||||
|
// This only happens if the event bubbles
|
||||||
|
if (event._bubbles) {
|
||||||
|
event._event_phase = .bubbling_phase;
|
||||||
|
for (path[1..]) |current_target| {
|
||||||
|
if (event._stop_propagation) break;
|
||||||
|
if (self.lookup.get(.{
|
||||||
|
.type_string = event._type_string,
|
||||||
|
.event_target = @intFromPtr(current_target),
|
||||||
|
})) |list| {
|
||||||
|
try self.dispatchPhase(list, current_target, event, &was_handled, &ls.local, comptime .init(false, opts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DispatchPhaseOpts = struct {
|
||||||
|
capture_only: ?bool = null,
|
||||||
|
apply_ignore: bool = false,
|
||||||
|
|
||||||
|
fn init(capture_only: ?bool, opts: DispatchOpts) DispatchPhaseOpts {
|
||||||
|
return .{
|
||||||
|
.capture_only = capture_only,
|
||||||
|
.apply_ignore = opts.apply_ignore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, local: *const js.Local, comptime opts: DispatchPhaseOpts) !void {
|
||||||
|
const page = self.page;
|
||||||
|
|
||||||
|
// Track dispatch depth for deferred removal
|
||||||
|
self.dispatch_depth += 1;
|
||||||
|
defer {
|
||||||
|
const dispatch_depth = self.dispatch_depth;
|
||||||
|
// Only destroy deferred listeners when we exit the outermost dispatch
|
||||||
|
if (dispatch_depth == 1) {
|
||||||
|
for (self.deferred_removals.items) |removal| {
|
||||||
|
removal.list.remove(&removal.listener.node);
|
||||||
|
self.listener_pool.destroy(removal.listener);
|
||||||
|
}
|
||||||
|
self.deferred_removals.clearRetainingCapacity();
|
||||||
|
} else {
|
||||||
|
self.dispatch_depth = dispatch_depth - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the last listener in the list as sentinel - listeners added during dispatch will be after it
|
||||||
|
const last_node = list.last orelse return;
|
||||||
|
const last_listener: *Listener = @alignCast(@fieldParentPtr("node", last_node));
|
||||||
|
|
||||||
|
// Iterate through the list, stopping after we've encountered the last_listener
|
||||||
|
var node = list.first;
|
||||||
|
var is_done = false;
|
||||||
|
node_loop: while (node) |n| {
|
||||||
|
if (is_done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
|
is_done = (listener == last_listener);
|
||||||
|
node = n.next;
|
||||||
|
|
||||||
|
// Skip non-matching listeners
|
||||||
|
if (comptime opts.capture_only) |capture| {
|
||||||
|
if (listener.capture != capture) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip removed listeners
|
||||||
|
if (listener.removed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the listener has an aborted signal, remove it and skip
|
||||||
|
if (listener.signal) |signal| {
|
||||||
|
if (signal.getAborted()) {
|
||||||
|
self.removeListener(list, listener);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime opts.apply_ignore) {
|
||||||
|
for (self.ignore_list.items) |ignored| {
|
||||||
|
if (ignored == listener) {
|
||||||
|
continue :node_loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
|
||||||
|
if (listener.once) {
|
||||||
|
self.removeListener(list, listener);
|
||||||
|
}
|
||||||
|
|
||||||
was_handled.* = true;
|
was_handled.* = true;
|
||||||
event._current_target = current_target;
|
event._current_target = current_target;
|
||||||
|
|
||||||
@@ -368,12 +645,13 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (listener.function) {
|
switch (listener.function) {
|
||||||
.value => |value| try value.callWithThis(void, current_target, .{event}),
|
.value => |value| try local.toLocal(value).callWithThis(void, current_target, .{event}),
|
||||||
.string => |string| {
|
.string => |string| {
|
||||||
const str = try page.call_arena.dupeZ(u8, string.str());
|
const str = try page.call_arena.dupeZ(u8, string.str());
|
||||||
try self.page.js.eval(str, null);
|
try local.eval(str, null);
|
||||||
},
|
},
|
||||||
.object => |obj| {
|
.object => |obj_global| {
|
||||||
|
const obj = local.toLocal(obj_global);
|
||||||
if (try obj.getFunction("handleEvent")) |handleEvent| {
|
if (try obj.getFunction("handleEvent")) |handleEvent| {
|
||||||
try handleEvent.callWithThis(void, obj, .{event});
|
try handleEvent.callWithThis(void, obj, .{event});
|
||||||
}
|
}
|
||||||
@@ -391,9 +669,20 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-Node dispatching (XHR, Window without propagation)
|
fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?js.Function.Global {
|
||||||
fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool) !void {
|
const global_event_handlers = @import("webapi/global_event_handlers.zig");
|
||||||
return self.dispatchPhase(list, current_target, event, was_handled, null);
|
const handler_type = global_event_handlers.fromEventType(event._type_string.str()) orelse return null;
|
||||||
|
|
||||||
|
// Look up the inline handler for this target
|
||||||
|
const html_element = switch (target._type) {
|
||||||
|
.node => |n| n.is(Element.Html) orelse return null,
|
||||||
|
else => return null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return html_element.getAttributeFunction(handler_type, self.page) catch |err| {
|
||||||
|
log.warn(.event, "inline html callback", .{ .type = handler_type, .err = err });
|
||||||
|
return null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {
|
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {
|
||||||
@@ -408,7 +697,7 @@ fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *L
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Callback, capture: bool) ?*Listener {
|
fn findListener(list: *const std.DoublyLinkedList, callback: Callback, capture: bool) ?*Listener {
|
||||||
var node = list.first;
|
var node = list.first;
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
node = n.next;
|
node = n.next;
|
||||||
@@ -423,9 +712,6 @@ fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Ca
|
|||||||
if (listener.capture != capture) {
|
if (listener.capture != capture) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!listener.typ.eqlSlice(typ)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return listener;
|
return listener;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -443,20 +729,20 @@ const Listener = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Function = union(enum) {
|
const Function = union(enum) {
|
||||||
value: js.Function,
|
value: js.Function.Global,
|
||||||
string: String,
|
string: String,
|
||||||
object: js.Object,
|
object: js.Object.Global,
|
||||||
|
|
||||||
fn eqlFunction(self: Function, func: js.Function) bool {
|
fn eqlFunction(self: Function, func: js.Function) bool {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.value => |v| return v.id == func.id,
|
.value => |v| v.isEqual(func),
|
||||||
else => false,
|
else => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eqlObject(self: Function, obj: js.Object) bool {
|
fn eqlObject(self: Function, obj: js.Object) bool {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.object => |o| return o.getId() == obj.getId(),
|
.object => |o| return o.isEqual(obj),
|
||||||
else => false,
|
else => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -514,3 +800,144 @@ fn isAncestorOrSelf(ancestor: *Node, node: *Node) bool {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles the default action for clicking on input checked/radio. Maybe this
|
||||||
|
// could be generalized if needed, but I'm not sure. This wasn't obvious to me
|
||||||
|
// but when an input is clicked, it's important to think about both the intent
|
||||||
|
// and the actual result. Imagine you have an unchecked checkbox. When clicked,
|
||||||
|
// the checkbox immediately becomes checked, and event handlers see this "checked"
|
||||||
|
// intent. But a listener can preventDefault() in which case the check we did at
|
||||||
|
// the start will be undone.
|
||||||
|
// This is a bit more complicated for radio buttons, as the checking/unchecking
|
||||||
|
// and the rollback can impact a different radio input. So if you "check" a radio
|
||||||
|
// the intent is that it becomes checked and whatever was checked before becomes
|
||||||
|
// unchecked, so that if you have to rollback (because of a preventDefault())
|
||||||
|
// then both inputs have to revert to their original values.
|
||||||
|
const ActivationState = struct {
|
||||||
|
old_checked: bool,
|
||||||
|
input: *Element.Html.Input,
|
||||||
|
previously_checked_radio: ?*Input,
|
||||||
|
|
||||||
|
const Input = Element.Html.Input;
|
||||||
|
|
||||||
|
fn create(event: *const Event, target: *Node, page: *Page) !?ActivationState {
|
||||||
|
if (event._type_string.eql(comptime .wrap("click")) == false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = target.is(Element.Html.Input) orelse return null;
|
||||||
|
if (input._input_type != .checkbox and input._input_type != .radio) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const old_checked = input._checked;
|
||||||
|
var previously_checked_radio: ?*Element.Html.Input = null;
|
||||||
|
|
||||||
|
// For radio buttons, find the currently checked radio in the group
|
||||||
|
if (input._input_type == .radio and !old_checked) {
|
||||||
|
previously_checked_radio = try findCheckedRadioInGroup(input, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle checkbox or check radio (which unchecks others in group)
|
||||||
|
const new_checked = if (input._input_type == .checkbox) !old_checked else true;
|
||||||
|
try input.setChecked(new_checked, page);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.input = input,
|
||||||
|
.old_checked = old_checked,
|
||||||
|
.previously_checked_radio = previously_checked_radio,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore(self: *const ActivationState, event: *const Event, page: *Page) void {
|
||||||
|
const input = self.input;
|
||||||
|
if (event._prevent_default) {
|
||||||
|
// Rollback: restore previous state
|
||||||
|
input._checked = self.old_checked;
|
||||||
|
input._checked_dirty = true;
|
||||||
|
if (self.previously_checked_radio) |prev_radio| {
|
||||||
|
prev_radio._checked = true;
|
||||||
|
prev_radio._checked_dirty = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit: fire input and change events only if state actually changed
|
||||||
|
// and the element is connected to a document (detached elements don't fire).
|
||||||
|
// For checkboxes, state always changes. For radios, only if was unchecked.
|
||||||
|
const state_changed = (input._input_type == .checkbox) or !self.old_checked;
|
||||||
|
if (state_changed and input.asElement().asNode().isConnected()) {
|
||||||
|
fireEvent(page, input, "input") catch |err| {
|
||||||
|
log.warn(.event, "input event", .{ .err = err });
|
||||||
|
};
|
||||||
|
fireEvent(page, input, "change") catch |err| {
|
||||||
|
log.warn(.event, "change event", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findCheckedRadioInGroup(input: *Input, page: *Page) !?*Input {
|
||||||
|
const elem = input.asElement();
|
||||||
|
|
||||||
|
const name = elem.getAttributeSafe(comptime .wrap("name")) orelse return null;
|
||||||
|
if (name.len == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = input.getForm(page);
|
||||||
|
|
||||||
|
// Walk from the root of the tree containing this element
|
||||||
|
// This handles both document-attached and orphaned elements
|
||||||
|
const root = elem.asNode().getRootNode(null);
|
||||||
|
|
||||||
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
|
var walker = TreeWalker.Full.init(root, .{});
|
||||||
|
|
||||||
|
while (walker.next()) |node| {
|
||||||
|
const other_element = node.is(Element) orelse continue;
|
||||||
|
const other_input = other_element.is(Input) orelse continue;
|
||||||
|
|
||||||
|
if (other_input._input_type != .radio) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the input we're checking from
|
||||||
|
if (other_input == input) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const other_name = other_element.getAttributeSafe(comptime .wrap("name")) orelse continue;
|
||||||
|
if (!std.mem.eql(u8, name, other_name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if same form context
|
||||||
|
const other_form = other_input.getForm(page);
|
||||||
|
if (form) |f| {
|
||||||
|
const of = other_form orelse continue;
|
||||||
|
if (f != of) {
|
||||||
|
continue; // Different forms
|
||||||
|
}
|
||||||
|
} else if (other_form != null) {
|
||||||
|
continue; // form is null but other has a form
|
||||||
|
}
|
||||||
|
|
||||||
|
if (other_input._checked) {
|
||||||
|
return other_input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire input or change event
|
||||||
|
fn fireEvent(page: *Page, input: *Input, comptime typ: []const u8) !void {
|
||||||
|
const event = try Event.initTrusted(comptime .wrap(typ), .{
|
||||||
|
.bubbles = true,
|
||||||
|
.cancelable = false,
|
||||||
|
}, page);
|
||||||
|
|
||||||
|
const target = input.asElement().asEventTarget();
|
||||||
|
try page._event_manager.dispatch(target, event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -17,10 +17,8 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const reflect = @import("reflect.zig");
|
const reflect = @import("reflect.zig");
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
const String = @import("../string.zig").String;
|
const String = @import("../string.zig").String;
|
||||||
@@ -31,6 +29,7 @@ const Page = @import("Page.zig");
|
|||||||
const Node = @import("webapi/Node.zig");
|
const Node = @import("webapi/Node.zig");
|
||||||
const Event = @import("webapi/Event.zig");
|
const Event = @import("webapi/Event.zig");
|
||||||
const UIEvent = @import("webapi/event/UIEvent.zig");
|
const UIEvent = @import("webapi/event/UIEvent.zig");
|
||||||
|
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||||
const Element = @import("webapi/Element.zig");
|
const Element = @import("webapi/Element.zig");
|
||||||
const Document = @import("webapi/Document.zig");
|
const Document = @import("webapi/Document.zig");
|
||||||
const EventTarget = @import("webapi/EventTarget.zig");
|
const EventTarget = @import("webapi/EventTarget.zig");
|
||||||
@@ -38,10 +37,99 @@ const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.
|
|||||||
const Blob = @import("webapi/Blob.zig");
|
const Blob = @import("webapi/Blob.zig");
|
||||||
const AbstractRange = @import("webapi/AbstractRange.zig");
|
const AbstractRange = @import("webapi/AbstractRange.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
// Shared across all frames of a Page.
|
||||||
const Factory = @This();
|
const Factory = @This();
|
||||||
_page: *Page,
|
|
||||||
|
_arena: Allocator,
|
||||||
_slab: SlabAllocator,
|
_slab: SlabAllocator,
|
||||||
|
|
||||||
|
pub fn init(arena: Allocator) Factory {
|
||||||
|
return .{
|
||||||
|
._arena = arena,
|
||||||
|
._slab = SlabAllocator.init(arena, 128),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a root object
|
||||||
|
pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
return self.eventTargetWithAllocator(self._slab.allocator(), child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eventTargetWithAllocator(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ EventTarget, @TypeOf(child) },
|
||||||
|
).allocate(allocator);
|
||||||
|
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = .{
|
||||||
|
._type = unionInit(EventTarget.Type, chain.get(1)),
|
||||||
|
};
|
||||||
|
chain.setLeaf(1, child);
|
||||||
|
|
||||||
|
return chain.get(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn standaloneEventTarget(self: *Factory, child: anytype) !*EventTarget {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
const et = try allocator.create(EventTarget);
|
||||||
|
et.* = .{ ._type = unionInit(EventTarget.Type, child) };
|
||||||
|
return et;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a root object
|
||||||
|
pub fn event(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Event, @TypeOf(child) },
|
||||||
|
).allocate(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setLeaf(1, child);
|
||||||
|
|
||||||
|
return chain.get(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uiEvent(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Event, UIEvent, @TypeOf(child) },
|
||||||
|
).allocate(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setMiddle(1, UIEvent.Type);
|
||||||
|
chain.setLeaf(2, child);
|
||||||
|
|
||||||
|
return chain.get(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mouseEvent(_: *const Factory, arena: Allocator, typ: String, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Event, UIEvent, MouseEvent, @TypeOf(child) },
|
||||||
|
).allocate(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setMiddle(1, UIEvent.Type);
|
||||||
|
|
||||||
|
// Set MouseEvent with all its fields
|
||||||
|
const mouse_ptr = chain.get(2);
|
||||||
|
mouse_ptr.* = mouse;
|
||||||
|
mouse_ptr._proto = chain.get(1);
|
||||||
|
mouse_ptr._type = unionInit(MouseEvent.Type, chain.get(3));
|
||||||
|
|
||||||
|
chain.setLeaf(3, child);
|
||||||
|
|
||||||
|
return chain.get(3);
|
||||||
|
}
|
||||||
|
|
||||||
fn PrototypeChain(comptime types: []const type) type {
|
fn PrototypeChain(comptime types: []const type) type {
|
||||||
return struct {
|
return struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
@@ -145,83 +233,29 @@ fn AutoPrototypeChain(comptime types: []const type) type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(page: *Page) Factory {
|
fn eventInit(arena: Allocator, typ: String, value: anytype) !Event {
|
||||||
return .{
|
|
||||||
._page = page,
|
|
||||||
._slab = SlabAllocator.init(page.arena, 128),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is a root object
|
|
||||||
pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ EventTarget, @TypeOf(child) },
|
|
||||||
).allocate(allocator);
|
|
||||||
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = .{
|
|
||||||
._type = unionInit(EventTarget.Type, chain.get(1)),
|
|
||||||
};
|
|
||||||
chain.setLeaf(1, child);
|
|
||||||
|
|
||||||
return chain.get(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eventInit(typ: []const u8, value: anytype, page: *Page) !Event {
|
|
||||||
// Round to 2ms for privacy (browsers do this)
|
// Round to 2ms for privacy (browsers do this)
|
||||||
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
|
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
|
||||||
const time_stamp = (raw_timestamp / 2) * 2;
|
const time_stamp = (raw_timestamp / 2) * 2;
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
|
._rc = 0,
|
||||||
|
._arena = arena,
|
||||||
._type = unionInit(Event.Type, value),
|
._type = unionInit(Event.Type, value),
|
||||||
._type_string = try String.init(page.arena, typ, .{}),
|
._type_string = typ,
|
||||||
._time_stamp = time_stamp,
|
._time_stamp = time_stamp,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is a root object
|
pub fn blob(_: *const Factory, arena: Allocator, child: anytype) !*@TypeOf(child) {
|
||||||
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
|
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ Event, @TypeOf(child) },
|
|
||||||
).allocate(allocator);
|
|
||||||
|
|
||||||
// Special case: Event has a _type_string field, so we need manual setup
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = try eventInit(typ, chain.get(1), self._page);
|
|
||||||
chain.setLeaf(1, child);
|
|
||||||
|
|
||||||
return chain.get(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn uiEvent(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
|
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ Event, UIEvent, @TypeOf(child) },
|
|
||||||
).allocate(allocator);
|
|
||||||
|
|
||||||
// Special case: Event has a _type_string field, so we need manual setup
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = try eventInit(typ, chain.get(1), self._page);
|
|
||||||
chain.setMiddle(1, UIEvent.Type);
|
|
||||||
chain.setLeaf(2, child);
|
|
||||||
|
|
||||||
return chain.get(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) {
|
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
|
|
||||||
// Special case: Blob has slice and mime fields, so we need manual setup
|
// Special case: Blob has slice and mime fields, so we need manual setup
|
||||||
const chain = try PrototypeChain(
|
const chain = try PrototypeChain(
|
||||||
&.{ Blob, @TypeOf(child) },
|
&.{ Blob, @TypeOf(child) },
|
||||||
).allocate(allocator);
|
).allocate(arena);
|
||||||
|
|
||||||
const blob_ptr = chain.get(0);
|
const blob_ptr = chain.get(0);
|
||||||
blob_ptr.* = .{
|
blob_ptr.* = .{
|
||||||
|
._arena = arena,
|
||||||
._type = unionInit(Blob.Type, chain.get(1)),
|
._type = unionInit(Blob.Type, chain.get(1)),
|
||||||
._slice = "",
|
._slice = "",
|
||||||
._mime = "",
|
._mime = "",
|
||||||
@@ -231,19 +265,23 @@ pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) {
|
|||||||
return chain.get(1);
|
return chain.get(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn abstractRange(self: *Factory, child: anytype, page: *Page) !*@TypeOf(child) {
|
pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, page: *Page) !*@TypeOf(child) {
|
||||||
const allocator = self._slab.allocator();
|
const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(arena);
|
||||||
const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(allocator);
|
|
||||||
|
|
||||||
const doc = page.document.asNode();
|
const doc = page.document.asNode();
|
||||||
chain.set(0, AbstractRange{
|
const abstract_range = chain.get(0);
|
||||||
|
abstract_range.* = AbstractRange{
|
||||||
|
._rc = 0,
|
||||||
|
._arena = arena,
|
||||||
|
._page_id = page.id,
|
||||||
._type = unionInit(AbstractRange.Type, chain.get(1)),
|
._type = unionInit(AbstractRange.Type, chain.get(1)),
|
||||||
._end_offset = 0,
|
._end_offset = 0,
|
||||||
._start_offset = 0,
|
._start_offset = 0,
|
||||||
._end_container = doc,
|
._end_container = doc,
|
||||||
._start_container = doc,
|
._start_container = doc,
|
||||||
});
|
};
|
||||||
chain.setLeaf(1, child);
|
chain.setLeaf(1, child);
|
||||||
|
page._live_ranges.append(&abstract_range._range_link);
|
||||||
return chain.get(1);
|
return chain.get(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +344,7 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO
|
|||||||
chain.setMiddle(2, Element.Type);
|
chain.setMiddle(2, Element.Type);
|
||||||
|
|
||||||
// will never allocate, can't fail
|
// will never allocate, can't fail
|
||||||
const tag_name_str = String.init(self._page.arena, tag_name, .{}) catch unreachable;
|
const tag_name_str = String.init(self._arena, tag_name, .{}) catch unreachable;
|
||||||
|
|
||||||
// Manually set Element.Svg with the tag_name
|
// Manually set Element.Svg with the tag_name
|
||||||
chain.set(3, .{
|
chain.set(3, .{
|
||||||
@@ -319,9 +357,7 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO
|
|||||||
return chain.get(4);
|
return chain.get(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
pub fn xhrEventTarget(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
|
|
||||||
return try AutoPrototypeChain(
|
return try AutoPrototypeChain(
|
||||||
&.{ EventTarget, XMLHttpRequestEventTarget, @TypeOf(child) },
|
&.{ EventTarget, XMLHttpRequestEventTarget, @TypeOf(child) },
|
||||||
).create(allocator, child);
|
).create(allocator, child);
|
||||||
@@ -336,32 +372,6 @@ pub fn textTrackCue(self: *Factory, child: anytype) !*@TypeOf(child) {
|
|||||||
).create(allocator, child);
|
).create(allocator, child);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hasChainRoot(comptime T: type) bool {
|
|
||||||
// Check if this is a root
|
|
||||||
if (@hasDecl(T, "_prototype_root")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no _proto field, we're at the top but not a recognized root
|
|
||||||
if (!@hasField(T, "_proto")) return false;
|
|
||||||
|
|
||||||
// Get the _proto field's type and recurse
|
|
||||||
const fields = @typeInfo(T).@"struct".fields;
|
|
||||||
inline for (fields) |field| {
|
|
||||||
if (std.mem.eql(u8, field.name, "_proto")) {
|
|
||||||
const ProtoType = reflect.Struct(field.type);
|
|
||||||
return hasChainRoot(ProtoType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isChainType(comptime T: type) bool {
|
|
||||||
if (@hasField(T, "_proto")) return false;
|
|
||||||
return comptime hasChainRoot(T);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn destroy(self: *Factory, value: anytype) void {
|
pub fn destroy(self: *Factory, value: anytype) void {
|
||||||
const S = reflect.Struct(@TypeOf(value));
|
const S = reflect.Struct(@TypeOf(value));
|
||||||
|
|
||||||
@@ -378,35 +388,21 @@ pub fn destroy(self: *Factory, value: anytype) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comptime isChainType(S)) {
|
if (comptime @hasField(S, "_proto")) {
|
||||||
self.destroyChain(value, true, 0, std.mem.Alignment.@"1");
|
self.destroyChain(value, 0, std.mem.Alignment.@"1");
|
||||||
} else {
|
} else {
|
||||||
self.destroyStandalone(value);
|
self.destroyStandalone(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroyStandalone(self: *Factory, value: anytype) void {
|
pub fn destroyStandalone(self: *Factory, value: anytype) void {
|
||||||
const S = reflect.Struct(@TypeOf(value));
|
|
||||||
assert(!@hasDecl(S, "_prototype_root"));
|
|
||||||
|
|
||||||
const allocator = self._slab.allocator();
|
const allocator = self._slab.allocator();
|
||||||
|
|
||||||
if (@hasDecl(S, "deinit")) {
|
|
||||||
// And it has a deinit, we'll call it
|
|
||||||
switch (@typeInfo(@TypeOf(S.deinit)).@"fn".params.len) {
|
|
||||||
1 => value.deinit(),
|
|
||||||
2 => value.deinit(self._page),
|
|
||||||
else => @compileLog(@typeName(S) ++ " has an invalid deinit function"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allocator.destroy(value);
|
allocator.destroy(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn destroyChain(
|
fn destroyChain(
|
||||||
self: *Factory,
|
self: *Factory,
|
||||||
value: anytype,
|
value: anytype,
|
||||||
comptime first: bool,
|
|
||||||
old_size: usize,
|
old_size: usize,
|
||||||
old_align: std.mem.Alignment,
|
old_align: std.mem.Alignment,
|
||||||
) void {
|
) void {
|
||||||
@@ -415,42 +411,20 @@ fn destroyChain(
|
|||||||
|
|
||||||
// aligns the old size to the alignment of this element
|
// aligns the old size to the alignment of this element
|
||||||
const current_size = std.mem.alignForward(usize, old_size, @alignOf(S));
|
const current_size = std.mem.alignForward(usize, old_size, @alignOf(S));
|
||||||
const alignment = std.mem.Alignment.fromByteUnits(@alignOf(S));
|
|
||||||
|
|
||||||
const new_align = std.mem.Alignment.max(old_align, alignment);
|
|
||||||
const new_size = current_size + @sizeOf(S);
|
const new_size = current_size + @sizeOf(S);
|
||||||
|
const new_align = std.mem.Alignment.max(old_align, std.mem.Alignment.of(S));
|
||||||
// This is initially called from a deinit. We don't want to call that
|
|
||||||
// same deinit. So when this is the first time destroyChain is called
|
|
||||||
// we don't call deinit (because we're in that deinit)
|
|
||||||
if (!comptime first) {
|
|
||||||
// But if it isn't the first time
|
|
||||||
if (@hasDecl(S, "deinit")) {
|
|
||||||
// And it has a deinit, we'll call it
|
|
||||||
switch (@typeInfo(@TypeOf(S.deinit)).@"fn".params.len) {
|
|
||||||
1 => value.deinit(),
|
|
||||||
2 => value.deinit(self._page),
|
|
||||||
else => @compileLog(@typeName(S) ++ " has an invalid deinit function"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (@hasField(S, "_proto")) {
|
if (@hasField(S, "_proto")) {
|
||||||
self.destroyChain(value._proto, false, new_size, new_align);
|
self.destroyChain(value._proto, new_size, new_align);
|
||||||
} else if (@hasDecl(S, "JsApi")) {
|
|
||||||
// Doesn't have a _proto, but has a JsApi.
|
|
||||||
if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| {
|
|
||||||
allocator.destroy(tagged);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// no proto so this is the head of the chain.
|
// no proto so this is the head of the chain.
|
||||||
// we use this as the ptr to the start of the chain.
|
// we use this as the ptr to the start of the chain.
|
||||||
// and we have summed up the length.
|
// and we have summed up the length.
|
||||||
assert(@hasDecl(S, "_prototype_root"));
|
assert(@hasDecl(S, "_prototype_root"));
|
||||||
|
|
||||||
const memory_ptr: [*]const u8 = @ptrCast(value);
|
const memory_ptr: [*]u8 = @ptrCast(@constCast(value));
|
||||||
const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits());
|
const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits());
|
||||||
allocator.free(memory_ptr[0..len]);
|
allocator.rawFree(memory_ptr[0..len], new_align, @returnAddress());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1494
src/browser/HttpClient.zig
Normal file
1494
src/browser/HttpClient.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -24,10 +24,12 @@ params: []const u8 = "",
|
|||||||
// IANA defines max. charset value length as 40.
|
// IANA defines max. charset value length as 40.
|
||||||
// We keep 41 for null-termination since HTML parser expects in this format.
|
// We keep 41 for null-termination since HTML parser expects in this format.
|
||||||
charset: [41]u8 = default_charset,
|
charset: [41]u8 = default_charset,
|
||||||
charset_len: usize = 5,
|
charset_len: usize = default_charset_len,
|
||||||
|
is_default_charset: bool = true,
|
||||||
|
|
||||||
/// String "UTF-8" continued by null characters.
|
/// String "UTF-8" continued by null characters.
|
||||||
pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
||||||
|
const default_charset_len = 5;
|
||||||
|
|
||||||
/// Mime with unknown Content-Type, empty params and empty charset.
|
/// Mime with unknown Content-Type, empty params and empty charset.
|
||||||
pub const unknown = Mime{ .content_type = .{ .unknown = {} } };
|
pub const unknown = Mime{ .content_type = .{ .unknown = {} } };
|
||||||
@@ -38,6 +40,10 @@ pub const ContentTypeEnum = enum {
|
|||||||
text_javascript,
|
text_javascript,
|
||||||
text_plain,
|
text_plain,
|
||||||
text_css,
|
text_css,
|
||||||
|
image_jpeg,
|
||||||
|
image_gif,
|
||||||
|
image_png,
|
||||||
|
image_webp,
|
||||||
application_json,
|
application_json,
|
||||||
unknown,
|
unknown,
|
||||||
other,
|
other,
|
||||||
@@ -49,6 +55,10 @@ pub const ContentType = union(ContentTypeEnum) {
|
|||||||
text_javascript: void,
|
text_javascript: void,
|
||||||
text_plain: void,
|
text_plain: void,
|
||||||
text_css: void,
|
text_css: void,
|
||||||
|
image_jpeg: void,
|
||||||
|
image_gif: void,
|
||||||
|
image_png: void,
|
||||||
|
image_webp: void,
|
||||||
application_json: void,
|
application_json: void,
|
||||||
unknown: void,
|
unknown: void,
|
||||||
other: struct { type: []const u8, sub_type: []const u8 },
|
other: struct { type: []const u8, sub_type: []const u8 },
|
||||||
@@ -61,6 +71,10 @@ pub fn contentTypeString(mime: *const Mime) []const u8 {
|
|||||||
.text_javascript => "application/javascript",
|
.text_javascript => "application/javascript",
|
||||||
.text_plain => "text/plain",
|
.text_plain => "text/plain",
|
||||||
.text_css => "text/css",
|
.text_css => "text/css",
|
||||||
|
.image_jpeg => "image/jpeg",
|
||||||
|
.image_png => "image/png",
|
||||||
|
.image_gif => "image/gif",
|
||||||
|
.image_webp => "image/webp",
|
||||||
.application_json => "application/json",
|
.application_json => "application/json",
|
||||||
else => "",
|
else => "",
|
||||||
};
|
};
|
||||||
@@ -115,17 +129,18 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
|
|
||||||
const params = trimLeft(normalized[type_len..]);
|
const params = trimLeft(normalized[type_len..]);
|
||||||
|
|
||||||
var charset: [41]u8 = undefined;
|
var charset: [41]u8 = default_charset;
|
||||||
var charset_len: usize = undefined;
|
var charset_len: usize = default_charset_len;
|
||||||
|
var has_explicit_charset = false;
|
||||||
|
|
||||||
var it = std.mem.splitScalar(u8, params, ';');
|
var it = std.mem.splitScalar(u8, params, ';');
|
||||||
while (it.next()) |attr| {
|
while (it.next()) |attr| {
|
||||||
const i = std.mem.indexOfScalarPos(u8, attr, 0, '=') orelse return error.Invalid;
|
const i = std.mem.indexOfScalarPos(u8, attr, 0, '=') orelse continue;
|
||||||
const name = trimLeft(attr[0..i]);
|
const name = trimLeft(attr[0..i]);
|
||||||
|
|
||||||
const value = trimRight(attr[i + 1 ..]);
|
const value = trimRight(attr[i + 1 ..]);
|
||||||
if (value.len == 0) {
|
if (value.len == 0) {
|
||||||
return error.Invalid;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attribute_name = std.meta.stringToEnum(enum {
|
const attribute_name = std.meta.stringToEnum(enum {
|
||||||
@@ -138,11 +153,12 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attribute_value = try parseCharset(value);
|
const attribute_value = parseCharset(value) catch continue;
|
||||||
@memcpy(charset[0..attribute_value.len], attribute_value);
|
@memcpy(charset[0..attribute_value.len], attribute_value);
|
||||||
// Null-terminate right after attribute value.
|
// Null-terminate right after attribute value.
|
||||||
charset[attribute_value.len] = 0;
|
charset[attribute_value.len] = 0;
|
||||||
charset_len = attribute_value.len;
|
charset_len = attribute_value.len;
|
||||||
|
has_explicit_charset = true;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,9 +168,137 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
.charset = charset,
|
.charset = charset,
|
||||||
.charset_len = charset_len,
|
.charset_len = charset_len,
|
||||||
.content_type = content_type,
|
.content_type = content_type,
|
||||||
|
.is_default_charset = !has_explicit_charset,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prescan the first 1024 bytes of an HTML document for a charset declaration.
|
||||||
|
/// Looks for `<meta charset="X">` and `<meta http-equiv="Content-Type" content="...;charset=X">`.
|
||||||
|
/// Returns the charset value or null if none found.
|
||||||
|
/// See: https://www.w3.org/International/questions/qa-html-encoding-declarations
|
||||||
|
pub fn prescanCharset(html: []const u8) ?[]const u8 {
|
||||||
|
const limit = @min(html.len, 1024);
|
||||||
|
const data = html[0..limit];
|
||||||
|
|
||||||
|
// Scan for <meta tags
|
||||||
|
var pos: usize = 0;
|
||||||
|
while (pos < data.len) {
|
||||||
|
// Find next '<'
|
||||||
|
pos = std.mem.indexOfScalarPos(u8, data, pos, '<') orelse return null;
|
||||||
|
pos += 1;
|
||||||
|
if (pos >= data.len) return null;
|
||||||
|
|
||||||
|
// Check for "meta" (case-insensitive)
|
||||||
|
if (pos + 4 >= data.len) return null;
|
||||||
|
var tag_buf: [4]u8 = undefined;
|
||||||
|
_ = std.ascii.lowerString(&tag_buf, data[pos..][0..4]);
|
||||||
|
if (!std.mem.eql(u8, &tag_buf, "meta")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pos += 4;
|
||||||
|
|
||||||
|
// Must be followed by whitespace or end of tag
|
||||||
|
if (pos >= data.len) return null;
|
||||||
|
if (data[pos] != ' ' and data[pos] != '\t' and data[pos] != '\n' and
|
||||||
|
data[pos] != '\r' and data[pos] != '/')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan attributes within this meta tag
|
||||||
|
const tag_end = std.mem.indexOfScalarPos(u8, data, pos, '>') orelse return null;
|
||||||
|
const attrs = data[pos..tag_end];
|
||||||
|
|
||||||
|
// Look for charset= attribute directly
|
||||||
|
if (findAttrValue(attrs, "charset")) |charset| {
|
||||||
|
if (charset.len > 0 and charset.len <= 40) return charset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for http-equiv="content-type" with content="...;charset=X"
|
||||||
|
if (findAttrValue(attrs, "http-equiv")) |he| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(he, "content-type")) {
|
||||||
|
if (findAttrValue(attrs, "content")) |content| {
|
||||||
|
if (extractCharsetFromContentType(content)) |charset| {
|
||||||
|
return charset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = tag_end + 1;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findAttrValue(attrs: []const u8, name: []const u8) ?[]const u8 {
|
||||||
|
var pos: usize = 0;
|
||||||
|
while (pos < attrs.len) {
|
||||||
|
// Skip whitespace
|
||||||
|
while (pos < attrs.len and (attrs[pos] == ' ' or attrs[pos] == '\t' or
|
||||||
|
attrs[pos] == '\n' or attrs[pos] == '\r'))
|
||||||
|
{
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if (pos >= attrs.len) return null;
|
||||||
|
|
||||||
|
// Read attribute name
|
||||||
|
const attr_start = pos;
|
||||||
|
while (pos < attrs.len and attrs[pos] != '=' and attrs[pos] != ' ' and
|
||||||
|
attrs[pos] != '\t' and attrs[pos] != '>' and attrs[pos] != '/')
|
||||||
|
{
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
const attr_name = attrs[attr_start..pos];
|
||||||
|
|
||||||
|
// Skip whitespace around =
|
||||||
|
while (pos < attrs.len and (attrs[pos] == ' ' or attrs[pos] == '\t')) pos += 1;
|
||||||
|
if (pos >= attrs.len or attrs[pos] != '=') {
|
||||||
|
// No '=' found - skip this token. Advance at least one byte to avoid infinite loop.
|
||||||
|
if (pos == attr_start) pos += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pos += 1; // skip '='
|
||||||
|
while (pos < attrs.len and (attrs[pos] == ' ' or attrs[pos] == '\t')) pos += 1;
|
||||||
|
if (pos >= attrs.len) return null;
|
||||||
|
|
||||||
|
// Read attribute value
|
||||||
|
const value = blk: {
|
||||||
|
if (attrs[pos] == '"' or attrs[pos] == '\'') {
|
||||||
|
const quote = attrs[pos];
|
||||||
|
pos += 1;
|
||||||
|
const val_start = pos;
|
||||||
|
while (pos < attrs.len and attrs[pos] != quote) pos += 1;
|
||||||
|
const val = attrs[val_start..pos];
|
||||||
|
if (pos < attrs.len) pos += 1; // skip closing quote
|
||||||
|
break :blk val;
|
||||||
|
} else {
|
||||||
|
const val_start = pos;
|
||||||
|
while (pos < attrs.len and attrs[pos] != ' ' and attrs[pos] != '\t' and
|
||||||
|
attrs[pos] != '>' and attrs[pos] != '/')
|
||||||
|
{
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
break :blk attrs[val_start..pos];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(attr_name, name)) return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extractCharsetFromContentType(content: []const u8) ?[]const u8 {
|
||||||
|
var it = std.mem.splitScalar(u8, content, ';');
|
||||||
|
while (it.next()) |part| {
|
||||||
|
const trimmed = std.mem.trimLeft(u8, part, &.{ ' ', '\t' });
|
||||||
|
if (trimmed.len > 8 and std.ascii.eqlIgnoreCase(trimmed[0..8], "charset=")) {
|
||||||
|
const val = std.mem.trim(u8, trimmed[8..], &.{ ' ', '\t', '"', '\'' });
|
||||||
|
if (val.len > 0 and val.len <= 40) return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sniff(body: []const u8) ?Mime {
|
pub fn sniff(body: []const u8) ?Mime {
|
||||||
// 0x0C is form feed
|
// 0x0C is form feed
|
||||||
const content = std.mem.trimLeft(u8, body, &.{ ' ', '\t', '\n', '\r', 0x0C });
|
const content = std.mem.trimLeft(u8, body, &.{ ' ', '\t', '\n', '\r', 0x0C });
|
||||||
@@ -165,15 +309,30 @@ pub fn sniff(body: []const u8) ?Mime {
|
|||||||
if (content[0] != '<') {
|
if (content[0] != '<') {
|
||||||
if (std.mem.startsWith(u8, content, &.{ 0xEF, 0xBB, 0xBF })) {
|
if (std.mem.startsWith(u8, content, &.{ 0xEF, 0xBB, 0xBF })) {
|
||||||
// UTF-8 BOM
|
// UTF-8 BOM
|
||||||
return .{ .content_type = .{ .text_plain = {} } };
|
return .{
|
||||||
|
.content_type = .{ .text_plain = {} },
|
||||||
|
.charset = default_charset,
|
||||||
|
.charset_len = default_charset_len,
|
||||||
|
.is_default_charset = false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (std.mem.startsWith(u8, content, &.{ 0xFE, 0xFF })) {
|
if (std.mem.startsWith(u8, content, &.{ 0xFE, 0xFF })) {
|
||||||
// UTF-16 big-endian BOM
|
// UTF-16 big-endian BOM
|
||||||
return .{ .content_type = .{ .text_plain = {} } };
|
return .{
|
||||||
|
.content_type = .{ .text_plain = {} },
|
||||||
|
.charset = .{ 'U', 'T', 'F', '-', '1', '6', 'B', 'E' } ++ .{0} ** 33,
|
||||||
|
.charset_len = 8,
|
||||||
|
.is_default_charset = false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (std.mem.startsWith(u8, content, &.{ 0xFF, 0xFE })) {
|
if (std.mem.startsWith(u8, content, &.{ 0xFF, 0xFE })) {
|
||||||
// UTF-16 little-endian BOM
|
// UTF-16 little-endian BOM
|
||||||
return .{ .content_type = .{ .text_plain = {} } };
|
return .{
|
||||||
|
.content_type = .{ .text_plain = {} },
|
||||||
|
.charset = .{ 'U', 'T', 'F', '-', '1', '6', 'L', 'E' } ++ .{0} ** 33,
|
||||||
|
.charset_len = 8,
|
||||||
|
.is_default_charset = false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -227,6 +386,14 @@ pub fn isHTML(self: *const Mime) bool {
|
|||||||
return self.content_type == .text_html;
|
return self.content_type == .text_html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn isText(mime: *const Mime) bool {
|
||||||
|
return switch (mime.content_type) {
|
||||||
|
.text_xml, .text_html, .text_javascript, .text_plain, .text_css => true,
|
||||||
|
.application_json => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// we expect value to be lowercase
|
// we expect value to be lowercase
|
||||||
fn parseContentType(value: []const u8) !struct { ContentType, usize } {
|
fn parseContentType(value: []const u8) !struct { ContentType, usize } {
|
||||||
const end = std.mem.indexOfScalarPos(u8, value, 0, ';') orelse value.len;
|
const end = std.mem.indexOfScalarPos(u8, value, 0, ';') orelse value.len;
|
||||||
@@ -243,6 +410,11 @@ fn parseContentType(value: []const u8) !struct { ContentType, usize } {
|
|||||||
@"application/javascript",
|
@"application/javascript",
|
||||||
@"application/x-javascript",
|
@"application/x-javascript",
|
||||||
|
|
||||||
|
@"image/jpeg",
|
||||||
|
@"image/png",
|
||||||
|
@"image/gif",
|
||||||
|
@"image/webp",
|
||||||
|
|
||||||
@"application/json",
|
@"application/json",
|
||||||
}, type_name)) |known_type| {
|
}, type_name)) |known_type| {
|
||||||
const ct: ContentType = switch (known_type) {
|
const ct: ContentType = switch (known_type) {
|
||||||
@@ -251,6 +423,10 @@ fn parseContentType(value: []const u8) !struct { ContentType, usize } {
|
|||||||
.@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} },
|
.@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} },
|
||||||
.@"text/plain" => .{ .text_plain = {} },
|
.@"text/plain" => .{ .text_plain = {} },
|
||||||
.@"text/css" => .{ .text_css = {} },
|
.@"text/css" => .{ .text_css = {} },
|
||||||
|
.@"image/jpeg" => .{ .image_jpeg = {} },
|
||||||
|
.@"image/png" => .{ .image_png = {} },
|
||||||
|
.@"image/gif" => .{ .image_gif = {} },
|
||||||
|
.@"image/webp" => .{ .image_webp = {} },
|
||||||
.@"application/json" => .{ .application_json = {} },
|
.@"application/json" => .{ .application_json = {} },
|
||||||
};
|
};
|
||||||
return .{ ct, attribute_start };
|
return .{ ct, attribute_start };
|
||||||
@@ -313,6 +489,19 @@ test "Mime: invalid" {
|
|||||||
"text/ html",
|
"text/ html",
|
||||||
"text / html",
|
"text / html",
|
||||||
"text/html other",
|
"text/html other",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (invalids) |invalid| {
|
||||||
|
const mutable_input = try testing.arena_allocator.dupe(u8, invalid);
|
||||||
|
try testing.expectError(error.Invalid, Mime.parse(mutable_input));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Mime: malformed parameters are ignored" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
// These should all parse successfully as text/html with malformed params ignored
|
||||||
|
const valid_with_malformed_params = [_][]const u8{
|
||||||
"text/html; x",
|
"text/html; x",
|
||||||
"text/html; x=",
|
"text/html; x=",
|
||||||
"text/html; x= ",
|
"text/html; x= ",
|
||||||
@@ -321,11 +510,13 @@ test "Mime: invalid" {
|
|||||||
"text/html; charset=\"\"",
|
"text/html; charset=\"\"",
|
||||||
"text/html; charset=\"",
|
"text/html; charset=\"",
|
||||||
"text/html; charset=\"\\",
|
"text/html; charset=\"\\",
|
||||||
|
"text/html;\"",
|
||||||
};
|
};
|
||||||
|
|
||||||
for (invalids) |invalid| {
|
for (valid_with_malformed_params) |input| {
|
||||||
const mutable_input = try testing.arena_allocator.dupe(u8, invalid);
|
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||||
try testing.expectError(error.Invalid, Mime.parse(mutable_input));
|
const mime = try Mime.parse(mutable_input);
|
||||||
|
try testing.expectEqual(.text_html, std.meta.activeTag(mime.content_type));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +549,11 @@ test "Mime: parse common" {
|
|||||||
|
|
||||||
try expect(.{ .content_type = .{ .application_json = {} } }, "application/json");
|
try expect(.{ .content_type = .{ .application_json = {} } }, "application/json");
|
||||||
try expect(.{ .content_type = .{ .text_css = {} } }, "text/css");
|
try expect(.{ .content_type = .{ .text_css = {} } }, "text/css");
|
||||||
|
|
||||||
|
try expect(.{ .content_type = .{ .image_jpeg = {} } }, "image/jpeg");
|
||||||
|
try expect(.{ .content_type = .{ .image_png = {} } }, "image/png");
|
||||||
|
try expect(.{ .content_type = .{ .image_gif = {} } }, "image/gif");
|
||||||
|
try expect(.{ .content_type = .{ .image_webp = {} } }, "image/webp");
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Mime: parse uncommon" {
|
test "Mime: parse uncommon" {
|
||||||
@@ -409,6 +605,12 @@ test "Mime: parse charset" {
|
|||||||
.charset = "custom-non-standard-charset-value",
|
.charset = "custom-non-standard-charset-value",
|
||||||
.params = "charset=\"custom-non-standard-charset-value\"",
|
.params = "charset=\"custom-non-standard-charset-value\"",
|
||||||
}, "text/xml;charset=\"custom-non-standard-charset-value\"");
|
}, "text/xml;charset=\"custom-non-standard-charset-value\"");
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_html = {} },
|
||||||
|
.charset = "UTF-8",
|
||||||
|
.params = "x=\"",
|
||||||
|
}, "text/html;x=\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Mime: isHTML" {
|
test "Mime: isHTML" {
|
||||||
@@ -492,6 +694,24 @@ test "Mime: sniff" {
|
|||||||
|
|
||||||
try expectHTML("<!-->");
|
try expectHTML("<!-->");
|
||||||
try expectHTML(" \n\t <!-->");
|
try expectHTML(" \n\t <!-->");
|
||||||
|
|
||||||
|
{
|
||||||
|
const mime = Mime.sniff(&.{ 0xEF, 0xBB, 0xBF }).?;
|
||||||
|
try testing.expectEqual(.text_plain, std.meta.activeTag(mime.content_type));
|
||||||
|
try testing.expectEqual("UTF-8", mime.charsetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const mime = Mime.sniff(&.{ 0xFE, 0xFF }).?;
|
||||||
|
try testing.expectEqual(.text_plain, std.meta.activeTag(mime.content_type));
|
||||||
|
try testing.expectEqual("UTF-16BE", mime.charsetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const mime = Mime.sniff(&.{ 0xFF, 0xFE }).?;
|
||||||
|
try testing.expectEqual(.text_plain, std.meta.activeTag(mime.content_type));
|
||||||
|
try testing.expectEqual("UTF-16LE", mime.charsetString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Expectation = struct {
|
const Expectation = struct {
|
||||||
@@ -528,3 +748,35 @@ fn expect(expected: Expectation, input: []const u8) !void {
|
|||||||
try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ());
|
try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Mime: prescanCharset" {
|
||||||
|
// <meta charset="X">
|
||||||
|
try testing.expectEqual("utf-8", Mime.prescanCharset("<html><head><meta charset=\"utf-8\">").?);
|
||||||
|
try testing.expectEqual("iso-8859-1", Mime.prescanCharset("<html><head><meta charset=\"iso-8859-1\">").?);
|
||||||
|
try testing.expectEqual("shift_jis", Mime.prescanCharset("<meta charset='shift_jis'>").?);
|
||||||
|
|
||||||
|
// Case-insensitive tag matching
|
||||||
|
try testing.expectEqual("utf-8", Mime.prescanCharset("<META charset=\"utf-8\">").?);
|
||||||
|
try testing.expectEqual("utf-8", Mime.prescanCharset("<Meta charset=\"utf-8\">").?);
|
||||||
|
|
||||||
|
// <meta http-equiv="Content-Type" content="text/html; charset=X">
|
||||||
|
try testing.expectEqual(
|
||||||
|
"iso-8859-1",
|
||||||
|
Mime.prescanCharset("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\">").?,
|
||||||
|
);
|
||||||
|
|
||||||
|
// No charset found
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset("<html><head><title>Test</title>"));
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset(""));
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset("no html here"));
|
||||||
|
|
||||||
|
// Self-closing meta without charset must not loop forever
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset("<meta foo=\"bar\"/>"));
|
||||||
|
|
||||||
|
// Charset after 1024 bytes should not be found
|
||||||
|
var long_html: [1100]u8 = undefined;
|
||||||
|
@memset(&long_html, ' ');
|
||||||
|
const suffix = "<meta charset=\"windows-1252\">";
|
||||||
|
@memcpy(long_html[1050 .. 1050 + suffix.len], suffix);
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset(&long_html));
|
||||||
|
}
|
||||||
|
|||||||
3139
src/browser/Page.zig
3139
src/browser/Page.zig
File diff suppressed because it is too large
Load Diff
238
src/browser/Runner.zig
Normal file
238
src/browser/Runner.zig
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const lp = @import("lightpanda");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const Session = @import("Session.zig");
|
||||||
|
const HttpClient = @import("HttpClient.zig");
|
||||||
|
|
||||||
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
|
const Runner = @This();
|
||||||
|
|
||||||
|
page: *Page,
|
||||||
|
session: *Session,
|
||||||
|
http_client: *HttpClient,
|
||||||
|
|
||||||
|
pub const Opts = struct {};
|
||||||
|
|
||||||
|
pub fn init(session: *Session, _: Opts) !Runner {
|
||||||
|
const page = &(session.page orelse return error.NoPage);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.page = page,
|
||||||
|
.session = session,
|
||||||
|
.http_client = session.browser.http_client,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const WaitOpts = struct {
|
||||||
|
ms: u32,
|
||||||
|
until: lp.Config.WaitUntil = .done,
|
||||||
|
};
|
||||||
|
pub fn wait(self: *Runner, opts: WaitOpts) !void {
|
||||||
|
_ = try self._wait(false, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CDPWaitResult = enum {
|
||||||
|
done,
|
||||||
|
cdp_socket,
|
||||||
|
};
|
||||||
|
pub fn waitCDP(self: *Runner, opts: WaitOpts) !CDPWaitResult {
|
||||||
|
return self._wait(true, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _wait(self: *Runner, comptime is_cdp: bool, opts: WaitOpts) !CDPWaitResult {
|
||||||
|
var timer = try std.time.Timer.start();
|
||||||
|
var ms_remaining = opts.ms;
|
||||||
|
|
||||||
|
const tick_opts = TickOpts{
|
||||||
|
.ms = 200,
|
||||||
|
.until = opts.until,
|
||||||
|
};
|
||||||
|
while (true) {
|
||||||
|
const tick_result = self._tick(is_cdp, tick_opts) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.JsError => {}, // already logged (with hopefully more context)
|
||||||
|
else => log.err(.browser, "session wait", .{
|
||||||
|
.err = err,
|
||||||
|
.url = self.page.url,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
const next_ms = switch (tick_result) {
|
||||||
|
.ok => |next_ms| next_ms,
|
||||||
|
.done => return .done,
|
||||||
|
.cdp_socket => if (comptime is_cdp) return .cdp_socket else unreachable,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ms_elapsed = timer.lap() / 1_000_000;
|
||||||
|
if (ms_elapsed >= ms_remaining) {
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
ms_remaining -= @intCast(ms_elapsed);
|
||||||
|
if (next_ms > 0) {
|
||||||
|
std.Thread.sleep(std.time.ns_per_ms * next_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TickOpts = struct {
|
||||||
|
ms: u32,
|
||||||
|
until: lp.Config.WaitUntil = .done,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TickResult = union(enum) {
|
||||||
|
done,
|
||||||
|
ok: u32,
|
||||||
|
};
|
||||||
|
pub fn tick(self: *Runner, opts: TickOpts) !TickResult {
|
||||||
|
return switch (try self._tick(false, opts)) {
|
||||||
|
.ok => |ms| .{ .ok = ms },
|
||||||
|
.done => .done,
|
||||||
|
.cdp_socket => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CDPTickResult = union(enum) {
|
||||||
|
done,
|
||||||
|
cdp_socket,
|
||||||
|
ok: u32,
|
||||||
|
};
|
||||||
|
pub fn tickCDP(self: *Runner, opts: TickOpts) !CDPTickResult {
|
||||||
|
return self._tick(true, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
|
||||||
|
const page = self.page;
|
||||||
|
const http_client = self.http_client;
|
||||||
|
|
||||||
|
switch (page._parse_state) {
|
||||||
|
.pre, .raw, .text, .image => {
|
||||||
|
// The main page hasn't started/finished navigating.
|
||||||
|
// There's no JS to run, and no reason to run the scheduler.
|
||||||
|
if (http_client.active == 0 and (comptime is_cdp) == false) {
|
||||||
|
// haven't started navigating, I guess.
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either we have active http connections, or we're in CDP
|
||||||
|
// mode with an extra socket. Either way, we're waiting
|
||||||
|
// for http traffic
|
||||||
|
const http_result = try http_client.tick(@intCast(opts.ms));
|
||||||
|
if ((comptime is_cdp) and http_result == .cdp_socket) {
|
||||||
|
return .cdp_socket;
|
||||||
|
}
|
||||||
|
return .{ .ok = 0 };
|
||||||
|
},
|
||||||
|
.html, .complete => {
|
||||||
|
const session = self.session;
|
||||||
|
if (session.queued_navigation.items.len != 0) {
|
||||||
|
try session.processQueuedNavigation();
|
||||||
|
self.page = &session.page.?; // might have changed
|
||||||
|
return .{ .ok = 0 };
|
||||||
|
}
|
||||||
|
const browser = session.browser;
|
||||||
|
|
||||||
|
// The HTML page was parsed. We now either have JS scripts to
|
||||||
|
// download, or scheduled tasks to execute, or both.
|
||||||
|
|
||||||
|
// scheduler.run could trigger new http transfers, so do not
|
||||||
|
// store http_client.active BEFORE this call and then use
|
||||||
|
// it AFTER.
|
||||||
|
try browser.runMacrotasks();
|
||||||
|
|
||||||
|
// Each call to this runs scheduled load events.
|
||||||
|
try page.dispatchLoad();
|
||||||
|
|
||||||
|
const http_active = http_client.active;
|
||||||
|
const total_network_activity = http_active + http_client.intercepted;
|
||||||
|
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
||||||
|
page.notifyNetworkAlmostIdle();
|
||||||
|
}
|
||||||
|
if (page._notified_network_idle.check(total_network_activity == 0)) {
|
||||||
|
page.notifyNetworkIdle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (http_active == 0 and (comptime is_cdp == false)) {
|
||||||
|
// we don't need to consider http_client.intercepted here
|
||||||
|
// because is_cdp is true, and that can only be
|
||||||
|
// the case when interception isn't possible.
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(http_client.intercepted == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browser.hasBackgroundTasks()) {
|
||||||
|
// _we_ have nothing to run, but v8 is working on
|
||||||
|
// background tasks. We'll wait for them.
|
||||||
|
browser.waitForBackgroundTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (opts.until) {
|
||||||
|
.done => {},
|
||||||
|
.domcontentloaded => if (page._load_state == .load or page._load_state == .complete) {
|
||||||
|
return .done;
|
||||||
|
},
|
||||||
|
.load => if (page._load_state == .complete) {
|
||||||
|
return .done;
|
||||||
|
},
|
||||||
|
.networkidle => if (page._notified_network_idle == .done) {
|
||||||
|
return .done;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// We never advertise a wait time of more than 20, there can
|
||||||
|
// always be new background tasks to run.
|
||||||
|
if (browser.msToNextMacrotask()) |ms_to_next_task| {
|
||||||
|
return .{ .ok = @min(ms_to_next_task, 20) };
|
||||||
|
}
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're here because we either have active HTTP
|
||||||
|
// connections, or is_cdp == false (aka, there's
|
||||||
|
// an cdp_socket registered with the http client).
|
||||||
|
// We should continue to run tasks, so we minimize how long
|
||||||
|
// we'll poll for network I/O.
|
||||||
|
var ms_to_wait = @min(opts.ms, browser.msToNextMacrotask() orelse 200);
|
||||||
|
if (ms_to_wait > 10 and browser.hasBackgroundTasks()) {
|
||||||
|
// if we have background tasks, we don't want to wait too
|
||||||
|
// long for a message from the client. We want to go back
|
||||||
|
// to the top of the loop and run macrotasks.
|
||||||
|
ms_to_wait = 10;
|
||||||
|
}
|
||||||
|
const http_result = try http_client.tick(@intCast(@min(opts.ms, ms_to_wait)));
|
||||||
|
if ((comptime is_cdp) and http_result == .cdp_socket) {
|
||||||
|
return .cdp_socket;
|
||||||
|
}
|
||||||
|
return .{ .ok = 0 };
|
||||||
|
},
|
||||||
|
.err => |err| {
|
||||||
|
page._parse_state = .{ .raw_done = @errorName(err) };
|
||||||
|
return err;
|
||||||
|
},
|
||||||
|
.raw_done => return .done,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,20 +17,21 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const lp = @import("lightpanda");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const js = @import("js/js.zig");
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
|
const HttpClient = @import("HttpClient.zig");
|
||||||
|
const net_http = @import("../network/http.zig");
|
||||||
|
const String = @import("../string.zig").String;
|
||||||
|
|
||||||
|
const js = @import("js/js.zig");
|
||||||
const URL = @import("URL.zig");
|
const URL = @import("URL.zig");
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const Browser = @import("Browser.zig");
|
|
||||||
const Http = @import("../http/Http.zig");
|
|
||||||
|
|
||||||
const Element = @import("webapi/Element.zig");
|
const Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArrayListUnmanaged = std.ArrayListUnmanaged;
|
|
||||||
|
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
@@ -58,11 +59,8 @@ ready_scripts: std.DoublyLinkedList,
|
|||||||
|
|
||||||
shutdown: bool = false,
|
shutdown: bool = false,
|
||||||
|
|
||||||
client: *Http.Client,
|
client: *HttpClient,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
buffer_pool: BufferPool,
|
|
||||||
|
|
||||||
script_pool: std.heap.MemoryPool(Script),
|
|
||||||
|
|
||||||
// We can download multiple sync modules in parallel, but we want to process
|
// We can download multiple sync modules in parallel, but we want to process
|
||||||
// them in order. We can't use an std.DoublyLinkedList, like the other script types,
|
// them in order. We can't use an std.DoublyLinkedList, like the other script types,
|
||||||
@@ -82,10 +80,11 @@ imported_modules: std.StringHashMapUnmanaged(ImportedModule),
|
|||||||
// importmap contains resolved urls.
|
// importmap contains resolved urls.
|
||||||
importmap: std.StringHashMapUnmanaged([:0]const u8),
|
importmap: std.StringHashMapUnmanaged([:0]const u8),
|
||||||
|
|
||||||
pub fn init(page: *Page) ScriptManager {
|
// have we notified the page that all scripts are loaded (used to fire the "load"
|
||||||
// page isn't fully initialized, we can setup our reference, but that's it.
|
// event).
|
||||||
const browser = page._session.browser;
|
page_notified_of_completion: bool,
|
||||||
const allocator = browser.allocator;
|
|
||||||
|
pub fn init(allocator: Allocator, http_client: *HttpClient, page: *Page) ScriptManager {
|
||||||
return .{
|
return .{
|
||||||
.page = page,
|
.page = page,
|
||||||
.async_scripts = .{},
|
.async_scripts = .{},
|
||||||
@@ -95,19 +94,16 @@ pub fn init(page: *Page) ScriptManager {
|
|||||||
.is_evaluating = false,
|
.is_evaluating = false,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.imported_modules = .empty,
|
.imported_modules = .empty,
|
||||||
.client = browser.http_client,
|
.client = http_client,
|
||||||
.static_scripts_done = false,
|
.static_scripts_done = false,
|
||||||
.buffer_pool = BufferPool.init(allocator, 5),
|
.page_notified_of_completion = false,
|
||||||
.script_pool = std.heap.MemoryPool(Script).init(allocator),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *ScriptManager) void {
|
pub fn deinit(self: *ScriptManager) void {
|
||||||
// necessary to free any buffers scripts may be referencing
|
// necessary to free any arenas scripts may be referencing
|
||||||
self.reset();
|
self.reset();
|
||||||
|
|
||||||
self.buffer_pool.deinit();
|
|
||||||
self.script_pool.deinit();
|
|
||||||
self.imported_modules.deinit(self.allocator);
|
self.imported_modules.deinit(self.allocator);
|
||||||
// we don't deinit self.importmap b/c we use the page's arena for its
|
// we don't deinit self.importmap b/c we use the page's arena for its
|
||||||
// allocations.
|
// allocations.
|
||||||
@@ -116,7 +112,10 @@ pub fn deinit(self: *ScriptManager) void {
|
|||||||
pub fn reset(self: *ScriptManager) void {
|
pub fn reset(self: *ScriptManager) void {
|
||||||
var it = self.imported_modules.valueIterator();
|
var it = self.imported_modules.valueIterator();
|
||||||
while (it.next()) |value_ptr| {
|
while (it.next()) |value_ptr| {
|
||||||
self.buffer_pool.release(value_ptr.buffer);
|
switch (value_ptr.state) {
|
||||||
|
.done => |script| script.deinit(),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.imported_modules.clearRetainingCapacity();
|
self.imported_modules.clearRetainingCapacity();
|
||||||
|
|
||||||
@@ -133,10 +132,16 @@ pub fn reset(self: *ScriptManager) void {
|
|||||||
fn clearList(list: *std.DoublyLinkedList) void {
|
fn clearList(list: *std.DoublyLinkedList) void {
|
||||||
while (list.popFirst()) |n| {
|
while (list.popFirst()) |n| {
|
||||||
const script: *Script = @fieldParentPtr("node", n);
|
const script: *Script = @fieldParentPtr("node", n);
|
||||||
script.deinit(true);
|
script.deinit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn getHeaders(self: *ScriptManager) !net_http.Headers {
|
||||||
|
var headers = try self.client.newHeaders();
|
||||||
|
try self.page.headersForRequest(&headers);
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_element: *Element.Html.Script, comptime ctx: []const u8) !void {
|
pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_element: *Element.Html.Script, comptime ctx: []const u8) !void {
|
||||||
if (script_element._executed) {
|
if (script_element._executed) {
|
||||||
// If a script tag gets dynamically created and added to the dom:
|
// If a script tag gets dynamically created and added to the dom:
|
||||||
@@ -148,17 +153,16 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
// <script> has already been processed.
|
// <script> has already been processed.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
script_element._executed = true;
|
|
||||||
|
|
||||||
const element = script_element.asElement();
|
const element = script_element.asElement();
|
||||||
if (element.getAttributeSafe("nomodule") != null) {
|
if (element.getAttributeSafe(comptime .wrap("nomodule")) != null) {
|
||||||
// these scripts should only be loaded if we don't support modules
|
// these scripts should only be loaded if we don't support modules
|
||||||
// but since we do support modules, we can just skip them.
|
// but since we do support modules, we can just skip them.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const kind: Script.Kind = blk: {
|
const kind: Script.Kind = blk: {
|
||||||
const script_type = element.getAttributeSafe("type") orelse break :blk .javascript;
|
const script_type = element.getAttributeSafe(comptime .wrap("type")) orelse break :blk .javascript;
|
||||||
if (script_type.len == 0) {
|
if (script_type.len == 0) {
|
||||||
break :blk .javascript;
|
break :blk .javascript;
|
||||||
}
|
}
|
||||||
@@ -181,30 +185,48 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var handover = false;
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
|
|
||||||
|
const arena = try page.getArena(.{ .debug = "addFromElement" });
|
||||||
|
errdefer if (!handover) {
|
||||||
|
page.releaseArena(arena);
|
||||||
|
};
|
||||||
|
|
||||||
var source: Script.Source = undefined;
|
var source: Script.Source = undefined;
|
||||||
var remote_url: ?[:0]const u8 = null;
|
var remote_url: ?[:0]const u8 = null;
|
||||||
const base_url = page.base();
|
const base_url = page.base();
|
||||||
if (element.getAttributeSafe("src")) |src| {
|
if (element.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||||
if (try parseDataURI(page.arena, src)) |data_uri| {
|
if (try parseDataURI(arena, src)) |data_uri| {
|
||||||
source = .{ .@"inline" = data_uri };
|
source = .{ .@"inline" = data_uri };
|
||||||
} else {
|
} else {
|
||||||
remote_url = try URL.resolve(page.arena, base_url, src, .{});
|
remote_url = try URL.resolve(arena, base_url, src, .{});
|
||||||
source = .{ .remote = .{} };
|
source = .{ .remote = .{} };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const inline_source = try element.asNode().getTextContentAlloc(page.arena);
|
var buf = std.Io.Writer.Allocating.init(arena);
|
||||||
|
try element.asNode().getChildTextContent(&buf.writer);
|
||||||
|
try buf.writer.writeByte(0);
|
||||||
|
const data = buf.written();
|
||||||
|
const inline_source: [:0]const u8 = data[0 .. data.len - 1 :0];
|
||||||
|
if (inline_source.len == 0) {
|
||||||
|
// we haven't set script_element._executed = true yet, which is good.
|
||||||
|
// If content is appended to the script, we will execute it then.
|
||||||
|
page.releaseArena(arena);
|
||||||
|
return;
|
||||||
|
}
|
||||||
source = .{ .@"inline" = inline_source };
|
source = .{ .@"inline" = inline_source };
|
||||||
}
|
}
|
||||||
|
|
||||||
const script = try self.script_pool.create();
|
// Only set _executed (already-started) when we actually have content to execute
|
||||||
errdefer self.script_pool.destroy(script);
|
script_element._executed = true;
|
||||||
|
|
||||||
const is_inline = source == .@"inline";
|
const is_inline = source == .@"inline";
|
||||||
|
|
||||||
|
const script = try arena.create(Script);
|
||||||
script.* = .{
|
script.* = .{
|
||||||
.kind = kind,
|
.kind = kind,
|
||||||
.node = .{},
|
.node = .{},
|
||||||
|
.arena = arena,
|
||||||
.manager = self,
|
.manager = self,
|
||||||
.source = source,
|
.source = source,
|
||||||
.script_element = script_element,
|
.script_element = script_element,
|
||||||
@@ -216,12 +238,12 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
break :blk if (kind == .module) .@"defer" else .normal;
|
break :blk if (kind == .module) .@"defer" else .normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.getAttributeSafe("async") != null) {
|
if (element.getAttributeSafe(comptime .wrap("async")) != null) {
|
||||||
break :blk .async;
|
break :blk .async;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for defer or module (before checking dynamic script default)
|
// Check for defer or module (before checking dynamic script default)
|
||||||
if (kind == .module or element.getAttributeSafe("defer") != null) {
|
if (kind == .module or element.getAttributeSafe(comptime .wrap("defer")) != null) {
|
||||||
break :blk .@"defer";
|
break :blk .@"defer";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,33 +270,38 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
if (is_blocking == false) {
|
if (is_blocking == false) {
|
||||||
self.scriptList(script).remove(&script.node);
|
self.scriptList(script).remove(&script.node);
|
||||||
}
|
}
|
||||||
script.deinit(true);
|
// Let the outer errdefer handle releasing the arena if client.request fails
|
||||||
}
|
}
|
||||||
|
|
||||||
var headers = try self.client.newHeaders();
|
|
||||||
try page.requestCookie(.{}).headersForRequest(page.arena, url, &headers);
|
|
||||||
|
|
||||||
try self.client.request(.{
|
try self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.headers = headers,
|
.frame_id = page._frame_id,
|
||||||
|
.headers = try self.getHeaders(),
|
||||||
.blocking = is_blocking,
|
.blocking = is_blocking,
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
|
.cookie_origin = page.url,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
|
.notification = page._session.notification,
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||||
.header_callback = Script.headerCallback,
|
.header_callback = Script.headerCallback,
|
||||||
.data_callback = Script.dataCallback,
|
.data_callback = Script.dataCallback,
|
||||||
.done_callback = Script.doneCallback,
|
.done_callback = Script.doneCallback,
|
||||||
.error_callback = Script.errorCallback,
|
.error_callback = Script.errorCallback,
|
||||||
});
|
});
|
||||||
|
handover = true;
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
log.debug(.http, "script queue", .{
|
||||||
.ctx = ctx,
|
.ctx = ctx,
|
||||||
.url = remote_url.?,
|
.url = remote_url.?,
|
||||||
.element = element,
|
.element = element,
|
||||||
.stack = page.js.stackTrace() catch "???",
|
.stack = ls.local.stackTrace() catch "???",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,7 +320,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
}
|
}
|
||||||
if (script.status == 0) {
|
if (script.status == 0) {
|
||||||
// an error (that we already logged)
|
// an error (that we already logged)
|
||||||
script.deinit(true);
|
script.deinit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +329,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
self.is_evaluating = true;
|
self.is_evaluating = true;
|
||||||
defer {
|
defer {
|
||||||
self.is_evaluating = was_evaluating;
|
self.is_evaluating = was_evaluating;
|
||||||
script.deinit(true);
|
script.deinit();
|
||||||
}
|
}
|
||||||
return script.eval(page);
|
return script.eval(page);
|
||||||
}
|
}
|
||||||
@@ -334,11 +361,14 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
}
|
}
|
||||||
errdefer _ = self.imported_modules.remove(url);
|
errdefer _ = self.imported_modules.remove(url);
|
||||||
|
|
||||||
const script = try self.script_pool.create();
|
const page = self.page;
|
||||||
errdefer self.script_pool.destroy(script);
|
const arena = try page.getArena(.{ .debug = "preloadImport" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
|
||||||
|
const script = try arena.create(Script);
|
||||||
script.* = .{
|
script.* = .{
|
||||||
.kind = .module,
|
.kind = .module,
|
||||||
|
.arena = arena,
|
||||||
.url = url,
|
.url = url,
|
||||||
.node = .{},
|
.node = .{},
|
||||||
.manager = self,
|
.manager = self,
|
||||||
@@ -348,41 +378,46 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
.mode = .import,
|
.mode = .import,
|
||||||
};
|
};
|
||||||
|
|
||||||
gop.value_ptr.* = ImportedModule{
|
gop.value_ptr.* = ImportedModule{};
|
||||||
.manager = self,
|
|
||||||
};
|
|
||||||
|
|
||||||
var headers = try self.client.newHeaders();
|
|
||||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
log.debug(.http, "script queue", .{
|
||||||
.url = url,
|
.url = url,
|
||||||
.ctx = "module",
|
.ctx = "module",
|
||||||
.referrer = referrer,
|
.referrer = referrer,
|
||||||
.stack = self.page.js.stackTrace() catch "???",
|
.stack = ls.local.stackTrace() catch "???",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.client.request(.{
|
|
||||||
.url = url,
|
|
||||||
.ctx = script,
|
|
||||||
.method = .GET,
|
|
||||||
.headers = headers,
|
|
||||||
.cookie_jar = &self.page._session.cookie_jar,
|
|
||||||
.resource_type = .script,
|
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
|
||||||
.header_callback = Script.headerCallback,
|
|
||||||
.data_callback = Script.dataCallback,
|
|
||||||
.done_callback = Script.doneCallback,
|
|
||||||
.error_callback = Script.errorCallback,
|
|
||||||
});
|
|
||||||
|
|
||||||
// This seems wrong since we're not dealing with an async import (unlike
|
// This seems wrong since we're not dealing with an async import (unlike
|
||||||
// getAsyncModule below), but all we're trying to do here is pre-load the
|
// getAsyncModule below), but all we're trying to do here is pre-load the
|
||||||
// script for execution at some point in the future (when waitForImport is
|
// script for execution at some point in the future (when waitForImport is
|
||||||
// called).
|
// called).
|
||||||
self.async_scripts.append(&script.node);
|
self.async_scripts.append(&script.node);
|
||||||
|
|
||||||
|
self.client.request(.{
|
||||||
|
.url = url,
|
||||||
|
.ctx = script,
|
||||||
|
.method = .GET,
|
||||||
|
.frame_id = page._frame_id,
|
||||||
|
.headers = try self.getHeaders(),
|
||||||
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
|
.cookie_origin = page.url,
|
||||||
|
.resource_type = .script,
|
||||||
|
.notification = page._session.notification,
|
||||||
|
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||||
|
.header_callback = Script.headerCallback,
|
||||||
|
.data_callback = Script.dataCallback,
|
||||||
|
.done_callback = Script.doneCallback,
|
||||||
|
.error_callback = Script.errorCallback,
|
||||||
|
}) catch |err| {
|
||||||
|
self.async_scripts.remove(&script.node);
|
||||||
|
return err;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
||||||
@@ -403,12 +438,12 @@ pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
|||||||
_ = try client.tick(200);
|
_ = try client.tick(200);
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
.done => {
|
.done => |script| {
|
||||||
var shared = false;
|
var shared = false;
|
||||||
const buffer = entry.value_ptr.buffer;
|
const buffer = entry.value_ptr.buffer;
|
||||||
const waiters = entry.value_ptr.waiters;
|
const waiters = entry.value_ptr.waiters;
|
||||||
|
|
||||||
if (waiters == 0) {
|
if (waiters == 1) {
|
||||||
self.imported_modules.removeByPtr(entry.key_ptr);
|
self.imported_modules.removeByPtr(entry.key_ptr);
|
||||||
} else {
|
} else {
|
||||||
shared = true;
|
shared = true;
|
||||||
@@ -417,7 +452,7 @@ pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
|||||||
return .{
|
return .{
|
||||||
.buffer = buffer,
|
.buffer = buffer,
|
||||||
.shared = shared,
|
.shared = shared,
|
||||||
.buffer_pool = &self.buffer_pool,
|
.script = script,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
.err => return error.Failed,
|
.err => return error.Failed,
|
||||||
@@ -426,11 +461,14 @@ pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.Callback, cb_data: *anyopaque, referrer: []const u8) !void {
|
pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.Callback, cb_data: *anyopaque, referrer: []const u8) !void {
|
||||||
const script = try self.script_pool.create();
|
const page = self.page;
|
||||||
errdefer self.script_pool.destroy(script);
|
const arena = try page.getArena(.{ .debug = "getAsyncImport" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
|
||||||
|
const script = try arena.create(Script);
|
||||||
script.* = .{
|
script.* = .{
|
||||||
.kind = .module,
|
.kind = .module,
|
||||||
|
.arena = arena,
|
||||||
.url = url,
|
.url = url,
|
||||||
.node = .{},
|
.node = .{},
|
||||||
.manager = self,
|
.manager = self,
|
||||||
@@ -443,15 +481,16 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
} },
|
} },
|
||||||
};
|
};
|
||||||
|
|
||||||
var headers = try self.client.newHeaders();
|
|
||||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
log.debug(.http, "script queue", .{
|
log.debug(.http, "script queue", .{
|
||||||
.url = url,
|
.url = url,
|
||||||
.ctx = "dynamic module",
|
.ctx = "dynamic module",
|
||||||
.referrer = referrer,
|
.referrer = referrer,
|
||||||
.stack = self.page.js.stackTrace() catch "???",
|
.stack = ls.local.stackTrace() catch "???",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,27 +503,32 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
self.is_evaluating = true;
|
self.is_evaluating = true;
|
||||||
defer self.is_evaluating = was_evaluating;
|
defer self.is_evaluating = was_evaluating;
|
||||||
|
|
||||||
try self.client.request(.{
|
self.async_scripts.append(&script.node);
|
||||||
|
self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.headers = headers,
|
.frame_id = page._frame_id,
|
||||||
|
.headers = try self.getHeaders(),
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
.cookie_jar = &self.page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
|
.cookie_origin = page.url,
|
||||||
|
.notification = page._session.notification,
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||||
.header_callback = Script.headerCallback,
|
.header_callback = Script.headerCallback,
|
||||||
.data_callback = Script.dataCallback,
|
.data_callback = Script.dataCallback,
|
||||||
.done_callback = Script.doneCallback,
|
.done_callback = Script.doneCallback,
|
||||||
.error_callback = Script.errorCallback,
|
.error_callback = Script.errorCallback,
|
||||||
});
|
}) catch |err| {
|
||||||
|
self.async_scripts.remove(&script.node);
|
||||||
self.async_scripts.append(&script.node);
|
return err;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called from the Page to let us know it's done parsing the HTML. Necessary that
|
// Called from the Page to let us know it's done parsing the HTML. Necessary that
|
||||||
// we know this so that we know that we can start evaluating deferred scripts.
|
// we know this so that we know that we can start evaluating deferred scripts.
|
||||||
pub fn staticScriptsDone(self: *ScriptManager) void {
|
pub fn staticScriptsDone(self: *ScriptManager) void {
|
||||||
std.debug.assert(self.static_scripts_done == false);
|
lp.assert(self.static_scripts_done == false, "ScriptManager.staticScriptsDone", .{});
|
||||||
self.static_scripts_done = true;
|
self.static_scripts_done = true;
|
||||||
self.evaluate();
|
self.evaluate();
|
||||||
}
|
}
|
||||||
@@ -503,18 +547,18 @@ fn evaluate(self: *ScriptManager) void {
|
|||||||
var script: *Script = @fieldParentPtr("node", n);
|
var script: *Script = @fieldParentPtr("node", n);
|
||||||
switch (script.mode) {
|
switch (script.mode) {
|
||||||
.async => {
|
.async => {
|
||||||
defer script.deinit(true);
|
defer script.deinit();
|
||||||
script.eval(page);
|
script.eval(page);
|
||||||
},
|
},
|
||||||
.import_async => |ia| {
|
.import_async => |ia| {
|
||||||
defer script.deinit(false);
|
|
||||||
if (script.status < 200 or script.status > 299) {
|
if (script.status < 200 or script.status > 299) {
|
||||||
|
script.deinit();
|
||||||
ia.callback(ia.data, error.FailedToLoad);
|
ia.callback(ia.data, error.FailedToLoad);
|
||||||
} else {
|
} else {
|
||||||
ia.callback(ia.data, .{
|
ia.callback(ia.data, .{
|
||||||
.shared = false,
|
.shared = false,
|
||||||
|
.script = script,
|
||||||
.buffer = script.source.remote,
|
.buffer = script.source.remote,
|
||||||
.buffer_pool = &self.buffer_pool,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -540,7 +584,7 @@ fn evaluate(self: *ScriptManager) void {
|
|||||||
}
|
}
|
||||||
defer {
|
defer {
|
||||||
_ = self.defer_scripts.popFirst();
|
_ = self.defer_scripts.popFirst();
|
||||||
script.deinit(true);
|
script.deinit();
|
||||||
}
|
}
|
||||||
script.eval(page);
|
script.eval(page);
|
||||||
}
|
}
|
||||||
@@ -554,19 +598,12 @@ fn evaluate(self: *ScriptManager) void {
|
|||||||
// Page makes this safe to call multiple times.
|
// Page makes this safe to call multiple times.
|
||||||
page.documentIsLoaded();
|
page.documentIsLoaded();
|
||||||
|
|
||||||
if (self.async_scripts.first == null) {
|
if (self.async_scripts.first == null and self.page_notified_of_completion == false) {
|
||||||
// Looks like all async scripts are done too!
|
self.page_notified_of_completion = true;
|
||||||
// Page makes this safe to call multiple times.
|
page.scriptsCompletedLoading();
|
||||||
page.documentIsComplete();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isDone(self: *const ScriptManager) bool {
|
|
||||||
return self.static_scripts_done and // page is done processing initial html
|
|
||||||
self.defer_scripts.first == null and // no deferred scripts
|
|
||||||
self.async_scripts.first == null; // no async scripts
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
||||||
const content = script.source.content();
|
const content = script.source.content();
|
||||||
|
|
||||||
@@ -598,16 +635,30 @@ fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const Script = struct {
|
pub const Script = struct {
|
||||||
complete: bool,
|
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
|
complete: bool,
|
||||||
status: u16 = 0,
|
status: u16 = 0,
|
||||||
source: Source,
|
source: Source,
|
||||||
url: []const u8,
|
url: []const u8,
|
||||||
|
arena: Allocator,
|
||||||
mode: ExecutionMode,
|
mode: ExecutionMode,
|
||||||
node: std.DoublyLinkedList.Node,
|
node: std.DoublyLinkedList.Node,
|
||||||
script_element: ?*Element.Html.Script,
|
script_element: ?*Element.Html.Script,
|
||||||
manager: *ScriptManager,
|
manager: *ScriptManager,
|
||||||
|
|
||||||
|
// for debugging a rare production issue
|
||||||
|
header_callback_called: bool = false,
|
||||||
|
|
||||||
|
// for debugging a rare production issue
|
||||||
|
debug_transfer_id: u32 = 0,
|
||||||
|
debug_transfer_tries: u8 = 0,
|
||||||
|
debug_transfer_aborted: bool = false,
|
||||||
|
debug_transfer_bytes_received: usize = 0,
|
||||||
|
debug_transfer_notified_fail: bool = false,
|
||||||
|
debug_transfer_intercept_state: u8 = 0,
|
||||||
|
debug_transfer_auth_challenge: bool = false,
|
||||||
|
debug_transfer_easy_id: usize = 0,
|
||||||
|
|
||||||
const Kind = enum {
|
const Kind = enum {
|
||||||
module,
|
module,
|
||||||
javascript,
|
javascript,
|
||||||
@@ -621,7 +672,7 @@ pub const Script = struct {
|
|||||||
|
|
||||||
const Source = union(enum) {
|
const Source = union(enum) {
|
||||||
@"inline": []const u8,
|
@"inline": []const u8,
|
||||||
remote: std.ArrayListUnmanaged(u8),
|
remote: std.ArrayList(u8),
|
||||||
|
|
||||||
fn content(self: Source) []const u8 {
|
fn content(self: Source) []const u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
@@ -639,18 +690,15 @@ pub const Script = struct {
|
|||||||
import_async: ImportAsync,
|
import_async: ImportAsync,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn deinit(self: *Script, comptime release_buffer: bool) void {
|
fn deinit(self: *Script) void {
|
||||||
if ((comptime release_buffer) and self.source == .remote) {
|
self.manager.page.releaseArena(self.arena);
|
||||||
self.manager.buffer_pool.release(self.source.remote);
|
|
||||||
}
|
|
||||||
self.manager.script_pool.destroy(self);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn startCallback(transfer: *Http.Transfer) !void {
|
fn startCallback(transfer: *HttpClient.Transfer) !void {
|
||||||
log.debug(.http, "script fetch start", .{ .req = transfer });
|
log.debug(.http, "script fetch start", .{ .req = transfer });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn headerCallback(transfer: *Http.Transfer) !void {
|
fn headerCallback(transfer: *HttpClient.Transfer) !bool {
|
||||||
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
||||||
const header = &transfer.response_header.?;
|
const header = &transfer.response_header.?;
|
||||||
self.status = header.status;
|
self.status = header.status;
|
||||||
@@ -660,7 +708,7 @@ pub const Script = struct {
|
|||||||
.status = header.status,
|
.status = header.status,
|
||||||
.content_type = header.contentType(),
|
.content_type = header.contentType(),
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -671,27 +719,58 @@ pub const Script = struct {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this isn't true, then we'll likely leak memory. If you don't
|
{
|
||||||
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
|
// temp debug, trying to figure out why the next assert sometimes
|
||||||
// will fail. This assertion exists to catch incorrect assumptions about
|
// fails. Is the buffer just corrupt or is headerCallback really
|
||||||
// how libcurl works, or about how we've configured it.
|
// being called twice?
|
||||||
std.debug.assert(self.source.remote.capacity == 0);
|
lp.assert(self.header_callback_called == false, "ScriptManager.Header recall", .{
|
||||||
var buffer = self.manager.buffer_pool.get();
|
.m = @tagName(std.meta.activeTag(self.mode)),
|
||||||
|
.a1 = self.debug_transfer_id,
|
||||||
|
.a2 = self.debug_transfer_tries,
|
||||||
|
.a3 = self.debug_transfer_aborted,
|
||||||
|
.a4 = self.debug_transfer_bytes_received,
|
||||||
|
.a5 = self.debug_transfer_notified_fail,
|
||||||
|
.a7 = self.debug_transfer_intercept_state,
|
||||||
|
.a8 = self.debug_transfer_auth_challenge,
|
||||||
|
.a9 = self.debug_transfer_easy_id,
|
||||||
|
.b1 = transfer.id,
|
||||||
|
.b2 = transfer._tries,
|
||||||
|
.b3 = transfer.aborted,
|
||||||
|
.b4 = transfer.bytes_received,
|
||||||
|
.b5 = transfer._notified_fail,
|
||||||
|
.b7 = @intFromEnum(transfer._intercept_state),
|
||||||
|
.b8 = transfer._auth_challenge != null,
|
||||||
|
.b9 = if (transfer._conn) |c| @intFromPtr(c._easy) else 0,
|
||||||
|
});
|
||||||
|
self.header_callback_called = true;
|
||||||
|
self.debug_transfer_id = transfer.id;
|
||||||
|
self.debug_transfer_tries = transfer._tries;
|
||||||
|
self.debug_transfer_aborted = transfer.aborted;
|
||||||
|
self.debug_transfer_bytes_received = transfer.bytes_received;
|
||||||
|
self.debug_transfer_notified_fail = transfer._notified_fail;
|
||||||
|
self.debug_transfer_intercept_state = @intFromEnum(transfer._intercept_state);
|
||||||
|
self.debug_transfer_auth_challenge = transfer._auth_challenge != null;
|
||||||
|
self.debug_transfer_easy_id = if (transfer._conn) |c| @intFromPtr(c._easy) else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
||||||
|
var buffer: std.ArrayList(u8) = .empty;
|
||||||
if (transfer.getContentLength()) |cl| {
|
if (transfer.getContentLength()) |cl| {
|
||||||
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
|
try buffer.ensureTotalCapacity(self.arena, cl);
|
||||||
}
|
}
|
||||||
self.source = .{ .remote = buffer };
|
self.source = .{ .remote = buffer };
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
||||||
self._dataCallback(transfer, data) catch |err| {
|
self._dataCallback(transfer, data) catch |err| {
|
||||||
log.err(.http, "SM.dataCallback", .{ .err = err, .transfer = transfer, .len = data.len });
|
log.err(.http, "SM.dataCallback", .{ .err = err, .transfer = transfer, .len = data.len });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
fn _dataCallback(self: *Script, _: *Http.Transfer, data: []const u8) !void {
|
fn _dataCallback(self: *Script, _: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
try self.source.remote.appendSlice(self.manager.allocator, data);
|
try self.source.remote.appendSlice(self.arena, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn doneCallback(ctx: *anyopaque) !void {
|
fn doneCallback(ctx: *anyopaque) !void {
|
||||||
@@ -708,9 +787,8 @@ pub const Script = struct {
|
|||||||
} else if (self.mode == .import) {
|
} else if (self.mode == .import) {
|
||||||
manager.async_scripts.remove(&self.node);
|
manager.async_scripts.remove(&self.node);
|
||||||
const entry = manager.imported_modules.getPtr(self.url).?;
|
const entry = manager.imported_modules.getPtr(self.url).?;
|
||||||
entry.state = .done;
|
entry.state = .{ .done = self };
|
||||||
entry.buffer = self.source.remote;
|
entry.buffer = self.source.remote;
|
||||||
self.deinit(false);
|
|
||||||
}
|
}
|
||||||
manager.evaluate();
|
manager.evaluate();
|
||||||
}
|
}
|
||||||
@@ -720,7 +798,7 @@ pub const Script = struct {
|
|||||||
log.warn(.http, "script fetch error", .{
|
log.warn(.http, "script fetch error", .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.req = self.url,
|
.req = self.url,
|
||||||
.mode = self.mode,
|
.mode = std.meta.activeTag(self.mode),
|
||||||
.kind = self.kind,
|
.kind = self.kind,
|
||||||
.status = self.status,
|
.status = self.status,
|
||||||
});
|
});
|
||||||
@@ -736,24 +814,30 @@ pub const Script = struct {
|
|||||||
const manager = self.manager;
|
const manager = self.manager;
|
||||||
manager.scriptList(self).remove(&self.node);
|
manager.scriptList(self).remove(&self.node);
|
||||||
if (manager.shutdown) {
|
if (manager.shutdown) {
|
||||||
self.deinit(true);
|
self.deinit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.mode == .import) {
|
switch (self.mode) {
|
||||||
const entry = self.manager.imported_modules.getPtr(self.url).?;
|
.import_async => |ia| ia.callback(ia.data, error.FailedToLoad),
|
||||||
entry.state = .err;
|
.import => {
|
||||||
|
const entry = manager.imported_modules.getPtr(self.url).?;
|
||||||
|
entry.state = .err;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
}
|
}
|
||||||
self.deinit(true);
|
self.deinit();
|
||||||
manager.evaluate();
|
manager.evaluate();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval(self: *Script, page: *Page) void {
|
fn eval(self: *Script, page: *Page) void {
|
||||||
// never evaluated, source is passed back to v8, via callbacks.
|
// never evaluated, source is passed back to v8, via callbacks.
|
||||||
std.debug.assert(self.mode != .import_async);
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(self.mode != .import_async);
|
||||||
|
|
||||||
// never evaluated, source is passed back to v8 when asked for it.
|
// never evaluated, source is passed back to v8 when asked for it.
|
||||||
std.debug.assert(self.mode != .import);
|
std.debug.assert(self.mode != .import);
|
||||||
|
}
|
||||||
|
|
||||||
if (page.isGoingAway()) {
|
if (page.isGoingAway()) {
|
||||||
// don't evaluate scripts for a dying page.
|
// don't evaluate scripts for a dying page.
|
||||||
@@ -782,6 +866,12 @@ pub const Script = struct {
|
|||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
const local = &ls.local;
|
||||||
|
|
||||||
// Handle importmap special case here: the content is a JSON containing
|
// Handle importmap special case here: the content is a JSON containing
|
||||||
// imports.
|
// imports.
|
||||||
if (self.kind == .importmap) {
|
if (self.kind == .importmap) {
|
||||||
@@ -792,25 +882,26 @@ pub const Script = struct {
|
|||||||
.kind = self.kind,
|
.kind = self.kind,
|
||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
self.executeCallback("error", script_element._on_error, page);
|
self.executeCallback(comptime .wrap("error"), page);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.executeCallback("load", script_element._on_load, page);
|
self.executeCallback(comptime .wrap("load"), page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const js_context = page.js;
|
defer page._event_manager.clearIgnoreList();
|
||||||
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(js_context);
|
try_catch.init(local);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
const success = blk: {
|
const success = blk: {
|
||||||
const content = self.source.content();
|
const content = self.source.content();
|
||||||
switch (self.kind) {
|
switch (self.kind) {
|
||||||
.javascript => _ = js_context.eval(content, url) catch break :blk false,
|
.javascript => _ = local.eval(content, url) catch break :blk false,
|
||||||
.module => {
|
.module => {
|
||||||
// We don't care about waiting for the evaluation here.
|
// We don't care about waiting for the evaluation here.
|
||||||
js_context.module(false, content, url, cacheable) catch break :blk false;
|
page.js.module(false, local, content, url, cacheable) catch break :blk false;
|
||||||
},
|
},
|
||||||
.importmap => unreachable, // handled before the try/catch.
|
.importmap => unreachable, // handled before the try/catch.
|
||||||
}
|
}
|
||||||
@@ -818,37 +909,32 @@ pub const Script = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.browser, "executed script", .{ .src = url, .success = success, .on_load = script_element._on_load != null });
|
log.debug(.browser, "executed script", .{ .src = url, .success = success });
|
||||||
}
|
}
|
||||||
|
|
||||||
defer {
|
defer {
|
||||||
// We should run microtasks even if script execution fails.
|
local.runMacrotasks(); // also runs microtasks
|
||||||
page.js.runMicrotasks();
|
_ = page.js.scheduler.run() catch |err| {
|
||||||
_ = page.scheduler.run() catch |err| {
|
|
||||||
log.err(.page, "scheduler", .{ .err = err });
|
log.err(.page, "scheduler", .{ .err = err });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
self.executeCallback("load", script_element._on_load, page);
|
self.executeCallback(comptime .wrap("load"), page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = try_catch.err(page.arena) catch |err| @errorName(err) orelse "unknown";
|
const caught = try_catch.caughtOrError(page.call_arena, error.Unknown);
|
||||||
log.warn(.js, "eval script", .{
|
log.warn(.js, "eval script", .{
|
||||||
.url = url,
|
.url = url,
|
||||||
.err = msg,
|
.caught = caught,
|
||||||
.stack = try_catch.stack(page.call_arena) catch null,
|
|
||||||
.line = try_catch.sourceLineNumber() orelse 0,
|
|
||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
|
|
||||||
self.executeCallback("error", script_element._on_error, page);
|
self.executeCallback(comptime .wrap("error"), page);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void {
|
fn executeCallback(self: *const Script, typ: String, page: *Page) void {
|
||||||
const cb = cb_ orelse return;
|
|
||||||
|
|
||||||
const Event = @import("webapi/Event.zig");
|
const Event = @import("webapi/Event.zig");
|
||||||
const event = Event.initTrusted(typ, .{}, page) catch |err| {
|
const event = Event.initTrusted(typ, .{}, page) catch |err| {
|
||||||
log.warn(.js, "script internal callback", .{
|
log.warn(.js, "script internal callback", .{
|
||||||
@@ -858,89 +944,16 @@ pub const Script = struct {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
page._event_manager.dispatchOpts(self.script_element.?.asNode().asEventTarget(), event, .{ .apply_ignore = true }) catch |err| {
|
||||||
var result: js.Function.Result = undefined;
|
|
||||||
cb.tryCall(void, .{event}, &result) catch {
|
|
||||||
log.warn(.js, "script callback", .{
|
log.warn(.js, "script callback", .{
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
.type = typ,
|
.type = typ,
|
||||||
.err = result.exception,
|
.err = err,
|
||||||
.stack = result.stack,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const BufferPool = struct {
|
|
||||||
count: usize,
|
|
||||||
available: List = .{},
|
|
||||||
allocator: Allocator,
|
|
||||||
max_concurrent_transfers: u8,
|
|
||||||
mem_pool: std.heap.MemoryPool(Container),
|
|
||||||
|
|
||||||
const List = std.DoublyLinkedList;
|
|
||||||
|
|
||||||
const Container = struct {
|
|
||||||
node: List.Node,
|
|
||||||
buf: std.ArrayListUnmanaged(u8),
|
|
||||||
};
|
|
||||||
|
|
||||||
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
|
|
||||||
return .{
|
|
||||||
.available = .{},
|
|
||||||
.count = 0,
|
|
||||||
.allocator = allocator,
|
|
||||||
.max_concurrent_transfers = max_concurrent_transfers,
|
|
||||||
.mem_pool = std.heap.MemoryPool(Container).init(allocator),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(self: *BufferPool) void {
|
|
||||||
const allocator = self.allocator;
|
|
||||||
|
|
||||||
var node = self.available.first;
|
|
||||||
while (node) |n| {
|
|
||||||
const container: *Container = @fieldParentPtr("node", n);
|
|
||||||
container.buf.deinit(allocator);
|
|
||||||
node = n.next;
|
|
||||||
}
|
|
||||||
self.mem_pool.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(self: *BufferPool) std.ArrayListUnmanaged(u8) {
|
|
||||||
const node = self.available.popFirst() orelse {
|
|
||||||
// return a new buffer
|
|
||||||
return .{};
|
|
||||||
};
|
|
||||||
|
|
||||||
self.count -= 1;
|
|
||||||
const container: *Container = @fieldParentPtr("node", node);
|
|
||||||
defer self.mem_pool.destroy(container);
|
|
||||||
return container.buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn release(self: *BufferPool, buffer: ArrayListUnmanaged(u8)) void {
|
|
||||||
// create mutable copy
|
|
||||||
var b = buffer;
|
|
||||||
|
|
||||||
if (self.count == self.max_concurrent_transfers) {
|
|
||||||
b.deinit(self.allocator);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = self.mem_pool.create() catch |err| {
|
|
||||||
b.deinit(self.allocator);
|
|
||||||
log.err(.http, "SM BufferPool release", .{ .err = err });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
b.clearRetainingCapacity();
|
|
||||||
container.* = .{ .buf = b, .node = .{} };
|
|
||||||
self.count += 1;
|
|
||||||
self.available.append(&container.node);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImportAsync = struct {
|
const ImportAsync = struct {
|
||||||
data: *anyopaque,
|
data: *anyopaque,
|
||||||
callback: ImportAsync.Callback,
|
callback: ImportAsync.Callback,
|
||||||
@@ -950,12 +963,12 @@ const ImportAsync = struct {
|
|||||||
|
|
||||||
pub const ModuleSource = struct {
|
pub const ModuleSource = struct {
|
||||||
shared: bool,
|
shared: bool,
|
||||||
buffer_pool: *BufferPool,
|
script: *Script,
|
||||||
buffer: std.ArrayList(u8),
|
buffer: std.ArrayList(u8),
|
||||||
|
|
||||||
pub fn deinit(self: *ModuleSource) void {
|
pub fn deinit(self: *ModuleSource) void {
|
||||||
if (self.shared == false) {
|
if (self.shared == false) {
|
||||||
self.buffer_pool.release(self.buffer);
|
self.script.deinit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -965,15 +978,14 @@ pub const ModuleSource = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ImportedModule = struct {
|
const ImportedModule = struct {
|
||||||
manager: *ScriptManager,
|
waiters: u16 = 1,
|
||||||
state: State = .loading,
|
state: State = .loading,
|
||||||
buffer: std.ArrayList(u8) = .{},
|
buffer: std.ArrayList(u8) = .{},
|
||||||
waiters: u16 = 1,
|
|
||||||
|
|
||||||
const State = enum {
|
const State = union(enum) {
|
||||||
err,
|
err,
|
||||||
done,
|
|
||||||
loading,
|
loading,
|
||||||
|
done: *Script,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -985,23 +997,35 @@ fn parseDataURI(allocator: Allocator, src: []const u8) !?[]const u8 {
|
|||||||
|
|
||||||
const uri = src[5..];
|
const uri = src[5..];
|
||||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
||||||
|
const data = uri[data_starts + 1 ..];
|
||||||
|
|
||||||
var data = uri[data_starts + 1 ..];
|
const unescaped = try URL.unescape(allocator, data);
|
||||||
|
|
||||||
// Extract the encoding.
|
|
||||||
const metadata = uri[0..data_starts];
|
const metadata = uri[0..data_starts];
|
||||||
if (std.mem.endsWith(u8, metadata, ";base64")) {
|
if (std.mem.endsWith(u8, metadata, ";base64") == false) {
|
||||||
const decoder = std.base64.standard.Decoder;
|
return unescaped;
|
||||||
const decoded_size = try decoder.calcSizeForSlice(data);
|
|
||||||
|
|
||||||
const buffer = try allocator.alloc(u8, decoded_size);
|
|
||||||
errdefer allocator.free(buffer);
|
|
||||||
|
|
||||||
try decoder.decode(buffer, data);
|
|
||||||
data = buffer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
// Forgiving base64 decode per WHATWG spec:
|
||||||
|
// https://infra.spec.whatwg.org/#forgiving-base64-decode
|
||||||
|
// Step 1: Remove all ASCII whitespace
|
||||||
|
var stripped = try std.ArrayList(u8).initCapacity(allocator, unescaped.len);
|
||||||
|
for (unescaped) |c| {
|
||||||
|
if (!std.ascii.isWhitespace(c)) {
|
||||||
|
stripped.appendAssumeCapacity(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const trimmed = std.mem.trimRight(u8, stripped.items, "=");
|
||||||
|
|
||||||
|
// Length % 4 == 1 is invalid
|
||||||
|
if (trimmed.len % 4 == 1) {
|
||||||
|
return error.InvalidCharacterError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded_size = std.base64.standard_no_pad.Decoder.calcSizeForSlice(trimmed) catch return error.InvalidCharacterError;
|
||||||
|
const buffer = try allocator.alloc(u8, decoded_size);
|
||||||
|
std.base64.standard_no_pad.Decoder.decode(buffer, trimmed) catch return error.InvalidCharacterError;
|
||||||
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
|
|||||||
@@ -17,67 +17,112 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const lp = @import("lightpanda");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
|
const App = @import("../App.zig");
|
||||||
|
|
||||||
const js = @import("js/js.zig");
|
const js = @import("js/js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
const storage = @import("webapi/storage/storage.zig");
|
const storage = @import("webapi/storage/storage.zig");
|
||||||
const Navigation = @import("webapi/navigation/Navigation.zig");
|
const Navigation = @import("webapi/navigation/Navigation.zig");
|
||||||
const History = @import("webapi/History.zig");
|
const History = @import("webapi/History.zig");
|
||||||
|
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
|
pub const Runner = @import("Runner.zig");
|
||||||
const Browser = @import("Browser.zig");
|
const Browser = @import("Browser.zig");
|
||||||
|
const Factory = @import("Factory.zig");
|
||||||
|
const Notification = @import("../Notification.zig");
|
||||||
|
const QueuedNavigation = Page.QueuedNavigation;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const ArenaPool = App.ArenaPool;
|
||||||
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
// Session is like a browser's tab.
|
|
||||||
// It owns the js env and the loader for all the pages of the session.
|
|
||||||
// You can create successively multiple pages for a session, but you must
|
// You can create successively multiple pages for a session, but you must
|
||||||
// deinit a page before running another one.
|
// deinit a page before running another one. It manages two distinct lifetimes.
|
||||||
|
//
|
||||||
|
// The first is the lifetime of the Session itself, where pages are created and
|
||||||
|
// removed, but share the same cookie jar and navigation history (etc...)
|
||||||
|
//
|
||||||
|
// The second is as a container the data needed by the full page hierarchy, i.e. \
|
||||||
|
// the root page and all of its frames (and all of their frames.)
|
||||||
const Session = @This();
|
const Session = @This();
|
||||||
|
|
||||||
|
// These are the fields that remain intact for the duration of the Session
|
||||||
browser: *Browser,
|
browser: *Browser,
|
||||||
|
|
||||||
// Used to create our Inspector and in the BrowserContext.
|
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
|
|
||||||
// The page's arena is unsuitable for data that has to existing while
|
|
||||||
// navigating from one page to another. For example, if we're clicking
|
|
||||||
// on an HREF, the URL exists in the original page (where the click
|
|
||||||
// originated) but also has to exist in the new page.
|
|
||||||
// While we could use the Session's arena, this could accumulate a lot of
|
|
||||||
// memory if we do many navigation events. The `transfer_arena` is meant to
|
|
||||||
// bridge the gap: existing long enough to store any data needed to end one
|
|
||||||
// page and start another.
|
|
||||||
transfer_arena: Allocator,
|
|
||||||
|
|
||||||
executor: js.ExecutionWorld,
|
|
||||||
cookie_jar: storage.Cookie.Jar,
|
|
||||||
storage_shed: storage.Shed,
|
|
||||||
|
|
||||||
history: History,
|
history: History,
|
||||||
navigation: Navigation,
|
navigation: Navigation,
|
||||||
|
storage_shed: storage.Shed,
|
||||||
|
notification: *Notification,
|
||||||
|
cookie_jar: storage.Cookie.Jar,
|
||||||
|
|
||||||
page: ?*Page = null,
|
// These are the fields that get reset whenever the Session's page (the root) is reset.
|
||||||
|
factory: Factory,
|
||||||
|
|
||||||
pub fn init(self: *Session, browser: *Browser) !void {
|
page_arena: Allocator,
|
||||||
var executor = try browser.env.newExecutionWorld();
|
|
||||||
errdefer executor.deinit();
|
|
||||||
|
|
||||||
|
// Origin map for same-origin context sharing. Scoped to the root page lifetime.
|
||||||
|
origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
|
||||||
|
|
||||||
|
// Identity tracking for the main world. All main world contexts share this,
|
||||||
|
// ensuring object identity works across same-origin frames.
|
||||||
|
identity: js.Identity = .{},
|
||||||
|
|
||||||
|
// Shared resources for all pages in this session.
|
||||||
|
// These live for the duration of the page tree (root + frames).
|
||||||
|
arena_pool: *ArenaPool,
|
||||||
|
|
||||||
|
page: ?Page,
|
||||||
|
|
||||||
|
// Double buffer so that, as we process one list of queued navigations, new entries
|
||||||
|
// are added to the separate buffer. This ensures that we don't end up with
|
||||||
|
// endless navigation loops AND that we don't invalidate the list while iterating
|
||||||
|
// if a new entry gets appended
|
||||||
|
queued_navigation_1: std.ArrayList(*Page),
|
||||||
|
queued_navigation_2: std.ArrayList(*Page),
|
||||||
|
// pointer to either queued_navigation_1 or queued_navigation_2
|
||||||
|
queued_navigation: *std.ArrayList(*Page),
|
||||||
|
|
||||||
|
// Temporary buffer for about:blank navigations during processing.
|
||||||
|
// We process async navigations first (safe from re-entrance), then sync
|
||||||
|
// about:blank navigations (which may add to queued_navigation).
|
||||||
|
queued_queued_navigation: std.ArrayList(*Page),
|
||||||
|
|
||||||
|
page_id_gen: u32 = 0,
|
||||||
|
frame_id_gen: u32 = 0,
|
||||||
|
|
||||||
|
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
|
||||||
const allocator = browser.app.allocator;
|
const allocator = browser.app.allocator;
|
||||||
const session_allocator = browser.session_arena.allocator();
|
const arena_pool = browser.arena_pool;
|
||||||
|
|
||||||
|
const arena = try arena_pool.acquire(.{ .debug = "Session" });
|
||||||
|
errdefer arena_pool.release(arena);
|
||||||
|
|
||||||
|
const page_arena = try arena_pool.acquire(.{ .debug = "Session.page_arena" });
|
||||||
|
errdefer arena_pool.release(page_arena);
|
||||||
|
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.browser = browser,
|
.page = null,
|
||||||
.executor = executor,
|
.arena = arena,
|
||||||
.storage_shed = .{},
|
.arena_pool = arena_pool,
|
||||||
.arena = session_allocator,
|
.page_arena = page_arena,
|
||||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
.factory = Factory.init(page_arena),
|
||||||
.navigation = .{},
|
|
||||||
.history = .{},
|
.history = .{},
|
||||||
.transfer_arena = browser.transfer_arena.allocator(),
|
// The prototype (EventTarget) for Navigation is created when a Page is created.
|
||||||
|
.navigation = .{ ._proto = undefined },
|
||||||
|
.storage_shed = .{},
|
||||||
|
.browser = browser,
|
||||||
|
.queued_navigation = undefined,
|
||||||
|
.queued_navigation_1 = .{},
|
||||||
|
.queued_navigation_2 = .{},
|
||||||
|
.queued_queued_navigation = .{},
|
||||||
|
.notification = notification,
|
||||||
|
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||||
};
|
};
|
||||||
|
self.queued_navigation = &self.queued_navigation_1;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Session) void {
|
pub fn deinit(self: *Session) void {
|
||||||
@@ -85,20 +130,20 @@ pub fn deinit(self: *Session) void {
|
|||||||
self.removePage();
|
self.removePage();
|
||||||
}
|
}
|
||||||
self.cookie_jar.deinit();
|
self.cookie_jar.deinit();
|
||||||
|
|
||||||
self.storage_shed.deinit(self.browser.app.allocator);
|
self.storage_shed.deinit(self.browser.app.allocator);
|
||||||
self.executor.deinit();
|
self.arena_pool.release(self.page_arena);
|
||||||
|
self.arena_pool.release(self.arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: the caller is not the owner of the returned value,
|
// NOTE: the caller is not the owner of the returned value,
|
||||||
// the pointer on Page is just returned as a convenience
|
// the pointer on Page is just returned as a convenience
|
||||||
pub fn createPage(self: *Session) !*Page {
|
pub fn createPage(self: *Session) !*Page {
|
||||||
std.debug.assert(self.page == null);
|
lp.assert(self.page == null, "Session.createPage - page not null", .{});
|
||||||
|
|
||||||
const page_arena = &self.browser.page_arena;
|
self.page = @as(Page, undefined);
|
||||||
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
const page = &self.page.?;
|
||||||
|
try Page.init(page, self.nextFrameId(), self, null);
|
||||||
self.page = try Page.init(page_arena.allocator(), self.browser.call_arena.allocator(), self);
|
|
||||||
const page = self.page.?;
|
|
||||||
|
|
||||||
// Creates a new NavigationEventTarget for this page.
|
// Creates a new NavigationEventTarget for this page.
|
||||||
try self.navigation.onNewPage(page);
|
try self.navigation.onNewPage(page);
|
||||||
@@ -108,69 +153,339 @@ pub fn createPage(self: *Session) !*Page {
|
|||||||
}
|
}
|
||||||
// start JS env
|
// start JS env
|
||||||
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||||
self.browser.notification.dispatch(.page_created, page);
|
self.notification.dispatch(.page_created, page);
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn removePage(self: *Session) void {
|
pub fn removePage(self: *Session) void {
|
||||||
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
|
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
|
||||||
self.browser.notification.dispatch(.page_remove, .{});
|
self.notification.dispatch(.page_remove, .{});
|
||||||
|
lp.assert(self.page != null, "Session.removePage - page is null", .{});
|
||||||
|
|
||||||
std.debug.assert(self.page != null);
|
self.page.?.deinit(false);
|
||||||
|
|
||||||
self.page.?.deinit();
|
|
||||||
self.page = null;
|
self.page = null;
|
||||||
|
|
||||||
self.navigation.onRemovePage();
|
self.navigation.onRemovePage();
|
||||||
|
self.resetPageResources();
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.browser, "remove page", .{});
|
log.debug(.browser, "remove page", .{});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn currentPage(self: *Session) ?*Page {
|
pub const GetArenaOpts = struct {
|
||||||
return self.page orelse return null;
|
debug: []const u8,
|
||||||
}
|
|
||||||
|
|
||||||
pub const WaitResult = enum {
|
|
||||||
done,
|
|
||||||
no_page,
|
|
||||||
cdp_socket,
|
|
||||||
navigate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
pub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator {
|
||||||
while (true) {
|
return self.arena_pool.acquire(.{ .debug = opts.debug });
|
||||||
const page = self.page orelse return .no_page;
|
}
|
||||||
switch (page.wait(wait_ms)) {
|
|
||||||
.navigate => self.processScheduledNavigation() catch return .done,
|
pub fn releaseArena(self: *Session, allocator: Allocator) void {
|
||||||
else => |result| return result,
|
self.arena_pool.release(allocator);
|
||||||
}
|
}
|
||||||
// if we've successfull navigated, we'll give the new page another
|
|
||||||
// page.wait(wait_ms)
|
pub fn getOrCreateOrigin(self: *Session, key_: ?[]const u8) !*js.Origin {
|
||||||
|
const key = key_ orelse {
|
||||||
|
var opaque_origin: [36]u8 = undefined;
|
||||||
|
@import("../id.zig").uuidv4(&opaque_origin);
|
||||||
|
// Origin.init will dupe opaque_origin. It's fine that this doesn't
|
||||||
|
// get added to self.origins. In fact, it further isolates it. When the
|
||||||
|
// context is freed, it'll call session.releaseOrigin which will free it.
|
||||||
|
return js.Origin.init(self.browser.app, self.browser.env.isolate, &opaque_origin);
|
||||||
|
};
|
||||||
|
|
||||||
|
const gop = try self.origins.getOrPut(self.arena, key);
|
||||||
|
if (gop.found_existing) {
|
||||||
|
const origin = gop.value_ptr.*;
|
||||||
|
origin.rc += 1;
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
errdefer _ = self.origins.remove(key);
|
||||||
|
|
||||||
|
const origin = try js.Origin.init(self.browser.app, self.browser.env.isolate, key);
|
||||||
|
gop.key_ptr.* = origin.key;
|
||||||
|
gop.value_ptr.* = origin;
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn releaseOrigin(self: *Session, origin: *js.Origin) void {
|
||||||
|
const rc = origin.rc;
|
||||||
|
if (rc == 1) {
|
||||||
|
_ = self.origins.remove(origin.key);
|
||||||
|
origin.deinit(self.browser.app);
|
||||||
|
} else {
|
||||||
|
origin.rc = rc - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn processScheduledNavigation(self: *Session) !void {
|
/// Reset page_arena and factory for a clean slate.
|
||||||
const qn = self.page.?._queued_navigation.?;
|
/// Called when root page is removed.
|
||||||
defer _ = self.browser.transfer_arena.reset(.{ .retain_with_limit = 8 * 1024 });
|
fn resetPageResources(self: *Session) void {
|
||||||
|
self.identity.deinit();
|
||||||
|
self.identity = .{};
|
||||||
|
|
||||||
// This was already aborted on the page, but it would be pretty
|
if (comptime IS_DEBUG) {
|
||||||
// bad if old requests went to the new page, so let's make double sure
|
std.debug.assert(self.origins.count() == 0);
|
||||||
self.browser.http_client.abort();
|
}
|
||||||
self.removePage();
|
// Defensive cleanup in case origins leaked
|
||||||
|
{
|
||||||
|
const app = self.browser.app;
|
||||||
|
var it = self.origins.valueIterator();
|
||||||
|
while (it.next()) |value| {
|
||||||
|
value.*.deinit(app);
|
||||||
|
}
|
||||||
|
self.origins = .empty;
|
||||||
|
}
|
||||||
|
|
||||||
const page = self.createPage() catch |err| {
|
self.frame_id_gen = 0;
|
||||||
log.err(.browser, "queued navigation page error", .{
|
self.arena_pool.reset(self.page_arena, 64 * 1024);
|
||||||
.err = err,
|
self.factory = Factory.init(self.page_arena);
|
||||||
.url = qn.url,
|
}
|
||||||
});
|
|
||||||
return err;
|
pub fn replacePage(self: *Session) !*Page {
|
||||||
};
|
if (comptime IS_DEBUG) {
|
||||||
|
log.debug(.browser, "replace page", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.assert(self.page != null, "Session.replacePage null page", .{});
|
||||||
|
lp.assert(self.page.?.parent == null, "Session.replacePage with parent", .{});
|
||||||
|
|
||||||
|
var current = self.page.?;
|
||||||
|
const frame_id = current._frame_id;
|
||||||
|
current.deinit(true);
|
||||||
|
|
||||||
|
self.resetPageResources();
|
||||||
|
self.browser.env.memoryPressureNotification(.moderate);
|
||||||
|
|
||||||
|
self.page = @as(Page, undefined);
|
||||||
|
const page = &self.page.?;
|
||||||
|
try Page.init(page, frame_id, self, null);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn currentPage(self: *Session) ?*Page {
|
||||||
|
return &(self.page orelse return null);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn findPageByFrameId(self: *Session, frame_id: u32) ?*Page {
|
||||||
|
const page = self.currentPage() orelse return null;
|
||||||
|
return findPageBy(page, "_frame_id", frame_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn findPageById(self: *Session, id: u32) ?*Page {
|
||||||
|
const page = self.currentPage() orelse return null;
|
||||||
|
return findPageBy(page, "id", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findPageBy(page: *Page, comptime field: []const u8, id: u32) ?*Page {
|
||||||
|
if (@field(page, field) == id) return page;
|
||||||
|
for (page.frames.items) |f| {
|
||||||
|
if (findPageBy(f, field, id)) |found| {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runner(self: *Session, opts: Runner.Opts) !Runner {
|
||||||
|
return Runner.init(self, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scheduleNavigation(self: *Session, page: *Page) !void {
|
||||||
|
const list = self.queued_navigation;
|
||||||
|
|
||||||
|
// Check if page is already queued
|
||||||
|
for (list.items) |existing| {
|
||||||
|
if (existing == page) {
|
||||||
|
// Already queued
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.append(self.arena, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn processQueuedNavigation(self: *Session) !void {
|
||||||
|
const navigations = self.queued_navigation;
|
||||||
|
if (self.queued_navigation == &self.queued_navigation_1) {
|
||||||
|
self.queued_navigation = &self.queued_navigation_2;
|
||||||
|
} else {
|
||||||
|
self.queued_navigation = &self.queued_navigation_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.page.?._queued_navigation != null) {
|
||||||
|
// This is both an optimization and a simplification of sorts. If the
|
||||||
|
// root page is navigating, then we don't need to process any other
|
||||||
|
// navigation. Also, the navigation for the root page and for a frame
|
||||||
|
// is different enough that have two distinct code blocks is, imo,
|
||||||
|
// better. Yes, there will be duplication.
|
||||||
|
navigations.clearRetainingCapacity();
|
||||||
|
return self.processRootQueuedNavigation();
|
||||||
|
}
|
||||||
|
|
||||||
|
const about_blank_queue = &self.queued_queued_navigation;
|
||||||
|
defer about_blank_queue.clearRetainingCapacity();
|
||||||
|
|
||||||
|
// First pass: process async navigations (non-about:blank)
|
||||||
|
for (navigations.items) |page| {
|
||||||
|
const qn = page._queued_navigation.?;
|
||||||
|
|
||||||
|
if (qn.is_about_blank) {
|
||||||
|
// Defer about:blank to second pass
|
||||||
|
try about_blank_queue.append(self.arena, page);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.processFrameNavigation(page, qn) catch |err| {
|
||||||
|
log.warn(.page, "frame navigation", .{ .url = qn.url, .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
navigations.clearRetainingCapacity();
|
||||||
|
|
||||||
|
// Second pass: process synchronous navigations (about:blank)
|
||||||
|
// These may trigger new navigations which go into queued_navigation
|
||||||
|
for (about_blank_queue.items) |page| {
|
||||||
|
const qn = page._queued_navigation.?;
|
||||||
|
try self.processFrameNavigation(page, qn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety: Remove any about:blank navigations that were queued during
|
||||||
|
// processing to prevent infinite loops. New navigations have been queued
|
||||||
|
// in the other buffer.
|
||||||
|
const new_navigations = self.queued_navigation;
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < new_navigations.items.len) {
|
||||||
|
const page = new_navigations.items[i];
|
||||||
|
if (page._queued_navigation) |qn| {
|
||||||
|
if (qn.is_about_blank) {
|
||||||
|
log.warn(.page, "recursive about blank", .{});
|
||||||
|
_ = self.queued_navigation.swapRemove(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !void {
|
||||||
|
lp.assert(page.parent != null, "root queued navigation", .{});
|
||||||
|
|
||||||
|
const iframe = page.iframe.?;
|
||||||
|
const parent = page.parent.?;
|
||||||
|
|
||||||
|
page._queued_navigation = null;
|
||||||
|
defer self.releaseArena(qn.arena);
|
||||||
|
|
||||||
|
errdefer iframe._window = null;
|
||||||
|
|
||||||
|
const parent_notified = page._parent_notified;
|
||||||
|
if (parent_notified) {
|
||||||
|
// we already notified the parent that we had loaded
|
||||||
|
parent._pending_loads += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame_id = page._frame_id;
|
||||||
|
page.deinit(true);
|
||||||
|
page.* = undefined;
|
||||||
|
|
||||||
|
try Page.init(page, frame_id, self, parent);
|
||||||
|
errdefer {
|
||||||
|
for (parent.frames.items, 0..) |frame, i| {
|
||||||
|
if (frame == page) {
|
||||||
|
parent.frames_sorted = false;
|
||||||
|
_ = parent.frames.swapRemove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parent_notified) {
|
||||||
|
parent._pending_loads -= 1;
|
||||||
|
}
|
||||||
|
page.deinit(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
page.iframe = iframe;
|
||||||
|
iframe._window = page.window;
|
||||||
|
|
||||||
page.navigate(qn.url, qn.opts) catch |err| {
|
page.navigate(qn.url, qn.opts) catch |err| {
|
||||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
log.err(.browser, "queued frame navigation error", .{ .err = err });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn processRootQueuedNavigation(self: *Session) !void {
|
||||||
|
const current_page = &self.page.?;
|
||||||
|
const frame_id = current_page._frame_id;
|
||||||
|
|
||||||
|
// create a copy before the page is cleared
|
||||||
|
const qn = current_page._queued_navigation.?;
|
||||||
|
current_page._queued_navigation = null;
|
||||||
|
|
||||||
|
defer self.arena_pool.release(qn.arena);
|
||||||
|
|
||||||
|
self.removePage();
|
||||||
|
|
||||||
|
self.page = @as(Page, undefined);
|
||||||
|
const new_page = &self.page.?;
|
||||||
|
try Page.init(new_page, frame_id, self, null);
|
||||||
|
|
||||||
|
// Creates a new NavigationEventTarget for this page.
|
||||||
|
try self.navigation.onNewPage(new_page);
|
||||||
|
|
||||||
|
// start JS env
|
||||||
|
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||||
|
self.notification.dispatch(.page_created, new_page);
|
||||||
|
|
||||||
|
new_page.navigate(qn.url, qn.opts) catch |err| {
|
||||||
|
log.err(.browser, "queued navigation error", .{ .err = err });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nextFrameId(self: *Session) u32 {
|
||||||
|
const id = self.frame_id_gen +% 1;
|
||||||
|
self.frame_id_gen = id;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nextPageId(self: *Session) u32 {
|
||||||
|
const id = self.page_id_gen +% 1;
|
||||||
|
self.page_id_gen = id;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A type that has a finalizer can have its finalizer called one of two ways.
|
||||||
|
// The first is from V8 via the WeakCallback we give to weakRef. But that isn't
|
||||||
|
// guaranteed to fire, so we track this in finalizer_callbacks and call them on
|
||||||
|
// page reset.
|
||||||
|
pub const FinalizerCallback = struct {
|
||||||
|
arena: Allocator,
|
||||||
|
session: *Session,
|
||||||
|
ptr: *anyopaque,
|
||||||
|
global: v8.Global,
|
||||||
|
identity: *js.Identity,
|
||||||
|
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
|
||||||
|
|
||||||
|
pub fn deinit(self: *FinalizerCallback) void {
|
||||||
|
self.zig_finalizer(self.ptr, self.session);
|
||||||
|
self.session.releaseArena(self.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release this item from the identity tracking maps (called after finalizer runs from V8)
|
||||||
|
pub fn releaseIdentity(self: *FinalizerCallback) void {
|
||||||
|
const session = self.session;
|
||||||
|
const id = @intFromPtr(self.ptr);
|
||||||
|
|
||||||
|
if (self.identity.identity_map.fetchRemove(id)) |kv| {
|
||||||
|
var global = kv.value;
|
||||||
|
v8.v8__Global__Reset(&global);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = self.identity.finalizer_callbacks.remove(id);
|
||||||
|
|
||||||
|
session.releaseArena(self.arena);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
855
src/browser/StyleManager.zig
Normal file
855
src/browser/StyleManager.zig
Normal file
@@ -0,0 +1,855 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const log = @import("../log.zig");
|
||||||
|
const String = @import("../string.zig").String;
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
|
||||||
|
const CssParser = @import("css/Parser.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
|
const Selector = @import("webapi/selector/Selector.zig");
|
||||||
|
const SelectorParser = @import("webapi/selector/Parser.zig");
|
||||||
|
const SelectorList = @import("webapi/selector/List.zig");
|
||||||
|
|
||||||
|
const CSSStyleRule = @import("webapi/css/CSSStyleRule.zig");
|
||||||
|
const CSSStyleSheet = @import("webapi/css/CSSStyleSheet.zig");
|
||||||
|
const CSSStyleProperties = @import("webapi/css/CSSStyleProperties.zig");
|
||||||
|
const CSSStyleProperty = @import("webapi/css/CSSStyleDeclaration.zig").Property;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const VisibilityCache = std.AutoHashMapUnmanaged(*Element, bool);
|
||||||
|
pub const PointerEventsCache = std.AutoHashMapUnmanaged(*Element, bool);
|
||||||
|
|
||||||
|
// Tracks visibility-relevant CSS rules from <style> elements.
|
||||||
|
// Rules are bucketed by their rightmost selector part for fast lookup.
|
||||||
|
const StyleManager = @This();
|
||||||
|
|
||||||
|
const Tag = Element.Tag;
|
||||||
|
const RuleList = std.MultiArrayList(VisibilityRule);
|
||||||
|
|
||||||
|
page: *Page,
|
||||||
|
|
||||||
|
arena: Allocator,
|
||||||
|
|
||||||
|
// Bucketed rules for fast lookup - keyed by rightmost selector part
|
||||||
|
id_rules: std.StringHashMapUnmanaged(RuleList) = .empty,
|
||||||
|
class_rules: std.StringHashMapUnmanaged(RuleList) = .empty,
|
||||||
|
tag_rules: std.AutoHashMapUnmanaged(Tag, RuleList) = .empty,
|
||||||
|
other_rules: RuleList = .empty, // universal, attribute, pseudo-class endings
|
||||||
|
|
||||||
|
// Document order counter for tie-breaking equal specificity
|
||||||
|
next_doc_order: u32 = 0,
|
||||||
|
|
||||||
|
// When true, rules need to be rebuilt
|
||||||
|
dirty: bool = false,
|
||||||
|
|
||||||
|
pub fn init(page: *Page) !StyleManager {
|
||||||
|
return .{
|
||||||
|
.page = page,
|
||||||
|
.arena = try page.getArena(.{ .debug = "StyleManager" }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *StyleManager) void {
|
||||||
|
self.page.releaseArena(self.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseSheet(self: *StyleManager, sheet: *CSSStyleSheet) !void {
|
||||||
|
if (sheet._css_rules) |css_rules| {
|
||||||
|
for (css_rules._rules.items) |rule| {
|
||||||
|
const style_rule = rule.is(CSSStyleRule) orelse continue;
|
||||||
|
try self.addRule(style_rule);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const owner_node = sheet.getOwnerNode() orelse return;
|
||||||
|
if (owner_node.is(Element.Html.Style)) |style| {
|
||||||
|
const text = try style.asNode().getTextContentAlloc(self.arena);
|
||||||
|
var it = CssParser.parseStylesheet(text);
|
||||||
|
while (it.next()) |parsed_rule| {
|
||||||
|
try self.addRawRule(parsed_rule.selector, parsed_rule.block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addRawRule(self: *StyleManager, selector_text: []const u8, block_text: []const u8) !void {
|
||||||
|
if (selector_text.len == 0) return;
|
||||||
|
|
||||||
|
var props = VisibilityProperties{};
|
||||||
|
var it = CssParser.parseDeclarationsList(block_text);
|
||||||
|
while (it.next()) |decl| {
|
||||||
|
const name = decl.name;
|
||||||
|
const val = decl.value;
|
||||||
|
if (std.ascii.eqlIgnoreCase(name, "display")) {
|
||||||
|
props.display_none = std.ascii.eqlIgnoreCase(val, "none");
|
||||||
|
} else if (std.ascii.eqlIgnoreCase(name, "visibility")) {
|
||||||
|
props.visibility_hidden = std.ascii.eqlIgnoreCase(val, "hidden") or std.ascii.eqlIgnoreCase(val, "collapse");
|
||||||
|
} else if (std.ascii.eqlIgnoreCase(name, "opacity")) {
|
||||||
|
props.opacity_zero = std.ascii.eqlIgnoreCase(val, "0");
|
||||||
|
} else if (std.ascii.eqlIgnoreCase(name, "pointer-events")) {
|
||||||
|
props.pointer_events_none = std.ascii.eqlIgnoreCase(val, "none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.isRelevant()) return;
|
||||||
|
|
||||||
|
const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return;
|
||||||
|
for (selectors) |selector| {
|
||||||
|
const rightmost = if (selector.segments.len > 0) selector.segments[selector.segments.len - 1].compound else selector.first;
|
||||||
|
const bucket_key = getBucketKey(rightmost) orelse continue;
|
||||||
|
const rule = VisibilityRule{
|
||||||
|
.props = props,
|
||||||
|
.selector = selector,
|
||||||
|
.priority = (@as(u64, computeSpecificity(selector)) << 32) | @as(u64, self.next_doc_order),
|
||||||
|
};
|
||||||
|
self.next_doc_order += 1;
|
||||||
|
|
||||||
|
switch (bucket_key) {
|
||||||
|
.id => |id| {
|
||||||
|
const gop = try self.id_rules.getOrPut(self.arena, id);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.class => |class| {
|
||||||
|
const gop = try self.class_rules.getOrPut(self.arena, class);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.tag => |tag| {
|
||||||
|
const gop = try self.tag_rules.getOrPut(self.arena, tag);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.other => {
|
||||||
|
try self.other_rules.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sheetRemoved(self: *StyleManager) void {
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sheetModified(self: *StyleManager) void {
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuilds the rule list from all document stylesheets.
|
||||||
|
/// Called lazily when dirty flag is set and rules are needed.
|
||||||
|
fn rebuildIfDirty(self: *StyleManager) !void {
|
||||||
|
if (!self.dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dirty = false;
|
||||||
|
errdefer self.dirty = true;
|
||||||
|
const id_rules_count = self.id_rules.count();
|
||||||
|
const class_rules_count = self.class_rules.count();
|
||||||
|
const tag_rules_count = self.tag_rules.count();
|
||||||
|
const other_rules_count = self.other_rules.len;
|
||||||
|
|
||||||
|
self.page._session.arena_pool.resetRetain(self.arena);
|
||||||
|
|
||||||
|
self.next_doc_order = 0;
|
||||||
|
|
||||||
|
self.id_rules = .empty;
|
||||||
|
try self.id_rules.ensureTotalCapacity(self.arena, id_rules_count);
|
||||||
|
|
||||||
|
self.class_rules = .empty;
|
||||||
|
try self.class_rules.ensureTotalCapacity(self.arena, class_rules_count);
|
||||||
|
|
||||||
|
self.tag_rules = .empty;
|
||||||
|
try self.tag_rules.ensureTotalCapacity(self.arena, tag_rules_count);
|
||||||
|
|
||||||
|
self.other_rules = .{};
|
||||||
|
try self.other_rules.ensureTotalCapacity(self.arena, other_rules_count);
|
||||||
|
|
||||||
|
const sheets = self.page.document._style_sheets orelse return;
|
||||||
|
for (sheets._sheets.items) |sheet| {
|
||||||
|
self.parseSheet(sheet) catch |err| {
|
||||||
|
log.err(.browser, "StyleManager parseSheet", .{ .err = err });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an element is hidden based on options.
|
||||||
|
// By default only checks display:none.
|
||||||
|
// Walks up the tree to check ancestors.
|
||||||
|
pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, options: CheckVisibilityOptions) bool {
|
||||||
|
self.rebuildIfDirty() catch return false;
|
||||||
|
|
||||||
|
var current: ?*Element = el;
|
||||||
|
|
||||||
|
while (current) |elem| {
|
||||||
|
// Check cache first (only when checking all properties for caching consistency)
|
||||||
|
if (cache) |c| {
|
||||||
|
if (c.get(elem)) |hidden| {
|
||||||
|
if (hidden) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = elem.parentElement();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hidden = self.isElementHidden(elem, options);
|
||||||
|
|
||||||
|
// Store in cache
|
||||||
|
if (cache) |c| {
|
||||||
|
c.put(self.page.call_arena, elem, hidden) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = elem.parentElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a single element (not ancestors) is hidden.
|
||||||
|
fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOptions) bool {
|
||||||
|
// Track best match per property (value + priority)
|
||||||
|
// Initialize priority to INLINE_PRIORITY for properties we don't care about - this makes
|
||||||
|
// the loop naturally skip them since no stylesheet rule can have priority >= INLINE_PRIORITY
|
||||||
|
var display_none: ?bool = null;
|
||||||
|
var display_priority: u64 = 0;
|
||||||
|
|
||||||
|
var visibility_hidden: ?bool = null;
|
||||||
|
var visibility_priority: u64 = 0;
|
||||||
|
|
||||||
|
var opacity_zero: ?bool = null;
|
||||||
|
var opacity_priority: u64 = 0;
|
||||||
|
|
||||||
|
// Check inline styles FIRST - they use INLINE_PRIORITY so no stylesheet can beat them
|
||||||
|
if (getInlineStyleProperty(el, comptime .wrap("display"), self.page)) |property| {
|
||||||
|
if (property._value.eql(comptime .wrap("none"))) {
|
||||||
|
return true; // Early exit for hiding value
|
||||||
|
}
|
||||||
|
display_none = false;
|
||||||
|
display_priority = INLINE_PRIORITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.check_visibility) {
|
||||||
|
if (getInlineStyleProperty(el, comptime .wrap("visibility"), self.page)) |property| {
|
||||||
|
if (property._value.eql(comptime .wrap("hidden")) or property._value.eql(comptime .wrap("collapse"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
visibility_hidden = false;
|
||||||
|
visibility_priority = INLINE_PRIORITY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This can't be beat. Setting this means that, when checking rules
|
||||||
|
// we no longer have to check if options.check_visibility is enabled.
|
||||||
|
// We can just compare the priority.
|
||||||
|
visibility_priority = INLINE_PRIORITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.check_opacity) {
|
||||||
|
if (getInlineStyleProperty(el, comptime .wrap("opacity"), self.page)) |property| {
|
||||||
|
if (property._value.eql(comptime .wrap("0"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
opacity_zero = false;
|
||||||
|
opacity_priority = INLINE_PRIORITY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opacity_priority = INLINE_PRIORITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (display_priority == INLINE_PRIORITY and visibility_priority == INLINE_PRIORITY and opacity_priority == INLINE_PRIORITY) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to check a single rule
|
||||||
|
const Ctx = struct {
|
||||||
|
display_none: *?bool,
|
||||||
|
display_priority: *u64,
|
||||||
|
visibility_hidden: *?bool,
|
||||||
|
visibility_priority: *u64,
|
||||||
|
opacity_zero: *?bool,
|
||||||
|
opacity_priority: *u64,
|
||||||
|
el: *Element,
|
||||||
|
page: *Page,
|
||||||
|
|
||||||
|
fn checkRules(ctx: @This(), rules: *const RuleList) void {
|
||||||
|
if (ctx.display_priority.* == INLINE_PRIORITY and
|
||||||
|
ctx.visibility_priority.* == INLINE_PRIORITY and
|
||||||
|
ctx.opacity_priority.* == INLINE_PRIORITY)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const priorities = rules.items(.priority);
|
||||||
|
const props_list = rules.items(.props);
|
||||||
|
const selectors = rules.items(.selector);
|
||||||
|
|
||||||
|
for (priorities, props_list, selectors) |p, props, selector| {
|
||||||
|
// Fast skip using packed u64 priority
|
||||||
|
if (p <= ctx.display_priority.* and p <= ctx.visibility_priority.* and p <= ctx.opacity_priority.*) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic for property dominance
|
||||||
|
const dominated = (props.display_none == null or p <= ctx.display_priority.*) and
|
||||||
|
(props.visibility_hidden == null or p <= ctx.visibility_priority.*) and
|
||||||
|
(props.opacity_zero == null or p <= ctx.opacity_priority.*);
|
||||||
|
|
||||||
|
if (dominated) continue;
|
||||||
|
|
||||||
|
if (matchesSelector(ctx.el, selector, ctx.page)) {
|
||||||
|
// Update best priorities
|
||||||
|
if (props.display_none != null and p > ctx.display_priority.*) {
|
||||||
|
ctx.display_none.* = props.display_none;
|
||||||
|
ctx.display_priority.* = p;
|
||||||
|
}
|
||||||
|
if (props.visibility_hidden != null and p > ctx.visibility_priority.*) {
|
||||||
|
ctx.visibility_hidden.* = props.visibility_hidden;
|
||||||
|
ctx.visibility_priority.* = p;
|
||||||
|
}
|
||||||
|
if (props.opacity_zero != null and p > ctx.opacity_priority.*) {
|
||||||
|
ctx.opacity_zero.* = props.opacity_zero;
|
||||||
|
ctx.opacity_priority.* = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const ctx = Ctx{
|
||||||
|
.display_none = &display_none,
|
||||||
|
.display_priority = &display_priority,
|
||||||
|
.visibility_hidden = &visibility_hidden,
|
||||||
|
.visibility_priority = &visibility_priority,
|
||||||
|
.opacity_zero = &opacity_zero,
|
||||||
|
.opacity_priority = &opacity_priority,
|
||||||
|
.el = el,
|
||||||
|
.page = self.page,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
|
||||||
|
if (self.id_rules.get(id)) |rules| {
|
||||||
|
ctx.checkRules(&rules);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("class"))) |class_attr| {
|
||||||
|
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
|
||||||
|
while (it.next()) |class| {
|
||||||
|
if (self.class_rules.get(class)) |rules| {
|
||||||
|
ctx.checkRules(&rules);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.tag_rules.get(el.getTag())) |rules| {
|
||||||
|
ctx.checkRules(&rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.checkRules(&self.other_rules);
|
||||||
|
|
||||||
|
return (display_none orelse false) or (visibility_hidden orelse false) or (opacity_zero orelse false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an element has pointer-events:none.
|
||||||
|
/// Checks inline style first - if set, skips stylesheet lookup.
|
||||||
|
/// Walks up the tree to check ancestors.
|
||||||
|
pub fn hasPointerEventsNone(self: *StyleManager, el: *Element, cache: ?*PointerEventsCache) bool {
|
||||||
|
self.rebuildIfDirty() catch return false;
|
||||||
|
|
||||||
|
var current: ?*Element = el;
|
||||||
|
|
||||||
|
while (current) |elem| {
|
||||||
|
// Check cache first
|
||||||
|
if (cache) |c| {
|
||||||
|
if (c.get(elem)) |pe_none| {
|
||||||
|
if (pe_none) return true;
|
||||||
|
current = elem.parentElement();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pe_none = self.elementHasPointerEventsNone(elem);
|
||||||
|
|
||||||
|
if (cache) |c| {
|
||||||
|
c.put(self.page.call_arena, elem, pe_none) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pe_none) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = elem.parentElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a single element (not ancestors) has pointer-events:none.
|
||||||
|
fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
|
||||||
|
const page = self.page;
|
||||||
|
|
||||||
|
// Check inline style first
|
||||||
|
if (getInlineStyleProperty(el, .wrap("pointer-events"), page)) |property| {
|
||||||
|
if (property._value.eql(comptime .wrap("none"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result: ?bool = null;
|
||||||
|
var best_priority: u64 = 0;
|
||||||
|
|
||||||
|
// Helper to check a single rule
|
||||||
|
const checkRules = struct {
|
||||||
|
fn check(rules: *const RuleList, res: *?bool, current_priority: *u64, elem: *Element, p: *Page) void {
|
||||||
|
if (current_priority.* == INLINE_PRIORITY) return;
|
||||||
|
|
||||||
|
const priorities = rules.items(.priority);
|
||||||
|
const props_list = rules.items(.props);
|
||||||
|
const selectors = rules.items(.selector);
|
||||||
|
|
||||||
|
for (priorities, props_list, selectors) |priority, props, selector| {
|
||||||
|
if (priority <= current_priority.*) continue;
|
||||||
|
if (props.pointer_events_none == null) continue;
|
||||||
|
|
||||||
|
if (matchesSelector(elem, selector, p)) {
|
||||||
|
res.* = props.pointer_events_none;
|
||||||
|
current_priority.* = priority;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.check;
|
||||||
|
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
|
||||||
|
if (self.id_rules.get(id)) |rules| {
|
||||||
|
checkRules(&rules, &result, &best_priority, el, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("class"))) |class_attr| {
|
||||||
|
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
|
||||||
|
while (it.next()) |class| {
|
||||||
|
if (self.class_rules.get(class)) |rules| {
|
||||||
|
checkRules(&rules, &result, &best_priority, el, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.tag_rules.get(el.getTag())) |rules| {
|
||||||
|
checkRules(&rules, &result, &best_priority, el, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkRules(&self.other_rules, &result, &best_priority, el, page);
|
||||||
|
|
||||||
|
return result orelse false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts visibility-relevant rules from a CSS rule.
|
||||||
|
// Creates one VisibilityRule per selector (not per selector list) so each has correct specificity.
|
||||||
|
// Buckets rules by their rightmost selector part for fast lookup.
|
||||||
|
fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !void {
|
||||||
|
const selector_text = style_rule._selector_text;
|
||||||
|
if (selector_text.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the rule has visibility-relevant properties
|
||||||
|
const style = style_rule._style orelse return;
|
||||||
|
const props = extractVisibilityProperties(style);
|
||||||
|
if (!props.isRelevant()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the selector list
|
||||||
|
const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return;
|
||||||
|
if (selectors.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create one rule per selector - each has its own specificity
|
||||||
|
// e.g., "#id, .class { display: none }" becomes two rules with different specificities
|
||||||
|
for (selectors) |selector| {
|
||||||
|
// Get the rightmost compound (last segment, or first if no segments)
|
||||||
|
const rightmost = if (selector.segments.len > 0)
|
||||||
|
selector.segments[selector.segments.len - 1].compound
|
||||||
|
else
|
||||||
|
selector.first;
|
||||||
|
|
||||||
|
// Find the bucketing key from rightmost compound
|
||||||
|
const bucket_key = getBucketKey(rightmost) orelse continue; // skip if dynamic pseudo-class
|
||||||
|
|
||||||
|
const rule = VisibilityRule{
|
||||||
|
.props = props,
|
||||||
|
.selector = selector,
|
||||||
|
.priority = (@as(u64, computeSpecificity(selector)) << 32) | @as(u64, self.next_doc_order),
|
||||||
|
};
|
||||||
|
self.next_doc_order += 1;
|
||||||
|
|
||||||
|
// Add to appropriate bucket
|
||||||
|
switch (bucket_key) {
|
||||||
|
.id => |id| {
|
||||||
|
const gop = try self.id_rules.getOrPut(self.arena, id);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.class => |class| {
|
||||||
|
const gop = try self.class_rules.getOrPut(self.arena, class);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.tag => |tag| {
|
||||||
|
const gop = try self.tag_rules.getOrPut(self.arena, tag);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .{};
|
||||||
|
try gop.value_ptr.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
.other => {
|
||||||
|
try self.other_rules.append(self.arena, rule);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BucketKey = union(enum) {
|
||||||
|
id: []const u8,
|
||||||
|
class: []const u8,
|
||||||
|
tag: Tag,
|
||||||
|
other,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Returns the best bucket key for a compound selector, or null if it contains
|
||||||
|
/// a dynamic pseudo-class we should skip (hover, active, focus, etc.)
|
||||||
|
/// Priority: id > class > tag > other
|
||||||
|
fn getBucketKey(compound: Selector.Compound) ?BucketKey {
|
||||||
|
var best_key: BucketKey = .other;
|
||||||
|
|
||||||
|
for (compound.parts) |part| {
|
||||||
|
switch (part) {
|
||||||
|
.id => |id| {
|
||||||
|
best_key = .{ .id = id };
|
||||||
|
},
|
||||||
|
.class => |class| {
|
||||||
|
if (best_key != .id) {
|
||||||
|
best_key = .{ .class = class };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.tag => |tag| {
|
||||||
|
if (best_key == .other) {
|
||||||
|
best_key = .{ .tag = tag };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.tag_name => {
|
||||||
|
// Custom tag - put in other bucket (can't efficiently look up)
|
||||||
|
// Keep current best_key if we have something better
|
||||||
|
},
|
||||||
|
.pseudo_class => |pc| {
|
||||||
|
// Skip dynamic pseudo-classes - they depend on interaction state
|
||||||
|
switch (pc) {
|
||||||
|
.hover, .active, .focus, .focus_within, .focus_visible, .visited, .target => {
|
||||||
|
return null; // Skip this selector entirely
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.universal, .attribute => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts visibility-relevant properties from a style declaration.
|
||||||
|
fn extractVisibilityProperties(style: *CSSStyleProperties) VisibilityProperties {
|
||||||
|
var props = VisibilityProperties{};
|
||||||
|
const decl = style.asCSSStyleDeclaration();
|
||||||
|
|
||||||
|
if (decl.findProperty(comptime .wrap("display"))) |property| {
|
||||||
|
props.display_none = property._value.eql(comptime .wrap("none"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decl.findProperty(comptime .wrap("visibility"))) |property| {
|
||||||
|
props.visibility_hidden = property._value.eql(comptime .wrap("hidden")) or property._value.eql(comptime .wrap("collapse"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decl.findProperty(comptime .wrap("opacity"))) |property| {
|
||||||
|
props.opacity_zero = property._value.eql(comptime .wrap("0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decl.findProperty(.wrap("pointer-events"))) |property| {
|
||||||
|
props.pointer_events_none = property._value.eql(comptime .wrap("none"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes CSS specificity for a selector.
|
||||||
|
// Returns packed value: (id_count << 20) | (class_count << 10) | element_count
|
||||||
|
pub fn computeSpecificity(selector: Selector.Selector) u32 {
|
||||||
|
var ids: u32 = 0;
|
||||||
|
var classes: u32 = 0; // includes classes, attributes, pseudo-classes
|
||||||
|
var elements: u32 = 0; // includes elements, pseudo-elements
|
||||||
|
|
||||||
|
// Count specificity for first compound
|
||||||
|
countCompoundSpecificity(selector.first, &ids, &classes, &elements);
|
||||||
|
|
||||||
|
// Count specificity for subsequent segments
|
||||||
|
for (selector.segments) |segment| {
|
||||||
|
countCompoundSpecificity(segment.compound, &ids, &classes, &elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack into single u32: (ids << 20) | (classes << 10) | elements
|
||||||
|
// This gives us 10 bits each, supporting up to 1023 of each type
|
||||||
|
return (@as(u32, @min(ids, 1023)) << 20) | (@as(u32, @min(classes, 1023)) << 10) | @min(elements, 1023);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn countCompoundSpecificity(compound: Selector.Compound, ids: *u32, classes: *u32, elements: *u32) void {
|
||||||
|
for (compound.parts) |part| {
|
||||||
|
switch (part) {
|
||||||
|
.id => ids.* += 1,
|
||||||
|
.class => classes.* += 1,
|
||||||
|
.tag, .tag_name => elements.* += 1,
|
||||||
|
.universal => {}, // zero specificity
|
||||||
|
.attribute => classes.* += 1,
|
||||||
|
.pseudo_class => |pc| {
|
||||||
|
switch (pc) {
|
||||||
|
// :where() has zero specificity
|
||||||
|
.where => {},
|
||||||
|
// :not(), :is(), :has() take specificity of their most specific argument
|
||||||
|
.not, .is, .has => |nested| {
|
||||||
|
var max_nested: u32 = 0;
|
||||||
|
for (nested) |nested_sel| {
|
||||||
|
const spec = computeSpecificity(nested_sel);
|
||||||
|
if (spec > max_nested) max_nested = spec;
|
||||||
|
}
|
||||||
|
// Unpack and add to our counts
|
||||||
|
ids.* += (max_nested >> 20) & 0x3FF;
|
||||||
|
classes.* += (max_nested >> 10) & 0x3FF;
|
||||||
|
elements.* += max_nested & 0x3FF;
|
||||||
|
},
|
||||||
|
// All other pseudo-classes count as class-level specificity
|
||||||
|
else => classes.* += 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matchesSelector(el: *Element, selector: Selector.Selector, page: *Page) bool {
|
||||||
|
const node = el.asNode();
|
||||||
|
return SelectorList.matches(node, selector, node, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const VisibilityProperties = struct {
|
||||||
|
display_none: ?bool = null,
|
||||||
|
visibility_hidden: ?bool = null,
|
||||||
|
opacity_zero: ?bool = null,
|
||||||
|
pointer_events_none: ?bool = null,
|
||||||
|
|
||||||
|
// returne true if any field in VisibilityProperties is not null
|
||||||
|
fn isRelevant(self: VisibilityProperties) bool {
|
||||||
|
return self.display_none != null or
|
||||||
|
self.visibility_hidden != null or
|
||||||
|
self.opacity_zero != null or
|
||||||
|
self.pointer_events_none != null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const VisibilityRule = struct {
|
||||||
|
selector: Selector.Selector, // Single selector, not a list
|
||||||
|
props: VisibilityProperties,
|
||||||
|
|
||||||
|
// Packed priority: (specificity << 32) | doc_order
|
||||||
|
priority: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CheckVisibilityOptions = struct {
|
||||||
|
check_opacity: bool = false,
|
||||||
|
check_visibility: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inline styles always win over stylesheets - use max u64 as sentinel
|
||||||
|
const INLINE_PRIORITY: u64 = std.math.maxInt(u64);
|
||||||
|
|
||||||
|
fn getInlineStyleProperty(el: *Element, property_name: String, page: *Page) ?*CSSStyleProperty {
|
||||||
|
const style = el.getOrCreateStyle(page) catch |err| {
|
||||||
|
log.err(.browser, "StyleManager getOrCreateStyle", .{ .err = err });
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return style.asCSSStyleDeclaration().findProperty(property_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
test "StyleManager: computeSpecificity: element selector" {
|
||||||
|
// div -> (0, 0, 1)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .tag = .div }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: class selector" {
|
||||||
|
// .foo -> (0, 1, 0)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 10, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: id selector" {
|
||||||
|
// #bar -> (1, 0, 0)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .id = "bar" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 20, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: combined selector" {
|
||||||
|
// div.foo#bar -> (1, 1, 1)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .tag = .div },
|
||||||
|
.{ .class = "foo" },
|
||||||
|
.{ .id = "bar" },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual((1 << 20) | (1 << 10) | 1, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: universal selector" {
|
||||||
|
// * -> (0, 0, 0)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.universal} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(0, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: multiple classes" {
|
||||||
|
// .a.b.c -> (0, 3, 0)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .class = "a" },
|
||||||
|
.{ .class = "b" },
|
||||||
|
.{ .class = "c" },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(3 << 10, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: descendant combinator" {
|
||||||
|
// div span -> (0, 0, 2)
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .tag = .div }} },
|
||||||
|
.segments = &.{
|
||||||
|
.{ .combinator = .descendant, .compound = .{ .parts = &.{.{ .tag = .span }} } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(2, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: :where() has zero specificity" {
|
||||||
|
// :where(.foo) -> (0, 0, 0) regardless of what's inside
|
||||||
|
const inner_selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .pseudo_class = .{ .where = &.{inner_selector} } },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(0, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: :not() takes inner specificity" {
|
||||||
|
// :not(.foo) -> (0, 1, 0) - takes specificity of .foo
|
||||||
|
const inner_selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .pseudo_class = .{ .not = &.{inner_selector} } },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 10, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: :is() takes most specific inner" {
|
||||||
|
// :is(.foo, #bar) -> (1, 0, 0) - takes the most specific (#bar)
|
||||||
|
const class_selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .class = "foo" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
const id_selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{.{ .id = "bar" }} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .pseudo_class = .{ .is = &.{ class_selector, id_selector } } },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 20, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: computeSpecificity: pseudo-class (general)" {
|
||||||
|
// :hover -> (0, 1, 0) - pseudo-classes count as class-level
|
||||||
|
const selector = Selector.Selector{
|
||||||
|
.first = .{ .parts = &.{
|
||||||
|
.{ .pseudo_class = .hover },
|
||||||
|
} },
|
||||||
|
.segments = &.{},
|
||||||
|
};
|
||||||
|
try testing.expectEqual(1 << 10, computeSpecificity(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "StyleManager: document order tie-breaking" {
|
||||||
|
// When specificity is equal, higher doc_order (later in document) wins
|
||||||
|
const beats = struct {
|
||||||
|
fn f(spec: u32, doc_order: u32, best_spec: u32, best_doc_order: u32) bool {
|
||||||
|
return spec > best_spec or (spec == best_spec and doc_order > best_doc_order);
|
||||||
|
}
|
||||||
|
}.f;
|
||||||
|
|
||||||
|
// Higher specificity always wins regardless of doc_order
|
||||||
|
try testing.expect(beats(2, 0, 1, 10));
|
||||||
|
try testing.expect(!beats(1, 10, 2, 0));
|
||||||
|
|
||||||
|
// Equal specificity: higher doc_order wins
|
||||||
|
try testing.expect(beats(1, 5, 1, 3)); // doc_order 5 > 3
|
||||||
|
try testing.expect(!beats(1, 3, 1, 5)); // doc_order 3 < 5
|
||||||
|
|
||||||
|
// Equal specificity and doc_order: no win
|
||||||
|
try testing.expect(!beats(1, 5, 1, 5));
|
||||||
|
}
|
||||||
1004
src/browser/URL.zig
1004
src/browser/URL.zig
File diff suppressed because it is too large
Load Diff
137
src/browser/actions.zig
Normal file
137
src/browser/actions.zig
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const lp = @import("../lightpanda.zig");
|
||||||
|
const DOMNode = @import("webapi/Node.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Event = @import("webapi/Event.zig");
|
||||||
|
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const Session = @import("Session.zig");
|
||||||
|
const Selector = @import("webapi/selector/Selector.zig");
|
||||||
|
|
||||||
|
pub fn click(node: *DOMNode, page: *Page) !void {
|
||||||
|
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||||
|
|
||||||
|
const mouse_event: *MouseEvent = try .initTrusted(comptime .wrap("click"), .{
|
||||||
|
.bubbles = true,
|
||||||
|
.cancelable = true,
|
||||||
|
.composed = true,
|
||||||
|
.clientX = 0,
|
||||||
|
.clientY = 0,
|
||||||
|
}, page);
|
||||||
|
|
||||||
|
page._event_manager.dispatch(el.asEventTarget(), mouse_event.asEvent()) catch |err| {
|
||||||
|
lp.log.err(.app, "click failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill(node: *DOMNode, text: []const u8, page: *Page) !void {
|
||||||
|
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||||
|
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
input.setValue(text, page) catch |err| {
|
||||||
|
lp.log.err(.app, "fill input failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
} else if (el.is(Element.Html.TextArea)) |textarea| {
|
||||||
|
textarea.setValue(text, page) catch |err| {
|
||||||
|
lp.log.err(.app, "fill textarea failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
} else if (el.is(Element.Html.Select)) |select| {
|
||||||
|
select.setValue(text, page) catch |err| {
|
||||||
|
lp.log.err(.app, "fill select failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return error.InvalidNodeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, page);
|
||||||
|
page._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| {
|
||||||
|
lp.log.err(.app, "dispatch input event failed", .{ .err = err });
|
||||||
|
};
|
||||||
|
|
||||||
|
const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, page);
|
||||||
|
page._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| {
|
||||||
|
lp.log.err(.app, "dispatch change event failed", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, page: *Page) !void {
|
||||||
|
if (node) |n| {
|
||||||
|
const el = n.is(Element) orelse return error.InvalidNodeType;
|
||||||
|
|
||||||
|
if (x) |val| {
|
||||||
|
el.setScrollLeft(val, page) catch |err| {
|
||||||
|
lp.log.err(.app, "setScrollLeft failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (y) |val| {
|
||||||
|
el.setScrollTop(val, page) catch |err| {
|
||||||
|
lp.log.err(.app, "setScrollTop failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const scroll_evt: *Event = try .initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, page);
|
||||||
|
page._event_manager.dispatch(el.asEventTarget(), scroll_evt) catch |err| {
|
||||||
|
lp.log.err(.app, "dispatch scroll event failed", .{ .err = err });
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
page.window.scrollTo(.{ .x = x orelse 0 }, y, page) catch |err| {
|
||||||
|
lp.log.err(.app, "scroll failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn waitForSelector(selector: [:0]const u8, timeout_ms: u32, session: *Session) !*DOMNode {
|
||||||
|
var timer = try std.time.Timer.start();
|
||||||
|
var runner = try session.runner(.{});
|
||||||
|
try runner.wait(.{ .ms = timeout_ms, .until = .load });
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const page = runner.page;
|
||||||
|
const element = Selector.querySelector(page.document.asNode(), selector, page) catch {
|
||||||
|
return error.InvalidSelector;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (element) |el| {
|
||||||
|
return el.asNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
|
||||||
|
if (elapsed >= timeout_ms) {
|
||||||
|
return error.Timeout;
|
||||||
|
}
|
||||||
|
switch (try runner.tick(.{ .ms = timeout_ms - elapsed })) {
|
||||||
|
.done => return error.Timeout,
|
||||||
|
.ok => |recommended_sleep_ms| {
|
||||||
|
if (recommended_sleep_ms > 0) {
|
||||||
|
// guanrateed to be <= 20ms
|
||||||
|
std.Thread.sleep(std.time.ns_per_ms * recommended_sleep_ms);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
298
src/browser/color.zig
Normal file
298
src/browser/color.zig
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
|
||||||
|
pub fn isHexColor(value: []const u8) bool {
|
||||||
|
if (value.len == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value[0] != '#') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hex_part = value[1..];
|
||||||
|
switch (hex_part.len) {
|
||||||
|
3, 4, 6, 8 => for (hex_part) |c| if (!std.ascii.isHex(c)) return false,
|
||||||
|
else => return false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const RGBA = packed struct(u32) {
|
||||||
|
r: u8,
|
||||||
|
g: u8,
|
||||||
|
b: u8,
|
||||||
|
/// Opaque by default.
|
||||||
|
a: u8 = std.math.maxInt(u8),
|
||||||
|
|
||||||
|
pub const Named = struct {
|
||||||
|
// Basic colors (CSS Level 1)
|
||||||
|
pub const black: RGBA = .init(0, 0, 0, 1);
|
||||||
|
pub const silver: RGBA = .init(192, 192, 192, 1);
|
||||||
|
pub const gray: RGBA = .init(128, 128, 128, 1);
|
||||||
|
pub const white: RGBA = .init(255, 255, 255, 1);
|
||||||
|
pub const maroon: RGBA = .init(128, 0, 0, 1);
|
||||||
|
pub const red: RGBA = .init(255, 0, 0, 1);
|
||||||
|
pub const purple: RGBA = .init(128, 0, 128, 1);
|
||||||
|
pub const fuchsia: RGBA = .init(255, 0, 255, 1);
|
||||||
|
pub const green: RGBA = .init(0, 128, 0, 1);
|
||||||
|
pub const lime: RGBA = .init(0, 255, 0, 1);
|
||||||
|
pub const olive: RGBA = .init(128, 128, 0, 1);
|
||||||
|
pub const yellow: RGBA = .init(255, 255, 0, 1);
|
||||||
|
pub const navy: RGBA = .init(0, 0, 128, 1);
|
||||||
|
pub const blue: RGBA = .init(0, 0, 255, 1);
|
||||||
|
pub const teal: RGBA = .init(0, 128, 128, 1);
|
||||||
|
pub const aqua: RGBA = .init(0, 255, 255, 1);
|
||||||
|
|
||||||
|
// Extended colors (CSS Level 2+)
|
||||||
|
pub const aliceblue: RGBA = .init(240, 248, 255, 1);
|
||||||
|
pub const antiquewhite: RGBA = .init(250, 235, 215, 1);
|
||||||
|
pub const aquamarine: RGBA = .init(127, 255, 212, 1);
|
||||||
|
pub const azure: RGBA = .init(240, 255, 255, 1);
|
||||||
|
pub const beige: RGBA = .init(245, 245, 220, 1);
|
||||||
|
pub const bisque: RGBA = .init(255, 228, 196, 1);
|
||||||
|
pub const blanchedalmond: RGBA = .init(255, 235, 205, 1);
|
||||||
|
pub const blueviolet: RGBA = .init(138, 43, 226, 1);
|
||||||
|
pub const brown: RGBA = .init(165, 42, 42, 1);
|
||||||
|
pub const burlywood: RGBA = .init(222, 184, 135, 1);
|
||||||
|
pub const cadetblue: RGBA = .init(95, 158, 160, 1);
|
||||||
|
pub const chartreuse: RGBA = .init(127, 255, 0, 1);
|
||||||
|
pub const chocolate: RGBA = .init(210, 105, 30, 1);
|
||||||
|
pub const coral: RGBA = .init(255, 127, 80, 1);
|
||||||
|
pub const cornflowerblue: RGBA = .init(100, 149, 237, 1);
|
||||||
|
pub const cornsilk: RGBA = .init(255, 248, 220, 1);
|
||||||
|
pub const crimson: RGBA = .init(220, 20, 60, 1);
|
||||||
|
pub const cyan: RGBA = .init(0, 255, 255, 1); // Synonym of aqua
|
||||||
|
pub const darkblue: RGBA = .init(0, 0, 139, 1);
|
||||||
|
pub const darkcyan: RGBA = .init(0, 139, 139, 1);
|
||||||
|
pub const darkgoldenrod: RGBA = .init(184, 134, 11, 1);
|
||||||
|
pub const darkgray: RGBA = .init(169, 169, 169, 1);
|
||||||
|
pub const darkgreen: RGBA = .init(0, 100, 0, 1);
|
||||||
|
pub const darkgrey: RGBA = .init(169, 169, 169, 1); // Synonym of darkgray
|
||||||
|
pub const darkkhaki: RGBA = .init(189, 183, 107, 1);
|
||||||
|
pub const darkmagenta: RGBA = .init(139, 0, 139, 1);
|
||||||
|
pub const darkolivegreen: RGBA = .init(85, 107, 47, 1);
|
||||||
|
pub const darkorange: RGBA = .init(255, 140, 0, 1);
|
||||||
|
pub const darkorchid: RGBA = .init(153, 50, 204, 1);
|
||||||
|
pub const darkred: RGBA = .init(139, 0, 0, 1);
|
||||||
|
pub const darksalmon: RGBA = .init(233, 150, 122, 1);
|
||||||
|
pub const darkseagreen: RGBA = .init(143, 188, 143, 1);
|
||||||
|
pub const darkslateblue: RGBA = .init(72, 61, 139, 1);
|
||||||
|
pub const darkslategray: RGBA = .init(47, 79, 79, 1);
|
||||||
|
pub const darkslategrey: RGBA = .init(47, 79, 79, 1); // Synonym of darkslategray
|
||||||
|
pub const darkturquoise: RGBA = .init(0, 206, 209, 1);
|
||||||
|
pub const darkviolet: RGBA = .init(148, 0, 211, 1);
|
||||||
|
pub const deeppink: RGBA = .init(255, 20, 147, 1);
|
||||||
|
pub const deepskyblue: RGBA = .init(0, 191, 255, 1);
|
||||||
|
pub const dimgray: RGBA = .init(105, 105, 105, 1);
|
||||||
|
pub const dimgrey: RGBA = .init(105, 105, 105, 1); // Synonym of dimgray
|
||||||
|
pub const dodgerblue: RGBA = .init(30, 144, 255, 1);
|
||||||
|
pub const firebrick: RGBA = .init(178, 34, 34, 1);
|
||||||
|
pub const floralwhite: RGBA = .init(255, 250, 240, 1);
|
||||||
|
pub const forestgreen: RGBA = .init(34, 139, 34, 1);
|
||||||
|
pub const gainsboro: RGBA = .init(220, 220, 220, 1);
|
||||||
|
pub const ghostwhite: RGBA = .init(248, 248, 255, 1);
|
||||||
|
pub const gold: RGBA = .init(255, 215, 0, 1);
|
||||||
|
pub const goldenrod: RGBA = .init(218, 165, 32, 1);
|
||||||
|
pub const greenyellow: RGBA = .init(173, 255, 47, 1);
|
||||||
|
pub const grey: RGBA = .init(128, 128, 128, 1); // Synonym of gray
|
||||||
|
pub const honeydew: RGBA = .init(240, 255, 240, 1);
|
||||||
|
pub const hotpink: RGBA = .init(255, 105, 180, 1);
|
||||||
|
pub const indianred: RGBA = .init(205, 92, 92, 1);
|
||||||
|
pub const indigo: RGBA = .init(75, 0, 130, 1);
|
||||||
|
pub const ivory: RGBA = .init(255, 255, 240, 1);
|
||||||
|
pub const khaki: RGBA = .init(240, 230, 140, 1);
|
||||||
|
pub const lavender: RGBA = .init(230, 230, 250, 1);
|
||||||
|
pub const lavenderblush: RGBA = .init(255, 240, 245, 1);
|
||||||
|
pub const lawngreen: RGBA = .init(124, 252, 0, 1);
|
||||||
|
pub const lemonchiffon: RGBA = .init(255, 250, 205, 1);
|
||||||
|
pub const lightblue: RGBA = .init(173, 216, 230, 1);
|
||||||
|
pub const lightcoral: RGBA = .init(240, 128, 128, 1);
|
||||||
|
pub const lightcyan: RGBA = .init(224, 255, 255, 1);
|
||||||
|
pub const lightgoldenrodyellow: RGBA = .init(250, 250, 210, 1);
|
||||||
|
pub const lightgray: RGBA = .init(211, 211, 211, 1);
|
||||||
|
pub const lightgreen: RGBA = .init(144, 238, 144, 1);
|
||||||
|
pub const lightgrey: RGBA = .init(211, 211, 211, 1); // Synonym of lightgray
|
||||||
|
pub const lightpink: RGBA = .init(255, 182, 193, 1);
|
||||||
|
pub const lightsalmon: RGBA = .init(255, 160, 122, 1);
|
||||||
|
pub const lightseagreen: RGBA = .init(32, 178, 170, 1);
|
||||||
|
pub const lightskyblue: RGBA = .init(135, 206, 250, 1);
|
||||||
|
pub const lightslategray: RGBA = .init(119, 136, 153, 1);
|
||||||
|
pub const lightslategrey: RGBA = .init(119, 136, 153, 1); // Synonym of lightslategray
|
||||||
|
pub const lightsteelblue: RGBA = .init(176, 196, 222, 1);
|
||||||
|
pub const lightyellow: RGBA = .init(255, 255, 224, 1);
|
||||||
|
pub const limegreen: RGBA = .init(50, 205, 50, 1);
|
||||||
|
pub const linen: RGBA = .init(250, 240, 230, 1);
|
||||||
|
pub const magenta: RGBA = .init(255, 0, 255, 1); // Synonym of fuchsia
|
||||||
|
pub const mediumaquamarine: RGBA = .init(102, 205, 170, 1);
|
||||||
|
pub const mediumblue: RGBA = .init(0, 0, 205, 1);
|
||||||
|
pub const mediumorchid: RGBA = .init(186, 85, 211, 1);
|
||||||
|
pub const mediumpurple: RGBA = .init(147, 112, 219, 1);
|
||||||
|
pub const mediumseagreen: RGBA = .init(60, 179, 113, 1);
|
||||||
|
pub const mediumslateblue: RGBA = .init(123, 104, 238, 1);
|
||||||
|
pub const mediumspringgreen: RGBA = .init(0, 250, 154, 1);
|
||||||
|
pub const mediumturquoise: RGBA = .init(72, 209, 204, 1);
|
||||||
|
pub const mediumvioletred: RGBA = .init(199, 21, 133, 1);
|
||||||
|
pub const midnightblue: RGBA = .init(25, 25, 112, 1);
|
||||||
|
pub const mintcream: RGBA = .init(245, 255, 250, 1);
|
||||||
|
pub const mistyrose: RGBA = .init(255, 228, 225, 1);
|
||||||
|
pub const moccasin: RGBA = .init(255, 228, 181, 1);
|
||||||
|
pub const navajowhite: RGBA = .init(255, 222, 173, 1);
|
||||||
|
pub const oldlace: RGBA = .init(253, 245, 230, 1);
|
||||||
|
pub const olivedrab: RGBA = .init(107, 142, 35, 1);
|
||||||
|
pub const orange: RGBA = .init(255, 165, 0, 1);
|
||||||
|
pub const orangered: RGBA = .init(255, 69, 0, 1);
|
||||||
|
pub const orchid: RGBA = .init(218, 112, 214, 1);
|
||||||
|
pub const palegoldenrod: RGBA = .init(238, 232, 170, 1);
|
||||||
|
pub const palegreen: RGBA = .init(152, 251, 152, 1);
|
||||||
|
pub const paleturquoise: RGBA = .init(175, 238, 238, 1);
|
||||||
|
pub const palevioletred: RGBA = .init(219, 112, 147, 1);
|
||||||
|
pub const papayawhip: RGBA = .init(255, 239, 213, 1);
|
||||||
|
pub const peachpuff: RGBA = .init(255, 218, 185, 1);
|
||||||
|
pub const peru: RGBA = .init(205, 133, 63, 1);
|
||||||
|
pub const pink: RGBA = .init(255, 192, 203, 1);
|
||||||
|
pub const plum: RGBA = .init(221, 160, 221, 1);
|
||||||
|
pub const powderblue: RGBA = .init(176, 224, 230, 1);
|
||||||
|
pub const rebeccapurple: RGBA = .init(102, 51, 153, 1);
|
||||||
|
pub const rosybrown: RGBA = .init(188, 143, 143, 1);
|
||||||
|
pub const royalblue: RGBA = .init(65, 105, 225, 1);
|
||||||
|
pub const saddlebrown: RGBA = .init(139, 69, 19, 1);
|
||||||
|
pub const salmon: RGBA = .init(250, 128, 114, 1);
|
||||||
|
pub const sandybrown: RGBA = .init(244, 164, 96, 1);
|
||||||
|
pub const seagreen: RGBA = .init(46, 139, 87, 1);
|
||||||
|
pub const seashell: RGBA = .init(255, 245, 238, 1);
|
||||||
|
pub const sienna: RGBA = .init(160, 82, 45, 1);
|
||||||
|
pub const skyblue: RGBA = .init(135, 206, 235, 1);
|
||||||
|
pub const slateblue: RGBA = .init(106, 90, 205, 1);
|
||||||
|
pub const slategray: RGBA = .init(112, 128, 144, 1);
|
||||||
|
pub const slategrey: RGBA = .init(112, 128, 144, 1); // Synonym of slategray
|
||||||
|
pub const snow: RGBA = .init(255, 250, 250, 1);
|
||||||
|
pub const springgreen: RGBA = .init(0, 255, 127, 1);
|
||||||
|
pub const steelblue: RGBA = .init(70, 130, 180, 1);
|
||||||
|
pub const tan: RGBA = .init(210, 180, 140, 1);
|
||||||
|
pub const thistle: RGBA = .init(216, 191, 216, 1);
|
||||||
|
pub const tomato: RGBA = .init(255, 99, 71, 1);
|
||||||
|
pub const transparent: RGBA = .init(0, 0, 0, 0);
|
||||||
|
pub const turquoise: RGBA = .init(64, 224, 208, 1);
|
||||||
|
pub const violet: RGBA = .init(238, 130, 238, 1);
|
||||||
|
pub const wheat: RGBA = .init(245, 222, 179, 1);
|
||||||
|
pub const whitesmoke: RGBA = .init(245, 245, 245, 1);
|
||||||
|
pub const yellowgreen: RGBA = .init(154, 205, 50, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(r: u8, g: u8, b: u8, a: f32) RGBA {
|
||||||
|
const clamped = std.math.clamp(a, 0, 1);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = @intFromFloat(clamped * 255) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds a color by its name.
|
||||||
|
pub fn find(name: []const u8) ?RGBA {
|
||||||
|
const match = std.meta.stringToEnum(std.meta.DeclEnum(Named), name) orelse return null;
|
||||||
|
|
||||||
|
return switch (match) {
|
||||||
|
inline else => |comptime_enum| @field(Named, @tagName(comptime_enum)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the given color.
|
||||||
|
/// Currently we only parse hex colors and named colors; other variants
|
||||||
|
/// require CSS evaluation.
|
||||||
|
pub fn parse(input: []const u8) !RGBA {
|
||||||
|
if (!isHexColor(input)) {
|
||||||
|
// Try named colors.
|
||||||
|
return find(input) orelse return error.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slice = input[1..];
|
||||||
|
switch (slice.len) {
|
||||||
|
// This means the digit for a color is repeated.
|
||||||
|
// Given HEX is #f0c, its interpreted the same as #FF00CC.
|
||||||
|
3 => {
|
||||||
|
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
|
||||||
|
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
|
||||||
|
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = 255 };
|
||||||
|
},
|
||||||
|
4 => {
|
||||||
|
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
|
||||||
|
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
|
||||||
|
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
|
||||||
|
const a = try std.fmt.parseInt(u8, &.{ slice[3], slice[3] }, 16);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = a };
|
||||||
|
},
|
||||||
|
// Regular HEX format.
|
||||||
|
6 => {
|
||||||
|
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
|
||||||
|
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
|
||||||
|
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = 255 };
|
||||||
|
},
|
||||||
|
8 => {
|
||||||
|
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
|
||||||
|
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
|
||||||
|
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
|
||||||
|
const a = try std.fmt.parseInt(u8, slice[6..8], 16);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = a };
|
||||||
|
},
|
||||||
|
else => return error.Invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// By default, browsers prefer lowercase formatting.
|
||||||
|
const format_upper = false;
|
||||||
|
|
||||||
|
/// Formats the `Color` according to web expectations.
|
||||||
|
/// If color is opaque, HEX is preferred; RGBA otherwise.
|
||||||
|
pub fn format(self: *const RGBA, writer: *Io.Writer) Io.Writer.Error!void {
|
||||||
|
if (self.isOpaque()) {
|
||||||
|
// Convert RGB to HEX.
|
||||||
|
// https://gristle.tripod.com/hexconv.html
|
||||||
|
// Hexadecimal characters up to 15.
|
||||||
|
const char: []const u8 = "0123456789" ++ if (format_upper) "ABCDEF" else "abcdef";
|
||||||
|
// This variant always prefers 6 digit format, +1 is for hash char.
|
||||||
|
const buffer = [7]u8{
|
||||||
|
'#',
|
||||||
|
char[self.r >> 4],
|
||||||
|
char[self.r & 15],
|
||||||
|
char[self.g >> 4],
|
||||||
|
char[self.g & 15],
|
||||||
|
char[self.b >> 4],
|
||||||
|
char[self.b & 15],
|
||||||
|
};
|
||||||
|
|
||||||
|
return writer.writeAll(&buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer RGBA format for everything else.
|
||||||
|
return writer.print("rgba({d}, {d}, {d}, {d:.2})", .{ self.r, self.g, self.b, self.normalizedAlpha() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if `Color` is opaque.
|
||||||
|
pub inline fn isOpaque(self: *const RGBA) bool {
|
||||||
|
return self.a == std.math.maxInt(u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the normalized alpha value.
|
||||||
|
pub inline fn normalizedAlpha(self: *const RGBA) f32 {
|
||||||
|
return @as(f32, @floatFromInt(self.a)) / 255;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -293,3 +293,191 @@ fn isBang(token: Tokenizer.Token) bool {
|
|||||||
else => false,
|
else => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const Rule = struct {
|
||||||
|
selector: []const u8,
|
||||||
|
block: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn parseStylesheet(input: []const u8) RulesIterator {
|
||||||
|
return RulesIterator.init(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const RulesIterator = struct {
|
||||||
|
input: []const u8,
|
||||||
|
stream: TokenStream,
|
||||||
|
has_skipped_at_rule: bool = false,
|
||||||
|
|
||||||
|
pub fn init(input: []const u8) RulesIterator {
|
||||||
|
return .{
|
||||||
|
.input = input,
|
||||||
|
.stream = TokenStream.init(input),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: *RulesIterator) ?Rule {
|
||||||
|
var selector_start: ?usize = null;
|
||||||
|
var selector_end: ?usize = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const peeked = self.stream.peek() orelse return null;
|
||||||
|
|
||||||
|
if (peeked.token == .curly_bracket_block) {
|
||||||
|
if (selector_start == null) {
|
||||||
|
self.skipBlock();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const open_brace = self.stream.next() orelse return null;
|
||||||
|
const block_start = open_brace.end;
|
||||||
|
var block_end = block_start;
|
||||||
|
|
||||||
|
var depth: usize = 1;
|
||||||
|
while (true) {
|
||||||
|
const span = self.stream.next() orelse {
|
||||||
|
block_end = self.input.len;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if (span.token == .curly_bracket_block) {
|
||||||
|
depth += 1;
|
||||||
|
} else if (span.token == .close_curly_bracket) {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth == 0) {
|
||||||
|
block_end = span.start;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selector = self.input[selector_start.?..selector_end.?];
|
||||||
|
selector = std.mem.trim(u8, selector, &std.ascii.whitespace);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.selector = selector,
|
||||||
|
.block = self.input[block_start..block_end],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peeked.token == .at_keyword) {
|
||||||
|
self.has_skipped_at_rule = true;
|
||||||
|
self.skipAtRule();
|
||||||
|
selector_start = null;
|
||||||
|
selector_end = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selector_start == null and (isWhitespaceOrComment(peeked.token) or isSemicolon(peeked.token))) {
|
||||||
|
_ = self.stream.next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = self.stream.next() orelse return null;
|
||||||
|
if (!isWhitespaceOrComment(span.token)) {
|
||||||
|
if (selector_start == null) selector_start = span.start;
|
||||||
|
selector_end = span.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipBlock(self: *RulesIterator) void {
|
||||||
|
const span = self.stream.next() orelse return;
|
||||||
|
if (span.token != .curly_bracket_block) return;
|
||||||
|
|
||||||
|
var depth: usize = 1;
|
||||||
|
while (true) {
|
||||||
|
const next_span = self.stream.next() orelse return;
|
||||||
|
if (next_span.token == .curly_bracket_block) {
|
||||||
|
depth += 1;
|
||||||
|
} else if (next_span.token == .close_curly_bracket) {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth == 0) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipAtRule(self: *RulesIterator) void {
|
||||||
|
_ = self.stream.next(); // consume @keyword
|
||||||
|
var depth: usize = 0;
|
||||||
|
var saw_block = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const peeked = self.stream.peek() orelse return;
|
||||||
|
if (!saw_block and isSemicolon(peeked.token) and depth == 0) {
|
||||||
|
_ = self.stream.next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = self.stream.next() orelse return;
|
||||||
|
if (isWhitespaceOrComment(span.token)) continue;
|
||||||
|
|
||||||
|
if (span.token == .curly_bracket_block) {
|
||||||
|
depth += 1;
|
||||||
|
saw_block = true;
|
||||||
|
} else if (span.token == .close_curly_bracket) {
|
||||||
|
if (depth > 0) depth -= 1;
|
||||||
|
if (saw_block and depth == 0) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
test "RulesIterator: single rule" {
|
||||||
|
var it = RulesIterator.init(".test { color: red; }");
|
||||||
|
const rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings(".test", rule.selector);
|
||||||
|
try testing.expectEqualStrings(" color: red; ", rule.block);
|
||||||
|
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "RulesIterator: multiple rules" {
|
||||||
|
var it = RulesIterator.init("h1 { margin: 0; } p { padding: 10px; }");
|
||||||
|
|
||||||
|
var rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings("h1", rule.selector);
|
||||||
|
try testing.expectEqualStrings(" margin: 0; ", rule.block);
|
||||||
|
|
||||||
|
rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings("p", rule.selector);
|
||||||
|
try testing.expectEqualStrings(" padding: 10px; ", rule.block);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "RulesIterator: skips at-rules without block" {
|
||||||
|
var it = RulesIterator.init("@import url('style.css'); .test { color: red; }");
|
||||||
|
|
||||||
|
const rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings(".test", rule.selector);
|
||||||
|
try testing.expectEqualStrings(" color: red; ", rule.block);
|
||||||
|
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "RulesIterator: skips at-rules with block" {
|
||||||
|
var it = RulesIterator.init("@media screen { .test { color: blue; } } .test2 { color: green; }");
|
||||||
|
|
||||||
|
const rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings(".test2", rule.selector);
|
||||||
|
try testing.expectEqualStrings(" color: green; ", rule.block);
|
||||||
|
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "RulesIterator: comments and whitespace" {
|
||||||
|
var it = RulesIterator.init(" /* comment */ .test /* comment */ { /* comment */ color: red; } \n\t");
|
||||||
|
|
||||||
|
const rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings(".test", rule.selector);
|
||||||
|
try testing.expectEqualStrings(" /* comment */ color: red; ", rule.block);
|
||||||
|
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "RulesIterator: top-level semicolons" {
|
||||||
|
var it = RulesIterator.init("*{}; ; p{}");
|
||||||
|
var rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings("*", rule.selector);
|
||||||
|
|
||||||
|
rule = it.next() orelse return error.MissingRule;
|
||||||
|
try testing.expectEqualStrings("p", rule.selector);
|
||||||
|
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||||
|
}
|
||||||
|
|||||||
@@ -480,10 +480,11 @@ fn consumeName(self: *Tokenizer) []const u8 {
|
|||||||
self.consumeEscape();
|
self.consumeEscape();
|
||||||
},
|
},
|
||||||
0x0 => self.advance(1),
|
0x0 => self.advance(1),
|
||||||
'\x80'...'\xBF', '\xC0'...'\xEF', '\xF0'...'\xFF' => {
|
'\x80'...'\xFF' => {
|
||||||
// This byte *is* part of a multi-byte code point,
|
// Non-ASCII: advance over the complete UTF-8 code point in one step.
|
||||||
// we’ll end up copying the whole code point before this loop does something else.
|
// Using consumeChar() instead of advance(1) ensures we never land on
|
||||||
self.advance(1);
|
// a continuation byte, which advance() asserts against.
|
||||||
|
self.consumeChar();
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
if (self.hasNonAsciiAt(0)) {
|
if (self.hasNonAsciiAt(0)) {
|
||||||
@@ -583,7 +584,7 @@ fn consumeNumeric(self: *Tokenizer) Token {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.advance(2);
|
self.advance(2);
|
||||||
} else if (self.hasAtLeast(1) and std.ascii.isDigit(self.byteAt(2))) {
|
} else if (self.hasAtLeast(2) and std.ascii.isDigit(self.byteAt(2))) {
|
||||||
self.advance(1);
|
self.advance(1);
|
||||||
} else {
|
} else {
|
||||||
break :blk;
|
break :blk;
|
||||||
|
|||||||
@@ -20,16 +20,15 @@ const std = @import("std");
|
|||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const Node = @import("webapi/Node.zig");
|
const Node = @import("webapi/Node.zig");
|
||||||
const Slot = @import("webapi/element/html/Slot.zig");
|
const Slot = @import("webapi/element/html/Slot.zig");
|
||||||
|
const IFrame = @import("webapi/element/html/IFrame.zig");
|
||||||
|
|
||||||
pub const RootOpts = struct {
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
with_base: bool = false,
|
|
||||||
strip: Opts.Strip = .{},
|
|
||||||
shadow: Opts.Shadow = .rendered,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Opts = struct {
|
pub const Opts = struct {
|
||||||
strip: Strip = .{},
|
with_base: bool = false,
|
||||||
shadow: Shadow = .rendered,
|
with_frames: bool = false,
|
||||||
|
strip: Opts.Strip = .{},
|
||||||
|
shadow: Opts.Shadow = .rendered,
|
||||||
|
|
||||||
pub const Strip = struct {
|
pub const Strip = struct {
|
||||||
js: bool = false,
|
js: bool = false,
|
||||||
@@ -49,18 +48,29 @@ pub const Opts = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn root(doc: *Node.Document, opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void {
|
pub fn root(doc: *Node.Document, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
|
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
|
||||||
try writer.writeAll("<!DOCTYPE html>");
|
blk: {
|
||||||
|
// Ideally we just render the doctype which is part of the document
|
||||||
|
if (doc.asNode().firstChild()) |first| {
|
||||||
|
if (first._type == .document_type) {
|
||||||
|
break :blk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// But if the doc has no child, or the first child isn't a doctype
|
||||||
|
// well force it.
|
||||||
|
try writer.writeAll("<!DOCTYPE html>");
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.with_base) {
|
if (opts.with_base) {
|
||||||
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
|
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
|
||||||
const base = try doc.createElement("base", null, page);
|
const base = try doc.createElement("base", null, page);
|
||||||
try base.setAttributeSafe("base", page.base(), page);
|
try base.setAttributeSafe(comptime .wrap("base"), .wrap(page.base()), page);
|
||||||
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);
|
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return deep(doc.asNode(), .{ .strip = opts.strip, .shadow = opts.shadow }, writer, page);
|
return deep(doc.asNode(), opts, writer, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
||||||
@@ -72,19 +82,19 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
|||||||
.cdata => |cd| {
|
.cdata => |cd| {
|
||||||
if (node.is(Node.CData.Comment)) |_| {
|
if (node.is(Node.CData.Comment)) |_| {
|
||||||
try writer.writeAll("<!--");
|
try writer.writeAll("<!--");
|
||||||
try writer.writeAll(cd.getData());
|
try writer.writeAll(cd.getData().str());
|
||||||
try writer.writeAll("-->");
|
try writer.writeAll("-->");
|
||||||
} else if (node.is(Node.CData.ProcessingInstruction)) |pi| {
|
} else if (node.is(Node.CData.ProcessingInstruction)) |pi| {
|
||||||
try writer.writeAll("<?");
|
try writer.writeAll("<?");
|
||||||
try writer.writeAll(pi._target);
|
try writer.writeAll(pi._target);
|
||||||
try writer.writeAll(" ");
|
try writer.writeAll(" ");
|
||||||
try writer.writeAll(cd.getData());
|
try writer.writeAll(cd.getData().str());
|
||||||
try writer.writeAll("?>");
|
try writer.writeAll("?>");
|
||||||
} else {
|
} else {
|
||||||
if (shouldEscapeText(node._parent)) {
|
if (shouldEscapeText(node._parent)) {
|
||||||
try writeEscapedText(cd.getData(), writer);
|
try writeEscapedText(cd.getData().str(), writer);
|
||||||
} else {
|
} else {
|
||||||
try writer.writeAll(cd.getData());
|
try writer.writeAll(cd.getData().str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -99,7 +109,7 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
|||||||
// to render that "active" content, so when we're trying to render
|
// to render that "active" content, so when we're trying to render
|
||||||
// it, we don't want to skip it.
|
// it, we don't want to skip it.
|
||||||
if ((comptime force_slot == false) and opts.shadow == .rendered) {
|
if ((comptime force_slot == false) and opts.shadow == .rendered) {
|
||||||
if (el.getAttributeSafe("slot")) |_| {
|
if (el.getAttributeSafe(comptime .wrap("slot"))) |_| {
|
||||||
// Skip - will be rendered by the Slot if it's the active container
|
// Skip - will be rendered by the Slot if it's the active container
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -129,7 +139,24 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try children(node, opts, writer, page);
|
if (opts.with_frames and el.is(IFrame) != null) {
|
||||||
|
const frame = el.as(IFrame);
|
||||||
|
if (frame.getContentDocument()) |doc| {
|
||||||
|
// A frame's document should always ahave a page, but
|
||||||
|
// I'm not willing to crash a release build on that assertion.
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(doc._page != null);
|
||||||
|
}
|
||||||
|
if (doc._page) |frame_page| {
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
root(doc, opts, writer, frame_page) catch return error.WriteFailed;
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try children(node, opts, writer, page);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isVoidElement(el)) {
|
if (!isVoidElement(el)) {
|
||||||
try writer.writeAll("</");
|
try writer.writeAll("</");
|
||||||
try writer.writeAll(el.getTagNameDump());
|
try writer.writeAll(el.getTagNameDump());
|
||||||
@@ -161,7 +188,11 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
|||||||
try writer.writeAll(">\n");
|
try writer.writeAll(">\n");
|
||||||
},
|
},
|
||||||
.document_fragment => try children(node, opts, writer, page),
|
.document_fragment => try children(node, opts, writer, page),
|
||||||
.attribute => unreachable,
|
.attribute => {
|
||||||
|
// Not called normally, but can be called via XMLSerializer.serializeToString
|
||||||
|
// in which case it should return an empty string
|
||||||
|
try writer.writeAll("");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,12 +273,12 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
|
|||||||
if (std.mem.eql(u8, tag_name, "noscript")) return true;
|
if (std.mem.eql(u8, tag_name, "noscript")) return true;
|
||||||
|
|
||||||
if (std.mem.eql(u8, tag_name, "link")) {
|
if (std.mem.eql(u8, tag_name, "link")) {
|
||||||
if (el.getAttributeSafe("as")) |as| {
|
if (el.getAttributeSafe(comptime .wrap("as"))) |as| {
|
||||||
if (std.mem.eql(u8, as, "script")) return true;
|
if (std.mem.eql(u8, as, "script")) return true;
|
||||||
}
|
}
|
||||||
if (el.getAttributeSafe("rel")) |rel| {
|
if (el.getAttributeSafe(comptime .wrap("rel"))) |rel| {
|
||||||
if (std.mem.eql(u8, rel, "modulepreload") or std.mem.eql(u8, rel, "preload")) {
|
if (std.mem.eql(u8, rel, "modulepreload") or std.mem.eql(u8, rel, "preload")) {
|
||||||
if (el.getAttributeSafe("as")) |as| {
|
if (el.getAttributeSafe(comptime .wrap("as"))) |as| {
|
||||||
if (std.mem.eql(u8, as, "script")) return true;
|
if (std.mem.eql(u8, as, "script")) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +290,7 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
|
|||||||
if (std.mem.eql(u8, tag_name, "style")) return true;
|
if (std.mem.eql(u8, tag_name, "style")) return true;
|
||||||
|
|
||||||
if (std.mem.eql(u8, tag_name, "link")) {
|
if (std.mem.eql(u8, tag_name, "link")) {
|
||||||
if (el.getAttributeSafe("rel")) |rel| {
|
if (el.getAttributeSafe(comptime .wrap("rel"))) |rel| {
|
||||||
if (std.mem.eql(u8, rel, "stylesheet")) return true;
|
if (std.mem.eql(u8, rel, "stylesheet")) return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,6 +314,12 @@ fn shouldEscapeText(node_: ?*Node) bool {
|
|||||||
if (node.is(Node.Element.Html.Script) != null) {
|
if (node.is(Node.Element.Html.Script) != null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// When scripting is enabled, <noscript> is a raw text element per the HTML spec
|
||||||
|
// (https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments).
|
||||||
|
// Its text content must not be HTML-escaped during serialization.
|
||||||
|
if (node.is(Node.Element.Html.Generic)) |generic| {
|
||||||
|
if (generic._tag == .noscript) return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
fn writeEscapedText(text: []const u8, writer: *std.Io.Writer) !void {
|
fn writeEscapedText(text: []const u8, writer: *std.Io.Writer) !void {
|
||||||
|
|||||||
460
src/browser/forms.zig
Normal file
460
src/browser/forms.zig
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const SelectOption = struct {
|
||||||
|
value: []const u8,
|
||||||
|
text: []const u8,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: *const SelectOption, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("value");
|
||||||
|
try jw.write(self.value);
|
||||||
|
try jw.objectField("text");
|
||||||
|
try jw.write(self.text);
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const FormField = struct {
|
||||||
|
backendNodeId: ?u32 = null,
|
||||||
|
node: *Node,
|
||||||
|
tag_name: []const u8,
|
||||||
|
name: ?[]const u8,
|
||||||
|
input_type: ?[]const u8,
|
||||||
|
required: bool,
|
||||||
|
disabled: bool,
|
||||||
|
value: ?[]const u8,
|
||||||
|
placeholder: ?[]const u8,
|
||||||
|
options: []SelectOption,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: *const FormField, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
|
||||||
|
if (self.backendNodeId) |id| {
|
||||||
|
try jw.objectField("backendNodeId");
|
||||||
|
try jw.write(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.objectField("tagName");
|
||||||
|
try jw.write(self.tag_name);
|
||||||
|
|
||||||
|
if (self.name) |v| {
|
||||||
|
try jw.objectField("name");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.input_type) |v| {
|
||||||
|
try jw.objectField("inputType");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.objectField("required");
|
||||||
|
try jw.write(self.required);
|
||||||
|
|
||||||
|
try jw.objectField("disabled");
|
||||||
|
try jw.write(self.disabled);
|
||||||
|
|
||||||
|
if (self.value) |v| {
|
||||||
|
try jw.objectField("value");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.placeholder) |v| {
|
||||||
|
try jw.objectField("placeholder");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.options.len > 0) {
|
||||||
|
try jw.objectField("options");
|
||||||
|
try jw.beginArray();
|
||||||
|
for (self.options) |opt| {
|
||||||
|
try opt.jsonStringify(jw);
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const FormInfo = struct {
|
||||||
|
backendNodeId: ?u32 = null,
|
||||||
|
node: *Node,
|
||||||
|
action: ?[]const u8,
|
||||||
|
method: ?[]const u8,
|
||||||
|
fields: []FormField,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: *const FormInfo, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
|
||||||
|
if (self.backendNodeId) |id| {
|
||||||
|
try jw.objectField("backendNodeId");
|
||||||
|
try jw.write(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.action) |v| {
|
||||||
|
try jw.objectField("action");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.method) |v| {
|
||||||
|
try jw.objectField("method");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.objectField("fields");
|
||||||
|
try jw.beginArray();
|
||||||
|
for (self.fields) |field| {
|
||||||
|
try field.jsonStringify(jw);
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Populate backendNodeId on each form and its fields by registering
|
||||||
|
/// their nodes in the given registry. Works with both CDP and MCP registries.
|
||||||
|
pub fn registerNodes(forms_data: []FormInfo, registry: anytype) !void {
|
||||||
|
for (forms_data) |*form| {
|
||||||
|
const form_registered = try registry.register(form.node);
|
||||||
|
form.backendNodeId = form_registered.id;
|
||||||
|
for (form.fields) |*field| {
|
||||||
|
const field_registered = try registry.register(field.node);
|
||||||
|
field.backendNodeId = field_registered.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect all forms and their fields under `root`.
|
||||||
|
/// Uses Form.getElements() to include fields outside the <form> that
|
||||||
|
/// reference it via the form="id" attribute, matching browser behavior.
|
||||||
|
/// `arena` must be an arena allocator — returned slices borrow its memory.
|
||||||
|
pub fn collectForms(
|
||||||
|
arena: Allocator,
|
||||||
|
root: *Node,
|
||||||
|
page: *Page,
|
||||||
|
) ![]FormInfo {
|
||||||
|
var forms: std.ArrayList(FormInfo) = .empty;
|
||||||
|
|
||||||
|
var tw = TreeWalker.Full.init(root, .{});
|
||||||
|
while (tw.next()) |node| {
|
||||||
|
const form = node.is(Element.Html.Form) orelse continue;
|
||||||
|
const el = form.asElement();
|
||||||
|
|
||||||
|
const fields = try collectFormFields(arena, form, page);
|
||||||
|
if (fields.len == 0) continue;
|
||||||
|
|
||||||
|
const action_attr = el.getAttributeSafe(comptime .wrap("action"));
|
||||||
|
const method_str = form.getMethod();
|
||||||
|
|
||||||
|
try forms.append(arena, .{
|
||||||
|
.node = node,
|
||||||
|
.action = if (action_attr) |a| if (a.len > 0) a else null else null,
|
||||||
|
.method = method_str,
|
||||||
|
.fields = fields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return forms.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectFormFields(
|
||||||
|
arena: Allocator,
|
||||||
|
form: *Element.Html.Form,
|
||||||
|
page: *Page,
|
||||||
|
) ![]FormField {
|
||||||
|
var fields: std.ArrayList(FormField) = .empty;
|
||||||
|
|
||||||
|
var elements = try form.getElements(page);
|
||||||
|
var it = try elements.iterator();
|
||||||
|
while (it.next()) |el| {
|
||||||
|
const node = el.asNode();
|
||||||
|
|
||||||
|
const is_disabled = el.isDisabled();
|
||||||
|
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
if (input._input_type == .hidden) continue;
|
||||||
|
if (input._input_type == .submit or input._input_type == .button or input._input_type == .image) continue;
|
||||||
|
|
||||||
|
try fields.append(arena, .{
|
||||||
|
.node = node,
|
||||||
|
.tag_name = "input",
|
||||||
|
.name = el.getAttributeSafe(comptime .wrap("name")),
|
||||||
|
.input_type = input._input_type.toString(),
|
||||||
|
.required = el.getAttributeSafe(comptime .wrap("required")) != null,
|
||||||
|
.disabled = is_disabled,
|
||||||
|
.value = input.getValue(),
|
||||||
|
.placeholder = el.getAttributeSafe(comptime .wrap("placeholder")),
|
||||||
|
.options = &.{},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.is(Element.Html.TextArea)) |textarea| {
|
||||||
|
try fields.append(arena, .{
|
||||||
|
.node = node,
|
||||||
|
.tag_name = "textarea",
|
||||||
|
.name = el.getAttributeSafe(comptime .wrap("name")),
|
||||||
|
.input_type = null,
|
||||||
|
.required = el.getAttributeSafe(comptime .wrap("required")) != null,
|
||||||
|
.disabled = is_disabled,
|
||||||
|
.value = textarea.getValue(),
|
||||||
|
.placeholder = el.getAttributeSafe(comptime .wrap("placeholder")),
|
||||||
|
.options = &.{},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.is(Element.Html.Select)) |select| {
|
||||||
|
const options = try collectSelectOptions(arena, node, page);
|
||||||
|
|
||||||
|
try fields.append(arena, .{
|
||||||
|
.node = node,
|
||||||
|
.tag_name = "select",
|
||||||
|
.name = el.getAttributeSafe(comptime .wrap("name")),
|
||||||
|
.input_type = null,
|
||||||
|
.required = el.getAttributeSafe(comptime .wrap("required")) != null,
|
||||||
|
.disabled = is_disabled,
|
||||||
|
.value = select.getValue(page),
|
||||||
|
.placeholder = null,
|
||||||
|
.options = options,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button elements from getElements() - skip (not fillable)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectSelectOptions(
|
||||||
|
arena: Allocator,
|
||||||
|
select_node: *Node,
|
||||||
|
page: *Page,
|
||||||
|
) ![]SelectOption {
|
||||||
|
var options: std.ArrayList(SelectOption) = .empty;
|
||||||
|
const Option = Element.Html.Option;
|
||||||
|
|
||||||
|
var tw = TreeWalker.Full.init(select_node, .{});
|
||||||
|
while (tw.next()) |node| {
|
||||||
|
const el = node.is(Element) orelse continue;
|
||||||
|
const option = el.is(Option) orelse continue;
|
||||||
|
|
||||||
|
try options.append(arena, .{
|
||||||
|
.value = option.getValue(page),
|
||||||
|
.text = option.getText(page),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
|
||||||
|
fn testForms(html: []const u8) ![]FormInfo {
|
||||||
|
const page = try testing.test_session.createPage();
|
||||||
|
|
||||||
|
const doc = page.window._document;
|
||||||
|
const div = try doc.createElement("div", null, page);
|
||||||
|
try page.parseHtmlAsChildren(div.asNode(), html);
|
||||||
|
|
||||||
|
return collectForms(page.call_arena, div.asNode(), page);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: login form" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form action="/login" method="POST">
|
||||||
|
\\ <input type="email" name="email" required placeholder="Email">
|
||||||
|
\\ <input type="password" name="password" required>
|
||||||
|
\\ <input type="submit" value="Log In">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual("/login", forms[0].action.?);
|
||||||
|
try testing.expectEqual("post", forms[0].method.?);
|
||||||
|
try testing.expectEqual(2, forms[0].fields.len);
|
||||||
|
try testing.expectEqual("email", forms[0].fields[0].name.?);
|
||||||
|
try testing.expectEqual("email", forms[0].fields[0].input_type.?);
|
||||||
|
try testing.expect(forms[0].fields[0].required);
|
||||||
|
try testing.expect(!forms[0].fields[0].disabled);
|
||||||
|
try testing.expectEqual("password", forms[0].fields[1].name.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: form with select" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form>
|
||||||
|
\\ <select name="color">
|
||||||
|
\\ <option value="red">Red</option>
|
||||||
|
\\ <option value="blue">Blue</option>
|
||||||
|
\\ </select>
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(1, forms[0].fields.len);
|
||||||
|
try testing.expectEqual("select", forms[0].fields[0].tag_name);
|
||||||
|
try testing.expectEqual(2, forms[0].fields[0].options.len);
|
||||||
|
try testing.expectEqual("red", forms[0].fields[0].options[0].value);
|
||||||
|
try testing.expectEqual("Red", forms[0].fields[0].options[0].text);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: form with textarea" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form method="POST">
|
||||||
|
\\ <textarea name="message" placeholder="Your message"></textarea>
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(1, forms[0].fields.len);
|
||||||
|
try testing.expectEqual("textarea", forms[0].fields[0].tag_name);
|
||||||
|
try testing.expectEqual("Your message", forms[0].fields[0].placeholder.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: empty form skipped" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form action="/empty">
|
||||||
|
\\ <p>No fields here</p>
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(0, forms.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: hidden inputs excluded" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form>
|
||||||
|
\\ <input type="hidden" name="csrf" value="token123">
|
||||||
|
\\ <input type="text" name="username">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(1, forms[0].fields.len);
|
||||||
|
try testing.expectEqual("username", forms[0].fields[0].name.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: multiple forms" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form action="/search" method="GET">
|
||||||
|
\\ <input type="text" name="q" placeholder="Search">
|
||||||
|
\\</form>
|
||||||
|
\\<form action="/login" method="POST">
|
||||||
|
\\ <input type="email" name="email">
|
||||||
|
\\ <input type="password" name="pass">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(2, forms.len);
|
||||||
|
try testing.expectEqual(1, forms[0].fields.len);
|
||||||
|
try testing.expectEqual(2, forms[1].fields.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: disabled fields flagged" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form>
|
||||||
|
\\ <input type="text" name="enabled_field">
|
||||||
|
\\ <input type="text" name="disabled_field" disabled>
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(2, forms[0].fields.len);
|
||||||
|
try testing.expect(!forms[0].fields[0].disabled);
|
||||||
|
try testing.expect(forms[0].fields[1].disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: disabled fieldset" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form>
|
||||||
|
\\ <fieldset disabled>
|
||||||
|
\\ <input type="text" name="in_disabled_fieldset">
|
||||||
|
\\ </fieldset>
|
||||||
|
\\ <input type="text" name="outside_fieldset">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(2, forms[0].fields.len);
|
||||||
|
try testing.expect(forms[0].fields[0].disabled);
|
||||||
|
try testing.expect(!forms[0].fields[1].disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: external field via form attribute" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<input type="text" name="external" form="myform">
|
||||||
|
\\<form id="myform" action="/submit">
|
||||||
|
\\ <input type="text" name="internal">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(2, forms[0].fields.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: checkbox and radio return value attribute" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form>
|
||||||
|
\\ <input type="checkbox" name="agree" value="yes" checked>
|
||||||
|
\\ <input type="radio" name="color" value="red">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(2, forms[0].fields.len);
|
||||||
|
try testing.expectEqual("checkbox", forms[0].fields[0].input_type.?);
|
||||||
|
try testing.expectEqual("yes", forms[0].fields[0].value.?);
|
||||||
|
try testing.expectEqual("radio", forms[0].fields[1].input_type.?);
|
||||||
|
try testing.expectEqual("red", forms[0].fields[1].value.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.forms: form without action or method" {
|
||||||
|
defer testing.reset();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
const forms = try testForms(
|
||||||
|
\\<form>
|
||||||
|
\\ <input type="text" name="q">
|
||||||
|
\\</form>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, forms.len);
|
||||||
|
try testing.expectEqual(null, forms[0].action);
|
||||||
|
try testing.expectEqual("get", forms[0].method.?);
|
||||||
|
try testing.expectEqual(1, forms[0].fields.len);
|
||||||
|
}
|
||||||
577
src/browser/interactive.zig
Normal file
577
src/browser/interactive.zig
Normal file
@@ -0,0 +1,577 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const URL = @import("URL.zig");
|
||||||
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
const EventTarget = @import("webapi/EventTarget.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const InteractivityType = enum {
|
||||||
|
native,
|
||||||
|
aria,
|
||||||
|
contenteditable,
|
||||||
|
listener,
|
||||||
|
focusable,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const InteractiveElement = struct {
|
||||||
|
backendNodeId: ?u32 = null,
|
||||||
|
node: *Node,
|
||||||
|
tag_name: []const u8,
|
||||||
|
role: ?[]const u8,
|
||||||
|
name: ?[]const u8,
|
||||||
|
interactivity_type: InteractivityType,
|
||||||
|
listener_types: []const []const u8,
|
||||||
|
disabled: bool,
|
||||||
|
tab_index: i32,
|
||||||
|
id: ?[]const u8,
|
||||||
|
class: ?[]const u8,
|
||||||
|
href: ?[]const u8,
|
||||||
|
input_type: ?[]const u8,
|
||||||
|
value: ?[]const u8,
|
||||||
|
element_name: ?[]const u8,
|
||||||
|
placeholder: ?[]const u8,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: *const InteractiveElement, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
|
||||||
|
if (self.backendNodeId) |id| {
|
||||||
|
try jw.objectField("backendNodeId");
|
||||||
|
try jw.write(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.objectField("tagName");
|
||||||
|
try jw.write(self.tag_name);
|
||||||
|
|
||||||
|
try jw.objectField("role");
|
||||||
|
try jw.write(self.role);
|
||||||
|
|
||||||
|
try jw.objectField("name");
|
||||||
|
try jw.write(self.name);
|
||||||
|
|
||||||
|
try jw.objectField("type");
|
||||||
|
try jw.write(@tagName(self.interactivity_type));
|
||||||
|
|
||||||
|
if (self.listener_types.len > 0) {
|
||||||
|
try jw.objectField("listeners");
|
||||||
|
try jw.beginArray();
|
||||||
|
for (self.listener_types) |lt| {
|
||||||
|
try jw.write(lt);
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.disabled) {
|
||||||
|
try jw.objectField("disabled");
|
||||||
|
try jw.write(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.objectField("tabIndex");
|
||||||
|
try jw.write(self.tab_index);
|
||||||
|
|
||||||
|
if (self.id) |v| {
|
||||||
|
try jw.objectField("id");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.class) |v| {
|
||||||
|
try jw.objectField("class");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.href) |v| {
|
||||||
|
try jw.objectField("href");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.input_type) |v| {
|
||||||
|
try jw.objectField("inputType");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.value) |v| {
|
||||||
|
try jw.objectField("value");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.element_name) |v| {
|
||||||
|
try jw.objectField("elementName");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.placeholder) |v| {
|
||||||
|
try jw.objectField("placeholder");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Populate backendNodeId on each interactive element by registering
|
||||||
|
/// their nodes in the given registry. Works with both CDP and MCP registries.
|
||||||
|
pub fn registerNodes(elements: []InteractiveElement, registry: anytype) !void {
|
||||||
|
for (elements) |*el| {
|
||||||
|
const registered = try registry.register(el.node);
|
||||||
|
el.backendNodeId = registered.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect all interactive elements under `root`.
|
||||||
|
pub fn collectInteractiveElements(
|
||||||
|
root: *Node,
|
||||||
|
arena: Allocator,
|
||||||
|
page: *Page,
|
||||||
|
) ![]InteractiveElement {
|
||||||
|
// Pre-build a map of event_target pointer → event type names,
|
||||||
|
// so classify and getListenerTypes are both O(1) per element.
|
||||||
|
const listener_targets = try buildListenerTargetMap(page, arena);
|
||||||
|
|
||||||
|
var css_cache: Element.PointerEventsCache = .empty;
|
||||||
|
|
||||||
|
var results: std.ArrayList(InteractiveElement) = .empty;
|
||||||
|
|
||||||
|
var tw = TreeWalker.Full.init(root, .{});
|
||||||
|
while (tw.next()) |node| {
|
||||||
|
const el = node.is(Element) orelse continue;
|
||||||
|
const html_el = el.is(Element.Html) orelse continue;
|
||||||
|
|
||||||
|
// Skip non-visual elements that are never user-interactive.
|
||||||
|
switch (el.getTag()) {
|
||||||
|
.script, .style, .link, .meta, .head, .noscript, .template => continue,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const itype = classifyInteractivity(page, el, html_el, listener_targets, &css_cache) orelse continue;
|
||||||
|
|
||||||
|
const listener_types = getListenerTypes(
|
||||||
|
el.asEventTarget(),
|
||||||
|
listener_targets,
|
||||||
|
);
|
||||||
|
|
||||||
|
try results.append(arena, .{
|
||||||
|
.node = node,
|
||||||
|
.tag_name = el.getTagNameLower(),
|
||||||
|
.role = getRole(el),
|
||||||
|
.name = try getAccessibleName(el, arena),
|
||||||
|
.interactivity_type = itype,
|
||||||
|
.listener_types = listener_types,
|
||||||
|
.disabled = el.isDisabled(),
|
||||||
|
.tab_index = html_el.getTabIndex(),
|
||||||
|
.id = el.getAttributeSafe(comptime .wrap("id")),
|
||||||
|
.class = el.getAttributeSafe(comptime .wrap("class")),
|
||||||
|
.href = if (el.getAttributeSafe(comptime .wrap("href"))) |href|
|
||||||
|
URL.resolve(arena, page.base(), href, .{ .encode = true }) catch href
|
||||||
|
else
|
||||||
|
null,
|
||||||
|
.input_type = getInputType(el),
|
||||||
|
.value = getInputValue(el),
|
||||||
|
.element_name = el.getAttributeSafe(comptime .wrap("name")),
|
||||||
|
.placeholder = el.getAttributeSafe(comptime .wrap("placeholder")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ListenerTargetMap = std.AutoHashMapUnmanaged(usize, std.ArrayList([]const u8));
|
||||||
|
|
||||||
|
/// Pre-build a map from event_target pointer → list of event type names.
|
||||||
|
/// This lets both classifyInteractivity (O(1) "has any?") and
|
||||||
|
/// getListenerTypes (O(1) "which ones?") avoid re-iterating per element.
|
||||||
|
pub fn buildListenerTargetMap(page: *Page, arena: Allocator) !ListenerTargetMap {
|
||||||
|
var map = ListenerTargetMap{};
|
||||||
|
|
||||||
|
// addEventListener registrations
|
||||||
|
var it = page._event_manager.lookup.iterator();
|
||||||
|
while (it.next()) |entry| {
|
||||||
|
const list = entry.value_ptr.*;
|
||||||
|
if (list.first != null) {
|
||||||
|
const gop = try map.getOrPut(arena, entry.key_ptr.event_target);
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .empty;
|
||||||
|
try gop.value_ptr.append(arena, entry.key_ptr.type_string.str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline handlers (onclick, onmousedown, etc.)
|
||||||
|
var attr_it = page._event_target_attr_listeners.iterator();
|
||||||
|
while (attr_it.next()) |entry| {
|
||||||
|
const gop = try map.getOrPut(arena, @intFromPtr(entry.key_ptr.target));
|
||||||
|
if (!gop.found_existing) gop.value_ptr.* = .empty;
|
||||||
|
// Strip "on" prefix to get the event type name.
|
||||||
|
try gop.value_ptr.append(arena, @tagName(entry.key_ptr.handler)[2..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn classifyInteractivity(
|
||||||
|
page: *Page,
|
||||||
|
el: *Element,
|
||||||
|
html_el: *Element.Html,
|
||||||
|
listener_targets: ListenerTargetMap,
|
||||||
|
cache: ?*Element.PointerEventsCache,
|
||||||
|
) ?InteractivityType {
|
||||||
|
if (el.hasPointerEventsNone(cache, page)) return null;
|
||||||
|
|
||||||
|
// 1. Native interactive by tag
|
||||||
|
switch (el.getTag()) {
|
||||||
|
.button, .summary, .details, .select, .textarea => return .native,
|
||||||
|
.anchor, .area => {
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("href")) != null) return .native;
|
||||||
|
},
|
||||||
|
.input => {
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
if (input._input_type != .hidden) return .native;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ARIA interactive role
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("role"))) |role| {
|
||||||
|
if (isInteractiveRole(role)) return .aria;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. contenteditable (15 bytes, exceeds SSO limit for comptime)
|
||||||
|
if (el.getAttributeSafe(.wrap("contenteditable"))) |ce| {
|
||||||
|
if (ce.len == 0 or std.ascii.eqlIgnoreCase(ce, "true")) return .contenteditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Event listeners (addEventListener or inline handlers)
|
||||||
|
const et_ptr = @intFromPtr(html_el.asEventTarget());
|
||||||
|
if (listener_targets.get(et_ptr) != null) return .listener;
|
||||||
|
|
||||||
|
// 5. Explicitly focusable via tabindex.
|
||||||
|
// Only count elements with an EXPLICIT tabindex attribute,
|
||||||
|
// since getTabIndex() returns 0 for all interactive tags by default
|
||||||
|
// (including anchors without href and hidden inputs).
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("tabindex"))) |_| {
|
||||||
|
if (html_el.getTabIndex() >= 0) return .focusable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isInteractiveRole(role: []const u8) bool {
|
||||||
|
const MAX_LEN = "menuitemcheckbox".len;
|
||||||
|
if (role.len > MAX_LEN) return false;
|
||||||
|
var buf: [MAX_LEN]u8 = undefined;
|
||||||
|
const lowered = std.ascii.lowerString(&buf, role);
|
||||||
|
const interactive_roles = std.StaticStringMap(void).initComptime(.{
|
||||||
|
.{ "button", {} },
|
||||||
|
.{ "checkbox", {} },
|
||||||
|
.{ "combobox", {} },
|
||||||
|
.{ "iframe", {} },
|
||||||
|
.{ "link", {} },
|
||||||
|
.{ "listbox", {} },
|
||||||
|
.{ "menuitem", {} },
|
||||||
|
.{ "menuitemcheckbox", {} },
|
||||||
|
.{ "menuitemradio", {} },
|
||||||
|
.{ "option", {} },
|
||||||
|
.{ "radio", {} },
|
||||||
|
.{ "searchbox", {} },
|
||||||
|
.{ "slider", {} },
|
||||||
|
.{ "spinbutton", {} },
|
||||||
|
.{ "switch", {} },
|
||||||
|
.{ "tab", {} },
|
||||||
|
.{ "textbox", {} },
|
||||||
|
.{ "treeitem", {} },
|
||||||
|
});
|
||||||
|
return interactive_roles.has(lowered);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isContentRole(role: []const u8) bool {
|
||||||
|
const MAX_LEN = "columnheader".len;
|
||||||
|
if (role.len > MAX_LEN) return false;
|
||||||
|
var buf: [MAX_LEN]u8 = undefined;
|
||||||
|
const lowered = std.ascii.lowerString(&buf, role);
|
||||||
|
const content_roles = std.StaticStringMap(void).initComptime(.{
|
||||||
|
.{ "article", {} },
|
||||||
|
.{ "cell", {} },
|
||||||
|
.{ "columnheader", {} },
|
||||||
|
.{ "gridcell", {} },
|
||||||
|
.{ "heading", {} },
|
||||||
|
.{ "listitem", {} },
|
||||||
|
.{ "main", {} },
|
||||||
|
.{ "navigation", {} },
|
||||||
|
.{ "region", {} },
|
||||||
|
.{ "rowheader", {} },
|
||||||
|
});
|
||||||
|
return content_roles.has(lowered);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getRole(el: *Element) ?[]const u8 {
|
||||||
|
// Explicit role attribute takes precedence
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("role"))) |role| return role;
|
||||||
|
|
||||||
|
// Implicit role from tag
|
||||||
|
return switch (el.getTag()) {
|
||||||
|
.button, .summary => "button",
|
||||||
|
.anchor, .area => if (el.getAttributeSafe(comptime .wrap("href")) != null) "link" else null,
|
||||||
|
.input => blk: {
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
break :blk switch (input._input_type) {
|
||||||
|
.text, .tel, .url, .email => "textbox",
|
||||||
|
.checkbox => "checkbox",
|
||||||
|
.radio => "radio",
|
||||||
|
.button, .submit, .reset, .image => "button",
|
||||||
|
.range => "slider",
|
||||||
|
.number => "spinbutton",
|
||||||
|
.search => "searchbox",
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break :blk null;
|
||||||
|
},
|
||||||
|
.select => "combobox",
|
||||||
|
.textarea => "textbox",
|
||||||
|
.details => "group",
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getAccessibleName(el: *Element, arena: Allocator) !?[]const u8 {
|
||||||
|
// aria-label
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("aria-label"))) |v| {
|
||||||
|
if (v.len > 0) return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// alt (for img, input[type=image])
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("alt"))) |v| {
|
||||||
|
if (v.len > 0) return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// title
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("title"))) |v| {
|
||||||
|
if (v.len > 0) return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// placeholder
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("placeholder"))) |v| {
|
||||||
|
if (v.len > 0) return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// value (for buttons)
|
||||||
|
if (el.getTag() == .input) {
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("value"))) |v| {
|
||||||
|
if (v.len > 0) return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text content (first non-empty text node, trimmed)
|
||||||
|
return try getTextContent(el.asNode(), arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getTextContent(node: *Node, arena: Allocator) !?[]const u8 {
|
||||||
|
var tw: TreeWalker.FullExcludeSelf = .init(node, .{});
|
||||||
|
|
||||||
|
var arr: std.ArrayList(u8) = .empty;
|
||||||
|
var single_chunk: ?[]const u8 = null;
|
||||||
|
|
||||||
|
while (tw.next()) |child| {
|
||||||
|
// Skip text inside script/style elements.
|
||||||
|
if (child.is(Element)) |el| {
|
||||||
|
switch (el.getTag()) {
|
||||||
|
.script, .style => {
|
||||||
|
tw.skipChildren();
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (child.is(Node.CData)) |cdata| {
|
||||||
|
if (cdata.is(Node.CData.Text)) |text| {
|
||||||
|
const content = std.mem.trim(u8, text.getWholeText(), &std.ascii.whitespace);
|
||||||
|
if (content.len > 0) {
|
||||||
|
if (single_chunk == null and arr.items.len == 0) {
|
||||||
|
single_chunk = content;
|
||||||
|
} else {
|
||||||
|
if (single_chunk) |sc| {
|
||||||
|
try arr.appendSlice(arena, sc);
|
||||||
|
try arr.append(arena, ' ');
|
||||||
|
single_chunk = null;
|
||||||
|
}
|
||||||
|
try arr.appendSlice(arena, content);
|
||||||
|
try arr.append(arena, ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (single_chunk) |sc| return sc;
|
||||||
|
if (arr.items.len == 0) return null;
|
||||||
|
|
||||||
|
// strip out trailing space
|
||||||
|
return arr.items[0 .. arr.items.len - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getInputType(el: *Element) ?[]const u8 {
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
return input._input_type.toString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getInputValue(el: *Element) ?[]const u8 {
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
return input.getValue();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all event listener types registered on this target.
|
||||||
|
fn getListenerTypes(target: *EventTarget, listener_targets: ListenerTargetMap) []const []const u8 {
|
||||||
|
if (listener_targets.get(@intFromPtr(target))) |types| return types.items;
|
||||||
|
return &.{};
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
|
||||||
|
fn testInteractive(html: []const u8) ![]InteractiveElement {
|
||||||
|
const page = try testing.test_session.createPage();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
|
||||||
|
const doc = page.window._document;
|
||||||
|
const div = try doc.createElement("div", null, page);
|
||||||
|
try page.parseHtmlAsChildren(div.asNode(), html);
|
||||||
|
|
||||||
|
return collectInteractiveElements(div.asNode(), page.call_arena, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: button" {
|
||||||
|
const elements = try testInteractive("<button>Click me</button>");
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expectEqual("button", elements[0].tag_name);
|
||||||
|
try testing.expectEqual("button", elements[0].role.?);
|
||||||
|
try testing.expectEqual("Click me", elements[0].name.?);
|
||||||
|
try testing.expectEqual(InteractivityType.native, elements[0].interactivity_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: anchor with href" {
|
||||||
|
const elements = try testInteractive("<a href=\"/page\">Link</a>");
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expectEqual("a", elements[0].tag_name);
|
||||||
|
try testing.expectEqual("link", elements[0].role.?);
|
||||||
|
try testing.expectEqual("Link", elements[0].name.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: anchor without href" {
|
||||||
|
const elements = try testInteractive("<a>Not a link</a>");
|
||||||
|
try testing.expectEqual(0, elements.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: input types" {
|
||||||
|
const elements = try testInteractive(
|
||||||
|
\\<input type="text" placeholder="Search">
|
||||||
|
\\<input type="hidden" name="csrf">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expectEqual("input", elements[0].tag_name);
|
||||||
|
try testing.expectEqual("text", elements[0].input_type.?);
|
||||||
|
try testing.expectEqual("Search", elements[0].placeholder.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: select and textarea" {
|
||||||
|
const elements = try testInteractive(
|
||||||
|
\\<select name="color"><option>Red</option></select>
|
||||||
|
\\<textarea name="msg"></textarea>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(2, elements.len);
|
||||||
|
try testing.expectEqual("select", elements[0].tag_name);
|
||||||
|
try testing.expectEqual("textarea", elements[1].tag_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: aria role" {
|
||||||
|
const elements = try testInteractive("<div role=\"button\">Custom</div>");
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expectEqual("div", elements[0].tag_name);
|
||||||
|
try testing.expectEqual("button", elements[0].role.?);
|
||||||
|
try testing.expectEqual(InteractivityType.aria, elements[0].interactivity_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: contenteditable" {
|
||||||
|
const elements = try testInteractive("<div contenteditable=\"true\">Edit me</div>");
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expectEqual(InteractivityType.contenteditable, elements[0].interactivity_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: tabindex" {
|
||||||
|
const elements = try testInteractive("<div tabindex=\"0\">Focusable</div>");
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expectEqual(InteractivityType.focusable, elements[0].interactivity_type);
|
||||||
|
try testing.expectEqual(@as(i32, 0), elements[0].tab_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: disabled" {
|
||||||
|
const elements = try testInteractive("<button disabled>Off</button>");
|
||||||
|
try testing.expectEqual(1, elements.len);
|
||||||
|
try testing.expect(elements[0].disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: disabled by fieldset" {
|
||||||
|
const elements = try testInteractive(
|
||||||
|
\\<fieldset disabled>
|
||||||
|
\\ <button>Disabled</button>
|
||||||
|
\\ <legend><button>In legend</button></legend>
|
||||||
|
\\</fieldset>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(2, elements.len);
|
||||||
|
// Button outside legend is disabled by fieldset
|
||||||
|
try testing.expect(elements[0].disabled);
|
||||||
|
// Button inside first legend is NOT disabled
|
||||||
|
try testing.expect(!elements[1].disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: pointer-events none" {
|
||||||
|
const elements = try testInteractive("<button style=\"pointer-events: none;\">Click me</button>");
|
||||||
|
try testing.expectEqual(0, elements.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: non-interactive div" {
|
||||||
|
const elements = try testInteractive("<div>Just text</div>");
|
||||||
|
try testing.expectEqual(0, elements.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: details and summary" {
|
||||||
|
const elements = try testInteractive("<details><summary>More</summary><p>Content</p></details>");
|
||||||
|
try testing.expectEqual(2, elements.len);
|
||||||
|
try testing.expectEqual("details", elements[0].tag_name);
|
||||||
|
try testing.expectEqual("summary", elements[1].tag_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.interactive: mixed elements" {
|
||||||
|
const elements = try testInteractive(
|
||||||
|
\\<div>
|
||||||
|
\\ <a href="/home">Home</a>
|
||||||
|
\\ <p>Some text</p>
|
||||||
|
\\ <button id="btn1">Submit</button>
|
||||||
|
\\ <input type="email" placeholder="Email">
|
||||||
|
\\ <div>Not interactive</div>
|
||||||
|
\\ <div role="tab">Tab</div>
|
||||||
|
\\</div>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(4, elements.len);
|
||||||
|
}
|
||||||
@@ -21,18 +21,46 @@ const js = @import("js.zig");
|
|||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const Array = @This();
|
const Array = @This();
|
||||||
js_arr: v8.Array,
|
|
||||||
context: *js.Context,
|
local: *const js.Local,
|
||||||
|
handle: *const v8.Array,
|
||||||
|
|
||||||
pub fn len(self: Array) usize {
|
pub fn len(self: Array) usize {
|
||||||
return @intCast(self.js_arr.length());
|
return v8.v8__Array__Length(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: Array, index: usize) !js.Value {
|
pub fn get(self: Array, index: u32) !js.Value {
|
||||||
const idx_key = v8.Integer.initU32(self.context.isolate, @intCast(index));
|
const ctx = self.local.ctx;
|
||||||
const js_obj = self.js_arr.castTo(v8.Object);
|
|
||||||
|
const idx = js.Integer.init(ctx.isolate.handle, index);
|
||||||
|
const handle = v8.v8__Object__Get(@ptrCast(self.handle), self.local.handle, idx.handle) orelse {
|
||||||
|
return error.JsException;
|
||||||
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.context = self.context,
|
.local = self.local,
|
||||||
.js_val = try js_obj.getValue(self.context.v8_context, idx_key.toValue()),
|
.handle = handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
|
||||||
|
const js_value = try self.local.zigValueToJs(value, opts);
|
||||||
|
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__SetAtIndex(@ptrCast(self.handle), self.local.handle, index, js_value.handle, &out);
|
||||||
|
return out.has_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toObject(self: Array) js.Object {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = @ptrCast(self.handle),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toValue(self: Array) js.Value {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/browser/js/BigInt.zig
Normal file
41
src/browser/js/BigInt.zig
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// 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 js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const BigInt = @This();
|
||||||
|
|
||||||
|
handle: *const v8.Integer,
|
||||||
|
|
||||||
|
pub fn init(isolate: *v8.Isolate, val: anytype) BigInt {
|
||||||
|
const handle = switch (@TypeOf(val)) {
|
||||||
|
i8, i16, i32, i64, isize => v8.v8__BigInt__New(isolate, val).?,
|
||||||
|
u8, u16, u32, u64, usize => v8.v8__BigInt__NewFromUnsigned(isolate, val).?,
|
||||||
|
else => |T| @compileError("cannot create v8::BigInt from: " ++ @typeName(T)),
|
||||||
|
};
|
||||||
|
return .{ .handle = handle };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getInt64(self: BigInt) i64 {
|
||||||
|
return v8.v8__BigInt__Int64Value(self.handle, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getUint64(self: BigInt) u64 {
|
||||||
|
return v8.v8__BigInt__Uint64Value(self.handle, null);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -18,20 +18,34 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const App = @import("../../App.zig");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const bridge = @import("bridge.zig");
|
const bridge = @import("bridge.zig");
|
||||||
const Context = @import("Context.zig");
|
const Context = @import("Context.zig");
|
||||||
|
const Isolate = @import("Isolate.zig");
|
||||||
const Platform = @import("Platform.zig");
|
const Platform = @import("Platform.zig");
|
||||||
const Snapshot = @import("Snapshot.zig");
|
const Snapshot = @import("Snapshot.zig");
|
||||||
const Inspector = @import("Inspector.zig");
|
const Inspector = @import("Inspector.zig");
|
||||||
const ExecutionWorld = @import("ExecutionWorld.zig");
|
|
||||||
|
const Page = @import("../Page.zig");
|
||||||
|
const Window = @import("../webapi/Window.zig");
|
||||||
|
|
||||||
const JsApis = bridge.JsApis;
|
const JsApis = bridge.JsApis;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
|
fn initClassIds() void {
|
||||||
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
|
JsApi.Meta.class_id = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var class_id_once = std.once(initClassIds);
|
||||||
|
|
||||||
// The Env maps to a V8 isolate, which represents a isolated sandbox for
|
// The Env maps to a V8 isolate, which represents a isolated sandbox for
|
||||||
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
||||||
@@ -41,118 +55,436 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
|||||||
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
|
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
|
||||||
const Env = @This();
|
const Env = @This();
|
||||||
|
|
||||||
|
app: *App,
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
|
||||||
platform: *const Platform,
|
platform: *const Platform,
|
||||||
|
|
||||||
// the global isolate
|
// the global isolate
|
||||||
isolate: v8.Isolate,
|
isolate: js.Isolate,
|
||||||
|
|
||||||
|
contexts: [64]*Context,
|
||||||
|
context_count: usize,
|
||||||
|
|
||||||
// just kept around because we need to free it on deinit
|
// just kept around because we need to free it on deinit
|
||||||
isolate_params: *v8.CreateParams,
|
isolate_params: *v8.CreateParams,
|
||||||
|
|
||||||
context_id: usize,
|
context_id: usize,
|
||||||
|
|
||||||
// Dynamic slice to avoid circular dependency on JsApis.len at comptime
|
// Maps origin -> shared Origin contains, for v8 values shared across
|
||||||
templates: []v8.FunctionTemplate,
|
// same-origin Contexts. There's a mismatch here between our JS model and our
|
||||||
|
// Browser model. Origins only live as long as the root page of a session exists.
|
||||||
|
// It would be wrong/dangerous to re-use an Origin across root page navigations.
|
||||||
|
|
||||||
|
// Global handles that need to be freed on deinit
|
||||||
|
eternal_function_templates: []v8.Eternal,
|
||||||
|
|
||||||
|
// Dynamic slice to avoid circular dependency on JsApis.len at comptime
|
||||||
|
templates: []*const v8.FunctionTemplate,
|
||||||
|
|
||||||
|
// Global template created once per isolate and reused across all contexts
|
||||||
|
global_template: v8.Eternal,
|
||||||
|
|
||||||
|
// Inspector associated with the Isolate. Exists when CDP is being used.
|
||||||
|
inspector: ?*Inspector,
|
||||||
|
|
||||||
|
// We can store data in a v8::Object's Private data bag. The keys are v8::Private
|
||||||
|
// which an be created once per isolaet.
|
||||||
|
private_symbols: PrivateSymbols,
|
||||||
|
|
||||||
|
microtask_queues_are_running: bool,
|
||||||
|
|
||||||
|
pub const InitOpts = struct {
|
||||||
|
with_inspector: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(app: *App, opts: InitOpts) !Env {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
comptime {
|
||||||
|
// V8 requirement for any data using SetAlignedPointerInInternalField
|
||||||
|
const a = @alignOf(@import("TaggedOpaque.zig"));
|
||||||
|
std.debug.assert(a >= 2 and a % 2 == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize class IDs once before any V8 work
|
||||||
|
class_id_once.call();
|
||||||
|
|
||||||
|
const allocator = app.allocator;
|
||||||
|
const snapshot = &app.snapshot;
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot) !Env {
|
|
||||||
var params = try allocator.create(v8.CreateParams);
|
var params = try allocator.create(v8.CreateParams);
|
||||||
errdefer allocator.destroy(params);
|
errdefer allocator.destroy(params);
|
||||||
v8.c.v8__Isolate__CreateParams__CONSTRUCT(params);
|
v8.v8__Isolate__CreateParams__CONSTRUCT(params);
|
||||||
params.snapshot_blob = @ptrCast(&snapshot.startup_data);
|
params.snapshot_blob = @ptrCast(&snapshot.startup_data);
|
||||||
|
|
||||||
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
|
params.array_buffer_allocator = v8.v8__ArrayBuffer__Allocator__NewDefaultAllocator().?;
|
||||||
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
|
errdefer v8.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);
|
||||||
|
|
||||||
params.external_references = &snapshot.external_references;
|
params.external_references = &snapshot.external_references;
|
||||||
|
|
||||||
var isolate = v8.Isolate.init(params);
|
var isolate = js.Isolate.init(params);
|
||||||
errdefer isolate.deinit();
|
errdefer isolate.deinit();
|
||||||
|
const isolate_handle = isolate.handle;
|
||||||
|
|
||||||
// This is the callback that runs whenever a module is dynamically imported.
|
v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate_handle, Context.dynamicModuleCallback);
|
||||||
isolate.setHostImportModuleDynamicallyCallback(Context.dynamicModuleCallback);
|
v8.v8__Isolate__SetPromiseRejectCallback(isolate_handle, promiseRejectCallback);
|
||||||
isolate.setPromiseRejectCallback(promiseRejectCallback);
|
v8.v8__Isolate__SetMicrotasksPolicy(isolate_handle, v8.kExplicit);
|
||||||
isolate.setMicrotasksPolicy(v8.c.kExplicit);
|
v8.v8__Isolate__SetFatalErrorHandler(isolate_handle, fatalCallback);
|
||||||
|
v8.v8__Isolate__SetOOMErrorHandler(isolate_handle, oomCallback);
|
||||||
|
|
||||||
isolate.enter();
|
isolate.enter();
|
||||||
errdefer isolate.exit();
|
errdefer isolate.exit();
|
||||||
|
|
||||||
isolate.setHostInitializeImportMetaObjectCallback(Context.metaObjectCallback);
|
v8.v8__Isolate__SetHostInitializeImportMetaObjectCallback(isolate_handle, Context.metaObjectCallback);
|
||||||
|
|
||||||
// Allocate templates array dynamically to avoid comptime dependency on JsApis.len
|
// Allocate arrays dynamically to avoid comptime dependency on JsApis.len
|
||||||
const templates = try allocator.alloc(v8.FunctionTemplate, JsApis.len);
|
const eternal_function_templates = try allocator.alloc(v8.Eternal, JsApis.len);
|
||||||
|
errdefer allocator.free(eternal_function_templates);
|
||||||
|
|
||||||
|
const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len);
|
||||||
errdefer allocator.free(templates);
|
errdefer allocator.free(templates);
|
||||||
|
|
||||||
|
var global_eternal: v8.Eternal = undefined;
|
||||||
|
var private_symbols: PrivateSymbols = undefined;
|
||||||
{
|
{
|
||||||
var temp_scope: v8.HandleScope = undefined;
|
var temp_scope: js.HandleScope = undefined;
|
||||||
v8.HandleScope.init(&temp_scope, isolate);
|
temp_scope.init(isolate);
|
||||||
defer temp_scope.deinit();
|
defer temp_scope.deinit();
|
||||||
const context = v8.Context.init(isolate, null, null);
|
|
||||||
|
|
||||||
context.enter();
|
inline for (JsApis, 0..) |_, i| {
|
||||||
defer context.exit();
|
const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate_handle, snapshot.data_start + i);
|
||||||
|
const function_handle: *const v8.FunctionTemplate = @ptrCast(data);
|
||||||
|
// Make function template eternal
|
||||||
|
v8.v8__Eternal__New(isolate_handle, @ptrCast(function_handle), &eternal_function_templates[i]);
|
||||||
|
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
// Extract the local handle from the global for easy access
|
||||||
JsApi.Meta.class_id = i;
|
const eternal_ptr = v8.v8__Eternal__Get(&eternal_function_templates[i], isolate_handle);
|
||||||
const data = context.getDataFromSnapshotOnce(snapshot.data_start + i);
|
templates[i] = @ptrCast(@alignCast(eternal_ptr.?));
|
||||||
const function = v8.FunctionTemplate{ .handle = @ptrCast(data) };
|
|
||||||
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, function).castToFunctionTemplate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create global template once per isolate
|
||||||
|
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate_handle);
|
||||||
|
const window_name = v8.v8__String__NewFromUtf8(isolate_handle, "Window", v8.kNormal, 6);
|
||||||
|
v8.v8__FunctionTemplate__SetClassName(js_global, window_name);
|
||||||
|
|
||||||
|
// Find Window in JsApis by name (avoids circular import)
|
||||||
|
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
|
||||||
|
v8.v8__FunctionTemplate__Inherit(js_global, templates[window_index]);
|
||||||
|
|
||||||
|
const global_template_local = v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;
|
||||||
|
v8.v8__ObjectTemplate__SetNamedHandler(global_template_local, &.{
|
||||||
|
.getter = bridge.unknownWindowPropertyCallback,
|
||||||
|
.setter = null,
|
||||||
|
.query = null,
|
||||||
|
.deleter = null,
|
||||||
|
.enumerator = null,
|
||||||
|
.definer = null,
|
||||||
|
.descriptor = null,
|
||||||
|
.data = null,
|
||||||
|
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||||
|
});
|
||||||
|
// I don't 100% understand this. We actually set this up in the snapshot,
|
||||||
|
// but for the global instance, it doesn't work. SetIndexedHandler and
|
||||||
|
// SetNamedHandler are set on the Instance template, and that's the key
|
||||||
|
// difference. The context has its own global instance, so we need to set
|
||||||
|
// these back up directly on it. There might be a better way to do this.
|
||||||
|
v8.v8__ObjectTemplate__SetIndexedHandler(global_template_local, &.{
|
||||||
|
.getter = Window.JsApi.index.getter,
|
||||||
|
.setter = null,
|
||||||
|
.query = null,
|
||||||
|
.deleter = null,
|
||||||
|
.enumerator = null,
|
||||||
|
.definer = null,
|
||||||
|
.descriptor = null,
|
||||||
|
.data = null,
|
||||||
|
.flags = 0,
|
||||||
|
});
|
||||||
|
v8.v8__Eternal__New(isolate_handle, @ptrCast(global_template_local), &global_eternal);
|
||||||
|
private_symbols = PrivateSymbols.init(isolate_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
var inspector: ?*js.Inspector = null;
|
||||||
|
if (opts.with_inspector) {
|
||||||
|
inspector = try Inspector.init(allocator, isolate_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
|
.app = app,
|
||||||
.context_id = 0,
|
.context_id = 0,
|
||||||
.isolate = isolate,
|
|
||||||
.platform = platform,
|
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
|
.contexts = undefined,
|
||||||
|
.context_count = 0,
|
||||||
|
.isolate = isolate,
|
||||||
|
.platform = &app.platform,
|
||||||
.templates = templates,
|
.templates = templates,
|
||||||
.isolate_params = params,
|
.isolate_params = params,
|
||||||
|
.inspector = inspector,
|
||||||
|
.global_template = global_eternal,
|
||||||
|
.private_symbols = private_symbols,
|
||||||
|
.microtask_queues_are_running = false,
|
||||||
|
.eternal_function_templates = eternal_function_templates,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Env) void {
|
pub fn deinit(self: *Env) void {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(self.context_count == 0);
|
||||||
|
}
|
||||||
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
|
ctx.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = self.app;
|
||||||
|
const allocator = app.allocator;
|
||||||
|
|
||||||
|
if (self.inspector) |i| {
|
||||||
|
i.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
allocator.free(self.templates);
|
||||||
|
allocator.free(self.eternal_function_templates);
|
||||||
|
self.private_symbols.deinit();
|
||||||
|
|
||||||
self.isolate.exit();
|
self.isolate.exit();
|
||||||
self.isolate.deinit();
|
self.isolate.deinit();
|
||||||
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
|
v8.v8__ArrayBuffer__Allocator__DELETE(self.isolate_params.array_buffer_allocator.?);
|
||||||
self.allocator.destroy(self.isolate_params);
|
allocator.destroy(self.isolate_params);
|
||||||
self.allocator.free(self.templates);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector {
|
pub const ContextParams = struct {
|
||||||
return Inspector.init(arena, self.isolate, ctx);
|
identity: *js.Identity,
|
||||||
|
identity_arena: Allocator,
|
||||||
|
call_arena: Allocator,
|
||||||
|
debug_name: []const u8 = "Context",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context {
|
||||||
|
const context_arena = try self.app.arena_pool.acquire(.{ .debug = params.debug_name });
|
||||||
|
errdefer self.app.arena_pool.release(context_arena);
|
||||||
|
|
||||||
|
const isolate = self.isolate;
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
// Create a per-context microtask queue for isolation
|
||||||
|
const microtask_queue = v8.v8__MicrotaskQueue__New(isolate.handle, v8.kExplicit).?;
|
||||||
|
errdefer v8.v8__MicrotaskQueue__DELETE(microtask_queue);
|
||||||
|
|
||||||
|
// Get the global template that was created once per isolate
|
||||||
|
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?));
|
||||||
|
v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime Snapshot.countInternalFields(Window.JsApi));
|
||||||
|
|
||||||
|
const v8_context = v8.v8__Context__New__Config(isolate.handle, &.{
|
||||||
|
.global_template = global_template,
|
||||||
|
.global_object = null,
|
||||||
|
.microtask_queue = microtask_queue,
|
||||||
|
}).?;
|
||||||
|
|
||||||
|
// Create the v8::Context and wrap it in a v8::Global
|
||||||
|
var context_global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
|
||||||
|
|
||||||
|
// get the global object for the context, this maps to our Window
|
||||||
|
const global_obj = v8.v8__Context__Global(v8_context).?;
|
||||||
|
|
||||||
|
{
|
||||||
|
// Store our TAO inside the internal field of the global object. This
|
||||||
|
// maps the v8::Object -> Zig instance. Almost all objects have this, and
|
||||||
|
// it gets setup automatically as objects are created, but the Window
|
||||||
|
// object already exists in v8 (it's the global) so we manually create
|
||||||
|
// the mapping here.
|
||||||
|
const tao = try context_arena.create(@import("TaggedOpaque.zig"));
|
||||||
|
tao.* = .{
|
||||||
|
.value = @ptrCast(page.window),
|
||||||
|
.prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr,
|
||||||
|
.prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len),
|
||||||
|
.subtype = .node, // this probably isn't right, but it's what we've been doing all along
|
||||||
|
};
|
||||||
|
v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
|
||||||
|
}
|
||||||
|
|
||||||
|
const context_id = self.context_id;
|
||||||
|
self.context_id = context_id + 1;
|
||||||
|
|
||||||
|
const session = page._session;
|
||||||
|
const origin = try session.getOrCreateOrigin(null);
|
||||||
|
errdefer session.releaseOrigin(origin);
|
||||||
|
|
||||||
|
const context = try context_arena.create(Context);
|
||||||
|
context.* = .{
|
||||||
|
.env = self,
|
||||||
|
.page = page,
|
||||||
|
.origin = origin,
|
||||||
|
.id = context_id,
|
||||||
|
.session = session,
|
||||||
|
.isolate = isolate,
|
||||||
|
.arena = context_arena,
|
||||||
|
.handle = context_global,
|
||||||
|
.templates = self.templates,
|
||||||
|
.call_arena = params.call_arena,
|
||||||
|
.microtask_queue = microtask_queue,
|
||||||
|
.script_manager = &page._script_manager,
|
||||||
|
.scheduler = .init(context_arena),
|
||||||
|
.identity = params.identity,
|
||||||
|
.identity_arena = params.identity_arena,
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
// Multiple contexts can be created for the same Window (via CDP). We only
|
||||||
|
// need to register the first one.
|
||||||
|
const gop = try params.identity.identity_map.getOrPut(params.identity_arena, @intFromPtr(page.window));
|
||||||
|
if (gop.found_existing == false) {
|
||||||
|
// our window wrapped in a v8::Global
|
||||||
|
var global_global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
|
||||||
|
gop.value_ptr.* = global_global;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store a pointer to our context inside the v8 context so that, given
|
||||||
|
// a v8 context, we can get our context out
|
||||||
|
v8.v8__Context__SetAlignedPointerInEmbedderData(v8_context, 1, @ptrCast(context));
|
||||||
|
|
||||||
|
const count = self.context_count;
|
||||||
|
if (count >= self.contexts.len) {
|
||||||
|
return error.TooManyContexts;
|
||||||
|
}
|
||||||
|
self.contexts[count] = context;
|
||||||
|
self.context_count = count + 1;
|
||||||
|
|
||||||
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMicrotasks(self: *const Env) void {
|
pub fn destroyContext(self: *Env, context: *Context) void {
|
||||||
self.isolate.performMicrotasksCheckpoint();
|
for (self.contexts[0..self.context_count], 0..) |ctx, i| {
|
||||||
|
if (ctx == context) {
|
||||||
|
// Swap with last element and decrement count
|
||||||
|
self.context_count -= 1;
|
||||||
|
self.contexts[i] = self.contexts[self.context_count];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
@panic("Tried to remove unknown context");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isolate = self.isolate;
|
||||||
|
if (self.inspector) |inspector| {
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
inspector.contextDestroyed(@ptrCast(v8.v8__Global__Get(&context.handle, isolate.handle)));
|
||||||
|
}
|
||||||
|
|
||||||
|
context.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pumpMessageLoop(self: *const Env) bool {
|
pub fn runMicrotasks(self: *Env) void {
|
||||||
return self.platform.inner.pumpMessageLoop(self.isolate, false);
|
if (self.microtask_queues_are_running == false) {
|
||||||
|
const v8_isolate = self.isolate.handle;
|
||||||
|
|
||||||
|
self.microtask_queues_are_running = true;
|
||||||
|
defer self.microtask_queues_are_running = false;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < self.context_count) : (i += 1) {
|
||||||
|
const ctx = self.contexts[i];
|
||||||
|
v8.v8__MicrotaskQueue__PerformCheckpoint(ctx.microtask_queue, v8_isolate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runMacrotasks(self: *Env) !void {
|
||||||
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
|
if (comptime builtin.is_test == false) {
|
||||||
|
// I hate this comptime check as much as you do. But we have tests
|
||||||
|
// which rely on short execution before shutdown. In real world, it's
|
||||||
|
// underterministic whether a timer will or won't run before the
|
||||||
|
// page shutsdown. But for tests, we need to run them to their end.
|
||||||
|
if (ctx.scheduler.hasReadyTasks() == false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
const entered = ctx.enter(&hs);
|
||||||
|
defer entered.exit();
|
||||||
|
try ctx.scheduler.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn msToNextMacrotask(self: *Env) ?u64 {
|
||||||
|
var next_task: u64 = std.math.maxInt(u64);
|
||||||
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
|
const candidate = ctx.scheduler.msToNextHigh() orelse continue;
|
||||||
|
next_task = @min(candidate, next_task);
|
||||||
|
}
|
||||||
|
return if (next_task == std.math.maxInt(u64)) null else next_task;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pumpMessageLoop(self: *const Env) void {
|
||||||
|
var hs: v8.HandleScope = undefined;
|
||||||
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);
|
||||||
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
|
const isolate = self.isolate.handle;
|
||||||
|
const platform = self.platform.handle;
|
||||||
|
while (v8.v8__Platform__PumpMessageLoop(platform, isolate, false)) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hasBackgroundTasks(self: *const Env) bool {
|
||||||
|
return v8.v8__Isolate__HasPendingBackgroundTasks(self.isolate.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn waitForBackgroundTasks(self: *Env) void {
|
||||||
|
var hs: v8.HandleScope = undefined;
|
||||||
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);
|
||||||
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
|
const isolate = self.isolate.handle;
|
||||||
|
const platform = self.platform.handle;
|
||||||
|
while (v8.v8__Isolate__HasPendingBackgroundTasks(isolate)) {
|
||||||
|
_ = v8.v8__Platform__PumpMessageLoop(platform, isolate, true);
|
||||||
|
self.runMicrotasks();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runIdleTasks(self: *const Env) void {
|
pub fn runIdleTasks(self: *const Env) void {
|
||||||
return self.platform.inner.runIdleTasks(self.isolate, 1);
|
v8.v8__Platform__RunIdleTasks(self.platform.handle, self.isolate.handle, 1);
|
||||||
}
|
|
||||||
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
|
|
||||||
return .{
|
|
||||||
.env = self,
|
|
||||||
.context = null,
|
|
||||||
.context_arena = ArenaAllocator.init(self.allocator),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// V8 doesn't immediately free memory associated with
|
// V8 doesn't immediately free memory associated with
|
||||||
// a Context, it's managed by the garbage collector. We use the
|
// a Context, it's managed by the garbage collector. We use the
|
||||||
// `lowMemoryNotification` call on the isolate to encourage v8 to free
|
// `lowMemoryNotification` call on the isolate to encourage v8 to free
|
||||||
// any contexts which have been freed.
|
// any contexts which have been freed.
|
||||||
|
// This GC is very aggressive. Use memoryPressureNotification for less
|
||||||
|
// aggressive GC passes.
|
||||||
pub fn lowMemoryNotification(self: *Env) void {
|
pub fn lowMemoryNotification(self: *Env) void {
|
||||||
var handle_scope: v8.HandleScope = undefined;
|
var handle_scope: js.HandleScope = undefined;
|
||||||
v8.HandleScope.init(&handle_scope, self.isolate);
|
handle_scope.init(self.isolate);
|
||||||
defer handle_scope.deinit();
|
defer handle_scope.deinit();
|
||||||
self.isolate.lowMemoryNotification();
|
self.isolate.lowMemoryNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// V8 doesn't immediately free memory associated with
|
||||||
|
// a Context, it's managed by the garbage collector. We use the
|
||||||
|
// `memoryPressureNotification` call on the isolate to encourage v8 to free
|
||||||
|
// any contexts which have been freed.
|
||||||
|
// The level indicates the aggressivity of the GC required:
|
||||||
|
// moderate speeds up incremental GC
|
||||||
|
// critical runs one full GC
|
||||||
|
// For a more aggressive GC, use lowMemoryNotification.
|
||||||
|
pub fn memoryPressureNotification(self: *Env, level: Isolate.MemoryPressureLevel) void {
|
||||||
|
var handle_scope: js.HandleScope = undefined;
|
||||||
|
handle_scope.init(self.isolate);
|
||||||
|
defer handle_scope.deinit();
|
||||||
|
self.isolate.memoryPressureNotification(level);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dumpMemoryStats(self: *Env) void {
|
pub fn dumpMemoryStats(self: *Env) void {
|
||||||
const stats = self.isolate.getHeapStatistics();
|
const stats = self.isolate.getHeapStatistics();
|
||||||
std.debug.print(
|
std.debug.print(
|
||||||
@@ -174,20 +506,63 @@ pub fn dumpMemoryStats(self: *Env) void {
|
|||||||
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
|
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
|
pub fn terminate(self: *const Env) void {
|
||||||
const msg = v8.PromiseRejectMessage.initFromC(v8_msg);
|
v8.v8__Isolate__TerminateExecution(self.isolate.handle);
|
||||||
const isolate = msg.getPromise().toObject().getIsolate();
|
|
||||||
const context = Context.fromIsolate(isolate);
|
|
||||||
|
|
||||||
const value =
|
|
||||||
if (msg.getValue()) |v8_value|
|
|
||||||
context.valueToString(v8_value, .{}) catch |err| @errorName(err)
|
|
||||||
else
|
|
||||||
"no value";
|
|
||||||
|
|
||||||
log.debug(.js, "unhandled rejection", .{
|
|
||||||
.value = value,
|
|
||||||
.stack = context.stackTrace() catch |err| @errorName(err) orelse "???",
|
|
||||||
.note = "This should be updated to call window.unhandledrejection",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
||||||
|
const promise_event = v8.v8__PromiseRejectMessage__GetEvent(&message_handle);
|
||||||
|
if (promise_event != v8.kPromiseRejectWithNoHandler and promise_event != v8.kPromiseHandlerAddedAfterReject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
||||||
|
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
||||||
|
const isolate = js.Isolate{ .handle = v8_isolate };
|
||||||
|
const ctx, const v8_context = Context.fromIsolate(isolate);
|
||||||
|
|
||||||
|
const local = js.Local{
|
||||||
|
.ctx = ctx,
|
||||||
|
.isolate = isolate,
|
||||||
|
.handle = v8_context,
|
||||||
|
.call_arena = ctx.call_arena,
|
||||||
|
};
|
||||||
|
|
||||||
|
const page = ctx.page;
|
||||||
|
page.window.unhandledPromiseRejection(promise_event == v8.kPromiseRejectWithNoHandler, .{
|
||||||
|
.local = &local,
|
||||||
|
.handle = &message_handle,
|
||||||
|
}, page) catch |err| {
|
||||||
|
log.warn(.browser, "unhandled rejection handler", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void {
|
||||||
|
const location = std.mem.span(c_location);
|
||||||
|
const message = std.mem.span(c_message);
|
||||||
|
log.fatal(.app, "V8 fatal callback", .{ .location = location, .message = message });
|
||||||
|
@import("../../crash_handler.zig").crash("Fatal V8 Error", .{ .location = location, .message = message }, @returnAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oomCallback(c_location: [*c]const u8, details: ?*const v8.OOMDetails) callconv(.c) void {
|
||||||
|
const location = std.mem.span(c_location);
|
||||||
|
const detail = if (details) |d| std.mem.span(d.detail) else "";
|
||||||
|
log.fatal(.app, "V8 OOM", .{ .location = location, .detail = detail });
|
||||||
|
@import("../../crash_handler.zig").crash("V8 OOM", .{ .location = location, .detail = detail }, @returnAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
const PrivateSymbols = struct {
|
||||||
|
const Private = @import("Private.zig");
|
||||||
|
|
||||||
|
child_nodes: Private,
|
||||||
|
|
||||||
|
fn init(isolate: *v8.Isolate) PrivateSymbols {
|
||||||
|
return .{
|
||||||
|
.child_nodes = Private.init(isolate, "child_nodes"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *PrivateSymbols) void {
|
||||||
|
self.child_nodes.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
|
||||||
//
|
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
|
||||||
|
|
||||||
const js = @import("js.zig");
|
|
||||||
const v8 = js.v8;
|
|
||||||
|
|
||||||
const Env = @import("Env.zig");
|
|
||||||
const Context = @import("Context.zig");
|
|
||||||
|
|
||||||
const Page = @import("../Page.zig");
|
|
||||||
|
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
||||||
|
|
||||||
const CONTEXT_ARENA_RETAIN = 1024 * 64;
|
|
||||||
|
|
||||||
// ExecutionWorld closely models a JS World.
|
|
||||||
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#World
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
|
|
||||||
const ExecutionWorld = @This();
|
|
||||||
|
|
||||||
env: *Env,
|
|
||||||
|
|
||||||
// Arena whose lifetime is for a single page load. Where
|
|
||||||
// the call_arena lives for a single function call, the context_arena
|
|
||||||
// lives for the lifetime of the entire page. The allocator will be
|
|
||||||
// owned by the Context, but the arena itself is owned by the ExecutionWorld
|
|
||||||
// so that we can re-use it from context to context.
|
|
||||||
context_arena: ArenaAllocator,
|
|
||||||
|
|
||||||
// Currently a context maps to a Browser's Page. Here though, it's only a
|
|
||||||
// mechanism to organization page-specific memory. The ExecutionWorld
|
|
||||||
// does all the work, but having all page-specific data structures
|
|
||||||
// grouped together helps keep things clean.
|
|
||||||
context: ?Context = null,
|
|
||||||
|
|
||||||
// no init, must be initialized via env.newExecutionWorld()
|
|
||||||
|
|
||||||
pub fn deinit(self: *ExecutionWorld) void {
|
|
||||||
if (self.context != null) {
|
|
||||||
self.removeContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.context_arena.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only the top Context in the Main ExecutionWorld should hold a handle_scope.
|
|
||||||
// A v8.HandleScope is like an arena. Once created, any "Local" that
|
|
||||||
// v8 creates will be released (or at least, releasable by the v8 GC)
|
|
||||||
// when the handle_scope is freed.
|
|
||||||
// We also maintain our own "context_arena" which allows us to have
|
|
||||||
// all page related memory easily managed.
|
|
||||||
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context {
|
|
||||||
std.debug.assert(self.context == null);
|
|
||||||
|
|
||||||
const env = self.env;
|
|
||||||
const isolate = env.isolate;
|
|
||||||
const arena = self.context_arena.allocator();
|
|
||||||
|
|
||||||
var v8_context: v8.Context = blk: {
|
|
||||||
var temp_scope: v8.HandleScope = undefined;
|
|
||||||
v8.HandleScope.init(&temp_scope, isolate);
|
|
||||||
defer temp_scope.deinit();
|
|
||||||
|
|
||||||
// Creates a global template that inherits from Window.
|
|
||||||
const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate, env.templates);
|
|
||||||
|
|
||||||
// Add the named property handler
|
|
||||||
global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{
|
|
||||||
.getter = unknownPropertyCallback,
|
|
||||||
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
|
|
||||||
}, null);
|
|
||||||
|
|
||||||
const context_local = v8.Context.init(isolate, global_template, null);
|
|
||||||
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
|
|
||||||
break :blk v8_context;
|
|
||||||
};
|
|
||||||
|
|
||||||
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
|
|
||||||
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
|
|
||||||
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
|
|
||||||
var handle_scope: ?v8.HandleScope = null;
|
|
||||||
if (enter) {
|
|
||||||
handle_scope = @as(v8.HandleScope, undefined);
|
|
||||||
v8.HandleScope.init(&handle_scope.?, isolate);
|
|
||||||
v8_context.enter();
|
|
||||||
}
|
|
||||||
errdefer if (enter) {
|
|
||||||
v8_context.exit();
|
|
||||||
handle_scope.?.deinit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const context_id = env.context_id;
|
|
||||||
env.context_id = context_id + 1;
|
|
||||||
|
|
||||||
self.context = Context{
|
|
||||||
.page = page,
|
|
||||||
.id = context_id,
|
|
||||||
.isolate = isolate,
|
|
||||||
.v8_context = v8_context,
|
|
||||||
.templates = env.templates,
|
|
||||||
.handle_scope = handle_scope,
|
|
||||||
.script_manager = &page._script_manager,
|
|
||||||
.call_arena = page.call_arena,
|
|
||||||
.arena = arena,
|
|
||||||
};
|
|
||||||
|
|
||||||
var context = &self.context.?;
|
|
||||||
// Store a pointer to our context inside the v8 context so that, given
|
|
||||||
// a v8 context, we can get our context out
|
|
||||||
const data = isolate.initBigIntU64(@intCast(@intFromPtr(context)));
|
|
||||||
v8_context.setEmbedderData(1, data);
|
|
||||||
|
|
||||||
try context.setupGlobal();
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn removeContext(self: *ExecutionWorld) void {
|
|
||||||
// Force running the micro task to drain the queue before reseting the
|
|
||||||
// context arena.
|
|
||||||
// Tasks in the queue are relying to the arena memory could be present in
|
|
||||||
// the queue. Running them later could lead to invalid memory accesses.
|
|
||||||
self.env.runMicrotasks();
|
|
||||||
|
|
||||||
self.context.?.deinit();
|
|
||||||
self.context = null;
|
|
||||||
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn terminateExecution(self: *const ExecutionWorld) void {
|
|
||||||
self.env.isolate.terminateExecution();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resumeExecution(self: *const ExecutionWorld) void {
|
|
||||||
self.env.isolate.cancelTerminateExecution();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
|
||||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
|
||||||
|
|
||||||
const context = Context.fromIsolate(info.getIsolate());
|
|
||||||
const maybe_property: ?[]u8 = context.valueToString(.{ .handle = c_name.? }, .{}) catch null;
|
|
||||||
|
|
||||||
const ignored = std.StaticStringMap(void).initComptime(.{
|
|
||||||
.{ "process", {} },
|
|
||||||
.{ "ShadyDOM", {} },
|
|
||||||
.{ "ShadyCSS", {} },
|
|
||||||
|
|
||||||
.{ "litNonce", {} },
|
|
||||||
.{ "litHtmlVersions", {} },
|
|
||||||
.{ "litElementVersions", {} },
|
|
||||||
.{ "litHtmlPolyfillSupport", {} },
|
|
||||||
.{ "litElementHydrateSupport", {} },
|
|
||||||
.{ "litElementPolyfillSupport", {} },
|
|
||||||
.{ "reactiveElementVersions", {} },
|
|
||||||
|
|
||||||
.{ "recaptcha", {} },
|
|
||||||
.{ "grecaptcha", {} },
|
|
||||||
.{ "___grecaptcha_cfg", {} },
|
|
||||||
.{ "__recaptcha_api", {} },
|
|
||||||
.{ "__google_recaptcha_client", {} },
|
|
||||||
|
|
||||||
.{ "CLOSURE_FLAGS", {} },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (maybe_property) |prop| {
|
|
||||||
if (!ignored.has(prop)) {
|
|
||||||
const page = context.page;
|
|
||||||
const document = page.document;
|
|
||||||
|
|
||||||
if (document.getElementById(prop, page)) |el| {
|
|
||||||
const js_value = context.zigValueToJs(el, .{}) catch {
|
|
||||||
return v8.Intercepted.No;
|
|
||||||
};
|
|
||||||
|
|
||||||
info.getReturnValue().set(js_value);
|
|
||||||
return v8.Intercepted.Yes;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug(.unknown_prop, "unknown global property", .{
|
|
||||||
.info = "but the property can exist in pure JS",
|
|
||||||
.stack = context.stackTrace() catch "???",
|
|
||||||
.property = prop,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return v8.Intercepted.No;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -20,101 +20,91 @@ const std = @import("std");
|
|||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const PersistentFunction = v8.Persistent(v8.Function);
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const Function = @This();
|
const Function = @This();
|
||||||
|
|
||||||
id: usize,
|
local: *const js.Local,
|
||||||
context: *js.Context,
|
this: ?*const v8.Object = null,
|
||||||
this: ?v8.Object = null,
|
handle: *const v8.Function,
|
||||||
func: PersistentFunction,
|
|
||||||
|
|
||||||
pub const Result = struct {
|
pub const Result = struct {
|
||||||
stack: ?[]const u8,
|
stack: ?[]const u8,
|
||||||
exception: []const u8,
|
exception: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 {
|
|
||||||
const name = self.func.castToFunction().getName();
|
|
||||||
return self.context.valueToString(name, .{ .allocator = allocator });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setName(self: *const Function, name: []const u8) void {
|
|
||||||
const v8_name = v8.String.initUtf8(self.context.isolate, name);
|
|
||||||
self.func.castToFunction().setName(v8_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn withThis(self: *const Function, value: anytype) !Function {
|
pub fn withThis(self: *const Function, value: anytype) !Function {
|
||||||
|
const local = self.local;
|
||||||
const this_obj = if (@TypeOf(value) == js.Object)
|
const this_obj = if (@TypeOf(value) == js.Object)
|
||||||
value.js_obj
|
value.handle
|
||||||
else
|
else
|
||||||
(try self.context.zigValueToJs(value, .{})).castTo(v8.Object);
|
(try local.zigValueToJs(value, .{})).handle;
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.id = self.id,
|
.local = local,
|
||||||
.this = this_obj,
|
.this = this_obj,
|
||||||
.func = self.func,
|
.handle = self.handle,
|
||||||
.context = self.context,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn newInstance(self: *const Function, result: *Result) !js.Object {
|
pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object {
|
||||||
const context = self.context;
|
const local = self.local;
|
||||||
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(context);
|
try_catch.init(local);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
// This creates a new instance using this Function as a constructor.
|
// This creates a new instance using this Function as a constructor.
|
||||||
// This returns a generic Object
|
// const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
|
||||||
const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse {
|
const handle = v8.v8__Function__NewInstance(self.handle, local.handle, 0, null) orelse {
|
||||||
if (try_catch.hasCaught()) {
|
caught.* = try_catch.caughtOrError(local.call_arena, error.Unknown);
|
||||||
const allocator = context.call_arena;
|
|
||||||
result.stack = try_catch.stack(allocator) catch null;
|
|
||||||
result.exception = (try_catch.exception(allocator) catch "???") orelse "???";
|
|
||||||
} else {
|
|
||||||
result.stack = null;
|
|
||||||
result.exception = "???";
|
|
||||||
}
|
|
||||||
return error.JsConstructorFailed;
|
return error.JsConstructorFailed;
|
||||||
};
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.context = context,
|
.local = local,
|
||||||
.js_obj = js_obj,
|
.handle = handle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
||||||
return self.callWithThis(T, self.getThis(), args);
|
var caught: js.TryCatch.Caught = undefined;
|
||||||
|
return self._tryCallWithThis(T, self.getThis(), args, &caught, .{}) catch |err| {
|
||||||
|
log.warn(.js, "call caught", .{ .err = err, .caught = caught });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T {
|
pub fn callRethrow(self: *const Function, comptime T: type, args: anytype) !T {
|
||||||
return self.tryCallWithThis(T, self.getThis(), args, result);
|
var caught: js.TryCatch.Caught = undefined;
|
||||||
}
|
return self._tryCallWithThis(T, self.getThis(), args, &caught, .{ .rethrow = true }) catch |err| {
|
||||||
|
log.warn(.js, "call caught", .{ .err = err, .caught = caught });
|
||||||
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !T {
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
|
||||||
try_catch.init(self.context);
|
|
||||||
defer try_catch.deinit();
|
|
||||||
|
|
||||||
return self.callWithThis(T, this, args) catch |err| {
|
|
||||||
if (try_catch.hasCaught()) {
|
|
||||||
const allocator = self.context.call_arena;
|
|
||||||
result.stack = try_catch.stack(allocator) catch null;
|
|
||||||
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
|
|
||||||
} else {
|
|
||||||
result.stack = null;
|
|
||||||
result.exception = @errorName(err);
|
|
||||||
}
|
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
||||||
const context = self.context;
|
var caught: js.TryCatch.Caught = undefined;
|
||||||
|
return self._tryCallWithThis(T, this, args, &caught, .{}) catch |err| {
|
||||||
|
log.warn(.js, "callWithThis caught", .{ .err = err, .caught = caught });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T {
|
||||||
|
return self._tryCallWithThis(T, self.getThis(), args, caught, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
|
||||||
|
return self._tryCallWithThis(T, this, args, caught, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
const CallOpts = struct {
|
||||||
|
rethrow: bool = false,
|
||||||
|
};
|
||||||
|
fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught, comptime opts: CallOpts) !T {
|
||||||
|
caught.* = .{};
|
||||||
|
const local = self.local;
|
||||||
|
|
||||||
// When we're calling a function from within JavaScript itself, this isn't
|
// When we're calling a function from within JavaScript itself, this isn't
|
||||||
// necessary. We're within a Caller instantiation, which will already have
|
// necessary. We're within a Caller instantiation, which will already have
|
||||||
@@ -125,65 +115,152 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
|
|||||||
// need to increase the call_depth so that the call_arena remains valid for
|
// need to increase the call_depth so that the call_arena remains valid for
|
||||||
// the duration of the function call. If we don't do this, the call_arena
|
// the duration of the function call. If we don't do this, the call_arena
|
||||||
// will be reset after each statement of the function which executes Zig code.
|
// will be reset after each statement of the function which executes Zig code.
|
||||||
const call_depth = context.call_depth;
|
const ctx = local.ctx;
|
||||||
context.call_depth = call_depth + 1;
|
const call_depth = ctx.call_depth;
|
||||||
defer context.call_depth = call_depth;
|
ctx.call_depth = call_depth + 1;
|
||||||
|
defer ctx.call_depth = call_depth;
|
||||||
|
|
||||||
const js_this = blk: {
|
const js_this = blk: {
|
||||||
if (@TypeOf(this) == v8.Object) {
|
if (@TypeOf(this) == js.Object) {
|
||||||
break :blk this;
|
break :blk this;
|
||||||
}
|
}
|
||||||
|
break :blk try local.zigValueToJs(this, .{});
|
||||||
if (@TypeOf(this) == js.Object) {
|
|
||||||
break :blk this.js_obj;
|
|
||||||
}
|
|
||||||
break :blk try context.zigValueToJs(this, .{});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
|
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
|
||||||
|
|
||||||
const js_args: []const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
|
const js_args: []const *const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
|
||||||
.@"struct" => |s| blk: {
|
.@"struct" => |s| blk: {
|
||||||
const fields = s.fields;
|
const fields = s.fields;
|
||||||
var js_args: [fields.len]v8.Value = undefined;
|
var js_args: [fields.len]*const v8.Value = undefined;
|
||||||
inline for (fields, 0..) |f, i| {
|
inline for (fields, 0..) |f, i| {
|
||||||
js_args[i] = try context.zigValueToJs(@field(aargs, f.name), .{});
|
js_args[i] = (try local.zigValueToJs(@field(aargs, f.name), .{})).handle;
|
||||||
}
|
}
|
||||||
const cargs: [fields.len]v8.Value = js_args;
|
const cargs: [fields.len]*const v8.Value = js_args;
|
||||||
break :blk &cargs;
|
break :blk &cargs;
|
||||||
},
|
},
|
||||||
.pointer => blk: {
|
.pointer => blk: {
|
||||||
var values = try context.call_arena.alloc(v8.Value, args.len);
|
var values = try local.call_arena.alloc(*const v8.Value, args.len);
|
||||||
for (args, 0..) |a, i| {
|
for (args, 0..) |a, i| {
|
||||||
values[i] = try context.zigValueToJs(a, .{});
|
values[i] = (try local.zigValueToJs(a, .{})).handle;
|
||||||
}
|
}
|
||||||
break :blk values;
|
break :blk values;
|
||||||
},
|
},
|
||||||
else => @compileError("JS Function called with invalid paremter type"),
|
else => @compileError("JS Function called with invalid paremter type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
|
const c_args = @as(?[*]const ?*v8.Value, @ptrCast(js_args.ptr));
|
||||||
if (result == null) {
|
|
||||||
// std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
|
|
||||||
return error.JSExecCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (@typeInfo(T) == .void) return {};
|
var try_catch: js.TryCatch = undefined;
|
||||||
return context.jsValueToZig(T, result.?);
|
try_catch.init(local);
|
||||||
|
defer try_catch.deinit();
|
||||||
|
|
||||||
|
const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {
|
||||||
|
if ((comptime opts.rethrow) and try_catch.hasCaught()) {
|
||||||
|
try_catch.rethrow();
|
||||||
|
return error.TryCatchRethrow;
|
||||||
|
}
|
||||||
|
caught.* = try_catch.caughtOrError(local.call_arena, error.JsException);
|
||||||
|
return error.JsException;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (@typeInfo(T) == .void) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return local.jsValueToZig(T, .{ .local = local, .handle = handle });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getThis(self: *const Function) v8.Object {
|
fn getThis(self: *const Function) js.Object {
|
||||||
return self.this orelse self.context.v8_context.getGlobal();
|
const handle = if (self.this) |t| t else v8.v8__Context__Global(self.local.handle).?;
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = handle,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn src(self: *const Function) ![]const u8 {
|
pub fn src(self: *const Function) ![]const u8 {
|
||||||
const value = self.func.castToFunction().toValue();
|
return self.local.valueToString(.{ .local = self.local, .handle = @ptrCast(self.handle) }, .{});
|
||||||
return self.context.valueToString(value, .{});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
|
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
|
||||||
const func_obj = self.func.castToFunction().toObject();
|
const local = self.local;
|
||||||
const key = v8.String.initUtf8(self.context.isolate, name);
|
const key = local.isolate.initStringHandle(name);
|
||||||
const value = func_obj.getValue(self.context.v8_context, key) catch return null;
|
const handle = v8.v8__Object__Get(self.handle, self.local.handle, key) orelse {
|
||||||
return self.context.createValue(value);
|
return error.JsException;
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.local = local,
|
||||||
|
.handle = handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: *const Function) !Global {
|
||||||
|
return self._persist(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn temp(self: *const Function) !Temp {
|
||||||
|
return self._persist(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
if (comptime is_global) {
|
||||||
|
try ctx.trackGlobal(global);
|
||||||
|
return .{ .handle = global, .temps = {} };
|
||||||
|
}
|
||||||
|
try ctx.trackTemp(global);
|
||||||
|
return .{ .handle = global, .temps = &ctx.identity.temps };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tempWithThis(self: *const Function, value: anytype) !Temp {
|
||||||
|
const with_this = try self.withThis(value);
|
||||||
|
return with_this.temp();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persistWithThis(self: *const Function, value: anytype) !Global {
|
||||||
|
const with_this = try self.withThis(value);
|
||||||
|
return with_this.persist();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Temp = G(.temp);
|
||||||
|
pub const Global = G(.global);
|
||||||
|
|
||||||
|
const GlobalType = enum(u8) {
|
||||||
|
temp,
|
||||||
|
global,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn G(comptime global_type: GlobalType) type {
|
||||||
|
return struct {
|
||||||
|
handle: v8.Global,
|
||||||
|
temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Self, l: *const js.Local) Function {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEqual(self: *const Self, other: Function) bool {
|
||||||
|
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(self: *const Self) void {
|
||||||
|
if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| {
|
||||||
|
var g = kv.value;
|
||||||
|
v8.v8__Global__Reset(&g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/browser/js/HandleScope.zig
Normal file
40
src/browser/js/HandleScope.zig
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// 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 js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const HandleScope = @This();
|
||||||
|
|
||||||
|
handle: v8.HandleScope,
|
||||||
|
|
||||||
|
// V8 takes an address of the value that's passed in, so it needs to be stable.
|
||||||
|
// We can't create the v8.HandleScope here, pass it to v8 and then return the
|
||||||
|
// value, as v8 will then have taken the address of the function-scopped (and no
|
||||||
|
// longer valid) local.
|
||||||
|
pub fn init(self: *HandleScope, isolate: js.Isolate) void {
|
||||||
|
self.initWithIsolateHandle(isolate.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initWithIsolateHandle(self: *HandleScope, isolate: *v8.Isolate) void {
|
||||||
|
v8.v8__HandleScope__CONSTRUCT(&self.handle, isolate);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *HandleScope) void {
|
||||||
|
v8.v8__HandleScope__DESTRUCT(&self.handle);
|
||||||
|
}
|
||||||
75
src/browser/js/Identity.zig
Normal file
75
src/browser/js/Identity.zig
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// Identity manages the mapping between Zig instances and their v8::Object wrappers.
|
||||||
|
// This provides object identity semantics - the same Zig instance always maps to
|
||||||
|
// the same JS object within a given Identity scope.
|
||||||
|
//
|
||||||
|
// Main world contexts share a single Identity (on Session), ensuring that
|
||||||
|
// `window.top.document === top's document` works across same-origin frames.
|
||||||
|
//
|
||||||
|
// Isolated worlds (CDP inspector) have their own Identity, ensuring their
|
||||||
|
// v8::Global wrappers don't leak into the main world.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Identity = @This();
|
||||||
|
|
||||||
|
// Maps Zig instance pointers to their v8::Global(Object) wrappers.
|
||||||
|
identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||||
|
|
||||||
|
// Tracked global v8 objects that need to be released on cleanup.
|
||||||
|
globals: std.ArrayList(v8.Global) = .empty,
|
||||||
|
|
||||||
|
// Temporary v8 globals that can be released early. Key is global.data_ptr.
|
||||||
|
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||||
|
|
||||||
|
// Finalizer callbacks for weak references. Key is @intFromPtr of the Zig instance.
|
||||||
|
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *Session.FinalizerCallback) = .empty,
|
||||||
|
|
||||||
|
pub fn deinit(self: *Identity) void {
|
||||||
|
{
|
||||||
|
var it = self.finalizer_callbacks.valueIterator();
|
||||||
|
while (it.next()) |finalizer| {
|
||||||
|
finalizer.*.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var it = self.identity_map.valueIterator();
|
||||||
|
while (it.next()) |global| {
|
||||||
|
v8.v8__Global__Reset(global);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self.globals.items) |*global| {
|
||||||
|
v8.v8__Global__Reset(global);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var it = self.temps.valueIterator();
|
||||||
|
while (it.next()) |global| {
|
||||||
|
v8.v8__Global__Reset(global);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,63 +20,79 @@ const std = @import("std");
|
|||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const Context = @import("Context.zig");
|
const TaggedOpaque = @import("TaggedOpaque.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const CONTEXT_GROUP_ID = 1;
|
||||||
|
const CLIENT_TRUST_LEVEL = 1;
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
|
// Inspector exists for the lifetime of the Isolate/Env. 1 Isolate = 1 Inspector.
|
||||||
|
// It combines the v8.Inspector and the v8.InspectorClientImpl. The v8.InspectorClientImpl
|
||||||
|
// is our own implementation that fulfills the InspectorClient API, i.e. it's the
|
||||||
|
// mechanism v8 provides to let us tweak how the inspector works. For example, it
|
||||||
|
// Below, you'll find a few pub export fn v8_inspector__Client__IMPL__XYZ functions
|
||||||
|
// which is our implementation of what the v8::Inspector requires of our Client
|
||||||
|
// (not much at all)
|
||||||
const Inspector = @This();
|
const Inspector = @This();
|
||||||
|
|
||||||
pub const RemoteObject = v8.RemoteObject;
|
unique_id: i64,
|
||||||
|
isolate: *v8.Isolate,
|
||||||
|
handle: *v8.Inspector,
|
||||||
|
client: *v8.InspectorClientImpl,
|
||||||
|
default_context: ?v8.Global,
|
||||||
|
session: ?Session,
|
||||||
|
|
||||||
isolate: v8.Isolate,
|
pub fn init(allocator: Allocator, isolate: *v8.Isolate) !*Inspector {
|
||||||
inner: *v8.Inspector,
|
const self = try allocator.create(Inspector);
|
||||||
session: v8.InspectorSession,
|
errdefer allocator.destroy(self);
|
||||||
|
|
||||||
// We expect allocator to be an arena
|
self.* = .{
|
||||||
pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector {
|
.unique_id = 1,
|
||||||
const ContextT = @TypeOf(ctx);
|
.session = null,
|
||||||
|
.isolate = isolate,
|
||||||
const InspectorContainer = switch (@typeInfo(ContextT)) {
|
.client = undefined,
|
||||||
.@"struct" => ContextT,
|
.handle = undefined,
|
||||||
.pointer => |ptr| ptr.child,
|
.default_context = null,
|
||||||
.void => NoopInspector,
|
|
||||||
else => @compileError("invalid context type"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// If necessary, turn a void context into something we can safely ptrCast
|
self.client = v8.v8_inspector__Client__IMPL__CREATE();
|
||||||
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
|
errdefer v8.v8_inspector__Client__IMPL__DELETE(self.client);
|
||||||
|
v8.v8_inspector__Client__IMPL__SET_DATA(self.client, self);
|
||||||
|
|
||||||
const channel = v8.InspectorChannel.init(
|
self.handle = v8.v8_inspector__Inspector__Create(isolate, self.client).?;
|
||||||
safe_context,
|
errdefer v8.v8_inspector__Inspector__DELETE(self.handle);
|
||||||
InspectorContainer.onInspectorResponse,
|
|
||||||
InspectorContainer.onInspectorEvent,
|
|
||||||
InspectorContainer.onRunMessageLoopOnPause,
|
|
||||||
InspectorContainer.onQuitMessageLoopOnPause,
|
|
||||||
isolate,
|
|
||||||
);
|
|
||||||
|
|
||||||
const client = v8.InspectorClient.init();
|
return self;
|
||||||
|
|
||||||
const inner = try allocator.create(v8.Inspector);
|
|
||||||
v8.Inspector.init(inner, client, channel, isolate);
|
|
||||||
return .{ .inner = inner, .isolate = isolate, .session = inner.connect() };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *const Inspector) void {
|
pub fn deinit(self: *const Inspector, allocator: Allocator) void {
|
||||||
self.session.deinit();
|
var hs: v8.HandleScope = undefined;
|
||||||
self.inner.deinit();
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
|
||||||
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
|
if (self.session) |*s| {
|
||||||
|
s.deinit();
|
||||||
|
}
|
||||||
|
v8.v8_inspector__Client__IMPL__DELETE(self.client);
|
||||||
|
v8.v8_inspector__Inspector__DELETE(self.handle);
|
||||||
|
allocator.destroy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(self: *const Inspector, msg: []const u8) void {
|
pub fn startSession(self: *Inspector, ctx: anytype) *Session {
|
||||||
// Can't assume the main Context exists (with its HandleScope)
|
if (comptime IS_DEBUG) {
|
||||||
// available when doing this. Pages (and thus the HandleScope)
|
std.debug.assert(self.session == null);
|
||||||
// comes and goes, but CDP can keep sending messages.
|
}
|
||||||
const isolate = self.isolate;
|
|
||||||
var temp_scope: v8.HandleScope = undefined;
|
|
||||||
v8.HandleScope.init(&temp_scope, isolate);
|
|
||||||
defer temp_scope.deinit();
|
|
||||||
|
|
||||||
self.session.dispatchProtocolMessage(isolate, msg);
|
self.session = @as(Session, undefined);
|
||||||
|
Session.init(&self.session.?, self, ctx);
|
||||||
|
return &self.session.?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stopSession(self: *Inspector) void {
|
||||||
|
self.session.?.deinit();
|
||||||
|
self.session = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From CDP docs
|
// From CDP docs
|
||||||
@@ -88,75 +104,359 @@ pub fn send(self: *const Inspector, msg: []const u8) void {
|
|||||||
// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
|
// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
|
||||||
// - is_default_context: Whether the execution context is default, should match the auxData
|
// - is_default_context: Whether the execution context is default, should match the auxData
|
||||||
pub fn contextCreated(
|
pub fn contextCreated(
|
||||||
self: *const Inspector,
|
self: *Inspector,
|
||||||
context: *const Context,
|
local: *const js.Local,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
origin: []const u8,
|
origin: []const u8,
|
||||||
aux_data: ?[]const u8,
|
aux_data: []const u8,
|
||||||
is_default_context: bool,
|
is_default_context: bool,
|
||||||
) void {
|
) void {
|
||||||
self.inner.contextCreated(context.v8_context, name, origin, aux_data, is_default_context);
|
v8.v8_inspector__Inspector__ContextCreated(
|
||||||
}
|
self.handle,
|
||||||
|
name.ptr,
|
||||||
// Retrieves the RemoteObject for a given value.
|
name.len,
|
||||||
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
|
origin.ptr,
|
||||||
// just like a method return value. Therefore, if we've mapped this
|
origin.len,
|
||||||
// value before, we'll get the existing JS PersistedObject and if not
|
aux_data.ptr,
|
||||||
// we'll create it and track it for cleanup when the context ends.
|
aux_data.len,
|
||||||
pub fn getRemoteObject(
|
CONTEXT_GROUP_ID,
|
||||||
self: *const Inspector,
|
local.handle,
|
||||||
context: *Context,
|
|
||||||
group: []const u8,
|
|
||||||
value: anytype,
|
|
||||||
) !RemoteObject {
|
|
||||||
const js_value = try context.zigValueToJs(value, .{});
|
|
||||||
|
|
||||||
// We do not want to expose this as a parameter for now
|
|
||||||
const generate_preview = false;
|
|
||||||
return self.session.wrapObject(
|
|
||||||
context.isolate,
|
|
||||||
context.v8_context,
|
|
||||||
js_value,
|
|
||||||
group,
|
|
||||||
generate_preview,
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Gets a value by object ID regardless of which context it is in.
|
if (is_default_context) {
|
||||||
// Our TaggedAnyOpaque stores the "resolved" ptr value (the most specific _type,
|
self.default_context = local.ctx.handle;
|
||||||
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
|
|
||||||
// the pointer to the Node, so we need to use the same resolution mechanism which
|
|
||||||
// is used when we're calling a function to turn the Div into a Node, which is
|
|
||||||
// what Context.typeTaggedAnyOpaque does.
|
|
||||||
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !*anyopaque {
|
|
||||||
const unwrapped = try self.session.unwrapObject(allocator, object_id);
|
|
||||||
// The values context and groupId are not used here
|
|
||||||
const js_val = unwrapped.value;
|
|
||||||
if (js_val.isObject() == false) {
|
|
||||||
return error.ObjectIdIsNotANode;
|
|
||||||
}
|
}
|
||||||
const Node = @import("../webapi/Node.zig");
|
|
||||||
return Context.typeTaggedAnyOpaque(*Node, js_val.castTo(v8.Object)) catch {
|
|
||||||
return error.ObjectIdIsNotANode;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoopInspector = struct {
|
pub fn contextDestroyed(self: *Inspector, context: *const v8.Context) void {
|
||||||
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
v8.v8_inspector__Inspector__ContextDestroyed(self.handle, context);
|
||||||
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
|
|
||||||
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {}
|
if (self.default_context) |*dc| {
|
||||||
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
|
if (v8.v8__Global__IsEqual(dc, context)) {
|
||||||
|
self.default_context = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resetContextGroup(self: *const Inspector) void {
|
||||||
|
var hs: v8.HandleScope = undefined;
|
||||||
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
|
||||||
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
|
v8.v8_inspector__Inspector__ResetContextGroup(self.handle, CONTEXT_GROUP_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const RemoteObject = struct {
|
||||||
|
handle: *v8.RemoteObject,
|
||||||
|
|
||||||
|
pub fn deinit(self: RemoteObject) void {
|
||||||
|
v8.v8_inspector__RemoteObject__DELETE(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getType(self: RemoteObject, allocator: Allocator) ![]const u8 {
|
||||||
|
var ctype_: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
if (!v8.v8_inspector__RemoteObject__getType(self.handle, &allocator, &ctype_)) return error.V8AllocFailed;
|
||||||
|
return cZigStringToString(ctype_) orelse return error.InvalidType;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getSubtype(self: RemoteObject, allocator: Allocator) !?[]const u8 {
|
||||||
|
if (!v8.v8_inspector__RemoteObject__hasSubtype(self.handle)) return null;
|
||||||
|
|
||||||
|
var csubtype: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
if (!v8.v8_inspector__RemoteObject__getSubtype(self.handle, &allocator, &csubtype)) return error.V8AllocFailed;
|
||||||
|
return cZigStringToString(csubtype);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getClassName(self: RemoteObject, allocator: Allocator) !?[]const u8 {
|
||||||
|
if (!v8.v8_inspector__RemoteObject__hasClassName(self.handle)) return null;
|
||||||
|
|
||||||
|
var cclass_name: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
if (!v8.v8_inspector__RemoteObject__getClassName(self.handle, &allocator, &cclass_name)) return error.V8AllocFailed;
|
||||||
|
return cZigStringToString(cclass_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getDescription(self: RemoteObject, allocator: Allocator) !?[]const u8 {
|
||||||
|
if (!v8.v8_inspector__RemoteObject__hasDescription(self.handle)) return null;
|
||||||
|
|
||||||
|
var description: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
if (!v8.v8_inspector__RemoteObject__getDescription(self.handle, &allocator, &description)) return error.V8AllocFailed;
|
||||||
|
return cZigStringToString(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getObjectId(self: RemoteObject, allocator: Allocator) !?[]const u8 {
|
||||||
|
if (!v8.v8_inspector__RemoteObject__hasObjectId(self.handle)) return null;
|
||||||
|
|
||||||
|
var cobject_id: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
if (!v8.v8_inspector__RemoteObject__getObjectId(self.handle, &allocator, &cobject_id)) return error.V8AllocFailed;
|
||||||
|
return cZigStringToString(cobject_id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
|
// Combines a v8::InspectorSession and a v8::InspectorChannelImpl. The
|
||||||
if (value.isObject() == false) {
|
// InspectorSession is for zig -> v8 (sending messages to the inspector). The
|
||||||
|
// Channel is for v8 -> zig, getting events from the Inspector (that we'll pass
|
||||||
|
// back ot some opaque context, i.e the CDP BrowserContext).
|
||||||
|
// The channel callbacks are defined below, as:
|
||||||
|
// pub export fn v8_inspector__Channel__IMPL__XYZ
|
||||||
|
pub const Session = struct {
|
||||||
|
inspector: *Inspector,
|
||||||
|
handle: *v8.InspectorSession,
|
||||||
|
channel: *v8.InspectorChannelImpl,
|
||||||
|
|
||||||
|
// callbacks
|
||||||
|
ctx: *anyopaque,
|
||||||
|
onNotif: *const fn (ctx: *anyopaque, msg: []const u8) void,
|
||||||
|
onResp: *const fn (ctx: *anyopaque, call_id: u32, msg: []const u8) void,
|
||||||
|
|
||||||
|
fn init(self: *Session, inspector: *Inspector, ctx: anytype) void {
|
||||||
|
const Container = @typeInfo(@TypeOf(ctx)).pointer.child;
|
||||||
|
|
||||||
|
const channel = v8.v8_inspector__Channel__IMPL__CREATE(inspector.isolate);
|
||||||
|
const handle = v8.v8_inspector__Inspector__Connect(
|
||||||
|
inspector.handle,
|
||||||
|
CONTEXT_GROUP_ID,
|
||||||
|
channel,
|
||||||
|
CLIENT_TRUST_LEVEL,
|
||||||
|
).?;
|
||||||
|
v8.v8_inspector__Channel__IMPL__SET_DATA(channel, self);
|
||||||
|
|
||||||
|
self.* = .{
|
||||||
|
.ctx = ctx,
|
||||||
|
.handle = handle,
|
||||||
|
.channel = channel,
|
||||||
|
.inspector = inspector,
|
||||||
|
.onResp = Container.onInspectorResponse,
|
||||||
|
.onNotif = Container.onInspectorEvent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *const Session) void {
|
||||||
|
v8.v8_inspector__Session__DELETE(self.handle);
|
||||||
|
v8.v8_inspector__Channel__IMPL__DELETE(self.channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(self: *const Session, msg: []const u8) void {
|
||||||
|
const isolate = self.inspector.isolate;
|
||||||
|
var hs: v8.HandleScope = undefined;
|
||||||
|
v8.v8__HandleScope__CONSTRUCT(&hs, isolate);
|
||||||
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
|
v8.v8_inspector__Session__dispatchProtocolMessage(
|
||||||
|
self.handle,
|
||||||
|
isolate,
|
||||||
|
msg.ptr,
|
||||||
|
msg.len,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets a value by object ID regardless of which context it is in.
|
||||||
|
// Our TaggedOpaque stores the "resolved" ptr value (the most specific _type,
|
||||||
|
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
|
||||||
|
// the pointer to the Node, so we need to use the same resolution mechanism which
|
||||||
|
// is used when we're calling a function to turn the Div into a Node, which is
|
||||||
|
// what TaggedOpaque.fromJS does.
|
||||||
|
pub fn getNodePtr(self: *const Session, allocator: Allocator, object_id: []const u8, local: *js.Local) !*anyopaque {
|
||||||
|
// just to indicate that the caller is responsible for ensuring there's a local environment
|
||||||
|
_ = local;
|
||||||
|
|
||||||
|
const unwrapped = try self.unwrapObject(allocator, object_id);
|
||||||
|
// The values context and groupId are not used here
|
||||||
|
const js_val = unwrapped.value;
|
||||||
|
if (!v8.v8__Value__IsObject(js_val)) {
|
||||||
|
return error.ObjectIdIsNotANode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Node = @import("../webapi/Node.zig");
|
||||||
|
// Cast to *const v8.Object for typeTaggedAnyOpaque
|
||||||
|
return TaggedOpaque.fromJS(*Node, @ptrCast(js_val)) catch return error.ObjectIdIsNotANode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieves the RemoteObject for a given value.
|
||||||
|
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
|
||||||
|
// just like a method return value. Therefore, if we've mapped this
|
||||||
|
// value before, we'll get the existing js.Global(js.Object) and if not
|
||||||
|
// we'll create it and track it for cleanup when the context ends.
|
||||||
|
pub fn getRemoteObject(
|
||||||
|
self: *const Session,
|
||||||
|
local: *const js.Local,
|
||||||
|
group: []const u8,
|
||||||
|
value: anytype,
|
||||||
|
) !RemoteObject {
|
||||||
|
const js_val = try local.zigValueToJs(value, .{});
|
||||||
|
|
||||||
|
// We do not want to expose this as a parameter for now
|
||||||
|
const generate_preview = false;
|
||||||
|
return self.wrapObject(
|
||||||
|
local.isolate.handle,
|
||||||
|
local.handle,
|
||||||
|
js_val.handle,
|
||||||
|
group,
|
||||||
|
generate_preview,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrapObject(
|
||||||
|
self: Session,
|
||||||
|
isolate: *v8.Isolate,
|
||||||
|
ctx: *const v8.Context,
|
||||||
|
val: *const v8.Value,
|
||||||
|
grpname: []const u8,
|
||||||
|
generatepreview: bool,
|
||||||
|
) !RemoteObject {
|
||||||
|
const remote_object = v8.v8_inspector__Session__wrapObject(
|
||||||
|
self.handle,
|
||||||
|
isolate,
|
||||||
|
ctx,
|
||||||
|
val,
|
||||||
|
grpname.ptr,
|
||||||
|
grpname.len,
|
||||||
|
generatepreview,
|
||||||
|
).?;
|
||||||
|
return .{ .handle = remote_object };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unwrapObject(
|
||||||
|
self: Session,
|
||||||
|
allocator: Allocator,
|
||||||
|
object_id: []const u8,
|
||||||
|
) !UnwrappedObject {
|
||||||
|
const in_object_id = v8.CZigString{
|
||||||
|
.ptr = object_id.ptr,
|
||||||
|
.len = object_id.len,
|
||||||
|
};
|
||||||
|
var out_error: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
var out_value_handle: ?*v8.Value = null;
|
||||||
|
var out_context_handle: ?*v8.Context = null;
|
||||||
|
var out_object_group: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||||
|
|
||||||
|
const result = v8.v8_inspector__Session__unwrapObject(
|
||||||
|
self.handle,
|
||||||
|
&allocator,
|
||||||
|
&out_error,
|
||||||
|
in_object_id,
|
||||||
|
&out_value_handle,
|
||||||
|
&out_context_handle,
|
||||||
|
&out_object_group,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
const error_str = cZigStringToString(out_error) orelse return error.UnwrapFailed;
|
||||||
|
std.log.err("unwrapObject failed: {s}", .{error_str});
|
||||||
|
return error.UnwrapFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.value = out_value_handle.?,
|
||||||
|
.context = out_context_handle.?,
|
||||||
|
.object_group = cZigStringToString(out_object_group),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const UnwrappedObject = struct {
|
||||||
|
value: *const v8.Value,
|
||||||
|
context: *const v8.Context,
|
||||||
|
object_group: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getTaggedOpaque(value: *const v8.Value) ?*TaggedOpaque {
|
||||||
|
if (!v8.v8__Value__IsObject(value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const obj = value.castTo(v8.Object);
|
const internal_field_count = v8.v8__Object__InternalFieldCount(value);
|
||||||
if (obj.internalFieldCount() == 0) {
|
if (internal_field_count == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const external_data = obj.getInternalField(0).castTo(v8.External).get().?;
|
const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(value, 0).?;
|
||||||
return @ptrCast(@alignCast(external_data));
|
return @ptrCast(@alignCast(tao_ptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cZigStringToString(s: v8.CZigString) ?[]const u8 {
|
||||||
|
if (s.ptr == null) return null;
|
||||||
|
return s.ptr[0..s.len];
|
||||||
|
}
|
||||||
|
|
||||||
|
// C export functions for Inspector callbacks
|
||||||
|
pub export fn v8_inspector__Client__IMPL__generateUniqueId(
|
||||||
|
_: *v8.InspectorClientImpl,
|
||||||
|
data: *anyopaque,
|
||||||
|
) callconv(.c) i64 {
|
||||||
|
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
||||||
|
const unique_id = inspector.unique_id + 1;
|
||||||
|
inspector.unique_id = unique_id;
|
||||||
|
return unique_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Client__IMPL__runMessageLoopOnPause(
|
||||||
|
_: *v8.InspectorClientImpl,
|
||||||
|
data: *anyopaque,
|
||||||
|
context_group_id: c_int,
|
||||||
|
) callconv(.c) void {
|
||||||
|
_ = data;
|
||||||
|
_ = context_group_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Client__IMPL__quitMessageLoopOnPause(
|
||||||
|
_: *v8.InspectorClientImpl,
|
||||||
|
data: *anyopaque,
|
||||||
|
) callconv(.c) void {
|
||||||
|
_ = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Client__IMPL__runIfWaitingForDebugger(
|
||||||
|
_: *v8.InspectorClientImpl,
|
||||||
|
_: *anyopaque,
|
||||||
|
_: c_int,
|
||||||
|
) callconv(.c) void {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Client__IMPL__consoleAPIMessage(
|
||||||
|
_: *v8.InspectorClientImpl,
|
||||||
|
_: *anyopaque,
|
||||||
|
_: c_int,
|
||||||
|
_: v8.MessageErrorLevel,
|
||||||
|
_: *v8.StringView,
|
||||||
|
_: *v8.StringView,
|
||||||
|
_: c_uint,
|
||||||
|
_: c_uint,
|
||||||
|
_: *v8.StackTrace,
|
||||||
|
) callconv(.c) void {}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Client__IMPL__ensureDefaultContextInGroup(
|
||||||
|
_: *v8.InspectorClientImpl,
|
||||||
|
data: *anyopaque,
|
||||||
|
) callconv(.c) ?*const v8.Context {
|
||||||
|
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
||||||
|
const global_handle = inspector.default_context orelse return null;
|
||||||
|
return v8.v8__Global__Get(&global_handle, inspector.isolate);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Channel__IMPL__sendResponse(
|
||||||
|
_: *v8.InspectorChannelImpl,
|
||||||
|
data: *anyopaque,
|
||||||
|
call_id: c_int,
|
||||||
|
msg: [*c]u8,
|
||||||
|
length: usize,
|
||||||
|
) callconv(.c) void {
|
||||||
|
const session: *Session = @ptrCast(@alignCast(data));
|
||||||
|
session.onResp(session.ctx, @intCast(call_id), msg[0..length]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Channel__IMPL__sendNotification(
|
||||||
|
_: *v8.InspectorChannelImpl,
|
||||||
|
data: *anyopaque,
|
||||||
|
msg: [*c]u8,
|
||||||
|
length: usize,
|
||||||
|
) callconv(.c) void {
|
||||||
|
const session: *Session = @ptrCast(@alignCast(data));
|
||||||
|
session.onNotif(session.ctx, msg[0..length]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub export fn v8_inspector__Channel__IMPL__flushProtocolNotifications(
|
||||||
|
_: *v8.InspectorChannelImpl,
|
||||||
|
_: *anyopaque,
|
||||||
|
) callconv(.c) void {
|
||||||
|
// TODO
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,22 +19,17 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
|
|
||||||
// This only exists so that we know whether a function wants the opaque
|
const v8 = js.v8;
|
||||||
// JS argument (js.Object), or if it wants the receiver as an opaque
|
|
||||||
// value.
|
|
||||||
// js.Object is normally used when a method wants an opaque JS object
|
|
||||||
// that it'll pass into a callback.
|
|
||||||
// This is used when the function wants to do advanced manipulation
|
|
||||||
// of the v8.Object bound to the instance. For example, postAttach is an
|
|
||||||
// example of using This.
|
|
||||||
|
|
||||||
const This = @This();
|
const Integer = @This();
|
||||||
obj: js.Object,
|
|
||||||
|
|
||||||
pub fn setIndex(self: This, index: u32, value: anytype, opts: js.Object.SetOpts) !void {
|
handle: *const v8.Integer,
|
||||||
return self.obj.setIndex(index, value, opts);
|
|
||||||
}
|
pub fn init(isolate: *v8.Isolate, value: anytype) Integer {
|
||||||
|
const handle = switch (@TypeOf(value)) {
|
||||||
pub fn set(self: This, key: []const u8, value: anytype, opts: js.Object.SetOpts) !void {
|
i8, i16, i32 => v8.v8__Integer__New(isolate, value).?,
|
||||||
return self.obj.set(key, value, opts);
|
u8, u16, u32 => v8.v8__Integer__NewFromUnsigned(isolate, value).?,
|
||||||
|
else => |T| @compileError("cannot create v8::Integer from: " ++ @typeName(T)),
|
||||||
|
};
|
||||||
|
return .{ .handle = handle };
|
||||||
}
|
}
|
||||||
131
src/browser/js/Isolate.zig
Normal file
131
src/browser/js/Isolate.zig
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// 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 js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Isolate = @This();
|
||||||
|
|
||||||
|
handle: *v8.Isolate,
|
||||||
|
|
||||||
|
pub fn init(params: *v8.CreateParams) Isolate {
|
||||||
|
return .{
|
||||||
|
.handle = v8.v8__Isolate__New(params).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Isolate) void {
|
||||||
|
v8.v8__Isolate__Dispose(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enter(self: Isolate) void {
|
||||||
|
v8.v8__Isolate__Enter(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit(self: Isolate) void {
|
||||||
|
v8.v8__Isolate__Exit(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lowMemoryNotification(self: Isolate) void {
|
||||||
|
v8.v8__Isolate__LowMemoryNotification(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const MemoryPressureLevel = enum(u32) {
|
||||||
|
none = v8.kNone,
|
||||||
|
moderate = v8.kModerate,
|
||||||
|
critical = v8.kCritical,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn memoryPressureNotification(self: Isolate, level: MemoryPressureLevel) void {
|
||||||
|
v8.v8__Isolate__MemoryPressureNotification(self.handle, @intFromEnum(level));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notifyContextDisposed(self: Isolate) void {
|
||||||
|
_ = v8.v8__Isolate__ContextDisposedNotification(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getHeapStatistics(self: Isolate) v8.HeapStatistics {
|
||||||
|
var res: v8.HeapStatistics = undefined;
|
||||||
|
v8.v8__Isolate__GetHeapStatistics(self.handle, &res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn throwException(self: Isolate, value: *const v8.Value) *const v8.Value {
|
||||||
|
return v8.v8__Isolate__ThrowException(self.handle, value).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initStringHandle(self: Isolate, str: []const u8) *const v8.String {
|
||||||
|
return v8.v8__String__NewFromUtf8(self.handle, str.ptr, v8.kNormal, @as(c_int, @intCast(str.len))).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__Error(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createRangeError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__RangeError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createReferenceError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__ReferenceError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createSyntaxError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__SyntaxError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createTypeError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__TypeError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initNull(self: Isolate) *const v8.Value {
|
||||||
|
return v8.v8__Null(self.handle).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initUndefined(self: Isolate) *const v8.Value {
|
||||||
|
return v8.v8__Undefined(self.handle).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initFalse(self: Isolate) *const v8.Value {
|
||||||
|
return v8.v8__False(self.handle).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initTrue(self: Isolate) *const v8.Value {
|
||||||
|
return v8.v8__True(self.handle).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initInteger(self: Isolate, val: anytype) js.Integer {
|
||||||
|
return js.Integer.init(self.handle, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initBigInt(self: Isolate, val: anytype) js.BigInt {
|
||||||
|
return js.BigInt.init(self.handle, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initNumber(self: Isolate, val: anytype) js.Number {
|
||||||
|
return js.Number.init(self.handle, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createExternal(self: Isolate, val: *anyopaque) *const v8.External {
|
||||||
|
return v8.v8__External__New(self.handle, val).?;
|
||||||
|
}
|
||||||
1421
src/browser/js/Local.zig
Normal file
1421
src/browser/js/Local.zig
Normal file
File diff suppressed because it is too large
Load Diff
137
src/browser/js/Module.zig
Normal file
137
src/browser/js/Module.zig
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// 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 js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Module = @This();
|
||||||
|
|
||||||
|
local: *const js.Local,
|
||||||
|
handle: *const v8.Module,
|
||||||
|
|
||||||
|
pub const Status = enum(u32) {
|
||||||
|
kUninstantiated = v8.kUninstantiated,
|
||||||
|
kInstantiating = v8.kInstantiating,
|
||||||
|
kInstantiated = v8.kInstantiated,
|
||||||
|
kEvaluating = v8.kEvaluating,
|
||||||
|
kEvaluated = v8.kEvaluated,
|
||||||
|
kErrored = v8.kErrored,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getStatus(self: Module) Status {
|
||||||
|
return @enumFromInt(v8.v8__Module__GetStatus(self.handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getException(self: Module) js.Value {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = v8.v8__Module__GetException(self.handle).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getModuleRequests(self: Module) Requests {
|
||||||
|
return .{
|
||||||
|
.context_handle = self.local.handle,
|
||||||
|
.handle = v8.v8__Module__GetModuleRequests(self.handle).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool {
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Module__InstantiateModule(self.handle, self.local.handle, cb, &out);
|
||||||
|
if (out.has_value) {
|
||||||
|
return out.value;
|
||||||
|
}
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate(self: Module) !js.Value {
|
||||||
|
const res = v8.v8__Module__Evaluate(self.handle, self.local.handle) orelse return error.JsException;
|
||||||
|
|
||||||
|
if (self.getStatus() == .kErrored) {
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = res,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getIdentityHash(self: Module) u32 {
|
||||||
|
return @bitCast(v8.v8__Module__GetIdentityHash(self.handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getModuleNamespace(self: Module) js.Value {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = v8.v8__Module__GetModuleNamespace(self.handle).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getScriptId(self: Module) u32 {
|
||||||
|
return @intCast(v8.v8__Module__ScriptId(self.handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: Module) !Global {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
try ctx.global_modules.append(ctx.arena, global);
|
||||||
|
return .{ .handle = global };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Global = struct {
|
||||||
|
handle: v8.Global,
|
||||||
|
|
||||||
|
pub fn deinit(self: *Global) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Global, l: *const js.Local) Module {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEqual(self: *const Global, other: Module) bool {
|
||||||
|
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Requests = struct {
|
||||||
|
handle: *const v8.FixedArray,
|
||||||
|
context_handle: *const v8.Context,
|
||||||
|
|
||||||
|
pub fn len(self: Requests) usize {
|
||||||
|
return @intCast(v8.v8__FixedArray__Length(self.handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: Requests, idx: usize) Request {
|
||||||
|
return .{ .handle = v8.v8__FixedArray__Get(self.handle, self.context_handle, @intCast(idx)).? };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Request = struct {
|
||||||
|
handle: *const v8.ModuleRequest,
|
||||||
|
|
||||||
|
pub fn specifier(self: Request, local: *const js.Local) js.String {
|
||||||
|
return .{ .local = local, .handle = v8.v8__ModuleRequest__GetSpecifier(self.handle).? };
|
||||||
|
}
|
||||||
|
};
|
||||||
31
src/browser/js/Number.zig
Normal file
31
src/browser/js/Number.zig
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Number = @This();
|
||||||
|
|
||||||
|
handle: *const v8.Number,
|
||||||
|
|
||||||
|
pub fn init(isolate: *v8.Isolate, value: anytype) Number {
|
||||||
|
const handle = v8.v8__Number__New(isolate, value).?;
|
||||||
|
return .{ .handle = handle };
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -22,103 +22,102 @@ const v8 = js.v8;
|
|||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const Context = @import("Context.zig");
|
|
||||||
const PersistentObject = v8.Persistent(v8.Object);
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const Object = @This();
|
const Object = @This();
|
||||||
js_obj: v8.Object,
|
|
||||||
context: *js.Context,
|
|
||||||
|
|
||||||
pub fn getId(self: Object) u32 {
|
local: *const js.Local,
|
||||||
return self.js_obj.getIdentityHash();
|
handle: *const v8.Object,
|
||||||
|
|
||||||
|
pub fn has(self: Object, key: anytype) bool {
|
||||||
|
const ctx = self.local.ctx;
|
||||||
|
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
||||||
|
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__Has(self.handle, self.local.handle, key_handle, &out);
|
||||||
|
if (out.has_value) {
|
||||||
|
return out.value;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const SetOpts = packed struct(u32) {
|
pub fn get(self: Object, key: anytype) !js.Value {
|
||||||
READ_ONLY: bool = false,
|
const ctx = self.local.ctx;
|
||||||
DONT_ENUM: bool = false,
|
|
||||||
DONT_DELETE: bool = false,
|
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
||||||
_: u29 = 0,
|
const js_val_handle = v8.v8__Object__Get(self.handle, self.local.handle, key_handle) orelse return error.JsException;
|
||||||
};
|
|
||||||
pub fn setIndex(self: Object, index: u32, value: anytype, opts: SetOpts) !void {
|
return .{
|
||||||
@setEvalBranchQuota(10000);
|
.local = self.local,
|
||||||
const key = switch (index) {
|
.handle = js_val_handle,
|
||||||
inline 0...20 => |i| std.fmt.comptimePrint("{d}", .{i}),
|
|
||||||
else => try std.fmt.allocPrint(self.context.arena, "{d}", .{index}),
|
|
||||||
};
|
};
|
||||||
return self.set(key, value, opts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{ FailedToSet, OutOfMemory }!void {
|
pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
|
||||||
const context = self.context;
|
const ctx = self.local.ctx;
|
||||||
|
|
||||||
const js_key = v8.String.initUtf8(context.isolate, key);
|
const js_value = try self.local.zigValueToJs(value, opts);
|
||||||
const js_value = try context.zigValueToJs(value, .{});
|
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
||||||
|
|
||||||
const res = self.js_obj.defineOwnProperty(context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false;
|
var out: v8.MaybeBool = undefined;
|
||||||
if (!res) {
|
v8.v8__Object__Set(self.handle, self.local.handle, key_handle, js_value.handle, &out);
|
||||||
return error.FailedToSet;
|
return out.has_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr: v8.PropertyAttribute) ?bool {
|
||||||
|
const ctx = self.local.ctx;
|
||||||
|
const name_handle = ctx.isolate.initStringHandle(name);
|
||||||
|
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__DefineOwnProperty(self.handle, self.local.handle, @ptrCast(name_handle), value.handle, attr, &out);
|
||||||
|
|
||||||
|
if (out.has_value) {
|
||||||
|
return out.value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: Object, key: []const u8) !js.Value {
|
pub fn toValue(self: Object) js.Value {
|
||||||
const context = self.context;
|
return .{
|
||||||
const js_key = v8.String.initUtf8(context.isolate, key);
|
.local = self.local,
|
||||||
const js_val = try self.js_obj.getValue(context.v8_context, js_key);
|
.handle = @ptrCast(self.handle),
|
||||||
return context.createValue(js_val);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isTruthy(self: Object) bool {
|
|
||||||
const js_value = self.js_obj.toValue();
|
|
||||||
return js_value.toBool(self.context.isolate);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toString(self: Object) ![]const u8 {
|
|
||||||
const js_value = self.js_obj.toValue();
|
|
||||||
return self.context.valueToString(js_value, .{});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format(self: Object, writer: *std.Io.Writer) !void {
|
pub fn format(self: Object, writer: *std.Io.Writer) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
return self.context.debugValue(self.js_obj.toValue(), writer);
|
return self.local.ctx.debugValue(self.toValue(), writer);
|
||||||
}
|
}
|
||||||
const str = self.toString() catch return error.WriteFailed;
|
const str = self.toString() catch return error.WriteFailed;
|
||||||
return writer.writeAll(str);
|
return writer.writeAll(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toJson(self: Object, allocator: Allocator) ![]u8 {
|
pub fn persist(self: Object) !Global {
|
||||||
const json_string = try v8.Json.stringify(self.context.v8_context, self.js_obj.toValue(), null);
|
var ctx = self.local.ctx;
|
||||||
const str = try self.context.jsStringToZig(json_string, .{ .allocator = allocator });
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn persist(self: Object) !Object {
|
var global: v8.Global = undefined;
|
||||||
var context = self.context;
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
const js_obj = self.js_obj;
|
|
||||||
|
|
||||||
const persisted = PersistentObject.init(context.isolate, js_obj);
|
try ctx.trackGlobal(global);
|
||||||
try context.js_object_list.append(context.arena, persisted);
|
|
||||||
|
|
||||||
return .{
|
return .{ .handle = global };
|
||||||
.context = context,
|
|
||||||
.js_obj = persisted.castToObject(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
|
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
|
||||||
if (self.isNullOrUndefined()) {
|
if (self.isNullOrUndefined()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const context = self.context;
|
const local = self.local;
|
||||||
|
|
||||||
const js_name = v8.String.initUtf8(context.isolate, name);
|
const js_name = local.isolate.initStringHandle(name);
|
||||||
|
const js_val_handle = v8.v8__Object__Get(self.handle, local.handle, js_name) orelse return error.JsException;
|
||||||
|
|
||||||
const js_value = try self.js_obj.getValue(context.v8_context, js_name.toName());
|
if (v8.v8__Value__IsFunction(js_val_handle) == false) {
|
||||||
if (!js_value.isFunction()) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return try context.createFunction(js_value);
|
return .{
|
||||||
|
.local = local,
|
||||||
|
.handle = @ptrCast(js_val_handle),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args: anytype) !T {
|
pub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args: anytype) !T {
|
||||||
@@ -126,41 +125,75 @@ pub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args:
|
|||||||
return func.callWithThis(T, self, args);
|
return func.callWithThis(T, self, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isNull(self: Object) bool {
|
|
||||||
return self.js_obj.toValue().isNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isUndefined(self: Object) bool {
|
|
||||||
return self.js_obj.toValue().isUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isNullOrUndefined(self: Object) bool {
|
pub fn isNullOrUndefined(self: Object) bool {
|
||||||
return self.js_obj.toValue().isNullOrUndefined();
|
return v8.v8__Value__IsNullOrUndefined(@ptrCast(self.handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nameIterator(self: Object) NameIterator {
|
pub fn getOwnPropertyNames(self: Object) !js.Array {
|
||||||
const context = self.context;
|
const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.local.handle) orelse {
|
||||||
const js_obj = self.js_obj;
|
// This is almost always a fatal error case. Either we're in some exception
|
||||||
|
// and things are messy, or we're shutting down, or someone has messed up
|
||||||
const array = js_obj.getPropertyNames(context.v8_context);
|
// the object (like some WPT tests do).
|
||||||
const count = array.length();
|
return error.TypeError;
|
||||||
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getPropertyNames(self: Object) js.Array {
|
||||||
|
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?;
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nameIterator(self: Object) !NameIterator {
|
||||||
|
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle) orelse {
|
||||||
|
// see getOwnPropertyNames above
|
||||||
|
return error.TypeError;
|
||||||
|
};
|
||||||
|
const count = v8.v8__Array__Length(handle);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = handle,
|
||||||
.count = count,
|
.count = count,
|
||||||
.context = context,
|
|
||||||
.js_obj = array.castTo(v8.Object),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toZig(self: Object, comptime T: type) !T {
|
pub fn toZig(self: Object, comptime T: type) !T {
|
||||||
return self.context.jsValueToZig(T, self.js_obj.toValue());
|
const js_value = js.Value{ .local = self.local, .handle = @ptrCast(self.handle) };
|
||||||
|
return self.local.jsValueToZig(T, js_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const Global = struct {
|
||||||
|
handle: v8.Global,
|
||||||
|
|
||||||
|
pub fn deinit(self: *Global) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Global, l: *const js.Local) Object {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEqual(self: *const Global, other: Object) bool {
|
||||||
|
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub const NameIterator = struct {
|
pub const NameIterator = struct {
|
||||||
count: u32,
|
count: u32,
|
||||||
idx: u32 = 0,
|
idx: u32 = 0,
|
||||||
js_obj: v8.Object,
|
local: *const js.Local,
|
||||||
context: *const Context,
|
handle: *const v8.Array,
|
||||||
|
|
||||||
pub fn next(self: *NameIterator) !?[]const u8 {
|
pub fn next(self: *NameIterator) !?[]const u8 {
|
||||||
const idx = self.idx;
|
const idx = self.idx;
|
||||||
@@ -169,8 +202,8 @@ pub const NameIterator = struct {
|
|||||||
}
|
}
|
||||||
self.idx += 1;
|
self.idx += 1;
|
||||||
|
|
||||||
const context = self.context;
|
const local = self.local;
|
||||||
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx);
|
const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), local.handle, idx) orelse return error.JsException;
|
||||||
return try context.valueToString(js_val, .{});
|
return try js.Value.toStringSlice(.{ .local = local, .handle = js_val_handle });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
73
src/browser/js/Origin.zig
Normal file
73
src/browser/js/Origin.zig
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// Origin represents the security token for contexts within the same origin.
|
||||||
|
// Multiple contexts (frames) from the same origin share a single Origin,
|
||||||
|
// which provides the V8 SecurityToken that allows cross-context access.
|
||||||
|
//
|
||||||
|
// Note: Identity tracking (mapping Zig instances to v8::Objects) is managed
|
||||||
|
// separately via js.Identity - Session has the main world Identity, and
|
||||||
|
// IsolatedWorlds have their own Identity instances.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
|
||||||
|
const App = @import("../../App.zig");
|
||||||
|
|
||||||
|
const v8 = js.v8;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const Origin = @This();
|
||||||
|
|
||||||
|
rc: usize = 1,
|
||||||
|
arena: Allocator,
|
||||||
|
|
||||||
|
// The key, e.g. lightpanda.io:443
|
||||||
|
key: []const u8,
|
||||||
|
|
||||||
|
// Security token - all contexts in this origin must use the same v8::Value instance
|
||||||
|
// as their security token for V8 to allow cross-context access
|
||||||
|
security_token: v8.Global,
|
||||||
|
|
||||||
|
pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
|
||||||
|
const arena = try app.arena_pool.acquire(.{ .debug = "Origin" });
|
||||||
|
errdefer app.arena_pool.release(arena);
|
||||||
|
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const owned_key = try arena.dupe(u8, key);
|
||||||
|
const token_local = isolate.initStringHandle(owned_key);
|
||||||
|
var token_global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(isolate.handle, token_local, &token_global);
|
||||||
|
|
||||||
|
const self = try arena.create(Origin);
|
||||||
|
self.* = .{
|
||||||
|
.rc = 1,
|
||||||
|
.arena = arena,
|
||||||
|
.key = owned_key,
|
||||||
|
.security_token = token_global,
|
||||||
|
};
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Origin, app: *App) void {
|
||||||
|
v8.v8__Global__Reset(&self.security_token);
|
||||||
|
app.arena_pool.release(self.arena);
|
||||||
|
}
|
||||||
@@ -20,20 +20,22 @@ const js = @import("js.zig");
|
|||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const Platform = @This();
|
const Platform = @This();
|
||||||
inner: v8.Platform,
|
handle: *v8.Platform,
|
||||||
|
|
||||||
pub fn init() !Platform {
|
pub fn init() !Platform {
|
||||||
if (v8.initV8ICU() == false) {
|
if (v8.v8__V8__InitializeICU() == false) {
|
||||||
return error.FailedToInitializeICU;
|
return error.FailedToInitializeICU;
|
||||||
}
|
}
|
||||||
const platform = v8.Platform.initDefault(0, true);
|
// 0 - threadpool size, 0 == let v8 decide
|
||||||
v8.initV8Platform(platform);
|
// 1 - idle_task_support, 1 == enabled
|
||||||
v8.initV8();
|
const handle = v8.v8__Platform__NewDefaultPlatform(0, 1).?;
|
||||||
return .{ .inner = platform };
|
v8.v8__V8__InitializePlatform(handle);
|
||||||
|
v8.v8__V8__Initialize();
|
||||||
|
return .{ .handle = handle };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: Platform) void {
|
pub fn deinit(self: Platform) void {
|
||||||
_ = v8.deinitV8();
|
_ = v8.v8__V8__Dispose();
|
||||||
v8.deinitV8Platform();
|
v8.v8__V8__DisposePlatform();
|
||||||
self.inner.deinit();
|
v8.v8__Platform__DELETE(self.handle);
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/browser/js/Private.zig
Normal file
42
src/browser/js/Private.zig
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Private = @This();
|
||||||
|
|
||||||
|
// Unlike most types, we always store the Private as a Global. It makes more
|
||||||
|
// sense for this type given how it's used.
|
||||||
|
handle: v8.Global,
|
||||||
|
|
||||||
|
pub fn init(isolate: *v8.Isolate, name: []const u8) Private {
|
||||||
|
const v8_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
|
const private_handle = v8.v8__Private__New(isolate, v8_name);
|
||||||
|
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(isolate, private_handle, &global);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.handle = global,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Private) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
106
src/browser/js/Promise.zig
Normal file
106
src/browser/js/Promise.zig
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const Promise = @This();
|
||||||
|
|
||||||
|
local: *const js.Local,
|
||||||
|
handle: *const v8.Promise,
|
||||||
|
|
||||||
|
pub fn toObject(self: Promise) js.Object {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = @ptrCast(self.handle),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toValue(self: Promise) js.Value {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = @ptrCast(self.handle),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thenAndCatch(self: Promise, on_fulfilled: js.Function, on_rejected: js.Function) !Promise {
|
||||||
|
if (v8.v8__Promise__Then2(self.handle, self.local.handle, on_fulfilled.handle, on_rejected.handle)) |handle| {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return error.PromiseChainFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: Promise) !Global {
|
||||||
|
return self._persist(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn temp(self: Promise) !Temp {
|
||||||
|
return self._persist(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
if (comptime is_global) {
|
||||||
|
try ctx.trackGlobal(global);
|
||||||
|
return .{ .handle = global, .temps = {} };
|
||||||
|
}
|
||||||
|
try ctx.trackTemp(global);
|
||||||
|
return .{ .handle = global, .temps = &ctx.identity.temps };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Temp = G(.temp);
|
||||||
|
pub const Global = G(.global);
|
||||||
|
|
||||||
|
const GlobalType = enum(u8) {
|
||||||
|
temp,
|
||||||
|
global,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn G(comptime global_type: GlobalType) type {
|
||||||
|
return struct {
|
||||||
|
handle: v8.Global,
|
||||||
|
temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Self, l: *const js.Local) Promise {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(self: *const Self) void {
|
||||||
|
if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| {
|
||||||
|
var g = kv.value;
|
||||||
|
v8.v8__Global__Reset(&g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
41
src/browser/js/PromiseRejection.zig
Normal file
41
src/browser/js/PromiseRejection.zig
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const PromiseRejection = @This();
|
||||||
|
|
||||||
|
local: *const js.Local,
|
||||||
|
handle: *const v8.PromiseRejectMessage,
|
||||||
|
|
||||||
|
pub fn promise(self: PromiseRejection) js.Promise {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = v8.v8__PromiseRejectMessage__GetPromise(self.handle).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reason(self: PromiseRejection) ?js.Value {
|
||||||
|
const value_handle = v8.v8__PromiseRejectMessage__GetValue(self.handle) orelse return null;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = value_handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
139
src/browser/js/PromiseResolver.zig
Normal file
139
src/browser/js/PromiseResolver.zig
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// 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 js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const DOMException = @import("../webapi/DOMException.zig");
|
||||||
|
|
||||||
|
const PromiseResolver = @This();
|
||||||
|
|
||||||
|
local: *const js.Local,
|
||||||
|
handle: *const v8.PromiseResolver,
|
||||||
|
|
||||||
|
pub fn init(local: *const js.Local) PromiseResolver {
|
||||||
|
return .{
|
||||||
|
.local = local,
|
||||||
|
.handle = v8.v8__Promise__Resolver__New(local.handle).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn promise(self: PromiseResolver) js.Promise {
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = v8.v8__Promise__Resolver__GetPromise(self.handle).?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||||
|
self._resolve(value) catch |err| {
|
||||||
|
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = false });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _resolve(self: PromiseResolver, value: anytype) !void {
|
||||||
|
const local = self.local;
|
||||||
|
const js_val = try local.zigValueToJs(value, .{});
|
||||||
|
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Promise__Resolver__Resolve(self.handle, self.local.handle, js_val.handle, &out);
|
||||||
|
if (!out.has_value or !out.value) {
|
||||||
|
return error.FailedToResolvePromise;
|
||||||
|
}
|
||||||
|
local.runMicrotasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||||
|
self._reject(value) catch |err| {
|
||||||
|
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = false });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const RejectError = union(enum) {
|
||||||
|
/// Not to be confused with `DOMException`; this is bare `Error`.
|
||||||
|
generic_error: []const u8,
|
||||||
|
range_error: []const u8,
|
||||||
|
reference_error: []const u8,
|
||||||
|
syntax_error: []const u8,
|
||||||
|
type_error: []const u8,
|
||||||
|
/// DOM exceptions are unknown to V8, belongs to web standards.
|
||||||
|
dom_exception: struct { err: anyerror },
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Rejects the promise w/ an error object.
|
||||||
|
pub fn rejectError(
|
||||||
|
self: PromiseResolver,
|
||||||
|
comptime source: []const u8,
|
||||||
|
err: RejectError,
|
||||||
|
) void {
|
||||||
|
const handle = switch (err) {
|
||||||
|
.generic_error => |msg| self.local.isolate.createError(msg),
|
||||||
|
.range_error => |msg| self.local.isolate.createRangeError(msg),
|
||||||
|
.reference_error => |msg| self.local.isolate.createReferenceError(msg),
|
||||||
|
.syntax_error => |msg| self.local.isolate.createSyntaxError(msg),
|
||||||
|
.type_error => |msg| self.local.isolate.createTypeError(msg),
|
||||||
|
// "Exceptional".
|
||||||
|
.dom_exception => |exception| {
|
||||||
|
self._reject(DOMException.fromError(exception.err) orelse unreachable) catch |reject_err| {
|
||||||
|
log.err(.bug, "rejectDomException", .{ .source = source, .err = reject_err, .persistent = false });
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
|
||||||
|
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _reject(self: PromiseResolver, value: anytype) !void {
|
||||||
|
const local = self.local;
|
||||||
|
const js_val = try local.zigValueToJs(value, .{});
|
||||||
|
|
||||||
|
var out: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Promise__Resolver__Reject(self.handle, local.handle, js_val.handle, &out);
|
||||||
|
if (!out.has_value or !out.value) {
|
||||||
|
return error.FailedToRejectPromise;
|
||||||
|
}
|
||||||
|
local.runMicrotasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: PromiseResolver) !Global {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
try ctx.trackGlobal(global);
|
||||||
|
return .{ .handle = global };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Global = struct {
|
||||||
|
handle: v8.Global,
|
||||||
|
|
||||||
|
pub fn deinit(self: *Global) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Global, l: *const js.Local) PromiseResolver {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
|
||||||
|
|
||||||
const IS_DEBUG = builtin.mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
|
|
||||||
@@ -47,9 +47,15 @@ pub fn init(allocator: std.mem.Allocator) Scheduler {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Scheduler) void {
|
||||||
|
finalizeTasks(&self.low_priority);
|
||||||
|
finalizeTasks(&self.high_priority);
|
||||||
|
}
|
||||||
|
|
||||||
const AddOpts = struct {
|
const AddOpts = struct {
|
||||||
name: []const u8 = "",
|
name: []const u8 = "",
|
||||||
low_priority: bool = false,
|
low_priority: bool = false,
|
||||||
|
finalizer: ?Finalizer = null,
|
||||||
};
|
};
|
||||||
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
|
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -63,25 +69,39 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts
|
|||||||
.callback = cb,
|
.callback = cb,
|
||||||
.sequence = seq,
|
.sequence = seq,
|
||||||
.name = opts.name,
|
.name = opts.name,
|
||||||
|
.finalizer = opts.finalizer,
|
||||||
.run_at = milliTimestamp(.monotonic) + run_in_ms,
|
.run_at = milliTimestamp(.monotonic) + run_in_ms,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(self: *Scheduler) !?u64 {
|
pub fn run(self: *Scheduler) !void {
|
||||||
_ = try self.runQueue(&self.low_priority);
|
const now = milliTimestamp(.monotonic);
|
||||||
return self.runQueue(&self.high_priority);
|
try self.runQueue(&self.low_priority, now);
|
||||||
|
try self.runQueue(&self.high_priority, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
pub fn hasReadyTasks(self: *Scheduler) bool {
|
||||||
if (queue.count() == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = milliTimestamp(.monotonic);
|
const now = milliTimestamp(.monotonic);
|
||||||
|
return queueuHasReadyTask(&self.low_priority, now) or queueuHasReadyTask(&self.high_priority, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn msToNextHigh(self: *Scheduler) ?u64 {
|
||||||
|
const task = self.high_priority.peek() orelse return null;
|
||||||
|
const now = milliTimestamp(.monotonic);
|
||||||
|
if (task.run_at <= now) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return @intCast(task.run_at - now);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runQueue(self: *Scheduler, queue: *Queue, now: u64) !void {
|
||||||
|
if (queue.count() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
while (queue.peek()) |*task_| {
|
while (queue.peek()) |*task_| {
|
||||||
if (task_.run_at > now) {
|
if (task_.run_at > now) {
|
||||||
return @intCast(task_.run_at - now);
|
return;
|
||||||
}
|
}
|
||||||
var task = queue.remove();
|
var task = queue.remove();
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -95,12 +115,28 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
|||||||
|
|
||||||
if (repeat_in_ms) |ms| {
|
if (repeat_in_ms) |ms| {
|
||||||
// Task cannot be repeated immediately, and they should know that
|
// Task cannot be repeated immediately, and they should know that
|
||||||
std.debug.assert(ms != 0);
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(ms != 0);
|
||||||
|
}
|
||||||
task.run_at = now + ms;
|
task.run_at = now + ms;
|
||||||
try self.low_priority.add(task);
|
try self.low_priority.add(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queueuHasReadyTask(queue: *Queue, now: u64) bool {
|
||||||
|
const task = queue.peek() orelse return false;
|
||||||
|
return task.run_at <= now;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finalizeTasks(queue: *Queue) void {
|
||||||
|
var it = queue.iterator();
|
||||||
|
while (it.next()) |t| {
|
||||||
|
if (t.finalizer) |func| {
|
||||||
|
func(t.ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Task = struct {
|
const Task = struct {
|
||||||
@@ -109,6 +145,8 @@ const Task = struct {
|
|||||||
ctx: *anyopaque,
|
ctx: *anyopaque,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
callback: Callback,
|
callback: Callback,
|
||||||
|
finalizer: ?Finalizer,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
|
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
|
||||||
|
const Finalizer = *const fn (ctx: *anyopaque) void;
|
||||||
@@ -22,11 +22,9 @@ const bridge = @import("bridge.zig");
|
|||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
const Window = @import("../webapi/Window.zig");
|
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
const JsApis = bridge.JsApis;
|
const JsApis = bridge.JsApis;
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const Snapshot = @This();
|
const Snapshot = @This();
|
||||||
|
|
||||||
@@ -53,14 +51,14 @@ startup_data: v8.StartupData,
|
|||||||
external_references: [countExternalReferences()]isize,
|
external_references: [countExternalReferences()]isize,
|
||||||
|
|
||||||
// Track whether this snapshot owns its data (was created in-process)
|
// Track whether this snapshot owns its data (was created in-process)
|
||||||
// If false, the data points into embedded_snapshot_blob and should not be freed
|
// If false, the data points into embedded_snapshot_blob and will not be freed
|
||||||
owns_data: bool = false,
|
owns_data: bool = false,
|
||||||
|
|
||||||
pub fn load(allocator: Allocator) !Snapshot {
|
pub fn load() !Snapshot {
|
||||||
if (loadEmbedded()) |snapshot| {
|
if (loadEmbedded()) |snapshot| {
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
return create(allocator);
|
return create();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn loadEmbedded() ?Snapshot {
|
fn loadEmbedded() ?Snapshot {
|
||||||
@@ -75,7 +73,7 @@ fn loadEmbedded() ?Snapshot {
|
|||||||
const blob = embedded_snapshot_blob[@sizeOf(usize)..];
|
const blob = embedded_snapshot_blob[@sizeOf(usize)..];
|
||||||
|
|
||||||
const startup_data = v8.StartupData{ .data = blob.ptr, .raw_size = @intCast(blob.len) };
|
const startup_data = v8.StartupData{ .data = blob.ptr, .raw_size = @intCast(blob.len) };
|
||||||
if (!v8.SnapshotCreator.startupDataIsValid(startup_data)) {
|
if (!v8.v8__StartupData__IsValid(startup_data)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,10 +85,11 @@ fn loadEmbedded() ?Snapshot {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: Snapshot, allocator: Allocator) void {
|
pub fn deinit(self: Snapshot) void {
|
||||||
// Only free if we own the data (was created in-process)
|
// Only free if we own the data (was created in-process)
|
||||||
if (self.owns_data) {
|
if (self.owns_data) {
|
||||||
allocator.free(self.startup_data.data[0..@intCast(self.startup_data.raw_size)]);
|
// V8 allocated this with `new char[]`, so we need to use the C++ delete[] operator
|
||||||
|
v8.v8__StartupData__DELETE(self.startup_data.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,50 +104,39 @@ pub fn write(self: Snapshot, writer: *std.Io.Writer) !void {
|
|||||||
|
|
||||||
pub fn fromEmbedded(self: Snapshot) bool {
|
pub fn fromEmbedded(self: Snapshot) bool {
|
||||||
// if the snapshot comes from the embedFile, then it'll be flagged as not
|
// if the snapshot comes from the embedFile, then it'll be flagged as not
|
||||||
// owneing (aka, not needing to free) the data.
|
// owning (aka, not needing to free) the data.
|
||||||
return self.owns_data == false;
|
return self.owns_data == false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isValid(self: Snapshot) bool {
|
fn isValid(self: Snapshot) bool {
|
||||||
return v8.SnapshotCreator.startupDataIsValid(self.startup_data);
|
return v8.v8__StartupData__IsValid(self.startup_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createGlobalTemplate(isolate: v8.Isolate, templates: []const v8.FunctionTemplate) v8.ObjectTemplate {
|
pub fn create() !Snapshot {
|
||||||
// Set up the global template to inherit from Window's template
|
|
||||||
// This way the global object gets all Window properties through inheritance
|
|
||||||
const js_global = v8.FunctionTemplate.initDefault(isolate);
|
|
||||||
js_global.setClassName(v8.String.initUtf8(isolate, "Window"));
|
|
||||||
// Find Window in JsApis by name (avoids circular import)
|
|
||||||
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
|
|
||||||
js_global.inherit(templates[window_index]);
|
|
||||||
return js_global.getInstanceTemplate();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create(allocator: Allocator) !Snapshot {
|
|
||||||
var external_references = collectExternalReferences();
|
var external_references = collectExternalReferences();
|
||||||
|
|
||||||
var params = v8.initCreateParams();
|
var params: v8.CreateParams = undefined;
|
||||||
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
|
v8.v8__Isolate__CreateParams__CONSTRUCT(¶ms);
|
||||||
defer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
|
params.array_buffer_allocator = v8.v8__ArrayBuffer__Allocator__NewDefaultAllocator();
|
||||||
|
defer v8.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);
|
||||||
params.external_references = @ptrCast(&external_references);
|
params.external_references = @ptrCast(&external_references);
|
||||||
|
|
||||||
var snapshot_creator: v8.SnapshotCreator = undefined;
|
const snapshot_creator = v8.v8__SnapshotCreator__CREATE(¶ms);
|
||||||
v8.SnapshotCreator.init(&snapshot_creator, ¶ms);
|
defer v8.v8__SnapshotCreator__DESTRUCT(snapshot_creator);
|
||||||
defer snapshot_creator.deinit();
|
|
||||||
|
|
||||||
var data_start: usize = 0;
|
var data_start: usize = 0;
|
||||||
const isolate = snapshot_creator.getIsolate();
|
const isolate = v8.v8__SnapshotCreator__getIsolate(snapshot_creator).?;
|
||||||
|
|
||||||
{
|
{
|
||||||
// CreateBlob, which we'll call once everything is setup, MUST NOT
|
// CreateBlob, which we'll call once everything is setup, MUST NOT
|
||||||
// be called from an active HandleScope. Hence we have this scope to
|
// be called from an active HandleScope. Hence we have this scope to
|
||||||
// clean it up before we call CreateBlob
|
// clean it up before we call CreateBlob
|
||||||
var handle_scope: v8.HandleScope = undefined;
|
var handle_scope: v8.HandleScope = undefined;
|
||||||
v8.HandleScope.init(&handle_scope, isolate);
|
v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate);
|
||||||
defer handle_scope.deinit();
|
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
|
||||||
|
|
||||||
// Create templates (constructors only) FIRST
|
// Create templates (constructors only) FIRST
|
||||||
var templates: [JsApis.len]v8.FunctionTemplate = undefined;
|
var templates: [JsApis.len]*const v8.FunctionTemplate = undefined;
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
@setEvalBranchQuota(10_000);
|
@setEvalBranchQuota(10_000);
|
||||||
templates[i] = generateConstructor(JsApi, isolate);
|
templates[i] = generateConstructor(JsApi, isolate);
|
||||||
@@ -159,23 +147,21 @@ pub fn create(allocator: Allocator) !Snapshot {
|
|||||||
// This must come before attachClass so inheritance is set up first
|
// This must come before attachClass so inheritance is set up first
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
||||||
templates[i].inherit(templates[proto_index]);
|
v8.v8__FunctionTemplate__Inherit(templates[i], templates[proto_index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the global template to inherit from Window's template
|
// Set up the global template to inherit from Window's template
|
||||||
// This way the global object gets all Window properties through inheritance
|
// This way the global object gets all Window properties through inheritance
|
||||||
const global_template = createGlobalTemplate(isolate, templates[0..]);
|
const context = v8.v8__Context__New(isolate, null, null);
|
||||||
|
v8.v8__Context__Enter(context);
|
||||||
const context = v8.Context.init(isolate, global_template, null);
|
defer v8.v8__Context__Exit(context);
|
||||||
context.enter();
|
|
||||||
defer context.exit();
|
|
||||||
|
|
||||||
// Add templates to context snapshot
|
// Add templates to context snapshot
|
||||||
var last_data_index: usize = 0;
|
var last_data_index: usize = 0;
|
||||||
inline for (JsApis, 0..) |_, i| {
|
inline for (JsApis, 0..) |_, i| {
|
||||||
@setEvalBranchQuota(10_000);
|
@setEvalBranchQuota(10_000);
|
||||||
const data_index = snapshot_creator.addDataWithContext(context, @ptrCast(templates[i].handle));
|
const data_index = v8.v8__SnapshotCreator__AddData(snapshot_creator, @ptrCast(templates[i]));
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
data_start = data_index;
|
data_start = data_index;
|
||||||
last_data_index = data_index;
|
last_data_index = data_index;
|
||||||
@@ -193,16 +179,18 @@ pub fn create(allocator: Allocator) !Snapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Realize all templates by getting their functions and attaching to global
|
// Realize all templates by getting their functions and attaching to global
|
||||||
const global_obj = context.getGlobal();
|
const global_obj = v8.v8__Context__Global(context);
|
||||||
|
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
const func = templates[i].getFunction(context);
|
const func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
|
||||||
|
|
||||||
// Attach to global if it has a name
|
// Attach to global if it has a name
|
||||||
if (@hasDecl(JsApi.Meta, "name")) {
|
if (@hasDecl(JsApi.Meta, "name")) {
|
||||||
if (@hasDecl(JsApi.Meta, "constructor_alias")) {
|
if (@hasDecl(JsApi.Meta, "constructor_alias")) {
|
||||||
const v8_class_name = v8.String.initUtf8(isolate, JsApi.Meta.constructor_alias);
|
const alias = JsApi.Meta.constructor_alias;
|
||||||
_ = global_obj.setValue(context, v8_class_name, func);
|
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len));
|
||||||
|
var maybe_result: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
|
||||||
|
|
||||||
// @TODO: This is wrong. This name should be registered with the
|
// @TODO: This is wrong. This name should be registered with the
|
||||||
// illegalConstructorCallback. I.e. new Image() is OK, but
|
// illegalConstructorCallback. I.e. new Image() is OK, but
|
||||||
@@ -210,11 +198,19 @@ pub fn create(allocator: Allocator) !Snapshot {
|
|||||||
// But we _have_ to register the name, i.e. HTMLImageElement
|
// But we _have_ to register the name, i.e. HTMLImageElement
|
||||||
// has to be registered so, for now, instead of creating another
|
// has to be registered so, for now, instead of creating another
|
||||||
// template, we just hook it into the constructor.
|
// template, we just hook it into the constructor.
|
||||||
const illegal_class_name = v8.String.initUtf8(isolate, JsApi.Meta.name);
|
const name = JsApi.Meta.name;
|
||||||
_ = global_obj.setValue(context, illegal_class_name, func);
|
const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
|
var maybe_result2: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__DefineOwnProperty(global_obj, context, illegal_class_name, func, 0, &maybe_result2);
|
||||||
} else {
|
} else {
|
||||||
const v8_class_name = v8.String.initUtf8(isolate, JsApi.Meta.name);
|
const name = JsApi.Meta.name;
|
||||||
_ = global_obj.setValue(context, v8_class_name, func);
|
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
|
var maybe_result: v8.MaybeBool = undefined;
|
||||||
|
var properties: v8.PropertyAttribute = v8.None;
|
||||||
|
if (@hasDecl(JsApi.Meta, "enumerable") and JsApi.Meta.enumerable == false) {
|
||||||
|
properties |= v8.DontEnum;
|
||||||
|
}
|
||||||
|
v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, properties, &maybe_result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,8 +218,10 @@ pub fn create(allocator: Allocator) !Snapshot {
|
|||||||
{
|
{
|
||||||
// If we want to overwrite the built-in console, we have to
|
// If we want to overwrite the built-in console, we have to
|
||||||
// delete the built-in one.
|
// delete the built-in one.
|
||||||
const console_key = v8.String.initUtf8(isolate, "console");
|
const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7);
|
||||||
if (global_obj.deleteValue(context, console_key) == false) {
|
var maybe_deleted: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted);
|
||||||
|
if (maybe_deleted.value == false) {
|
||||||
return error.ConsoleDeleteError;
|
return error.ConsoleDeleteError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,39 +231,63 @@ pub fn create(allocator: Allocator) !Snapshot {
|
|||||||
// TODO: see if newer V8 engines have a way around this.
|
// TODO: see if newer V8 engines have a way around this.
|
||||||
inline for (JsApis, 0..) |JsApi, i| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
||||||
const proto_obj = templates[proto_index].getFunction(context).toObject();
|
const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context);
|
||||||
const self_obj = templates[i].getFunction(context).toObject();
|
const proto_obj: *const v8.Object = @ptrCast(proto_func);
|
||||||
_ = self_obj.setPrototype(context, proto_obj);
|
|
||||||
|
const self_func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
|
||||||
|
const self_obj: *const v8.Object = @ptrCast(self_func);
|
||||||
|
|
||||||
|
var maybe_result: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__Object__SetPrototype(self_obj, context, proto_obj, &maybe_result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Custom exception
|
// Custom exception
|
||||||
// TODO: this is an horrible hack, I can't figure out how to do this cleanly.
|
// TODO: this is an horrible hack, I can't figure out how to do this cleanly.
|
||||||
const code = v8.String.initUtf8(isolate, "DOMException.prototype.__proto__ = Error.prototype");
|
const code_str = "DOMException.prototype.__proto__ = Error.prototype";
|
||||||
_ = try (try v8.Script.compile(context, code, null)).run(context);
|
const code = v8.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.kNormal, @intCast(code_str.len));
|
||||||
|
const script = v8.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed;
|
||||||
|
_ = v8.v8__Script__Run(script, context) orelse return error.ScriptRunFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot_creator.setDefaultContext(context);
|
v8.v8__SnapshotCreator__setDefaultContext(snapshot_creator, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = snapshot_creator.createBlob(v8.FunctionCodeHandling.kKeep);
|
const blob = v8.v8__SnapshotCreator__createBlob(snapshot_creator, v8.kKeep);
|
||||||
const owned = try allocator.dupe(u8, blob.data[0..@intCast(blob.raw_size)]);
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.owns_data = true,
|
.owns_data = true,
|
||||||
.data_start = data_start,
|
.data_start = data_start,
|
||||||
.external_references = external_references,
|
.external_references = external_references,
|
||||||
.startup_data = .{ .data = owned.ptr, .raw_size = @intCast(owned.len) },
|
.startup_data = blob,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to check if a JsApi has a NamedIndexed handler
|
||||||
|
fn hasNamedIndexedGetter(comptime JsApi: type) bool {
|
||||||
|
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||||
|
inline for (declarations) |d| {
|
||||||
|
const value = @field(JsApi, d.name);
|
||||||
|
const T = @TypeOf(value);
|
||||||
|
if (T == bridge.NamedIndexed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Count total callbacks needed for external_references array
|
// Count total callbacks needed for external_references array
|
||||||
fn countExternalReferences() comptime_int {
|
fn countExternalReferences() comptime_int {
|
||||||
@setEvalBranchQuota(100_000);
|
@setEvalBranchQuota(100_000);
|
||||||
|
|
||||||
// +1 for the illegal constructor callback
|
var count: comptime_int = 0;
|
||||||
var count: comptime_int = 1;
|
|
||||||
|
// +1 for the illegal constructor callback shared by various types
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
// +1 for the noop function shared by various types
|
||||||
|
count += 1;
|
||||||
|
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
// Constructor (only if explicit)
|
// Constructor (only if explicit)
|
||||||
@@ -285,13 +307,18 @@ fn countExternalReferences() comptime_int {
|
|||||||
const T = @TypeOf(value);
|
const T = @TypeOf(value);
|
||||||
if (T == bridge.Accessor) {
|
if (T == bridge.Accessor) {
|
||||||
count += 1; // getter
|
count += 1; // getter
|
||||||
if (value.setter != null) count += 1; // setter
|
if (value.setter != null) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
} else if (T == bridge.Function) {
|
} else if (T == bridge.Function) {
|
||||||
count += 1;
|
count += 1;
|
||||||
} else if (T == bridge.Iterator) {
|
} else if (T == bridge.Iterator) {
|
||||||
count += 1;
|
count += 1;
|
||||||
} else if (T == bridge.Indexed) {
|
} else if (T == bridge.Indexed) {
|
||||||
count += 1;
|
count += 1;
|
||||||
|
if (value.enumerator != null) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
} else if (T == bridge.NamedIndexed) {
|
} else if (T == bridge.NamedIndexed) {
|
||||||
count += 1; // getter
|
count += 1; // getter
|
||||||
if (value.setter != null) count += 1;
|
if (value.setter != null) count += 1;
|
||||||
@@ -300,6 +327,15 @@ fn countExternalReferences() comptime_int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In debug mode, add unknown property callbacks for types without NamedIndexed
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
inline for (JsApis) |JsApi| {
|
||||||
|
if (!hasNamedIndexedGetter(JsApi)) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return count + 1; // +1 for null terminator
|
return count + 1; // +1 for null terminator
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +346,9 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
|
references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
|
||||||
|
references[idx] = @bitCast(@intFromPtr(&bridge.Function.noopFunction));
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
if (@hasDecl(JsApi, "constructor")) {
|
if (@hasDecl(JsApi, "constructor")) {
|
||||||
references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
|
references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
|
||||||
@@ -341,6 +380,10 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
} else if (T == bridge.Indexed) {
|
} else if (T == bridge.Indexed) {
|
||||||
references[idx] = @bitCast(@intFromPtr(value.getter));
|
references[idx] = @bitCast(@intFromPtr(value.getter));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
if (value.enumerator) |enumerator| {
|
||||||
|
references[idx] = @bitCast(@intFromPtr(enumerator));
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
} else if (T == bridge.NamedIndexed) {
|
} else if (T == bridge.NamedIndexed) {
|
||||||
references[idx] = @bitCast(@intFromPtr(value.getter));
|
references[idx] = @bitCast(@intFromPtr(value.getter));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
@@ -356,6 +399,16 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In debug mode, collect unknown property callbacks for types without NamedIndexed
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
inline for (JsApis) |JsApi| {
|
||||||
|
if (!hasNamedIndexedGetter(JsApi)) {
|
||||||
|
references[idx] = @bitCast(@intFromPtr(bridge.unknownObjectPropertyCallback(JsApi)));
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return references;
|
return references;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +418,7 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
// via `new ClassName()` - but they could, for example, be created in
|
// via `new ClassName()` - but they could, for example, be created in
|
||||||
// Zig and returned from a function call, which is why we need the
|
// Zig and returned from a function call, which is why we need the
|
||||||
// FunctionTemplate.
|
// FunctionTemplate.
|
||||||
fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate {
|
fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate {
|
||||||
const callback = blk: {
|
const callback = blk: {
|
||||||
if (@hasDecl(JsApi, "constructor")) {
|
if (@hasDecl(JsApi, "constructor")) {
|
||||||
break :blk JsApi.constructor.func;
|
break :blk JsApi.constructor.func;
|
||||||
@@ -375,19 +428,71 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem
|
|||||||
break :blk illegalConstructorCallback;
|
break :blk illegalConstructorCallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
const template = v8.FunctionTemplate.initCallback(isolate, callback);
|
const template = v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?;
|
||||||
if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
{
|
||||||
template.getInstanceTemplate().setInternalFieldCount(1);
|
const internal_field_count = comptime countInternalFields(JsApi);
|
||||||
|
if (internal_field_count > 0) {
|
||||||
|
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||||
|
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const class_name = v8.String.initUtf8(isolate, if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi));
|
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
|
||||||
template.setClassName(class_name);
|
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
|
||||||
|
v8.v8__FunctionTemplate__SetClassName(template, class_name);
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn countInternalFields(comptime JsApi: type) u8 {
|
||||||
|
var last_used_id = 0;
|
||||||
|
var cache_count: u8 = 0;
|
||||||
|
|
||||||
|
inline for (@typeInfo(JsApi).@"struct".decls) |d| {
|
||||||
|
const name: [:0]const u8 = d.name;
|
||||||
|
const value = @field(JsApi, name);
|
||||||
|
const definition = @TypeOf(value);
|
||||||
|
|
||||||
|
switch (definition) {
|
||||||
|
inline bridge.Accessor, bridge.Function => {
|
||||||
|
const cache = value.cache orelse continue;
|
||||||
|
if (cache != .internal) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// We assert that they are declared in-order. This isn't necessary
|
||||||
|
// but I don't want to do anything fancy to look for gaps or
|
||||||
|
// duplicates.
|
||||||
|
const internal_id = cache.internal;
|
||||||
|
if (internal_id != last_used_id + 1) {
|
||||||
|
@compileError(@typeName(JsApi) ++ "." ++ name ++ " has a non-monotonic cache index");
|
||||||
|
}
|
||||||
|
last_used_id = internal_id;
|
||||||
|
cache_count += 1; // this is just last_used, but it's more explicit this way
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
||||||
|
return cache_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need cache_count internal fields, + 1 for the TAO pointer (the v8 -> Zig)
|
||||||
|
// mapping) itself.
|
||||||
|
return cache_count + 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Attaches JsApi members to the prototype template (normal case)
|
// Attaches JsApi members to the prototype template (normal case)
|
||||||
fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
|
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.FunctionTemplate) void {
|
||||||
const target = template.getPrototypeTemplate();
|
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||||
|
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);
|
||||||
|
|
||||||
|
// Create a signature that validates the receiver is an instance of this template.
|
||||||
|
// This prevents crashes when JavaScript extracts a getter/method and calls it
|
||||||
|
// with the wrong `this` (e.g., documentGetter.call(null)).
|
||||||
|
const signature = v8.v8__Signature__New(isolate, template);
|
||||||
|
|
||||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||||
|
var has_named_index_getter = false;
|
||||||
|
|
||||||
inline for (declarations) |d| {
|
inline for (declarations) |d| {
|
||||||
const name: [:0]const u8 = d.name;
|
const name: [:0]const u8 = d.name;
|
||||||
const value = @field(JsApi, name);
|
const value = @field(JsApi, name);
|
||||||
@@ -395,60 +500,108 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
|
|||||||
|
|
||||||
switch (definition) {
|
switch (definition) {
|
||||||
bridge.Accessor => {
|
bridge.Accessor => {
|
||||||
const js_name = v8.String.initUtf8(isolate, name).toName();
|
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
const getter_callback = v8.FunctionTemplate.initCallback(isolate, value.getter);
|
const getter_signature = if (value.static) null else signature;
|
||||||
|
const getter_callback = v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
||||||
|
.callback = value.getter,
|
||||||
|
.signature = getter_signature,
|
||||||
|
}).?;
|
||||||
|
const setter_callback = if (value.setter) |setter|
|
||||||
|
v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
||||||
|
.callback = setter,
|
||||||
|
.signature = getter_signature,
|
||||||
|
}).?
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
|
||||||
|
var attribute: v8.PropertyAttribute = 0;
|
||||||
if (value.setter == null) {
|
if (value.setter == null) {
|
||||||
if (value.static) {
|
attribute |= v8.ReadOnly;
|
||||||
template.setAccessorGetter(js_name, getter_callback);
|
}
|
||||||
} else {
|
if (value.deletable == false) {
|
||||||
target.setAccessorGetter(js_name, getter_callback);
|
attribute |= v8.DontDelete;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value.static) {
|
||||||
|
// Static accessors: use Template's SetAccessorProperty
|
||||||
|
v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute);
|
||||||
} else {
|
} else {
|
||||||
std.debug.assert(value.static == false);
|
v8.v8__ObjectTemplate__SetAccessorProperty__Config(prototype, &.{
|
||||||
const setter_callback = v8.FunctionTemplate.initCallback(isolate, value.setter);
|
.key = js_name,
|
||||||
target.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
|
.getter = getter_callback,
|
||||||
|
.setter = setter_callback,
|
||||||
|
.attribute = attribute,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Function => {
|
bridge.Function => {
|
||||||
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
|
// For non-static functions, use the signature to validate the receiver
|
||||||
const js_name = v8.String.initUtf8(isolate, name).toName();
|
const func_signature = if (value.static) null else signature;
|
||||||
|
const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
||||||
|
.callback = value.func,
|
||||||
|
.length = value.arity,
|
||||||
|
.signature = func_signature,
|
||||||
|
}).?;
|
||||||
|
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
if (value.static) {
|
if (value.static) {
|
||||||
template.set(js_name, function_template, v8.PropertyAttribute.None);
|
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
||||||
} else {
|
} else {
|
||||||
target.set(js_name, function_template, v8.PropertyAttribute.None);
|
v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.None);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Indexed => {
|
bridge.Indexed => {
|
||||||
const configuration = v8.IndexedPropertyHandlerConfiguration{
|
var configuration: v8.IndexedPropertyHandlerConfiguration = .{
|
||||||
.getter = value.getter,
|
.getter = value.getter,
|
||||||
|
.enumerator = value.enumerator,
|
||||||
|
.setter = null,
|
||||||
|
.query = null,
|
||||||
|
.deleter = null,
|
||||||
|
.definer = null,
|
||||||
|
.descriptor = null,
|
||||||
|
.data = null,
|
||||||
|
.flags = 0,
|
||||||
};
|
};
|
||||||
target.setIndexedProperty(configuration, null);
|
v8.v8__ObjectTemplate__SetIndexedHandler(instance, &configuration);
|
||||||
|
},
|
||||||
|
bridge.NamedIndexed => {
|
||||||
|
var configuration: v8.NamedPropertyHandlerConfiguration = .{
|
||||||
|
.getter = value.getter,
|
||||||
|
.setter = value.setter,
|
||||||
|
.query = null,
|
||||||
|
.deleter = value.deleter,
|
||||||
|
.enumerator = null,
|
||||||
|
.definer = null,
|
||||||
|
.descriptor = null,
|
||||||
|
.data = null,
|
||||||
|
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||||
|
};
|
||||||
|
v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);
|
||||||
|
has_named_index_getter = true;
|
||||||
},
|
},
|
||||||
bridge.NamedIndexed => template.getInstanceTemplate().setNamedProperty(.{
|
|
||||||
.getter = value.getter,
|
|
||||||
.setter = value.setter,
|
|
||||||
.deleter = value.deleter,
|
|
||||||
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
|
|
||||||
}, null),
|
|
||||||
bridge.Iterator => {
|
bridge.Iterator => {
|
||||||
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
|
const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func }).?;
|
||||||
const js_name = if (value.async)
|
const js_name = if (value.async)
|
||||||
v8.Symbol.getAsyncIterator(isolate).toName()
|
v8.v8__Symbol__GetAsyncIterator(isolate)
|
||||||
else
|
else
|
||||||
v8.Symbol.getIterator(isolate).toName();
|
v8.v8__Symbol__GetIterator(isolate);
|
||||||
target.set(js_name, function_template, v8.PropertyAttribute.None);
|
v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.None);
|
||||||
},
|
},
|
||||||
bridge.Property => {
|
bridge.Property => {
|
||||||
const js_value = switch (value) {
|
const js_value = switch (value.value) {
|
||||||
.int => |v| js.simpleZigValueToJs(isolate, v, true, false),
|
.null => js.simpleZigValueToJs(.{ .handle = isolate }, null, true, false),
|
||||||
|
inline .bool, .int, .float, .string => |v| js.simpleZigValueToJs(.{ .handle = isolate }, v, true, false),
|
||||||
};
|
};
|
||||||
|
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
|
|
||||||
const js_name = v8.String.initUtf8(isolate, name).toName();
|
{
|
||||||
// apply it both to the type itself
|
const flags = if (value.readonly) v8.ReadOnly + v8.DontDelete else 0;
|
||||||
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
v8.v8__Template__Set(@ptrCast(prototype), js_name, js_value, flags);
|
||||||
|
}
|
||||||
|
|
||||||
// and to instances of the type
|
if (value.template) {
|
||||||
target.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
// apply it both to the type itself (e.g. Node.Elem)
|
||||||
|
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bridge.Constructor => {}, // already handled in generateConstructor
|
bridge.Constructor => {}, // already handled in generateConstructor
|
||||||
else => {},
|
else => {},
|
||||||
@@ -456,15 +609,31 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (@hasDecl(JsApi.Meta, "htmldda")) {
|
if (@hasDecl(JsApi.Meta, "htmldda")) {
|
||||||
const instance_template = template.getInstanceTemplate();
|
v8.v8__ObjectTemplate__MarkAsUndetectable(instance);
|
||||||
instance_template.markAsUndetectable();
|
v8.v8__ObjectTemplate__SetCallAsFunctionHandler(instance, JsApi.Meta.callable.func);
|
||||||
instance_template.setCallAsFunctionHandler(JsApi.Meta.callable.func);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@hasDecl(JsApi.Meta, "name")) {
|
if (@hasDecl(JsApi.Meta, "name")) {
|
||||||
const js_name = v8.Symbol.getToStringTag(isolate).toName();
|
const js_name = v8.v8__Symbol__GetToStringTag(isolate);
|
||||||
const instance_template = template.getInstanceTemplate();
|
const js_value = v8.v8__String__NewFromUtf8(isolate, JsApi.Meta.name.ptr, v8.kNormal, @intCast(JsApi.Meta.name.len));
|
||||||
instance_template.set(js_name, v8.String.initUtf8(isolate, JsApi.Meta.name), v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
v8.v8__Template__Set(@ptrCast(instance), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
if (!has_named_index_getter) {
|
||||||
|
var configuration: v8.NamedPropertyHandlerConfiguration = .{
|
||||||
|
.getter = bridge.unknownObjectPropertyCallback(JsApi),
|
||||||
|
.setter = null,
|
||||||
|
.query = null,
|
||||||
|
.deleter = null,
|
||||||
|
.enumerator = null,
|
||||||
|
.definer = null,
|
||||||
|
.descriptor = null,
|
||||||
|
.data = null,
|
||||||
|
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||||
|
};
|
||||||
|
v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,10 +651,15 @@ fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Shared illegal constructor callback for types without explicit constructors
|
// Shared illegal constructor callback for types without explicit constructors
|
||||||
fn illegalConstructorCallback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info);
|
||||||
const iso = info.getIsolate();
|
|
||||||
log.warn(.js, "Illegal constructor call", .{});
|
log.warn(.js, "Illegal constructor call", .{});
|
||||||
const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
|
|
||||||
info.getReturnValue().set(js_exception);
|
const message = v8.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.kNormal, 19);
|
||||||
|
const js_exception = v8.v8__Exception__TypeError(message);
|
||||||
|
|
||||||
|
_ = v8.v8__Isolate__ThrowException(isolate, js_exception);
|
||||||
|
var return_value: v8.ReturnValue = undefined;
|
||||||
|
v8.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value);
|
||||||
|
v8.v8__ReturnValue__Set(return_value, js_exception);
|
||||||
}
|
}
|
||||||
|
|||||||
111
src/browser/js/String.zig
Normal file
111
src/browser/js/String.zig
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const SSO = @import("../../string.zig").String;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const String = @This();
|
||||||
|
|
||||||
|
local: *const js.Local,
|
||||||
|
handle: *const v8.String,
|
||||||
|
|
||||||
|
pub fn toSlice(self: String) ![]u8 {
|
||||||
|
return self._toSlice(false, self.local.call_arena);
|
||||||
|
}
|
||||||
|
pub fn toSliceZ(self: String) ![:0]u8 {
|
||||||
|
return self._toSlice(true, self.local.call_arena);
|
||||||
|
}
|
||||||
|
pub fn toSliceWithAlloc(self: String, allocator: Allocator) ![]u8 {
|
||||||
|
return self._toSlice(false, allocator);
|
||||||
|
}
|
||||||
|
fn _toSlice(self: String, comptime null_terminate: bool, allocator: Allocator) !(if (null_terminate) [:0]u8 else []u8) {
|
||||||
|
const local = self.local;
|
||||||
|
const handle = self.handle;
|
||||||
|
const isolate = local.isolate.handle;
|
||||||
|
|
||||||
|
const len = v8.v8__String__Utf8Length(handle, isolate);
|
||||||
|
const buf = try (if (comptime null_terminate) allocator.allocSentinel(u8, @intCast(len), 0) else allocator.alloc(u8, @intCast(len)));
|
||||||
|
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(n == len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) {
|
||||||
|
if (comptime global) {
|
||||||
|
return .{ .str = try self.toSSOWithAlloc(self.local.ctx.session.page_arena) };
|
||||||
|
}
|
||||||
|
return self.toSSOWithAlloc(self.local.call_arena);
|
||||||
|
}
|
||||||
|
pub fn toSSOWithAlloc(self: String, allocator: Allocator) !SSO {
|
||||||
|
const handle = self.handle;
|
||||||
|
const isolate = self.local.isolate.handle;
|
||||||
|
|
||||||
|
const len: usize = @intCast(v8.v8__String__Utf8Length(handle, isolate));
|
||||||
|
|
||||||
|
if (len <= 12) {
|
||||||
|
var content: [12]u8 = undefined;
|
||||||
|
const n = v8.v8__String__WriteUtf8(handle, isolate, &content[0], content.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(n == len);
|
||||||
|
}
|
||||||
|
// Weird that we do this _after_, but we have to..I've seen weird issues
|
||||||
|
// in ReleaseMode where v8 won't write to content if it starts off zero
|
||||||
|
// initiated
|
||||||
|
@memset(content[len..], 0);
|
||||||
|
return .{ .len = @intCast(len), .payload = .{ .content = content } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = try allocator.alloc(u8, len);
|
||||||
|
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(n == len);
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefix: [4]u8 = @splat(0);
|
||||||
|
@memcpy(&prefix, buf[0..4]);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.len = @intCast(len),
|
||||||
|
.payload = .{ .heap = .{
|
||||||
|
.prefix = prefix,
|
||||||
|
.ptr = buf.ptr,
|
||||||
|
} },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(self: String, writer: *std.Io.Writer) !void {
|
||||||
|
const local = self.local;
|
||||||
|
const handle = self.handle;
|
||||||
|
const isolate = local.isolate.handle;
|
||||||
|
|
||||||
|
var small: [1024]u8 = undefined;
|
||||||
|
const len = v8.v8__String__Utf8Length(handle, isolate);
|
||||||
|
var buf = if (len < 1024) &small else local.call_arena.alloc(u8, @intCast(len)) catch return error.WriteFailed;
|
||||||
|
|
||||||
|
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||||
|
return writer.writeAll(buf[0..n]);
|
||||||
|
}
|
||||||
129
src/browser/js/TaggedOpaque.zig
Normal file
129
src/browser/js/TaggedOpaque.zig
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const js = @import("js.zig");
|
||||||
|
const v8 = js.v8;
|
||||||
|
const bridge = js.bridge;
|
||||||
|
|
||||||
|
// When we return a Zig object to V8, we put it on the heap and pass it into
|
||||||
|
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
|
||||||
|
// function parameter, we know what type it _should_ be.
|
||||||
|
//
|
||||||
|
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
|
||||||
|
// to the parameter type:
|
||||||
|
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
|
||||||
|
//
|
||||||
|
// But there are 2 reasons we can't do that.
|
||||||
|
//
|
||||||
|
// == Reason 1 ==
|
||||||
|
// The JS code might pass the wrong type:
|
||||||
|
//
|
||||||
|
// var cat = new Cat();
|
||||||
|
// cat.setOwner(new Cat());
|
||||||
|
//
|
||||||
|
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
|
||||||
|
// the JS code passed a *Cat.
|
||||||
|
//
|
||||||
|
// To solve this issue, we tag every returned value so that we can check what
|
||||||
|
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
|
||||||
|
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
|
||||||
|
//
|
||||||
|
// == Reason 2 ==
|
||||||
|
// Because of prototype inheritance, even "correct" code can be a challenge. For
|
||||||
|
// example, say the above JavaScript is fixed:
|
||||||
|
//
|
||||||
|
// var cat = new Cat();
|
||||||
|
// cat.setOwner(new Owner("Leto"));
|
||||||
|
//
|
||||||
|
// The issue is that setOwner might not expect an *Owner, but rather a
|
||||||
|
// *Person, which is the prototype for Owner. Now our Zig code is expecting
|
||||||
|
// a *Person, but it was (correctly) given an *Owner.
|
||||||
|
// For this reason, we also store the prototype chain.
|
||||||
|
const TaggedOpaque = @This();
|
||||||
|
|
||||||
|
prototype_len: u16,
|
||||||
|
prototype_chain: [*]const PrototypeChainEntry,
|
||||||
|
|
||||||
|
// Ptr to the Zig instance. Between the context where it's called (i.e.
|
||||||
|
// we have the comptime parameter info for all functions), and the index field
|
||||||
|
// we can figure out what type this is.
|
||||||
|
value: *anyopaque,
|
||||||
|
|
||||||
|
// When we're asked to describe an object via the Inspector, we _must_ include
|
||||||
|
// the proper subtype (and description) fields in the returned JSON.
|
||||||
|
// V8 will give us a Value and ask us for the subtype. From the js.Value we
|
||||||
|
// can get a js.Object, and from the js.Object, we can get out TaggedOpaque
|
||||||
|
// which is where we store the subtype.
|
||||||
|
subtype: ?bridge.SubType,
|
||||||
|
|
||||||
|
pub const PrototypeChainEntry = struct {
|
||||||
|
index: bridge.JsApiLookup.BackingInt,
|
||||||
|
offset: u16, // offset to the _proto field
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reverses the mapZigInstanceToJs, making sure that our TaggedOpaque
|
||||||
|
// contains a ptr to the correct type.
|
||||||
|
pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R {
|
||||||
|
const ti = @typeInfo(R);
|
||||||
|
if (ti != .pointer) {
|
||||||
|
@compileError("non-pointer Zig parameter type: " ++ @typeName(R));
|
||||||
|
}
|
||||||
|
|
||||||
|
const T = ti.pointer.child;
|
||||||
|
const JsApi = bridge.Struct(T).JsApi;
|
||||||
|
|
||||||
|
if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
||||||
|
// Empty structs aren't stored as TOAs and there's no data
|
||||||
|
// stored in the JSObject's IntenrnalField. Why bother when
|
||||||
|
// we can just return an empty struct here?
|
||||||
|
return @constCast(@as(*const T, &.{}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle);
|
||||||
|
// if it isn't an empty struct, then the v8.Object should have an
|
||||||
|
// InternalFieldCount > 0, since our toa pointer should be embedded
|
||||||
|
// at index 0 of the internal field count.
|
||||||
|
if (internal_field_count == 0) {
|
||||||
|
return error.InvalidArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bridge.JsApiLookup.has(JsApi)) {
|
||||||
|
@compileError("unknown Zig type: " ++ @typeName(R));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(js_obj_handle, 0).?;
|
||||||
|
const tao: *TaggedOpaque = @ptrCast(@alignCast(tao_ptr));
|
||||||
|
const expected_type_index = bridge.JsApiLookup.getId(JsApi);
|
||||||
|
|
||||||
|
const prototype_chain = tao.prototype_chain[0..tao.prototype_len];
|
||||||
|
if (prototype_chain[0].index == expected_type_index) {
|
||||||
|
return @ptrCast(@alignCast(tao.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok, let's walk up the chain
|
||||||
|
var ptr = @intFromPtr(tao.value);
|
||||||
|
for (prototype_chain[1..]) |proto| {
|
||||||
|
ptr += proto.offset; // the offset to the _proto field
|
||||||
|
const proto_ptr: **anyopaque = @ptrFromInt(ptr);
|
||||||
|
if (proto.index == expected_type_index) {
|
||||||
|
return @ptrCast(@alignCast(proto_ptr.*));
|
||||||
|
}
|
||||||
|
ptr = @intFromPtr(proto_ptr.*);
|
||||||
|
}
|
||||||
|
return error.InvalidArgument;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -20,63 +20,131 @@ const std = @import("std");
|
|||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const TryCatch = @This();
|
const TryCatch = @This();
|
||||||
|
|
||||||
inner: v8.TryCatch,
|
handle: v8.TryCatch,
|
||||||
context: *const js.Context,
|
local: *const js.Local,
|
||||||
|
|
||||||
pub fn init(self: *TryCatch, context: *const js.Context) void {
|
pub fn init(self: *TryCatch, l: *const js.Local) void {
|
||||||
self.context = context;
|
self.local = l;
|
||||||
self.inner.init(context.isolate);
|
v8.v8__TryCatch__CONSTRUCT(&self.handle, l.isolate.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hasCaught(self: TryCatch) bool {
|
pub fn hasCaught(self: TryCatch) bool {
|
||||||
return self.inner.hasCaught();
|
return v8.v8__TryCatch__HasCaught(&self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// the caller needs to deinit the string returned
|
pub fn rethrow(self: *TryCatch) void {
|
||||||
pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
if (comptime IS_DEBUG) {
|
||||||
const msg = self.inner.getException() orelse return null;
|
std.debug.assert(self.hasCaught());
|
||||||
return try self.context.valueToString(msg, .{ .allocator = allocator });
|
|
||||||
}
|
|
||||||
|
|
||||||
// the caller needs to deinit the string returned
|
|
||||||
pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
|
||||||
const context = self.context;
|
|
||||||
const s = self.inner.getStackTrace(context.v8_context) orelse return null;
|
|
||||||
return try context.valueToString(s, .{ .allocator = allocator });
|
|
||||||
}
|
|
||||||
|
|
||||||
// the caller needs to deinit the string returned
|
|
||||||
pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
|
||||||
const context = self.context;
|
|
||||||
const msg = self.inner.getMessage() orelse return null;
|
|
||||||
const sl = msg.getSourceLine(context.v8_context) orelse return null;
|
|
||||||
return try context.jsStringToZig(sl, .{ .allocator = allocator });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sourceLineNumber(self: TryCatch) ?u32 {
|
|
||||||
const context = self.context;
|
|
||||||
const msg = self.inner.getMessage() orelse return null;
|
|
||||||
return msg.getLineNumber(context.v8_context);
|
|
||||||
}
|
|
||||||
|
|
||||||
// a shorthand method to return either the entire stack message
|
|
||||||
// or just the exception message
|
|
||||||
// - in Debug mode return the stack if available
|
|
||||||
// - otherwise return the exception if available
|
|
||||||
// the caller needs to deinit the string returned
|
|
||||||
pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
|
||||||
if (comptime @import("builtin").mode == .Debug) {
|
|
||||||
if (try self.stack(allocator)) |msg| {
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return try self.exception(allocator);
|
_ = v8.v8__TryCatch__ReThrow(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn caught(self: TryCatch, allocator: Allocator) ?Caught {
|
||||||
|
if (self.hasCaught() == false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const l = self.local;
|
||||||
|
const line: ?u32 = blk: {
|
||||||
|
const handle = v8.v8__TryCatch__Message(&self.handle) orelse return null;
|
||||||
|
const line = v8.v8__Message__GetLineNumber(handle, l.handle);
|
||||||
|
break :blk if (line < 0) null else @intCast(line);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exception: ?[]const u8 = blk: {
|
||||||
|
const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null;
|
||||||
|
var js_val = js.Value{ .local = l, .handle = handle };
|
||||||
|
|
||||||
|
// If it's an Error object, try to get the message property
|
||||||
|
if (js_val.isObject()) {
|
||||||
|
const js_obj = js_val.toObject();
|
||||||
|
if (js_obj.has("message")) {
|
||||||
|
js_val = js_obj.get("message") catch break :blk null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (js_val.isString()) |js_str| {
|
||||||
|
break :blk js_str.toSliceWithAlloc(allocator) catch |err| @errorName(err);
|
||||||
|
}
|
||||||
|
break :blk null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stack: ?[]const u8 = blk: {
|
||||||
|
const handle = v8.v8__TryCatch__StackTrace(&self.handle, l.handle) orelse break :blk null;
|
||||||
|
var js_val = js.Value{ .local = l, .handle = handle };
|
||||||
|
|
||||||
|
// If it's an Error object, try to get the stack property
|
||||||
|
if (js_val.isObject()) {
|
||||||
|
const js_obj = js_val.toObject();
|
||||||
|
if (js_obj.has("stack")) {
|
||||||
|
js_val = js_obj.get("stack") catch break :blk null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (js_val.isString()) |js_str| {
|
||||||
|
break :blk js_str.toSliceWithAlloc(allocator) catch |err| @errorName(err);
|
||||||
|
}
|
||||||
|
break :blk null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.line = line,
|
||||||
|
.stack = stack,
|
||||||
|
.caught = true,
|
||||||
|
.exception = exception,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn caughtOrError(self: TryCatch, allocator: Allocator, err: anyerror) Caught {
|
||||||
|
return self.caught(allocator) orelse .{
|
||||||
|
.caught = false,
|
||||||
|
.line = null,
|
||||||
|
.stack = null,
|
||||||
|
.exception = @errorName(err),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *TryCatch) void {
|
pub fn deinit(self: *TryCatch) void {
|
||||||
self.inner.deinit();
|
v8.v8__TryCatch__DESTRUCT(&self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const Caught = struct {
|
||||||
|
line: ?u32 = null,
|
||||||
|
caught: bool = false,
|
||||||
|
stack: ?[]const u8 = null,
|
||||||
|
exception: ?[]const u8 = null,
|
||||||
|
|
||||||
|
pub fn format(self: Caught, writer: *std.Io.Writer) !void {
|
||||||
|
const separator = @import("../../log.zig").separator();
|
||||||
|
try writer.print("{s}exception: {?s}", .{ separator, self.exception });
|
||||||
|
try writer.print("{s}stack: {?s}", .{ separator, self.stack });
|
||||||
|
try writer.print("{s}line: {?d}", .{ separator, self.line });
|
||||||
|
try writer.print("{s}caught: {any}", .{ separator, self.caught });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logFmt(self: Caught, comptime prefix: []const u8, writer: anytype) !void {
|
||||||
|
try writer.write(prefix ++ ".exception", self.exception orelse "???");
|
||||||
|
try writer.write(prefix ++ ".stack", self.stack orelse "na");
|
||||||
|
try writer.write(prefix ++ ".line", self.line);
|
||||||
|
try writer.write(prefix ++ ".caught", self.caught);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: Caught, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("exception");
|
||||||
|
try jw.write(self.exception);
|
||||||
|
try jw.objectField("stack");
|
||||||
|
try jw.write(self.stack);
|
||||||
|
try jw.objectField("line");
|
||||||
|
try jw.write(self.line);
|
||||||
|
try jw.objectField("caught");
|
||||||
|
try jw.write(self.caught);
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -18,87 +18,373 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
|
const SSO = @import("../../string.zig").String;
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const PersistentValue = v8.Persistent(v8.Value);
|
|
||||||
|
|
||||||
const Value = @This();
|
const Value = @This();
|
||||||
js_val: v8.Value,
|
|
||||||
context: *js.Context,
|
local: *const js.Local,
|
||||||
|
handle: *const v8.Value,
|
||||||
|
|
||||||
pub fn isObject(self: Value) bool {
|
pub fn isObject(self: Value) bool {
|
||||||
return self.js_val.isObject();
|
return v8.v8__Value__IsObject(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isString(self: Value) bool {
|
pub fn isString(self: Value) ?js.String {
|
||||||
return self.js_val.isString();
|
const handle = self.handle;
|
||||||
|
if (!v8.v8__Value__IsString(handle)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return .{ .local = self.local, .handle = @ptrCast(handle) };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isArray(self: Value) bool {
|
pub fn isArray(self: Value) bool {
|
||||||
return self.js_val.isArray();
|
return v8.v8__Value__IsArray(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isSymbol(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsSymbol(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isFunction(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsFunction(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isNull(self: Value) bool {
|
pub fn isNull(self: Value) bool {
|
||||||
return self.js_val.isNull();
|
return v8.v8__Value__IsNull(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isUndefined(self: Value) bool {
|
pub fn isUndefined(self: Value) bool {
|
||||||
return self.js_val.isUndefined();
|
return v8.v8__Value__IsUndefined(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
|
pub fn isNullOrUndefined(self: Value) bool {
|
||||||
return self.context.valueToString(self.js_val, .{ .allocator = allocator });
|
return v8.v8__Value__IsNullOrUndefined(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fromJson(ctx: *js.Context, json: []const u8) !Value {
|
pub fn isNumber(self: Value) bool {
|
||||||
const json_string = v8.String.initUtf8(ctx.isolate, json);
|
return v8.v8__Value__IsNumber(self.handle);
|
||||||
const value = try v8.Json.parse(ctx.v8_context, json_string);
|
|
||||||
return Value{ .context = ctx, .js_val = value };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn persist(self: Value) !Value {
|
pub fn isNumberObject(self: Value) bool {
|
||||||
const js_val = self.js_val;
|
return v8.v8__Value__IsNumberObject(self.handle);
|
||||||
var context = self.context;
|
}
|
||||||
|
|
||||||
const persisted = PersistentValue.init(context.isolate, js_val);
|
pub fn isInt32(self: Value) bool {
|
||||||
try context.js_value_list.append(context.arena, persisted);
|
return v8.v8__Value__IsInt32(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
return Value{ .context = context, .js_val = persisted.toValue() };
|
pub fn isUint32(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsUint32(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isBigInt(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsBigInt(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isBigIntObject(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsBigIntObject(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isBoolean(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsBoolean(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isBooleanObject(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsBooleanObject(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isTrue(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsTrue(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isFalse(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsFalse(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isTypedArray(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsTypedArray(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isArrayBufferView(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsArrayBufferView(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isArrayBuffer(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsArrayBuffer(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isUint8Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsUint8Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isUint8ClampedArray(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsUint8ClampedArray(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isInt8Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsInt8Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isUint16Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsUint16Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isInt16Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsInt16Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isUint32Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsUint32Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isInt32Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsInt32Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isBigUint64Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsBigUint64Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isBigInt64Array(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsBigInt64Array(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isPromise(self: Value) bool {
|
||||||
|
return v8.v8__Value__IsPromise(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toBool(self: Value) bool {
|
||||||
|
return v8.v8__Value__BooleanValue(self.handle, self.local.isolate.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn typeOf(self: Value) js.String {
|
||||||
|
const str_handle = v8.v8__Value__TypeOf(self.handle, self.local.isolate.handle).?;
|
||||||
|
return js.String{ .local = self.local, .handle = str_handle };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toF32(self: Value) !f32 {
|
||||||
|
return @floatCast(try self.toF64());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toF64(self: Value) !f64 {
|
||||||
|
var maybe: v8.MaybeF64 = undefined;
|
||||||
|
v8.v8__Value__NumberValue(self.handle, self.local.handle, &maybe);
|
||||||
|
if (!maybe.has_value) {
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
return maybe.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toI32(self: Value) !i32 {
|
||||||
|
var maybe: v8.MaybeI32 = undefined;
|
||||||
|
v8.v8__Value__Int32Value(self.handle, self.local.handle, &maybe);
|
||||||
|
if (!maybe.has_value) {
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
return maybe.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toU32(self: Value) !u32 {
|
||||||
|
var maybe: v8.MaybeU32 = undefined;
|
||||||
|
v8.v8__Value__Uint32Value(self.handle, self.local.handle, &maybe);
|
||||||
|
if (!maybe.has_value) {
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
return maybe.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toPromise(self: Value) js.Promise {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(self.isPromise());
|
||||||
|
}
|
||||||
|
return .{
|
||||||
|
.local = self.local,
|
||||||
|
.handle = @ptrCast(self.handle),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toString(self: Value) !js.String {
|
||||||
|
const l = self.local;
|
||||||
|
const value_handle: *const v8.Value = blk: {
|
||||||
|
if (self.isSymbol()) {
|
||||||
|
break :blk @ptrCast(v8.v8__Symbol__Description(@ptrCast(self.handle), l.isolate.handle).?);
|
||||||
|
}
|
||||||
|
break :blk self.handle;
|
||||||
|
};
|
||||||
|
|
||||||
|
const str_handle = v8.v8__Value__ToString(value_handle, l.handle) orelse return error.JsException;
|
||||||
|
return .{ .local = self.local, .handle = str_handle };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toSSO(self: Value, comptime global: bool) !(if (global) SSO.Global else SSO) {
|
||||||
|
return (try self.toString()).toSSO(global);
|
||||||
|
}
|
||||||
|
pub fn toSSOWithAlloc(self: Value, allocator: Allocator) !SSO {
|
||||||
|
return (try self.toString()).toSSOWithAlloc(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toStringSlice(self: Value) ![]u8 {
|
||||||
|
return (try self.toString()).toSlice();
|
||||||
|
}
|
||||||
|
pub fn toStringSliceZ(self: Value) ![:0]u8 {
|
||||||
|
return (try self.toString()).toSliceZ();
|
||||||
|
}
|
||||||
|
pub fn toStringSliceWithAlloc(self: Value, allocator: Allocator) ![]u8 {
|
||||||
|
return (try self.toString()).toSliceWithAlloc(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toJson(self: Value, allocator: Allocator) ![]u8 {
|
||||||
|
const local = self.local;
|
||||||
|
const str_handle = v8.v8__JSON__Stringify(local.handle, self.handle, null) orelse return error.JsException;
|
||||||
|
return js.String.toSliceWithAlloc(.{ .local = local, .handle = str_handle }, allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently does not support host objects (Blob, File, etc.) or transferables
|
||||||
|
// which require delegate callbacks to be implemented.
|
||||||
|
pub fn structuredClone(self: Value) !Value {
|
||||||
|
const local = self.local;
|
||||||
|
const v8_context = local.handle;
|
||||||
|
const v8_isolate = local.isolate.handle;
|
||||||
|
|
||||||
|
const size, const data = blk: {
|
||||||
|
const serializer = v8.v8__ValueSerializer__New(v8_isolate, null) orelse return error.JsException;
|
||||||
|
defer v8.v8__ValueSerializer__DELETE(serializer);
|
||||||
|
|
||||||
|
var write_result: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__ValueSerializer__WriteHeader(serializer);
|
||||||
|
v8.v8__ValueSerializer__WriteValue(serializer, v8_context, self.handle, &write_result);
|
||||||
|
if (!write_result.has_value or !write_result.value) {
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
|
||||||
|
var size: usize = undefined;
|
||||||
|
const data = v8.v8__ValueSerializer__Release(serializer, &size) orelse return error.JsException;
|
||||||
|
break :blk .{ size, data };
|
||||||
|
};
|
||||||
|
|
||||||
|
defer v8.v8__ValueSerializer__FreeBuffer(data);
|
||||||
|
|
||||||
|
const cloned_handle = blk: {
|
||||||
|
const deserializer = v8.v8__ValueDeserializer__New(v8_isolate, data, size, null) orelse return error.JsException;
|
||||||
|
defer v8.v8__ValueDeserializer__DELETE(deserializer);
|
||||||
|
|
||||||
|
var read_header_result: v8.MaybeBool = undefined;
|
||||||
|
v8.v8__ValueDeserializer__ReadHeader(deserializer, v8_context, &read_header_result);
|
||||||
|
if (!read_header_result.has_value or !read_header_result.value) {
|
||||||
|
return error.JsException;
|
||||||
|
}
|
||||||
|
break :blk v8.v8__ValueDeserializer__ReadValue(deserializer, v8_context) orelse return error.JsException;
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{ .local = local, .handle = cloned_handle };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: Value) !Global {
|
||||||
|
return self._persist(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn temp(self: Value) !Temp {
|
||||||
|
return self._persist(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
if (comptime is_global) {
|
||||||
|
try ctx.trackGlobal(global);
|
||||||
|
return .{ .handle = global, .temps = {} };
|
||||||
|
}
|
||||||
|
try ctx.trackTemp(global);
|
||||||
|
return .{ .handle = global, .temps = &ctx.identity.temps };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toZig(self: Value, comptime T: type) !T {
|
pub fn toZig(self: Value, comptime T: type) !T {
|
||||||
return self.context.jsValueToZig(T, self.js_val);
|
return self.local.jsValueToZig(T, self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toObject(self: Value) js.Object {
|
pub fn toObject(self: Value) js.Object {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(self.isObject());
|
||||||
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.context = self.context,
|
.local = self.local,
|
||||||
.js_obj = self.js_val.castTo(v8.Object),
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toArray(self: Value) js.Array {
|
pub fn toArray(self: Value) js.Array {
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(self.isArray());
|
||||||
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.context = self.context,
|
.local = self.local,
|
||||||
.js_arr = self.js_val.castTo(v8.Array),
|
.handle = @ptrCast(self.handle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub const Value = struct {
|
pub fn toBigInt(self: Value) js.BigInt {
|
||||||
// value: v8.Value,
|
if (comptime IS_DEBUG) {
|
||||||
// context: *const Context,
|
std.debug.assert(self.isBigInt());
|
||||||
|
}
|
||||||
|
|
||||||
// // the caller needs to deinit the string returned
|
return .{
|
||||||
// pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
|
.handle = @ptrCast(self.handle),
|
||||||
// return self.context.valueToString(self.value, .{ .allocator = allocator });
|
};
|
||||||
// }
|
}
|
||||||
|
|
||||||
// pub fn fromJson(ctx: *Context, json: []const u8) !Value {
|
pub fn format(self: Value, writer: *std.Io.Writer) !void {
|
||||||
// const json_string = v8.String.initUtf8(ctx.isolate, json);
|
if (comptime IS_DEBUG) {
|
||||||
// const value = try v8.Json.parse(ctx.v8_context, json_string);
|
return self.local.debugValue(self, writer);
|
||||||
// return Value{ .context = ctx, .value = value };
|
}
|
||||||
// }
|
const js_str = self.toString() catch return error.WriteFailed;
|
||||||
// };
|
return js_str.format(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Temp = G(.temp);
|
||||||
|
pub const Global = G(.global);
|
||||||
|
|
||||||
|
const GlobalType = enum(u8) {
|
||||||
|
temp,
|
||||||
|
global,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn G(comptime global_type: GlobalType) type {
|
||||||
|
return struct {
|
||||||
|
handle: v8.Global,
|
||||||
|
temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Self, l: *const js.Local) Value {
|
||||||
|
return .{
|
||||||
|
.local = l,
|
||||||
|
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEqual(self: *const Self, other: Value) bool {
|
||||||
|
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(self: *const Self) void {
|
||||||
|
if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| {
|
||||||
|
var g = kv.value;
|
||||||
|
v8.v8__Global__Reset(&g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -18,12 +18,15 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const log = @import("../../log.zig");
|
const Page = @import("../Page.zig");
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const Caller = @import("Caller.zig");
|
const Caller = @import("Caller.zig");
|
||||||
|
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
pub fn Builder(comptime T: type) type {
|
pub fn Builder(comptime T: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
pub const @"type" = T;
|
pub const @"type" = T;
|
||||||
@@ -33,16 +36,16 @@ pub fn Builder(comptime T: type) type {
|
|||||||
return Constructor.init(T, func, opts);
|
return Constructor.init(T, func, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn accessor(comptime getter: anytype, comptime setter: anytype, comptime opts: Accessor.Opts) Accessor {
|
pub fn accessor(comptime getter: anytype, comptime setter: anytype, comptime opts: Caller.Function.Opts) Accessor {
|
||||||
return Accessor.init(T, getter, setter, opts);
|
return Accessor.init(T, getter, setter, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn function(comptime func: anytype, comptime opts: Function.Opts) Function {
|
pub fn function(comptime func: anytype, comptime opts: Caller.Function.Opts) Function {
|
||||||
return Function.init(T, func, opts);
|
return Function.init(T, func, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn indexed(comptime getter_func: anytype, comptime opts: Indexed.Opts) Indexed {
|
pub fn indexed(comptime getter_func: anytype, comptime enumerator_func: anytype, comptime opts: Indexed.Opts) Indexed {
|
||||||
return Indexed.init(T, getter_func, opts);
|
return Indexed.init(T, getter_func, enumerator_func, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn namedIndexed(comptime getter_func: anytype, setter_func: anytype, deleter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed {
|
pub fn namedIndexed(comptime getter_func: anytype, setter_func: anytype, deleter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed {
|
||||||
@@ -57,16 +60,29 @@ pub fn Builder(comptime T: type) type {
|
|||||||
return Callable.init(T, func, opts);
|
return Callable.init(T, func, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn property(value: anytype) Property {
|
pub fn property(value: anytype, opts: Property.Opts) Property {
|
||||||
switch (@typeInfo(@TypeOf(value))) {
|
switch (@typeInfo(@TypeOf(value))) {
|
||||||
.comptime_int, .int => return .{ .int = value },
|
.bool => return Property.init(.{ .bool = value }, opts),
|
||||||
|
.null => return Property.init(.null, opts),
|
||||||
|
.comptime_int, .int => return Property.init(.{ .int = value }, opts),
|
||||||
|
.comptime_float, .float => return Property.init(.{ .float = value }, opts),
|
||||||
|
.pointer => |ptr| switch (ptr.size) {
|
||||||
|
.one => {
|
||||||
|
const one_info = @typeInfo(ptr.child);
|
||||||
|
if (one_info == .array and one_info.array.child == u8) {
|
||||||
|
return Property.init(.{ .string = value }, opts);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
@compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet");
|
@compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prototypeChain() [prototypeChainLength(T)]js.PrototypeChainEntry {
|
const PrototypeChainEntry = @import("TaggedOpaque.zig").PrototypeChainEntry;
|
||||||
var entries: [prototypeChainLength(T)]js.PrototypeChainEntry = undefined;
|
pub fn prototypeChain() [prototypeChainLength(T)]PrototypeChainEntry {
|
||||||
|
var entries: [prototypeChainLength(T)]PrototypeChainEntry = undefined;
|
||||||
|
|
||||||
entries[0] = .{ .offset = 0, .index = JsApiLookup.getId(T.JsApi) };
|
entries[0] = .{ .offset = 0, .index = JsApiLookup.getId(T.JsApi) };
|
||||||
|
|
||||||
@@ -85,11 +101,38 @@ pub fn Builder(comptime T: type) type {
|
|||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool, session: *Session) void) Finalizer {
|
||||||
|
return .{
|
||||||
|
.from_zig = struct {
|
||||||
|
fn wrap(ptr: *anyopaque, session: *Session) void {
|
||||||
|
func(@ptrCast(@alignCast(ptr)), true, session);
|
||||||
|
}
|
||||||
|
}.wrap,
|
||||||
|
|
||||||
|
.from_v8 = struct {
|
||||||
|
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
|
||||||
|
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
|
||||||
|
const fc: *Session.FinalizerCallback = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
|
const value_ptr = fc.ptr;
|
||||||
|
if (fc.identity.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
||||||
|
func(@ptrCast(@alignCast(value_ptr)), false, fc.session);
|
||||||
|
fc.releaseIdentity();
|
||||||
|
} else {
|
||||||
|
// A bit weird, but v8 _requires_ that we release it
|
||||||
|
// If we don't. We'll 100% crash.
|
||||||
|
v8.v8__Global__Reset(&fc.global);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.wrap,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Constructor = struct {
|
pub const Constructor = struct {
|
||||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
dom_exception: bool = false,
|
dom_exception: bool = false,
|
||||||
@@ -97,12 +140,13 @@ pub const Constructor = struct {
|
|||||||
|
|
||||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor {
|
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor {
|
||||||
return .{ .func = struct {
|
return .{ .func = struct {
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||||
var caller = Caller.init(info);
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
defer caller.deinit();
|
defer caller.deinit();
|
||||||
|
|
||||||
caller.constructor(T, func, info, .{
|
caller.constructor(T, func, handle.?, .{
|
||||||
.dom_exception = opts.dom_exception,
|
.dom_exception = opts.dom_exception,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -112,88 +156,69 @@ pub const Constructor = struct {
|
|||||||
|
|
||||||
pub const Function = struct {
|
pub const Function = struct {
|
||||||
static: bool,
|
static: bool,
|
||||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
arity: usize,
|
||||||
|
noop: bool = false,
|
||||||
|
cache: ?Caller.Function.Opts.Caching = null,
|
||||||
|
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||||
|
|
||||||
const Opts = struct {
|
fn init(comptime T: type, comptime func: anytype, comptime opts: Caller.Function.Opts) Function {
|
||||||
static: bool = false,
|
|
||||||
dom_exception: bool = false,
|
|
||||||
as_typed_array: bool = false,
|
|
||||||
null_as_undefined: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Function {
|
|
||||||
return .{
|
return .{
|
||||||
|
.cache = opts.cache,
|
||||||
.static = opts.static,
|
.static = opts.static,
|
||||||
.func = struct {
|
.arity = getArity(@TypeOf(func)),
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
.func = if (opts.noop) noopFunction else struct {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
var caller = Caller.init(info);
|
Caller.Function.call(T, handle.?, func, opts);
|
||||||
defer caller.deinit();
|
|
||||||
|
|
||||||
if (comptime opts.static) {
|
|
||||||
caller.function(T, func, info, .{
|
|
||||||
.dom_exception = opts.dom_exception,
|
|
||||||
.as_typed_array = opts.as_typed_array,
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
caller.method(T, func, info, .{
|
|
||||||
.dom_exception = opts.dom_exception,
|
|
||||||
.as_typed_array = opts.as_typed_array,
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.wrap,
|
}.wrap,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn noopFunction(_: ?*const v8.FunctionCallbackInfo) callconv(.c) void {}
|
||||||
|
|
||||||
|
fn getArity(comptime T: type) usize {
|
||||||
|
var count: usize = 0;
|
||||||
|
var params = @typeInfo(T).@"fn".params;
|
||||||
|
for (params[1..]) |p| { // start at 1, skip self
|
||||||
|
const PT = p.type.?;
|
||||||
|
if (PT == *Page or PT == *const Page) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (@typeInfo(PT) == .optional) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Accessor = struct {
|
pub const Accessor = struct {
|
||||||
static: bool = false,
|
static: bool = false,
|
||||||
getter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null,
|
deletable: bool = true,
|
||||||
setter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null,
|
cache: ?Caller.Function.Opts.Caching = null,
|
||||||
|
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
||||||
|
setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
||||||
|
|
||||||
const Opts = struct {
|
fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Caller.Function.Opts) Accessor {
|
||||||
static: bool = false,
|
|
||||||
cache: ?[]const u8 = null, // @ZIGDOM
|
|
||||||
as_typed_array: bool = false,
|
|
||||||
null_as_undefined: bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Opts) Accessor {
|
|
||||||
var accessor = Accessor{
|
var accessor = Accessor{
|
||||||
|
.cache = opts.cache,
|
||||||
.static = opts.static,
|
.static = opts.static,
|
||||||
|
.deletable = opts.deletable,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (@typeInfo(@TypeOf(getter)) != .null) {
|
if (@typeInfo(@TypeOf(getter)) != .null) {
|
||||||
accessor.getter = struct {
|
accessor.getter = struct {
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
Caller.Function.call(T, handle.?, getter, opts);
|
||||||
var caller = Caller.init(info);
|
|
||||||
defer caller.deinit();
|
|
||||||
|
|
||||||
caller.method(T, getter, info, .{
|
|
||||||
.as_typed_array = opts.as_typed_array,
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}.wrap;
|
}.wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@typeInfo(@TypeOf(setter)) != .null) {
|
if (@typeInfo(@TypeOf(setter)) != .null) {
|
||||||
accessor.setter = struct {
|
accessor.setter = struct {
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
Caller.Function.call(T, handle.?, setter, opts);
|
||||||
std.debug.assert(info.length() == 1);
|
|
||||||
|
|
||||||
var caller = Caller.init(info);
|
|
||||||
defer caller.deinit();
|
|
||||||
|
|
||||||
caller.method(T, setter, info, .{
|
|
||||||
.as_typed_array = opts.as_typed_array,
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}.wrap;
|
}.wrap;
|
||||||
}
|
}
|
||||||
@@ -203,32 +228,52 @@ pub const Accessor = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Indexed = struct {
|
pub const Indexed = struct {
|
||||||
getter: *const fn (idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8,
|
getter: *const fn (idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||||
|
enumerator: ?*const fn (handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
as_typed_array: bool = false,
|
as_typed_array: bool = false,
|
||||||
null_as_undefined: bool = false,
|
null_as_undefined: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) Indexed {
|
fn init(comptime T: type, comptime getter: anytype, comptime enumerator: anytype, comptime opts: Opts) Indexed {
|
||||||
return .{ .getter = struct {
|
var indexed = Indexed{
|
||||||
fn wrap(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
.enumerator = null,
|
||||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
.getter = struct {
|
||||||
var caller = Caller.init(info);
|
fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
defer caller.deinit();
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
return caller.getIndex(T, getter, idx, info, .{
|
var caller: Caller = undefined;
|
||||||
.as_typed_array = opts.as_typed_array,
|
caller.init(v8_isolate);
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
defer caller.deinit();
|
||||||
});
|
|
||||||
}
|
return caller.getIndex(T, getter, idx, handle.?, .{
|
||||||
}.wrap };
|
.as_typed_array = opts.as_typed_array,
|
||||||
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}.wrap,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (@typeInfo(@TypeOf(enumerator)) != .null) {
|
||||||
|
indexed.enumerator = struct {
|
||||||
|
fn wrap(handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
|
defer caller.deinit();
|
||||||
|
return caller.getEnumerator(T, enumerator, handle.?, .{});
|
||||||
|
}
|
||||||
|
}.wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexed;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const NamedIndexed = struct {
|
pub const NamedIndexed = struct {
|
||||||
getter: *const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8,
|
getter: *const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||||
setter: ?*const fn (c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null,
|
setter: ?*const fn (c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
|
||||||
deleter: ?*const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null,
|
deleter: ?*const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
as_typed_array: bool = false,
|
as_typed_array: bool = false,
|
||||||
@@ -237,11 +282,13 @@ pub const NamedIndexed = struct {
|
|||||||
|
|
||||||
fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {
|
fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {
|
||||||
const getter_fn = struct {
|
const getter_fn = struct {
|
||||||
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
var caller = Caller.init(info);
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
defer caller.deinit();
|
defer caller.deinit();
|
||||||
return caller.getNamedIndex(T, getter, .{ .handle = c_name.? }, info, .{
|
|
||||||
|
return caller.getNamedIndex(T, getter, c_name.?, handle.?, .{
|
||||||
.as_typed_array = opts.as_typed_array,
|
.as_typed_array = opts.as_typed_array,
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
});
|
});
|
||||||
@@ -249,12 +296,13 @@ pub const NamedIndexed = struct {
|
|||||||
}.wrap;
|
}.wrap;
|
||||||
|
|
||||||
const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct {
|
const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct {
|
||||||
fn wrap(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
fn wrap(c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
var caller = Caller.init(info);
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
defer caller.deinit();
|
defer caller.deinit();
|
||||||
|
|
||||||
return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{
|
return caller.setNamedIndex(T, setter, c_name.?, c_value.?, handle.?, .{
|
||||||
.as_typed_array = opts.as_typed_array,
|
.as_typed_array = opts.as_typed_array,
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
});
|
});
|
||||||
@@ -262,12 +310,13 @@ pub const NamedIndexed = struct {
|
|||||||
}.wrap;
|
}.wrap;
|
||||||
|
|
||||||
const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct {
|
const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct {
|
||||||
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
var caller = Caller.init(info);
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
defer caller.deinit();
|
defer caller.deinit();
|
||||||
|
|
||||||
return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{
|
return caller.deleteNamedIndex(T, deleter, c_name.?, handle.?, .{
|
||||||
.as_typed_array = opts.as_typed_array,
|
.as_typed_array = opts.as_typed_array,
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
});
|
});
|
||||||
@@ -283,7 +332,7 @@ pub const NamedIndexed = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Iterator = struct {
|
pub const Iterator = struct {
|
||||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||||
async: bool,
|
async: bool,
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
@@ -296,8 +345,8 @@ pub const Iterator = struct {
|
|||||||
return .{
|
return .{
|
||||||
.async = opts.async,
|
.async = opts.async,
|
||||||
.func = struct {
|
.func = struct {
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
const info = Caller.FunctionCallbackInfo{ .handle = handle.? };
|
||||||
info.getReturnValue().set(info.getThis());
|
info.getReturnValue().set(info.getThis());
|
||||||
}
|
}
|
||||||
}.wrap,
|
}.wrap,
|
||||||
@@ -307,11 +356,10 @@ pub const Iterator = struct {
|
|||||||
return .{
|
return .{
|
||||||
.async = opts.async,
|
.async = opts.async,
|
||||||
.func = struct {
|
.func = struct {
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
return Caller.Function.call(T, handle.?, struct_or_func, .{
|
||||||
var caller = Caller.init(info);
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
defer caller.deinit();
|
});
|
||||||
caller.method(T, struct_or_func, info, .{});
|
|
||||||
}
|
}
|
||||||
}.wrap,
|
}.wrap,
|
||||||
};
|
};
|
||||||
@@ -319,7 +367,7 @@ pub const Iterator = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Callable = struct {
|
pub const Callable = struct {
|
||||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
null_as_undefined: bool = false,
|
null_as_undefined: bool = false,
|
||||||
@@ -327,11 +375,8 @@ pub const Callable = struct {
|
|||||||
|
|
||||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {
|
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {
|
||||||
return .{ .func = struct {
|
return .{ .func = struct {
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
Caller.Function.call(T, handle.?, func, .{
|
||||||
var caller = Caller.init(info);
|
|
||||||
defer caller.deinit();
|
|
||||||
caller.method(T, func, info, .{
|
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -339,10 +384,191 @@ pub const Callable = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Property = union(enum) {
|
pub const Property = struct {
|
||||||
int: i64,
|
value: Value,
|
||||||
|
template: bool,
|
||||||
|
readonly: bool,
|
||||||
|
|
||||||
|
const Value = union(enum) {
|
||||||
|
null,
|
||||||
|
int: i64,
|
||||||
|
float: f64,
|
||||||
|
bool: bool,
|
||||||
|
string: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Opts = struct {
|
||||||
|
template: bool,
|
||||||
|
readonly: bool = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn init(value: Value, opts: Opts) Property {
|
||||||
|
return .{
|
||||||
|
.value = value,
|
||||||
|
.template = opts.template,
|
||||||
|
.readonly = opts.readonly,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Finalizer = struct {
|
||||||
|
// The finalizer wrapper when called from Zig. This is only called on
|
||||||
|
// Origin.deinit
|
||||||
|
from_zig: *const fn (ctx: *anyopaque, session: *Session) void,
|
||||||
|
|
||||||
|
// The finalizer wrapper when called from V8. This may never be called
|
||||||
|
// (hence why we fallback to calling in Origin.deinit). If it is called,
|
||||||
|
// it is only ever called after we SetWeak on the Global.
|
||||||
|
from_v8: *const fn (?*const v8.WeakCallbackInfo) callconv(.c) void,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const local = &caller.local;
|
||||||
|
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const page = local.ctx.page;
|
||||||
|
const document = page.document;
|
||||||
|
|
||||||
|
if (document.getElementById(property, page)) |el| {
|
||||||
|
const js_val = local.zigValueToJs(el, .{}) catch return 0;
|
||||||
|
var pc = Caller.PropertyCallbackInfo{ .handle = handle.? };
|
||||||
|
pc.getReturnValue().set(js_val);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
if (std.mem.startsWith(u8, property, "__")) {
|
||||||
|
// some frameworks will extend built-in types using a __ prefix
|
||||||
|
// these should always be safe to ignore.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ignored = std.StaticStringMap(void).initComptime(.{
|
||||||
|
.{ "Deno", {} },
|
||||||
|
.{ "process", {} },
|
||||||
|
.{ "ShadyDOM", {} },
|
||||||
|
.{ "ShadyCSS", {} },
|
||||||
|
|
||||||
|
// a lot of sites seem to like having their own window.config.
|
||||||
|
.{ "config", {} },
|
||||||
|
|
||||||
|
.{ "litNonce", {} },
|
||||||
|
.{ "litHtmlVersions", {} },
|
||||||
|
.{ "litElementVersions", {} },
|
||||||
|
.{ "litHtmlPolyfillSupport", {} },
|
||||||
|
.{ "litElementHydrateSupport", {} },
|
||||||
|
.{ "litElementPolyfillSupport", {} },
|
||||||
|
.{ "reactiveElementVersions", {} },
|
||||||
|
|
||||||
|
.{ "recaptcha", {} },
|
||||||
|
.{ "grecaptcha", {} },
|
||||||
|
.{ "___grecaptcha_cfg", {} },
|
||||||
|
.{ "__recaptcha_api", {} },
|
||||||
|
.{ "__google_recaptcha_client", {} },
|
||||||
|
|
||||||
|
.{ "CLOSURE_FLAGS", {} },
|
||||||
|
.{ "__REACT_DEVTOOLS_GLOBAL_HOOK__", {} },
|
||||||
|
.{ "ApplePaySession", {} },
|
||||||
|
});
|
||||||
|
if (!ignored.has(property)) {
|
||||||
|
const key = std.fmt.bufPrint(&local.ctx.page.buf, "Window:{s}", .{property}) catch return 0;
|
||||||
|
logUnknownProperty(local, key) catch return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// not intercepted
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only used for debugging
|
||||||
|
pub fn unknownObjectPropertyCallback(comptime JsApi: type) *const fn (?*const v8.Name, ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
if (comptime !IS_DEBUG) {
|
||||||
|
@compileError("unknownObjectPropertyCallback should only be used in debug builds");
|
||||||
|
}
|
||||||
|
|
||||||
|
return struct {
|
||||||
|
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
|
|
||||||
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
|
const local = &caller.local;
|
||||||
|
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, property, "__")) {
|
||||||
|
// some frameworks will extend built-in types using a __ prefix
|
||||||
|
// these should always be safe to ignore.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, property, "jQuery")) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JsApi == @import("../webapi/cdata/Text.zig").JsApi or JsApi == @import("../webapi/cdata/Comment.zig").JsApi) {
|
||||||
|
if (std.mem.eql(u8, property, "tagName")) {
|
||||||
|
// knockout does this, a lot.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JsApi == @import("../webapi/element/Html.zig").JsApi or JsApi == @import("../webapi/Element.zig").JsApi or JsApi == @import("../webapi/element/html/Custom.zig").JsApi) {
|
||||||
|
// react ?
|
||||||
|
if (std.mem.eql(u8, property, "props")) return 0;
|
||||||
|
if (std.mem.eql(u8, property, "hydrated")) return 0;
|
||||||
|
if (std.mem.eql(u8, property, "isHydrated")) return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JsApi == @import("../webapi/Console.zig").JsApi) {
|
||||||
|
if (std.mem.eql(u8, property, "firebug")) return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ignored = std.StaticStringMap(void).initComptime(.{});
|
||||||
|
if (!ignored.has(property)) {
|
||||||
|
const key = std.fmt.bufPrint(&local.ctx.page.buf, "{s}:{s}", .{ if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi), property }) catch return 0;
|
||||||
|
logUnknownProperty(local, key) catch return 0;
|
||||||
|
}
|
||||||
|
// not intercepted
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}.wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logUnknownProperty(local: *const js.Local, key: []const u8) !void {
|
||||||
|
const ctx = local.ctx;
|
||||||
|
const gop = try ctx.unknown_properties.getOrPut(ctx.arena, key);
|
||||||
|
if (gop.found_existing) {
|
||||||
|
gop.value_ptr.count += 1;
|
||||||
|
} else {
|
||||||
|
gop.key_ptr.* = try ctx.arena.dupe(u8, key);
|
||||||
|
gop.value_ptr.* = .{
|
||||||
|
.count = 1,
|
||||||
|
.first_stack = try ctx.arena.dupe(u8, (try local.stackTrace()) orelse "???"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Given a Type, returns the length of the prototype chain, including self
|
// Given a Type, returns the length of the prototype chain, including self
|
||||||
fn prototypeChainLength(comptime T: type) usize {
|
fn prototypeChainLength(comptime T: type) usize {
|
||||||
var l: usize = 1;
|
var l: usize = 1;
|
||||||
@@ -496,6 +722,8 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/collections.zig"),
|
@import("../webapi/collections.zig"),
|
||||||
@import("../webapi/Console.zig"),
|
@import("../webapi/Console.zig"),
|
||||||
@import("../webapi/Crypto.zig"),
|
@import("../webapi/Crypto.zig"),
|
||||||
|
@import("../webapi/Permissions.zig"),
|
||||||
|
@import("../webapi/StorageManager.zig"),
|
||||||
@import("../webapi/CSS.zig"),
|
@import("../webapi/CSS.zig"),
|
||||||
@import("../webapi/css/CSSRule.zig"),
|
@import("../webapi/css/CSSRule.zig"),
|
||||||
@import("../webapi/css/CSSRuleList.zig"),
|
@import("../webapi/css/CSSRuleList.zig"),
|
||||||
@@ -503,6 +731,8 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/css/CSSStyleRule.zig"),
|
@import("../webapi/css/CSSStyleRule.zig"),
|
||||||
@import("../webapi/css/CSSStyleSheet.zig"),
|
@import("../webapi/css/CSSStyleSheet.zig"),
|
||||||
@import("../webapi/css/CSSStyleProperties.zig"),
|
@import("../webapi/css/CSSStyleProperties.zig"),
|
||||||
|
@import("../webapi/css/FontFace.zig"),
|
||||||
|
@import("../webapi/css/FontFaceSet.zig"),
|
||||||
@import("../webapi/css/MediaQueryList.zig"),
|
@import("../webapi/css/MediaQueryList.zig"),
|
||||||
@import("../webapi/css/StyleSheetList.zig"),
|
@import("../webapi/css/StyleSheetList.zig"),
|
||||||
@import("../webapi/Document.zig"),
|
@import("../webapi/Document.zig"),
|
||||||
@@ -529,16 +759,24 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/element/Html.zig"),
|
@import("../webapi/element/Html.zig"),
|
||||||
@import("../webapi/element/html/IFrame.zig"),
|
@import("../webapi/element/html/IFrame.zig"),
|
||||||
@import("../webapi/element/html/Anchor.zig"),
|
@import("../webapi/element/html/Anchor.zig"),
|
||||||
|
@import("../webapi/element/html/Area.zig"),
|
||||||
@import("../webapi/element/html/Audio.zig"),
|
@import("../webapi/element/html/Audio.zig"),
|
||||||
|
@import("../webapi/element/html/Base.zig"),
|
||||||
@import("../webapi/element/html/Body.zig"),
|
@import("../webapi/element/html/Body.zig"),
|
||||||
@import("../webapi/element/html/BR.zig"),
|
@import("../webapi/element/html/BR.zig"),
|
||||||
@import("../webapi/element/html/Button.zig"),
|
@import("../webapi/element/html/Button.zig"),
|
||||||
@import("../webapi/element/html/Canvas.zig"),
|
@import("../webapi/element/html/Canvas.zig"),
|
||||||
@import("../webapi/element/html/Custom.zig"),
|
@import("../webapi/element/html/Custom.zig"),
|
||||||
@import("../webapi/element/html/Data.zig"),
|
@import("../webapi/element/html/Data.zig"),
|
||||||
|
@import("../webapi/element/html/DataList.zig"),
|
||||||
|
@import("../webapi/element/html/Details.zig"),
|
||||||
@import("../webapi/element/html/Dialog.zig"),
|
@import("../webapi/element/html/Dialog.zig"),
|
||||||
|
@import("../webapi/element/html/Directory.zig"),
|
||||||
|
@import("../webapi/element/html/DList.zig"),
|
||||||
@import("../webapi/element/html/Div.zig"),
|
@import("../webapi/element/html/Div.zig"),
|
||||||
@import("../webapi/element/html/Embed.zig"),
|
@import("../webapi/element/html/Embed.zig"),
|
||||||
|
@import("../webapi/element/html/FieldSet.zig"),
|
||||||
|
@import("../webapi/element/html/Font.zig"),
|
||||||
@import("../webapi/element/html/Form.zig"),
|
@import("../webapi/element/html/Form.zig"),
|
||||||
@import("../webapi/element/html/Generic.zig"),
|
@import("../webapi/element/html/Generic.zig"),
|
||||||
@import("../webapi/element/html/Head.zig"),
|
@import("../webapi/element/html/Head.zig"),
|
||||||
@@ -547,20 +785,43 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/element/html/Html.zig"),
|
@import("../webapi/element/html/Html.zig"),
|
||||||
@import("../webapi/element/html/Image.zig"),
|
@import("../webapi/element/html/Image.zig"),
|
||||||
@import("../webapi/element/html/Input.zig"),
|
@import("../webapi/element/html/Input.zig"),
|
||||||
|
@import("../webapi/element/html/Label.zig"),
|
||||||
|
@import("../webapi/element/html/Legend.zig"),
|
||||||
@import("../webapi/element/html/LI.zig"),
|
@import("../webapi/element/html/LI.zig"),
|
||||||
@import("../webapi/element/html/Link.zig"),
|
@import("../webapi/element/html/Link.zig"),
|
||||||
|
@import("../webapi/element/html/Map.zig"),
|
||||||
@import("../webapi/element/html/Media.zig"),
|
@import("../webapi/element/html/Media.zig"),
|
||||||
@import("../webapi/element/html/Meta.zig"),
|
@import("../webapi/element/html/Meta.zig"),
|
||||||
|
@import("../webapi/element/html/Meter.zig"),
|
||||||
|
@import("../webapi/element/html/Mod.zig"),
|
||||||
|
@import("../webapi/element/html/Object.zig"),
|
||||||
@import("../webapi/element/html/OL.zig"),
|
@import("../webapi/element/html/OL.zig"),
|
||||||
|
@import("../webapi/element/html/OptGroup.zig"),
|
||||||
@import("../webapi/element/html/Option.zig"),
|
@import("../webapi/element/html/Option.zig"),
|
||||||
|
@import("../webapi/element/html/Output.zig"),
|
||||||
@import("../webapi/element/html/Paragraph.zig"),
|
@import("../webapi/element/html/Paragraph.zig"),
|
||||||
|
@import("../webapi/element/html/Picture.zig"),
|
||||||
|
@import("../webapi/element/html/Param.zig"),
|
||||||
|
@import("../webapi/element/html/Pre.zig"),
|
||||||
|
@import("../webapi/element/html/Progress.zig"),
|
||||||
|
@import("../webapi/element/html/Quote.zig"),
|
||||||
@import("../webapi/element/html/Script.zig"),
|
@import("../webapi/element/html/Script.zig"),
|
||||||
@import("../webapi/element/html/Select.zig"),
|
@import("../webapi/element/html/Select.zig"),
|
||||||
@import("../webapi/element/html/Slot.zig"),
|
@import("../webapi/element/html/Slot.zig"),
|
||||||
|
@import("../webapi/element/html/Source.zig"),
|
||||||
|
@import("../webapi/element/html/Span.zig"),
|
||||||
@import("../webapi/element/html/Style.zig"),
|
@import("../webapi/element/html/Style.zig"),
|
||||||
|
@import("../webapi/element/html/Table.zig"),
|
||||||
|
@import("../webapi/element/html/TableCaption.zig"),
|
||||||
|
@import("../webapi/element/html/TableCell.zig"),
|
||||||
|
@import("../webapi/element/html/TableCol.zig"),
|
||||||
|
@import("../webapi/element/html/TableRow.zig"),
|
||||||
|
@import("../webapi/element/html/TableSection.zig"),
|
||||||
@import("../webapi/element/html/Template.zig"),
|
@import("../webapi/element/html/Template.zig"),
|
||||||
@import("../webapi/element/html/TextArea.zig"),
|
@import("../webapi/element/html/TextArea.zig"),
|
||||||
|
@import("../webapi/element/html/Time.zig"),
|
||||||
@import("../webapi/element/html/Title.zig"),
|
@import("../webapi/element/html/Title.zig"),
|
||||||
|
@import("../webapi/element/html/Track.zig"),
|
||||||
@import("../webapi/element/html/Video.zig"),
|
@import("../webapi/element/html/Video.zig"),
|
||||||
@import("../webapi/element/html/UL.zig"),
|
@import("../webapi/element/html/UL.zig"),
|
||||||
@import("../webapi/element/html/Unknown.zig"),
|
@import("../webapi/element/html/Unknown.zig"),
|
||||||
@@ -568,6 +829,8 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/element/svg/Generic.zig"),
|
@import("../webapi/element/svg/Generic.zig"),
|
||||||
@import("../webapi/encoding/TextDecoder.zig"),
|
@import("../webapi/encoding/TextDecoder.zig"),
|
||||||
@import("../webapi/encoding/TextEncoder.zig"),
|
@import("../webapi/encoding/TextEncoder.zig"),
|
||||||
|
@import("../webapi/encoding/TextEncoderStream.zig"),
|
||||||
|
@import("../webapi/encoding/TextDecoderStream.zig"),
|
||||||
@import("../webapi/Event.zig"),
|
@import("../webapi/Event.zig"),
|
||||||
@import("../webapi/event/CompositionEvent.zig"),
|
@import("../webapi/event/CompositionEvent.zig"),
|
||||||
@import("../webapi/event/CustomEvent.zig"),
|
@import("../webapi/event/CustomEvent.zig"),
|
||||||
@@ -579,7 +842,15 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/event/PopStateEvent.zig"),
|
@import("../webapi/event/PopStateEvent.zig"),
|
||||||
@import("../webapi/event/UIEvent.zig"),
|
@import("../webapi/event/UIEvent.zig"),
|
||||||
@import("../webapi/event/MouseEvent.zig"),
|
@import("../webapi/event/MouseEvent.zig"),
|
||||||
|
@import("../webapi/event/PointerEvent.zig"),
|
||||||
@import("../webapi/event/KeyboardEvent.zig"),
|
@import("../webapi/event/KeyboardEvent.zig"),
|
||||||
|
@import("../webapi/event/FocusEvent.zig"),
|
||||||
|
@import("../webapi/event/WheelEvent.zig"),
|
||||||
|
@import("../webapi/event/TextEvent.zig"),
|
||||||
|
@import("../webapi/event/InputEvent.zig"),
|
||||||
|
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
||||||
|
@import("../webapi/event/SubmitEvent.zig"),
|
||||||
|
@import("../webapi/event/FormDataEvent.zig"),
|
||||||
@import("../webapi/MessageChannel.zig"),
|
@import("../webapi/MessageChannel.zig"),
|
||||||
@import("../webapi/MessagePort.zig"),
|
@import("../webapi/MessagePort.zig"),
|
||||||
@import("../webapi/media/MediaError.zig"),
|
@import("../webapi/media/MediaError.zig"),
|
||||||
@@ -599,11 +870,16 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/streams/ReadableStream.zig"),
|
@import("../webapi/streams/ReadableStream.zig"),
|
||||||
@import("../webapi/streams/ReadableStreamDefaultReader.zig"),
|
@import("../webapi/streams/ReadableStreamDefaultReader.zig"),
|
||||||
@import("../webapi/streams/ReadableStreamDefaultController.zig"),
|
@import("../webapi/streams/ReadableStreamDefaultController.zig"),
|
||||||
|
@import("../webapi/streams/WritableStream.zig"),
|
||||||
|
@import("../webapi/streams/WritableStreamDefaultWriter.zig"),
|
||||||
|
@import("../webapi/streams/WritableStreamDefaultController.zig"),
|
||||||
|
@import("../webapi/streams/TransformStream.zig"),
|
||||||
@import("../webapi/Node.zig"),
|
@import("../webapi/Node.zig"),
|
||||||
@import("../webapi/storage/storage.zig"),
|
@import("../webapi/storage/storage.zig"),
|
||||||
@import("../webapi/URL.zig"),
|
@import("../webapi/URL.zig"),
|
||||||
@import("../webapi/Window.zig"),
|
@import("../webapi/Window.zig"),
|
||||||
@import("../webapi/Performance.zig"),
|
@import("../webapi/Performance.zig"),
|
||||||
|
@import("../webapi/PluginArray.zig"),
|
||||||
@import("../webapi/MutationObserver.zig"),
|
@import("../webapi/MutationObserver.zig"),
|
||||||
@import("../webapi/IntersectionObserver.zig"),
|
@import("../webapi/IntersectionObserver.zig"),
|
||||||
@import("../webapi/CustomElementRegistry.zig"),
|
@import("../webapi/CustomElementRegistry.zig"),
|
||||||
@@ -611,10 +887,20 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/IdleDeadline.zig"),
|
@import("../webapi/IdleDeadline.zig"),
|
||||||
@import("../webapi/Blob.zig"),
|
@import("../webapi/Blob.zig"),
|
||||||
@import("../webapi/File.zig"),
|
@import("../webapi/File.zig"),
|
||||||
|
@import("../webapi/FileList.zig"),
|
||||||
|
@import("../webapi/FileReader.zig"),
|
||||||
@import("../webapi/Screen.zig"),
|
@import("../webapi/Screen.zig"),
|
||||||
|
@import("../webapi/VisualViewport.zig"),
|
||||||
@import("../webapi/PerformanceObserver.zig"),
|
@import("../webapi/PerformanceObserver.zig"),
|
||||||
@import("../webapi/navigation/Navigation.zig"),
|
@import("../webapi/navigation/Navigation.zig"),
|
||||||
@import("../webapi/navigation/NavigationEventTarget.zig"),
|
|
||||||
@import("../webapi/navigation/NavigationHistoryEntry.zig"),
|
@import("../webapi/navigation/NavigationHistoryEntry.zig"),
|
||||||
@import("../webapi/navigation/NavigationActivation.zig"),
|
@import("../webapi/navigation/NavigationActivation.zig"),
|
||||||
|
@import("../webapi/canvas/CanvasRenderingContext2D.zig"),
|
||||||
|
@import("../webapi/canvas/WebGLRenderingContext.zig"),
|
||||||
|
@import("../webapi/canvas/OffscreenCanvas.zig"),
|
||||||
|
@import("../webapi/canvas/OffscreenCanvasRenderingContext2D.zig"),
|
||||||
|
@import("../webapi/SubtleCrypto.zig"),
|
||||||
|
@import("../webapi/CryptoKey.zig"),
|
||||||
|
@import("../webapi/Selection.zig"),
|
||||||
|
@import("../webapi/ImageData.zig"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -17,25 +17,36 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
pub const v8 = @import("v8");
|
pub const v8 = @import("v8").c;
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
const string = @import("../../string.zig");
|
||||||
|
|
||||||
pub const Env = @import("Env.zig");
|
pub const Env = @import("Env.zig");
|
||||||
pub const bridge = @import("bridge.zig");
|
pub const bridge = @import("bridge.zig");
|
||||||
pub const ExecutionWorld = @import("ExecutionWorld.zig");
|
pub const Caller = @import("Caller.zig");
|
||||||
|
pub const Origin = @import("Origin.zig");
|
||||||
|
pub const Identity = @import("Identity.zig");
|
||||||
pub const Context = @import("Context.zig");
|
pub const Context = @import("Context.zig");
|
||||||
|
pub const Local = @import("Local.zig");
|
||||||
pub const Inspector = @import("Inspector.zig");
|
pub const Inspector = @import("Inspector.zig");
|
||||||
pub const Snapshot = @import("Snapshot.zig");
|
pub const Snapshot = @import("Snapshot.zig");
|
||||||
pub const Platform = @import("Platform.zig");
|
pub const Platform = @import("Platform.zig");
|
||||||
|
pub const Isolate = @import("Isolate.zig");
|
||||||
|
pub const HandleScope = @import("HandleScope.zig");
|
||||||
|
|
||||||
// TODO: Is "This" really necessary?
|
|
||||||
pub const This = @import("This.zig");
|
|
||||||
pub const Value = @import("Value.zig");
|
pub const Value = @import("Value.zig");
|
||||||
pub const Array = @import("Array.zig");
|
pub const Array = @import("Array.zig");
|
||||||
|
pub const String = @import("String.zig");
|
||||||
pub const Object = @import("Object.zig");
|
pub const Object = @import("Object.zig");
|
||||||
pub const TryCatch = @import("TryCatch.zig");
|
pub const TryCatch = @import("TryCatch.zig");
|
||||||
pub const Function = @import("Function.zig");
|
pub const Function = @import("Function.zig");
|
||||||
|
pub const Promise = @import("Promise.zig");
|
||||||
|
pub const Module = @import("Module.zig");
|
||||||
|
pub const BigInt = @import("BigInt.zig");
|
||||||
|
pub const Number = @import("Number.zig");
|
||||||
|
pub const Integer = @import("Integer.zig");
|
||||||
|
pub const PromiseResolver = @import("PromiseResolver.zig");
|
||||||
|
pub const PromiseRejection = @import("PromiseRejection.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
@@ -68,246 +79,144 @@ pub const ArrayBuffer = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PromiseResolver = struct {
|
pub const ArrayType = enum(u8) {
|
||||||
context: *Context,
|
int8,
|
||||||
resolver: v8.PromiseResolver,
|
uint8,
|
||||||
|
uint8_clamped,
|
||||||
pub fn promise(self: PromiseResolver) Promise {
|
int16,
|
||||||
return self.resolver.getPromise();
|
uint16,
|
||||||
}
|
int32,
|
||||||
|
uint32,
|
||||||
pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
float16,
|
||||||
self._resolve(value) catch |err| {
|
float32,
|
||||||
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = false });
|
float64,
|
||||||
};
|
|
||||||
}
|
|
||||||
fn _resolve(self: PromiseResolver, value: anytype) !void {
|
|
||||||
const context = self.context;
|
|
||||||
const js_value = try context.zigValueToJs(value, .{});
|
|
||||||
|
|
||||||
if (self.resolver.resolve(context.v8_context, js_value) == null) {
|
|
||||||
return error.FailedToResolvePromise;
|
|
||||||
}
|
|
||||||
self.context.runMicrotasks();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
|
||||||
self._reject(value) catch |err| {
|
|
||||||
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = false });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
fn _reject(self: PromiseResolver, value: anytype) !void {
|
|
||||||
const context = self.context;
|
|
||||||
const js_value = try context.zigValueToJs(value);
|
|
||||||
|
|
||||||
if (self.resolver.reject(context.v8_context, js_value) == null) {
|
|
||||||
return error.FailedToRejectPromise;
|
|
||||||
}
|
|
||||||
self.context.runMicrotasks();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PersistentPromiseResolver = struct {
|
pub fn ArrayBufferRef(comptime kind: ArrayType) type {
|
||||||
context: *Context,
|
return struct {
|
||||||
resolver: v8.Persistent(v8.PromiseResolver),
|
const Self = @This();
|
||||||
|
|
||||||
pub fn deinit(self: *PersistentPromiseResolver) void {
|
const BackingInt = switch (kind) {
|
||||||
self.resolver.deinit();
|
.int8 => i8,
|
||||||
}
|
.uint8, .uint8_clamped => u8,
|
||||||
|
.int16 => i16,
|
||||||
pub fn promise(self: PersistentPromiseResolver) Promise {
|
.uint16 => u16,
|
||||||
return self.resolver.castToPromiseResolver().getPromise();
|
.int32 => i32,
|
||||||
}
|
.uint32 => u32,
|
||||||
|
.float16 => f16,
|
||||||
pub fn resolve(self: PersistentPromiseResolver, comptime source: []const u8, value: anytype) void {
|
.float32 => f32,
|
||||||
self._resolve(value) catch |err| {
|
.float64 => f64,
|
||||||
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = true });
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
fn _resolve(self: PersistentPromiseResolver, value: anytype) !void {
|
|
||||||
const context = self.context;
|
|
||||||
const js_value = try context.zigValueToJs(value, .{});
|
|
||||||
defer context.runMicrotasks();
|
|
||||||
|
|
||||||
if (self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) == null) {
|
local: *const Local,
|
||||||
return error.FailedToResolvePromise;
|
handle: *const v8.Value,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reject(self: PersistentPromiseResolver, comptime source: []const u8, value: anytype) void {
|
/// Persisted typed array.
|
||||||
self._reject(value) catch |err| {
|
pub const Global = struct {
|
||||||
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = true });
|
handle: v8.Global,
|
||||||
|
|
||||||
|
pub fn deinit(self: *Global) void {
|
||||||
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local(self: *const Global, l: *const Local) Self {
|
||||||
|
return .{ .local = l, .handle = v8.v8__Global__Get(&self.handle, l.isolate.handle).? };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
fn _reject(self: PersistentPromiseResolver, value: anytype) !void {
|
pub fn init(local: *const Local, size: usize) Self {
|
||||||
const context = self.context;
|
const ctx = local.ctx;
|
||||||
const js_value = try context.zigValueToJs(value, .{});
|
const isolate = ctx.isolate;
|
||||||
defer context.runMicrotasks();
|
const bits = switch (@typeInfo(BackingInt)) {
|
||||||
|
.int => |n| n.bits,
|
||||||
|
.float => |f| f.bits,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
|
||||||
// resolver.reject will return null if the promise isn't pending
|
var array_buffer: *const v8.ArrayBuffer = undefined;
|
||||||
if (self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) == null) {
|
if (size == 0) {
|
||||||
return error.FailedToRejectPromise;
|
array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;
|
||||||
|
} else {
|
||||||
|
const buffer_len = size * bits / 8;
|
||||||
|
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, buffer_len).?;
|
||||||
|
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
|
||||||
|
array_buffer = v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle: *const v8.Value = switch (comptime kind) {
|
||||||
|
.int8 => @ptrCast(v8.v8__Int8Array__New(array_buffer, 0, size).?),
|
||||||
|
.uint8 => @ptrCast(v8.v8__Uint8Array__New(array_buffer, 0, size).?),
|
||||||
|
.uint8_clamped => @ptrCast(v8.v8__Uint8ClampedArray__New(array_buffer, 0, size).?),
|
||||||
|
.int16 => @ptrCast(v8.v8__Int16Array__New(array_buffer, 0, size).?),
|
||||||
|
.uint16 => @ptrCast(v8.v8__Uint16Array__New(array_buffer, 0, size).?),
|
||||||
|
.int32 => @ptrCast(v8.v8__Int32Array__New(array_buffer, 0, size).?),
|
||||||
|
.uint32 => @ptrCast(v8.v8__Uint32Array__New(array_buffer, 0, size).?),
|
||||||
|
.float16 => @ptrCast(v8.v8__Float16Array__New(array_buffer, 0, size).?),
|
||||||
|
.float32 => @ptrCast(v8.v8__Float32Array__New(array_buffer, 0, size).?),
|
||||||
|
.float64 => @ptrCast(v8.v8__Float64Array__New(array_buffer, 0, size).?),
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{ .local = local, .handle = handle };
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Promise = v8.Promise;
|
pub fn persist(self: *const Self) !Global {
|
||||||
|
var ctx = self.local.ctx;
|
||||||
|
var global: v8.Global = undefined;
|
||||||
|
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||||
|
try ctx.trackGlobal(global);
|
||||||
|
|
||||||
// When doing jsValueToZig, string ([]const u8) are managed by the
|
return .{ .handle = global };
|
||||||
// call_arena. That means that if the API wants to persist the string
|
}
|
||||||
// (which is relatively common), it needs to dupe it again.
|
};
|
||||||
// If the parameter is an Env.String rather than a []const u8, then
|
}
|
||||||
// the page's arena will be used (rather than the call arena).
|
|
||||||
pub const String = struct {
|
// If a WebAPI takes a []const u8, then we'll coerce any JS value to that string
|
||||||
string: []const u8,
|
// so null -> "null". But if a WebAPI takes an optional string, ?[]const u8,
|
||||||
|
// how should we handle null? If the parameter _isn't_ passed, then it's obvious
|
||||||
|
// that it should be null, but what if `null` is passed? It's ambiguous, should
|
||||||
|
// that be null, or "null"? It could depend on the api. So, `null` passed to
|
||||||
|
// ?[]const u8 will be `null`. If you want it to be "null", use a `.js.NullableString`.
|
||||||
|
pub const NullableString = struct {
|
||||||
|
value: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Exception = struct {
|
pub const Exception = struct {
|
||||||
inner: v8.Value,
|
local: *const Local,
|
||||||
context: *const Context,
|
handle: *const v8.Value,
|
||||||
|
|
||||||
// the caller needs to deinit the string returned
|
|
||||||
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
|
|
||||||
return self.context.valueToString(self.inner, .{ .allocator = allocator });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn UndefinedOr(comptime T: type) type {
|
|
||||||
return union(enum) {
|
|
||||||
undefined: void,
|
|
||||||
value: T,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// An interface for types that want to have their jsScopeEnd function be
|
|
||||||
// called when the call context ends
|
|
||||||
const CallScopeEndCallback = struct {
|
|
||||||
ptr: *anyopaque,
|
|
||||||
callScopeEndFn: *const fn (ptr: *anyopaque) void,
|
|
||||||
|
|
||||||
fn init(ptr: anytype) CallScopeEndCallback {
|
|
||||||
const T = @TypeOf(ptr);
|
|
||||||
const ptr_info = @typeInfo(T);
|
|
||||||
|
|
||||||
const gen = struct {
|
|
||||||
pub fn callScopeEnd(pointer: *anyopaque) void {
|
|
||||||
const self: T = @ptrCast(@alignCast(pointer));
|
|
||||||
return ptr_info.pointer.child.jsCallScopeEnd(self);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.ptr = ptr,
|
|
||||||
.callScopeEndFn = gen.callScopeEnd,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn callScopeEnd(self: CallScopeEndCallback) void {
|
|
||||||
self.callScopeEndFn(self.ptr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Callback called on global's property missing.
|
|
||||||
// Return true to intercept the execution or false to let the call
|
|
||||||
// continue the chain.
|
|
||||||
pub const GlobalMissingCallback = struct {
|
|
||||||
ptr: *anyopaque,
|
|
||||||
missingFn: *const fn (ptr: *anyopaque, name: []const u8, ctx: *Context) bool,
|
|
||||||
|
|
||||||
pub fn init(ptr: anytype) GlobalMissingCallback {
|
|
||||||
const T = @TypeOf(ptr);
|
|
||||||
const ptr_info = @typeInfo(T);
|
|
||||||
|
|
||||||
const gen = struct {
|
|
||||||
pub fn missing(pointer: *anyopaque, name: []const u8, ctx: *Context) bool {
|
|
||||||
const self: T = @ptrCast(@alignCast(pointer));
|
|
||||||
return ptr_info.pointer.child.missing(self, name, ctx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.ptr = ptr,
|
|
||||||
.missingFn = gen.missing,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn missing(self: GlobalMissingCallback, name: []const u8, ctx: *Context) bool {
|
|
||||||
return self.missingFn(self.ptr, name, ctx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attributes that return a primitive type are setup directly on the
|
|
||||||
// FunctionTemplate when the Env is setup. More complex types need a v8.Context
|
|
||||||
// and cannot be set directly on the FunctionTemplate.
|
|
||||||
// We default to saying types are primitives because that's mostly what
|
|
||||||
// we have. If we add a new complex type that isn't explictly handled here,
|
|
||||||
// we'll get a compiler error in simpleZigValueToJs, and can then explicitly
|
|
||||||
// add the type here.
|
|
||||||
pub fn isComplexAttributeType(ti: std.builtin.Type) bool {
|
|
||||||
return switch (ti) {
|
|
||||||
.array => true,
|
|
||||||
else => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// These are simple types that we can convert to JS with only an isolate. This
|
// These are simple types that we can convert to JS with only an isolate. This
|
||||||
// is separated from the Caller's zigValueToJs to make it available when we
|
// is separated from the Caller's zigValueToJs to make it available when we
|
||||||
// don't have a caller (i.e., when setting static attributes on types)
|
// don't have a caller (i.e., when setting static attributes on types)
|
||||||
pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool, comptime null_as_undefined: bool) if (fail) v8.Value else ?v8.Value {
|
pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool, comptime null_as_undefined: bool) if (fail) *const v8.Value else ?*const v8.Value {
|
||||||
switch (@typeInfo(@TypeOf(value))) {
|
switch (@typeInfo(@TypeOf(value))) {
|
||||||
.void => return v8.initUndefined(isolate).toValue(),
|
.void => return isolate.initUndefined(),
|
||||||
.null => if (comptime null_as_undefined) return v8.initUndefined(isolate).toValue() else return v8.initNull(isolate).toValue(),
|
.null => if (comptime null_as_undefined) return isolate.initUndefined() else return isolate.initNull(),
|
||||||
.bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)),
|
.bool => return if (value) isolate.initTrue() else isolate.initFalse(),
|
||||||
.int => |n| switch (n.signedness) {
|
.int => |n| {
|
||||||
.signed => {
|
if (comptime n.bits <= 32) {
|
||||||
if (value > 0 and value <= 4_294_967_295) {
|
return @ptrCast(isolate.initInteger(value).handle);
|
||||||
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
}
|
||||||
}
|
if (value >= 0 and value <= 4_294_967_295) {
|
||||||
if (value >= -2_147_483_648 and value <= 2_147_483_647) {
|
return @ptrCast(isolate.initInteger(@as(u32, @intCast(value))).handle);
|
||||||
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
|
}
|
||||||
}
|
return @ptrCast(isolate.initBigInt(value).handle);
|
||||||
if (comptime n.bits <= 64) {
|
|
||||||
return v8.getValue(v8.BigInt.initI64(isolate, @intCast(value)));
|
|
||||||
}
|
|
||||||
@compileError(@typeName(value) ++ " is not supported");
|
|
||||||
},
|
|
||||||
.unsigned => {
|
|
||||||
if (value <= 4_294_967_295) {
|
|
||||||
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
|
||||||
}
|
|
||||||
if (comptime n.bits <= 64) {
|
|
||||||
return v8.getValue(v8.BigInt.initU64(isolate, @intCast(value)));
|
|
||||||
}
|
|
||||||
@compileError(@typeName(value) ++ " is not supported");
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
.comptime_int => {
|
.comptime_int => {
|
||||||
if (value >= 0) {
|
if (value > -2_147_483_648 and value <= 4_294_967_295) {
|
||||||
if (value <= 4_294_967_295) {
|
return @ptrCast(isolate.initInteger(value).handle);
|
||||||
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
|
||||||
}
|
|
||||||
return v8.BigInt.initU64(isolate, @intCast(value)).toValue();
|
|
||||||
}
|
}
|
||||||
if (value >= -2_147_483_648) {
|
return @ptrCast(isolate.initBigInt(value).handle);
|
||||||
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
|
|
||||||
}
|
|
||||||
return v8.BigInt.initI64(isolate, @intCast(value)).toValue();
|
|
||||||
},
|
|
||||||
.comptime_float => return v8.Number.init(isolate, value).toValue(),
|
|
||||||
.float => |f| switch (f.bits) {
|
|
||||||
64 => return v8.Number.init(isolate, value).toValue(),
|
|
||||||
32 => return v8.Number.init(isolate, @floatCast(value)).toValue(),
|
|
||||||
else => @compileError(@typeName(value) ++ " is not supported"),
|
|
||||||
},
|
},
|
||||||
|
.float, .comptime_float => return @ptrCast(isolate.initNumber(value).handle),
|
||||||
.pointer => |ptr| {
|
.pointer => |ptr| {
|
||||||
if (ptr.size == .slice and ptr.child == u8) {
|
if (ptr.size == .slice and ptr.child == u8) {
|
||||||
return v8.String.initUtf8(isolate, value).toValue();
|
return @ptrCast(isolate.initStringHandle(value));
|
||||||
}
|
}
|
||||||
if (ptr.size == .one) {
|
if (ptr.size == .one) {
|
||||||
const one_info = @typeInfo(ptr.child);
|
const one_info = @typeInfo(ptr.child);
|
||||||
if (one_info == .array and one_info.array.child == u8) {
|
if (one_info == .array and one_info.array.child == u8) {
|
||||||
return v8.String.initUtf8(isolate, value).toValue();
|
return @ptrCast(isolate.initStringHandle(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -317,22 +226,23 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
|
|||||||
return simpleZigValueToJs(isolate, v, fail, null_as_undefined);
|
return simpleZigValueToJs(isolate, v, fail, null_as_undefined);
|
||||||
}
|
}
|
||||||
if (comptime null_as_undefined) {
|
if (comptime null_as_undefined) {
|
||||||
return v8.initUndefined(isolate).toValue();
|
return isolate.initUndefined();
|
||||||
}
|
}
|
||||||
return v8.initNull(isolate).toValue();
|
return isolate.initNull();
|
||||||
},
|
},
|
||||||
.@"struct" => {
|
.@"struct" => {
|
||||||
switch (@TypeOf(value)) {
|
switch (@TypeOf(value)) {
|
||||||
|
string.String => return isolate.initStringHandle(value.str()),
|
||||||
ArrayBuffer => {
|
ArrayBuffer => {
|
||||||
const values = value.values;
|
const values = value.values;
|
||||||
const len = values.len;
|
const len = values.len;
|
||||||
var array_buffer: v8.ArrayBuffer = undefined;
|
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, len);
|
||||||
const backing_store = v8.BackingStore.init(isolate, len);
|
if (len > 0) {
|
||||||
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
|
const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
|
||||||
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
|
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
|
||||||
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
}
|
||||||
|
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
|
||||||
return .{ .handle = array_buffer.handle };
|
return @ptrCast(v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?);
|
||||||
},
|
},
|
||||||
// zig fmt: off
|
// zig fmt: off
|
||||||
TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64),
|
TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64),
|
||||||
@@ -349,37 +259,38 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
|
|||||||
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
|
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
|
||||||
};
|
};
|
||||||
|
|
||||||
var array_buffer: v8.ArrayBuffer = undefined;
|
var array_buffer: *const v8.ArrayBuffer = undefined;
|
||||||
if (len == 0) {
|
if (len == 0) {
|
||||||
array_buffer = v8.ArrayBuffer.init(isolate, 0);
|
array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;
|
||||||
} else {
|
} else {
|
||||||
const buffer_len = len * bits / 8;
|
const buffer_len = len * bits / 8;
|
||||||
const backing_store = v8.BackingStore.init(isolate, buffer_len);
|
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, buffer_len).?;
|
||||||
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
|
const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
|
||||||
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
|
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
|
||||||
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
|
||||||
|
array_buffer = v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (@typeInfo(value_type)) {
|
switch (@typeInfo(value_type)) {
|
||||||
.int => |n| switch (n.signedness) {
|
.int => |n| switch (n.signedness) {
|
||||||
.unsigned => switch (n.bits) {
|
.unsigned => switch (n.bits) {
|
||||||
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
|
8 => return @ptrCast(v8.v8__Uint8Array__New(array_buffer, 0, len).?),
|
||||||
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
|
16 => return @ptrCast(v8.v8__Uint16Array__New(array_buffer, 0, len).?),
|
||||||
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
|
32 => return @ptrCast(v8.v8__Uint32Array__New(array_buffer, 0, len).?),
|
||||||
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
|
64 => return @ptrCast(v8.v8__BigUint64Array__New(array_buffer, 0, len).?),
|
||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
.signed => switch (n.bits) {
|
.signed => switch (n.bits) {
|
||||||
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
|
8 => return @ptrCast(v8.v8__Int8Array__New(array_buffer, 0, len).?),
|
||||||
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
|
16 => return @ptrCast(v8.v8__Int16Array__New(array_buffer, 0, len).?),
|
||||||
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
|
32 => return @ptrCast(v8.v8__Int32Array__New(array_buffer, 0, len).?),
|
||||||
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
|
64 => return @ptrCast(v8.v8__BigInt64Array__New(array_buffer, 0, len).?),
|
||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
.float => |f| switch (f.bits) {
|
.float => |f| switch (f.bits) {
|
||||||
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
|
32 => return @ptrCast(v8.v8__Float32Array__New(array_buffer, 0, len).?),
|
||||||
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
|
64 => return @ptrCast(v8.v8__Float64Array__New(array_buffer, 0, len).?),
|
||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
@@ -388,6 +299,7 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
|
|||||||
// but this can never be valid.
|
// but this can never be valid.
|
||||||
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
|
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
|
||||||
},
|
},
|
||||||
|
inline String, BigInt, Integer, Number, Value, Object => return value.handle,
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -406,76 +318,6 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _createException(isolate: v8.Isolate, msg: []const u8) v8.Value {
|
|
||||||
return v8.Exception.initError(v8.String.initUtf8(isolate, msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn classNameForStruct(comptime Struct: type) []const u8 {
|
|
||||||
if (@hasDecl(Struct, "js_name")) {
|
|
||||||
return Struct.js_name;
|
|
||||||
}
|
|
||||||
@setEvalBranchQuota(10_000);
|
|
||||||
const full_name = @typeName(Struct);
|
|
||||||
const last = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse return full_name;
|
|
||||||
return full_name[last + 1 ..];
|
|
||||||
}
|
|
||||||
|
|
||||||
// When we return a Zig object to V8, we put it on the heap and pass it into
|
|
||||||
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
|
|
||||||
// function parameter, we know what type it _should_ be.
|
|
||||||
//
|
|
||||||
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
|
|
||||||
// to the parameter type:
|
|
||||||
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
|
|
||||||
//
|
|
||||||
// But there are 2 reasons we can't do that.
|
|
||||||
//
|
|
||||||
// == Reason 1 ==
|
|
||||||
// The JS code might pass the wrong type:
|
|
||||||
//
|
|
||||||
// var cat = new Cat();
|
|
||||||
// cat.setOwner(new Cat());
|
|
||||||
//
|
|
||||||
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
|
|
||||||
// the JS code passed a *Cat.
|
|
||||||
//
|
|
||||||
// To solve this issue, we tag every returned value so that we can check what
|
|
||||||
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
|
|
||||||
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
|
|
||||||
//
|
|
||||||
// == Reason 2 ==
|
|
||||||
// Because of prototype inheritance, even "correct" code can be a challenge. For
|
|
||||||
// example, say the above JavaScript is fixed:
|
|
||||||
//
|
|
||||||
// var cat = new Cat();
|
|
||||||
// cat.setOwner(new Owner("Leto"));
|
|
||||||
//
|
|
||||||
// The issue is that setOwner might not expect an *Owner, but rather a
|
|
||||||
// *Person, which is the prototype for Owner. Now our Zig code is expecting
|
|
||||||
// a *Person, but it was (correctly) given an *Owner.
|
|
||||||
// For this reason, we also store the prototype chain.
|
|
||||||
pub const TaggedAnyOpaque = struct {
|
|
||||||
prototype_len: u16,
|
|
||||||
prototype_chain: [*]const PrototypeChainEntry,
|
|
||||||
|
|
||||||
// Ptr to the Zig instance. Between the context where it's called (i.e.
|
|
||||||
// we have the comptime parameter info for all functions), and the index field
|
|
||||||
// we can figure out what type this is.
|
|
||||||
value: *anyopaque,
|
|
||||||
|
|
||||||
// When we're asked to describe an object via the Inspector, we _must_ include
|
|
||||||
// the proper subtype (and description) fields in the returned JSON.
|
|
||||||
// V8 will give us a Value and ask us for the subtype. From the v8.Value we
|
|
||||||
// can get a v8.Object, and from the v8.Object, we can get out TaggedAnyOpaque
|
|
||||||
// which is where we store the subtype.
|
|
||||||
subtype: ?bridge.SubType,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const PrototypeChainEntry = struct {
|
|
||||||
index: bridge.JsApiLookup.BackingInt,
|
|
||||||
offset: u16, // offset to the _proto field
|
|
||||||
};
|
|
||||||
|
|
||||||
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
|
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
|
||||||
// included (e.g. in the wpt build).
|
// included (e.g. in the wpt build).
|
||||||
|
|
||||||
@@ -483,10 +325,10 @@ pub const PrototypeChainEntry = struct {
|
|||||||
// it'll call this function to gets its [optional] subtype - which, from V8's
|
// it'll call this function to gets its [optional] subtype - which, from V8's
|
||||||
// point of view, is an arbitrary string.
|
// point of view, is an arbitrary string.
|
||||||
pub export fn v8_inspector__Client__IMPL__valueSubtype(
|
pub export fn v8_inspector__Client__IMPL__valueSubtype(
|
||||||
_: *v8.c.InspectorClientImpl,
|
_: *v8.InspectorClientImpl,
|
||||||
c_value: *const v8.C_Value,
|
c_value: *const v8.Value,
|
||||||
) callconv(.c) [*c]const u8 {
|
) callconv(.c) [*c]const u8 {
|
||||||
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
|
const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;
|
||||||
return if (external_entry.subtype) |st| @tagName(st) else null;
|
return if (external_entry.subtype) |st| @tagName(st) else null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,19 +337,19 @@ pub export fn v8_inspector__Client__IMPL__valueSubtype(
|
|||||||
// present, even if it's empty. So if we have a subType for the value, we'll
|
// present, even if it's empty. So if we have a subType for the value, we'll
|
||||||
// put an empty description.
|
// put an empty description.
|
||||||
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
|
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
|
||||||
_: *v8.c.InspectorClientImpl,
|
_: *v8.InspectorClientImpl,
|
||||||
v8_context: *const v8.C_Context,
|
v8_context: *const v8.Context,
|
||||||
c_value: *const v8.C_Value,
|
c_value: *const v8.Value,
|
||||||
) callconv(.c) [*c]const u8 {
|
) callconv(.c) [*c]const u8 {
|
||||||
_ = v8_context;
|
_ = v8_context;
|
||||||
|
|
||||||
// We _must_ include a non-null description in order for the subtype value
|
// We _must_ include a non-null description in order for the subtype value
|
||||||
// to be included. Besides that, I don't know if the value has any meaning
|
// to be included. Besides that, I don't know if the value has any meaning
|
||||||
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
|
const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;
|
||||||
return if (external_entry.subtype == null) null else "";
|
return if (external_entry.subtype == null) null else "";
|
||||||
}
|
}
|
||||||
|
|
||||||
test "TaggedAnyOpaque" {
|
test "TaggedAnyOpaque" {
|
||||||
// If we grow this, fine, but it should be a conscious decision
|
// If we grow this, fine, but it should be a conscious decision
|
||||||
try std.testing.expectEqual(24, @sizeOf(TaggedAnyOpaque));
|
try std.testing.expectEqual(24, @sizeOf(@import("TaggedOpaque.zig")));
|
||||||
}
|
}
|
||||||
|
|||||||
54
src/browser/links.zig
Normal file
54
src/browser/links.zig
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const Selector = @import("webapi/selector/Selector.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
/// Collect all links (href attributes from anchor tags) under `root`.
|
||||||
|
/// Returns a slice of strings allocated with `arena`.
|
||||||
|
pub fn collectLinks(arena: Allocator, root: *Node, page: *Page) ![]const []const u8 {
|
||||||
|
var links: std.ArrayList([]const u8) = .empty;
|
||||||
|
|
||||||
|
if (Selector.querySelectorAll(root, "a[href]", page)) |list| {
|
||||||
|
defer list.deinit(page._session);
|
||||||
|
|
||||||
|
for (list._nodes) |node| {
|
||||||
|
if (node.is(Element.Html.Anchor)) |anchor| {
|
||||||
|
const href = anchor.getHref(page) catch |err| {
|
||||||
|
@import("../lightpanda.zig").log.err(.app, "resolve href failed", .{ .err = err });
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (href.len > 0) {
|
||||||
|
try links.append(arena, href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else |err| {
|
||||||
|
@import("../lightpanda.zig").log.err(.app, "query links failed", .{ .err = err });
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return links.items;
|
||||||
|
}
|
||||||
691
src/browser/markdown.zig
Normal file
691
src/browser/markdown.zig
Normal file
@@ -0,0 +1,691 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const URL = @import("URL.zig");
|
||||||
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
const isAllWhitespace = @import("../string.zig").isAllWhitespace;
|
||||||
|
|
||||||
|
pub const Opts = struct {
|
||||||
|
// Options for future customization (e.g., dialect)
|
||||||
|
};
|
||||||
|
|
||||||
|
const State = struct {
|
||||||
|
const ListType = enum { ordered, unordered };
|
||||||
|
const ListState = struct {
|
||||||
|
type: ListType,
|
||||||
|
index: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
list_depth: usize = 0,
|
||||||
|
list_stack: [32]ListState = undefined,
|
||||||
|
pre_node: ?*Node = null,
|
||||||
|
in_code: bool = false,
|
||||||
|
in_table: bool = false,
|
||||||
|
table_row_index: usize = 0,
|
||||||
|
table_col_count: usize = 0,
|
||||||
|
last_char_was_newline: bool = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn shouldAddSpacing(tag: Element.Tag) bool {
|
||||||
|
return switch (tag) {
|
||||||
|
.p, .h1, .h2, .h3, .h4, .h5, .h6, .blockquote, .pre, .table => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isLayoutBlock(tag: Element.Tag) bool {
|
||||||
|
return switch (tag) {
|
||||||
|
.main, .section, .article, .nav, .aside, .header, .footer, .div, .ul, .ol => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isStandaloneAnchor(el: *Element) bool {
|
||||||
|
const node = el.asNode();
|
||||||
|
const parent = node.parentNode() orelse return false;
|
||||||
|
const parent_el = parent.is(Element) orelse return false;
|
||||||
|
|
||||||
|
if (!isLayoutBlock(parent_el.getTag())) return false;
|
||||||
|
|
||||||
|
var prev = node.previousSibling();
|
||||||
|
while (prev) |p| : (prev = p.previousSibling()) {
|
||||||
|
if (isSignificantText(p)) return false;
|
||||||
|
if (p.is(Element)) |pe| {
|
||||||
|
if (isVisibleElement(pe)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = node.nextSibling();
|
||||||
|
while (next) |n| : (next = n.nextSibling()) {
|
||||||
|
if (isSignificantText(n)) return false;
|
||||||
|
if (n.is(Element)) |ne| {
|
||||||
|
if (isVisibleElement(ne)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isSignificantText(node: *Node) bool {
|
||||||
|
const text = node.is(Node.CData.Text) orelse return false;
|
||||||
|
return !isAllWhitespace(text.getWholeText());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isVisibleElement(el: *Element) bool {
|
||||||
|
const tag = el.getTag();
|
||||||
|
return !tag.isMetadata() and tag != .svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getAnchorLabel(el: *Element) ?[]const u8 {
|
||||||
|
return el.getAttributeSafe(comptime .wrap("aria-label")) orelse el.getAttributeSafe(comptime .wrap("title"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasBlockDescendant(root: *Node) bool {
|
||||||
|
var tw = TreeWalker.FullExcludeSelf.Elements.init(root, .{});
|
||||||
|
while (tw.next()) |el| {
|
||||||
|
if (el.getTag().isBlock()) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasVisibleContent(root: *Node) bool {
|
||||||
|
var tw = TreeWalker.FullExcludeSelf.init(root, .{});
|
||||||
|
while (tw.next()) |node| {
|
||||||
|
if (isSignificantText(node)) return true;
|
||||||
|
if (node.is(Element)) |el| {
|
||||||
|
if (!isVisibleElement(el)) {
|
||||||
|
tw.skipChildren();
|
||||||
|
} else if (el.getTag() == .img) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Context = struct {
|
||||||
|
state: State,
|
||||||
|
writer: *std.Io.Writer,
|
||||||
|
page: *Page,
|
||||||
|
|
||||||
|
fn ensureNewline(self: *Context) !void {
|
||||||
|
if (!self.state.last_char_was_newline) {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(self: *Context, node: *Node) error{WriteFailed}!void {
|
||||||
|
switch (node._type) {
|
||||||
|
.document, .document_fragment => {
|
||||||
|
try self.renderChildren(node);
|
||||||
|
},
|
||||||
|
.element => |el| {
|
||||||
|
try self.renderElement(el);
|
||||||
|
},
|
||||||
|
.cdata => |cd| {
|
||||||
|
if (node.is(Node.CData.Text)) |_| {
|
||||||
|
var text = cd.getData().str();
|
||||||
|
if (self.state.pre_node) |pre| {
|
||||||
|
if (node.parentNode() == pre and node.nextSibling() == null) {
|
||||||
|
text = std.mem.trimRight(u8, text, " \t\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try self.renderText(text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderChildren(self: *Context, parent: *Node) !void {
|
||||||
|
var it = parent.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
try self.render(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderElement(self: *Context, el: *Element) !void {
|
||||||
|
const tag = el.getTag();
|
||||||
|
|
||||||
|
if (!isVisibleElement(el)) return;
|
||||||
|
|
||||||
|
// --- Opening Tag Logic ---
|
||||||
|
|
||||||
|
// Ensure block elements start on a new line (double newline for paragraphs etc)
|
||||||
|
if (tag.isBlock() and !self.state.in_table) {
|
||||||
|
try self.ensureNewline();
|
||||||
|
if (shouldAddSpacing(tag)) {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
} else if (tag == .li or tag == .tr) {
|
||||||
|
try self.ensureNewline();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefixes
|
||||||
|
switch (tag) {
|
||||||
|
.h1 => try self.writer.writeAll("# "),
|
||||||
|
.h2 => try self.writer.writeAll("## "),
|
||||||
|
.h3 => try self.writer.writeAll("### "),
|
||||||
|
.h4 => try self.writer.writeAll("#### "),
|
||||||
|
.h5 => try self.writer.writeAll("##### "),
|
||||||
|
.h6 => try self.writer.writeAll("###### "),
|
||||||
|
.ul => {
|
||||||
|
if (self.state.list_depth < self.state.list_stack.len) {
|
||||||
|
self.state.list_stack[self.state.list_depth] = .{ .type = .unordered, .index = 0 };
|
||||||
|
self.state.list_depth += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.ol => {
|
||||||
|
if (self.state.list_depth < self.state.list_stack.len) {
|
||||||
|
self.state.list_stack[self.state.list_depth] = .{ .type = .ordered, .index = 1 };
|
||||||
|
self.state.list_depth += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.li => {
|
||||||
|
const indent = if (self.state.list_depth > 0) self.state.list_depth - 1 else 0;
|
||||||
|
for (0..indent) |_| try self.writer.writeAll(" ");
|
||||||
|
|
||||||
|
if (self.state.list_depth > 0 and self.state.list_stack[self.state.list_depth - 1].type == .ordered) {
|
||||||
|
const current_list = &self.state.list_stack[self.state.list_depth - 1];
|
||||||
|
try self.writer.print("{d}. ", .{current_list.index});
|
||||||
|
current_list.index += 1;
|
||||||
|
} else {
|
||||||
|
try self.writer.writeAll("- ");
|
||||||
|
}
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.table => {
|
||||||
|
self.state.in_table = true;
|
||||||
|
self.state.table_row_index = 0;
|
||||||
|
self.state.table_col_count = 0;
|
||||||
|
},
|
||||||
|
.tr => {
|
||||||
|
self.state.table_col_count = 0;
|
||||||
|
try self.writer.writeByte('|');
|
||||||
|
},
|
||||||
|
.td, .th => {
|
||||||
|
// Note: leading pipe handled by previous cell closing or tr opening
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
},
|
||||||
|
.blockquote => {
|
||||||
|
try self.writer.writeAll("> ");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.pre => {
|
||||||
|
try self.writer.writeAll("```\n");
|
||||||
|
self.state.pre_node = el.asNode();
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
},
|
||||||
|
.code => {
|
||||||
|
if (self.state.pre_node == null) {
|
||||||
|
try self.writer.writeByte('`');
|
||||||
|
self.state.in_code = true;
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.b, .strong => {
|
||||||
|
try self.writer.writeAll("**");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.i, .em => {
|
||||||
|
try self.writer.writeAll("*");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.s, .del => {
|
||||||
|
try self.writer.writeAll("~~");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.hr => {
|
||||||
|
try self.writer.writeAll("---\n");
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.br => {
|
||||||
|
if (self.state.in_table) {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
} else {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.img => {
|
||||||
|
try self.writer.writeAll(";
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||||
|
const absolute_src = URL.resolve(self.page.call_arena, self.page.base(), src, .{ .encode = true }) catch src;
|
||||||
|
try self.writer.writeAll(absolute_src);
|
||||||
|
}
|
||||||
|
try self.writer.writeAll(")");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.anchor => {
|
||||||
|
const has_content = hasVisibleContent(el.asNode());
|
||||||
|
const label = getAnchorLabel(el);
|
||||||
|
const href_raw = el.getAttributeSafe(comptime .wrap("href"));
|
||||||
|
|
||||||
|
if (!has_content and label == null and href_raw == null) return;
|
||||||
|
|
||||||
|
const has_block = hasBlockDescendant(el.asNode());
|
||||||
|
const href = if (href_raw) |h| URL.resolve(self.page.call_arena, self.page.base(), h, .{ .encode = true }) catch h else null;
|
||||||
|
|
||||||
|
if (has_block) {
|
||||||
|
try self.renderChildren(el.asNode());
|
||||||
|
if (href) |h| {
|
||||||
|
if (!self.state.last_char_was_newline) try self.writer.writeByte('\n');
|
||||||
|
try self.writer.writeAll("([](");
|
||||||
|
try self.writer.writeAll(h);
|
||||||
|
try self.writer.writeAll("))\n");
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStandaloneAnchor(el)) {
|
||||||
|
if (!self.state.last_char_was_newline) try self.writer.writeByte('\n');
|
||||||
|
try self.writer.writeByte('[');
|
||||||
|
if (has_content) {
|
||||||
|
try self.renderChildren(el.asNode());
|
||||||
|
} else {
|
||||||
|
try self.writer.writeAll(label orelse "");
|
||||||
|
}
|
||||||
|
try self.writer.writeAll("](");
|
||||||
|
if (href) |h| {
|
||||||
|
try self.writer.writeAll(h);
|
||||||
|
}
|
||||||
|
try self.writer.writeAll(")\n");
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.writer.writeByte('[');
|
||||||
|
if (has_content) {
|
||||||
|
try self.renderChildren(el.asNode());
|
||||||
|
} else {
|
||||||
|
try self.writer.writeAll(label orelse "");
|
||||||
|
}
|
||||||
|
try self.writer.writeAll("](");
|
||||||
|
if (href) |h| {
|
||||||
|
try self.writer.writeAll(h);
|
||||||
|
}
|
||||||
|
try self.writer.writeByte(')');
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.input => {
|
||||||
|
const type_attr = el.getAttributeSafe(comptime .wrap("type")) orelse return;
|
||||||
|
if (std.ascii.eqlIgnoreCase(type_attr, "checkbox")) {
|
||||||
|
const checked = el.getAttributeSafe(comptime .wrap("checked")) != null;
|
||||||
|
try self.writer.writeAll(if (checked) "[x] " else "[ ] ");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Render Children ---
|
||||||
|
try self.renderChildren(el.asNode());
|
||||||
|
|
||||||
|
// --- Closing Tag Logic ---
|
||||||
|
|
||||||
|
// Suffixes
|
||||||
|
switch (tag) {
|
||||||
|
.pre => {
|
||||||
|
if (!self.state.last_char_was_newline) {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
try self.writer.writeAll("```\n");
|
||||||
|
self.state.pre_node = null;
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
},
|
||||||
|
.code => {
|
||||||
|
if (self.state.pre_node == null) {
|
||||||
|
try self.writer.writeByte('`');
|
||||||
|
self.state.in_code = false;
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.b, .strong => {
|
||||||
|
try self.writer.writeAll("**");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.i, .em => {
|
||||||
|
try self.writer.writeAll("*");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.s, .del => {
|
||||||
|
try self.writer.writeAll("~~");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.blockquote => {},
|
||||||
|
.ul, .ol => {
|
||||||
|
if (self.state.list_depth > 0) self.state.list_depth -= 1;
|
||||||
|
},
|
||||||
|
.table => {
|
||||||
|
self.state.in_table = false;
|
||||||
|
},
|
||||||
|
.tr => {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
if (self.state.table_row_index == 0) {
|
||||||
|
try self.writer.writeByte('|');
|
||||||
|
for (0..self.state.table_col_count) |_| {
|
||||||
|
try self.writer.writeAll("---|");
|
||||||
|
}
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
self.state.table_row_index += 1;
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
},
|
||||||
|
.td, .th => {
|
||||||
|
try self.writer.writeAll(" |");
|
||||||
|
self.state.table_col_count += 1;
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-block newlines
|
||||||
|
if (tag.isBlock() and !self.state.in_table) {
|
||||||
|
try self.ensureNewline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderText(self: *Context, text: []const u8) !void {
|
||||||
|
if (text.len == 0) return;
|
||||||
|
|
||||||
|
if (self.state.pre_node) |_| {
|
||||||
|
try self.writer.writeAll(text);
|
||||||
|
self.state.last_char_was_newline = text[text.len - 1] == '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for pure whitespace
|
||||||
|
if (isAllWhitespace(text)) {
|
||||||
|
if (!self.state.last_char_was_newline) {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse whitespace
|
||||||
|
var it = std.mem.tokenizeAny(u8, text, " \t\n\r");
|
||||||
|
var first = true;
|
||||||
|
while (it.next()) |word| {
|
||||||
|
if (!first or (!self.state.last_char_was_newline and std.ascii.isWhitespace(text[0]))) {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.escape(word);
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle trailing whitespace from the original text
|
||||||
|
if (!first and !self.state.last_char_was_newline and std.ascii.isWhitespace(text[text.len - 1])) {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape(self: *Context, text: []const u8) !void {
|
||||||
|
for (text) |c| {
|
||||||
|
switch (c) {
|
||||||
|
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '!', '|' => {
|
||||||
|
try self.writer.writeByte('\\');
|
||||||
|
try self.writer.writeByte(c);
|
||||||
|
},
|
||||||
|
else => try self.writer.writeByte(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
|
_ = opts;
|
||||||
|
var ctx: Context = .{
|
||||||
|
.state = .{},
|
||||||
|
.writer = writer,
|
||||||
|
.page = page,
|
||||||
|
};
|
||||||
|
try ctx.render(node);
|
||||||
|
if (!ctx.state.last_char_was_newline) {
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn testMarkdownHTML(html: []const u8, expected: []const u8) !void {
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
const page = try testing.test_session.createPage();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
page.url = "http://localhost/";
|
||||||
|
|
||||||
|
const doc = page.window._document;
|
||||||
|
|
||||||
|
const div = try doc.createElement("div", null, page);
|
||||||
|
try page.parseHtmlAsChildren(div.asNode(), html);
|
||||||
|
|
||||||
|
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||||
|
defer aw.deinit();
|
||||||
|
try dump(div.asNode(), .{}, &aw.writer, page);
|
||||||
|
|
||||||
|
try testing.expectString(expected, aw.written());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: basic" {
|
||||||
|
try testMarkdownHTML("Hello world", "Hello world\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: whitespace" {
|
||||||
|
try testMarkdownHTML("<span>A</span> <span>B</span>", "A B\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: escaping" {
|
||||||
|
try testMarkdownHTML("<p># Not a header</p>", "\n\\# Not a header\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: strikethrough" {
|
||||||
|
try testMarkdownHTML("<s>deleted</s>", "~~deleted~~\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: task list" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<input type="checkbox" checked><input type="checkbox">
|
||||||
|
, "[x] [ ] \n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: ordered list" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<ol><li>First</li><li>Second</li></ol>
|
||||||
|
, "1. First\n2. Second\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: table" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<table><thead><tr><th>Head 1</th><th>Head 2</th></tr></thead>
|
||||||
|
\\<tbody><tr><td>Cell 1</td><td>Cell 2</td></tr></tbody></table>
|
||||||
|
,
|
||||||
|
\\
|
||||||
|
\\| Head 1 | Head 2 |
|
||||||
|
\\|---|---|
|
||||||
|
\\| Cell 1 | Cell 2 |
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: nested lists" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<ul><li>Parent<ul><li>Child</li></ul></li></ul>
|
||||||
|
,
|
||||||
|
\\- Parent
|
||||||
|
\\ - Child
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: blockquote" {
|
||||||
|
try testMarkdownHTML("<blockquote>Hello world</blockquote>", "\n> Hello world\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: links" {
|
||||||
|
try testMarkdownHTML("<a href=\"/relative\">Link</a>", "[Link](http://localhost/relative)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: images" {
|
||||||
|
try testMarkdownHTML("<img src=\"logo.png\" alt=\"Logo\">", "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: headings" {
|
||||||
|
try testMarkdownHTML("<h1>Title</h1><h2>Subtitle</h2>",
|
||||||
|
\\
|
||||||
|
\\# Title
|
||||||
|
\\
|
||||||
|
\\## Subtitle
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: code" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<p>Use git push</p>
|
||||||
|
\\<pre><code>line 1
|
||||||
|
\\line 2</code></pre>
|
||||||
|
,
|
||||||
|
\\
|
||||||
|
\\Use git push
|
||||||
|
\\
|
||||||
|
\\```
|
||||||
|
\\line 1
|
||||||
|
\\line 2
|
||||||
|
\\```
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: block link" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<a href="https://example.com">
|
||||||
|
\\ <h3>Title</h3>
|
||||||
|
\\ <p>Description</p>
|
||||||
|
\\</a>
|
||||||
|
,
|
||||||
|
\\
|
||||||
|
\\### Title
|
||||||
|
\\
|
||||||
|
\\Description
|
||||||
|
\\([](https://example.com))
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: inline link" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<p>Visit <a href="https://example.com">Example</a>.</p>
|
||||||
|
,
|
||||||
|
\\
|
||||||
|
\\Visit [Example](https://example.com).
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: standalone anchors" {
|
||||||
|
// Inside main, with whitespace between anchors -> treated as blocks
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<main>
|
||||||
|
\\ <a href="1">Link 1</a>
|
||||||
|
\\ <a href="2">Link 2</a>
|
||||||
|
\\</main>
|
||||||
|
,
|
||||||
|
\\[Link 1](http://localhost/1)
|
||||||
|
\\[Link 2](http://localhost/2)
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: mixed anchors in main" {
|
||||||
|
// Anchors surrounded by text should remain inline
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<main>
|
||||||
|
\\ Welcome <a href="1">Link 1</a>.
|
||||||
|
\\</main>
|
||||||
|
,
|
||||||
|
\\Welcome [Link 1](http://localhost/1).
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: skip empty links" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<a href="/"></a>
|
||||||
|
\\<a href="/"><svg></svg></a>
|
||||||
|
,
|
||||||
|
\\[](http://localhost/)
|
||||||
|
\\[](http://localhost/)
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: resolve links" {
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
const page = try testing.test_session.createPage();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
page.url = "https://example.com/a/index.html";
|
||||||
|
|
||||||
|
const doc = page.window._document;
|
||||||
|
const div = try doc.createElement("div", null, page);
|
||||||
|
try page.parseHtmlAsChildren(div.asNode(),
|
||||||
|
\\<a href="b">Link</a>
|
||||||
|
\\<img src="../c.png" alt="Img">
|
||||||
|
\\<a href="/my page">Space</a>
|
||||||
|
);
|
||||||
|
|
||||||
|
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||||
|
defer aw.deinit();
|
||||||
|
try dump(div.asNode(), .{}, &aw.writer, page);
|
||||||
|
|
||||||
|
try testing.expectString(
|
||||||
|
\\[Link](https://example.com/a/b)
|
||||||
|
\\
|
||||||
|
\\[Space](https://example.com/my%20page)
|
||||||
|
\\
|
||||||
|
, aw.written());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "browser.markdown: anchor fallback label" {
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<a href="/discord" aria-label="Discord Server"><svg></svg></a>
|
||||||
|
, "[Discord Server](http://localhost/discord)\n");
|
||||||
|
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<a href="/search" title="Search Site"><svg></svg></a>
|
||||||
|
, "[Search Site](http://localhost/search)\n");
|
||||||
|
|
||||||
|
try testMarkdownHTML(
|
||||||
|
\\<a href="/no-label"><svg></svg></a>
|
||||||
|
, "[](http://localhost/no-label)\n");
|
||||||
|
}
|
||||||
@@ -17,12 +17,17 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const lp = @import("lightpanda");
|
||||||
const h5e = @import("html5ever.zig");
|
const h5e = @import("html5ever.zig");
|
||||||
|
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
const Node = @import("../webapi/Node.zig");
|
const Node = @import("../webapi/Node.zig");
|
||||||
const Element = @import("../webapi/Element.zig");
|
const Element = @import("../webapi/Element.zig");
|
||||||
|
|
||||||
|
pub const AttributeIterator = h5e.AttributeIterator;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
pub const ParsedNode = struct {
|
pub const ParsedNode = struct {
|
||||||
node: *Node,
|
node: *Node,
|
||||||
@@ -104,7 +109,7 @@ pub fn parseXML(self: *Parser, xml: []const u8) void {
|
|||||||
xml.len,
|
xml.len,
|
||||||
&self.container,
|
&self.container,
|
||||||
self,
|
self,
|
||||||
createElementCallback,
|
createXMLElementCallback,
|
||||||
getDataCallback,
|
getDataCallback,
|
||||||
appendCallback,
|
appendCallback,
|
||||||
parseErrorCallback,
|
parseErrorCallback,
|
||||||
@@ -162,7 +167,7 @@ pub const Streaming = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(self: *Streaming) !void {
|
pub fn start(self: *Streaming) !void {
|
||||||
std.debug.assert(self.handle == null);
|
lp.assert(self.handle == null, "Parser.start non-null handle", .{});
|
||||||
|
|
||||||
self.handle = h5e.html5ever_streaming_parser_create(
|
self.handle = h5e.html5ever_streaming_parser_create(
|
||||||
&self.parser.container,
|
&self.parser.container,
|
||||||
@@ -225,17 +230,26 @@ fn _popCallback(self: *Parser, node: *Node) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn createElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
|
fn createElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
|
||||||
|
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn createXMLElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
|
||||||
|
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _createElementCallbackWithDefaultnamespace(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) ?*anyopaque {
|
||||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||||
return self._createElementCallback(data, qname, attributes) catch |err| {
|
return self._createElementCallback(data, qname, attributes, default_namespace) catch |err| {
|
||||||
self.err = .{ .err = err, .source = .create_element };
|
self.err = .{ .err = err, .source = .create_element };
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) !*anyopaque {
|
fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) !*anyopaque {
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
const name = qname.local.slice();
|
const name = qname.local.slice();
|
||||||
const namespace = qname.ns.slice();
|
const namespace_string = qname.ns.slice();
|
||||||
const node = try page.createElement(namespace, name, attributes);
|
const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string);
|
||||||
|
const node = try page.createElementNS(namespace, name, attributes);
|
||||||
|
|
||||||
const pn = try self.arena.create(ParsedNode);
|
const pn = try self.arena.create(ParsedNode);
|
||||||
pn.* = .{
|
pn.* = .{
|
||||||
@@ -348,7 +362,7 @@ fn getDataCallback(ctx: *anyopaque) callconv(.c) *anyopaque {
|
|||||||
const pn: *ParsedNode = @ptrCast(@alignCast(ctx));
|
const pn: *ParsedNode = @ptrCast(@alignCast(ctx));
|
||||||
// For non-elements, data is null. But, we expect this to only ever
|
// For non-elements, data is null. But, we expect this to only ever
|
||||||
// be called for elements.
|
// be called for elements.
|
||||||
std.debug.assert(pn.data != null);
|
lp.assert(pn.data != null, "Parser.getDataCallback null data", .{});
|
||||||
return pn.data.?;
|
return pn.data.?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +377,17 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !
|
|||||||
switch (node_or_text.toUnion()) {
|
switch (node_or_text.toUnion()) {
|
||||||
.node => |cpn| {
|
.node => |cpn| {
|
||||||
const child = getNode(cpn);
|
const child = getNode(cpn);
|
||||||
|
if (child._parent) |previous_parent| {
|
||||||
|
// html5ever says this can't happen, but we might be screwing up
|
||||||
|
// the node on our side. We shouldn't be, but we're seeing this
|
||||||
|
// in the wild, and I'm not sure why. In debug, let's crash so
|
||||||
|
// we can try to figure it out. In release, let's disconnect
|
||||||
|
// the child first.
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });
|
||||||
|
}
|
||||||
try self.page.appendNew(parent, .{ .node = child });
|
try self.page.appendNew(parent, .{ .node = child });
|
||||||
},
|
},
|
||||||
.text => |txt| try self.page.appendNew(parent, .{ .text = txt }),
|
.text => |txt| try self.page.appendNew(parent, .{ .text = txt }),
|
||||||
@@ -399,7 +424,16 @@ fn appendBeforeSiblingCallback(ctx: *anyopaque, sibling_ref: *anyopaque, node_or
|
|||||||
fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e.NodeOrText) !void {
|
fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e.NodeOrText) !void {
|
||||||
const parent = sibling.parentNode() orelse return error.NoParent;
|
const parent = sibling.parentNode() orelse return error.NoParent;
|
||||||
const node: *Node = switch (node_or_text.toUnion()) {
|
const node: *Node = switch (node_or_text.toUnion()) {
|
||||||
.node => |cpn| getNode(cpn),
|
.node => |cpn| blk: {
|
||||||
|
const child = getNode(cpn);
|
||||||
|
if (child._parent) |previous_parent| {
|
||||||
|
// A custom element constructor may have inserted the node into the
|
||||||
|
// DOM before the parser officially places it (e.g. via foster
|
||||||
|
// parenting). Detach it first so insertNodeRelative's assertion holds.
|
||||||
|
self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });
|
||||||
|
}
|
||||||
|
break :blk child;
|
||||||
|
},
|
||||||
.text => |txt| try self.page.createTextNode(txt),
|
.text => |txt| try self.page.createTextNode(txt),
|
||||||
};
|
};
|
||||||
try self.page.insertNodeRelative(parent, node, .{ .before = sibling }, .{});
|
try self.page.insertNodeRelative(parent, node, .{ .before = sibling }, .{});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -16,8 +16,6 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
// Gets the Parent of child.
|
// Gets the Parent of child.
|
||||||
// HtmlElement.of(script) -> *HTMLElement
|
// HtmlElement.of(script) -> *HTMLElement
|
||||||
pub fn Struct(comptime T: type) type {
|
pub fn Struct(comptime T: type) type {
|
||||||
@@ -28,37 +26,3 @@ pub fn Struct(comptime T: type) type {
|
|||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates an enum of N enums. Doesn't perserve their underlying integer
|
|
||||||
pub fn mergeEnums(comptime enums: []const type) type {
|
|
||||||
const field_count = blk: {
|
|
||||||
var count: usize = 0;
|
|
||||||
inline for (enums) |e| {
|
|
||||||
count += @typeInfo(e).@"enum".fields.len;
|
|
||||||
}
|
|
||||||
break :blk count;
|
|
||||||
};
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
var fields: [field_count]std.builtin.Type.EnumField = undefined;
|
|
||||||
for (enums) |e| {
|
|
||||||
for (@typeInfo(e).@"enum".fields) |f| {
|
|
||||||
fields[i] = .{
|
|
||||||
.name = f.name,
|
|
||||||
.value = i,
|
|
||||||
};
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return @Type(.{ .@"enum" = .{
|
|
||||||
.decls = &.{},
|
|
||||||
.tag_type = blk: {
|
|
||||||
if (field_count <= std.math.maxInt(u8)) break :blk u8;
|
|
||||||
if (field_count <= std.math.maxInt(u16)) break :blk u16;
|
|
||||||
unreachable;
|
|
||||||
},
|
|
||||||
.fields = &fields,
|
|
||||||
.is_exhaustive = true,
|
|
||||||
} });
|
|
||||||
}
|
|
||||||
|
|||||||
489
src/browser/structured_data.zig
Normal file
489
src/browser/structured_data.zig
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
const URL = @import("URL.zig");
|
||||||
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
/// Key-value pair for structured data properties.
|
||||||
|
pub const Property = struct {
|
||||||
|
key: []const u8,
|
||||||
|
value: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const AlternateLink = struct {
|
||||||
|
href: []const u8,
|
||||||
|
hreflang: ?[]const u8,
|
||||||
|
type: ?[]const u8,
|
||||||
|
title: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const StructuredData = struct {
|
||||||
|
json_ld: []const []const u8,
|
||||||
|
open_graph: []const Property,
|
||||||
|
twitter_card: []const Property,
|
||||||
|
meta: []const Property,
|
||||||
|
links: []const Property,
|
||||||
|
alternate: []const AlternateLink,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: *const StructuredData, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
|
||||||
|
try jw.objectField("jsonLd");
|
||||||
|
try jw.write(self.json_ld);
|
||||||
|
|
||||||
|
try jw.objectField("openGraph");
|
||||||
|
try writeProperties(jw, self.open_graph);
|
||||||
|
|
||||||
|
try jw.objectField("twitterCard");
|
||||||
|
try writeProperties(jw, self.twitter_card);
|
||||||
|
|
||||||
|
try jw.objectField("meta");
|
||||||
|
try writeProperties(jw, self.meta);
|
||||||
|
|
||||||
|
try jw.objectField("links");
|
||||||
|
try writeProperties(jw, self.links);
|
||||||
|
|
||||||
|
if (self.alternate.len > 0) {
|
||||||
|
try jw.objectField("alternate");
|
||||||
|
try jw.beginArray();
|
||||||
|
for (self.alternate) |alt| {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("href");
|
||||||
|
try jw.write(alt.href);
|
||||||
|
if (alt.hreflang) |v| {
|
||||||
|
try jw.objectField("hreflang");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
if (alt.type) |v| {
|
||||||
|
try jw.objectField("type");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
if (alt.title) |v| {
|
||||||
|
try jw.objectField("title");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Serializes properties as a JSON object. When a key appears multiple times
|
||||||
|
/// (e.g. multiple og:image tags), values are grouped into an array.
|
||||||
|
/// Alternatives considered: always-array values (verbose), or an array of
|
||||||
|
/// {key, value} pairs (preserves order but less ergonomic for consumers).
|
||||||
|
fn writeProperties(jw: anytype, properties: []const Property) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
for (properties, 0..) |prop, i| {
|
||||||
|
// Skip keys already written by an earlier occurrence.
|
||||||
|
var already_written = false;
|
||||||
|
for (properties[0..i]) |prev| {
|
||||||
|
if (std.mem.eql(u8, prev.key, prop.key)) {
|
||||||
|
already_written = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (already_written) continue;
|
||||||
|
|
||||||
|
// Count total occurrences to decide string vs array.
|
||||||
|
var count: usize = 0;
|
||||||
|
for (properties) |p| {
|
||||||
|
if (std.mem.eql(u8, p.key, prop.key)) count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.objectField(prop.key);
|
||||||
|
if (count == 1) {
|
||||||
|
try jw.write(prop.value);
|
||||||
|
} else {
|
||||||
|
try jw.beginArray();
|
||||||
|
for (properties) |p| {
|
||||||
|
if (std.mem.eql(u8, p.key, prop.key)) {
|
||||||
|
try jw.write(p.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract all structured data from the page.
|
||||||
|
pub fn collectStructuredData(
|
||||||
|
root: *Node,
|
||||||
|
arena: Allocator,
|
||||||
|
page: *Page,
|
||||||
|
) !StructuredData {
|
||||||
|
var json_ld: std.ArrayList([]const u8) = .empty;
|
||||||
|
var open_graph: std.ArrayList(Property) = .empty;
|
||||||
|
var twitter_card: std.ArrayList(Property) = .empty;
|
||||||
|
var meta: std.ArrayList(Property) = .empty;
|
||||||
|
var links: std.ArrayList(Property) = .empty;
|
||||||
|
var alternate: std.ArrayList(AlternateLink) = .empty;
|
||||||
|
|
||||||
|
// Extract language from the root <html> element.
|
||||||
|
if (root.is(Element)) |root_el| {
|
||||||
|
if (root_el.getAttributeSafe(comptime .wrap("lang"))) |lang| {
|
||||||
|
try meta.append(arena, .{ .key = "language", .value = lang });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Root is document — check documentElement.
|
||||||
|
var children = root.childrenIterator();
|
||||||
|
while (children.next()) |child| {
|
||||||
|
const el = child.is(Element) orelse continue;
|
||||||
|
if (el.getTag() == .html) {
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("lang"))) |lang| {
|
||||||
|
try meta.append(arena, .{ .key = "language", .value = lang });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tw = TreeWalker.Full.init(root, .{});
|
||||||
|
while (tw.next()) |node| {
|
||||||
|
const el = node.is(Element) orelse continue;
|
||||||
|
|
||||||
|
switch (el.getTag()) {
|
||||||
|
.script => {
|
||||||
|
try collectJsonLd(el, arena, &json_ld);
|
||||||
|
tw.skipChildren();
|
||||||
|
},
|
||||||
|
.meta => collectMeta(el, &open_graph, &twitter_card, &meta, arena) catch {},
|
||||||
|
.title => try collectTitle(node, arena, &meta),
|
||||||
|
.link => try collectLink(el, arena, page, &links, &alternate),
|
||||||
|
// Skip body subtree for non-JSON-LD — all other metadata is in <head>.
|
||||||
|
// JSON-LD can appear in <body> so we don't skip the whole body.
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.json_ld = json_ld.items,
|
||||||
|
.open_graph = open_graph.items,
|
||||||
|
.twitter_card = twitter_card.items,
|
||||||
|
.meta = meta.items,
|
||||||
|
.links = links.items,
|
||||||
|
.alternate = alternate.items,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectJsonLd(
|
||||||
|
el: *Element,
|
||||||
|
arena: Allocator,
|
||||||
|
json_ld: *std.ArrayList([]const u8),
|
||||||
|
) !void {
|
||||||
|
const type_attr = el.getAttributeSafe(comptime .wrap("type")) orelse return;
|
||||||
|
if (!std.ascii.eqlIgnoreCase(type_attr, "application/ld+json")) return;
|
||||||
|
|
||||||
|
var buf: std.Io.Writer.Allocating = .init(arena);
|
||||||
|
try el.asNode().getTextContent(&buf.writer);
|
||||||
|
const text = buf.written();
|
||||||
|
if (text.len > 0) {
|
||||||
|
try json_ld.append(arena, std.mem.trim(u8, text, &std.ascii.whitespace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectMeta(
|
||||||
|
el: *Element,
|
||||||
|
open_graph: *std.ArrayList(Property),
|
||||||
|
twitter_card: *std.ArrayList(Property),
|
||||||
|
meta: *std.ArrayList(Property),
|
||||||
|
arena: Allocator,
|
||||||
|
) !void {
|
||||||
|
// charset: <meta charset="..."> (no content attribute needed).
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("charset"))) |charset| {
|
||||||
|
try meta.append(arena, .{ .key = "charset", .value = charset });
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = el.getAttributeSafe(comptime .wrap("content")) orelse return;
|
||||||
|
|
||||||
|
// Open Graph: <meta property="og:...">
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("property"))) |property| {
|
||||||
|
if (std.mem.startsWith(u8, property, "og:")) {
|
||||||
|
try open_graph.append(arena, .{ .key = property[3..], .value = content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Article, profile, etc. are OG sub-namespaces.
|
||||||
|
if (std.mem.startsWith(u8, property, "article:") or
|
||||||
|
std.mem.startsWith(u8, property, "profile:") or
|
||||||
|
std.mem.startsWith(u8, property, "book:") or
|
||||||
|
std.mem.startsWith(u8, property, "music:") or
|
||||||
|
std.mem.startsWith(u8, property, "video:"))
|
||||||
|
{
|
||||||
|
try open_graph.append(arena, .{ .key = property, .value = content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twitter Cards: <meta name="twitter:...">
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("name"))) |name| {
|
||||||
|
if (std.mem.startsWith(u8, name, "twitter:")) {
|
||||||
|
try twitter_card.append(arena, .{ .key = name[8..], .value = content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard meta tags by name.
|
||||||
|
const known_names = [_][]const u8{
|
||||||
|
"description", "author", "keywords", "robots",
|
||||||
|
"viewport", "generator", "theme-color",
|
||||||
|
};
|
||||||
|
for (known_names) |known| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(name, known)) {
|
||||||
|
try meta.append(arena, .{ .key = known, .value = content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// http-equiv (e.g. Content-Type, refresh)
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("http-equiv"))) |http_equiv| {
|
||||||
|
try meta.append(arena, .{ .key = http_equiv, .value = content });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectTitle(
|
||||||
|
node: *Node,
|
||||||
|
arena: Allocator,
|
||||||
|
meta: *std.ArrayList(Property),
|
||||||
|
) !void {
|
||||||
|
var buf: std.Io.Writer.Allocating = .init(arena);
|
||||||
|
try node.getTextContent(&buf.writer);
|
||||||
|
const text = std.mem.trim(u8, buf.written(), &std.ascii.whitespace);
|
||||||
|
if (text.len > 0) {
|
||||||
|
try meta.append(arena, .{ .key = "title", .value = text });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectLink(
|
||||||
|
el: *Element,
|
||||||
|
arena: Allocator,
|
||||||
|
page: *Page,
|
||||||
|
links: *std.ArrayList(Property),
|
||||||
|
alternate: *std.ArrayList(AlternateLink),
|
||||||
|
) !void {
|
||||||
|
const rel = el.getAttributeSafe(comptime .wrap("rel")) orelse return;
|
||||||
|
const raw_href = el.getAttributeSafe(comptime .wrap("href")) orelse return;
|
||||||
|
const href = URL.resolve(arena, page.base(), raw_href, .{ .encode = true }) catch raw_href;
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(rel, "alternate")) {
|
||||||
|
try alternate.append(arena, .{
|
||||||
|
.href = href,
|
||||||
|
.hreflang = el.getAttributeSafe(comptime .wrap("hreflang")),
|
||||||
|
.type = el.getAttributeSafe(comptime .wrap("type")),
|
||||||
|
.title = el.getAttributeSafe(comptime .wrap("title")),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relevant_rels = [_][]const u8{
|
||||||
|
"canonical", "icon", "manifest", "shortcut icon",
|
||||||
|
"apple-touch-icon", "search", "author", "license",
|
||||||
|
"dns-prefetch", "preconnect",
|
||||||
|
};
|
||||||
|
for (relevant_rels) |known| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(rel, known)) {
|
||||||
|
try links.append(arena, .{ .key = known, .value = href });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
|
||||||
|
fn testStructuredData(html: []const u8) !StructuredData {
|
||||||
|
const page = try testing.test_session.createPage();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
|
||||||
|
const doc = page.window._document;
|
||||||
|
const div = try doc.createElement("div", null, page);
|
||||||
|
try page.parseHtmlAsChildren(div.asNode(), html);
|
||||||
|
|
||||||
|
return collectStructuredData(div.asNode(), page.call_arena, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findProperty(props: []const Property, key: []const u8) ?[]const u8 {
|
||||||
|
for (props) |p| {
|
||||||
|
if (std.mem.eql(u8, p.key, key)) return p.value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: json-ld" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<script type="application/ld+json">
|
||||||
|
\\{"@context":"https://schema.org","@type":"Article","headline":"Test"}
|
||||||
|
\\</script>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, data.json_ld.len);
|
||||||
|
try testing.expect(std.mem.indexOf(u8, data.json_ld[0], "Article") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: multiple json-ld" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<script type="application/ld+json">{"@type":"Organization"}</script>
|
||||||
|
\\<script type="application/ld+json">{"@type":"BreadcrumbList"}</script>
|
||||||
|
\\<script type="text/javascript">var x = 1;</script>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(2, data.json_ld.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: open graph" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<meta property="og:title" content="My Page">
|
||||||
|
\\<meta property="og:description" content="A description">
|
||||||
|
\\<meta property="og:image" content="https://example.com/img.jpg">
|
||||||
|
\\<meta property="og:url" content="https://example.com">
|
||||||
|
\\<meta property="og:type" content="article">
|
||||||
|
\\<meta property="article:published_time" content="2026-03-10">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(6, data.open_graph.len);
|
||||||
|
try testing.expectEqual("My Page", findProperty(data.open_graph, "title").?);
|
||||||
|
try testing.expectEqual("article", findProperty(data.open_graph, "type").?);
|
||||||
|
try testing.expectEqual("2026-03-10", findProperty(data.open_graph, "article:published_time").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: open graph duplicate keys" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<meta property="og:title" content="My Page">
|
||||||
|
\\<meta property="og:image" content="https://example.com/img1.jpg">
|
||||||
|
\\<meta property="og:image" content="https://example.com/img2.jpg">
|
||||||
|
\\<meta property="og:image" content="https://example.com/img3.jpg">
|
||||||
|
);
|
||||||
|
// Duplicate keys are preserved as separate Property entries.
|
||||||
|
try testing.expectEqual(4, data.open_graph.len);
|
||||||
|
|
||||||
|
// Verify serialization groups duplicates into arrays.
|
||||||
|
const json = try std.json.Stringify.valueAlloc(testing.allocator, data, .{});
|
||||||
|
defer testing.allocator.free(json);
|
||||||
|
|
||||||
|
const parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, json, .{});
|
||||||
|
defer parsed.deinit();
|
||||||
|
const og = parsed.value.object.get("openGraph").?.object;
|
||||||
|
// "title" appears once → string.
|
||||||
|
switch (og.get("title").?) {
|
||||||
|
.string => {},
|
||||||
|
else => return error.TestUnexpectedResult,
|
||||||
|
}
|
||||||
|
// "image" appears 3 times → array.
|
||||||
|
switch (og.get("image").?) {
|
||||||
|
.array => |arr| try testing.expectEqual(3, arr.items.len),
|
||||||
|
else => return error.TestUnexpectedResult,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: twitter card" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<meta name="twitter:card" content="summary_large_image">
|
||||||
|
\\<meta name="twitter:site" content="@example">
|
||||||
|
\\<meta name="twitter:title" content="My Page">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(3, data.twitter_card.len);
|
||||||
|
try testing.expectEqual("summary_large_image", findProperty(data.twitter_card, "card").?);
|
||||||
|
try testing.expectEqual("@example", findProperty(data.twitter_card, "site").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: meta tags" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<title>Page Title</title>
|
||||||
|
\\<meta name="description" content="A test page">
|
||||||
|
\\<meta name="author" content="Test Author">
|
||||||
|
\\<meta name="keywords" content="test, example">
|
||||||
|
\\<meta name="robots" content="index, follow">
|
||||||
|
);
|
||||||
|
try testing.expectEqual("Page Title", findProperty(data.meta, "title").?);
|
||||||
|
try testing.expectEqual("A test page", findProperty(data.meta, "description").?);
|
||||||
|
try testing.expectEqual("Test Author", findProperty(data.meta, "author").?);
|
||||||
|
try testing.expectEqual("test, example", findProperty(data.meta, "keywords").?);
|
||||||
|
try testing.expectEqual("index, follow", findProperty(data.meta, "robots").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: link elements" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<link rel="canonical" href="https://example.com/page">
|
||||||
|
\\<link rel="icon" href="/favicon.ico">
|
||||||
|
\\<link rel="manifest" href="/manifest.json">
|
||||||
|
\\<link rel="stylesheet" href="/style.css">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(3, data.links.len);
|
||||||
|
try testing.expectEqual("https://example.com/page", findProperty(data.links, "canonical").?);
|
||||||
|
// stylesheet should be filtered out
|
||||||
|
try testing.expectEqual(null, findProperty(data.links, "stylesheet"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: alternate links" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<link rel="alternate" href="https://example.com/fr" hreflang="fr" title="French">
|
||||||
|
\\<link rel="alternate" href="https://example.com/de" hreflang="de">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(2, data.alternate.len);
|
||||||
|
try testing.expectEqual("fr", data.alternate[0].hreflang.?);
|
||||||
|
try testing.expectEqual("French", data.alternate[0].title.?);
|
||||||
|
try testing.expectEqual("de", data.alternate[1].hreflang.?);
|
||||||
|
try testing.expectEqual(null, data.alternate[1].title);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: non-metadata elements ignored" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<div>Just text</div>
|
||||||
|
\\<p>More text</p>
|
||||||
|
\\<a href="/link">Link</a>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(0, data.json_ld.len);
|
||||||
|
try testing.expectEqual(0, data.open_graph.len);
|
||||||
|
try testing.expectEqual(0, data.twitter_card.len);
|
||||||
|
try testing.expectEqual(0, data.meta.len);
|
||||||
|
try testing.expectEqual(0, data.links.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: charset and http-equiv" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<meta charset="utf-8">
|
||||||
|
\\<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
);
|
||||||
|
try testing.expectEqual("utf-8", findProperty(data.meta, "charset").?);
|
||||||
|
try testing.expectEqual("text/html; charset=utf-8", findProperty(data.meta, "Content-Type").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: mixed content" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<title>My Site</title>
|
||||||
|
\\<meta property="og:title" content="OG Title">
|
||||||
|
\\<meta name="twitter:card" content="summary">
|
||||||
|
\\<meta name="description" content="A page">
|
||||||
|
\\<link rel="canonical" href="https://example.com">
|
||||||
|
\\<script type="application/ld+json">{"@type":"WebSite"}</script>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, data.json_ld.len);
|
||||||
|
try testing.expectEqual(1, data.open_graph.len);
|
||||||
|
try testing.expectEqual(1, data.twitter_card.len);
|
||||||
|
try testing.expectEqual("My Site", findProperty(data.meta, "title").?);
|
||||||
|
try testing.expectEqual("A page", findProperty(data.meta, "description").?);
|
||||||
|
try testing.expectEqual(1, data.links.len);
|
||||||
|
}
|
||||||
@@ -3,13 +3,68 @@
|
|||||||
|
|
||||||
<script id=animation>
|
<script id=animation>
|
||||||
let a1 = document.createElement('div').animate(null, null);
|
let a1 = document.createElement('div').animate(null, null);
|
||||||
testing.expectEqual('finished', a1.playState);
|
testing.expectEqual('idle', a1.playState);
|
||||||
|
|
||||||
let cb = [];
|
let cb = [];
|
||||||
a1.ready.then(() => { cb.push('ready') });
|
|
||||||
a1.finished.then((x) => {
|
a1.finished.then((x) => {
|
||||||
cb.push('finished');
|
cb.push(a1.playState);
|
||||||
cb.push(x == a1);
|
cb.push(x == a1);
|
||||||
});
|
});
|
||||||
testing.eventually(() => testing.expectEqual(['finished', true], cb));
|
a1.ready.then(() => {
|
||||||
|
cb.push(a1.playState);
|
||||||
|
a1.play();
|
||||||
|
cb.push(a1.playState);
|
||||||
|
});
|
||||||
|
testing.onload(() => testing.expectEqual(['idle', 'running', 'finished', true], cb));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- <script id=startTime>
|
||||||
|
let a2 = document.createElement('div').animate(null, null);
|
||||||
|
// startTime defaults to null
|
||||||
|
testing.expectEqual(null, a2.startTime);
|
||||||
|
// startTime is settable
|
||||||
|
a2.startTime = 42.5;
|
||||||
|
testing.expectEqual(42.5, a2.startTime);
|
||||||
|
// startTime can be reset to null
|
||||||
|
a2.startTime = null;
|
||||||
|
testing.expectEqual(null, a2.startTime);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onfinish>
|
||||||
|
let a3 = document.createElement('div').animate(null, null);
|
||||||
|
// onfinish defaults to null
|
||||||
|
testing.expectEqual(null, a3.onfinish);
|
||||||
|
|
||||||
|
let calls = [];
|
||||||
|
// onfinish callback should be scheduled and called asynchronously
|
||||||
|
a3.onfinish = function() { calls.push('finish'); };
|
||||||
|
a3.play();
|
||||||
|
testing.onload(() => testing.expectEqual(['finish'], calls));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=pause>
|
||||||
|
let a4 = document.createElement('div').animate(null, null);
|
||||||
|
let cb4 = [];
|
||||||
|
a4.finished.then((x) => { cb4.push(a4.playState) });
|
||||||
|
a4.ready.then(() => {
|
||||||
|
a4.play();
|
||||||
|
cb4.push(a4.playState)
|
||||||
|
a4.pause();
|
||||||
|
cb4.push(a4.playState)
|
||||||
|
});
|
||||||
|
testing.onload(() => testing.expectEqual(['running', 'paused'], cb4));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=finish>
|
||||||
|
let a5 = document.createElement('div').animate(null, null);
|
||||||
|
testing.expectEqual('idle', a5.playState);
|
||||||
|
|
||||||
|
let cb5 = [];
|
||||||
|
a5.finished.then((x) => { cb5.push(a5.playState) });
|
||||||
|
a5.ready.then(() => {
|
||||||
|
cb5.push(a5.playState);
|
||||||
|
a5.play();
|
||||||
|
});
|
||||||
|
testing.onload(() => testing.expectEqual(['idle', 'finished'], cb5));
|
||||||
|
</script>
|
||||||
|
-->
|
||||||
|
|||||||
@@ -98,6 +98,64 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=mime_parsing>
|
||||||
|
// MIME types are lowercased
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "TEXT/HTML" });
|
||||||
|
testing.expectEqual("text/html", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "Application/JSON" });
|
||||||
|
testing.expectEqual("application/json", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIME with parameters - lowercased
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "text/html; charset=UTF-8" });
|
||||||
|
testing.expectEqual("text/html; charset=utf-8", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any ASCII string is accepted and lowercased (no MIME structure validation)
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "invalid" });
|
||||||
|
testing.expectEqual("invalid", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "/" });
|
||||||
|
testing.expectEqual("/", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-ASCII characters cause empty string (chars outside U+0020-U+007E)
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "ý/x" });
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "text/plàin" });
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control characters cause empty string
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "text/html\x00" });
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty type stays empty
|
||||||
|
{
|
||||||
|
const blob = new Blob([]);
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "" });
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id=slice>
|
<script id=slice>
|
||||||
{
|
{
|
||||||
const parts = ["la", "symphonie", "des", "éclairs"];
|
const parts = ["la", "symphonie", "des", "éclairs"];
|
||||||
|
|||||||
150
src/browser/tests/canvas/canvas_rendering_context_2d.html
Normal file
150
src/browser/tests/canvas/canvas_rendering_context_2d.html
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=CanvasRenderingContext2D>
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
testing.expectEqual(true, ctx instanceof CanvasRenderingContext2D);
|
||||||
|
// We can't really test this but let's try to call it at least.
|
||||||
|
ctx.fillRect(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=CanvasRenderingContext2D#fillStyle>
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
// Black by default.
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#000000");
|
||||||
|
ctx.fillStyle = "red";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#ff0000");
|
||||||
|
ctx.fillStyle = "rebeccapurple";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#663399");
|
||||||
|
// No changes made if color is invalid.
|
||||||
|
ctx.fillStyle = "invalid-color";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#663399");
|
||||||
|
ctx.fillStyle = "#fc0";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#ffcc00");
|
||||||
|
ctx.fillStyle = "#ff0000";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#ff0000");
|
||||||
|
ctx.fillStyle = "#fF00000F";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "rgba(255, 0, 0, 0.06)");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CanvasRenderingContext2D#createImageData(width, height)">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
const imageData = ctx.createImageData(100, 200);
|
||||||
|
testing.expectEqual(true, imageData instanceof ImageData);
|
||||||
|
testing.expectEqual(imageData.width, 100);
|
||||||
|
testing.expectEqual(imageData.height, 200);
|
||||||
|
testing.expectEqual(imageData.data.length, 100 * 200 * 4);
|
||||||
|
testing.expectEqual(true, imageData.data instanceof Uint8ClampedArray);
|
||||||
|
|
||||||
|
// All pixels should be initialized to 0.
|
||||||
|
testing.expectEqual(imageData.data[0], 0);
|
||||||
|
testing.expectEqual(imageData.data[1], 0);
|
||||||
|
testing.expectEqual(imageData.data[2], 0);
|
||||||
|
testing.expectEqual(imageData.data[3], 0);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CanvasRenderingContext2D#createImageData(imageData)">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
const source = ctx.createImageData(50, 75);
|
||||||
|
const imageData = ctx.createImageData(source);
|
||||||
|
testing.expectEqual(true, imageData instanceof ImageData);
|
||||||
|
testing.expectEqual(imageData.width, 50);
|
||||||
|
testing.expectEqual(imageData.height, 75);
|
||||||
|
testing.expectEqual(imageData.data.length, 50 * 75 * 4);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CanvasRenderingContext2D#putImageData">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
const imageData = ctx.createImageData(10, 10);
|
||||||
|
testing.expectEqual(true, imageData instanceof ImageData);
|
||||||
|
// Modify some pixel data.
|
||||||
|
imageData.data[0] = 255;
|
||||||
|
imageData.data[1] = 0;
|
||||||
|
imageData.data[2] = 0;
|
||||||
|
imageData.data[3] = 255;
|
||||||
|
|
||||||
|
// putImageData should not throw.
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
ctx.putImageData(imageData, 10, 20);
|
||||||
|
// With dirty rect parameters.
|
||||||
|
ctx.putImageData(imageData, 0, 0, 0, 0, 5, 5);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CanvasRenderingContext2D#getImageData">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
element.width = 100;
|
||||||
|
element.height = 50;
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, 10, 20);
|
||||||
|
testing.expectEqual(true, imageData instanceof ImageData);
|
||||||
|
testing.expectEqual(imageData.width, 10);
|
||||||
|
testing.expectEqual(imageData.height, 20);
|
||||||
|
testing.expectEqual(imageData.data.length, 10 * 20 * 4);
|
||||||
|
testing.expectEqual(true, imageData.data instanceof Uint8ClampedArray);
|
||||||
|
|
||||||
|
// Undrawn canvas should return transparent black pixels.
|
||||||
|
testing.expectEqual(imageData.data[0], 0);
|
||||||
|
testing.expectEqual(imageData.data[1], 0);
|
||||||
|
testing.expectEqual(imageData.data[2], 0);
|
||||||
|
testing.expectEqual(imageData.data[3], 0);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CanvasRenderingContext2D#getImageData invalid">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
// Zero or negative width/height should throw IndexSizeError.
|
||||||
|
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 0, 10));
|
||||||
|
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 10, 0));
|
||||||
|
testing.expectError('Index or size', () => ctx.getImageData(0, 0, -5, 10));
|
||||||
|
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 10, -5));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script id="CanvasRenderingContext2D#canvas">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
testing.expectEqual(ctx.canvas, element);
|
||||||
|
// Setting dimensions via ctx.canvas should update the element.
|
||||||
|
ctx.canvas.width = 40;
|
||||||
|
ctx.canvas.height = 25;
|
||||||
|
testing.expectEqual(element.width, 40);
|
||||||
|
testing.expectEqual(element.height, 25);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="getter">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
testing.expectEqual('10px sans-serif', ctx.font);
|
||||||
|
ctx.font = 'bold 48px serif'
|
||||||
|
testing.expectEqual('bold 48px serif', ctx.font);
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
87
src/browser/tests/canvas/offscreen_canvas.html
Normal file
87
src/browser/tests/canvas/offscreen_canvas.html
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(256, 256);
|
||||||
|
testing.expectEqual(true, canvas instanceof OffscreenCanvas);
|
||||||
|
testing.expectEqual(canvas.width, 256);
|
||||||
|
testing.expectEqual(canvas.height, 256);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas#width>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(100, 200);
|
||||||
|
testing.expectEqual(canvas.width, 100);
|
||||||
|
canvas.width = 300;
|
||||||
|
testing.expectEqual(canvas.width, 300);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas#height>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(100, 200);
|
||||||
|
testing.expectEqual(canvas.height, 200);
|
||||||
|
canvas.height = 400;
|
||||||
|
testing.expectEqual(canvas.height, 400);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas#getContext>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(64, 64);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
testing.expectEqual(true, ctx instanceof OffscreenCanvasRenderingContext2D);
|
||||||
|
// We can't really test rendering but let's try to call it at least.
|
||||||
|
ctx.fillRect(0, 0, 10, 10);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas#convertToBlob>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(64, 64);
|
||||||
|
const promise = canvas.convertToBlob();
|
||||||
|
testing.expectEqual(true, promise instanceof Promise);
|
||||||
|
// The promise should resolve to a Blob (even if empty)
|
||||||
|
promise.then(blob => {
|
||||||
|
testing.expectEqual(true, blob instanceof Blob);
|
||||||
|
testing.expectEqual(blob.size, 0); // Empty since no rendering
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=HTMLCanvasElement#transferControlToOffscreen>
|
||||||
|
{
|
||||||
|
const htmlCanvas = document.createElement("canvas");
|
||||||
|
htmlCanvas.width = 128;
|
||||||
|
htmlCanvas.height = 96;
|
||||||
|
const offscreen = htmlCanvas.transferControlToOffscreen();
|
||||||
|
testing.expectEqual(true, offscreen instanceof OffscreenCanvas);
|
||||||
|
testing.expectEqual(offscreen.width, 128);
|
||||||
|
testing.expectEqual(offscreen.height, 96);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvasRenderingContext2D#getImageData>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(100, 50);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, 10, 20);
|
||||||
|
testing.expectEqual(true, imageData instanceof ImageData);
|
||||||
|
testing.expectEqual(imageData.width, 10);
|
||||||
|
testing.expectEqual(imageData.height, 20);
|
||||||
|
testing.expectEqual(imageData.data.length, 10 * 20 * 4);
|
||||||
|
|
||||||
|
// Undrawn canvas should return transparent black pixels.
|
||||||
|
testing.expectEqual(imageData.data[0], 0);
|
||||||
|
testing.expectEqual(imageData.data[1], 0);
|
||||||
|
testing.expectEqual(imageData.data[2], 0);
|
||||||
|
testing.expectEqual(imageData.data[3], 0);
|
||||||
|
|
||||||
|
// Zero or negative dimensions should throw.
|
||||||
|
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 0, 10));
|
||||||
|
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 10, -5));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
87
src/browser/tests/canvas/webgl_rendering_context.html
Normal file
87
src/browser/tests/canvas/webgl_rendering_context.html
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=WebGLRenderingContext#getSupportedExtensions>
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("webgl");
|
||||||
|
testing.expectEqual(true, ctx instanceof WebGLRenderingContext);
|
||||||
|
|
||||||
|
const supportedExtensions = ctx.getSupportedExtensions();
|
||||||
|
// The order Chrome prefer.
|
||||||
|
const expectedExtensions = [
|
||||||
|
"ANGLE_instanced_arrays",
|
||||||
|
"EXT_blend_minmax",
|
||||||
|
"EXT_clip_control",
|
||||||
|
"EXT_color_buffer_half_float",
|
||||||
|
"EXT_depth_clamp",
|
||||||
|
"EXT_disjoint_timer_query",
|
||||||
|
"EXT_float_blend",
|
||||||
|
"EXT_frag_depth",
|
||||||
|
"EXT_polygon_offset_clamp",
|
||||||
|
"EXT_shader_texture_lod",
|
||||||
|
"EXT_texture_compression_bptc",
|
||||||
|
"EXT_texture_compression_rgtc",
|
||||||
|
"EXT_texture_filter_anisotropic",
|
||||||
|
"EXT_texture_mirror_clamp_to_edge",
|
||||||
|
"EXT_sRGB",
|
||||||
|
"KHR_parallel_shader_compile",
|
||||||
|
"OES_element_index_uint",
|
||||||
|
"OES_fbo_render_mipmap",
|
||||||
|
"OES_standard_derivatives",
|
||||||
|
"OES_texture_float",
|
||||||
|
"OES_texture_float_linear",
|
||||||
|
"OES_texture_half_float",
|
||||||
|
"OES_texture_half_float_linear",
|
||||||
|
"OES_vertex_array_object",
|
||||||
|
"WEBGL_blend_func_extended",
|
||||||
|
"WEBGL_color_buffer_float",
|
||||||
|
"WEBGL_compressed_texture_astc",
|
||||||
|
"WEBGL_compressed_texture_etc",
|
||||||
|
"WEBGL_compressed_texture_etc1",
|
||||||
|
"WEBGL_compressed_texture_pvrtc",
|
||||||
|
"WEBGL_compressed_texture_s3tc",
|
||||||
|
"WEBGL_compressed_texture_s3tc_srgb",
|
||||||
|
"WEBGL_debug_renderer_info",
|
||||||
|
"WEBGL_debug_shaders",
|
||||||
|
"WEBGL_depth_texture",
|
||||||
|
"WEBGL_draw_buffers",
|
||||||
|
"WEBGL_lose_context",
|
||||||
|
"WEBGL_multi_draw",
|
||||||
|
"WEBGL_polygon_mode"
|
||||||
|
];
|
||||||
|
|
||||||
|
testing.expectEqual(expectedExtensions.length, supportedExtensions.length);
|
||||||
|
for (let i = 0; i < expectedExtensions.length; i++) {
|
||||||
|
testing.expectEqual(expectedExtensions[i], supportedExtensions[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=WebGLRenderingCanvas#getExtension>
|
||||||
|
// WEBGL_debug_renderer_info
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("webgl");
|
||||||
|
const rendererInfo = ctx.getExtension("WEBGL_debug_renderer_info");
|
||||||
|
testing.expectEqual(true, rendererInfo instanceof WEBGL_debug_renderer_info);
|
||||||
|
|
||||||
|
const { UNMASKED_VENDOR_WEBGL, UNMASKED_RENDERER_WEBGL } = rendererInfo;
|
||||||
|
testing.expectEqual(UNMASKED_VENDOR_WEBGL, 0x9245);
|
||||||
|
testing.expectEqual(UNMASKED_RENDERER_WEBGL, 0x9246);
|
||||||
|
|
||||||
|
testing.expectEqual("", ctx.getParameter(UNMASKED_VENDOR_WEBGL));
|
||||||
|
testing.expectEqual("", ctx.getParameter(UNMASKED_RENDERER_WEBGL));
|
||||||
|
}
|
||||||
|
|
||||||
|
// WEBGL_lose_context
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("webgl");
|
||||||
|
const loseContext = ctx.getExtension("WEBGL_lose_context");
|
||||||
|
testing.expectEqual(true, loseContext instanceof WEBGL_lose_context);
|
||||||
|
|
||||||
|
loseContext.loseContext();
|
||||||
|
loseContext.restoreContext();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -201,8 +201,8 @@ cdataClassName<!DOCTYPE html>
|
|||||||
root.appendChild(cdata);
|
root.appendChild(cdata);
|
||||||
root.appendChild(elem2);
|
root.appendChild(elem2);
|
||||||
|
|
||||||
testing.expectEqual('LAST', cdata.nextElementSibling.tagName);
|
testing.expectEqual('last', cdata.nextElementSibling.tagName);
|
||||||
testing.expectEqual('FIRST', cdata.previousElementSibling.tagName);
|
testing.expectEqual('first', cdata.previousElementSibling.tagName);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,6 @@
|
|||||||
<script id=comment>
|
<script id=comment>
|
||||||
testing.expectEqual('', new Comment().data);
|
testing.expectEqual('', new Comment().data);
|
||||||
testing.expectEqual('over 9000! ', new Comment('over 9000! ').data);
|
testing.expectEqual('over 9000! ', new Comment('over 9000! ').data);
|
||||||
|
|
||||||
|
testing.expectEqual('null', new Comment(null).data);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<a id="link" href="foo" class="ok">OK</a>
|
<a id="link" href="foo" class="ok">OK</a>
|
||||||
|
|
||||||
<script src="../../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
<script id=text>
|
<script id=text>
|
||||||
let t = new Text('foo');
|
let t = new Text('foo');
|
||||||
testing.expectEqual('foo', t.data);
|
testing.expectEqual('foo', t.data);
|
||||||
@@ -16,4 +16,7 @@
|
|||||||
let split = text.splitText('OK'.length);
|
let split = text.splitText('OK'.length);
|
||||||
testing.expectEqual(' modified', split.data);
|
testing.expectEqual(' modified', split.data);
|
||||||
testing.expectEqual('OK', text.data);
|
testing.expectEqual('OK', text.data);
|
||||||
|
|
||||||
|
let x = new Text(null);
|
||||||
|
testing.expectEqual("null", x.data);
|
||||||
</script>
|
</script>
|
||||||
25
src/browser/tests/cdp/dom3.html
Normal file
25
src/browser/tests/cdp/dom3.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Test Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Test Page</h1>
|
||||||
|
<nav>
|
||||||
|
<a href="/page1" id="link1">First Link</a>
|
||||||
|
<a href="/page2" id="link2">Second Link</a>
|
||||||
|
</nav>
|
||||||
|
<form id="testForm" action="/submit" method="post">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" id="username" name="username" placeholder="Enter username">
|
||||||
|
|
||||||
|
<label for="email">Email:</label>
|
||||||
|
<input type="email" id="email" name="email" placeholder="Enter email">
|
||||||
|
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password">
|
||||||
|
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
28
src/browser/tests/console/console.html
Normal file
28
src/browser/tests/console/console.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id="time">
|
||||||
|
// should not crash
|
||||||
|
console.time();
|
||||||
|
console.timeLog();
|
||||||
|
console.timeEnd();
|
||||||
|
|
||||||
|
console.time("test");
|
||||||
|
console.timeLog("test");
|
||||||
|
console.timeEnd("test");
|
||||||
|
|
||||||
|
testing.expectEqual(true, true);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="count">
|
||||||
|
// should not crash
|
||||||
|
console.count();
|
||||||
|
console.count();
|
||||||
|
console.countReset();
|
||||||
|
|
||||||
|
console.count("test");
|
||||||
|
console.count("test");
|
||||||
|
console.countReset("test");
|
||||||
|
|
||||||
|
testing.expectEqual(true, true);
|
||||||
|
</script>
|
||||||
@@ -16,41 +16,119 @@
|
|||||||
isRandom(ti8a)
|
isRandom(ti8a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// {
|
{
|
||||||
// let tu16a = new Uint16Array(100)
|
let tu16a = new Uint16Array(100)
|
||||||
// testing.expectEqual(tu16a, crypto.getRandomValues(tu16a))
|
testing.expectEqual(tu16a, crypto.getRandomValues(tu16a))
|
||||||
// isRandom(tu16a)
|
isRandom(tu16a)
|
||||||
|
|
||||||
// let ti16a = new Int16Array(100)
|
let ti16a = new Int16Array(100)
|
||||||
// testing.expectEqual(ti16a, crypto.getRandomValues(ti16a))
|
testing.expectEqual(ti16a, crypto.getRandomValues(ti16a))
|
||||||
// isRandom(ti16a)
|
isRandom(ti16a)
|
||||||
// }
|
}
|
||||||
|
|
||||||
// {
|
{
|
||||||
// let tu32a = new Uint32Array(100)
|
let tu32a = new Uint32Array(100)
|
||||||
// testing.expectEqual(tu32a, crypto.getRandomValues(tu32a))
|
testing.expectEqual(tu32a, crypto.getRandomValues(tu32a))
|
||||||
// isRandom(tu32a)
|
isRandom(tu32a)
|
||||||
|
|
||||||
// let ti32a = new Int32Array(100)
|
let ti32a = new Int32Array(100)
|
||||||
// testing.expectEqual(ti32a, crypto.getRandomValues(ti32a))
|
testing.expectEqual(ti32a, crypto.getRandomValues(ti32a))
|
||||||
// isRandom(ti32a)
|
isRandom(ti32a)
|
||||||
// }
|
}
|
||||||
|
|
||||||
// {
|
{
|
||||||
// let tu64a = new BigUint64Array(100)
|
let tu64a = new BigUint64Array(100)
|
||||||
// testing.expectEqual(tu64a, crypto.getRandomValues(tu64a))
|
testing.expectEqual(tu64a, crypto.getRandomValues(tu64a))
|
||||||
// isRandom(tu64a)
|
isRandom(tu64a)
|
||||||
|
|
||||||
// let ti64a = new BigInt64Array(100)
|
let ti64a = new BigInt64Array(100)
|
||||||
// testing.expectEqual(ti64a, crypto.getRandomValues(ti64a))
|
testing.expectEqual(ti64a, crypto.getRandomValues(ti64a))
|
||||||
// isRandom(ti64a)
|
isRandom(ti64a)
|
||||||
// }
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- <script id="randomUUID">
|
<script id="randomUUID">
|
||||||
const uuid = crypto.randomUUID();
|
const uuid = crypto.randomUUID();
|
||||||
testing.expectEqual('string', typeof uuid);
|
testing.expectEqual('string', typeof uuid);
|
||||||
testing.expectEqual(36, uuid.length);
|
testing.expectEqual(36, uuid.length);
|
||||||
const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
testing.expectEqual(true, regex.test(uuid));
|
testing.expectEqual(true, regex.test(uuid));
|
||||||
</script> -->
|
</script>
|
||||||
|
|
||||||
|
<script id=SubtleCrypto>
|
||||||
|
testing.expectEqual(true, crypto.subtle instanceof SubtleCrypto);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=sign-and-verify-hmac>
|
||||||
|
testing.async(async () => {
|
||||||
|
let key = await crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: "HMAC",
|
||||||
|
hash: { name: "SHA-512" },
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["sign", "verify"],
|
||||||
|
);
|
||||||
|
|
||||||
|
testing.expectEqual(true, key instanceof CryptoKey);
|
||||||
|
|
||||||
|
const raw = await crypto.subtle.exportKey("raw", key);
|
||||||
|
testing.expectEqual(128, raw.byteLength);
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
const signature = await crypto.subtle.sign(
|
||||||
|
"HMAC",
|
||||||
|
key,
|
||||||
|
encoder.encode("Hello, world!")
|
||||||
|
);
|
||||||
|
|
||||||
|
testing.expectEqual(true, signature instanceof ArrayBuffer);
|
||||||
|
|
||||||
|
const result = await window.crypto.subtle.verify(
|
||||||
|
{ name: "HMAC" },
|
||||||
|
key,
|
||||||
|
signature,
|
||||||
|
encoder.encode("Hello, world!")
|
||||||
|
);
|
||||||
|
|
||||||
|
testing.expectEqual(true, result);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=derive-shared-key-x25519>
|
||||||
|
testing.async(async () => {
|
||||||
|
const { privateKey, publicKey } = await crypto.subtle.generateKey(
|
||||||
|
{ name: "X25519" },
|
||||||
|
true,
|
||||||
|
["deriveBits"],
|
||||||
|
);
|
||||||
|
|
||||||
|
testing.expectEqual(true, privateKey instanceof CryptoKey);
|
||||||
|
testing.expectEqual(true, publicKey instanceof CryptoKey);
|
||||||
|
|
||||||
|
const sharedKey = await crypto.subtle.deriveBits(
|
||||||
|
{
|
||||||
|
name: "X25519",
|
||||||
|
public: publicKey,
|
||||||
|
},
|
||||||
|
privateKey,
|
||||||
|
128,
|
||||||
|
);
|
||||||
|
|
||||||
|
testing.expectEqual(16, sharedKey.byteLength);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="digest">
|
||||||
|
testing.async(async () => {
|
||||||
|
async function hash(algo, data) {
|
||||||
|
const buffer = await window.crypto.subtle.digest(algo, new TextEncoder().encode(data));
|
||||||
|
return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
testing.expectEqual("a6a1e3375239f215f09a156df29c17c7d1ac6722", await hash('sha-1', 'over 9000'));
|
||||||
|
testing.expectEqual("1bc375bb92459685194dda18a4b835f4e2972ec1bde6d9ab3db53fcc584a6580", await hash('sha-256', 'over 9000'));
|
||||||
|
testing.expectEqual("a4260d64c2eea9fd30c1f895c5e48a26d817e19d3a700b61b3ce665864ff4b8e012bd357d345aa614c5f642dab865ea1", await hash('sha-384', 'over 9000'));
|
||||||
|
testing.expectEqual("6cad17e6f3f76680d6dd18ed043b75b4f6e1aa1d08b917294942e882fb6466c3510948c34af8b903ed0725b582b3b39c0e485ae2c1b7dfdb192ee38b79c782b6", await hash('sha-512', 'over 9000'));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -20,8 +20,10 @@
|
|||||||
{
|
{
|
||||||
testing.expectEqual('\\30 abc', CSS.escape('0abc'));
|
testing.expectEqual('\\30 abc', CSS.escape('0abc'));
|
||||||
testing.expectEqual('\\31 23', CSS.escape('123'));
|
testing.expectEqual('\\31 23', CSS.escape('123'));
|
||||||
testing.expectEqual('\\-test', CSS.escape('-test'));
|
testing.expectEqual('\\-', CSS.escape('-'));
|
||||||
testing.expectEqual('\\--test', CSS.escape('--test'));
|
testing.expectEqual('-test', CSS.escape('-test'));
|
||||||
|
testing.expectEqual('--test', CSS.escape('--test'));
|
||||||
|
testing.expectEqual('-\\33 ', CSS.escape('-3'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -67,3 +69,11 @@
|
|||||||
testing.expectEqual(true, CSS.supports('z-index', '10'));
|
testing.expectEqual(true, CSS.supports('z-index', '10'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="escape_null_character">
|
||||||
|
{
|
||||||
|
testing.expectEqual('\uFFFD', CSS.escape('\x00'));
|
||||||
|
testing.expectEqual('test\uFFFDvalue', CSS.escape('test\x00value'));
|
||||||
|
testing.expectEqual('\uFFFDabc', CSS.escape('\x00abc'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
63
src/browser/tests/css/font_face.html
Normal file
63
src/browser/tests/css/font_face.html
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id="constructor_basic">
|
||||||
|
{
|
||||||
|
const face = new FontFace("TestFont", "url(test.woff)");
|
||||||
|
testing.expectTrue(face instanceof FontFace);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="constructor_name">
|
||||||
|
{
|
||||||
|
testing.expectEqual('FontFace', FontFace.name);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="family_property">
|
||||||
|
{
|
||||||
|
const face = new FontFace("MyFont", "url(font.woff2)");
|
||||||
|
testing.expectEqual("MyFont", face.family);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="status_is_loaded">
|
||||||
|
{
|
||||||
|
const face = new FontFace("F", "url(f.woff)");
|
||||||
|
testing.expectEqual("loaded", face.status);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="loaded_is_promise">
|
||||||
|
{
|
||||||
|
const face = new FontFace("F", "url(f.woff)");
|
||||||
|
testing.expectTrue(face.loaded instanceof Promise);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="load_returns_promise">
|
||||||
|
{
|
||||||
|
const face = new FontFace("F", "url(f.woff)");
|
||||||
|
testing.expectTrue(face.load() instanceof Promise);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="default_descriptors">
|
||||||
|
{
|
||||||
|
const face = new FontFace("F", "url(f.woff)");
|
||||||
|
testing.expectEqual("normal", face.style);
|
||||||
|
testing.expectEqual("normal", face.weight);
|
||||||
|
testing.expectEqual("normal", face.stretch);
|
||||||
|
testing.expectEqual("normal", face.variant);
|
||||||
|
testing.expectEqual("normal", face.featureSettings);
|
||||||
|
testing.expectEqual("auto", face.display);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_add">
|
||||||
|
{
|
||||||
|
const face = new FontFace("AddedFont", "url(added.woff)");
|
||||||
|
const result = document.fonts.add(face);
|
||||||
|
testing.expectTrue(result === document.fonts);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
80
src/browser/tests/css/font_face_set.html
Normal file
80
src/browser/tests/css/font_face_set.html
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id="document_fonts_exists">
|
||||||
|
{
|
||||||
|
testing.expectTrue(document.fonts !== undefined);
|
||||||
|
testing.expectTrue(document.fonts !== null);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_same_instance">
|
||||||
|
{
|
||||||
|
// Should return same instance each time
|
||||||
|
const f1 = document.fonts;
|
||||||
|
const f2 = document.fonts;
|
||||||
|
testing.expectTrue(f1 === f2);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_status">
|
||||||
|
{
|
||||||
|
testing.expectEqual('loaded', document.fonts.status);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_size">
|
||||||
|
{
|
||||||
|
testing.expectEqual(0, document.fonts.size);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_ready_is_promise">
|
||||||
|
{
|
||||||
|
const ready = document.fonts.ready;
|
||||||
|
testing.expectTrue(ready instanceof Promise);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_ready_resolves">
|
||||||
|
{
|
||||||
|
let resolved = false;
|
||||||
|
document.fonts.ready.then(() => { resolved = true; });
|
||||||
|
// Promise resolution is async; just confirm .then() does not throw
|
||||||
|
testing.expectTrue(typeof document.fonts.ready.then === 'function');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_check">
|
||||||
|
{
|
||||||
|
testing.expectTrue(document.fonts.check('16px sans-serif'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_constructor_name">
|
||||||
|
{
|
||||||
|
testing.expectEqual('FontFaceSet', document.fonts.constructor.name);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_addEventListener">
|
||||||
|
{
|
||||||
|
let loading = false;
|
||||||
|
document.fonts.addEventListener('loading', function() {
|
||||||
|
loading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
let loadingdone = false;
|
||||||
|
document.fonts.addEventListener('loadingdone', function() {
|
||||||
|
loadingdone = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.fonts.load("italic bold 16px Roboto");
|
||||||
|
|
||||||
|
testing.onload(() => {
|
||||||
|
testing.expectEqual(true, loading);
|
||||||
|
testing.expectEqual(true, loadingdone);
|
||||||
|
});
|
||||||
|
testing.expectEqual(true, true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -205,3 +205,331 @@
|
|||||||
testing.expectEqual('', style.getPropertyPriority('content'));
|
testing.expectEqual('', style.getPropertyPriority('content'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_style_syncs_to_attribute">
|
||||||
|
{
|
||||||
|
// JS style modifications must be reflected in getAttribute.
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
// Named property assignment (element.style.X = ...)
|
||||||
|
div.style.opacity = '0';
|
||||||
|
testing.expectEqual('opacity: 0;', div.getAttribute('style'));
|
||||||
|
|
||||||
|
// Update existing property
|
||||||
|
div.style.opacity = '1';
|
||||||
|
testing.expectEqual('opacity: 1;', div.getAttribute('style'));
|
||||||
|
|
||||||
|
// Add a second property
|
||||||
|
div.style.color = 'red';
|
||||||
|
testing.expectTrue(div.getAttribute('style').includes('opacity: 1'));
|
||||||
|
testing.expectTrue(div.getAttribute('style').includes('color: red'));
|
||||||
|
|
||||||
|
// removeProperty syncs back
|
||||||
|
div.style.removeProperty('opacity');
|
||||||
|
testing.expectTrue(!div.getAttribute('style').includes('opacity'));
|
||||||
|
testing.expectTrue(div.getAttribute('style').includes('color: red'));
|
||||||
|
|
||||||
|
// setCssText syncs back
|
||||||
|
div.style.cssText = 'filter: blur(0px)';
|
||||||
|
testing.expectEqual('filter: blur(0px);', div.getAttribute('style'));
|
||||||
|
|
||||||
|
// setCssText with empty string clears attribute
|
||||||
|
div.style.cssText = '';
|
||||||
|
testing.expectEqual('', div.getAttribute('style'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_outerHTML_reflects_style_changes">
|
||||||
|
{
|
||||||
|
// outerHTML must reflect JS-modified styles (regression test for
|
||||||
|
// DOM serialization reading stale HTML-parsed attribute values).
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.setAttribute('style', 'filter:blur(10px);opacity:0');
|
||||||
|
|
||||||
|
div.style.filter = 'blur(0px)';
|
||||||
|
div.style.opacity = '1';
|
||||||
|
|
||||||
|
const html = div.outerHTML;
|
||||||
|
testing.expectTrue(html.includes('filter: blur(0px)'));
|
||||||
|
testing.expectTrue(html.includes('opacity: 1'));
|
||||||
|
testing.expectTrue(!html.includes('blur(10px)'));
|
||||||
|
testing.expectTrue(!html.includes('opacity:0'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_non_ascii_custom_property">
|
||||||
|
{
|
||||||
|
// Regression test: accessing element.style must not crash when the inline
|
||||||
|
// style attribute contains CSS custom properties with non-ASCII (UTF-8
|
||||||
|
// multibyte) names, such as French accented characters.
|
||||||
|
// The CSS Tokenizer's consumeName() must advance over whole UTF-8 sequences
|
||||||
|
// rather than byte-by-byte to avoid landing on a continuation byte.
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.setAttribute('style',
|
||||||
|
'--color-store-bulles-\u00e9t\u00e9-fg: #6a818f;' +
|
||||||
|
'--color-store-soir\u00e9es-odl-fg: #56b3b3;' +
|
||||||
|
'color: red;'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Must not crash, and ASCII properties that follow non-ASCII ones must be readable.
|
||||||
|
testing.expectEqual('red', div.style.getPropertyValue('color'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_normalize_zero_to_0px">
|
||||||
|
{
|
||||||
|
// Per CSSOM spec, unitless zero in length properties should serialize as "0px"
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.style.width = '0';
|
||||||
|
testing.expectEqual('0px', div.style.width);
|
||||||
|
|
||||||
|
div.style.margin = '0';
|
||||||
|
testing.expectEqual('0px', div.style.margin);
|
||||||
|
|
||||||
|
div.style.padding = '0';
|
||||||
|
testing.expectEqual('0px', div.style.padding);
|
||||||
|
|
||||||
|
div.style.top = '0';
|
||||||
|
testing.expectEqual('0px', div.style.top);
|
||||||
|
|
||||||
|
// Scroll properties
|
||||||
|
div.style.scrollMarginTop = '0';
|
||||||
|
testing.expectEqual('0px', div.style.scrollMarginTop);
|
||||||
|
|
||||||
|
div.style.scrollPaddingBottom = '0';
|
||||||
|
testing.expectEqual('0px', div.style.scrollPaddingBottom);
|
||||||
|
|
||||||
|
// Multi-column
|
||||||
|
div.style.columnWidth = '0';
|
||||||
|
testing.expectEqual('0px', div.style.columnWidth);
|
||||||
|
|
||||||
|
div.style.columnRuleWidth = '0';
|
||||||
|
testing.expectEqual('0px', div.style.columnRuleWidth);
|
||||||
|
|
||||||
|
// Outline shorthand
|
||||||
|
div.style.outline = '0';
|
||||||
|
testing.expectEqual('0px', div.style.outline);
|
||||||
|
|
||||||
|
// Shapes
|
||||||
|
div.style.shapeMargin = '0';
|
||||||
|
testing.expectEqual('0px', div.style.shapeMargin);
|
||||||
|
|
||||||
|
// Non-length properties should not be affected
|
||||||
|
div.style.opacity = '0';
|
||||||
|
testing.expectEqual('0', div.style.opacity);
|
||||||
|
|
||||||
|
div.style.zIndex = '0';
|
||||||
|
testing.expectEqual('0', div.style.zIndex);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_normalize_first_baseline">
|
||||||
|
{
|
||||||
|
// "first baseline" should serialize canonically as "baseline"
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.style.alignItems = 'first baseline';
|
||||||
|
testing.expectEqual('baseline', div.style.alignItems);
|
||||||
|
|
||||||
|
div.style.alignContent = 'first baseline';
|
||||||
|
testing.expectEqual('baseline', div.style.alignContent);
|
||||||
|
|
||||||
|
div.style.alignSelf = 'first baseline';
|
||||||
|
testing.expectEqual('baseline', div.style.alignSelf);
|
||||||
|
|
||||||
|
div.style.justifySelf = 'first baseline';
|
||||||
|
testing.expectEqual('baseline', div.style.justifySelf);
|
||||||
|
|
||||||
|
// "last baseline" should remain unchanged
|
||||||
|
div.style.alignItems = 'last baseline';
|
||||||
|
testing.expectEqual('last baseline', div.style.alignItems);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_normalize_duplicate_values">
|
||||||
|
{
|
||||||
|
// For 2-value shorthand properties, "X X" should collapse to "X"
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.style.placeContent = 'center center';
|
||||||
|
testing.expectEqual('center', div.style.placeContent);
|
||||||
|
|
||||||
|
div.style.placeContent = 'start start';
|
||||||
|
testing.expectEqual('start', div.style.placeContent);
|
||||||
|
|
||||||
|
div.style.gap = '10px 10px';
|
||||||
|
testing.expectEqual('10px', div.style.gap);
|
||||||
|
|
||||||
|
// Different values should not collapse
|
||||||
|
div.style.placeContent = 'center start';
|
||||||
|
testing.expectEqual('center start', div.style.placeContent);
|
||||||
|
|
||||||
|
div.style.gap = '10px 20px';
|
||||||
|
testing.expectEqual('10px 20px', div.style.gap);
|
||||||
|
|
||||||
|
// New shorthands
|
||||||
|
div.style.overflow = 'hidden hidden';
|
||||||
|
testing.expectEqual('hidden', div.style.overflow);
|
||||||
|
|
||||||
|
div.style.scrollSnapAlign = 'start start';
|
||||||
|
testing.expectEqual('start', div.style.scrollSnapAlign);
|
||||||
|
|
||||||
|
div.style.overscrollBehavior = 'auto auto';
|
||||||
|
testing.expectEqual('auto', div.style.overscrollBehavior);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_normalize_anchor_size">
|
||||||
|
{
|
||||||
|
// anchor-size() should serialize with dashed ident (anchor name) before size keyword
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
// Already canonical order - should stay the same
|
||||||
|
div.style.width = 'anchor-size(--foo width)';
|
||||||
|
testing.expectEqual('anchor-size(--foo width)', div.style.width);
|
||||||
|
|
||||||
|
// Non-canonical order - should be reordered
|
||||||
|
div.style.width = 'anchor-size(width --foo)';
|
||||||
|
testing.expectEqual('anchor-size(--foo width)', div.style.width);
|
||||||
|
|
||||||
|
// With fallback value
|
||||||
|
div.style.width = 'anchor-size(height --bar, 100px)';
|
||||||
|
testing.expectEqual('anchor-size(--bar height, 100px)', div.style.width);
|
||||||
|
|
||||||
|
// Different size keywords
|
||||||
|
div.style.width = 'anchor-size(block --baz)';
|
||||||
|
testing.expectEqual('anchor-size(--baz block)', div.style.width);
|
||||||
|
|
||||||
|
div.style.width = 'anchor-size(inline --qux)';
|
||||||
|
testing.expectEqual('anchor-size(--qux inline)', div.style.width);
|
||||||
|
|
||||||
|
div.style.width = 'anchor-size(self-block --test)';
|
||||||
|
testing.expectEqual('anchor-size(--test self-block)', div.style.width);
|
||||||
|
|
||||||
|
div.style.width = 'anchor-size(self-inline --test)';
|
||||||
|
testing.expectEqual('anchor-size(--test self-inline)', div.style.width);
|
||||||
|
|
||||||
|
// Without anchor name (implicit default anchor)
|
||||||
|
div.style.width = 'anchor-size(width)';
|
||||||
|
testing.expectEqual('anchor-size(width)', div.style.width);
|
||||||
|
|
||||||
|
// Nested anchor-size in fallback
|
||||||
|
div.style.width = 'anchor-size(width --foo, anchor-size(height --bar))';
|
||||||
|
testing.expectEqual('anchor-size(--foo width, anchor-size(--bar height))', div.style.width);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleSheet_insertRule_deleteRule">
|
||||||
|
{
|
||||||
|
const style = document.createElement('style');
|
||||||
|
document.head.appendChild(style);
|
||||||
|
const sheet = style.sheet;
|
||||||
|
|
||||||
|
testing.expectEqual(0, sheet.cssRules.length);
|
||||||
|
|
||||||
|
sheet.insertRule('.test { color: green; }', 0);
|
||||||
|
testing.expectEqual(1, sheet.cssRules.length);
|
||||||
|
testing.expectEqual('.test', sheet.cssRules[0].selectorText);
|
||||||
|
testing.expectEqual('green', sheet.cssRules[0].style.color);
|
||||||
|
|
||||||
|
sheet.deleteRule(0);
|
||||||
|
testing.expectEqual(0, sheet.cssRules.length);
|
||||||
|
|
||||||
|
let caught = false;
|
||||||
|
try {
|
||||||
|
sheet.deleteRule(5);
|
||||||
|
} catch (e) {
|
||||||
|
caught = true;
|
||||||
|
testing.expectEqual('IndexSizeError', e.name);
|
||||||
|
}
|
||||||
|
testing.expectTrue(caught);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleSheet_insertRule_default_index">
|
||||||
|
{
|
||||||
|
const style = document.createElement('style');
|
||||||
|
document.head.appendChild(style);
|
||||||
|
const sheet = style.sheet;
|
||||||
|
|
||||||
|
testing.expectEqual(0, sheet.cssRules.length);
|
||||||
|
|
||||||
|
// Call without index, should default to 0
|
||||||
|
sheet.insertRule('.test-default { color: blue; }');
|
||||||
|
testing.expectEqual(1, sheet.cssRules.length);
|
||||||
|
testing.expectEqual('.test-default', sheet.cssRules[0].selectorText);
|
||||||
|
|
||||||
|
// Insert another rule without index, should default to 0 and push the first one to index 1
|
||||||
|
sheet.insertRule('.test-at-0 { color: red; }');
|
||||||
|
testing.expectEqual(2, sheet.cssRules.length);
|
||||||
|
testing.expectEqual('.test-at-0', sheet.cssRules[0].selectorText);
|
||||||
|
testing.expectEqual('.test-default', sheet.cssRules[1].selectorText);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleSheet_insertRule_semicolon">
|
||||||
|
{
|
||||||
|
const style = document.createElement('style');
|
||||||
|
document.head.appendChild(style);
|
||||||
|
const sheet = style.sheet;
|
||||||
|
|
||||||
|
// Should not throw even with trailing semicolon
|
||||||
|
sheet.insertRule('*{};');
|
||||||
|
testing.expectEqual(1, sheet.cssRules.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleSheet_insertRule_multiple_rules">
|
||||||
|
{
|
||||||
|
const style = document.createElement('style');
|
||||||
|
document.head.appendChild(style);
|
||||||
|
const sheet = style.sheet;
|
||||||
|
|
||||||
|
let caught = false;
|
||||||
|
try {
|
||||||
|
sheet.insertRule('a { color: red; } b { color: blue; }');
|
||||||
|
} catch (e) {
|
||||||
|
caught = true;
|
||||||
|
testing.expectEqual('SyntaxError', e.name);
|
||||||
|
}
|
||||||
|
testing.expectTrue(caught);
|
||||||
|
testing.expectEqual(0, sheet.cssRules.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleSheet_replaceSync">
|
||||||
|
{
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
testing.expectEqual(0, sheet.cssRules.length);
|
||||||
|
|
||||||
|
sheet.replaceSync('.test { color: blue; }');
|
||||||
|
testing.expectEqual(1, sheet.cssRules.length);
|
||||||
|
testing.expectEqual('.test', sheet.cssRules[0].selectorText);
|
||||||
|
testing.expectEqual('blue', sheet.cssRules[0].style.color);
|
||||||
|
|
||||||
|
let replacedAsync = false;
|
||||||
|
testing.async(async () => {
|
||||||
|
const result = await sheet.replace('.async-test { margin: 10px; }');
|
||||||
|
testing.expectTrue(result === sheet);
|
||||||
|
testing.expectEqual(1, sheet.cssRules.length);
|
||||||
|
testing.expectEqual('.async-test', sheet.cssRules[0].selectorText);
|
||||||
|
replacedAsync = true;
|
||||||
|
});
|
||||||
|
testing.onload(() => testing.expectTrue(replacedAsync));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleRule_cssText">
|
||||||
|
{
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
sheet.replaceSync('.test { color: red; margin: 10px; }');
|
||||||
|
|
||||||
|
// Check serialization format
|
||||||
|
const cssText = sheet.cssRules[0].cssText;
|
||||||
|
testing.expectTrue(cssText.includes('.test { '));
|
||||||
|
testing.expectTrue(cssText.includes('color: red;'));
|
||||||
|
testing.expectTrue(cssText.includes('margin: 10px;'));
|
||||||
|
testing.expectTrue(cssText.includes('}'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -53,3 +53,78 @@
|
|||||||
testing.expectEqual('NO-CONSTRUCTOR-ELEMENT', el.tagName);
|
testing.expectEqual('NO-CONSTRUCTOR-ELEMENT', el.tagName);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div id=clone_container></div>
|
||||||
|
|
||||||
|
<script id=clone>
|
||||||
|
{
|
||||||
|
let calls = 0;
|
||||||
|
class MyCloneElementA extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
calls += 1;
|
||||||
|
$('#clone_container').appendChild(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('my-clone_element_a', MyCloneElementA);
|
||||||
|
const original = document.createElement('my-clone_element_a');
|
||||||
|
$('#clone_container').cloneNode(true);
|
||||||
|
testing.expectEqual(2, calls);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id=fragment_clone_container></div>
|
||||||
|
|
||||||
|
<script id=clone_fragment>
|
||||||
|
{
|
||||||
|
let calls = 0;
|
||||||
|
class MyFragmentCloneElement extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
calls += 1;
|
||||||
|
$('#fragment_clone_container').appendChild(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('my-fragment-clone-element', MyFragmentCloneElement);
|
||||||
|
|
||||||
|
// Create a DocumentFragment with a custom element
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const customEl = document.createElement('my-fragment-clone-element');
|
||||||
|
fragment.appendChild(customEl);
|
||||||
|
|
||||||
|
// Clone the fragment - this should trigger the crash
|
||||||
|
// because the constructor will attach the element during cloning
|
||||||
|
const clonedFragment = fragment.cloneNode(true);
|
||||||
|
testing.expectEqual(2, calls);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id=range_clone_container></div>
|
||||||
|
|
||||||
|
<script id=clone_range>
|
||||||
|
{
|
||||||
|
let calls = 0;
|
||||||
|
class MyRangeCloneElement extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
calls += 1;
|
||||||
|
$('#range_clone_container').appendChild(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('my-range-clone-element', MyRangeCloneElement);
|
||||||
|
|
||||||
|
// Create a container with a custom element
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const customEl = document.createElement('my-range-clone-element');
|
||||||
|
container.appendChild(customEl);
|
||||||
|
|
||||||
|
// Create a range that includes the custom element
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(container);
|
||||||
|
|
||||||
|
// Clone the range contents - this should trigger the crash
|
||||||
|
// because the constructor will attach the element during cloning
|
||||||
|
const clonedContents = range.cloneContents();
|
||||||
|
testing.expectEqual(2, calls);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -119,3 +119,33 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="constructor_self_insert_foster_parent">
|
||||||
|
{
|
||||||
|
// Regression: custom element constructor inserting itself (via appendChild) during
|
||||||
|
// innerHTML parsing. When the element is not valid table content, the HTML5 parser
|
||||||
|
// foster-parents it before the <table> via appendBeforeSiblingCallback. That callback
|
||||||
|
// previously didn't check for an existing _parent before calling insertNodeRelative,
|
||||||
|
// causing the "Page.insertNodeRelative parent" assertion to fire.
|
||||||
|
let constructorCalled = 0;
|
||||||
|
let container;
|
||||||
|
|
||||||
|
class CtorSelfInsert extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
constructorCalled++;
|
||||||
|
// Insert self into container so _parent is set before the parser
|
||||||
|
// officially places this element via appendBeforeSiblingCallback.
|
||||||
|
if (container) container.appendChild(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('ctor-self-insert', CtorSelfInsert);
|
||||||
|
|
||||||
|
container = document.createElement('div');
|
||||||
|
// ctor-self-insert is not valid table content; the parser foster-parents it
|
||||||
|
// before the <table>, calling appendBeforeSiblingCallback(sibling=table, node=element).
|
||||||
|
// At that point the element already has _parent=container from the constructor.
|
||||||
|
container.innerHTML = '<table><ctor-self-insert></ctor-self-insert></table>';
|
||||||
|
|
||||||
|
testing.expectEqual(1, constructorCalled);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script>
|
||||||
|
// Test that document.open/write/close throw InvalidStateError during custom element
|
||||||
|
// reactions when the element is parsed from HTML
|
||||||
|
|
||||||
|
window.constructorOpenException = null;
|
||||||
|
window.constructorWriteException = null;
|
||||||
|
window.constructorCloseException = null;
|
||||||
|
window.constructorCalled = false;
|
||||||
|
|
||||||
|
class ThrowTestElement extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
window.constructorCalled = true;
|
||||||
|
|
||||||
|
// Try document.open on the same document during constructor - should throw
|
||||||
|
try {
|
||||||
|
document.open();
|
||||||
|
} catch (e) {
|
||||||
|
window.constructorOpenException = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try document.write on the same document during constructor - should throw
|
||||||
|
try {
|
||||||
|
document.write('<b>test</b>');
|
||||||
|
} catch (e) {
|
||||||
|
window.constructorWriteException = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try document.close on the same document during constructor - should throw
|
||||||
|
try {
|
||||||
|
document.close();
|
||||||
|
} catch (e) {
|
||||||
|
window.constructorCloseException = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('throw-test-element', ThrowTestElement);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- This element will be parsed from HTML, triggering the constructor -->
|
||||||
|
<throw-test-element id="test-element"></throw-test-element>
|
||||||
|
|
||||||
|
<script id="verify_throws">
|
||||||
|
{
|
||||||
|
// Verify the constructor was called
|
||||||
|
testing.expectEqual(true, window.constructorCalled);
|
||||||
|
|
||||||
|
// Verify document.open threw InvalidStateError
|
||||||
|
testing.expectEqual(true, window.constructorOpenException !== null);
|
||||||
|
testing.expectEqual('InvalidStateError', window.constructorOpenException.name);
|
||||||
|
|
||||||
|
// Verify document.write threw InvalidStateError
|
||||||
|
testing.expectEqual(true, window.constructorWriteException !== null);
|
||||||
|
testing.expectEqual('InvalidStateError', window.constructorWriteException.name);
|
||||||
|
|
||||||
|
// Verify document.close threw InvalidStateError
|
||||||
|
testing.expectEqual(true, window.constructorCloseException !== null);
|
||||||
|
testing.expectEqual('InvalidStateError', window.constructorCloseException.name);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
@@ -27,329 +27,329 @@
|
|||||||
customElements.define('my-early', MyEarly);
|
customElements.define('my-early', MyEarly);
|
||||||
testing.expectEqual(true, early.upgraded);
|
testing.expectEqual(true, early.upgraded);
|
||||||
testing.expectEqual(1, constructorCalled);
|
testing.expectEqual(1, constructorCalled);
|
||||||
testing.expectEqual(1, connectedCalled);
|
// testing.expectEqual(1, connectedCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
// {
|
||||||
let order = [];
|
// let order = [];
|
||||||
|
|
||||||
class UpgradeParent extends HTMLElement {
|
// class UpgradeParent extends HTMLElement {
|
||||||
constructor() {
|
// constructor() {
|
||||||
super();
|
// super();
|
||||||
order.push('parent-constructor');
|
// order.push('parent-constructor');
|
||||||
}
|
// }
|
||||||
|
|
||||||
connectedCallback() {
|
// connectedCallback() {
|
||||||
order.push('parent-connected');
|
// order.push('parent-connected');
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
class UpgradeChild extends HTMLElement {
|
// class UpgradeChild extends HTMLElement {
|
||||||
constructor() {
|
// constructor() {
|
||||||
super();
|
// super();
|
||||||
order.push('child-constructor');
|
// order.push('child-constructor');
|
||||||
}
|
// }
|
||||||
|
|
||||||
connectedCallback() {
|
// connectedCallback() {
|
||||||
order.push('child-connected');
|
// order.push('child-connected');
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<upgrade-parent><upgrade-child></upgrade-child></upgrade-parent>';
|
// container.innerHTML = '<upgrade-parent><upgrade-child></upgrade-child></upgrade-parent>';
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
|
|
||||||
testing.expectEqual(0, order.length);
|
// testing.expectEqual(0, order.length);
|
||||||
|
|
||||||
customElements.define('upgrade-parent', UpgradeParent);
|
// customElements.define('upgrade-parent', UpgradeParent);
|
||||||
testing.expectEqual(2, order.length);
|
// testing.expectEqual(2, order.length);
|
||||||
testing.expectEqual('parent-constructor', order[0]);
|
// testing.expectEqual('parent-constructor', order[0]);
|
||||||
testing.expectEqual('parent-connected', order[1]);
|
// testing.expectEqual('parent-connected', order[1]);
|
||||||
|
|
||||||
customElements.define('upgrade-child', UpgradeChild);
|
// customElements.define('upgrade-child', UpgradeChild);
|
||||||
testing.expectEqual(4, order.length);
|
// testing.expectEqual(4, order.length);
|
||||||
testing.expectEqual('child-constructor', order[2]);
|
// testing.expectEqual('child-constructor', order[2]);
|
||||||
testing.expectEqual('child-connected', order[3]);
|
// testing.expectEqual('child-connected', order[3]);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let connectedCalled = 0;
|
// let connectedCalled = 0;
|
||||||
|
|
||||||
class DetachedUpgrade extends HTMLElement {
|
// class DetachedUpgrade extends HTMLElement {
|
||||||
connectedCallback() {
|
// connectedCallback() {
|
||||||
connectedCalled++;
|
// connectedCalled++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<detached-upgrade></detached-upgrade>';
|
// container.innerHTML = '<detached-upgrade></detached-upgrade>';
|
||||||
|
|
||||||
testing.expectEqual(0, connectedCalled);
|
// testing.expectEqual(0, connectedCalled);
|
||||||
|
|
||||||
customElements.define('detached-upgrade', DetachedUpgrade);
|
// customElements.define('detached-upgrade', DetachedUpgrade);
|
||||||
testing.expectEqual(0, connectedCalled);
|
// testing.expectEqual(0, connectedCalled);
|
||||||
|
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
testing.expectEqual(1, connectedCalled);
|
// testing.expectEqual(1, connectedCalled);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let constructorCalled = 0;
|
// let constructorCalled = 0;
|
||||||
let connectedCalled = 0;
|
// let connectedCalled = 0;
|
||||||
|
|
||||||
class ManualUpgrade extends HTMLElement {
|
// class ManualUpgrade extends HTMLElement {
|
||||||
constructor() {
|
// constructor() {
|
||||||
super();
|
// super();
|
||||||
constructorCalled++;
|
// constructorCalled++;
|
||||||
this.manuallyUpgraded = true;
|
// this.manuallyUpgraded = true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
connectedCallback() {
|
// connectedCallback() {
|
||||||
connectedCalled++;
|
// connectedCalled++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
customElements.define('manual-upgrade', ManualUpgrade);
|
// customElements.define('manual-upgrade', ManualUpgrade);
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<manual-upgrade id="m1"><manual-upgrade id="m2"></manual-upgrade></manual-upgrade>';
|
// container.innerHTML = '<manual-upgrade id="m1"><manual-upgrade id="m2"></manual-upgrade></manual-upgrade>';
|
||||||
|
|
||||||
testing.expectEqual(2, constructorCalled);
|
// testing.expectEqual(2, constructorCalled);
|
||||||
testing.expectEqual(0, connectedCalled);
|
// testing.expectEqual(0, connectedCalled);
|
||||||
|
|
||||||
customElements.upgrade(container);
|
// customElements.upgrade(container);
|
||||||
|
|
||||||
testing.expectEqual(2, constructorCalled);
|
// testing.expectEqual(2, constructorCalled);
|
||||||
testing.expectEqual(0, connectedCalled);
|
// testing.expectEqual(0, connectedCalled);
|
||||||
|
|
||||||
const m1 = container.querySelector('#m1');
|
// const m1 = container.querySelector('#m1');
|
||||||
const m2 = container.querySelector('#m2');
|
// const m2 = container.querySelector('#m2');
|
||||||
testing.expectEqual(true, m1.manuallyUpgraded);
|
// testing.expectEqual(true, m1.manuallyUpgraded);
|
||||||
testing.expectEqual(true, m2.manuallyUpgraded);
|
// testing.expectEqual(true, m2.manuallyUpgraded);
|
||||||
|
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
testing.expectEqual(2, connectedCalled);
|
// testing.expectEqual(2, connectedCalled);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let alreadyUpgradedCalled = 0;
|
// let alreadyUpgradedCalled = 0;
|
||||||
|
|
||||||
class AlreadyUpgraded extends HTMLElement {
|
// class AlreadyUpgraded extends HTMLElement {
|
||||||
constructor() {
|
// constructor() {
|
||||||
super();
|
// super();
|
||||||
alreadyUpgradedCalled++;
|
// alreadyUpgradedCalled++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const elem = document.createElement('div');
|
// const elem = document.createElement('div');
|
||||||
elem.innerHTML = '<already-upgraded></already-upgraded>';
|
// elem.innerHTML = '<already-upgraded></already-upgraded>';
|
||||||
document.body.appendChild(elem);
|
// document.body.appendChild(elem);
|
||||||
|
|
||||||
customElements.define('already-upgraded', AlreadyUpgraded);
|
// customElements.define('already-upgraded', AlreadyUpgraded);
|
||||||
testing.expectEqual(1, alreadyUpgradedCalled);
|
// testing.expectEqual(1, alreadyUpgradedCalled);
|
||||||
|
|
||||||
customElements.upgrade(elem);
|
// customElements.upgrade(elem);
|
||||||
testing.expectEqual(1, alreadyUpgradedCalled);
|
// testing.expectEqual(1, alreadyUpgradedCalled);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let attributeChangedCalls = [];
|
// let attributeChangedCalls = [];
|
||||||
|
|
||||||
class UpgradeWithAttrs extends HTMLElement {
|
// class UpgradeWithAttrs extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['data-foo', 'data-bar'];
|
// return ['data-foo', 'data-bar'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<upgrade-with-attrs data-foo="hello" data-bar="world"></upgrade-with-attrs>';
|
// container.innerHTML = '<upgrade-with-attrs data-foo="hello" data-bar="world"></upgrade-with-attrs>';
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
|
|
||||||
testing.expectEqual(0, attributeChangedCalls.length);
|
// testing.expectEqual(0, attributeChangedCalls.length);
|
||||||
|
|
||||||
customElements.define('upgrade-with-attrs', UpgradeWithAttrs);
|
// customElements.define('upgrade-with-attrs', UpgradeWithAttrs);
|
||||||
|
|
||||||
testing.expectEqual(2, attributeChangedCalls.length);
|
// testing.expectEqual(2, attributeChangedCalls.length);
|
||||||
testing.expectEqual('data-foo', attributeChangedCalls[0].name);
|
// testing.expectEqual('data-foo', attributeChangedCalls[0].name);
|
||||||
testing.expectEqual(null, attributeChangedCalls[0].oldValue);
|
// testing.expectEqual(null, attributeChangedCalls[0].oldValue);
|
||||||
testing.expectEqual('hello', attributeChangedCalls[0].newValue);
|
// testing.expectEqual('hello', attributeChangedCalls[0].newValue);
|
||||||
testing.expectEqual('data-bar', attributeChangedCalls[1].name);
|
// testing.expectEqual('data-bar', attributeChangedCalls[1].name);
|
||||||
testing.expectEqual(null, attributeChangedCalls[1].oldValue);
|
// testing.expectEqual(null, attributeChangedCalls[1].oldValue);
|
||||||
testing.expectEqual('world', attributeChangedCalls[1].newValue);
|
// testing.expectEqual('world', attributeChangedCalls[1].newValue);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let attributeChangedCalls = [];
|
// let attributeChangedCalls = [];
|
||||||
let connectedCalls = 0;
|
// let connectedCalls = 0;
|
||||||
|
|
||||||
class DetachedWithAttrs extends HTMLElement {
|
// class DetachedWithAttrs extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['foo'];
|
// return ['foo'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
|
|
||||||
connectedCallback() {
|
// connectedCallback() {
|
||||||
connectedCalls++;
|
// connectedCalls++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<detached-with-attrs foo="bar"></detached-with-attrs>';
|
// container.innerHTML = '<detached-with-attrs foo="bar"></detached-with-attrs>';
|
||||||
|
|
||||||
testing.expectEqual(0, attributeChangedCalls.length);
|
// testing.expectEqual(0, attributeChangedCalls.length);
|
||||||
|
|
||||||
customElements.define('detached-with-attrs', DetachedWithAttrs);
|
// customElements.define('detached-with-attrs', DetachedWithAttrs);
|
||||||
|
|
||||||
testing.expectEqual(0, attributeChangedCalls.length);
|
// testing.expectEqual(0, attributeChangedCalls.length);
|
||||||
testing.expectEqual(0, connectedCalls);
|
// testing.expectEqual(0, connectedCalls);
|
||||||
|
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
|
|
||||||
testing.expectEqual(1, attributeChangedCalls.length);
|
// testing.expectEqual(1, attributeChangedCalls.length);
|
||||||
testing.expectEqual('foo', attributeChangedCalls[0].name);
|
// testing.expectEqual('foo', attributeChangedCalls[0].name);
|
||||||
testing.expectEqual(null, attributeChangedCalls[0].oldValue);
|
// testing.expectEqual(null, attributeChangedCalls[0].oldValue);
|
||||||
testing.expectEqual('bar', attributeChangedCalls[0].newValue);
|
// testing.expectEqual('bar', attributeChangedCalls[0].newValue);
|
||||||
testing.expectEqual(1, connectedCalls);
|
// testing.expectEqual(1, connectedCalls);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let attributeChangedCalls = [];
|
// let attributeChangedCalls = [];
|
||||||
let constructorCalled = 0;
|
// let constructorCalled = 0;
|
||||||
|
|
||||||
class ManualUpgradeWithAttrs extends HTMLElement {
|
// class ManualUpgradeWithAttrs extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['x', 'y'];
|
// return ['x', 'y'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
constructor() {
|
// constructor() {
|
||||||
super();
|
// super();
|
||||||
constructorCalled++;
|
// constructorCalled++;
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
customElements.define('manual-upgrade-with-attrs', ManualUpgradeWithAttrs);
|
// customElements.define('manual-upgrade-with-attrs', ManualUpgradeWithAttrs);
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<manual-upgrade-with-attrs x="1" y="2"></manual-upgrade-with-attrs>';
|
// container.innerHTML = '<manual-upgrade-with-attrs x="1" y="2"></manual-upgrade-with-attrs>';
|
||||||
|
|
||||||
testing.expectEqual(1, constructorCalled);
|
// testing.expectEqual(1, constructorCalled);
|
||||||
testing.expectEqual(2, attributeChangedCalls.length);
|
// testing.expectEqual(2, attributeChangedCalls.length);
|
||||||
|
|
||||||
const elem = container.querySelector('manual-upgrade-with-attrs');
|
// const elem = container.querySelector('manual-upgrade-with-attrs');
|
||||||
elem.setAttribute('z', '3');
|
// elem.setAttribute('z', '3');
|
||||||
|
|
||||||
customElements.upgrade(container);
|
// customElements.upgrade(container);
|
||||||
|
|
||||||
testing.expectEqual(1, constructorCalled);
|
// testing.expectEqual(1, constructorCalled);
|
||||||
testing.expectEqual(2, attributeChangedCalls.length);
|
// testing.expectEqual(2, attributeChangedCalls.length);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let attributeChangedCalls = [];
|
// let attributeChangedCalls = [];
|
||||||
|
|
||||||
class MixedAttrs extends HTMLElement {
|
// class MixedAttrs extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['watched'];
|
// return ['watched'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<mixed-attrs watched="yes" ignored="no" also-ignored="maybe"></mixed-attrs>';
|
// container.innerHTML = '<mixed-attrs watched="yes" ignored="no" also-ignored="maybe"></mixed-attrs>';
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
|
|
||||||
testing.expectEqual(0, attributeChangedCalls.length);
|
// testing.expectEqual(0, attributeChangedCalls.length);
|
||||||
|
|
||||||
customElements.define('mixed-attrs', MixedAttrs);
|
// customElements.define('mixed-attrs', MixedAttrs);
|
||||||
|
|
||||||
testing.expectEqual(1, attributeChangedCalls.length);
|
// testing.expectEqual(1, attributeChangedCalls.length);
|
||||||
testing.expectEqual('watched', attributeChangedCalls[0].name);
|
// testing.expectEqual('watched', attributeChangedCalls[0].name);
|
||||||
testing.expectEqual('yes', attributeChangedCalls[0].newValue);
|
// testing.expectEqual('yes', attributeChangedCalls[0].newValue);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let attributeChangedCalls = [];
|
// let attributeChangedCalls = [];
|
||||||
|
|
||||||
class EmptyAttr extends HTMLElement {
|
// class EmptyAttr extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['empty', 'non-empty'];
|
// return ['empty', 'non-empty'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<empty-attr empty="" non-empty="value"></empty-attr>';
|
// container.innerHTML = '<empty-attr empty="" non-empty="value"></empty-attr>';
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
|
|
||||||
customElements.define('empty-attr', EmptyAttr);
|
// customElements.define('empty-attr', EmptyAttr);
|
||||||
|
|
||||||
testing.expectEqual(2, attributeChangedCalls.length);
|
// testing.expectEqual(2, attributeChangedCalls.length);
|
||||||
testing.expectEqual('empty', attributeChangedCalls[0].name);
|
// testing.expectEqual('empty', attributeChangedCalls[0].name);
|
||||||
testing.expectEqual('', attributeChangedCalls[0].newValue);
|
// testing.expectEqual('', attributeChangedCalls[0].newValue);
|
||||||
testing.expectEqual('non-empty', attributeChangedCalls[1].name);
|
// testing.expectEqual('non-empty', attributeChangedCalls[1].name);
|
||||||
testing.expectEqual('value', attributeChangedCalls[1].newValue);
|
// testing.expectEqual('value', attributeChangedCalls[1].newValue);
|
||||||
}
|
// }
|
||||||
|
|
||||||
{
|
// {
|
||||||
let parentCalls = [];
|
// let parentCalls = [];
|
||||||
let childCalls = [];
|
// let childCalls = [];
|
||||||
|
|
||||||
class NestedParent extends HTMLElement {
|
// class NestedParent extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['parent-attr'];
|
// return ['parent-attr'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
parentCalls.push({ name, oldValue, newValue });
|
// parentCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
class NestedChild extends HTMLElement {
|
// class NestedChild extends HTMLElement {
|
||||||
static get observedAttributes() {
|
// static get observedAttributes() {
|
||||||
return ['child-attr'];
|
// return ['child-attr'];
|
||||||
}
|
// }
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
// attributeChangedCallback(name, oldValue, newValue) {
|
||||||
childCalls.push({ name, oldValue, newValue });
|
// childCalls.push({ name, oldValue, newValue });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const container = document.createElement('div');
|
// const container = document.createElement('div');
|
||||||
container.innerHTML = '<nested-parent parent-attr="p"><nested-child child-attr="c"></nested-child></nested-parent>';
|
// container.innerHTML = '<nested-parent parent-attr="p"><nested-child child-attr="c"></nested-child></nested-parent>';
|
||||||
document.body.appendChild(container);
|
// document.body.appendChild(container);
|
||||||
|
|
||||||
testing.expectEqual(0, parentCalls.length);
|
// testing.expectEqual(0, parentCalls.length);
|
||||||
testing.expectEqual(0, childCalls.length);
|
// testing.expectEqual(0, childCalls.length);
|
||||||
|
|
||||||
customElements.define('nested-parent', NestedParent);
|
// customElements.define('nested-parent', NestedParent);
|
||||||
|
|
||||||
testing.expectEqual(1, parentCalls.length);
|
// testing.expectEqual(1, parentCalls.length);
|
||||||
testing.expectEqual('parent-attr', parentCalls[0].name);
|
// testing.expectEqual('parent-attr', parentCalls[0].name);
|
||||||
testing.expectEqual('p', parentCalls[0].newValue);
|
// testing.expectEqual('p', parentCalls[0].newValue);
|
||||||
testing.expectEqual(0, childCalls.length);
|
// testing.expectEqual(0, childCalls.length);
|
||||||
|
|
||||||
customElements.define('nested-child', NestedChild);
|
// customElements.define('nested-child', NestedChild);
|
||||||
|
|
||||||
testing.expectEqual(1, parentCalls.length);
|
// testing.expectEqual(1, parentCalls.length);
|
||||||
testing.expectEqual(1, childCalls.length);
|
// testing.expectEqual(1, childCalls.length);
|
||||||
testing.expectEqual('child-attr', childCalls[0].name);
|
// testing.expectEqual('child-attr', childCalls[0].name);
|
||||||
testing.expectEqual('c', childCalls[0].newValue);
|
// testing.expectEqual('c', childCalls[0].newValue);
|
||||||
}
|
// }
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
<body></body>
|
<body></body>
|
||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
<script id=createElement>
|
<script id=createElement>
|
||||||
const div = document.createElement('div');
|
testing.expectEqual(1, document.createElement.length);
|
||||||
testing.expectEqual("DIV", div.tagName);
|
|
||||||
div.id = "hello";
|
const div1 = document.createElement('div');
|
||||||
|
testing.expectEqual(true, div1 instanceof HTMLDivElement);
|
||||||
|
testing.expectEqual("DIV", div1.tagName);
|
||||||
|
div1.id = "hello";
|
||||||
testing.expectEqual(null, $('#hello'));
|
testing.expectEqual(null, $('#hello'));
|
||||||
|
|
||||||
document.getElementsByTagName('body')[0].appendChild(div);
|
const div2 = document.createElement('DIV');
|
||||||
testing.expectEqual(div, $('#hello'));
|
testing.expectEqual(true, div2 instanceof HTMLDivElement);
|
||||||
|
|
||||||
|
document.getElementsByTagName('body')[0].appendChild(div1);
|
||||||
|
testing.expectEqual(div1, $('#hello'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,17 @@
|
|||||||
<body></body>
|
<body></body>
|
||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
<script id=createElementNS>
|
<script id=createElementNS>
|
||||||
const htmlDiv = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
const htmlDiv1 = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
||||||
testing.expectEqual('DIV', htmlDiv.tagName);
|
testing.expectEqual('DIV', htmlDiv1.tagName);
|
||||||
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv.namespaceURI);
|
testing.expectEqual(true, htmlDiv1 instanceof HTMLDivElement);
|
||||||
|
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv1.namespaceURI);
|
||||||
|
|
||||||
|
// Per spec, createElementNS does NOT lowercase — 'DIV' != 'div', so this
|
||||||
|
// creates an HTMLUnknownElement, not an HTMLDivElement.
|
||||||
|
const htmlDiv2 = document.createElementNS('http://www.w3.org/1999/xhtml', 'DIV');
|
||||||
|
testing.expectEqual('DIV', htmlDiv2.tagName);
|
||||||
|
testing.expectEqual(false, htmlDiv2 instanceof HTMLDivElement);
|
||||||
|
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv2.namespaceURI);
|
||||||
|
|
||||||
const svgRect = document.createElementNS('http://www.w3.org/2000/svg', 'RecT');
|
const svgRect = document.createElementNS('http://www.w3.org/2000/svg', 'RecT');
|
||||||
testing.expectEqual('RecT', svgRect.tagName);
|
testing.expectEqual('RecT', svgRect.tagName);
|
||||||
@@ -19,12 +27,13 @@
|
|||||||
testing.expectEqual('http://www.w3.org/XML/1998/namespace', xmlElement.namespaceURI);
|
testing.expectEqual('http://www.w3.org/XML/1998/namespace', xmlElement.namespaceURI);
|
||||||
|
|
||||||
const nullNsElement = document.createElementNS(null, 'span');
|
const nullNsElement = document.createElementNS(null, 'span');
|
||||||
testing.expectEqual('SPAN', nullNsElement.tagName);
|
testing.expectEqual('span', nullNsElement.tagName);
|
||||||
testing.expectEqual('http://www.w3.org/1999/xhtml', nullNsElement.namespaceURI);
|
testing.expectEqual(null, nullNsElement.namespaceURI);
|
||||||
|
|
||||||
const unknownNsElement = document.createElementNS('http://example.com/unknown', 'custom');
|
const unknownNsElement = document.createElementNS('http://example.com/unknown', 'custom');
|
||||||
testing.expectEqual('CUSTOM', unknownNsElement.tagName);
|
testing.expectEqual('custom', unknownNsElement.tagName);
|
||||||
testing.expectEqual('http://www.w3.org/1999/xhtml', unknownNsElement.namespaceURI);
|
// Should be http://example.com/unknown
|
||||||
|
testing.expectEqual('http://lightpanda.io/unsupported/namespace', unknownNsElement.namespaceURI);
|
||||||
|
|
||||||
const regularDiv = document.createElement('div');
|
const regularDiv = document.createElement('div');
|
||||||
testing.expectEqual('DIV', regularDiv.tagName);
|
testing.expectEqual('DIV', regularDiv.tagName);
|
||||||
@@ -36,5 +45,5 @@
|
|||||||
testing.expectEqual('te:ST', custom.tagName);
|
testing.expectEqual('te:ST', custom.tagName);
|
||||||
testing.expectEqual('te', custom.prefix);
|
testing.expectEqual('te', custom.prefix);
|
||||||
testing.expectEqual('ST', custom.localName);
|
testing.expectEqual('ST', custom.localName);
|
||||||
testing.expectEqual('http://www.w3.org/1999/xhtml', custom.namespaceURI); // Should be test
|
testing.expectEqual('http://lightpanda.io/unsupported/namespace', custom.namespaceURI); // Should be test
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<head id="the_head">
|
<head id="the_head">
|
||||||
|
<meta charset="UTF-8">
|
||||||
<title>Test Document Title</title>
|
<title>Test Document Title</title>
|
||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -11,8 +12,12 @@
|
|||||||
testing.expectEqual(10, document.childNodes[0].nodeType);
|
testing.expectEqual(10, document.childNodes[0].nodeType);
|
||||||
testing.expectEqual(null, document.parentNode);
|
testing.expectEqual(null, document.parentNode);
|
||||||
testing.expectEqual(undefined, document.getCurrentScript);
|
testing.expectEqual(undefined, document.getCurrentScript);
|
||||||
testing.expectEqual("http://127.0.0.1:9582/src/browser/tests/document/document.html", document.URL);
|
testing.expectEqual(testing.BASE_URL + 'document/document.html', document.URL);
|
||||||
testing.expectEqual(window, document.defaultView);
|
testing.expectEqual(window, document.defaultView);
|
||||||
|
testing.expectEqual(false, document.hidden);
|
||||||
|
testing.expectEqual("visible", document.visibilityState);
|
||||||
|
testing.expectEqual(false, document.prerendering);
|
||||||
|
testing.expectEqual(undefined, Document.prerendering);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=headAndbody>
|
<script id=headAndbody>
|
||||||
@@ -22,6 +27,7 @@
|
|||||||
|
|
||||||
<script id=documentElement>
|
<script id=documentElement>
|
||||||
testing.expectEqual($('#the_body').parentNode, document.documentElement);
|
testing.expectEqual($('#the_body').parentNode, document.documentElement);
|
||||||
|
testing.expectEqual(document.documentElement, document.scrollingElement);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=title>
|
<script id=title>
|
||||||
@@ -51,7 +57,7 @@
|
|||||||
testing.expectEqual('CSS1Compat', document.compatMode);
|
testing.expectEqual('CSS1Compat', document.compatMode);
|
||||||
testing.expectEqual(document.URL, document.documentURI);
|
testing.expectEqual(document.URL, document.documentURI);
|
||||||
testing.expectEqual('', document.referrer);
|
testing.expectEqual('', document.referrer);
|
||||||
testing.expectEqual('127.0.0.1', document.domain);
|
testing.expectEqual(testing.HOST, document.domain);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=programmatic_document_metadata>
|
<script id=programmatic_document_metadata>
|
||||||
@@ -64,7 +70,7 @@
|
|||||||
testing.expectEqual('CSS1Compat', doc.compatMode);
|
testing.expectEqual('CSS1Compat', doc.compatMode);
|
||||||
testing.expectEqual('', doc.referrer);
|
testing.expectEqual('', doc.referrer);
|
||||||
// Programmatic document should have empty domain (no URL/origin)
|
// Programmatic document should have empty domain (no URL/origin)
|
||||||
testing.expectEqual('127.0.0.1', doc.domain);
|
testing.expectEqual(testing.HOST, doc.domain);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Test anchors and links -->
|
<!-- Test anchors and links -->
|
||||||
@@ -171,15 +177,111 @@
|
|||||||
testing.expectEqual(initialLength, anchors.length);
|
testing.expectEqual(initialLength, anchors.length);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=cookie>
|
<script id=cookie_basic>
|
||||||
testing.expectEqual('', document.cookie);
|
// Basic cookie operations
|
||||||
document.cookie = 'name=Oeschger;';
|
document.cookie = 'testbasic1=Oeschger';
|
||||||
document.cookie = 'favorite_food=tripe;';
|
testing.expectEqual(true, document.cookie.includes('testbasic1=Oeschger'));
|
||||||
|
|
||||||
testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);
|
document.cookie = 'testbasic2=tripe';
|
||||||
// "" should be returned, but the framework overrules it atm
|
testing.expectEqual(true, document.cookie.includes('testbasic1=Oeschger'));
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testbasic2=tripe'));
|
||||||
|
|
||||||
|
// HttpOnly should be ignored from JavaScript
|
||||||
|
const beforeHttp = document.cookie;
|
||||||
document.cookie = 'IgnoreMy=Ghost; HttpOnly';
|
document.cookie = 'IgnoreMy=Ghost; HttpOnly';
|
||||||
testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);
|
testing.expectEqual(false, document.cookie.includes('IgnoreMy=Ghost'));
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
document.cookie = 'testbasic1=; Max-Age=0';
|
||||||
|
document.cookie = 'testbasic2=; Max-Age=0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_special_chars>
|
||||||
|
// Test special characters in cookie values
|
||||||
|
document.cookie = 'testspaces=hello world';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testspaces=hello world'));
|
||||||
|
document.cookie = 'testspaces=; Max-Age=0';
|
||||||
|
|
||||||
|
// Test various allowed special characters
|
||||||
|
document.cookie = 'testspecial=!#$%&\'()*+-./';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testspecial='));
|
||||||
|
document.cookie = 'testspecial=; Max-Age=0';
|
||||||
|
|
||||||
|
// Semicolon terminates the cookie value
|
||||||
|
document.cookie = 'testsemi=before;after';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testsemi=before'));
|
||||||
|
testing.expectEqual(false, document.cookie.includes('after'));
|
||||||
|
document.cookie = 'testsemi=; Max-Age=0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_empty_name>
|
||||||
|
// Cookie with empty name (just a value)
|
||||||
|
document.cookie = 'teststandalone';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('teststandalone'));
|
||||||
|
document.cookie = 'teststandalone; Max-Age=0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_whitespace>
|
||||||
|
// Names and values should be trimmed
|
||||||
|
document.cookie = ' testtrim = trimmed_value ';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testtrim=trimmed_value'));
|
||||||
|
document.cookie = 'testtrim=; Max-Age=0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_max_age>
|
||||||
|
// Max-Age=0 should immediately delete
|
||||||
|
document.cookie = 'testtemp0=value; Max-Age=0';
|
||||||
|
testing.expectEqual(false, document.cookie.includes('testtemp0=value'));
|
||||||
|
|
||||||
|
// Negative Max-Age should also delete
|
||||||
|
document.cookie = 'testinstant=value';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testinstant=value'));
|
||||||
|
document.cookie = 'testinstant=value; Max-Age=-1';
|
||||||
|
testing.expectEqual(false, document.cookie.includes('testinstant=value'));
|
||||||
|
|
||||||
|
// Positive Max-Age should keep cookie
|
||||||
|
document.cookie = 'testkept=value; Max-Age=3600';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testkept=value'));
|
||||||
|
document.cookie = 'testkept=; Max-Age=0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_overwrite>
|
||||||
|
// Setting a cookie with the same name should overwrite
|
||||||
|
document.cookie = 'testoverwrite=first';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testoverwrite=first'));
|
||||||
|
|
||||||
|
document.cookie = 'testoverwrite=second';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testoverwrite=second'));
|
||||||
|
testing.expectEqual(false, document.cookie.includes('testoverwrite=first'));
|
||||||
|
|
||||||
|
document.cookie = 'testoverwrite=; Max-Age=0';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_path>
|
||||||
|
// Path attribute
|
||||||
|
document.cookie = 'testpath1=value; Path=/';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testpath1=value'));
|
||||||
|
|
||||||
|
// Different path cookie should coexist
|
||||||
|
document.cookie = 'testpath2=value2; Path=/src';
|
||||||
|
testing.expectEqual(true, document.cookie.includes('testpath1=value'));
|
||||||
|
|
||||||
|
document.cookie = 'testpath1=; Max-Age=0; Path=/';
|
||||||
|
document.cookie = 'testpath2=; Max-Age=0; Path=/src';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cookie_invalid_chars>
|
||||||
|
// Control characters (< 32 or > 126) should be rejected
|
||||||
|
const beforeBad = document.cookie;
|
||||||
|
|
||||||
|
document.cookie = 'testbad1\x00=value';
|
||||||
|
testing.expectEqual(false, document.cookie.includes('testbad1'));
|
||||||
|
|
||||||
|
document.cookie = 'testbad2\x1F=value';
|
||||||
|
testing.expectEqual(false, document.cookie.includes('testbad2'));
|
||||||
|
|
||||||
|
document.cookie = 'testbad3=val\x7F';
|
||||||
|
testing.expectEqual(false, document.cookie.includes('testbad3'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=createAttribute>
|
<script id=createAttribute>
|
||||||
|
|||||||
@@ -81,6 +81,172 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script id="focusin_focusout_events">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
const input2 = $('#input2');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
let events = [];
|
||||||
|
|
||||||
|
input1.addEventListener('focus', () => events.push('focus1'));
|
||||||
|
input1.addEventListener('focusin', () => events.push('focusin1'));
|
||||||
|
input1.addEventListener('blur', () => events.push('blur1'));
|
||||||
|
input1.addEventListener('focusout', () => events.push('focusout1'));
|
||||||
|
input2.addEventListener('focus', () => events.push('focus2'));
|
||||||
|
input2.addEventListener('focusin', () => events.push('focusin2'));
|
||||||
|
|
||||||
|
// Focus input1 — should fire focus then focusin
|
||||||
|
input1.focus();
|
||||||
|
testing.expectEqual('focus1,focusin1', events.join(','));
|
||||||
|
|
||||||
|
// Focus input2 — should fire blur, focusout on input1, then focus, focusin on input2
|
||||||
|
events = [];
|
||||||
|
input2.focus();
|
||||||
|
testing.expectEqual('blur1,focusout1,focus2,focusin2', events.join(','));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="focusin_bubbles">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
let bodyFocusin = 0;
|
||||||
|
let bodyFocus = 0;
|
||||||
|
|
||||||
|
document.body.addEventListener('focusin', () => bodyFocusin++);
|
||||||
|
document.body.addEventListener('focus', () => bodyFocus++);
|
||||||
|
|
||||||
|
input1.focus();
|
||||||
|
|
||||||
|
// focusin should bubble to body, focus should not
|
||||||
|
testing.expectEqual(1, bodyFocusin);
|
||||||
|
testing.expectEqual(0, bodyFocus);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="focusout_bubbles">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
|
||||||
|
input1.focus();
|
||||||
|
|
||||||
|
let bodyFocusout = 0;
|
||||||
|
let bodyBlur = 0;
|
||||||
|
|
||||||
|
document.body.addEventListener('focusout', () => bodyFocusout++);
|
||||||
|
document.body.addEventListener('blur', () => bodyBlur++);
|
||||||
|
|
||||||
|
input1.blur();
|
||||||
|
|
||||||
|
// focusout should bubble to body, blur should not
|
||||||
|
testing.expectEqual(1, bodyFocusout);
|
||||||
|
testing.expectEqual(0, bodyBlur);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="focus_relatedTarget">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
const input2 = $('#input2');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
let focusRelated = null;
|
||||||
|
let blurRelated = null;
|
||||||
|
let focusinRelated = null;
|
||||||
|
let focusoutRelated = null;
|
||||||
|
|
||||||
|
input1.addEventListener('blur', (e) => { blurRelated = e.relatedTarget; });
|
||||||
|
input1.addEventListener('focusout', (e) => { focusoutRelated = e.relatedTarget; });
|
||||||
|
input2.addEventListener('focus', (e) => { focusRelated = e.relatedTarget; });
|
||||||
|
input2.addEventListener('focusin', (e) => { focusinRelated = e.relatedTarget; });
|
||||||
|
|
||||||
|
input1.focus();
|
||||||
|
input2.focus();
|
||||||
|
|
||||||
|
// blur/focusout on input1 should have relatedTarget = input2 (gaining focus)
|
||||||
|
testing.expectEqual(input2, blurRelated);
|
||||||
|
testing.expectEqual(input2, focusoutRelated);
|
||||||
|
|
||||||
|
// focus/focusin on input2 should have relatedTarget = input1 (losing focus)
|
||||||
|
testing.expectEqual(input1, focusRelated);
|
||||||
|
testing.expectEqual(input1, focusinRelated);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="blur_relatedTarget_null">
|
||||||
|
{
|
||||||
|
const btn = $('#btn1');
|
||||||
|
|
||||||
|
btn.focus();
|
||||||
|
|
||||||
|
let blurRelated = 'not_set';
|
||||||
|
btn.addEventListener('blur', (e) => { blurRelated = e.relatedTarget; });
|
||||||
|
btn.blur();
|
||||||
|
|
||||||
|
// blur without moving to another element should have relatedTarget = null
|
||||||
|
testing.expectEqual(null, blurRelated);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="focus_event_properties">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
const input2 = $('#input2');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
let focusEvent = null;
|
||||||
|
let focusinEvent = null;
|
||||||
|
let blurEvent = null;
|
||||||
|
let focusoutEvent = null;
|
||||||
|
|
||||||
|
input1.addEventListener('blur', (e) => { blurEvent = e; });
|
||||||
|
input1.addEventListener('focusout', (e) => { focusoutEvent = e; });
|
||||||
|
input2.addEventListener('focus', (e) => { focusEvent = e; });
|
||||||
|
input2.addEventListener('focusin', (e) => { focusinEvent = e; });
|
||||||
|
|
||||||
|
input1.focus();
|
||||||
|
input2.focus();
|
||||||
|
|
||||||
|
// All four should be FocusEvent instances
|
||||||
|
testing.expectEqual(true, blurEvent instanceof FocusEvent);
|
||||||
|
testing.expectEqual(true, focusoutEvent instanceof FocusEvent);
|
||||||
|
testing.expectEqual(true, focusEvent instanceof FocusEvent);
|
||||||
|
testing.expectEqual(true, focusinEvent instanceof FocusEvent);
|
||||||
|
|
||||||
|
// All four should be composed per spec
|
||||||
|
testing.expectEqual(true, blurEvent.composed);
|
||||||
|
testing.expectEqual(true, focusoutEvent.composed);
|
||||||
|
testing.expectEqual(true, focusEvent.composed);
|
||||||
|
testing.expectEqual(true, focusinEvent.composed);
|
||||||
|
|
||||||
|
// None should be cancelable
|
||||||
|
testing.expectEqual(false, blurEvent.cancelable);
|
||||||
|
testing.expectEqual(false, focusoutEvent.cancelable);
|
||||||
|
testing.expectEqual(false, focusEvent.cancelable);
|
||||||
|
testing.expectEqual(false, focusinEvent.cancelable);
|
||||||
|
|
||||||
|
// blur/focus don't bubble, focusin/focusout do
|
||||||
|
testing.expectEqual(false, blurEvent.bubbles);
|
||||||
|
testing.expectEqual(true, focusoutEvent.bubbles);
|
||||||
|
testing.expectEqual(false, focusEvent.bubbles);
|
||||||
|
testing.expectEqual(true, focusinEvent.bubbles);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id="focus_disconnected">
|
<script id="focus_disconnected">
|
||||||
{
|
{
|
||||||
const focused = document.activeElement;
|
const focused = document.activeElement;
|
||||||
@@ -88,3 +254,68 @@
|
|||||||
testing.expectEqual(focused, document.activeElement);
|
testing.expectEqual(focused, document.activeElement);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="click_focuses_element">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
const input2 = $('#input2');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
let focusCount = 0;
|
||||||
|
let blurCount = 0;
|
||||||
|
|
||||||
|
input1.addEventListener('focus', () => focusCount++);
|
||||||
|
input1.addEventListener('blur', () => blurCount++);
|
||||||
|
input2.addEventListener('focus', () => focusCount++);
|
||||||
|
|
||||||
|
// Click input1 — should focus it and fire focus event
|
||||||
|
input1.click();
|
||||||
|
testing.expectEqual(input1, document.activeElement);
|
||||||
|
testing.expectEqual(1, focusCount);
|
||||||
|
testing.expectEqual(0, blurCount);
|
||||||
|
|
||||||
|
// Click input2 — should move focus, fire blur on input1 and focus on input2
|
||||||
|
input2.click();
|
||||||
|
testing.expectEqual(input2, document.activeElement);
|
||||||
|
testing.expectEqual(2, focusCount);
|
||||||
|
testing.expectEqual(1, blurCount);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="click_focuses_button">
|
||||||
|
{
|
||||||
|
const btn = $('#btn1');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.click();
|
||||||
|
testing.expectEqual(btn, document.activeElement);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="focus_disconnected_no_blur">
|
||||||
|
{
|
||||||
|
const input1 = $('#input1');
|
||||||
|
|
||||||
|
if (document.activeElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
input1.focus();
|
||||||
|
testing.expectEqual(input1, document.activeElement);
|
||||||
|
|
||||||
|
let blurCount = 0;
|
||||||
|
input1.addEventListener('blur', () => { blurCount++ });
|
||||||
|
|
||||||
|
// Focusing a disconnected element should be a no-op:
|
||||||
|
// blur must not fire on the currently focused element
|
||||||
|
document.createElement('a').focus();
|
||||||
|
testing.expectEqual(input1, document.activeElement);
|
||||||
|
testing.expectEqual(0, blurCount);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -41,4 +41,53 @@
|
|||||||
testing.expectEqual("DIV", newElement.tagName);
|
testing.expectEqual("DIV", newElement.tagName);
|
||||||
testing.expectEqual("after begin", newElement.innerText);
|
testing.expectEqual("after begin", newElement.innerText);
|
||||||
testing.expectEqual("afterbegin", newElement.className);
|
testing.expectEqual("afterbegin", newElement.className);
|
||||||
|
|
||||||
|
const fuzzWrapper = document.createElement("div");
|
||||||
|
fuzzWrapper.id = "fuzz-wrapper";
|
||||||
|
document.body.appendChild(fuzzWrapper);
|
||||||
|
|
||||||
|
const fuzzCases = [
|
||||||
|
// These cases have no <body> element (or empty body), so nothing is inserted
|
||||||
|
{ name: "empty string", html: "", expectElements: 0 },
|
||||||
|
{ name: "comment only", html: "<!-- comment -->", expectElements: 0 },
|
||||||
|
{ name: "doctype only", html: "<!DOCTYPE html>", expectElements: 0 },
|
||||||
|
{ name: "full empty doc", html: "<!DOCTYPE html><html><head></head><body></body></html>", expectElements: 0 },
|
||||||
|
|
||||||
|
{ name: "whitespace only", html: " ", expectElements: 0 },
|
||||||
|
{ name: "newlines only", html: "\n\n\n", expectElements: 0 },
|
||||||
|
{ name: "just text", html: "plain text", expectElements: 0 },
|
||||||
|
// Head-only elements: Extracted from <head> container
|
||||||
|
{ name: "empty meta", html: "<meta>", expectElements: 1 },
|
||||||
|
{ name: "empty title", html: "<title></title>", expectElements: 1 },
|
||||||
|
{ name: "empty head", html: "<head></head>", expectElements: 0 }, // Container with no children
|
||||||
|
{ name: "empty body", html: "<body></body>", expectElements: 0 }, // Container with no children
|
||||||
|
{ name: "empty html", html: "<html></html>", expectElements: 0 }, // Container with no children
|
||||||
|
{ name: "meta only", html: "<meta charset='utf-8'>", expectElements: 1 },
|
||||||
|
{ name: "title only", html: "<title>Test</title>", expectElements: 1 },
|
||||||
|
{ name: "link only", html: "<link rel='stylesheet' href='test.css'>", expectElements: 1 },
|
||||||
|
{ name: "meta and title", html: "<meta charset='utf-8'><title>Test</title>", expectElements: 2 },
|
||||||
|
{ name: "script only", html: "<script>console.log('hi')<\/script>", expectElements: 1 },
|
||||||
|
{ name: "style only", html: "<style>body { color: red; }<\/style>", expectElements: 1 },
|
||||||
|
{ name: "unclosed div", html: "<div>content", expectElements: 1 },
|
||||||
|
{ name: "unclosed span", html: "<span>text", expectElements: 1 },
|
||||||
|
{ name: "invalid tag", html: "<notarealtag>content</notarealtag>", expectElements: 1 },
|
||||||
|
{ name: "malformed", html: "<<div>>test<</div>>", expectElements: 1 }, // Parser handles it
|
||||||
|
{ name: "just closing tag", html: "</div>", expectElements: 0 },
|
||||||
|
{ name: "nested empty", html: "<div><div></div></div>", expectElements: 1 },
|
||||||
|
{ name: "multiple top-level", html: "<span>1</span><span>2</span><span>3</span>", expectElements: 3 },
|
||||||
|
{ name: "mixed text and elements", html: "Text before<b>bold</b>Text after", expectElements: 1 },
|
||||||
|
{ name: "deeply nested", html: "<div><div><div><span>deep</span></div></div></div>", expectElements: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
fuzzCases.forEach((tc, idx) => {
|
||||||
|
fuzzWrapper.innerHTML = "";
|
||||||
|
fuzzWrapper.insertAdjacentHTML("beforeend", tc.html);
|
||||||
|
if (tc.expectElements !== fuzzWrapper.childElementCount) {
|
||||||
|
console.warn(`Fuzz idx: ${idx}`);
|
||||||
|
testing.expectEqual(tc.expectElements, fuzzWrapper.childElementCount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
document.body.removeChild(fuzzWrapper);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user