262 Commits

Author SHA1 Message Date
Pierre Tachoire
dabded8d1e build: add -Dx86 option to enable x86 backend
see https://ziglang.org/download/0.12.0/release-notes.html#x86-Backend
2024-09-19 17:49:00 +02:00
Pierre Tachoire
c3fd0dbf7a Merge pull request #223 from lightpanda-io/settimout
dom: first draft for window setTimeout
2024-09-19 16:49:55 +02:00
Pierre Tachoire
aeaa745600 clearTimeout: ignore invalid timeout ids 2024-09-19 16:37:42 +02:00
Pierre Tachoire
be27359109 dom: implement clearTimeout 2024-09-19 16:37:42 +02:00
Pierre Tachoire
e8a2ce3614 upgrade zig-js-runtime lib 2024-09-19 16:37:40 +02:00
Pierre Tachoire
0559fb9365 dom: first draft for window setTimeout 2024-09-19 16:36:34 +02:00
Pierre Tachoire
89f898cfa9 Merge pull request #268 from lightpanda-io/HttpHeadersOversize
use 64KB for header buffer
2024-09-19 15:36:25 +02:00
Pierre Tachoire
183abc4610 use 64KB for header buffer 2024-09-18 09:24:57 +02:00
Pierre Tachoire
5dfdedea0e Merge pull request #266 from lightpanda-io/ci-node20
ci: upgrade cache action to node20
2024-09-13 15:04:11 +02:00
Pierre Tachoire
8d89c6053e ci: upgrade cache action to node20 2024-09-13 14:52:06 +02:00
Pierre Tachoire
58d184dba1 Merge pull request #265 from lightpanda-io/ci-cpu
ci: target cpu x86_64 to improve CPU compat
2024-09-13 14:47:58 +02:00
Pierre Tachoire
fd0813fead ci: target cpu x86_64 to improve CPU compat
The `-Dcpu` option force compiling using less CPU's capabilities.
This will improve the binary compatibility with older CPUs.
2024-09-13 14:36:44 +02:00
Pierre Tachoire
c9fae67649 Merge pull request #258 from lightpanda-io/tls.zig
use tls.zig with async client
2024-07-22 10:44:38 +02:00
Pierre Tachoire
3e7f9aaa82 Merge pull request #262 from lightpanda-io/browser-deinit
browser: fix invalid deinit order
2024-07-19 17:20:28 +02:00
Pierre Tachoire
22b03bf7df browser: fix invalid deinit order
http client depends on io loop and must be deinit before.
2024-07-19 17:15:53 +02:00
Pierre Tachoire
6a4d64ed00 use tls.zig with async client
see https://github.com/ziglang/zig/compare/master...ianic:zig:tls23 for
http.std.Client integration
2024-07-19 14:39:50 +02:00
Pierre Tachoire
df27ce09ca Merge pull request #260 from lightpanda-io/zig0.13
upgrade to zig 0.13
2024-07-19 14:36:13 +02:00
Pierre Tachoire
6da2954e0b upgrade to zig 0.13 2024-07-19 14:32:47 +02:00
Pierre Tachoire
95245229b0 upgrade zig-js-runtime 2024-07-19 14:32:47 +02:00
Pierre Tachoire
65c4b471af browser: handle wait with unstarted env 2024-07-19 11:14:50 +02:00
Pierre Tachoire
d6e0559efd upgrade to zig 0.13 2024-07-18 16:57:16 +02:00
Pierre Tachoire
5f5e01a2cf Merge pull request #253 from lightpanda-io/refacto_exec
Adapt to js_exec changes in zig-js-runtime
2024-07-18 16:56:41 +02:00
Pierre Tachoire
8c3939b842 wpt: remove useless Suite.stack 2024-07-18 16:52:32 +02:00
Pierre Tachoire
b537e52a6d browser: use log instead of std.log 2024-07-18 16:46:12 +02:00
Pierre Tachoire
4434e11bdd wpt: restore the test results 2024-07-18 16:40:44 +02:00
Francis Bouvier
b8ec53f708 Adapt to js_exec changes in zig-js-runtime
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-07-18 12:10:06 +02:00
Pierre Tachoire
f8395fec5c Merge pull request #257 from lightpanda-io/build-x86
ci: target cpu x86_64_v3+aes for compatibility
2024-07-17 17:04:20 +02:00
Pierre Tachoire
28155cb8d3 ci: target cpu x86_64_v3+aes for compatibility 2024-07-17 17:00:09 +02:00
Pierre Tachoire
f793278dfe Merge pull request #255 from lightpanda-io/ci
ci: remove container usage and download v8 from release
2024-07-17 15:16:41 +02:00
Pierre Tachoire
cdbbc71b0a ci: add nightly build for macos aarch64 2024-07-17 10:28:24 +02:00
Pierre Tachoire
bfa6f55551 ci: remove container usage and download v8 from release 2024-07-17 09:56:34 +02:00
Pierre Tachoire
5fb25cf015 Merge pull request #254 from lightpanda-io/nightly
ci: add linux amd64 nightly build
2024-07-16 14:29:36 +02:00
Pierre Tachoire
5cad450e5a ci: add linux amd64 nightly build 2024-07-16 12:47:26 +02:00
Pierre Tachoire
8ec22ca6b8 Merge pull request #246 from lightpanda-io/mutation-observer
dom: implement MutationObserver
2024-06-24 16:03:58 +02:00
Pierre Tachoire
d8fe029f80 mutation: very partial childList implementation 2024-06-24 15:19:39 +02:00
Pierre Tachoire
c9cfb1ecba dom: add attributeNS funcs 2024-06-24 15:19:39 +02:00
Pierre Tachoire
94e47f4f35 mutation: implement attribute and cdata observer 2024-06-24 15:19:38 +02:00
Pierre Tachoire
12111d4cdf dom: first draft for MutationObserver 2024-06-24 15:19:30 +02:00
Pierre Tachoire
00e8f13f62 Merge pull request #251 from lightpanda-io/eventlistener-data
events: create an EventHandlerData struct
2024-06-24 15:19:09 +02:00
Pierre Tachoire
e1e501e14a Merge pull request #252 from lightpanda-io/wpt-subtest
Wpt subtest
2024-06-21 16:32:36 +02:00
Pierre Tachoire
32015eae3c upgrade tests/wpt 2024-06-21 16:20:14 +02:00
Pierre Tachoire
ab31cc0a18 wpt: if no test case found, the suite fails 2024-06-21 16:19:37 +02:00
Pierre Tachoire
1924f136c6 events: create an EventHandlerData struct
It simplifies the EventHandlerFunc creation and allows to insert user's
data.
2024-06-21 09:20:03 +02:00
Francis Bouvier
0481676be5 Merge pull request #250 from lightpanda-io/deps
Upgrade zig-js-runtime
2024-06-19 22:31:05 +02:00
Francis Bouvier
0da1955b52 Upgrade zig-js-runtime
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-19 22:30:30 +02:00
Pierre Tachoire
522b293149 Merge pull request #249 from lightpanda-io/upgrade-jsruntime
upgrade zig-js-runtime
2024-06-19 15:32:44 +02:00
Pierre Tachoire
0a50b586fd upgrade zig-js-runtime 2024-06-19 15:11:40 +02:00
Pierre Tachoire
11a63b776e Merge pull request #245 from lightpanda-io/hundred-types
generate: handle more than 100 in itoa
2024-06-19 13:56:06 +02:00
Pierre Tachoire
78467ff209 generate: handle more than 100 in itoa 2024-06-19 11:42:19 +02:00
Pierre Tachoire
6e5f67cd66 Merge pull request #243 from lightpanda-io/http-buf
http: use 16KB for the client header buffer
2024-06-18 17:21:59 +02:00
Pierre Tachoire
17a86cc1a6 http: use 16KB for the client header buffer 2024-06-18 17:19:05 +02:00
Pierre Tachoire
f6e080bdfd Merge pull request #203 from lightpanda-io/upgrade-zig
Upgrade zig 0.12
2024-06-18 16:16:44 +02:00
Pierre Tachoire
3744dc1e58 upgrade zig-js-runtime 2024-06-18 16:13:34 +02:00
Pierre Tachoire
9cdf1f5762 xhr: fix unit tests for 0.12.1 2024-06-18 16:13:34 +02:00
Pierre Tachoire
25ee34e65d jsruntime upgrade 2024-06-18 16:13:29 +02:00
Pierre Tachoire
720d3f4df9 test: fix comptime var 2024-06-18 16:13:28 +02:00
Pierre Tachoire
dcca5e60e3 upgrade jsruntime deps 2024-06-18 16:13:28 +02:00
Pierre Tachoire
f2a406d224 move netsurf and mimalloc into modules 2024-06-18 16:13:27 +02:00
Pierre Tachoire
68c8372493 build: use path() func 2024-06-18 16:13:27 +02:00
Pierre Tachoire
ef364f83c8 upgrade to zig 0.12.1 2024-06-18 16:13:27 +02:00
Pierre Tachoire
33c92776f0 build.zig: upgrade to zig 0.12.1 2024-06-18 16:13:26 +02:00
Pierre Tachoire
f5a2c8d303 upgrade to zig 0.12 2024-06-18 16:13:26 +02:00
Pierre Tachoire
c555c325e9 upgrade to zig 0.12
0.12.0-dev.3439+31a7f22b8
2024-06-18 16:13:26 +02:00
Pierre Tachoire
9310b91ad5 README: upgrade zig version 2024-06-18 16:13:25 +02:00
Pierre Tachoire
a708bc7d0f CI: upgrade zig version 0.12.1 2024-06-18 16:13:25 +02:00
Pierre Tachoire
4ba4ce0f7c build: upgrade build.zig to 0.12 2024-06-18 16:13:25 +02:00
Pierre Tachoire
49f20adbab upgrade jsruntime-lib 2024-06-18 16:13:22 +02:00
Pierre Tachoire
6724004a49 Merge pull request #242 from lightpanda-io/ci-EPERM
ci: add --security-opt seccomp=unconfined docker option
2024-06-18 16:08:34 +02:00
Pierre Tachoire
33ec300947 ci: run ci on .github changes 2024-06-18 16:05:21 +02:00
Pierre Tachoire
0c2f0b78aa ci: add --security-opt seccomp=unconfined docker option
It seems docker blocks io_uring by default using seccomp.

see tigerbeetle/tigerbeetle#1995 and
moby/moby#46762
2024-06-18 16:03:44 +02:00
Francis Bouvier
152a4e5e7f Merge pull request #241 from lightpanda-io/README
Add a status section in the README
2024-06-11 16:55:28 +02:00
Francis Bouvier
e6f4b9cc66 Update README.md
Co-authored-by: Pierre Tachoire <pierre@lightpanda.io>
2024-06-11 16:55:18 +02:00
Francis Bouvier
67c8647e25 Add a status section in the README
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-11 16:35:00 +02:00
Pierre Tachoire
9fe56c5889 Merge pull request #240 from lightpanda-io/wpt-wait
Wpt wait
2024-05-23 15:24:38 +02:00
Pierre Tachoire
77bf332f13 wpt: split script exec and wait loop 2024-05-23 15:11:24 +02:00
Pierre Tachoire
b4f445183c wpt: add log.debug method in pure JS 2024-05-23 15:11:24 +02:00
Pierre Tachoire
38d48d7515 upgrade zig-js-runtime 2024-05-23 15:11:24 +02:00
Pierre Tachoire
c5776f0ae6 Merge pull request #231 from lightpanda-io/browser-wait
browser: extract loop wait
2024-05-22 15:18:00 +02:00
Pierre Tachoire
7a6d929f08 browser: add wait to run the loop 2024-05-22 15:13:23 +02:00
Pierre Tachoire
11f7fc4550 Merge pull request #227 from lightpanda-io/listener-error
event: log listeners errors and continue execution
2024-05-22 15:11:34 +02:00
Pierre Tachoire
d50f761c8f event: log listeners errors and continue execution 2024-05-22 15:07:49 +02:00
Pierre Tachoire
23dafb0f12 Merge pull request #216 from lightpanda-io/usrctx
Add user context
2024-05-22 15:07:02 +02:00
Pierre Tachoire
9ac46ea0cb upgrade zig-js-runtime 2024-05-22 14:59:36 +02:00
Pierre Tachoire
00d75584db usrctx: use ctx http client with xhr 2024-05-22 14:58:48 +02:00
Pierre Tachoire
c2e64c131a userctx: document is not opational anymore 2024-05-22 14:56:41 +02:00
Pierre Tachoire
840aea9013 dom: fix document creation process 2024-05-22 14:56:41 +02:00
Pierre Tachoire
bf522937e1 shell: add missing try 2024-05-22 14:56:41 +02:00
Pierre Tachoire
7d91f7992c dom: first draft for hierachy check in nodes 2024-05-22 14:56:40 +02:00
Pierre Tachoire
b2df0c1541 dom: implement document fragment constructor 2024-05-22 14:56:40 +02:00
Pierre Tachoire
d823eebce5 dom: remove useless TODO 2024-05-22 14:56:40 +02:00
Pierre Tachoire
55b80ecd15 dom: implement text constructor 2024-05-22 14:56:39 +02:00
Pierre Tachoire
14e1c44eb0 dom: implement comment constructor 2024-05-22 14:56:29 +02:00
Pierre Tachoire
eef2fa94d0 text: return error on constructor
Blocked by #102
2024-05-22 14:45:40 +02:00
Pierre Tachoire
49ee5e4e68 comment: return error on constructor
Blocked by https://github.com/lightpanda-io/browsercore/issues/102
2024-05-22 14:45:39 +02:00
Pierre Tachoire
b7f589ee1a dom: fix document constructor 2024-05-22 14:45:39 +02:00
Pierre Tachoire
e18d04a799 userctx: inject user context 2024-05-22 14:45:34 +02:00
Francis Bouvier
c1b73dfdc2 Merge pull request #239 from lightpanda-io/update_readme
Add benchmark graph in the README
2024-05-17 18:36:48 +02:00
Francis Bouvier
63e3f8d48a Add benchmark graph in the README
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-17 18:29:58 +02:00
Pierre Tachoire
194328f2de Merge pull request #238 from lightpanda-io/update_readme
Update README
2024-05-16 07:41:22 +02:00
Francis Bouvier
0636240a58 Update README
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-15 18:10:05 +02:00
Pierre Tachoire
7ec5a8d15b Merge pull request #234 from lightpanda-io/html-anchor
dom: add target and href accessors to anchor
2024-05-14 16:23:37 +02:00
Pierre Tachoire
ead08557bf script: implement interface 2024-05-14 16:19:22 +02:00
Pierre Tachoire
6d808d89b0 anchor: implement set_host 2024-05-14 13:44:27 +02:00
Pierre Tachoire
6b42b5abdd anchor: implement HTMLHyperlinkElementUtils interface
https://html.spec.whatwg.org/#htmlhyperlinkelementutils
2024-05-14 13:44:27 +02:00
Pierre Tachoire
e12d6e85f0 url: add origin getter 2024-05-14 13:44:26 +02:00
Pierre Tachoire
7da440e9d3 anchro: implement url manipulation 2024-05-14 13:44:26 +02:00
Pierre Tachoire
d93a065db9 url: improve url format 2024-05-14 13:44:26 +02:00
Pierre Tachoire
8d6ee42096 anchor: follow up 2024-05-14 13:44:25 +02:00
Pierre Tachoire
df6a905683 dom: add target and href accessors to anchor 2024-05-14 13:44:16 +02:00
Francis Bouvier
88c9875664 Merge pull request #237 from lightpanda-io/zig-js-runtime_rename
Update dependancy jsruntime-lib -> zig-js-runtime
2024-05-14 13:14:05 +02:00
Pierre Tachoire
5fbbf1b59f readme: fix zig-js-runtime step 2024-05-14 12:22:13 +02:00
Pierre Tachoire
f040f422e4 ci: rename jsruntime-lib into zig-js-runtime 2024-05-14 12:18:24 +02:00
Francis Bouvier
986e69f45d Update dependancy jsruntime-lib -> zig-js-runtime
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-14 11:35:23 +02:00
Pierre Tachoire
deed0546cc Merge pull request #236 from lightpanda-io/license-source
add AGPL license header in zig files
2024-05-14 08:45:44 +02:00
Pierre Tachoire
2a3a243d1c add AGPL license header in zig files 2024-05-13 20:51:36 +02:00
Pierre Tachoire
eba1a715b1 Merge pull request #235 from lightpanda-io/open
add AGPL license
2024-05-13 14:46:47 +02:00
Pierre Tachoire
973ebcb7b5 add AGPL license 2024-05-13 12:10:15 +02:00
Pierre Tachoire
e1aec8bc07 Merge pull request #230 from lightpanda-io/dom-url
URL api
2024-05-13 11:26:55 +02:00
Pierre Tachoire
806b6c0c1e Merge pull request #233 from lightpanda-io/fix-window-global
wpt: dispatch native event
2024-05-07 16:18:10 +02:00
Pierre Tachoire
c6754d6a6e wpt: dispatch native event 2024-05-07 16:09:20 +02:00
Pierre Tachoire
1be9470942 Merge pull request #232 from lightpanda-io/fix-window-global
browser: fix global object window
2024-05-07 16:08:45 +02:00
Pierre Tachoire
edd0c7d0a3 browser: fix global object window 2024-05-07 15:59:00 +02:00
Pierre Tachoire
d0c741f3bb url: search query dynamic and encoded 2024-05-06 16:32:20 +02:00
Pierre Tachoire
a9842fd790 url: decode query 2024-05-06 15:06:03 +02:00
Pierre Tachoire
f7040153cd url: implement query parsing 2024-05-06 12:45:14 +02:00
Pierre Tachoire
e42b03acd8 mime: extract string parser 2024-05-06 12:44:45 +02:00
Pierre Tachoire
28a87c2a47 url: first draft 2024-05-03 16:18:11 +02:00
Pierre Tachoire
e2cd983851 Merge pull request #220 from lightpanda-io/session_page
Keep reference of current Page in Session
2024-05-02 18:15:53 +02:00
Francis Bouvier
df82d25e91 Return error if a Page already exists in Session
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-02 17:52:23 +02:00
Francis Bouvier
bcf4083f9c Keep reference of current Page in Session
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-02 17:52:14 +02:00
Pierre Tachoire
4444d8a008 Merge pull request #229 from lightpanda-io/upgrade-libdom
upgrade libdom
2024-05-02 17:42:27 +02:00
Pierre Tachoire
d193b73ee3 upgrade libdom 2024-05-02 17:36:44 +02:00
Pierre Tachoire
89bfc8ccdc Merge pull request #222 from lightpanda-io/webstorage
storage: first implementation of webstorage API
2024-05-02 15:35:22 +02:00
Pierre Tachoire
7c06067991 Merge pull request #214 from lightpanda-io/event-callback
Event callback
2024-05-02 15:30:08 +02:00
Pierre Tachoire
8b47d72079 event: set this arg on callback 2024-05-02 15:25:41 +02:00
Pierre Tachoire
a2a0db7bc4 upgrade jsruntime 2024-05-02 15:25:41 +02:00
Pierre Tachoire
5f6e5d57c0 upgrade tests/wpt 2024-05-02 15:23:15 +02:00
Pierre Tachoire
61357ee7e0 storage: update comment about dispatch event 2024-05-02 15:21:21 +02:00
Pierre Tachoire
88106f8449 Merge pull request #226 from lightpanda-io/upgrade-jsruntime
upgrade jsruntime-lib
2024-04-30 08:54:59 +02:00
Pierre Tachoire
6f2f0af0ef upgrade jsruntime-lib 2024-04-30 08:38:53 +02:00
Pierre Tachoire
eb829f4d36 Merge pull request #225 from lightpanda-io/netsurf-empty-event-type
dom: an event type can be null
2024-04-29 17:51:59 +02:00
Pierre Tachoire
d155421a40 netsurf: add missing netsurf DOM errors 2024-04-29 17:42:38 +02:00
Pierre Tachoire
9f2bad7498 dom: an event type can be null
In this case we return empty string
2024-04-29 16:31:37 +02:00
Pierre Tachoire
3c5d601622 storage: first implementation of webstorage API 2024-04-24 11:54:41 +02:00
Pierre Tachoire
2a94e5a69e Merge pull request #199 from lightpanda-io/c_alloc
setCAllocator
2024-04-19 12:05:56 +02:00
Pierre Tachoire
8e96ee337d wpt: skip tests/wpt/dom/events/remove-all-listeners.html 2024-04-19 11:55:55 +02:00
Pierre Tachoire
304a28a79d mimalloc: add strdup and strndup overrride 2024-04-19 11:48:03 +02:00
Pierre Tachoire
a3e91debea deps: upgrade netsurf deps 2024-04-19 11:48:03 +02:00
Pierre Tachoire
545bcc403a ci: rebuild mimalloc if it has changed 2024-04-19 11:48:01 +02:00
Pierre Tachoire
69b5a3db15 readme: add mimalloc info 2024-04-19 11:47:04 +02:00
Pierre Tachoire
53a5326248 mimalloc: avoid mimalloc override
By default mimalloc is built to override default allocation functions.
So it is used also by v8.

This change avoid the mimalloc override to keep the native stdlib
functions.
2024-04-19 11:47:03 +02:00
Pierre Tachoire
3834ebcfa4 replace calloc with mimalloc 2024-04-19 11:46:42 +02:00
Pierre Tachoire
9363acf4ec glue mimalloc with netsurf C libs 2024-04-19 11:46:42 +02:00
Pierre Tachoire
dad51a4179 upgrade libwapcaplet deps 2024-04-19 11:46:42 +02:00
Pierre Tachoire
59b2954ff4 deps: add mimalloc dependency 2024-04-19 11:46:41 +02:00
Pierre Tachoire
5e9d31b053 deps: use our fork for all netsurf deps 2024-04-19 11:46:41 +02:00
Francis Bouvier
76c88d049f setCAllocator
Replace custom malloc functions in netsurf libs with a global Zig allocator.

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 11:46:41 +02:00
Pierre Tachoire
f0773a3ca2 Merge pull request #219 from lightpanda-io/ci-deps
ci: force netsurf build deps each time
2024-04-19 11:45:48 +02:00
Pierre Tachoire
f9cff763d8 ci: disable build release on PR 2024-04-19 11:41:38 +02:00
Pierre Tachoire
8d606d5dc5 ci: force netsurf build deps each time 2024-04-19 10:51:20 +02:00
Pierre Tachoire
7347e1d414 Merge pull request #218 from lightpanda-io/upgrade-wpt
upgrade wpt
2024-04-18 16:33:49 +02:00
Pierre Tachoire
b65e0e8d77 upgrade wpt 2024-04-18 16:32:59 +02:00
Pierre Tachoire
4da25fafd4 Merge pull request #217 from lightpanda-io/ci-refacto
CI refacto
2024-04-16 16:04:43 +02:00
Pierre Tachoire
d8f21e3c67 ci: ugrade GH actions versions 2024-04-16 15:55:41 +02:00
Pierre Tachoire
fe8b6e3060 ci: add missing permissions for wpt 2024-04-16 15:55:41 +02:00
Pierre Tachoire
8b03c0c651 ci: force ci on YAML changes 2024-04-16 15:55:41 +02:00
Pierre Tachoire
ffbcfc18f1 ci: extract install steps in its own action 2024-04-16 15:55:40 +02:00
Pierre Tachoire
e3f487a7f1 ci: force netsurf rebuild on change 2024-04-16 15:38:50 +02:00
Pierre Tachoire
309b6370f7 Merge pull request #213 from lightpanda-io/ci-build
ci: fix build dev command
2024-04-10 11:46:36 +02:00
Pierre Tachoire
6a560fd20c ci: fix build dev command 2024-04-10 11:46:08 +02:00
Pierre Tachoire
c8abbf411b Merge pull request #212 from lightpanda-io/ci-build
ci: split build dev and build release
2024-04-10 11:34:30 +02:00
Pierre Tachoire
c2f17cb216 ci: add missing s3 credentials for test 2024-04-10 11:32:51 +02:00
Pierre Tachoire
25332fd095 ci: split build dev and build release 2024-04-10 11:29:54 +02:00
Pierre Tachoire
e2a8a74906 Merge pull request #209 from lightpanda-io/bench
Add benchmark output when running js tests
2024-04-10 11:20:41 +02:00
Pierre Tachoire
cca6e363c7 ci: split zig test and zig build steps 2024-04-10 09:50:53 +02:00
Pierre Tachoire
cb2b488d27 bench: prepare v8, libdom and main metrics 2024-04-10 09:50:36 +02:00
Pierre Tachoire
a9e2569a1b bench: display duration in ms 2024-04-10 09:42:15 +02:00
Pierre Tachoire
44271cac1a Merge pull request #211 from lightpanda-io/upgrade-jsruntime
upgrade jsruntime-lib
2024-04-09 09:10:10 +02:00
Pierre Tachoire
762dfe8f31 upgrade jsruntime-lib 2024-04-09 09:00:16 +02:00
Pierre Tachoire
37350b0701 ci: save and export browser bench 2024-04-08 18:08:03 +02:00
Pierre Tachoire
e3f7504572 test: rename js bench into browser 2024-04-08 18:01:38 +02:00
Pierre Tachoire
deb8490991 Merge pull request #210 from lightpanda-io/upgrade-libdom
upgrade libdom
2024-04-08 15:43:09 +02:00
Pierre Tachoire
82c5019a44 Merge pull request #208 from lightpanda-io/pi-clone
dom: fix processing instruction clone
2024-04-08 15:22:53 +02:00
Pierre Tachoire
55c747ad45 upgrade libdom 2024-04-08 15:22:00 +02:00
Pierre Tachoire
d080dde361 test: bench: use pretty for console output 2024-04-08 14:54:55 +02:00
Pierre Tachoire
32349e472c test: add test arguments and expose json benchmark result 2024-04-08 14:40:52 +02:00
Pierre Tachoire
49e3d569de dom: fix processing instruction clone 2024-04-05 16:34:22 +02:00
Pierre Tachoire
d58045c330 Merge pull request #196 from lightpanda-io/css
css: implement css query
2024-04-05 10:57:41 +02:00
Pierre Tachoire
c80ef7ca96 Merge pull request #206 from lightpanda-io/upgrade-jsruntime
upgrade jsruntime
2024-04-03 15:16:41 +02:00
Pierre Tachoire
9db39e4165 Merge pull request #205 from lightpanda-io/upgrade-libdom
upgrade libdom
2024-04-03 15:15:54 +02:00
Pierre Tachoire
1e263cfc1b Merge pull request #204 from lightpanda-io/small-build-improvements
Small build improvements
2024-04-03 15:15:42 +02:00
Pierre Tachoire
5c804f2c3d upgrade jsruntime 2024-04-03 15:05:33 +02:00
Pierre Tachoire
29ce31f2fd upgrade libdom 2024-04-03 15:03:37 +02:00
Pierre Tachoire
6e8398be96 ci: track build.zig changes 2024-04-03 15:02:07 +02:00
Pierre Tachoire
0af69fee6d build: remove deprecated usage 2024-04-03 14:58:11 +02:00
Pierre Tachoire
20f25fc352 build: remove useless getInstallStep deps
the dependance of getInstallStep is useful only if we need a previous
binary to exists before using running the step.
2024-04-03 14:55:38 +02:00
Pierre Tachoire
a2eee9a278 README: upgrade zig version 2024-04-03 14:55:30 +02:00
Pierre Tachoire
b59618120f build: remove shell installation 2024-04-03 14:55:10 +02:00
Pierre Tachoire
ff0b7ed6bf build: fix path error 2024-04-03 14:55:01 +02:00
Pierre Tachoire
18d14f8c0c Merge pull request #201 from lightpanda-io/remove-lexbor
deps: remove lexbor
2024-04-03 14:43:18 +02:00
Pierre Tachoire
22459edccc CI: remove lexbor 2024-03-28 14:56:36 +01:00
Pierre Tachoire
52d3f3e966 deps: remove lexbor 2024-03-28 11:13:17 +01:00
Pierre Tachoire
17b20e1ad0 Merge pull request #200 from lightpanda-io/upgrade-wpt-dom
upgrade wpt deps
2024-03-26 11:58:17 +01:00
Pierre Tachoire
6b621fe5ab upgrade wpt deps 2024-03-26 11:40:07 +01:00
Pierre Tachoire
8eb4de9ccb css: ensure node is an element before accessing to attr 2024-03-26 11:08:25 +01:00
Pierre Tachoire
4d5f6d42fa dom: use the css matcher for DOM 2024-03-26 10:25:50 +01:00
Pierre Tachoire
0fa49b99bf css: add README 2024-03-25 18:35:28 +01:00
Pierre Tachoire
4c50b2af1a css: implement legend siblings check for :disabled 2024-03-25 17:56:28 +01:00
Pierre Tachoire
4e61a50946 css: add isEmptyText in node interface 2024-03-25 17:56:28 +01:00
Pierre Tachoire
2c7650cdb1 css: add isDocument, isText and isComment 2024-03-25 17:38:21 +01:00
Pierre Tachoire
8a91840783 css: comment :contains test 2024-03-25 17:09:55 +01:00
Pierre Tachoire
dcc7e51556 css: implement ~, + and > combinators 2024-03-25 17:09:11 +01:00
Pierre Tachoire
565d612abb css: trim attribute op value 2024-03-25 15:40:23 +01:00
Pierre Tachoire
e7738744cb css: add libdom tests 2024-03-25 15:39:59 +01:00
Pierre Tachoire
de9d253dc9 css: implement missing pseudo classes
:input :empty :root :link :enabled :disabled :checked
2024-03-25 14:48:08 +01:00
Pierre Tachoire
2671cda98f css: implement :lang match 2024-03-25 11:43:32 +01:00
Pierre Tachoire
bd899111d5 css: implement :only-child and :only-of-type 2024-03-25 10:25:46 +01:00
Pierre Tachoire
db5d933285 css: add nth- pseudo class 2024-03-25 08:50:57 +01:00
Pierre Tachoire
9c997ec86d css: add pseudo class relative match 2024-03-19 09:25:52 +01:00
Pierre Tachoire
75e80a47e6 css: implement group, compound and start combined match 2024-03-18 21:23:37 +01:00
Pierre Tachoire
d0dbbacd69 css: enable all css tests in zig build test 2024-03-18 21:22:45 +01:00
Pierre Tachoire
a2e747002b css: use parseSelectorGroup() with parse() 2024-03-18 21:22:45 +01:00
Pierre Tachoire
5e8ec4532d css: add attribute matcher 2024-03-18 16:01:46 +01:00
Pierre Tachoire
d64fffc5b3 css: implement id and class match selector 2024-03-18 12:48:03 +01:00
Pierre Tachoire
4629e8a9eb css: check if node is an html element 2024-03-18 11:36:06 +01:00
Pierre Tachoire
7839f466ea css: refacto test 2024-03-18 11:35:47 +01:00
Pierre Tachoire
954a693586 css: add matcher test w/ libdom 2024-03-18 09:51:05 +01:00
Pierre Tachoire
b59fd9b1fb css: matcher draft 2024-03-15 16:09:16 +01:00
Pierre Tachoire
a131e96ed5 css: lower case parse function 2024-03-15 15:03:55 +01:00
Pierre Tachoire
d9c76aa13e css: extract public api on its own file 2024-03-15 09:06:59 +01:00
Pierre Tachoire
6cf805360d css: extract selector in its own file 2024-03-15 08:59:41 +01:00
Pierre Tachoire
97c8053010 css: implement css query parser 2024-03-14 16:40:35 +01:00
Pierre Tachoire
621ffc5db7 Merge pull request #195 from lightpanda-io/browser-jstrace
browser: display js err trace on debug mode
2024-03-11 16:07:47 +01:00
Pierre Tachoire
a7efadabf5 browser: display js err trace on debug mode 2024-03-08 17:42:55 +01:00
Pierre Tachoire
a81e10f093 Merge pull request #184 from lightpanda-io/window-global
window: use window as global object
2024-03-08 12:43:24 +01:00
Pierre Tachoire
886c9daa47 window: inject DocumentHTML instead of Document 2024-03-08 12:24:24 +01:00
Pierre Tachoire
500da5bfd8 test: run JSRuntime test func directly
Instead of calling the bultin test functions
Indeed, it causes issue with type comparison.
See https://github.com/lightpanda-io/browsercore/pull/184#issuecomment-1964369066
2024-03-08 12:24:24 +01:00
Pierre Tachoire
fec212ab94 window: use window as global object 2024-03-08 12:24:23 +01:00
Pierre Tachoire
9221c810a6 Merge pull request #193 from lightpanda-io/build-test
build: use test step option struct
2024-03-07 11:27:33 +01:00
Pierre Tachoire
a1af89b6a0 build: use test step option struct 2024-03-06 15:59:12 +01:00
Pierre Tachoire
b8bf09c8e5 Merge pull request #192 from lightpanda-io/upgrade-jsruntime
upgrade jsruntime
2024-02-29 16:20:57 +01:00
Pierre Tachoire
026a6c0caf upgrade jsruntime 2024-02-29 16:05:51 +01:00
Pierre Tachoire
da763bf17d Merge pull request #191 from lightpanda-io/void-elements
dump: handle void HTML elements
2024-02-29 16:04:42 +01:00
Pierre Tachoire
6777ab9f3d dump: handle void HTML elements 2024-02-29 15:50:03 +01:00
Pierre Tachoire
45172461c7 Merge pull request #182 from lightpanda-io/innerHTML
dom: innerHTML
2024-02-29 15:47:48 +01:00
Pierre Tachoire
b4da2abff2 Merge pull request #189 from lightpanda-io/browser-set-document-uri
browser: inject document URL
2024-02-29 15:47:06 +01:00
Pierre Tachoire
63e19c7704 netsurf: factorize document parsing 2024-02-29 14:14:13 +01:00
Pierre Tachoire
399c7def51 browser: inject document URL 2024-02-29 13:37:08 +01:00
Pierre Tachoire
25bc2d5e75 DOM: improve innerHTML setter test 2024-02-28 14:44:40 +01:00
Pierre Tachoire
1c77d998c6 test: refacto dump test units 2024-02-28 14:40:31 +01:00
Pierre Tachoire
810bd11a5b dump: rename HTML dump funcs 2024-02-28 14:39:22 +01:00
Pierre Tachoire
08e2365d75 Merge pull request #181 from lightpanda-io/xhr-event-delay
xhr: respect 50ms min delay between two progress events
2024-02-27 17:54:50 +01:00
Pierre Tachoire
c0e2377e16 dom: implement innerHTML setter 2024-02-27 16:11:11 +01:00
Pierre Tachoire
f7c0bcceae dom: fix replace child 2024-02-27 16:11:11 +01:00
Pierre Tachoire
37f4a9c72c dom: add innerHTML getter 2024-02-27 16:11:10 +01:00
Pierre Tachoire
64ce07340b browser: expose nodeFile and accept a io.Writer 2024-02-27 16:11:07 +01:00
Pierre Tachoire
5a70db1322 Merge pull request #183 from lightpanda-io/xhr-json
xhr: use std.json.Value to parse JSON response
2024-02-26 18:13:15 +01:00
Pierre Tachoire
d4104883ef xhr: use std.json.Value to parse JSON response 2024-02-26 18:01:07 +01:00
Pierre Tachoire
4f51f28734 upgrade jsruntime 2024-02-26 18:01:06 +01:00
Pierre Tachoire
65e8b56db4 Merge pull request #177 from lightpanda-io/upgrade-wpt
upgrade tests/wpt
2024-02-26 16:01:38 +01:00
Pierre Tachoire
5439a37d25 xhr: respect 50ms min delay between two progress events 2024-02-15 17:53:54 +01:00
Pierre Tachoire
e222d72b46 upgrade tests/wpt 2024-02-12 16:02:28 +01:00
89 changed files with 11414 additions and 2074 deletions

78
.github/actions/install/action.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
name: "Browsercore install"
description: "Install deps for the project browsercore"
inputs:
zig:
description: 'Zig version to install'
required: false
default: '0.13.0'
arch:
description: 'CPU arch used to select the v8 lib'
required: false
default: 'x86_64'
os:
description: 'OS used to select the v8 lib'
required: false
default: 'linux'
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.1.6'
v8:
description: 'v8 version to install'
required: false
default: '11.1.134'
cache-dir:
description: 'cache dir to use'
required: false
default: '~/.cache'
runs:
using: "composite"
steps:
- name: Install apt deps
if: ${{ inputs.os == 'linux' }}
shell: bash
run: sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
- uses: mlugg/setup-zig@v1
with:
version: ${{ inputs.zig }}
- name: Cache v8
id: cache-v8
uses: actions/cache@v4
env:
cache-name: cache-v8
with:
path: ${{ inputs.cache-dir }}/v8
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
shell: bash
run: |
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
- name: install v8
shell: bash
run: |
mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug/libc_v8.a
mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release/libc_v8.a
- name: libiconv
shell: bash
run: make install-libiconv
- name: build mimalloc
shell: bash
run: make install-mimalloc
- name: build netsurf
shell: bash
run: make install-netsurf

View File

@@ -1,60 +0,0 @@
name: build-deps
on:
push:
branches:
- "main"
paths:
- "vendor/lexbor-src"
- "vendor/netsurf/**"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: lightpanda-io/browsercore-deps
ZIG_DOCKER_VERSION: 0.12.0-dev.1773-8a8fd47d2
jobs:
build-deps:
strategy:
matrix:
include:
- os: linux
build_arch: amd64
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
submodules: true
- name: Docker connect
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Docker build
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
file: Dockerfile.deps
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{env.ZIG_DOCKER_VERSION}}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
ZIG_DOCKER_VERSION=${{ env.ZIG_DOCKER_VERSION }}
platforms: ${{matrix.os}}/${{matrix.build_arch}}

75
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: nightly build
on:
schedule:
- cron: "2 2 * * *"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
permissions:
contents: write
jobs:
build-linux-x86_64:
env:
ARCH: x86_64
OS: linux
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dcpu=x86_64
- name: Rename binary
run: mv zig-out/bin/browsercore-get lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
- name: Upload the build
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
tag: nightly
build-macos-aarch64:
env:
ARCH: aarch64
OS: macos
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
with:
os: ${{env.OS}}
arch: ${{env.ARCH}}
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8
- name: Rename binary
run: mv zig-out/bin/browsercore-get lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
- name: Upload the build
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
tag: nightly

View File

@@ -1,7 +1,6 @@
name: wpt
env:
ARCH: x86_64-linux
AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
@@ -12,10 +11,12 @@ on:
branches:
- main
paths:
- "build.zig"
- "src/**/*.zig"
- "src/*.zig"
- "tests/wpt/**"
- "vendor/**"
- ".github/**"
pull_request:
# By default GH trigger on types opened, synchronize and reopened.
@@ -26,10 +27,13 @@ on:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- ".github/**"
- "build.zig"
- "src/**/*.zig"
- "src/*.zig"
- "tests/wpt/**"
- "vendor/**"
- ".github/**"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
@@ -41,37 +45,16 @@ jobs:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
container:
image: ghcr.io/lightpanda-io/zig-browsercore:0.12.0-dev.1773-8a8fd47d2
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get jsruntime-lib submodules also.
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- name: install v8
run: |
mkdir -p vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/debug
ln -s /usr/local/lib/libc_v8.a vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/debug/libc_v8.a
mkdir -p vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/release
ln -s /usr/local/lib/libc_v8.a vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/release/libc_v8.a
- name: install deps
run: |
ln -s /usr/local/lib/lexbor vendor/lexbor
ln -s /usr/local/lib/libiconv vendor/libiconv
ln -s /usr/local/lib/netsurf/build vendor/netsurf/build
ln -s /usr/local/lib/netsurf/lib vendor/netsurf/lib
ln -s /usr/local/lib/netsurf/include vendor/netsurf/include
- uses: ./.github/actions/install
- run: zig build wpt -Dengine=v8 -- --safe --summary
@@ -88,7 +71,7 @@ jobs:
echo "${{github.sha}}" > commit.txt
- name: upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wpt-results
path: |
@@ -112,7 +95,7 @@ jobs:
steps:
- name: download artifact
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: wpt-results

View File

@@ -1,5 +1,8 @@
name: zig-fmt
env:
ZIG_VERSION: 0.13.0
on:
pull_request:
@@ -11,6 +14,8 @@ on:
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
@@ -24,16 +29,13 @@ jobs:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
container:
image: ghcr.io/lightpanda-io/zig:0.12.0-dev.1773-8a8fd47d2
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
outputs:
zig_fmt_errs: ${{ steps.fmt.outputs.zig_fmt_errs }}
steps:
- uses: actions/checkout@v3
- uses: mlugg/setup-zig@v1
with:
version: ${{ env.ZIG_VERSION }}
- uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@@ -1,16 +1,21 @@
name: zig-test
env:
ARCH: x86_64-linux
AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
on:
push:
branches:
- main
paths:
- "build.zig"
- "src/**/*.zig"
- "src/*.zig"
- "vendor/jsruntime-lib"
- "vendor/zig-js-runtime"
- ".github/**"
pull_request:
# By default GH trigger on types opened, synchronize and reopened.
@@ -21,57 +26,111 @@ on:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- ".github/**"
- "build.zig"
- "src/**/*.zig"
- "src/*.zig"
- "vendor/**"
- ".github/**"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
zig-build-dev:
name: zig build dev
# Don't run the CI with draft PR.
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
- name: zig build debug
run: zig build -Dengine=v8
zig-build-release:
name: zig build release
# Don't run the CI on PR
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
- name: zig build release
run: zig build -Doptimize=ReleaseSafe -Dengine=v8
zig-test:
name: zig test
# Don't run the CI with draft PR.
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
- name: zig build test
run: zig build test -Dengine=v8 -- --json > bench.json
- name: write commit
run: |
echo "${{github.sha}}" > commit.txt
- name: upload artifact
uses: actions/upload-artifact@v4
with:
name: bench-results
path: |
bench.json
commit.txt
retention-days: 10
bench-fmt:
name: perf-fmt
needs: zig-test
# Don't execute on PR
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
container:
image: ghcr.io/lightpanda-io/zig-browsercore:0.12.0-dev.1773-8a8fd47d2
image: ghcr.io/lightpanda-io/perf-fmt:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v3
- name: download artifact
uses: actions/download-artifact@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get jsruntime-lib submodules also.
submodules: recursive
name: bench-results
- name: install v8
run: |
mkdir -p vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/debug
ln -s /usr/local/lib/libc_v8.a vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/debug/libc_v8.a
mkdir -p vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/release
ln -s /usr/local/lib/libc_v8.a vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/release/libc_v8.a
- name: install deps
run: |
ln -s /usr/local/lib/lexbor vendor/lexbor
ln -s /usr/local/lib/libiconv vendor/libiconv
ln -s /usr/local/lib/netsurf/build vendor/netsurf/build
ln -s /usr/local/lib/netsurf/lib vendor/netsurf/lib
ln -s /usr/local/lib/netsurf/include vendor/netsurf/include
- name: zig build debug
run: zig build -Dengine=v8
- name: zig build test
run: zig build test -Dengine=v8
- name: zig build release
run: zig build -Doptimize=ReleaseSafe -Dengine=v8
- name: format and send json result
run: /perf-fmt bench-browser ${{ github.sha }} bench.json

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
zig-cache
/.zig-cache/
zig-out
/vendor/lexbor/
/vendor/netsurf/build/
/vendor/netsurf/lib/
/vendor/netsurf/include/

21
.gitmodules vendored
View File

@@ -1,15 +1,12 @@
[submodule "vendor/jsruntime-lib"]
path = vendor/jsruntime-lib
url = git@github.com:lightpanda-io/jsruntime-lib.git
[submodule "vendor/lexbor-src"]
path = vendor/lexbor-src
url = https://github.com/lexbor/lexbor
[submodule "vendor/zig-js-runtime"]
path = vendor/zig-js-runtime
url = git@github.com:lightpanda-io/zig-js-runtime.git
[submodule "vendor/netsurf/libwapcaplet"]
path = vendor/netsurf/libwapcaplet
url = https://source.netsurf-browser.org/libwapcaplet.git
url = git@github.com:lightpanda-io/libwapcaplet.git
[submodule "vendor/netsurf/libparserutils"]
path = vendor/netsurf/libparserutils
url = https://source.netsurf-browser.org/libparserutils.git
url = git@github.com:lightpanda-io/libparserutils.git
[submodule "vendor/netsurf/libdom"]
path = vendor/netsurf/libdom
url = git@github.com:lightpanda-io/libdom.git
@@ -18,7 +15,13 @@
url = https://source.netsurf-browser.org/buildsystem.git
[submodule "vendor/netsurf/libhubbub"]
path = vendor/netsurf/libhubbub
url = https://source.netsurf-browser.org/libhubbub.git
url = git@github.com:lightpanda-io/libhubbub.git
[submodule "tests/wpt"]
path = tests/wpt
url = https://github.com/lightpanda-io/wpt
[submodule "vendor/mimalloc"]
path = vendor/mimalloc
url = git@github.com:microsoft/mimalloc.git
[submodule "vendor/tls.zig"]
path = vendor/tls.zig
url = git@github.com:ianic/tls.zig.git

View File

@@ -1,34 +0,0 @@
# This dockerfile is used to build browsercore vendor dependencies except
# jsruntime-lib v8.
# jsruntime-lib v8 is built via zig-v8-fork/Dockerfile.
ARG ZIG_DOCKER_VERSION=0.11.0
FROM ghcr.io/lightpanda-io/zig:${ZIG_DOCKER_VERSION} as build
# Install required dependencies
RUN apt update && \
apt install -y git curl bash xz-utils python3 ca-certificates pkg-config \
libglib2.0-dev gperf libexpat1-dev cmake build-essential
COPY ./Makefile /src/
WORKDIR /src
# build lexbor
ADD ./vendor/lexbor-src /src/vendor/lexbor-src
RUN make install-lexbor
# build libiconv
RUN make install-libiconv
# build netsurf
ADD ./vendor/netsurf /src/vendor/netsurf
RUN make install-netsurf
FROM scratch as artifact
COPY --from=build /src/vendor/libiconv /usr/local/lib/libiconv
COPY --from=build /src/vendor/lexbor /usr/local/lib/lexbor
COPY --from=build /src/vendor/netsurf/build /usr/local/lib/netsurf/build
COPY --from=build /src/vendor/netsurf/lib /usr/local/lib/netsurf/lib
COPY --from=build /src/vendor/netsurf/include /usr/local/lib/netsurf/include

661
LICENSE Normal file
View File

@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View File

@@ -23,9 +23,9 @@ help:
# $(ZIG) commands
# ------------
.PHONY: build build-release run run-release shell test bench download-zig wpt
.PHONY: build build-dev run run-release shell test bench download-zig wpt
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/jsruntime-lib/build.zig" | cut -d'"' -f2)
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
kernel = $(shell uname -ms)
## Download the zig recommended version
@@ -54,23 +54,24 @@ endif
@curl -o "$(dest)" -L "$(url)" || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mDownloaded $(dest)\e[0m\n"
## Build in debug mode
## Build in release-safe mode
build:
@printf "\e[36mBuilding (debug)...\e[0m\n"
@$(ZIG) build -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mBuild OK\e[0m\n"
build-release:
@printf "\e[36mBuilding (release safe)...\e[0m\n"
@$(ZIG) build -Doptimize=ReleaseSafe -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mBuild OK\e[0m\n"
## Run the server
## Build in debug mode
build-dev:
@printf "\e[36mBuilding (debug)...\e[0m\n"
@$(ZIG) build -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mBuild OK\e[0m\n"
## Run the server in debug mode
run: build
@printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/browsercore || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
## Run a JS shell in release-safe mode
## Run a JS shell in debug mode
shell:
@printf "\e[36mBuilding shell...\e[0m\n"
@$(ZIG) build shell -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@@ -93,15 +94,16 @@ test:
# Install and build required dependencies commands
# ------------
.PHONY: install-submodule
.PHONY: install-lexbor install-jsruntime install-jsruntime-dev install-libiconv
.PHONY: install-zig-js-runtime install-zig-js-runtime-dev install-libiconv
.PHONY: _install-netsurf install-netsurf clean-netsurf test-netsurf install-netsurf-dev
.PHONY: install-mimalloc install-mimalloc-dev clean-mimalloc
.PHONY: install-dev install
## Install and build dependencies for release
install: install-submodule install-lexbor install-jsruntime install-netsurf
install: install-submodule install-zig-js-runtime install-netsurf install-mimalloc
## Install and build dependencies for dev
install-dev: install-submodule install-lexbor install-jsruntime-dev install-netsurf-dev
install-dev: install-submodule install-zig-js-runtime-dev install-netsurf-dev install-mimalloc-dev
install-netsurf-dev: _install-netsurf
install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
@@ -177,20 +179,34 @@ ifeq ("$(wildcard vendor/libiconv/lib/libiconv.a)","")
make && make install
endif
install-lexbor:
@mkdir -p vendor/lexbor
@cd vendor/lexbor && \
cmake ../lexbor-src -DLEXBOR_BUILD_SHARED=OFF && \
make
install-jsruntime-dev:
@cd vendor/jsruntime-lib && \
install-zig-js-runtime-dev:
@cd vendor/zig-js-runtime && \
make install-dev
install-jsruntime:
@cd vendor/jsruntime-lib && \
install-zig-js-runtime:
@cd vendor/zig-js-runtime && \
make install
.PHONY: _build_mimalloc
_build_mimalloc:
@cd vendor/mimalloc && \
mkdir -p out/include && \
cp include/mimalloc.h out/include/ && \
cd out && \
cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) .. && \
make
install-mimalloc-dev: _build_mimalloc
install-mimalloc-dev: OPTS=-DCMAKE_BUILD_TYPE=Debug
install-mimalloc-dev:
@cd vendor/mimalloc/out && \
mv libmimalloc-debug.a libmimalloc.a
install-mimalloc: _build_mimalloc
clean-mimalloc:
@rm -fr vendor/mimalloc/lib/*
## Init and update git submodule
install-submodule:
@git submodule init && \

179
README.md
View File

@@ -1,19 +1,93 @@
# Browsercore
<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>
</p>
## Build
<h1 align="center">Lightpanda</h1>
<div align="center">
<br />
</div>
Lightpanda is the open-source browser made for headless usage:
- Javascript execution
- Support of the Web APIs (partial, WIP)
- Compatible with Playwright, Puppeteer through CDP (WIP)
Fast scraping and web automation with minimal memory footprint:
- Ultra-low memory footprint (12x less than Chrome)
- Blazingly fast & instant startup (64x faster than Chrome)
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark2.png">
See [benchmark details](https://github.com/lightpanda-io/demo).
## Why?
### Javascript execution is mandatory for the modern web
Back in the good old times, grabbing a webpage was as easy as making an HTTP request, cURL-like. Its not possible anymore, because Javascript is everywhere, like it or not:
- Ajax, Single Page App, Infinite loading, “click to display”, instant search, etc.
- JS web frameworks: React, Vue, Angular & others
### Chrome is not the right tool
So if we need Javascript, why not use a real web browser. Lets take a huge desktop application, hack it, and run it on the server, right? Hundreds of instance of Chrome if you use it at scale. Are you sure its such a good idea?
- Heavy on RAM and CPU, expensive to run
- Hard to package, deploy and maintain at scale
- Bloated, lots of features are not useful in headless usage
### Lightpanda is built for performance
If we want both Javascript and performance, for a real headless browser, we need to start from scratch. Not yet another iteration of Chromium, really from a blank page. Crazy right? But thats we did:
- Not based on Chromium, Blink or WebKit
- Low-level system programming language (Zig) with optimisations in mind
- Opinionated, no rendering
## Status
Lightpanda is still a work in progress and is currently at the Alpha stage.
Here are the key features we want to implement before releasing a Beta version:
- [x] Loader
- [x] HTML parser and DOM tree
- [x] Javascript support
- [x] Basic DOM APIs
- [x] Ajax
- [x] XHR API
- [ ] Fetch API
- [x] DOM dump
- [ ] Basic CDP server
We will not provide binary versions until we reach at least the Beta stage.
NOTE: There are hundreds of Web APIs. Developing a browser, even just for headless mode, is a huge task. It's more about coverage than a _working/not working_ binary situation.
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
We do not provide yet binary versions of Lightpanda, you have to compile it from source.
### Prerequisites
Browsercore is written with [Zig](https://ziglang.org/) `0.11.0`. You have to
Lightpanda is written with [Zig](https://ziglang.org/) `0.13.0`. You have to
install it with the right version in order to build the project.
Browsercore also depends on
[js-runtimelib](https://github.com/francisbouvier/jsruntime-lib/) and
[lexbor](https://github.com/lexbor/lexbor) libs.
Lightpanda also depends on
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
[Netsurf libs](https://www.netsurf-browser.org/) and
[Mimalloc](https://microsoft.github.io/mimalloc).
To be able to build the v8 engine for js-runtimelib, you have to install some libs:
To be able to build the v8 engine for zig-js-runtime, you have to install some libs:
For Debian/Ubuntu based Linux:
```
sudo apt install xz-utils \
python3 ca-certificates git \
@@ -22,94 +96,103 @@ sudo apt install xz-utils \
cmake clang
```
For MacOS, you only need Python 3 and cmake.
For MacOS, you only need cmake:
To be able to build lexbor, you need to install also `cmake`.
```
brew install cmake
```
### Install and build dependencies
The project uses git submodule for dependencies.
The `make install-submodule` will init and update the submodules in the `vendor/`
directory.
#### All in one build
You can run `make install` to install deps all in one (or `make install-dev` if you need the development versions).
Be aware that the build task is very long and cpu consuming, as you will build from sources all dependancies, including the v8 Javascript engine.
#### Step by step build dependancy
The project uses git submodules for dependencies.
To init or update the submodules in the `vendor/` directory:
```
make install-submodule
```
### Build netsurf
**Netsurf libs**
Netsurf libs are used for HTML parsing and DOM tree generation.
The command `make install-netsurf` will build netsurf libs used by browsercore.
```
make install-netsurf
```
### Build lexbor
For dev env, use `make install-netsurf-dev`.
The command `make install-lexbor` will build lexbor lib used by browsercore.
```
make install-lexbor
```
**Mimalloc**
### Build jsruntime-lib
The command `make install-jsruntime-dev` uses jsruntime-lib's `zig-v8` dependency to build v8 engine lib.
Be aware the build task is very long and cpu consuming.
Build v8 engine for debug/dev version, it creates
`vendor/jsruntime-lib/vendor/v8/$ARCH/debug/libc_v8.a` file.
Mimalloc is used as a C memory allocator.
```
make install-jsruntime-dev
make install-mimalloc
```
You should also build a release vesion of v8 with:
For dev env, use `make install-mimalloc-dev`.
Note: when Mimalloc is built in dev mode, you can dump memory stats with the
env var `MIMALLOC_SHOW_STATS=1`. See
[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html).
**zig-js-runtime**
Our own Zig/Javascript runtime, which includes the v8 Javascript engine.
This build task is very long and cpu consuming, as you will build v8 from sources.
```
make install-jsruntime
make install-zig-js-runtime
```
### All in one build
You can run `make intall` and `make install-dev` to install deps all in one.
For dev env, use `make iinstall-zig-js-runtime-dev`.
## Test
### Unit Tests
You can test browsercore by running `make test`.
You can test Lightpanda by running `make test`.
### Web Platform Tests
Browsercore is tested against the standardized [Web Platform
Lightpanda is tested against the standardized [Web Platform
Tests](https://web-platform-tests.org/).
The relevant tests cases for Browsercore are commit with the project.
All the tests cases executed are located in `tests/wpt` dir and come from an
external repository: https://github.com/lightpanda-io/wpt
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.
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
[wpt.live](https://wpt.live).
*Run WPT test suite*
#### Run WPT test suite
To run all the tests:
You can run all the test.
The runner execute all the tests ending with `.html`.
```
make wpt
```
Or one specific test by using a suffix.
Or one specific test:
```
make wpt Node-childNodes.html
```
*Add a new WPT test case*
#### Add a new WPT test case
We add new tests cases files with implemented changes in Browsercore.
We add new relevant tests cases files when we implemented changes in Lightpanda.
Copy the test case you want to add from the [WPT
repo](https://github.com/web-platform-tests/wpt) into `tests/wpt` dir, commit
the files in the https://github.com/lightpanda-io/wpt repository and update the
git submodule in browsercore.
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.
:warning: Please keep the original directory tree structure into `tests/wpt`.
:warning: Please keep the original directory tree structure of `tests/wpt`.

143
build.zig
View File

@@ -1,16 +1,34 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const jsruntime_path = "vendor/jsruntime-lib/";
const jsruntime = @import("vendor/jsruntime-lib/build.zig");
const jsruntime_path = "vendor/zig-js-runtime/";
const jsruntime = @import("vendor/zig-js-runtime/build.zig");
const jsruntime_pkgs = jsruntime.packages(jsruntime_path);
/// Do not rename this constant. It is scanned by some scripts to determine
/// which zig version to install.
const recommended_zig_version = jsruntime.recommended_zig_version;
pub fn build(b: *std.build.Builder) !void {
pub fn build(b: *std.Build) !void {
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
.eq => {},
.lt => {
@@ -29,22 +47,25 @@ pub fn build(b: *std.build.Builder) !void {
const options = try jsruntime.buildOptions(b);
const x86 = b.option(bool, "x86", "Use x86 backend") orelse false;
// browser
// -------
// compile and install
const exe = b.addExecutable(.{
.name = "browsercore",
.root_source_file = .{ .path = "src/main.zig" },
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(exe, options);
try common(b, exe, options);
b.installArtifact(exe);
// run
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
@@ -59,18 +80,17 @@ pub fn build(b: *std.build.Builder) !void {
// compile and install
const shell = b.addExecutable(.{
.name = "browsercore-shell",
.root_source_file = .{ .path = "src/main_shell.zig" },
.root_source_file = b.path("src/main_shell.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(shell, options);
try common(b, shell, options);
try jsruntime_pkgs.add_shell(shell);
// do not install shell binary
b.installArtifact(shell);
// run
const shell_cmd = b.addRunArtifact(shell);
shell_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
shell_cmd.addArgs(args);
}
@@ -83,11 +103,25 @@ pub fn build(b: *std.build.Builder) !void {
// ----
// compile
const tests = b.addTest(.{ .root_source_file = .{ .path = "src/run_tests.zig" } });
try common(tests, options);
tests.single_threaded = true;
tests.test_runner = "src/test_runner.zig";
const tests = b.addTest(.{
.root_source_file = b.path("src/run_tests.zig"),
.test_runner = b.path("src/test_runner.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(b, tests, options);
// add jsruntime pretty deps
tests.root_module.addAnonymousImport("pretty", .{
.root_source_file = b.path("vendor/zig-js-runtime/src/pretty.zig"),
});
const run_tests = b.addRunArtifact(tests);
if (b.args) |args| {
run_tests.addArgs(args);
}
// step
const test_step = b.step("test", "Run unit tests");
@@ -99,16 +133,16 @@ pub fn build(b: *std.build.Builder) !void {
// compile and install
const wpt = b.addExecutable(.{
.name = "browsercore-wpt",
.root_source_file = .{ .path = "src/main_wpt.zig" },
.root_source_file = b.path("src/main_wpt.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(wpt, options);
b.installArtifact(wpt);
try common(b, wpt, options);
// run
const wpt_cmd = b.addRunArtifact(wpt);
wpt_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
wpt_cmd.addArgs(args);
}
@@ -122,16 +156,17 @@ pub fn build(b: *std.build.Builder) !void {
// compile and install
const get = b.addExecutable(.{
.name = "browsercore-get",
.root_source_file = .{ .path = "src/main_get.zig" },
.root_source_file = b.path("src/main_get.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(get, options);
try common(b, get, options);
b.installArtifact(get);
// run
const get_cmd = b.addRunArtifact(get);
get_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
get_cmd.addArgs(args);
}
@@ -141,29 +176,43 @@ pub fn build(b: *std.build.Builder) !void {
}
fn common(
step: *std.Build.CompileStep,
b: *std.Build,
step: *std.Build.Step.Compile,
options: jsruntime.Options,
) !void {
try jsruntime_pkgs.add(step, options);
linkLexbor(step);
linkNetSurf(step);
const jsruntimemod = try jsruntime_pkgs.module(
b,
options,
step.root_module.optimize.?,
step.root_module.resolved_target.?,
);
step.root_module.addImport("jsruntime", jsruntimemod);
const netsurf = moduleNetSurf(b);
netsurf.addImport("jsruntime", jsruntimemod);
step.root_module.addImport("netsurf", netsurf);
const tlsmod = b.addModule("tls", .{
.root_source_file = b.path("vendor/tls.zig/src/main.zig"),
});
step.root_module.addImport("tls", tlsmod);
}
fn linkLexbor(step: *std.build.LibExeObjStep) void {
// cmake . -DLEXBOR_BUILD_SHARED=OFF
const lib_path = "vendor/lexbor/liblexbor_static.a";
step.addObjectFile(.{ .path = lib_path });
step.addIncludePath(.{ .path = "vendor/lexbor-src/source" });
}
fn linkNetSurf(step: *std.build.LibExeObjStep) void {
fn moduleNetSurf(b: *std.Build) *std.Build.Module {
const mod = b.addModule("netsurf", .{
.root_source_file = b.path("src/netsurf/netsurf.zig"),
});
// iconv
step.addObjectFile(.{ .path = "vendor/libiconv/lib/libiconv.a" });
step.addIncludePath(.{ .path = "vendor/libiconv/include" });
mod.addObjectFile(b.path("vendor/libiconv/lib/libiconv.a"));
mod.addIncludePath(b.path("vendor/libiconv/include"));
// mimalloc
mod.addImport("mimalloc", moduleMimalloc(b));
// netsurf libs
const ns = "vendor/netsurf/";
const ns = "vendor/netsurf";
mod.addIncludePath(b.path(ns ++ "/include"));
const libs: [4][]const u8 = .{
"libdom",
"libhubbub",
@@ -171,8 +220,20 @@ fn linkNetSurf(step: *std.build.LibExeObjStep) void {
"libwapcaplet",
};
inline for (libs) |lib| {
step.addObjectFile(.{ .path = ns ++ "/lib/" ++ lib ++ ".a" });
step.addIncludePath(.{ .path = ns ++ lib ++ "/src" });
mod.addObjectFile(b.path(ns ++ "/lib/" ++ lib ++ ".a"));
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
}
step.addIncludePath(.{ .path = ns ++ "/include" });
return mod;
}
fn moduleMimalloc(b: *std.Build) *std.Build.Module {
const mod = b.addModule("mimalloc", .{
.root_source_file = b.path("src/mimalloc/mimalloc.zig"),
});
mod.addObjectFile(b.path("vendor/mimalloc/out/libmimalloc.a"));
mod.addIncludePath(b.path("vendor/mimalloc/out/include"));
return mod;
}

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const generate = @import("generate.zig");
const Console = @import("jsruntime").Console;
@@ -6,6 +24,8 @@ const DOM = @import("dom/dom.zig");
const HTML = @import("html/html.zig");
const Events = @import("events/event.zig");
const XHR = @import("xhr/xhr.zig");
const Storage = @import("storage/storage.zig");
const URL = @import("url/url.zig");
pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
@@ -16,4 +36,8 @@ pub const Interfaces = generate.Tuple(.{
Events.Interfaces,
HTML.Interfaces,
XHR.Interfaces,
Storage.Interfaces,
URL.Interfaces,
});
pub const UserContext = @import("user_context.zig").UserContext;

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,24 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const os = std.os;
const posix = std.posix;
const io = std.io;
const assert = std.debug.assert;
@@ -10,15 +28,15 @@ pub const Stream = struct {
alloc: std.mem.Allocator,
conn: *tcp.Conn,
handle: std.os.socket_t,
handle: posix.socket_t,
pub fn close(self: Stream) void {
os.closeSocket(self.handle);
posix.close(self.handle);
self.alloc.destroy(self.conn);
}
pub const ReadError = os.ReadError;
pub const WriteError = os.WriteError;
pub const ReadError = posix.ReadError;
pub const WriteError = posix.WriteError;
pub const Reader = io.Reader(Stream, ReadError, read);
pub const Writer = io.Writer(Stream, WriteError, write);
@@ -37,8 +55,8 @@ pub const Stream = struct {
};
}
pub fn readv(s: Stream, iovecs: []const os.iovec) ReadError!usize {
return os.readv(s.handle, iovecs);
pub fn readv(s: Stream, iovecs: []const posix.iovec) ReadError!usize {
return posix.readv(s.handle, iovecs);
}
/// Returns the number of bytes read. If the number read is smaller than
@@ -87,9 +105,9 @@ pub const Stream = struct {
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.fs.File.writev`.
pub fn writev(self: Stream, iovecs: []const os.iovec_const) WriteError!usize {
pub fn writev(self: Stream, iovecs: []const posix.iovec_const) WriteError!usize {
if (iovecs.len == 0) return 0;
const first_buffer = iovecs[0].iov_base[0..iovecs[0].iov_len];
const first_buffer = iovecs[0].base[0..iovecs[0].len];
return try self.write(first_buffer);
}
@@ -97,19 +115,19 @@ pub const Stream = struct {
/// order to handle partial writes from the underlying OS layer.
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.fs.File.writevAll`.
pub fn writevAll(self: Stream, iovecs: []os.iovec_const) WriteError!void {
pub fn writevAll(self: Stream, iovecs: []posix.iovec_const) WriteError!void {
if (iovecs.len == 0) return;
var i: usize = 0;
while (true) {
var amt = try self.writev(iovecs[i..]);
while (amt >= iovecs[i].iov_len) {
amt -= iovecs[i].iov_len;
while (amt >= iovecs[i].len) {
amt -= iovecs[i].len;
i += 1;
if (i >= iovecs.len) return;
}
iovecs[i].iov_base += amt;
iovecs[i].iov_len -= amt;
iovecs[i].base += amt;
iovecs[i].len -= amt;
}
}
};

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const net = std.net;
const Stream = @import("stream.zig").Stream;
@@ -41,19 +59,19 @@ pub const Conn = struct {
loop: *Loop,
pub fn connect(self: *Conn, socket: std.os.socket_t, address: std.net.Address) !void {
pub fn connect(self: *Conn, socket: std.posix.socket_t, address: std.net.Address) !void {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.connect(&cmd, socket, address);
_ = try cmd.wait();
}
pub fn send(self: *Conn, socket: std.os.socket_t, buffer: []const u8) !usize {
pub fn send(self: *Conn, socket: std.posix.socket_t, buffer: []const u8) !usize {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.send(&cmd, socket, buffer);
return try cmd.wait();
}
pub fn receive(self: *Conn, socket: std.os.socket_t, buffer: []u8) !usize {
pub fn receive(self: *Conn, socket: std.posix.socket_t, buffer: []u8) !usize {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.receive(&cmd, socket, buffer);
return try cmd.wait();
@@ -75,12 +93,12 @@ pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8,
else => return err,
};
}
return std.os.ConnectError.ConnectionRefused;
return std.posix.ConnectError.ConnectionRefused;
}
pub fn tcpConnectToAddress(alloc: std.mem.Allocator, loop: *Loop, addr: net.Address) !Stream {
const sockfd = try std.os.socket(addr.any.family, std.os.SOCK.STREAM, std.os.IPPROTO.TCP);
errdefer std.os.closeSocket(sockfd);
const sockfd = try std.posix.socket(addr.any.family, std.posix.SOCK.STREAM, std.posix.IPPROTO.TCP);
errdefer std.posix.close(sockfd);
var conn = try alloc.create(Conn);
conn.* = Conn{ .loop = loop };

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const http = std.http;
const Client = @import("Client.zig");
@@ -22,11 +40,9 @@ test "blocking mode fetch API" {
// force client's CA cert scan from system.
try client.ca_bundle.rescan(client.allocator);
var res = try client.fetch(alloc, .{
const res = try client.fetch(.{
.location = .{ .uri = try std.Uri.parse(url) },
.payload = .none,
});
defer res.deinit();
try std.testing.expect(res.status == .ok);
}
@@ -46,13 +62,13 @@ test "blocking mode open/send/wait API" {
// force client's CA cert scan from system.
try client.ca_bundle.rescan(client.allocator);
var headers = try std.http.Headers.initList(alloc, &[_]std.http.Field{});
defer headers.deinit();
var req = try client.open(.GET, try std.Uri.parse(url), headers, .{});
var buf: [2014]u8 = undefined;
var req = try client.open(.GET, try std.Uri.parse(url), .{
.server_header_buffer = &buf,
});
defer req.deinit();
try req.send(.{});
try req.send();
try req.finish();
try req.wait();
@@ -69,7 +85,6 @@ const AsyncClient = struct {
cli: *Client,
uri: std.Uri,
headers: std.http.Headers,
req: ?Request = undefined,
state: State = .new,
@@ -77,9 +92,10 @@ const AsyncClient = struct {
impl: YieldImpl,
err: ?anyerror = null,
buf: [2014]u8 = undefined,
pub fn deinit(self: *AsyncRequest) void {
if (self.req) |*r| r.deinit();
self.headers.deinit();
}
pub fn fetch(self: *AsyncRequest) void {
@@ -98,11 +114,13 @@ const AsyncClient = struct {
switch (self.state) {
.new => {
self.state = .open;
self.req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e);
self.req = self.cli.open(.GET, self.uri, .{
.server_header_buffer = &self.buf,
}) catch |e| return self.onerr(e);
},
.open => {
self.state = .send;
self.req.?.send(.{}) catch |e| return self.onerr(e);
self.req.?.send() catch |e| return self.onerr(e);
},
.send => {
self.state = .finish;
@@ -146,7 +164,6 @@ const AsyncClient = struct {
.impl = YieldImpl.init(self.cli.loop),
.cli = &self.cli,
.uri = uri,
.headers = .{ .allocator = self.cli.allocator, .owned = false },
};
}
};

View File

@@ -1,8 +1,27 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const Types = @import("root").Types;
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const Loader = @import("loader.zig").Loader;
const Dump = @import("dump.zig");
const Mime = @import("mime.zig");
@@ -16,7 +35,12 @@ const apiweb = @import("../apiweb.zig");
const Window = @import("../html/window.zig").Window;
const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
const FetchResult = std.http.Client.FetchResult;
const storage = @import("../storage/storage.zig");
const FetchResult = @import("../http/Client.zig").Client.FetchResult;
const UserContext = @import("../user_context.zig").UserContext;
const HttpClient = @import("../async/Client.zig");
const log = std.log.scoped(.browser);
@@ -68,6 +92,10 @@ pub const Session = struct {
env: Env = undefined,
loop: Loop,
window: Window,
// TODO move the shed to the browser?
storageShed: storage.Shed,
page: ?*Page = null,
httpClient: HttpClient,
jstypes: [Types.len]usize = undefined,
@@ -80,19 +108,26 @@ pub const Session = struct {
.window = Window.create(null),
.loader = Loader.init(alloc),
.loop = try Loop.init(alloc),
.storageShed = storage.Shed.init(alloc),
.httpClient = undefined,
};
self.env = try Env.init(self.arena.allocator(), &self.loop);
self.env = try Env.init(self.arena.allocator(), &self.loop, null);
self.httpClient = .{ .allocator = alloc, .loop = &self.loop };
try self.env.load(&self.jstypes);
return self;
}
fn deinit(self: *Session) void {
if (self.page) |page| page.end();
self.env.deinit();
self.arena.deinit();
self.httpClient.deinit();
self.loader.deinit();
self.storageShed.deinit();
self.loop.deinit();
self.alloc.destroy(self);
}
@@ -115,17 +150,21 @@ pub const Page = struct {
// handle url
rawuri: ?[]const u8 = null,
uri: std.Uri = undefined,
origin: ?[]const u8 = null,
raw_data: ?[]const u8 = null,
fn init(
alloc: std.mem.Allocator,
session: *Session,
) Page {
return Page{
) !Page {
if (session.page != null) return error.SessionPageExists;
var page = Page{
.arena = std.heap.ArenaAllocator.init(alloc),
.session = session,
};
session.page = &page;
return page;
}
// reset js env and mem arena.
@@ -133,11 +172,15 @@ pub const Page = struct {
self.session.env.stop();
// TODO unload document: https://html.spec.whatwg.org/#unloading-documents
// clear netsurf memory arena.
parser.deinit();
_ = self.arena.reset(.free_all);
}
pub fn deinit(self: *Page) void {
self.arena.deinit();
self.session.page = null;
}
// dump writes the page content into the given file.
@@ -151,7 +194,28 @@ pub const Page = struct {
}
// if the page has a pointer to a document, dumps the HTML.
try Dump.htmlFile(self.doc.?, out);
try Dump.writeHTML(self.doc.?, out);
}
pub fn wait(self: *Page) !void {
// try catch
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(self.session.env);
defer try_catch.deinit();
self.session.env.wait() catch |err| {
// the js env could not be started if the document wasn't an HTML.
if (err == error.EnvNotStarted) return;
const alloc = self.arena.allocator();
if (try try_catch.err(alloc, self.session.env)) |msg| {
defer alloc.free(msg);
log.info("wait error: {s}", .{msg});
return;
}
};
log.debug("wait: OK", .{});
}
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
@@ -163,7 +227,16 @@ pub const Page = struct {
// own the url
if (self.rawuri) |prev| alloc.free(prev);
self.rawuri = try alloc.dupe(u8, uri);
self.uri = std.Uri.parse(self.rawuri.?) catch try std.Uri.parseWithoutScheme(self.rawuri.?);
self.uri = std.Uri.parse(self.rawuri.?) catch try std.Uri.parseAfterScheme("", self.rawuri.?);
// prepare origin value.
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try self.uri.writeToStream(.{
.scheme = true,
.authority = true,
}, buf.writer());
self.origin = try buf.toOwnedSlice();
// TODO handle fragment in url.
@@ -177,29 +250,39 @@ pub const Page = struct {
// TODO handle redirection
if (req.response.status != .ok) {
log.debug("{?} {d} {s}\n{any}", .{
log.debug("{?} {d} {s}", .{
req.response.version,
req.response.status,
req.response.reason,
req.response.headers,
// TODO log headers
});
return error.BadStatusCode;
}
// TODO handle charset
// https://html.spec.whatwg.org/#content-type
const ct = req.response.headers.getFirstValue("Content-Type") orelse {
var it = req.response.iterateHeaders();
var ct: ?[]const u8 = null;
while (true) {
const h = it.next() orelse break;
if (std.ascii.eqlIgnoreCase(h.name, "Content-Type")) {
ct = try alloc.dupe(u8, h.value);
}
}
if (ct == null) {
// no content type in HTTP headers.
// TODO try to sniff mime type from the body.
log.info("no content-type HTTP header", .{});
return;
};
log.debug("header content-type: {s}", .{ct});
const mime = try Mime.parse(ct);
}
defer alloc.free(ct.?);
log.debug("header content-type: {s}", .{ct.?});
const mime = try Mime.parse(ct.?);
if (mime.eql(Mime.HTML)) {
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8");
} else {
log.info("non-HTML document: {s}", .{ct});
log.info("non-HTML document: {s}", .{ct.?});
// save the body into the page.
self.raw_data = try req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
@@ -210,6 +293,9 @@ pub const Page = struct {
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
const alloc = self.arena.allocator();
// start netsurf memory arena.
try parser.init();
log.debug("parse html with charset {s}", .{charset});
const ccharset = try alloc.dupeZ(u8, charset);
@@ -224,23 +310,32 @@ pub const Page = struct {
// TODO set document.readyState to interactive
// https://html.spec.whatwg.org/#reporting-document-loading-status
// TODO inject the URL to the document including the fragment.
// inject the URL to the document including the fragment.
try parser.documentSetDocumentURI(doc, self.rawuri orelse "about:blank");
// TODO set the referrer to the document.
self.session.window.replaceDocument(doc);
self.session.window.replaceDocument(html_doc);
self.session.window.setStorageShelf(
try self.session.storageShed.getOrPut(self.origin orelse "null"),
);
// https://html.spec.whatwg.org/#read-html
// start JS env
// TODO load the js env concurrently with the HTML parsing.
log.debug("start js env", .{});
try self.session.env.start(alloc);
try self.session.env.start();
// replace the user context document with the new one.
try self.session.env.setUserContext(.{
.document = html_doc,
.httpClient = &self.session.httpClient,
});
// add global objects
log.debug("setup global env", .{});
try self.session.env.addObject(self.session.window, "window");
try self.session.env.addObject(self.session.window, "self");
try self.session.env.addObject(html_doc, "document");
try self.session.env.bindGlobal(&self.session.window);
// browse the DOM tree to retrieve scripts
// TODO execute the synchronous scripts during the HTL parsing.
@@ -326,6 +421,8 @@ pub const Page = struct {
// have loaded.
// https://html.spec.whatwg.org/#reporting-document-loading-status
const evt = try parser.eventCreate();
defer parser.eventDestroy(evt);
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
@@ -340,8 +437,13 @@ pub const Page = struct {
// dispatch window.load event
const loadevt = try parser.eventCreate();
defer parser.eventDestroy(loadevt);
try parser.eventInit(loadevt, "load", .{});
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(Window, &self.session.window), loadevt);
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, &self.session.window),
loadevt,
);
}
// evalScript evaluates the src in priority.
@@ -374,19 +476,26 @@ pub const Page = struct {
return;
}
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(self.session.env);
defer try_catch.deinit();
const opt_text = try parser.nodeTextContent(parser.elementToNode(e));
if (opt_text) |text| {
// TODO handle charset attribute
var res = jsruntime.JSResult{};
try self.session.env.run(alloc, text, "", &res, null);
defer res.deinit(alloc);
const res = self.session.env.exec(text, "") catch {
if (try try_catch.err(alloc, self.session.env)) |msg| {
defer alloc.free(msg);
log.info("eval inline {s}: {s}", .{ text, msg });
}
return;
};
if (res.success) {
log.debug("eval inline: {s}", .{res.result});
} else {
log.info("eval inline: {s}", .{res.result});
if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, self.session.env);
defer alloc.free(msg);
log.debug("eval inline {s}", .{msg});
}
return;
}
@@ -408,30 +517,42 @@ pub const Page = struct {
log.debug("starting fetch script {s}", .{src});
const u = std.Uri.parse(src) catch try std.Uri.parseWithoutScheme(src);
const ru = try std.Uri.resolve(self.uri, u, false, alloc);
var buffer: [1024]u8 = undefined;
var b: []u8 = buffer[0..];
const u = try std.Uri.resolve_inplace(self.uri, src, &b);
var fetchres = try self.session.loader.fetch(alloc, ru);
var fetchres = try self.session.loader.get(alloc, u);
defer fetchres.deinit();
log.info("fech script {any}: {d}", .{ ru, fetchres.status });
const resp = fetchres.req.response;
if (fetchres.status != .ok) return FetchError.BadStatusCode;
log.info("fech script {any}: {d}", .{ u, resp.status });
if (resp.status != .ok) return FetchError.BadStatusCode;
// TODO check content-type
const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
defer alloc.free(body);
// check no body
if (fetchres.body == null) return FetchError.NoBody;
if (body.len == 0) return FetchError.NoBody;
var res = jsruntime.JSResult{};
try self.session.env.run(alloc, fetchres.body.?, src, &res, null);
defer res.deinit(alloc);
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(self.session.env);
defer try_catch.deinit();
if (res.success) {
log.debug("eval remote {s}: {s}", .{ src, res.result });
} else {
log.info("eval remote {s}: {s}", .{ src, res.result });
const res = self.session.env.exec(body, src) catch {
if (try try_catch.err(alloc, self.session.env)) |msg| {
defer alloc.free(msg);
log.info("eval remote {s}: {s}", .{ src, msg });
}
return FetchError.JsErr;
};
if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, self.session.env);
defer alloc.free(msg);
log.debug("eval remote {s}: {s}", .{ src, msg });
}
}

View File

@@ -1,16 +1,36 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const File = std.fs.File;
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const Walker = @import("../dom/walker.zig").WalkerChildren;
pub fn htmlFile(doc: *parser.Document, out: File) !void {
try out.writeAll("<!DOCTYPE html>\n");
try nodeFile(parser.documentToNode(doc), out);
try out.writeAll("\n");
// writer must be a std.io.Writer
pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {
try writer.writeAll("<!DOCTYPE html>\n");
try writeNode(parser.documentToNode(doc), writer);
try writer.writeAll("\n");
}
fn nodeFile(root: *parser.Node, out: File) !void {
// writer must be a std.io.Writer
pub fn writeNode(root: *parser.Node, writer: anytype) !void {
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
@@ -19,8 +39,8 @@ fn nodeFile(root: *parser.Node, out: File) !void {
.element => {
// open the tag
const tag = try parser.nodeLocalName(next.?);
try out.writeAll("<");
try out.writeAll(tag);
try writer.writeAll("<");
try writer.writeAll(tag);
// write the attributes
const map = try parser.nodeGetAttributes(next.?);
@@ -28,40 +48,43 @@ fn nodeFile(root: *parser.Node, out: File) !void {
var i: u32 = 0;
while (i < ln) {
const attr = try parser.namedNodeMapItem(map, i) orelse break;
try out.writeAll(" ");
try out.writeAll(try parser.attributeGetName(attr));
try out.writeAll("=\"");
try out.writeAll(try parser.attributeGetValue(attr) orelse "");
try out.writeAll("\"");
try writer.writeAll(" ");
try writer.writeAll(try parser.attributeGetName(attr));
try writer.writeAll("=\"");
try writer.writeAll(try parser.attributeGetValue(attr) orelse "");
try writer.writeAll("\"");
i += 1;
}
try out.writeAll(">");
try writer.writeAll(">");
// void elements can't have any content.
if (try isVoid(parser.nodeToElement(next.?))) continue;
// write the children
// TODO avoid recursion
try nodeFile(next.?, out);
try writeNode(next.?, writer);
// close the tag
try out.writeAll("</");
try out.writeAll(tag);
try out.writeAll(">");
try writer.writeAll("</");
try writer.writeAll(tag);
try writer.writeAll(">");
},
.text => {
const v = try parser.nodeValue(next.?) orelse continue;
try out.writeAll(v);
try writer.writeAll(v);
},
.cdata_section => {
const v = try parser.nodeValue(next.?) orelse continue;
try out.writeAll("<![CDATA[");
try out.writeAll(v);
try out.writeAll("]]>");
try writer.writeAll("<![CDATA[");
try writer.writeAll(v);
try writer.writeAll("]]>");
},
.comment => {
const v = try parser.nodeValue(next.?) orelse continue;
try out.writeAll("<!--");
try out.writeAll(v);
try out.writeAll("-->");
try writer.writeAll("<!--");
try writer.writeAll(v);
try writer.writeAll("-->");
},
// TODO handle processing instruction dump
.processing_instruction => continue,
@@ -81,8 +104,21 @@ fn nodeFile(root: *parser.Node, out: File) !void {
}
}
// HTMLFileTestFn is run by run_tests.zig
pub fn HTMLFileTestFn(out: File) !void {
// area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
// https://html.spec.whatwg.org/#void-elements
fn isVoid(elem: *parser.Element) !bool {
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(elem)));
return switch (tag) {
.area, .base, .br, .col, .embed, .hr, .img, .input, .link => true,
.meta, .source, .track, .wbr => true,
else => false,
};
}
test "dump.writeHTML" {
const out = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only });
defer out.close();
const file = try std.fs.cwd().openFile("test.html", .{});
defer file.close();
@@ -92,5 +128,5 @@ pub fn HTMLFileTestFn(out: File) !void {
const doc = parser.documentHTMLToDocument(doc_html);
try htmlFile(doc, out);
try writeHTML(doc, out);
}

View File

@@ -1,13 +1,34 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Client = @import("../http/Client.zig");
const user_agent = "Lightpanda.io/1.0";
pub const Loader = struct {
client: std.http.Client,
client: Client,
// use 64KB for headers buffer size.
server_header_buffer: [1024 * 64]u8 = undefined,
pub const Response = struct {
alloc: std.mem.Allocator,
req: *std.http.Client.Request,
req: *Client.Request,
pub fn deinit(self: *Response) void {
self.req.deinit();
@@ -17,7 +38,7 @@ pub const Loader = struct {
pub fn init(alloc: std.mem.Allocator) Loader {
return Loader{
.client = std.http.Client{
.client = Client{
.allocator = alloc,
},
};
@@ -27,46 +48,30 @@ pub const Loader = struct {
self.client.deinit();
}
// the caller must deinit the FetchResult.
pub fn fetch(self: *Loader, alloc: std.mem.Allocator, uri: std.Uri) !std.http.Client.FetchResult {
var headers = try std.http.Headers.initList(alloc, &[_]std.http.Field{
.{ .name = "User-Agent", .value = user_agent },
.{ .name = "Accept", .value = "*/*" },
.{ .name = "Accept-Language", .value = "en-US,en;q=0.5" },
});
defer headers.deinit();
return try self.client.fetch(alloc, .{
.location = .{ .uri = uri },
.headers = headers,
.payload = .none,
});
}
// see
// https://ziglang.org/documentation/master/std/#A;std:http.Client.fetch
// for reference.
// The caller is responsible for calling `deinit()` on the `Response`.
pub fn get(self: *Loader, alloc: std.mem.Allocator, uri: std.Uri) !Response {
var headers = try std.http.Headers.initList(alloc, &[_]std.http.Field{
.{ .name = "User-Agent", .value = user_agent },
.{ .name = "Accept", .value = "*/*" },
.{ .name = "Accept-Language", .value = "en-US,en;q=0.5" },
});
defer headers.deinit();
var resp = Response{
.alloc = alloc,
.req = try alloc.create(std.http.Client.Request),
.req = try alloc.create(Client.Request),
};
errdefer alloc.destroy(resp.req);
resp.req.* = try self.client.open(.GET, uri, headers, .{
.handle_redirects = true, // TODO handle redirects manually
resp.req.* = try self.client.open(.GET, uri, .{
.headers = .{
.user_agent = .{ .override = user_agent },
},
.extra_headers = &.{
.{ .name = "Accept", .value = "*/*" },
.{ .name = "Accept-Language", .value = "en-US,en;q=0.5" },
},
.server_header_buffer = &self.server_header_buffer,
});
errdefer resp.req.deinit();
try resp.req.send(.{});
try resp.req.send();
try resp.req.finish();
try resp.req.wait();
@@ -74,13 +79,13 @@ pub const Loader = struct {
}
};
test "basic url fetch" {
test "basic url get" {
const alloc = std.testing.allocator;
var loader = Loader.init(alloc);
defer loader.deinit();
var result = try loader.fetch(alloc, "https://en.wikipedia.org/wiki/Main_Page");
var result = try loader.get(alloc, "https://en.wikipedia.org/wiki/Main_Page");
defer result.deinit();
try std.testing.expect(result.status == std.http.Status.ok);
try std.testing.expect(result.req.response.status == std.http.Status.ok);
}

View File

@@ -1,6 +1,28 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const testing = std.testing;
const strparser = @import("../str/parser.zig");
const Reader = strparser.Reader;
const trim = strparser.trim;
const Self = @This();
const MimeError = error{
@@ -21,91 +43,6 @@ pub const Empty = Self{ .mtype = "", .msubtype = "" };
pub const HTML = Self{ .mtype = "text", .msubtype = "html" };
pub const Javascript = Self{ .mtype = "application", .msubtype = "javascript" };
const reader = struct {
s: []const u8,
i: usize = 0,
fn until(self: *reader, c: u8) []const u8 {
const ln = self.s.len;
const start = self.i;
while (self.i < ln) {
if (c == self.s[self.i]) return self.s[start..self.i];
self.i += 1;
}
return self.s[start..self.i];
}
fn tail(self: *reader) []const u8 {
if (self.i > self.s.len) return "";
defer self.i = self.s.len;
return self.s[self.i..];
}
fn skip(self: *reader) bool {
if (self.i >= self.s.len) return false;
self.i += 1;
return true;
}
};
test "reader.skip" {
var r = reader{ .s = "foo" };
try testing.expect(r.skip());
try testing.expect(r.skip());
try testing.expect(r.skip());
try testing.expect(!r.skip());
try testing.expect(!r.skip());
}
test "reader.tail" {
var r = reader{ .s = "foo" };
try testing.expectEqualStrings("foo", r.tail());
try testing.expectEqualStrings("", r.tail());
}
test "reader.until" {
var r = reader{ .s = "foo.bar.baz" };
try testing.expectEqualStrings("foo", r.until('.'));
_ = r.skip();
try testing.expectEqualStrings("bar", r.until('.'));
_ = r.skip();
try testing.expectEqualStrings("baz", r.until('.'));
r = reader{ .s = "foo" };
try testing.expectEqualStrings("foo", r.until('.'));
r = reader{ .s = "" };
try testing.expectEqualStrings("", r.until('.'));
}
fn trim(s: []const u8) []const u8 {
const ln = s.len;
if (ln == 0) {
return "";
}
var start: usize = 0;
while (start < ln) {
if (!std.ascii.isWhitespace(s[start])) break;
start += 1;
}
var end: usize = ln;
while (end > 0) {
if (!std.ascii.isWhitespace(s[end - 1])) break;
end -= 1;
}
return s[start..end];
}
test "trim" {
try testing.expectEqualStrings("", trim(""));
try testing.expectEqualStrings("foo", trim("foo"));
try testing.expectEqualStrings("foo", trim(" \n\tfoo"));
try testing.expectEqualStrings("foo", trim("foo \n\t"));
}
// https://mimesniff.spec.whatwg.org/#http-token-code-point
fn isHTTPCodePoint(c: u8) bool {
return switch (c) {
@@ -133,7 +70,7 @@ pub fn parse(s: []const u8) Self.MimeError!Self {
if (ln > 255) return MimeError.TooBig;
var res = Self{ .mtype = "", .msubtype = "" };
var r = reader{ .s = s };
var r = Reader{ .s = s };
res.mtype = trim(r.until('/'));
if (res.mtype.len == 0) return MimeError.Invalid;
@@ -150,7 +87,7 @@ pub fn parse(s: []const u8) Self.MimeError!Self {
// parse well known parameters.
// don't check invalid parameter format.
var rp = reader{ .s = res.params };
var rp = Reader{ .s = res.params };
while (true) {
const name = trim(rp.until('='));
if (!rp.skip()) return res;

218
src/css/README.md Normal file
View File

@@ -0,0 +1,218 @@
# css
Lightpanda css implements CSS selectors parsing and matching in Zig.
This package is a port of the Go lib [andybalholm/cascadia](https://github.com/andybalholm/cascadia).
## Usage
### Query parser
```zig
const css = @import("css.zig");
const selector = try css.parse(alloc, "h1", .{});
defer selector.deinit(alloc);
```
### DOM tree match
The lib expects a `Node` interface implementation to match your DOM tree.
```zig
pub const Node = struct {
pub fn firstChild(_: Node) !?Node {
return error.TODO;
}
pub fn lastChild(_: Node) !?Node {
return error.TODO;
}
pub fn nextSibling(_: Node) !?Node {
return error.TODO;
}
pub fn prevSibling(_: Node) !?Node {
return error.TODO;
}
pub fn parent(_: Node) !?Node {
return error.TODO;
}
pub fn isElement(_: Node) bool {
return false;
}
pub fn isDocument(_: Node) bool {
return false;
}
pub fn isComment(_: Node) bool {
return false;
}
pub fn isText(_: Node) bool {
return false;
}
pub fn isEmptyText(_: Node) !bool {
return error.TODO;
}
pub fn tag(_: Node) ![]const u8 {
return error.TODO;
}
pub fn attr(_: Node, _: []const u8) !?[]const u8 {
return error.TODO;
}
pub fn eql(_: Node, _: Node) bool {
return false;
}
};
```
You also need do define a `Matcher` implementing a `match` function to
accumulate the results.
```zig
const Matcher = struct {
const Nodes = std.ArrayList(Node);
nodes: Nodes,
fn init(alloc: std.mem.Allocator) Matcher {
return .{ .nodes = Nodes.init(alloc) };
}
fn deinit(m: *Matcher) void {
m.nodes.deinit();
}
pub fn match(m: *Matcher, n: Node) !void {
try m.nodes.append(n);
}
};
```
Then you can use the lib itself.
```zig
var matcher = Matcher.init(alloc);
defer matcher.deinit();
try css.matchAll(selector, node, &matcher);
_ = try css.matchFirst(selector, node, &matcher); // returns true if a node matched.
```
## Features
* [x] parse query selector
* [x] `matchAll`
* [x] `matchFirst`
* [ ] specificity
### Selectors implemented
#### Selectors
* [x] Class selectors
* [x] Id selectors
* [x] Type selectors
* [x] Universal selectors
* [ ] Nesting selectors
#### Combinators
* [x] Child combinator
* [ ] Column combinator
* [x] Descendant combinator
* [ ] Namespace combinator
* [x] Next-sibling combinator
* [x] Selector list combinator
* [x] Subsequent-sibling combinator
#### Attribute
* [x] `[attr]`
* [x] `[attr=value]`
* [x] `[attr|=value]`
* [x] `[attr^=value]`
* [x] `[attr$=value]`
* [ ] `[attr*=value]`
* [x] `[attr operator value i]`
* [ ] `[attr operator value s]`
#### Pseudo classes
* [ ] `:active`
* [ ] `:any-link`
* [ ] `:autofill`
* [ ] `:blank Experimental`
* [x] `:checked`
* [ ] `:current Experimental`
* [ ] `:default`
* [ ] `:defined`
* [ ] `:dir() Experimental`
* [x] `:disabled`
* [x] `:empty`
* [x] `:enabled`
* [ ] `:first`
* [x] `:first-child`
* [x] `:first-of-type`
* [ ] `:focus`
* [ ] `:focus-visible`
* [ ] `:focus-within`
* [ ] `:fullscreen`
* [ ] `:future Experimental`
* [x] `:has() Experimental`
* [ ] `:host`
* [ ] `:host()`
* [ ] `:host-context() Experimental`
* [ ] `:hover`
* [ ] `:indeterminate`
* [ ] `:in-range`
* [ ] `:invalid`
* [ ] `:is()`
* [x] `:lang()`
* [x] `:last-child`
* [x] `:last-of-type`
* [ ] `:left`
* [x] `:link`
* [ ] `:local-link Experimental`
* [ ] `:modal`
* [x] `:not()`
* [x] `:nth-child()`
* [x] `:nth-last-child()`
* [x] `:nth-last-of-type()`
* [x] `:nth-of-type()`
* [x] `:only-child`
* [x] `:only-of-type`
* [ ] `:optional`
* [ ] `:out-of-range`
* [ ] `:past Experimental`
* [ ] `:paused`
* [ ] `:picture-in-picture`
* [ ] `:placeholder-shown`
* [ ] `:playing`
* [ ] `:read-only`
* [ ] `:read-write`
* [ ] `:required`
* [ ] `:right`
* [x] `:root`
* [ ] `:scope`
* [ ] `:state() Experimental`
* [ ] `:target`
* [ ] `:target-within Experimental`
* [ ] `:user-invalid Experimental`
* [ ] `:valid`
* [ ] `:visited`
* [ ] `:where()`
* [ ] `:contains()`
* [ ] `:containsown()`
* [ ] `:matched()`
* [ ] `:matchesown()`
* [x] `:root`

176
src/css/css.zig Normal file
View File

@@ -0,0 +1,176 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// CSS Selector parser and query
// This package is a rewrite in Zig of Cascadia CSS Selector parser.
// see https://github.com/andybalholm/cascadia
const std = @import("std");
const Selector = @import("selector.zig").Selector;
const parser = @import("parser.zig");
// parse parse a selector string and returns the parsed result or an error.
pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions) parser.ParseError!Selector {
var p = parser.Parser{ .s = s, .i = 0, .opts = opts };
return p.parse(alloc);
}
// matchFirst call m.match with the first node that matches the selector s, from the
// descendants of n and returns true. If none matches, it returns false.
pub fn matchFirst(s: Selector, node: anytype, m: anytype) !bool {
var c = try node.firstChild();
while (true) {
if (c == null) break;
if (try s.match(c.?)) {
try m.match(c.?);
return true;
}
if (try matchFirst(s, c.?, m)) return true;
c = try c.?.nextSibling();
}
return false;
}
// matchAll call m.match with the all the nodes that matches the selector s, from the
// descendants of n.
pub fn matchAll(s: Selector, node: anytype, m: anytype) !void {
var c = try node.firstChild();
while (true) {
if (c == null) break;
if (try s.match(c.?)) try m.match(c.?);
try matchAll(s, c.?, m);
c = try c.?.nextSibling();
}
}
test "parse" {
const alloc = std.testing.allocator;
const testcases = [_][]const u8{
"address",
"*",
"#foo",
"li#t1",
"*#t4",
".t1",
"p.t1",
"div.teST",
".t1.fail",
"p.t1.t2",
"p.--t1",
"p.--t1.--t2",
"p[title]",
"div[class=\"red\" i]",
"address[title=\"foo\"]",
"address[title=\"FoOIgnoRECaSe\" i]",
"address[title!=\"foo\"]",
"address[title!=\"foo\" i]",
"p[title!=\"FooBarUFoo\" i]",
"[ \t title ~= foo ]",
"p[title~=\"FOO\" i]",
"p[title~=toofoo i]",
"[title~=\"hello world\"]",
"[title~=\"hello\" i]",
"[title~=\"hello\" I]",
"[lang|=\"en\"]",
"[lang|=\"EN\" i]",
"[lang|=\"EN\" i]",
"[title^=\"foo\"]",
"[title^=\"foo\" i]",
"[title$=\"bar\"]",
"[title$=\"BAR\" i]",
"[title*=\"bar\"]",
"[title*=\"BaRu\" i]",
"[title*=\"BaRu\" I]",
"p[class$=\" \"]",
"p[class$=\"\"]",
"p[class^=\" \"]",
"p[class^=\"\"]",
"p[class*=\" \"]",
"p[class*=\"\"]",
"input[name=Sex][value=F]",
"table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]",
".t1:not(.t2)",
"div:not(.t1)",
"div:not([class=\"t2\"])",
"li:nth-child(odd)",
"li:nth-child(even)",
"li:nth-child(-n+2)",
"li:nth-child(3n+1)",
"li:nth-last-child(odd)",
"li:nth-last-child(even)",
"li:nth-last-child(-n+2)",
"li:nth-last-child(3n+1)",
"span:first-child",
"span:last-child",
"p:nth-of-type(2)",
"p:nth-last-of-type(2)",
"p:last-of-type",
"p:first-of-type",
"p:only-child",
"p:only-of-type",
":empty",
"div p",
"div table p",
"div > p",
"p ~ p",
"p + p",
"li, p",
"p +/*This is a comment*/ p",
"p:contains(\"that wraps\")",
"p:containsOwn(\"that wraps\")",
":containsOwn(\"inner\")",
"p:containsOwn(\"block\")",
"div:has(#p1)",
"div:has(:containsOwn(\"2\"))",
"body :has(:containsOwn(\"2\"))",
"body :haschild(:containsOwn(\"2\"))",
"p:matches([\\d])",
"p:matches([a-z])",
"p:matches([a-zA-Z])",
"p:matches([^\\d])",
"p:matches(^(0|a))",
"p:matches(^\\d+$)",
"p:not(:matches(^\\d+$))",
"div :matchesOwn(^\\d+$)",
"[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])",
"[href#=(^https:\\/\\/[^\\/]*\\/?news)]",
":input",
":root",
"*:root",
"html:nth-child(1)",
"*:root:first-child",
"*:root:nth-child(1)",
"a:not(:root)",
"body > *:nth-child(3n+2)",
"input:disabled",
":disabled",
":enabled",
"div.class1, div.class2",
};
for (testcases) |tc| {
const s = parse(alloc, tc, .{}) catch |e| {
std.debug.print("query {s}", .{tc});
return e;
};
defer s.deinit(alloc);
}
}

102
src/css/libdom.zig Normal file
View File

@@ -0,0 +1,102 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("netsurf");
// Node implementation with Netsurf Libdom C lib.
pub const Node = struct {
node: *parser.Node,
pub fn firstChild(n: Node) !?Node {
const c = try parser.nodeFirstChild(n.node);
if (c) |cc| return .{ .node = cc };
return null;
}
pub fn lastChild(n: Node) !?Node {
const c = try parser.nodeLastChild(n.node);
if (c) |cc| return .{ .node = cc };
return null;
}
pub fn nextSibling(n: Node) !?Node {
const c = try parser.nodeNextSibling(n.node);
if (c) |cc| return .{ .node = cc };
return null;
}
pub fn prevSibling(n: Node) !?Node {
const c = try parser.nodePreviousSibling(n.node);
if (c) |cc| return .{ .node = cc };
return null;
}
pub fn parent(n: Node) !?Node {
const c = try parser.nodeParentNode(n.node);
if (c) |cc| return .{ .node = cc };
return null;
}
pub fn isElement(n: Node) bool {
const t = parser.nodeType(n.node) catch return false;
return t == .element;
}
pub fn isDocument(n: Node) bool {
const t = parser.nodeType(n.node) catch return false;
return t == .document;
}
pub fn isComment(n: Node) bool {
const t = parser.nodeType(n.node) catch return false;
return t == .comment;
}
pub fn isText(n: Node) bool {
const t = parser.nodeType(n.node) catch return false;
return t == .text;
}
pub fn isEmptyText(n: Node) !bool {
const data = try parser.nodeTextContent(n.node);
if (data == null) return true;
if (data.?.len == 0) return true;
return std.mem.trim(u8, data.?, &std.ascii.whitespace).len == 0;
}
pub fn tag(n: Node) ![]const u8 {
return try parser.nodeName(n.node);
}
pub fn attr(n: Node, key: []const u8) !?[]const u8 {
if (!n.isElement()) return null;
return try parser.elementGetAttribute(parser.nodeToElement(n.node), key);
}
pub fn eql(a: Node, b: Node) bool {
return a.node == b.node;
}
};

325
src/css/libdom_test.zig Normal file
View File

@@ -0,0 +1,325 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const css = @import("css.zig");
const Node = @import("libdom.zig").Node;
const parser = @import("netsurf");
const Matcher = struct {
const Nodes = std.ArrayList(Node);
nodes: Nodes,
fn init(alloc: std.mem.Allocator) Matcher {
return .{ .nodes = Nodes.init(alloc) };
}
fn deinit(m: *Matcher) void {
m.nodes.deinit();
}
fn reset(m: *Matcher) void {
m.nodes.clearRetainingCapacity();
}
pub fn match(m: *Matcher, n: Node) !void {
try m.nodes.append(n);
}
};
test "matchFirst" {
const alloc = std.testing.allocator;
var matcher = Matcher.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
html: []const u8,
exp: usize,
}{
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
};
for (testcases) |tc| {
matcher.reset();
const doc = try parser.documentHTMLParseFromStr(tc.html);
defer parser.documentHTMLClose(doc) catch {};
const s = css.parse(alloc, tc.q, .{}) catch |e| {
std.debug.print("parse, query: {s}\n", .{tc.q});
return e;
};
defer s.deinit(alloc);
const node = Node{ .node = parser.documentHTMLToNode(doc) };
_ = css.matchFirst(s, node, &matcher) catch |e| {
std.debug.print("match, query: {s}\n", .{tc.q});
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("expectation, query: {s}\n", .{tc.q});
return e;
};
}
}
test "matchAll" {
const alloc = std.testing.allocator;
var matcher = Matcher.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
html: []const u8,
exp: usize,
}{
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 2 },
};
for (testcases) |tc| {
matcher.reset();
const doc = try parser.documentHTMLParseFromStr(tc.html);
defer parser.documentHTMLClose(doc) catch {};
const s = css.parse(alloc, tc.q, .{}) catch |e| {
std.debug.print("parse, query: {s}\n", .{tc.q});
return e;
};
defer s.deinit(alloc);
const node = Node{ .node = parser.documentHTMLToNode(doc) };
_ = css.matchAll(s, node, &matcher) catch |e| {
std.debug.print("match, query: {s}\n", .{tc.q});
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("expectation, query: {s}\n", .{tc.q});
return e;
};
}
}

587
src/css/match_test.zig Normal file
View File

@@ -0,0 +1,587 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const css = @import("css.zig");
// Node mock implementation for test only.
pub const Node = struct {
child: ?*const Node = null,
last: ?*const Node = null,
sibling: ?*const Node = null,
prev: ?*const Node = null,
par: ?*const Node = null,
name: []const u8 = "",
att: ?[]const u8 = null,
pub fn firstChild(n: *const Node) !?*const Node {
return n.child;
}
pub fn lastChild(n: *const Node) !?*const Node {
return n.last;
}
pub fn nextSibling(n: *const Node) !?*const Node {
return n.sibling;
}
pub fn prevSibling(n: *const Node) !?*const Node {
return n.prev;
}
pub fn parent(n: *const Node) !?*const Node {
return n.par;
}
pub fn isElement(_: *const Node) bool {
return true;
}
pub fn isDocument(_: *const Node) bool {
return false;
}
pub fn isComment(_: *const Node) bool {
return false;
}
pub fn isText(_: *const Node) bool {
return false;
}
pub fn isEmptyText(_: *const Node) !bool {
return false;
}
pub fn tag(n: *const Node) ![]const u8 {
return n.name;
}
pub fn attr(n: *const Node, _: []const u8) !?[]const u8 {
return n.att;
}
pub fn eql(a: *const Node, b: *const Node) bool {
return a == b;
}
};
const Matcher = struct {
const Nodes = std.ArrayList(*const Node);
nodes: Nodes,
fn init(alloc: std.mem.Allocator) Matcher {
return .{ .nodes = Nodes.init(alloc) };
}
fn deinit(m: *Matcher) void {
m.nodes.deinit();
}
fn reset(m: *Matcher) void {
m.nodes.clearRetainingCapacity();
}
pub fn match(m: *Matcher, n: *const Node) !void {
try m.nodes.append(n);
}
};
test "matchFirst" {
const alloc = std.testing.allocator;
var matcher = Matcher.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
n: Node,
exp: usize,
}{
.{
.q = "address",
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
.exp = 1,
},
.{
.q = "#foo",
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
.exp = 1,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
.exp = 0,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 0,
},
.{
.q = "[foo!=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo!=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
.exp = 1,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 0,
},
.{
.q = "[foo^=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo$=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo*=rb]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
.exp = 0,
},
.{
.q = "strong, a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
.name = "a",
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
} } } },
.exp = 1,
},
.{
.q = ":not(p)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:has(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:has(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:haschild(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:haschild(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:lang(en)",
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "a:lang(en)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
.exp = 1,
},
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
_ = css.matchFirst(s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}
test "matchAll" {
const alloc = std.testing.allocator;
var matcher = Matcher.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
n: Node,
exp: usize,
}{
.{
.q = "address",
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
.exp = 1,
},
.{
.q = "#foo",
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
.exp = 1,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
.exp = 0,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 0,
},
.{
.q = "[foo!=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo!=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 2,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
.exp = 1,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 0,
},
.{
.q = "[foo^=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo$=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo*=rb]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
.exp = 0,
},
.{
.q = "strong, a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 2,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
.name = "a",
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
} } } },
.exp = 1,
},
.{
.q = ":not(p)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 2,
},
.{
.q = "p:has(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:has(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:haschild(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:haschild(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:lang(en)",
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "a:lang(en)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
.exp = 1,
},
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
css.matchAll(s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}
test "pseudo class" {
const alloc = std.testing.allocator;
var matcher = Matcher.init(alloc);
defer matcher.deinit();
var p1: Node = .{ .name = "p" };
var p2: Node = .{ .name = "p" };
var a1: Node = .{ .name = "a" };
p1.sibling = &p2;
p2.prev = &p1;
p2.sibling = &a1;
a1.prev = &p2;
var root: Node = .{ .child = &p1, .last = &a1 };
p1.par = &root;
p2.par = &root;
a1.par = &root;
const testcases = [_]struct {
q: []const u8,
n: Node,
exp: ?*const Node,
}{
.{ .q = "p:only-child", .n = root, .exp = null },
.{ .q = "a:only-of-type", .n = root, .exp = &a1 },
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
css.matchAll(s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
if (tc.exp) |exp_n| {
const exp: usize = 1;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
continue;
}
const exp: usize = 0;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}
test "nth pseudo class" {
const alloc = std.testing.allocator;
var matcher = Matcher.init(alloc);
defer matcher.deinit();
var p1: Node = .{ .name = "p" };
var p2: Node = .{ .name = "p" };
p1.sibling = &p2;
p2.prev = &p1;
var root: Node = .{ .child = &p1, .last = &p2 };
p1.par = &root;
p2.par = &root;
const testcases = [_]struct {
q: []const u8,
n: Node,
exp: ?*const Node,
}{
.{ .q = "a:nth-of-type(1)", .n = root, .exp = null },
.{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 },
.{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 },
.{ .q = "p:nth-of-type(0)", .n = root, .exp = null },
.{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 },
.{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 },
.{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 },
.{ .q = "p:nth-child(1)", .n = root, .exp = &p1 },
.{ .q = "p:nth-child(2)", .n = root, .exp = &p2 },
.{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 },
.{ .q = "p:nth-child(even)", .n = root, .exp = &p2 },
.{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 },
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
css.matchAll(s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
if (tc.exp) |exp_n| {
const exp: usize = 1;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
continue;
}
const exp: usize = 0;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}

917
src/css/parser.zig Normal file
View File

@@ -0,0 +1,917 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// CSS Selector parser
// This file is a rewrite in Zig of Cascadia CSS Selector parser.
// see https://github.com/andybalholm/cascadia
// see https://github.com/andybalholm/cascadia/blob/master/parser.go
const std = @import("std");
const ascii = std.ascii;
const selector = @import("selector.zig");
const Selector = selector.Selector;
const PseudoClass = selector.PseudoClass;
const AttributeOP = selector.AttributeOP;
const Combinator = selector.Combinator;
pub const ParseError = error{
ExpectedSelector,
ExpectedIdentifier,
ExpectedName,
ExpectedIDSelector,
ExpectedClassSelector,
ExpectedAttributeSelector,
ExpectedString,
ExpectedRegexp,
ExpectedPseudoClassSelector,
ExpectedParenthesis,
ExpectedParenthesisClose,
ExpectedNthExpression,
ExpectedInteger,
InvalidEscape,
EscapeLineEndingOutsideString,
InvalidUnicode,
UnicodeIsNotHandled,
WriteError,
PseudoElementNotAtSelectorEnd,
PseudoElementNotUnique,
PseudoElementDisabled,
InvalidAttributeOperator,
InvalidAttributeSelector,
InvalidString,
InvalidRegexp,
InvalidPseudoClassSelector,
EmptyPseudoClassSelector,
InvalidPseudoClass,
InvalidPseudoElement,
UnmatchParenthesis,
NotHandled,
UnknownPseudoSelector,
InvalidNthExpression,
} || PseudoClass.Error || Combinator.Error || std.mem.Allocator.Error;
pub const ParseOptions = struct {
accept_pseudo_elts: bool = true,
};
pub const Parser = struct {
s: []const u8, // string to parse
i: usize = 0, // current position
opts: ParseOptions,
pub fn parse(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
return p.parseSelectorGroup(alloc);
}
// skipWhitespace consumes whitespace characters and comments.
// It returns true if there was actually anything to skip.
fn skipWhitespace(p: *Parser) bool {
var i = p.i;
while (i < p.s.len) {
const c = p.s[i];
// Whitespaces.
if (ascii.isWhitespace(c)) {
i += 1;
continue;
}
// Comments.
if (c == '/') {
if (std.mem.startsWith(u8, p.s[i..], "/*")) {
if (std.mem.indexOf(u8, p.s[i..], "*/")) |end| {
i += end + "*/".len;
continue;
}
}
}
break;
}
if (i > p.i) {
p.i = i;
return true;
}
return false;
}
// parseSimpleSelectorSequence parses a selector sequence that applies to
// a single element.
fn parseSimpleSelectorSequence(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
if (p.i >= p.s.len) {
return ParseError.ExpectedSelector;
}
var buf = std.ArrayList(Selector).init(alloc);
defer buf.deinit();
switch (p.s[p.i]) {
'*' => {
// It's the universal selector. Just skip over it, since it
// doesn't affect the meaning.
p.i += 1;
// other version of universal selector
if (p.i + 2 < p.s.len and std.mem.eql(u8, "|*", p.s[p.i .. p.i + 2])) {
p.i += 2;
}
},
'#', '.', '[', ':' => {
// There's no type selector. Wait to process the other till the
// main loop.
},
else => try buf.append(try p.parseTypeSelector(alloc)),
}
var pseudo_elt: ?PseudoClass = null;
loop: while (p.i < p.s.len) {
var ns: Selector = switch (p.s[p.i]) {
'#' => try p.parseIDSelector(alloc),
'.' => try p.parseClassSelector(alloc),
'[' => try p.parseAttributeSelector(alloc),
':' => try p.parsePseudoclassSelector(alloc),
else => break :loop,
};
errdefer ns.deinit(alloc);
// From https://drafts.csswg.org/selectors-3/#pseudo-elements :
// "Only one pseudo-element may appear per selector, and if present
// it must appear after the sequence of simple selectors that
// represents the subjects of the selector.""
switch (ns) {
.pseudo_element => |e| {
// We found a pseudo-element.
// Only one pseudo-element is accepted per selector.
if (pseudo_elt != null) return ParseError.PseudoElementNotUnique;
if (!p.opts.accept_pseudo_elts) return ParseError.PseudoElementDisabled;
pseudo_elt = e;
ns.deinit(alloc);
},
else => {
if (pseudo_elt != null) return ParseError.PseudoElementNotAtSelectorEnd;
try buf.append(ns);
},
}
}
// no need wrap the selectors in compoundSelector
if (buf.items.len == 1 and pseudo_elt == null) return buf.items[0];
return .{ .compound = .{ .selectors = try buf.toOwnedSlice(), .pseudo_elt = pseudo_elt } };
}
// parseTypeSelector parses a type selector (one that matches by tag name).
fn parseTypeSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try p.parseIdentifier(buf.writer());
return .{ .tag = try buf.toOwnedSlice() };
}
// parseIdentifier parses an identifier.
fn parseIdentifier(p: *Parser, w: anytype) ParseError!void {
const prefix = '-';
var numPrefix: usize = 0;
while (p.s.len > p.i and p.s[p.i] == prefix) {
p.i += 1;
numPrefix += 1;
}
if (p.s.len <= p.i) {
return ParseError.ExpectedSelector;
}
const c = p.s[p.i];
if (!nameStart(c) or c == '\\') {
return ParseError.ExpectedSelector;
}
var ii: usize = 0;
while (ii < numPrefix) {
w.writeByte(prefix) catch return ParseError.WriteError;
ii += 1;
}
try parseName(p, w);
}
// parseName parses a name (which is like an identifier, but doesn't have
// extra restrictions on the first character).
fn parseName(p: *Parser, w: anytype) ParseError!void {
var i = p.i;
var ok = false;
while (i < p.s.len) {
const c = p.s[i];
if (nameChar(c)) {
const start = i;
while (i < p.s.len and nameChar(p.s[i])) i += 1;
w.writeAll(p.s[start..i]) catch return ParseError.WriteError;
ok = true;
} else if (c == '\\') {
p.i = i;
try p.parseEscape(w);
i = p.i;
ok = true;
} else {
// default:
break;
}
}
if (!ok) return ParseError.ExpectedName;
p.i = i;
}
// parseEscape parses a backslash escape.
// The returned string is owned by the caller.
fn parseEscape(p: *Parser, w: anytype) ParseError!void {
if (p.s.len < p.i + 2 or p.s[p.i] != '\\') {
return ParseError.InvalidEscape;
}
const start = p.i + 1;
const c = p.s[start];
if (ascii.isWhitespace(c)) return ParseError.EscapeLineEndingOutsideString;
// unicode escape (hex)
if (ascii.isHex(c)) {
var i: usize = start;
while (i < start + 6 and i < p.s.len and ascii.isHex(p.s[i])) {
i += 1;
}
const v = std.fmt.parseUnsigned(u21, p.s[start..i], 16) catch return ParseError.InvalidUnicode;
if (p.s.len > i) {
switch (p.s[i]) {
'\r' => {
i += 1;
if (p.s.len > i and p.s[i] == '\n') i += 1;
},
' ', '\t', '\n', std.ascii.control_code.ff => i += 1,
else => {},
}
p.i = i;
var buf: [4]u8 = undefined;
const ln = std.unicode.utf8Encode(v, &buf) catch return ParseError.InvalidUnicode;
w.writeAll(buf[0..ln]) catch return ParseError.WriteError;
return;
}
}
// Return the literal character after the backslash.
p.i += 2;
w.writeAll(p.s[start .. start + 1]) catch return ParseError.WriteError;
}
// parseIDSelector parses a selector that matches by id attribute.
fn parseIDSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
if (p.i >= p.s.len) return ParseError.ExpectedIDSelector;
if (p.s[p.i] != '#') return ParseError.ExpectedIDSelector;
p.i += 1;
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try p.parseName(buf.writer());
return .{ .id = try buf.toOwnedSlice() };
}
// parseClassSelector parses a selector that matches by class attribute.
fn parseClassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
if (p.i >= p.s.len) return ParseError.ExpectedClassSelector;
if (p.s[p.i] != '.') return ParseError.ExpectedClassSelector;
p.i += 1;
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try p.parseIdentifier(buf.writer());
return .{ .class = try buf.toOwnedSlice() };
}
// parseAttributeSelector parses a selector that matches by attribute value.
fn parseAttributeSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
if (p.s[p.i] != '[') return ParseError.ExpectedAttributeSelector;
p.i += 1;
_ = p.skipWhitespace();
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try p.parseIdentifier(buf.writer());
const key = try buf.toOwnedSlice();
errdefer alloc.free(key);
lowerstr(key);
_ = p.skipWhitespace();
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
if (p.s[p.i] == ']') {
p.i += 1;
return .{ .attribute = .{ .key = key } };
}
if (p.i + 2 >= p.s.len) return ParseError.ExpectedAttributeSelector;
const op = try parseAttributeOP(p.s[p.i .. p.i + 2]);
p.i += op.len();
_ = p.skipWhitespace();
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
buf.clearRetainingCapacity();
var is_val: bool = undefined;
if (op == .regexp) {
is_val = false;
try p.parseRegex(buf.writer());
} else {
is_val = true;
switch (p.s[p.i]) {
'\'', '"' => try p.parseString(buf.writer()),
else => try p.parseIdentifier(buf.writer()),
}
}
_ = p.skipWhitespace();
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
// check if the attribute contains an ignore case flag
var ci = false;
if (p.s[p.i] == 'i' or p.s[p.i] == 'I') {
ci = true;
p.i += 1;
}
_ = p.skipWhitespace();
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
if (p.s[p.i] != ']') return ParseError.InvalidAttributeSelector;
p.i += 1;
return .{ .attribute = .{
.key = key,
.val = if (is_val) try buf.toOwnedSlice() else null,
.regexp = if (!is_val) try buf.toOwnedSlice() else null,
.op = op,
.ci = ci,
} };
}
// parseString parses a single- or double-quoted string.
fn parseString(p: *Parser, writer: anytype) ParseError!void {
var i = p.i;
if (p.s.len < i + 2) return ParseError.ExpectedString;
const quote = p.s[i];
i += 1;
loop: while (i < p.s.len) {
switch (p.s[i]) {
'\\' => {
if (p.s.len > i + 1) {
const c = p.s[i + 1];
switch (c) {
'\r' => {
if (p.s.len > i + 2 and p.s[i + 2] == '\n') {
i += 3;
continue :loop;
}
i += 2;
continue :loop;
},
'\n', std.ascii.control_code.ff => {
i += 2;
continue :loop;
},
else => {},
}
}
p.i = i;
try p.parseEscape(writer);
i = p.i;
},
'\r', '\n', std.ascii.control_code.ff => return ParseError.InvalidString,
else => |c| {
if (c == quote) break :loop;
const start = i;
while (i < p.s.len) {
const cc = p.s[i];
if (cc == quote or cc == '\\' or c == '\r' or c == '\n' or c == std.ascii.control_code.ff) break;
i += 1;
}
writer.writeAll(p.s[start..i]) catch return ParseError.WriteError;
},
}
}
if (i >= p.s.len) return ParseError.InvalidString;
// Consume the final quote.
i += 1;
p.i = i;
}
// parseRegex parses a regular expression; the end is defined by encountering an
// unmatched closing ')' or ']' which is not consumed
fn parseRegex(p: *Parser, writer: anytype) ParseError!void {
var i = p.i;
if (p.s.len < i + 2) return ParseError.ExpectedRegexp;
// number of open parens or brackets;
// when it becomes negative, finished parsing regex
var open: isize = 0;
loop: while (i < p.s.len) {
switch (p.s[i]) {
'(', '[' => open += 1,
')', ']' => {
open -= 1;
if (open < 0) break :loop;
},
else => {},
}
i += 1;
}
if (i >= p.s.len) return ParseError.InvalidRegexp;
writer.writeAll(p.s[p.i..i]) catch return ParseError.WriteError;
p.i = i;
}
// parsePseudoclassSelector parses a pseudoclass selector like :not(p) or a pseudo-element
// For backwards compatibility, both ':' and '::' prefix are allowed for pseudo-elements.
// https://drafts.csswg.org/selectors-3/#pseudo-elements
fn parsePseudoclassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
if (p.i >= p.s.len) return ParseError.ExpectedPseudoClassSelector;
if (p.s[p.i] != ':') return ParseError.ExpectedPseudoClassSelector;
p.i += 1;
var must_pseudo_elt: bool = false;
if (p.i >= p.s.len) return ParseError.EmptyPseudoClassSelector;
if (p.s[p.i] == ':') { // we found a pseudo-element
must_pseudo_elt = true;
p.i += 1;
}
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try p.parseIdentifier(buf.writer());
const pseudo_class = try PseudoClass.parse(buf.items);
// reset the buffer to reuse it.
buf.clearRetainingCapacity();
if (must_pseudo_elt and !pseudo_class.isPseudoElement()) return ParseError.InvalidPseudoElement;
switch (pseudo_class) {
.not, .has, .haschild => {
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
const sel = try p.parseSelectorGroup(alloc);
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
const s = try alloc.create(Selector);
errdefer alloc.destroy(s);
s.* = sel;
return .{ .pseudo_class_relative = .{ .pseudo_class = pseudo_class, .match = s } };
},
.contains, .containsown => {
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
switch (p.s[p.i]) {
'\'', '"' => try p.parseString(buf.writer()),
else => try p.parseString(buf.writer()),
}
_ = p.skipWhitespace();
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
const val = try buf.toOwnedSlice();
errdefer alloc.free(val);
lowerstr(val);
return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } };
},
.matches, .matchesown => {
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
try p.parseRegex(buf.writer());
if (p.i >= p.s.len) return ParseError.InvalidPseudoClassSelector;
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
return .{ .pseudo_class_regexp = .{ .own = pseudo_class == .matchesown, .regexp = try buf.toOwnedSlice() } };
},
.nth_child, .nth_last_child, .nth_of_type, .nth_last_of_type => {
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
const nth = try p.parseNth(alloc);
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
const last = pseudo_class == .nth_last_child or pseudo_class == .nth_last_of_type;
const of_type = pseudo_class == .nth_of_type or pseudo_class == .nth_last_of_type;
return .{ .pseudo_class_nth = .{ .a = nth[0], .b = nth[1], .of_type = of_type, .last = last } };
},
.first_child => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = false, .last = false } },
.last_child => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = false, .last = true } },
.first_of_type => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = true, .last = false } },
.last_of_type => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = true, .last = true } },
.only_child => return .{ .pseudo_class_only_child = false },
.only_of_type => return .{ .pseudo_class_only_child = true },
.input, .empty, .root, .link => return .{ .pseudo_class = pseudo_class },
.enabled, .disabled, .checked => return .{ .pseudo_class = pseudo_class },
.lang => {
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
try p.parseIdentifier(buf.writer());
_ = p.skipWhitespace();
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
const val = try buf.toOwnedSlice();
errdefer alloc.free(val);
lowerstr(val);
return .{ .pseudo_class_lang = val };
},
.visited, .hover, .active, .focus, .target => {
// Not applicable in a static context: never match.
return .{ .never_match = pseudo_class };
},
.after, .backdrop, .before, .cue, .first_letter => return .{ .pseudo_element = pseudo_class },
.first_line, .grammar_error, .marker, .placeholder => return .{ .pseudo_element = pseudo_class },
.selection, .spelling_error => return .{ .pseudo_element = pseudo_class },
}
}
// consumeParenthesis consumes an opening parenthesis and any following
// whitespace. It returns true if there was actually a parenthesis to skip.
fn consumeParenthesis(p: *Parser) bool {
if (p.i < p.s.len and p.s[p.i] == '(') {
p.i += 1;
_ = p.skipWhitespace();
return true;
}
return false;
}
// parseSelectorGroup parses a group of selectors, separated by commas.
fn parseSelectorGroup(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
const s = try p.parseSelector(alloc);
var buf = std.ArrayList(Selector).init(alloc);
defer buf.deinit();
try buf.append(s);
while (p.i < p.s.len) {
if (p.s[p.i] != ',') break;
p.i += 1;
const ss = try p.parseSelector(alloc);
try buf.append(ss);
}
if (buf.items.len == 1) return buf.items[0];
return .{ .group = try buf.toOwnedSlice() };
}
// parseSelector parses a selector that may include combinators.
fn parseSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
_ = p.skipWhitespace();
var s = try p.parseSimpleSelectorSequence(alloc);
while (true) {
var combinator: Combinator = .empty;
if (p.skipWhitespace()) {
combinator = .descendant;
}
if (p.i >= p.s.len) {
return s;
}
switch (p.s[p.i]) {
'+', '>', '~' => {
combinator = try Combinator.parse(p.s[p.i]);
p.i += 1;
_ = p.skipWhitespace();
},
// These characters can't begin a selector, but they can legally occur after one.
',', ')' => {
return s;
},
else => {},
}
if (combinator == .empty) {
return s;
}
const c = try p.parseSimpleSelectorSequence(alloc);
const first = try alloc.create(Selector);
errdefer alloc.destroy(first);
first.* = s;
const second = try alloc.create(Selector);
errdefer alloc.destroy(second);
second.* = c;
s = Selector{ .combined = .{ .first = first, .second = second, .combinator = combinator } };
}
return s;
}
// consumeClosingParenthesis consumes a closing parenthesis and any preceding
// whitespace. It returns true if there was actually a parenthesis to skip.
fn consumeClosingParenthesis(p: *Parser) bool {
const i = p.i;
_ = p.skipWhitespace();
if (p.i < p.s.len and p.s[p.i] == ')') {
p.i += 1;
return true;
}
p.i = i;
return false;
}
// parseInteger parses a decimal integer.
fn parseInteger(p: *Parser) ParseError!isize {
var i = p.i;
const start = i;
while (i < p.s.len and '0' <= p.s[i] and p.s[i] <= '9') i += 1;
if (i == start) return ParseError.ExpectedInteger;
p.i = i;
return std.fmt.parseUnsigned(isize, p.s[start..i], 10) catch ParseError.ExpectedInteger;
}
fn parseNthReadN(p: *Parser, a: isize) ParseError![2]isize {
_ = p.skipWhitespace();
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
return switch (p.s[p.i]) {
'+' => {
p.i += 1;
_ = p.skipWhitespace();
const b = try p.parseInteger();
return .{ a, b };
},
'-' => {
p.i += 1;
_ = p.skipWhitespace();
const b = try p.parseInteger();
return .{ a, -b };
},
else => .{ a, 0 },
};
}
fn parseNthReadA(p: *Parser, a: isize) ParseError![2]isize {
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
return switch (p.s[p.i]) {
'n', 'N' => {
p.i += 1;
return p.parseNthReadN(a);
},
else => .{ 0, a },
};
}
fn parseNthNegativeA(p: *Parser) ParseError![2]isize {
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
const c = p.s[p.i];
if (std.ascii.isDigit(c)) {
const a = try p.parseInteger() * -1;
return p.parseNthReadA(a);
}
if (c == 'n' or c == 'N') {
p.i += 1;
return p.parseNthReadN(-1);
}
return ParseError.InvalidNthExpression;
}
fn parseNthPositiveA(p: *Parser) ParseError![2]isize {
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
const c = p.s[p.i];
if (std.ascii.isDigit(c)) {
const a = try p.parseInteger();
return p.parseNthReadA(a);
}
if (c == 'n' or c == 'N') {
p.i += 1;
return p.parseNthReadN(1);
}
return ParseError.InvalidNthExpression;
}
// parseNth parses the argument for :nth-child (normally of the form an+b).
fn parseNth(p: *Parser, alloc: std.mem.Allocator) ParseError![2]isize {
// initial state
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
return switch (p.s[p.i]) {
'-' => {
p.i += 1;
return p.parseNthNegativeA();
},
'+' => {
p.i += 1;
return p.parseNthPositiveA();
},
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => p.parseNthPositiveA(),
'n', 'N' => {
p.i += 1;
return p.parseNthReadN(1);
},
'o', 'O', 'e', 'E' => {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try p.parseName(buf.writer());
if (std.ascii.eqlIgnoreCase("odd", buf.items)) return .{ 2, 1 };
if (std.ascii.eqlIgnoreCase("even", buf.items)) return .{ 2, 0 };
return ParseError.InvalidNthExpression;
},
else => ParseError.InvalidNthExpression,
};
}
};
// nameStart returns whether c can be the first character of an identifier
// (not counting an initial hyphen, or an escape sequence).
fn nameStart(c: u8) bool {
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127;
}
// nameChar returns whether c can be a character within an identifier
// (not counting an escape sequence).
fn nameChar(c: u8) bool {
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127 or
c == '-' or '0' <= c and c <= '9';
}
fn lowerstr(str: []u8) void {
for (str, 0..) |c, i| {
str[i] = std.ascii.toLower(c);
}
}
// parseAttributeOP parses an AttributeOP from a string of 1 or 2 bytes.
fn parseAttributeOP(s: []const u8) ParseError!AttributeOP {
if (s.len < 1 or s.len > 2) return ParseError.InvalidAttributeOperator;
// if the first sign is equal, we don't check anything else.
if (s[0] == '=') return .eql;
if (s.len != 2 or s[1] != '=') return ParseError.InvalidAttributeOperator;
return switch (s[0]) {
'=' => .eql,
'!' => .not_eql,
'~' => .one_of,
'|' => .prefix_hyphen,
'^' => .prefix,
'$' => .suffix,
'*' => .contains,
'#' => .regexp,
else => ParseError.InvalidAttributeOperator,
};
}
test "parser.skipWhitespace" {
const testcases = [_]struct {
s: []const u8,
i: usize,
r: bool,
}{
.{ .s = "", .i = 0, .r = false },
.{ .s = "foo", .i = 0, .r = false },
.{ .s = " ", .i = 1, .r = true },
.{ .s = " foo", .i = 1, .r = true },
.{ .s = "/* foo */ bar", .i = 10, .r = true },
.{ .s = "/* foo", .i = 0, .r = false },
};
for (testcases) |tc| {
var p = Parser{ .s = tc.s, .opts = .{} };
const res = p.skipWhitespace();
try std.testing.expectEqual(tc.r, res);
try std.testing.expectEqual(tc.i, p.i);
}
}
test "parser.parseIdentifier" {
const alloc = std.testing.allocator;
const testcases = [_]struct {
s: []const u8, // given value
exp: []const u8, // expected value
err: bool = false,
}{
.{ .s = "x", .exp = "x" },
.{ .s = "96", .exp = "", .err = true },
.{ .s = "-x", .exp = "-x" },
.{ .s = "r\\e9 sumé", .exp = "résumé" },
.{ .s = "r\\0000e9 sumé", .exp = "résumé" },
.{ .s = "r\\0000e9sumé", .exp = "résumé" },
.{ .s = "a\\\"b", .exp = "a\"b" },
};
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
for (testcases) |tc| {
buf.clearRetainingCapacity();
var p = Parser{ .s = tc.s, .opts = .{} };
p.parseIdentifier(buf.writer()) catch |e| {
// if error was expected, continue.
if (tc.err) continue;
std.debug.print("test case {s}\n", .{tc.s});
return e;
};
std.testing.expectEqualDeep(tc.exp, buf.items) catch |e| {
std.debug.print("test case {s} : {s}\n", .{ tc.s, buf.items });
return e;
};
}
}
test "parser.parseString" {
const alloc = std.testing.allocator;
const testcases = [_]struct {
s: []const u8, // given value
exp: []const u8, // expected value
err: bool = false,
}{
.{ .s = "\"x\"", .exp = "x" },
.{ .s = "'x'", .exp = "x" },
.{ .s = "'x", .exp = "", .err = true },
.{ .s = "'x\\\r\nx'", .exp = "xx" },
.{ .s = "\"r\\e9 sumé\"", .exp = "résumé" },
.{ .s = "\"r\\0000e9 sumé\"", .exp = "résumé" },
.{ .s = "\"r\\0000e9sumé\"", .exp = "résumé" },
.{ .s = "\"a\\\"b\"", .exp = "a\"b" },
.{ .s = "\"\\\n\"", .exp = "" },
.{ .s = "\"hello world\"", .exp = "hello world" },
};
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
for (testcases) |tc| {
buf.clearRetainingCapacity();
var p = Parser{ .s = tc.s, .opts = .{} };
p.parseString(buf.writer()) catch |e| {
// if error was expected, continue.
if (tc.err) continue;
std.debug.print("test case {s}\n", .{tc.s});
return e;
};
std.testing.expectEqualDeep(tc.exp, buf.items) catch |e| {
std.debug.print("test case {s} : {s}\n", .{ tc.s, buf.items });
return e;
};
}
}

767
src/css/selector.zig Normal file
View File

@@ -0,0 +1,767 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
pub const AttributeOP = enum {
eql, // =
not_eql, // !=
one_of, // ~=
prefix_hyphen, // |=
prefix, // ^=
suffix, // $=
contains, // *=
regexp, // #=
pub fn len(op: AttributeOP) u2 {
if (op == .eql) return 1;
return 2;
}
};
pub const Combinator = enum {
empty,
descendant, // space
child, // >
next_sibling, // +
subsequent_sibling, // ~
pub const Error = error{
InvalidCombinator,
};
pub fn parse(c: u8) Error!Combinator {
return switch (c) {
' ' => .descendant,
'>' => .child,
'+' => .next_sibling,
'~' => .subsequent_sibling,
else => Error.InvalidCombinator,
};
}
};
pub const PseudoClass = enum {
not,
has,
haschild,
contains,
containsown,
matches,
matchesown,
nth_child,
nth_last_child,
nth_of_type,
nth_last_of_type,
first_child,
last_child,
first_of_type,
last_of_type,
only_child,
only_of_type,
input,
empty,
root,
link,
lang,
enabled,
disabled,
checked,
visited,
hover,
active,
focus,
target,
after,
backdrop,
before,
cue,
first_letter,
first_line,
grammar_error,
marker,
placeholder,
selection,
spelling_error,
pub const Error = error{
InvalidPseudoClass,
};
pub fn isPseudoElement(pc: PseudoClass) bool {
return switch (pc) {
.after, .backdrop, .before, .cue, .first_letter => true,
.first_line, .grammar_error, .marker, .placeholder => true,
.selection, .spelling_error => true,
else => false,
};
}
pub fn parse(s: []const u8) Error!PseudoClass {
if (std.ascii.eqlIgnoreCase(s, "not")) return .not;
if (std.ascii.eqlIgnoreCase(s, "has")) return .has;
if (std.ascii.eqlIgnoreCase(s, "haschild")) return .haschild;
if (std.ascii.eqlIgnoreCase(s, "contains")) return .contains;
if (std.ascii.eqlIgnoreCase(s, "containsown")) return .containsown;
if (std.ascii.eqlIgnoreCase(s, "matches")) return .matches;
if (std.ascii.eqlIgnoreCase(s, "matchesown")) return .matchesown;
if (std.ascii.eqlIgnoreCase(s, "nth-child")) return .nth_child;
if (std.ascii.eqlIgnoreCase(s, "nth-last-child")) return .nth_last_child;
if (std.ascii.eqlIgnoreCase(s, "nth-of-type")) return .nth_of_type;
if (std.ascii.eqlIgnoreCase(s, "nth-last-of-type")) return .nth_last_of_type;
if (std.ascii.eqlIgnoreCase(s, "first-child")) return .first_child;
if (std.ascii.eqlIgnoreCase(s, "last-child")) return .last_child;
if (std.ascii.eqlIgnoreCase(s, "first-of-type")) return .first_of_type;
if (std.ascii.eqlIgnoreCase(s, "last-of-type")) return .last_of_type;
if (std.ascii.eqlIgnoreCase(s, "only-child")) return .only_child;
if (std.ascii.eqlIgnoreCase(s, "only-of-type")) return .only_of_type;
if (std.ascii.eqlIgnoreCase(s, "input")) return .input;
if (std.ascii.eqlIgnoreCase(s, "empty")) return .empty;
if (std.ascii.eqlIgnoreCase(s, "root")) return .root;
if (std.ascii.eqlIgnoreCase(s, "link")) return .link;
if (std.ascii.eqlIgnoreCase(s, "lang")) return .lang;
if (std.ascii.eqlIgnoreCase(s, "enabled")) return .enabled;
if (std.ascii.eqlIgnoreCase(s, "disabled")) return .disabled;
if (std.ascii.eqlIgnoreCase(s, "checked")) return .checked;
if (std.ascii.eqlIgnoreCase(s, "visited")) return .visited;
if (std.ascii.eqlIgnoreCase(s, "hover")) return .hover;
if (std.ascii.eqlIgnoreCase(s, "active")) return .active;
if (std.ascii.eqlIgnoreCase(s, "focus")) return .focus;
if (std.ascii.eqlIgnoreCase(s, "target")) return .target;
if (std.ascii.eqlIgnoreCase(s, "after")) return .after;
if (std.ascii.eqlIgnoreCase(s, "backdrop")) return .backdrop;
if (std.ascii.eqlIgnoreCase(s, "before")) return .before;
if (std.ascii.eqlIgnoreCase(s, "cue")) return .cue;
if (std.ascii.eqlIgnoreCase(s, "first-letter")) return .first_letter;
if (std.ascii.eqlIgnoreCase(s, "first-line")) return .first_line;
if (std.ascii.eqlIgnoreCase(s, "grammar-error")) return .grammar_error;
if (std.ascii.eqlIgnoreCase(s, "marker")) return .marker;
if (std.ascii.eqlIgnoreCase(s, "placeholder")) return .placeholder;
if (std.ascii.eqlIgnoreCase(s, "selection")) return .selection;
if (std.ascii.eqlIgnoreCase(s, "spelling-error")) return .spelling_error;
return Error.InvalidPseudoClass;
}
};
pub const Selector = union(enum) {
pub const Error = error{
UnknownCombinedCombinator,
UnsupportedRelativePseudoClass,
UnsupportedContainsPseudoClass,
UnsupportedPseudoClass,
UnsupportedPseudoElement,
UnsupportedRegexpPseudoClass,
UnsupportedAttrRegexpOperator,
};
compound: struct {
selectors: []Selector,
pseudo_elt: ?PseudoClass,
},
group: []Selector,
tag: []const u8,
id: []const u8,
class: []const u8,
attribute: struct {
key: []const u8,
val: ?[]const u8 = null,
op: ?AttributeOP = null,
regexp: ?[]const u8 = null,
ci: bool = false,
},
combined: struct {
first: *Selector,
second: *Selector,
combinator: Combinator,
},
never_match: PseudoClass,
pseudo_class: PseudoClass,
pseudo_class_only_child: bool,
pseudo_class_lang: []const u8,
pseudo_class_relative: struct {
pseudo_class: PseudoClass,
match: *Selector,
},
pseudo_class_contains: struct {
own: bool,
val: []const u8,
},
pseudo_class_regexp: struct {
own: bool,
regexp: []const u8,
},
pseudo_class_nth: struct {
a: isize,
b: isize,
of_type: bool,
last: bool,
},
pseudo_element: PseudoClass,
// returns true if s is a whitespace-separated list that includes val.
fn word(haystack: []const u8, needle: []const u8, ci: bool) bool {
if (haystack.len == 0) return false;
var it = std.mem.splitAny(u8, haystack, " \t\r\n"); // TODO add \f
while (it.next()) |part| {
if (eql(part, needle, ci)) return true;
}
return false;
}
fn eql(a: []const u8, b: []const u8, ci: bool) bool {
if (ci) return std.ascii.eqlIgnoreCase(a, b);
return std.mem.eql(u8, a, b);
}
fn starts(haystack: []const u8, needle: []const u8, ci: bool) bool {
if (ci) return std.ascii.startsWithIgnoreCase(haystack, needle);
return std.mem.startsWith(u8, haystack, needle);
}
fn ends(haystack: []const u8, needle: []const u8, ci: bool) bool {
if (ci) return std.ascii.endsWithIgnoreCase(haystack, needle);
return std.mem.endsWith(u8, haystack, needle);
}
fn contains(haystack: []const u8, needle: []const u8, ci: bool) bool {
if (ci) return std.ascii.indexOfIgnoreCase(haystack, needle) != null;
return std.mem.indexOf(u8, haystack, needle) != null;
}
// match returns true if the node matches the selector query.
pub fn match(s: Selector, n: anytype) !bool {
return switch (s) {
.tag => |v| n.isElement() and std.ascii.eqlIgnoreCase(v, try n.tag()),
.id => |v| return n.isElement() and std.mem.eql(u8, v, try n.attr("id") orelse return false),
.class => |v| return n.isElement() and word(try n.attr("class") orelse return false, v, false),
.group => |v| {
for (v) |sel| {
if (try sel.match(n)) return true;
}
return false;
},
.compound => |v| {
if (v.selectors.len == 0) return n.isElement();
for (v.selectors) |sel| {
if (!try sel.match(n)) return false;
}
return true;
},
.combined => |v| {
return switch (v.combinator) {
.empty => try v.first.match(n),
.descendant => {
if (!try v.second.match(n)) return false;
// The first must match a ascendent.
var p = try n.parent();
while (p != null) {
if (try v.first.match(p.?)) {
return true;
}
p = try p.?.parent();
}
return false;
},
.child => {
const p = try n.parent();
if (p == null) return false;
return try v.second.match(n) and try v.first.match(p.?);
},
.next_sibling => {
if (!try v.second.match(n)) return false;
var c = try n.prevSibling();
while (c != null) {
if (c.?.isText() or c.?.isComment()) {
c = try c.?.prevSibling();
continue;
}
return try v.first.match(c.?);
}
return false;
},
.subsequent_sibling => {
if (!try v.second.match(n)) return false;
var c = try n.prevSibling();
while (c != null) {
if (try v.first.match(c.?)) return true;
c = try c.?.prevSibling();
}
return false;
},
};
},
.attribute => |v| {
var attr = try n.attr(v.key);
if (v.op == null) return attr != null;
if (v.val == null or v.val.?.len == 0) return false;
const val = v.val.?;
return switch (v.op.?) {
.eql => attr != null and eql(attr.?, val, v.ci),
.not_eql => attr == null or !eql(attr.?, val, v.ci),
.one_of => attr != null and word(attr.?, val, v.ci),
.prefix => {
if (attr == null) return false;
attr.? = std.mem.trim(u8, attr.?, &std.ascii.whitespace);
if (attr.?.len == 0) return false;
return starts(attr.?, val, v.ci);
},
.suffix => {
if (attr == null) return false;
attr.? = std.mem.trim(u8, attr.?, &std.ascii.whitespace);
if (attr.?.len == 0) return false;
return ends(attr.?, val, v.ci);
},
.contains => {
if (attr == null) return false;
attr.? = std.mem.trim(u8, attr.?, &std.ascii.whitespace);
if (attr.?.len == 0) return false;
return contains(attr.?, val, v.ci);
},
.prefix_hyphen => {
if (attr == null) return false;
if (eql(attr.?, val, v.ci)) return true;
if (attr.?.len <= val.len) return false;
if (!starts(attr.?, val, v.ci)) return false;
return attr.?[val.len] == '-';
},
.regexp => return Error.UnsupportedAttrRegexpOperator, // TODO handle regexp attribute operator.
};
},
.never_match => return false,
.pseudo_class_relative => |v| {
if (!n.isElement()) return false;
return switch (v.pseudo_class) {
.not => !try v.match.match(n),
.has => try hasDescendantMatch(v.match, n),
.haschild => try hasChildMatch(v.match, n),
else => Error.UnsupportedRelativePseudoClass,
};
},
.pseudo_class_contains => return Error.UnsupportedContainsPseudoClass, // TODO, need mem allocation.
.pseudo_class_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation.
.pseudo_class_nth => |v| {
if (v.a == 0) {
if (v.last) {
return simpleNthLastChildMatch(v.b, v.of_type, n);
}
return simpleNthChildMatch(v.b, v.of_type, n);
}
return nthChildMatch(v.a, v.b, v.last, v.of_type, n);
},
.pseudo_class => |v| {
return switch (v) {
.input => {
if (!n.isElement()) return false;
const ntag = try n.tag();
return std.ascii.eqlIgnoreCase("input", ntag) or
std.ascii.eqlIgnoreCase("select", ntag) or
std.ascii.eqlIgnoreCase("button", ntag) or
std.ascii.eqlIgnoreCase("textarea", ntag);
},
.empty => {
if (!n.isElement()) return false;
var c = try n.firstChild();
while (c != null) {
if (c.?.isElement()) return false;
if (c.?.isText()) {
if (try c.?.isEmptyText()) continue;
return false;
}
c = try c.?.nextSibling();
}
return true;
},
.root => {
if (!n.isElement()) return false;
const p = try n.parent();
return (p != null and p.?.isDocument());
},
.link => {
const ntag = try n.tag();
return std.ascii.eqlIgnoreCase("a", ntag) or
std.ascii.eqlIgnoreCase("area", ntag) or
std.ascii.eqlIgnoreCase("link", ntag);
},
.enabled => {
if (!n.isElement()) return false;
const ntag = try n.tag();
if (std.ascii.eqlIgnoreCase("a", ntag) or
std.ascii.eqlIgnoreCase("area", ntag) or
std.ascii.eqlIgnoreCase("link", ntag))
{
return try n.attr("href") != null;
}
if (std.ascii.eqlIgnoreCase("optgroup", ntag) or
std.ascii.eqlIgnoreCase("menuitem", ntag) or
std.ascii.eqlIgnoreCase("fieldset", ntag))
{
return try n.attr("disabled") == null;
}
if (std.ascii.eqlIgnoreCase("input", ntag) or
std.ascii.eqlIgnoreCase("button", ntag) or
std.ascii.eqlIgnoreCase("select", ntag) or
std.ascii.eqlIgnoreCase("textarea", ntag) or
std.ascii.eqlIgnoreCase("option", ntag))
{
return try n.attr("disabled") == null and
!try inDisabledFieldset(n);
}
return false;
},
.disabled => {
if (!n.isElement()) return false;
const ntag = try n.tag();
if (std.ascii.eqlIgnoreCase("optgroup", ntag) or
std.ascii.eqlIgnoreCase("menuitem", ntag) or
std.ascii.eqlIgnoreCase("fieldset", ntag))
{
return try n.attr("disabled") != null;
}
if (std.ascii.eqlIgnoreCase("input", ntag) or
std.ascii.eqlIgnoreCase("button", ntag) or
std.ascii.eqlIgnoreCase("select", ntag) or
std.ascii.eqlIgnoreCase("textarea", ntag) or
std.ascii.eqlIgnoreCase("option", ntag))
{
return try n.attr("disabled") != null or
try inDisabledFieldset(n);
}
return false;
},
.checked => {
if (!n.isElement()) return false;
const ntag = try n.tag();
if (std.ascii.eqlIgnoreCase("intput", ntag)) {
const ntype = try n.attr("type");
if (ntype == null) return false;
if (std.mem.eql(u8, ntype.?, "checkbox") or
std.mem.eql(u8, ntype.?, "radio"))
{
return try n.attr("checked") != null;
}
return false;
}
if (std.ascii.eqlIgnoreCase("option", ntag)) {
return try n.attr("selected") != null;
}
return false;
},
.visited => return false,
.hover => return false,
.active => return false,
.focus => return false,
// TODO implement using the url fragment.
// see https://developer.mozilla.org/en-US/docs/Web/CSS/:target
.target => return false,
// all others pseudo class are handled by specialized
// pseudo_class_X selectors.
else => return Error.UnsupportedPseudoClass,
};
},
.pseudo_class_only_child => |v| onlyChildMatch(v, n),
.pseudo_class_lang => |v| langMatch(v, n),
// pseudo elements doesn't make sense in the matching process.
// > A CSS pseudo-element is a keyword added to a selector that
// > lets you style a specific part of the selected element(s).
// https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
.pseudo_element => return Error.UnsupportedPseudoElement,
};
}
fn hasLegendInPreviousSiblings(n: anytype) anyerror!bool {
var c = try n.prevSibling();
while (c != null) {
const ctag = try c.?.tag();
if (std.ascii.eqlIgnoreCase("legend", ctag)) return true;
c = try c.?.prevSibling();
}
return false;
}
fn inDisabledFieldset(n: anytype) anyerror!bool {
const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag();
const ptag = try p.?.tag();
if (std.ascii.eqlIgnoreCase("fieldset", ptag) and
try p.?.attr("disabled") != null and
(!std.ascii.eqlIgnoreCase("legend", ntag) or try hasLegendInPreviousSiblings(n)))
{
return true;
}
// TODO should we handle legend like cascadia does?
// The implemention below looks suspicious, I didn't find a test case
// in cascadia and I didn't find the reference about legend in the
// specs. For now I do prefer ignoring this part.
//
// ```
// (n.DataAtom != atom.Legend || hasLegendInPreviousSiblings(n)) {
// ```
// https://github.com/andybalholm/cascadia/blob/master/pseudo_classes.go#L434
return try inDisabledFieldset(p.?);
}
fn langMatch(lang: []const u8, n: anytype) anyerror!bool {
if (try n.attr("lang")) |own| {
if (std.mem.eql(u8, own, lang)) return true;
// check if the lang attr starts with lang+'-'
if (std.mem.startsWith(u8, own, lang)) {
if (own.len > lang.len and own[lang.len] == '-') return true;
}
}
// if the tag doesn't match, try the parent.
const p = try n.parent();
if (p == null) return false;
return langMatch(lang, p.?);
}
// onlyChildMatch implements :only-child
// If `ofType` is true, it implements :only-of-type instead.
fn onlyChildMatch(of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false;
const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag();
var count: usize = 0;
var c = try p.?.firstChild();
// loop hover all n siblings.
while (c != null) {
// ignore non elements or others tags if of-type is true.
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
c = try c.?.nextSibling();
continue;
}
count += 1;
if (count > 1) return false;
c = try c.?.nextSibling();
}
return count == 1;
}
// simpleNthLastChildMatch implements :nth-last-child(b).
// If ofType is true, implements :nth-last-of-type instead.
fn simpleNthLastChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false;
const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag();
var count: isize = 0;
var c = try p.?.lastChild();
// loop hover all n siblings.
while (c != null) {
// ignore non elements or others tags if of-type is true.
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
c = try c.?.prevSibling();
continue;
}
count += 1;
if (n.eql(c.?)) return count == b;
if (count >= b) return false;
c = try c.?.prevSibling();
}
return false;
}
// simpleNthChildMatch implements :nth-child(b).
// If ofType is true, implements :nth-of-type instead.
fn simpleNthChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false;
const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag();
var count: isize = 0;
var c = try p.?.firstChild();
// loop hover all n siblings.
while (c != null) {
// ignore non elements or others tags if of-type is true.
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
c = try c.?.nextSibling();
continue;
}
count += 1;
if (n.eql(c.?)) return count == b;
if (count >= b) return false;
c = try c.?.nextSibling();
}
return false;
}
// nthChildMatch implements :nth-child(an+b).
// If last is true, implements :nth-last-child instead.
// If ofType is true, implements :nth-of-type instead.
fn nthChildMatch(a: isize, b: isize, last: bool, of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false;
const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag();
var i: isize = -1;
var count: isize = 0;
var c = try p.?.firstChild();
// loop hover all n siblings.
while (c != null) {
// ignore non elements or others tags if of-type is true.
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
c = try c.?.nextSibling();
continue;
}
count += 1;
if (n.eql(c.?)) {
i = count;
if (!last) break;
}
c = try c.?.nextSibling();
}
if (i == -1) return false;
if (last) i = count - i + 1;
i -= b;
if (a == 0) return i == 0;
return @mod(i, a) == 0 and @divTrunc(i, a) >= 0;
}
fn hasDescendantMatch(s: *const Selector, n: anytype) anyerror!bool {
var c = try n.firstChild();
while (c != null) {
if (try s.match(c.?)) return true;
if (c.?.isElement() and try hasDescendantMatch(s, c.?)) return true;
c = try c.?.nextSibling();
}
return false;
}
fn hasChildMatch(s: *const Selector, n: anytype) anyerror!bool {
var c = try n.firstChild();
while (c != null) {
if (try s.match(c.?)) return true;
c = try c.?.nextSibling();
}
return false;
}
pub fn deinit(sel: Selector, alloc: std.mem.Allocator) void {
switch (sel) {
.group => |v| {
for (v) |vv| vv.deinit(alloc);
alloc.free(v);
},
.compound => |v| {
for (v.selectors) |vv| vv.deinit(alloc);
alloc.free(v.selectors);
},
.tag, .id, .class, .pseudo_class_lang => |v| alloc.free(v),
.attribute => |att| {
alloc.free(att.key);
if (att.val) |v| alloc.free(v);
if (att.regexp) |v| alloc.free(v);
},
.combined => |c| {
c.first.deinit(alloc);
alloc.destroy(c.first);
c.second.deinit(alloc);
alloc.destroy(c.second);
},
.pseudo_class_relative => |v| {
v.match.deinit(alloc);
alloc.destroy(v.match);
},
.pseudo_class_contains => |v| alloc.free(v.val),
.pseudo_class_regexp => |v| alloc.free(v.regexp),
.pseudo_class, .pseudo_element, .never_match => {},
.pseudo_class_nth, .pseudo_class_only_child => {},
}
}
};

View File

@@ -1,10 +1,28 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const Node = @import("node.zig").Node;
const DOMException = @import("exceptions.zig").DOMException;

View File

@@ -1,6 +1,24 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const Text = @import("text.zig").Text;

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
@@ -5,7 +23,7 @@ const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const Node = @import("node.zig").Node;
const Comment = @import("comment.zig").Comment;

View File

@@ -1,9 +1,59 @@
const parser = @import("../netsurf.zig");
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const CharacterData = @import("character_data.zig").CharacterData;
const UserContext = @import("../user_context.zig").UserContext;
// https://dom.spec.whatwg.org/#interface-comment
pub const Comment = struct {
pub const Self = parser.Comment;
pub const prototype = *CharacterData;
pub const mem_guarantied = true;
pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Comment {
return parser.documentCreateComment(
parser.documentHTMLToDocument(userctx.document),
data orelse "",
);
}
};
// Tests
// -----
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var constructor = [_]Case{
.{ .src = "let comment = new Comment('foo')", .ex = "undefined" },
.{ .src = "comment.data", .ex = "foo" },
.{ .src = "let emptycomment = new Comment()", .ex = "undefined" },
.{ .src = "emptycomment.data", .ex = "" },
};
try checkCases(js_env, &constructor);
}

79
src/dom/css.zig Normal file
View File

@@ -0,0 +1,79 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("netsurf");
const css = @import("../css/css.zig");
const Node = @import("../css/libdom.zig").Node;
const NodeList = @import("nodelist.zig").NodeList;
const MatchFirst = struct {
n: ?*parser.Node = null,
pub fn match(m: *MatchFirst, n: Node) !void {
m.n = n.node;
}
};
pub fn querySelector(alloc: std.mem.Allocator, n: *parser.Node, selector: []const u8) !?*parser.Node {
const ps = try css.parse(alloc, selector, .{ .accept_pseudo_elts = true });
defer ps.deinit(alloc);
var m = MatchFirst{};
_ = try css.matchFirst(ps, Node{ .node = n }, &m);
return m.n;
}
const MatchAll = struct {
alloc: std.mem.Allocator,
nl: NodeList,
fn init(alloc: std.mem.Allocator) MatchAll {
return .{
.alloc = alloc,
.nl = NodeList.init(),
};
}
fn deinit(m: *MatchAll) void {
m.nl.deinit(m.alloc);
}
pub fn match(m: *MatchAll, n: Node) !void {
try m.nl.append(m.alloc, n.node);
}
fn toOwnedList(m: *MatchAll) NodeList {
defer m.nl = NodeList.init();
return m.nl;
}
};
pub fn querySelectorAll(alloc: std.mem.Allocator, n: *parser.Node, selector: []const u8) !NodeList {
const ps = try css.parse(alloc, selector, .{ .accept_pseudo_elts = true });
defer ps.deinit(alloc);
var m = MatchAll.init(alloc);
defer m.deinit();
try css.matchAll(ps, Node{ .node = n }, &m);
return m.toOwnedList();
}

View File

@@ -1,6 +1,24 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
@@ -13,6 +31,7 @@ const NodeUnion = @import("node.zig").Union;
const Walker = @import("walker.zig").WalkerDepthFirst;
const collection = @import("html_collection.zig");
const css = @import("css.zig");
const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union;
@@ -21,14 +40,26 @@ const DocumentType = @import("document_type.zig").DocumentType;
const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
const DOMImplementation = @import("implementation.zig").DOMImplementation;
const UserContext = @import("../user_context.zig").UserContext;
// WEB IDL https://dom.spec.whatwg.org/#document
pub const Document = struct {
pub const Self = parser.Document;
pub const prototype = *Node;
pub const mem_guarantied = true;
pub fn constructor() !*parser.Document {
return try parser.domImplementationCreateHTMLDocument(null);
pub fn constructor(userctx: UserContext) !*parser.DocumentHTML {
const doc = try parser.documentCreateDocument(
try parser.documentHTMLGetTitle(userctx.document),
);
// we have to work w/ document instead of html document.
const ddoc = parser.documentHTMLToDocument(doc);
const ccur = parser.documentHTMLToDocument(userctx.document);
try parser.documentSetDocumentURI(ddoc, try parser.documentGetDocumentURI(ccur));
try parser.documentSetInputEncoding(ddoc, try parser.documentGetInputEncoding(ccur));
return doc;
}
// JS funcs
@@ -188,54 +219,18 @@ pub const Document = struct {
return 1;
}
// TODO netsurf doesn't handle query selectors. We have to implement a
// solution by ourselves.
// For now we handle only * and single id selector like `#foo`.
pub fn _querySelector(self: *parser.Document, selectors: []const u8) !?ElementUnion {
if (selectors.len == 0) return null;
pub fn _querySelector(self: *parser.Document, alloc: std.mem.Allocator, selector: []const u8) !?ElementUnion {
if (selector.len == 0) return null;
// catch-all, return the firstElementChild
if (selectors[0] == '*') return try get_firstElementChild(self);
const n = try css.querySelector(alloc, parser.documentToNode(self), selector);
// support only simple id selector.
if (selectors[0] != '#' or std.mem.indexOf(u8, selectors, " ") != null) return null;
if (n == null) return null;
return try _getElementById(self, selectors[1..]);
return try Element.toInterface(parser.nodeToElement(n.?));
}
// TODO netsurf doesn't handle query selectors. We have to implement a
// solution by ourselves.
// We handle only * and single id selector like `#foo`.
pub fn _querySelectorAll(self: *parser.Document, alloc: std.mem.Allocator, selectors: []const u8) !NodeList {
var list = try NodeList.init();
errdefer list.deinit(alloc);
if (selectors.len == 0) return list;
// catch-all, return all elements
if (selectors[0] == '*') {
// walk over the node tree fo find the node by id.
const root = parser.documentToNode(self);
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(root, next) orelse return list;
// ignore non-element nodes.
if (try parser.nodeType(next.?) != .element) {
continue;
}
try list.append(alloc, next.?);
}
}
// support only simple id selector.
if (selectors[0] != '#' or std.mem.indexOf(u8, selectors, " ") != null) return list;
// walk over the node tree fo find the node by id.
const e = try parser.documentGetElementById(self, selectors[1..]) orelse return list;
try list.append(alloc, parser.elementToNode(e));
return list;
pub fn _querySelectorAll(self: *parser.Document, alloc: std.mem.Allocator, selector: []const u8) !NodeList {
return css.querySelectorAll(alloc, parser.documentToNode(self), selector);
}
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
@@ -279,6 +274,13 @@ pub fn testExecFn(
.{ .src = "newdoc.children.length", .ex = "0" },
.{ .src = "newdoc.getElementsByTagName('*').length", .ex = "0" },
.{ .src = "newdoc.getElementsByTagName('*').item(0)", .ex = "null" },
.{ .src = "newdoc.inputEncoding === document.inputEncoding", .ex = "true" },
.{ .src = "newdoc.documentURI === document.documentURI", .ex = "true" },
.{ .src = "newdoc.URL === document.URL", .ex = "true" },
.{ .src = "newdoc.compatMode === document.compatMode", .ex = "true" },
.{ .src = "newdoc.characterSet === document.characterSet", .ex = "true" },
.{ .src = "newdoc.charset === document.charset", .ex = "true" },
.{ .src = "newdoc.contentType === document.contentType", .ex = "true" },
};
try checkCases(js_env, &constructor);
@@ -377,12 +379,14 @@ pub fn testExecFn(
var createComment = [_]Case{
.{ .src = "var v = document.createComment('foo')", .ex = "undefined" },
.{ .src = "v.nodeName", .ex = "#comment" },
.{ .src = "let v2 = v.cloneNode()", .ex = "undefined" },
};
try checkCases(js_env, &createComment);
var createProcessingInstruction = [_]Case{
.{ .src = "let pi = document.createProcessingInstruction('foo', 'bar')", .ex = "undefined" },
.{ .src = "pi.target", .ex = "foo" },
.{ .src = "let pi2 = pi.cloneNode()", .ex = "undefined" },
};
try checkCases(js_env, &createProcessingInstruction);
@@ -426,6 +430,12 @@ pub fn testExecFn(
.{ .src = "document.querySelector('*').nodeName", .ex = "HTML" },
.{ .src = "document.querySelector('#content').id", .ex = "content" },
.{ .src = "document.querySelector('#para').id", .ex = "para" },
.{ .src = "document.querySelector('.ok').id", .ex = "link" },
.{ .src = "document.querySelector('a ~ p').id", .ex = "para-empty" },
.{ .src = "document.querySelector(':root').nodeName", .ex = "HTML" },
.{ .src = "document.querySelectorAll('p').length", .ex = "2" },
.{ .src = "document.querySelectorAll('.ok').item(0).id", .ex = "link" },
};
try checkCases(js_env, &querySelector);
@@ -439,7 +449,7 @@ pub fn testExecFn(
try checkCases(js_env, &adoptNode);
const tags = comptime parser.Tag.all();
comptime var createElements: [(tags.len) * 2]Case = undefined;
var createElements: [(tags.len) * 2]Case = undefined;
inline for (tags, 0..) |tag, i| {
const tag_name = @tagName(tag);
createElements[i * 2] = Case{

View File

@@ -1,21 +1,56 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const Node = @import("node.zig").Node;
const UserContext = @import("../user_context.zig").UserContext;
// WEB IDL https://dom.spec.whatwg.org/#documentfragment
pub const DocumentFragment = struct {
pub const Self = parser.DocumentFragment;
pub const prototype = *Node;
pub const mem_guarantied = true;
// TODO add constructor, but I need to associate the new DocumentFragment
// with the current document global object...
// > The new DocumentFragment() constructor steps are to set thiss node
// > document to current global objects associated Document.
// https://dom.spec.whatwg.org/#dom-documentfragment-documentfragment
pub fn constructor() !*parser.DocumentFragment {
return error.NotImplemented;
pub fn constructor(userctx: UserContext) !*parser.DocumentFragment {
return parser.documentCreateDocumentFragment(
parser.documentHTMLToDocument(userctx.document),
);
}
};
// Tests
// -----
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var constructor = [_]Case{
.{ .src = "const dc = new DocumentFragment()", .ex = "undefined" },
.{ .src = "dc.constructor.name", .ex = "DocumentFragment" },
};
try checkCases(js_env, &constructor);
}

View File

@@ -1,6 +1,24 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const Node = @import("node.zig").Node;

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const generate = @import("../generate.zig");
const DOMException = @import("exceptions.zig").DOMException;
@@ -7,6 +25,7 @@ const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
const DOMTokenList = @import("token_list.zig").DOMTokenList;
const NodeList = @import("nodelist.zig").NodeList;
const Nod = @import("node.zig");
const MutationObserver = @import("mutation_observer.zig");
pub const Interfaces = generate.Tuple(.{
DOMException,
@@ -17,4 +36,5 @@ pub const Interfaces = generate.Tuple(.{
NodeList,
Nod.Node,
Nod.Interfaces,
MutationObserver.Interfaces,
});

View File

@@ -1,6 +1,24 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
@@ -8,6 +26,8 @@ const checkCases = jsruntime.test_utils.checkCases;
const Variadic = jsruntime.Variadic;
const collection = @import("html_collection.zig");
const writeNode = @import("../browser/dump.zig").writeNode;
const css = @import("css.zig");
const Node = @import("node.zig").Node;
const Walker = @import("walker.zig").WalkerDepthFirst;
@@ -78,6 +98,38 @@ pub const Element = struct {
return try parser.nodeGetAttributes(parser.elementToNode(self));
}
pub fn get_innerHTML(self: *parser.Element, alloc: std.mem.Allocator) ![]const u8 {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try writeNode(parser.elementToNode(self), buf.writer());
// TODO express the caller owned the slice.
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
return buf.toOwnedSlice();
}
pub fn set_innerHTML(self: *parser.Element, str: []const u8) !void {
const node = parser.elementToNode(self);
const doc = try parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
// parse the fragment
const fragment = try parser.documentParseFragmentFromStr(doc, str);
// remove existing children
try Node.removeChildren(node);
// get fragment body children
const children = try parser.documentFragmentBodyChildren(fragment) orelse return;
// append children to the node
const ln = try parser.nodeListLength(children);
var i: u32 = 0;
while (i < ln) {
defer i += 1;
const child = try parser.nodeListItem(children, i) orelse continue;
_ = try parser.nodeAppendChild(node, child);
}
}
pub fn _hasAttributes(self: *parser.Element) !bool {
return try parser.nodeHasAttributes(parser.elementToNode(self));
}
@@ -86,14 +138,26 @@ pub const Element = struct {
return try parser.elementGetAttribute(self, qname);
}
pub fn _getAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8) !?[]const u8 {
return try parser.elementGetAttributeNS(self, ns, qname);
}
pub fn _setAttribute(self: *parser.Element, qname: []const u8, value: []const u8) !void {
return try parser.elementSetAttribute(self, qname, value);
}
pub fn _setAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8, value: []const u8) !void {
return try parser.elementSetAttributeNS(self, ns, qname, value);
}
pub fn _removeAttribute(self: *parser.Element, qname: []const u8) !void {
return try parser.elementRemoveAttribute(self, qname);
}
pub fn _removeAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8) !void {
return try parser.elementRemoveAttributeNS(self, ns, qname);
}
pub fn _hasAttribute(self: *parser.Element, qname: []const u8) !bool {
return try parser.elementHasAttribute(self, qname);
}
@@ -230,56 +294,18 @@ pub const Element = struct {
}
}
// TODO netsurf doesn't handle query selectors. We have to implement a
// solution by ourselves.
// We handle only * and single id selector like `#foo`.
pub fn _querySelector(self: *parser.Element, selectors: []const u8) !?Union {
if (selectors.len == 0) return null;
pub fn _querySelector(self: *parser.Element, alloc: std.mem.Allocator, selector: []const u8) !?Union {
if (selector.len == 0) return null;
// catch-all, return the firstElementChild
if (selectors[0] == '*') return try get_firstElementChild(self);
const n = try css.querySelector(alloc, parser.elementToNode(self), selector);
// support only simple id selector.
if (selectors[0] != '#' or std.mem.indexOf(u8, selectors, " ") != null) return null;
if (n == null) return null;
// walk over the node tree fo find the node by id.
const n = try getElementById(self, selectors[1..]) orelse return null;
return try toInterface(parser.nodeToElement(n));
return try toInterface(parser.nodeToElement(n.?));
}
// TODO netsurf doesn't handle query selectors. We have to implement a
// solution by ourselves.
// We handle only * and single id selector like `#foo`.
pub fn _querySelectorAll(self: *parser.Element, alloc: std.mem.Allocator, selectors: []const u8) !NodeList {
var list = try NodeList.init();
errdefer list.deinit(alloc);
if (selectors.len == 0) return list;
// catch-all, return all elements
if (selectors[0] == '*') {
// walk over the node tree fo find the node by id.
const root = parser.elementToNode(self);
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(root, next) orelse return list;
// ignore non-element nodes.
if (try parser.nodeType(next.?) != .element) {
continue;
}
try list.append(alloc, next.?);
}
}
// support only simple id selector.
if (selectors[0] != '#' or std.mem.indexOf(u8, selectors, " ") != null) return list;
// walk over the node tree fo find the node by id.
const n = try getElementById(self, selectors[1..]) orelse return list;
try list.append(alloc, n);
return list;
pub fn _querySelectorAll(self: *parser.Element, alloc: std.mem.Allocator, selector: []const u8) !NodeList {
return css.querySelectorAll(alloc, parser.elementToNode(self), selector);
}
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
@@ -400,6 +426,12 @@ pub fn testExecFn(
.{ .src = "e.querySelector('#link').id", .ex = "link" },
.{ .src = "e.querySelector('#para').id", .ex = "para" },
.{ .src = "e.querySelector('*').id", .ex = "link" },
.{ .src = "e.querySelector('')", .ex = "null" },
.{ .src = "e.querySelector('*').id", .ex = "link" },
.{ .src = "e.querySelector('#content')", .ex = "null" },
.{ .src = "e.querySelector('#para').id", .ex = "para" },
.{ .src = "e.querySelector('.ok').id", .ex = "link" },
.{ .src = "e.querySelector('a ~ p').id", .ex = "para-empty" },
.{ .src = "e.querySelectorAll('foo').length", .ex = "0" },
.{ .src = "e.querySelectorAll('#foo').length", .ex = "0" },
@@ -408,6 +440,8 @@ pub fn testExecFn(
.{ .src = "e.querySelectorAll('#para').length", .ex = "1" },
.{ .src = "e.querySelectorAll('#para').item(0).id", .ex = "para" },
.{ .src = "e.querySelectorAll('*').length", .ex = "4" },
.{ .src = "e.querySelectorAll('p').length", .ex = "2" },
.{ .src = "e.querySelectorAll('.ok').item(0).id", .ex = "link" },
};
try checkCases(js_env, &querySelector);
@@ -420,4 +454,20 @@ pub fn testExecFn(
.{ .src = "f.getAttributeNode('bar')", .ex = "null" },
};
try checkCases(js_env, &attrNode);
var innerHTML = [_]Case{
.{ .src = "document.getElementById('para').innerHTML", .ex = " And" },
.{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "<span id=\"para-empty-child\"></span>" },
.{ .src = "let h = document.getElementById('para-empty')", .ex = "undefined" },
.{ .src = "const prev = h.innerHTML", .ex = "undefined" },
.{ .src = "h.innerHTML = '<p id=\"hello\">hello world</p>'", .ex = "<p id=\"hello\">hello world</p>" },
.{ .src = "h.innerHTML", .ex = "<p id=\"hello\">hello world</p>" },
.{ .src = "h.firstChild.nodeName", .ex = "P" },
.{ .src = "h.firstChild.id", .ex = "hello" },
.{ .src = "h.firstChild.textContent", .ex = "hello world" },
.{ .src = "h.innerHTML = prev; true", .ex = "true" },
.{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "<span id=\"para-empty-child\"></span>" },
};
try checkCases(js_env, &innerHTML);
}

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
@@ -6,7 +24,8 @@ const JSObjectID = jsruntime.JSObjectID;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const EventHandler = @import("../events/event.zig").EventHandler;
const DOMException = @import("exceptions.zig").DOMException;
const Nod = @import("node.zig");
@@ -54,7 +73,8 @@ pub const EventTarget = struct {
self,
alloc,
eventType,
cbk,
EventHandler,
.{ .cbk = cbk },
capture orelse false,
);
}

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const allocPrint = std.fmt.allocPrint;
@@ -5,7 +23,7 @@ const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
// https://webidl.spec.whatwg.org/#idl-DOMException
pub const DOMException = struct {
@@ -91,6 +109,12 @@ pub const DOMException = struct {
error.InvalidNodeType => "InvalidNodeTypeError",
error.DataClone => "DataCloneError",
error.NoError => unreachable,
// custom netsurf error
error.UnspecifiedEventType => "UnspecifiedEventTypeError",
error.DispatchRequest => "DispatchRequestError",
error.NoMemory => "NoMemoryError",
error.AttributeWrongType => "AttributeWrongTypeError",
};
}
@@ -124,6 +148,12 @@ pub const DOMException = struct {
error.InvalidNodeType => 24,
error.DataClone => 25,
error.NoError => unreachable,
// custom netsurf error
error.UnspecifiedEventType => 128,
error.DispatchRequest => 129,
error.NoMemory => 130,
error.AttributeWrongType => 131,
};
}

View File

@@ -1,6 +1,24 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;

View File

@@ -1,6 +1,24 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
@@ -57,7 +75,7 @@ pub const DOMImplementation = struct {
return try parser.domImplementationCreateDocument(cnamespace, cqname, doctype);
}
pub fn _createHTMLDocument(_: *DOMImplementation, title: ?[]const u8) !*parser.Document {
pub fn _createHTMLDocument(_: *DOMImplementation, title: ?[]const u8) !*parser.DocumentHTML {
return try parser.domImplementationCreateHTMLDocument(title);
}
@@ -77,7 +95,8 @@ pub fn testExecFn(
) anyerror!void {
var getImplementation = [_]Case{
.{ .src = "let impl = document.implementation", .ex = "undefined" },
.{ .src = "impl.createHTMLDocument();", .ex = "[object Document]" },
.{ .src = "impl.createHTMLDocument();", .ex = "[object HTMLDocument]" },
.{ .src = "impl.createHTMLDocument('foo');", .ex = "[object HTMLDocument]" },
.{ .src = "impl.createDocument(null, 'foo');", .ex = "[object Document]" },
.{ .src = "impl.createDocumentType('foo', 'bar', 'baz')", .ex = "[object DocumentType]" },
.{ .src = "impl.hasFeature()", .ex = "true" },

View File

@@ -0,0 +1,407 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Callback = jsruntime.Callback;
const CallbackResult = jsruntime.CallbackResult;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = generate.Tuple(.{
MutationObserver,
MutationRecord,
MutationRecords,
});
const Walker = @import("../dom/walker.zig").WalkerChildren;
const log = std.log.scoped(.events);
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
pub const MutationObserver = struct {
cbk: Callback,
observers: Observers,
pub const mem_guarantied = true;
const Observer = struct {
node: *parser.Node,
options: MutationObserverInit,
};
const deinitFunc = struct {
fn deinit(ctx: ?*anyopaque, alloc: std.mem.Allocator) void {
const o: *Observer = @ptrCast(@alignCast(ctx));
alloc.destroy(o);
}
}.deinit;
const Observers = std.ArrayListUnmanaged(*Observer);
pub const MutationObserverInit = struct {
childList: bool = false,
attributes: bool = false,
characterData: bool = false,
subtree: bool = false,
attributeOldValue: bool = false,
characterDataOldValue: bool = false,
// TODO
// attributeFilter: [][]const u8,
fn attr(self: MutationObserverInit) bool {
return self.attributes or self.attributeOldValue;
}
fn cdata(self: MutationObserverInit) bool {
return self.characterData or self.characterDataOldValue;
}
};
pub fn constructor(cbk: Callback) !MutationObserver {
return MutationObserver{
.cbk = cbk,
.observers = .{},
};
}
// TODO
fn resolveOptions(opt: ?MutationObserverInit) MutationObserverInit {
return opt orelse .{};
}
pub fn _observe(self: *MutationObserver, alloc: std.mem.Allocator, node: *parser.Node, options: ?MutationObserverInit) !void {
const o = try alloc.create(Observer);
o.* = .{
.node = node,
.options = resolveOptions(options),
};
errdefer alloc.destroy(o);
// register the new observer.
try self.observers.append(alloc, o);
// register node's events.
if (o.options.childList or o.options.subtree) {
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
alloc,
"DOMNodeInserted",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
false,
);
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
alloc,
"DOMNodeRemoved",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
false,
);
}
if (o.options.attr()) {
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
alloc,
"DOMAttrModified",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
false,
);
}
if (o.options.cdata()) {
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
alloc,
"DOMCharacterDataModified",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
false,
);
}
if (o.options.subtree) {
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
alloc,
"DOMSubtreeModified",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
false,
);
}
}
// TODO
pub fn _disconnect(_: *MutationObserver) !void {
// TODO unregister listeners.
}
pub fn deinit(self: *MutationObserver, alloc: std.mem.Allocator) void {
// TODO unregister listeners.
for (self.observers.items) |o| alloc.destroy(o);
self.observers.deinit(alloc);
}
// TODO
pub fn _takeRecords(_: MutationObserver) ?[]const u8 {
return &[_]u8{};
}
};
// Handle multiple record?
pub const MutationRecords = struct {
first: ?MutationRecord = null,
pub const mem_guarantied = true;
pub fn get_length(self: *MutationRecords) u32 {
if (self.first == null) return 0;
return 1;
}
pub fn postAttach(self: *MutationRecords, js_obj: jsruntime.JSObject) !void {
if (self.first) |mr| {
try js_obj.set("0", mr);
}
}
};
pub const MutationRecord = struct {
type: []const u8,
target: *parser.Node,
addedNodes: NodeList = NodeList.init(),
removedNodes: NodeList = NodeList.init(),
previousSibling: ?*parser.Node = null,
nextSibling: ?*parser.Node = null,
attributeName: ?[]const u8 = null,
attributeNamespace: ?[]const u8 = null,
oldValue: ?[]const u8 = null,
pub const mem_guarantied = true;
pub fn get_type(self: MutationRecord) []const u8 {
return self.type;
}
pub fn get_addedNodes(self: MutationRecord) NodeList {
return self.addedNodes;
}
pub fn get_removedNodes(self: MutationRecord) NodeList {
return self.addedNodes;
}
pub fn get_target(self: MutationRecord) *parser.Node {
return self.target;
}
pub fn get_attributeName(self: MutationRecord) ?[]const u8 {
return self.attributeName;
}
pub fn get_attributeNamespace(self: MutationRecord) ?[]const u8 {
return self.attributeNamespace;
}
pub fn get_previousSibling(self: MutationRecord) ?*parser.Node {
return self.previousSibling;
}
pub fn get_nextSibling(self: MutationRecord) ?*parser.Node {
return self.nextSibling;
}
pub fn get_oldValue(self: MutationRecord) ?[]const u8 {
return self.oldValue;
}
};
// EventHandler dedicated to mutation events.
const EventHandler = struct {
fn apply(o: *MutationObserver.Observer, target: *parser.Node) bool {
// mutation on any target is always ok.
if (o.options.subtree) return true;
// if target equals node, alway ok.
if (target == o.node) return true;
// no subtree, no same target and no childlist, always noky.
if (!o.options.childList) return false;
// target must be a child of o.node
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = walker.get_next(o.node, next) catch break orelse break;
if (next.? == target) return true;
}
return false;
}
fn handle(evt: ?*parser.Event, data: parser.EventHandlerData) void {
if (evt == null) return;
var mrs: MutationRecords = .{};
const t = parser.eventType(evt.?) catch |e| {
log.err("mutation observer event type: {any}", .{e});
return;
};
const et = parser.eventTarget(evt.?) catch |e| {
log.err("mutation observer event target: {any}", .{e});
return;
} orelse return;
const node = parser.eventTargetToNode(et);
// retrieve the observer from the data.
const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.data));
if (!apply(o, node)) return;
const muevt = parser.eventToMutationEvent(evt.?);
// TODO get the allocator by another way?
const alloc = data.cbk.nat_ctx.alloc;
if (std.mem.eql(u8, t, "DOMAttrModified")) {
mrs.first = .{
.type = "attributes",
.target = o.node,
.attributeName = parser.mutationEventAttributeName(muevt) catch null,
};
// record old value if required.
if (o.options.attributeOldValue) {
mrs.first.?.oldValue = parser.mutationEventPrevValue(muevt) catch null;
}
} else if (std.mem.eql(u8, t, "DOMCharacterDataModified")) {
mrs.first = .{
.type = "characterData",
.target = o.node,
};
// record old value if required.
if (o.options.characterDataOldValue) {
mrs.first.?.oldValue = parser.mutationEventPrevValue(muevt) catch null;
}
} else if (std.mem.eql(u8, t, "DOMNodeInserted")) {
mrs.first = .{
.type = "childList",
.target = o.node,
.addedNodes = NodeList.init(),
.removedNodes = NodeList.init(),
};
const rn = parser.mutationEventRelatedNode(muevt) catch null;
if (rn) |n| {
mrs.first.?.addedNodes.append(alloc, n) catch |e| {
log.err("mutation event handler error: {any}", .{e});
return;
};
}
} else if (std.mem.eql(u8, t, "DOMNodeRemoved")) {
mrs.first = .{
.type = "childList",
.target = o.node,
.addedNodes = NodeList.init(),
.removedNodes = NodeList.init(),
};
const rn = parser.mutationEventRelatedNode(muevt) catch null;
if (rn) |n| {
mrs.first.?.removedNodes.append(alloc, n) catch |e| {
log.err("mutation event handler error: {any}", .{e});
return;
};
}
} else {
return;
}
var res = CallbackResult.init(alloc);
defer res.deinit();
// TODO pass MutationRecords and MutationObserver
data.cbk.trycall(.{mrs}, &res) catch |e| log.err("mutation event handler error: {any}", .{e});
// in case of function error, we log the result and the trace.
if (!res.success) {
log.info("mutation observer event handler error: {s}", .{res.result orelse "unknown"});
log.debug("{s}", .{res.stack orelse "no stack trace"});
}
}
}.handle;
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var constructor = [_]Case{
.{ .src = "new MutationObserver(() => {}).observe(document, { childList: true });", .ex = "undefined" },
};
try checkCases(js_env, &constructor);
var attr = [_]Case{
.{ .src =
\\var nb = 0;
\\var mrs;
\\new MutationObserver((mu) => {
\\ mrs = mu;
\\ nb++;
\\}).observe(document.firstElementChild, { attributes: true, attributeOldValue: true });
\\document.firstElementChild.setAttribute("foo", "bar");
\\// ignored b/c it's about another target.
\\document.firstElementChild.firstChild.setAttribute("foo", "bar");
\\nb;
, .ex = "1" },
.{ .src = "mrs[0].type", .ex = "attributes" },
.{ .src = "mrs[0].target == document.firstElementChild", .ex = "true" },
.{ .src = "mrs[0].target.getAttribute('foo')", .ex = "bar" },
.{ .src = "mrs[0].attributeName", .ex = "foo" },
.{ .src = "mrs[0].oldValue", .ex = "null" },
};
try checkCases(js_env, &attr);
var cdata = [_]Case{
.{ .src =
\\var node = document.getElementById("para").firstChild;
\\var nb2 = 0;
\\var mrs2;
\\new MutationObserver((mu) => {
\\ mrs2 = mu;
\\ nb2++;
\\}).observe(node, { characterData: true, characterDataOldValue: true });
\\node.data = "foo";
\\nb2;
, .ex = "1" },
.{ .src = "mrs2[0].type", .ex = "characterData" },
.{ .src = "mrs2[0].target == node", .ex = "true" },
.{ .src = "mrs2[0].target.data", .ex = "foo" },
.{ .src = "mrs2[0].oldValue", .ex = " And" },
};
try checkCases(js_env, &cdata);
}

View File

@@ -1,6 +1,24 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
@@ -8,7 +26,7 @@ const Variadic = jsruntime.Variadic;
const generate = @import("../generate.zig");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const EventTarget = @import("event_target.zig").EventTarget;
@@ -199,7 +217,7 @@ pub const Node = struct {
}
pub fn get_childNodes(self: *parser.Node, alloc: std.mem.Allocator) !NodeList {
var list = try NodeList.init();
var list = NodeList.init();
errdefer list.deinit(alloc);
var n = try parser.nodeFirstChild(self) orelse return list;
@@ -259,14 +277,30 @@ pub const Node = struct {
return try Node.toInterface(res);
}
// Check if the hierarchy node tree constraints are respected.
// For now, it checks only if new nodes are not self.
// TODO implements the others contraints.
// see https://dom.spec.whatwg.org/#concept-node-tree
pub fn hierarchy(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !bool {
if (nodes == null) return true;
if (nodes.?.slice.len == 0) return true;
for (nodes.?.slice) |node| if (self == node) return false;
return true;
}
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
pub fn prepend(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void {
if (nodes == null) return;
if (nodes.?.slice.len == 0) return;
const first = try parser.nodeFirstChild(self);
// check hierarchy
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
const first = try parser.nodeFirstChild(self);
if (first == null) {
for (nodes.?.slice) |node| {
_ = try parser.nodeAppendChild(self, node);
@@ -285,6 +319,10 @@ pub const Node = struct {
pub fn append(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void {
if (nodes == null) return;
if (nodes.?.slice.len == 0) return;
// check hierarchy
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
for (nodes.?.slice) |node| {
_ = try parser.nodeAppendChild(self, node);
}
@@ -297,17 +335,11 @@ pub const Node = struct {
if (nodes == null) return;
if (nodes.?.slice.len == 0) return;
// check hierarchy
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
// remove existing children
if (try parser.nodeHasChildNodes(self)) {
const children = try parser.nodeGetChildNodes(self);
const ln = try parser.nodeListLength(children);
var i: u32 = 0;
while (i < ln) {
defer i += 1;
const child = try parser.nodeListItem(children, i) orelse continue;
_ = try parser.nodeRemoveChild(self, child);
}
}
try removeChildren(self);
// add new children
for (nodes.?.slice) |node| {
@@ -315,6 +347,21 @@ pub const Node = struct {
}
}
pub fn removeChildren(self: *parser.Node) !void {
if (!try parser.nodeHasChildNodes(self)) return;
const children = try parser.nodeGetChildNodes(self);
const ln = try parser.nodeListLength(children);
var i: u32 = 0;
while (i < ln) {
defer i += 1;
// we always retrieve the 0 index child on purpose: libdom nodelist
// are dynamic. So the next child to remove is always as pos 0.
const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeRemoveChild(self, child);
}
}
pub fn deinit(_: *parser.Node, _: std.mem.Allocator) void {}
};

View File

@@ -1,6 +1,24 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
@@ -26,7 +44,7 @@ pub const NodeList = struct {
nodes: NodesArrayList,
pub fn init() !NodeList {
pub fn init() NodeList {
return NodeList{
.nodes = NodesArrayList{},
};

View File

@@ -1,6 +1,29 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("netsurf");
const Node = @import("node.zig").Node;
// https://dom.spec.whatwg.org/#processinginstruction
pub const ProcessingInstruction = struct {
@@ -8,18 +31,39 @@ pub const ProcessingInstruction = struct {
// TODO for libdom processing instruction inherit from node.
// But the spec says it must inherit from CDATA.
// Moreover, inherit from Node causes also a crash with cloneNode.
// https://github.com/lightpanda-io/browsercore/issues/123
//
// In consequence, for now, we don't implement all these func for
// ProcessingInstruction.
//
//pub const prototype = *CharacterData;
pub const prototype = *Node;
pub const mem_guarantied = true;
pub fn get_target(self: *parser.ProcessingInstruction) ![]const u8 {
// libdom stores the ProcessingInstruction target in the node's name.
return try parser.nodeName(@as(*parser.Node, @ptrCast(self)));
return try parser.nodeName(parser.processingInstructionToNode(self));
}
pub fn _cloneNode(self: *parser.ProcessingInstruction, _: ?bool) !*parser.ProcessingInstruction {
return try parser.processInstructionCopy(self);
}
pub fn get_data(self: *parser.ProcessingInstruction) !?[]const u8 {
return try parser.nodeValue(parser.processingInstructionToNode(self));
}
pub fn set_data(self: *parser.ProcessingInstruction, data: []u8) !void {
try parser.nodeSetValue(parser.processingInstructionToNode(self), data);
}
};
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var createProcessingInstruction = [_]Case{
.{ .src = "let pi = document.createProcessingInstruction('foo', 'bar')", .ex = "undefined" },
.{ .src = "pi.target", .ex = "foo" },
.{ .src = "pi.data", .ex = "bar" },
.{ .src = "pi.data = 'foo'", .ex = "foo" },
.{ .src = "pi.data", .ex = "foo" },
.{ .src = "let pi2 = pi.cloneNode()", .ex = "undefined" },
};
try checkCases(js_env, &createProcessingInstruction);
}

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
@@ -5,11 +23,13 @@ const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const CharacterData = @import("character_data.zig").CharacterData;
const CDATASection = @import("cdata_section.zig").CDATASection;
const UserContext = @import("../user_context.zig").UserContext;
// Text interfaces
pub const Interfaces = generate.Tuple(.{
CDATASection,
@@ -20,6 +40,13 @@ pub const Text = struct {
pub const prototype = *CharacterData;
pub const mem_guarantied = true;
pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Text {
return parser.documentCreateTextNode(
parser.documentHTMLToDocument(userctx.document),
data orelse "",
);
}
// JS funcs
// --------
@@ -44,6 +71,15 @@ pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var constructor = [_]Case{
.{ .src = "let t = new Text('foo')", .ex = "undefined" },
.{ .src = "t.data", .ex = "foo" },
.{ .src = "let emptyt = new Text()", .ex = "undefined" },
.{ .src = "emptyt.data", .ex = "" },
};
try checkCases(js_env, &constructor);
var get_whole_text = [_]Case{
.{ .src = "let text = document.getElementById('link').firstChild", .ex = "undefined" },
.{ .src = "text.wholeText === 'OK'", .ex = "true" },

View File

@@ -1,6 +1,24 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;

View File

@@ -1,6 +1,24 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
pub const Walker = union(enum) {
walkerDepthFirst: WalkerDepthFirst,

View File

@@ -1,13 +1,32 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const generate = @import("../generate.zig");
const jsruntime = @import("jsruntime");
const Callback = jsruntime.Callback;
const CallbackResult = jsruntime.CallbackResult;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const DOMException = @import("../dom/exceptions.zig").DOMException;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
@@ -15,6 +34,8 @@ const EventTargetUnion = @import("../dom/event_target.zig").Union;
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
const log = std.log.scoped(.events);
// Event interfaces
pub const Interfaces = generate.Tuple(.{
Event,
@@ -218,3 +239,25 @@ pub fn testExecFn(
};
try checkCases(js_env, &remove);
}
pub const EventHandler = struct {
fn handle(event: ?*parser.Event, data: parser.EventHandlerData) void {
// TODO get the allocator by another way?
var res = CallbackResult.init(data.cbk.nat_ctx.alloc);
defer res.deinit();
if (event) |evt| {
data.cbk.trycall(.{
Event.toInterface(evt) catch unreachable,
}, &res) catch |e| log.err("event handler error: {any}", .{e});
} else {
data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error: {any}", .{e});
}
// in case of function error, we log the result and the trace.
if (!res.success) {
log.info("event handler error: {s}", .{res.result orelse "unknown"});
log.debug("{s}", .{res.stack orelse "no stack trace"});
}
}
}.handle;

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
@@ -10,6 +28,8 @@ fn itoa(comptime i: u8) ![]const u8 {
len = 1;
} else if (i < 100) {
len = 2;
} else if (i < 1000) {
len = 3;
} else {
return error.GenerateTooMuchMembers;
}
@@ -17,9 +37,9 @@ fn itoa(comptime i: u8) ![]const u8 {
return try std.fmt.bufPrint(buf[0..], "{d}", .{i});
}
fn fmtName(comptime T: type) []const u8 {
fn fmtName(comptime T: type) [:0]const u8 {
var it = std.mem.splitBackwards(u8, @typeName(T), ".");
return it.first();
return it.first() ++ "";
}
// Union
@@ -150,7 +170,11 @@ pub const Union = struct {
T = *T;
}
union_fields[done] = .{
.name = fmtName(member_T),
// UnionField.name expect a null terminated string.
// concatenate the `[]const u8` string with an empty string
// literal (`name ++ ""`) to explicitly coerce it to `[:0]const
// u8`.
.name = fmtName(member_T) ++ "",
.type = T,
.alignment = @alignOf(T),
};
@@ -158,7 +182,7 @@ pub const Union = struct {
}
}
const union_info = std.builtin.Type.Union{
.layout = .Auto,
.layout = .auto,
.tag_type = enum_T,
.fields = &union_fields,
.decls = &decls,
@@ -268,7 +292,11 @@ fn TupleT(comptime tuple: anytype) type {
continue;
}
fields[done] = .{
.name = try itoa(done),
// StructField.name expect a null terminated string.
// concatenate the `[]const u8` string with an empty string
// literal (`name ++ ""`) to explicitly coerce it to `[:0]const
// u8`.
.name = try itoa(done) ++ "",
.type = type,
.default_value = null,
.is_comptime = false,
@@ -278,7 +306,7 @@ fn TupleT(comptime tuple: anytype) type {
}
const decls: [0]std.builtin.Type.Declaration = undefined;
const info = std.builtin.Type.Struct{
.layout = .Auto,
.layout = .auto,
.fields = &fields,
.decls = &decls,
.is_tuple = true,

View File

@@ -1,6 +1,24 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
@@ -80,7 +98,7 @@ pub const HTMLDocument = struct {
}
pub fn _getElementsByName(self: *parser.DocumentHTML, alloc: std.mem.Allocator, name: []const u8) !NodeList {
var list = try NodeList.init();
var list = NodeList.init();
errdefer list.deinit(alloc);
if (name.len == 0) return list;

View File

@@ -1,7 +1,31 @@
const parser = @import("../netsurf.zig");
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("netsurf");
const generate = @import("../generate.zig");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const Element = @import("../dom/element.zig").Element;
const URL = @import("../url/url.zig").URL;
// HTMLElement interfaces
pub const Interfaces = .{
@@ -108,10 +132,286 @@ pub const HTMLUnknownElement = struct {
pub const mem_guarantied = true;
};
// https://html.spec.whatwg.org/#the-a-element
pub const HTMLAnchorElement = struct {
pub const Self = parser.Anchor;
pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
pub fn get_target(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetTarget(self);
}
pub fn set_target(self: *parser.Anchor, href: []const u8) !void {
return try parser.anchorSetTarget(self, href);
}
pub fn get_download(_: *parser.Anchor) ![]const u8 {
return ""; // TODO
}
pub fn get_href(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetHref(self);
}
pub fn set_href(self: *parser.Anchor, href: []const u8) !void {
return try parser.anchorSetHref(self, href);
}
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetHrefLang(self);
}
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
return try parser.anchorSetHrefLang(self, href);
}
pub fn get_type(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetType(self);
}
pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
return try parser.anchorSetType(self, t);
}
pub fn get_rel(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetRel(self);
}
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
return try parser.anchorSetRel(self, t);
}
pub fn get_text(self: *parser.Anchor) !?[]const u8 {
return try parser.nodeTextContent(parser.anchorToNode(self));
}
pub fn set_text(self: *parser.Anchor, v: []const u8) !void {
return try parser.nodeSetTextContent(parser.anchorToNode(self), v);
}
inline fn url(self: *parser.Anchor, alloc: std.mem.Allocator) !URL {
const href = try parser.anchorGetHref(self);
return URL.constructor(alloc, href, null); // TODO inject base url
}
// TODO return a disposable string
pub fn get_origin(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
var u = try url(self, alloc);
defer u.deinit(alloc);
return try u.get_origin(alloc);
}
// TODO return a disposable string
pub fn get_protocol(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
var u = try url(self, alloc);
defer u.deinit(alloc);
return u.get_protocol(alloc);
}
pub fn set_protocol(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
var u = try url(self, alloc);
defer u.deinit(alloc);
u.uri.scheme = v;
const href = try u.format(alloc);
defer alloc.free(href);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_host(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
var u = try url(self, alloc);
defer u.deinit(alloc);
return try u.get_host(alloc);
}
pub fn set_host(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
// search : separator
var p: ?u16 = null;
var h: []const u8 = undefined;
for (v, 0..) |c, i| {
if (c == ':') {
h = v[0..i];
p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
break;
}
}
var u = try url(self, alloc);
defer u.deinit(alloc);
if (p) |pp| {
u.uri.host = .{ .raw = h };
u.uri.port = pp;
} else {
u.uri.host = .{ .raw = v };
u.uri.port = null;
}
const href = try u.format(alloc);
defer alloc.free(href);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_hostname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
var u = try url(self, alloc);
defer u.deinit(alloc);
return try alloc.dupe(u8, u.get_hostname());
}
pub fn set_hostname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
var u = try url(self, alloc);
defer u.deinit(alloc);
u.uri.host = .{ .raw = v };
const href = try u.format(alloc);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_port(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
var u = try url(self, alloc);
defer u.deinit(alloc);
return try u.get_port(alloc);
}
pub fn set_port(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
var u = try url(self, alloc);
defer u.deinit(alloc);
if (v != null and v.?.len > 0) {
u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
} else {
u.uri.port = null;
}
const href = try u.format(alloc);
defer alloc.free(href);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_username(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
var u = try url(self, alloc);
defer u.deinit(alloc);
return try alloc.dupe(u8, u.get_username());
}
pub fn set_username(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
var u = try url(self, alloc);
defer u.deinit(alloc);
if (v) |vv| {
u.uri.user = .{ .raw = vv };
} else {
u.uri.user = null;
}
const href = try u.format(alloc);
defer alloc.free(href);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_password(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
var u = try url(self, alloc);
defer u.deinit(alloc);
return try alloc.dupe(u8, u.get_password());
}
pub fn set_password(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
var u = try url(self, alloc);
defer u.deinit(alloc);
if (v) |vv| {
u.uri.password = .{ .raw = vv };
} else {
u.uri.password = null;
}
const href = try u.format(alloc);
defer alloc.free(href);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_pathname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
var u = try url(self, alloc);
defer u.deinit(alloc);
return try alloc.dupe(u8, u.get_pathname());
}
pub fn set_pathname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
var u = try url(self, alloc);
defer u.deinit(alloc);
u.uri.path = .{ .raw = v };
const href = try u.format(alloc);
defer alloc.free(href);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_search(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
var u = try url(self, alloc);
defer u.deinit(alloc);
return try u.get_search(alloc);
}
pub fn set_search(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
var u = try url(self, alloc);
defer u.deinit(alloc);
if (v) |vv| {
u.uri.query = .{ .raw = vv };
} else {
u.uri.query = null;
}
const href = try u.format(alloc);
defer alloc.free(href);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_hash(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
var u = try url(self, alloc);
defer u.deinit(alloc);
return try u.get_hash(alloc);
}
pub fn set_hash(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
var u = try url(self, alloc);
defer u.deinit(alloc);
if (v) |vv| {
u.uri.fragment = .{ .raw = vv };
} else {
u.uri.fragment = null;
}
const href = try u.format(alloc);
defer alloc.free(href);
try parser.anchorSetHref(self, href);
}
pub fn deinit(_: *parser.Anchor, _: std.mem.Allocator) void {}
};
pub const HTMLAppletElement = struct {
@@ -390,10 +690,120 @@ pub const HTMLQuoteElement = struct {
pub const mem_guarantied = true;
};
// https://html.spec.whatwg.org/#the-script-element
pub const HTMLScriptElement = struct {
pub const Self = parser.Script;
pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
pub fn get_src(self: *parser.Script) !?[]const u8 {
return try parser.elementGetAttribute(
parser.scriptToElt(self),
"src",
) orelse "";
}
pub fn set_src(self: *parser.Script, v: []const u8) !void {
return try parser.elementSetAttribute(
parser.scriptToElt(self),
"src",
v,
);
}
pub fn get_type(self: *parser.Script) !?[]const u8 {
return try parser.elementGetAttribute(
parser.scriptToElt(self),
"type",
) orelse "";
}
pub fn set_type(self: *parser.Script, v: []const u8) !void {
return try parser.elementSetAttribute(
parser.scriptToElt(self),
"type",
v,
);
}
pub fn get_text(self: *parser.Script) !?[]const u8 {
return try parser.elementGetAttribute(
parser.scriptToElt(self),
"text",
) orelse "";
}
pub fn set_text(self: *parser.Script, v: []const u8) !void {
return try parser.elementSetAttribute(
parser.scriptToElt(self),
"text",
v,
);
}
pub fn get_integrity(self: *parser.Script) !?[]const u8 {
return try parser.elementGetAttribute(
parser.scriptToElt(self),
"integrity",
) orelse "";
}
pub fn set_integrity(self: *parser.Script, v: []const u8) !void {
return try parser.elementSetAttribute(
parser.scriptToElt(self),
"integrity",
v,
);
}
pub fn get_async(self: *parser.Script) !bool {
_ = try parser.elementGetAttribute(
parser.scriptToElt(self),
"async",
) orelse return false;
return true;
}
pub fn set_async(self: *parser.Script, v: bool) !void {
if (v) {
return try parser.elementSetAttribute(parser.scriptToElt(self), "async", "");
}
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "async");
}
pub fn get_defer(self: *parser.Script) !bool {
_ = try parser.elementGetAttribute(
parser.scriptToElt(self),
"defer",
) orelse false;
return true;
}
pub fn set_defer(self: *parser.Script, v: bool) !void {
if (v) {
return try parser.elementSetAttribute(parser.scriptToElt(self), "defer", "");
}
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "defer");
}
pub fn get_noModule(self: *parser.Script) !bool {
_ = try parser.elementGetAttribute(
parser.scriptToElt(self),
"nomodule",
) orelse false;
return true;
}
pub fn set_noModule(self: *parser.Script, v: bool) !void {
if (v) {
return try parser.elementSetAttribute(parser.scriptToElt(self), "nomodule", "");
}
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule");
}
};
pub const HTMLSelectElement = struct {
@@ -571,3 +981,82 @@ pub fn toInterface(comptime T: type, e: *parser.Element) !T {
.undef => .{ .HTMLUnknownElement = @as(*parser.Unknown, @ptrCast(elem)) },
};
}
// Tests
// -----
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var anchor = [_]Case{
.{ .src = "let a = document.getElementById('link')", .ex = "undefined" },
.{ .src = "a.target", .ex = "" },
.{ .src = "a.target = '_blank'", .ex = "_blank" },
.{ .src = "a.target", .ex = "_blank" },
.{ .src = "a.target = ''", .ex = "" },
.{ .src = "a.href", .ex = "foo" },
.{ .src = "a.href = 'https://lightpanda.io/'", .ex = "https://lightpanda.io/" },
.{ .src = "a.href", .ex = "https://lightpanda.io/" },
.{ .src = "a.origin", .ex = "https://lightpanda.io" },
.{ .src = "a.host = 'lightpanda.io:443'", .ex = "lightpanda.io:443" },
.{ .src = "a.host", .ex = "lightpanda.io:443" },
.{ .src = "a.port", .ex = "443" },
.{ .src = "a.hostname", .ex = "lightpanda.io" },
.{ .src = "a.host = 'lightpanda.io'", .ex = "lightpanda.io" },
.{ .src = "a.host", .ex = "lightpanda.io" },
.{ .src = "a.port", .ex = "" },
.{ .src = "a.hostname", .ex = "lightpanda.io" },
.{ .src = "a.host", .ex = "lightpanda.io" },
.{ .src = "a.hostname", .ex = "lightpanda.io" },
.{ .src = "a.hostname = 'foo.bar'", .ex = "foo.bar" },
.{ .src = "a.href", .ex = "https://foo.bar/" },
.{ .src = "a.search", .ex = "" },
.{ .src = "a.search = 'q=bar'", .ex = "q=bar" },
.{ .src = "a.search", .ex = "?q=bar" },
.{ .src = "a.href", .ex = "https://foo.bar/?q=bar" },
.{ .src = "a.hash", .ex = "" },
.{ .src = "a.hash = 'frag'", .ex = "frag" },
.{ .src = "a.hash", .ex = "#frag" },
.{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" },
.{ .src = "a.port", .ex = "" },
.{ .src = "a.port = '443'", .ex = "443" },
.{ .src = "a.host", .ex = "foo.bar:443" },
.{ .src = "a.hostname", .ex = "foo.bar" },
.{ .src = "a.href", .ex = "https://foo.bar:443/?q=bar#frag" },
.{ .src = "a.port = null", .ex = "null" },
.{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" },
.{ .src = "a.href = 'foo'", .ex = "foo" },
.{ .src = "a.type", .ex = "" },
.{ .src = "a.type = 'text/html'", .ex = "text/html" },
.{ .src = "a.type", .ex = "text/html" },
.{ .src = "a.type = ''", .ex = "" },
.{ .src = "a.text", .ex = "OK" },
.{ .src = "a.text = 'foo'", .ex = "foo" },
.{ .src = "a.text", .ex = "foo" },
.{ .src = "a.text = 'OK'", .ex = "OK" },
};
try checkCases(js_env, &anchor);
var script = [_]Case{
.{ .src = "let script = document.createElement('script')", .ex = "undefined" },
.{ .src = "script.src = 'foo.bar'", .ex = "foo.bar" },
.{ .src = "script.async = true", .ex = "true" },
.{ .src = "script.async", .ex = "true" },
.{ .src = "script.async = false", .ex = "false" },
.{ .src = "script.async", .ex = "false" },
};
try checkCases(js_env, &script);
}

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const generate = @import("../generate.zig");
const HTMLDocument = @import("document.zig").HTMLDocument;

View File

@@ -1,31 +1,67 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Callback = jsruntime.Callback;
const CallbackArg = jsruntime.CallbackArg;
const Loop = jsruntime.Loop;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const storage = @import("../storage/storage.zig");
// https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
pub const Window = struct {
pub const prototype = *EventTarget;
pub const mem_guarantied = true;
pub const global_type = true;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{},
document: ?*parser.Document = null,
document: ?*parser.DocumentHTML = null,
target: []const u8,
storageShelf: ?*storage.Shelf = null,
// store a map between internal timeouts ids and pointers to uint.
// the maximum number of possible timeouts is fixed.
timeoutid: u32 = 0,
timeoutids: [512]u64 = undefined,
pub fn create(target: ?[]const u8) Window {
return Window{
.target = target orelse "",
};
}
pub fn replaceDocument(self: *Window, doc: *parser.Document) void {
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) void {
self.document = doc;
}
pub fn setStorageShelf(self: *Window, shelf: *storage.Shelf) void {
self.storageShelf = shelf;
}
pub fn get_window(self: *Window) *Window {
return self;
}
@@ -38,11 +74,43 @@ pub const Window = struct {
return self;
}
pub fn get_document(self: *Window) ?*parser.Document {
pub fn get_document(self: *Window) ?*parser.DocumentHTML {
return self.document;
}
pub fn get_name(self: *Window) []const u8 {
return self.target;
}
pub fn get_localStorage(self: *Window) !*storage.Bottle {
if (self.storageShelf == null) return parser.DOMError.NotSupported;
return &self.storageShelf.?.bucket.local;
}
pub fn get_sessionStorage(self: *Window) !*storage.Bottle {
if (self.storageShelf == null) return parser.DOMError.NotSupported;
return &self.storageShelf.?.bucket.session;
}
// TODO handle callback arguments.
pub fn _setTimeout(self: *Window, loop: *Loop, cbk: Callback, delay: ?u32) !u32 {
if (self.timeoutid >= self.timeoutids.len) return error.TooMuchTimeout;
const ddelay: u63 = delay orelse 0;
const id = loop.timeout(ddelay * std.time.ns_per_ms, cbk);
self.timeoutids[self.timeoutid] = id;
defer self.timeoutid += 1;
return self.timeoutid;
}
pub fn _clearTimeout(self: *Window, loop: *Loop, id: u32) void {
// I do would prefer return an error in this case, but it seems some JS
// uses invalid id, in particular id 0.
// So we silently ignore invalid id for now.
if (id >= self.timeoutid) return;
loop.cancel(self.timeoutids[id], null);
}
};

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub const html: []const u8 =
\\<main id='content'>
\\<a href='foo'>OK</a>

1794
src/http/Client.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,458 +0,0 @@
const std = @import("std");
const c = @cImport({
@cInclude("lexbor/html/html.h");
});
// Public API
// ----------
// Tag
pub const Tag = enum(u8) {
a = c.LXB_TAG_A,
area = c.LXB_TAG_AREA,
audio = c.LXB_TAG_AUDIO,
br = c.LXB_TAG_BR,
base = c.LXB_TAG_BASE,
body = c.LXB_TAG_BODY,
button = c.LXB_TAG_BUTTON,
canvas = c.LXB_TAG_CANVAS,
dl = c.LXB_TAG_DL,
dialog = c.LXB_TAG_DIALOG,
data = c.LXB_TAG_DATA,
div = c.LXB_TAG_DIV,
embed = c.LXB_TAG_EMBED,
fieldset = c.LXB_TAG_FIELDSET,
form = c.LXB_TAG_FORM,
frameset = c.LXB_TAG_FRAMESET,
hr = c.LXB_TAG_HR,
head = c.LXB_TAG_HEAD,
h1 = c.LXB_TAG_H1,
h2 = c.LXB_TAG_H2,
h3 = c.LXB_TAG_H3,
h4 = c.LXB_TAG_H4,
h5 = c.LXB_TAG_H5,
h6 = c.LXB_TAG_H6,
html = c.LXB_TAG_HTML,
iframe = c.LXB_TAG_IFRAME,
img = c.LXB_TAG_IMG,
input = c.LXB_TAG_INPUT,
li = c.LXB_TAG_LI,
label = c.LXB_TAG_LABEL,
legend = c.LXB_TAG_LEGEND,
link = c.LXB_TAG_LINK,
map = c.LXB_TAG_MAP,
meta = c.LXB_TAG_META,
meter = c.LXB_TAG_METER,
ins = c.LXB_TAG_INS,
del = c.LXB_TAG_DEL,
ol = c.LXB_TAG_OL,
object = c.LXB_TAG_OBJECT,
optgroup = c.LXB_TAG_OPTGROUP,
option = c.LXB_TAG_OPTION,
output = c.LXB_TAG_OUTPUT,
p = c.LXB_TAG_P,
picture = c.LXB_TAG_PICTURE,
pre = c.LXB_TAG_PRE,
progress = c.LXB_TAG_PROGRESS,
blockquote = c.LXB_TAG_BLOCKQUOTE,
q = c.LXB_TAG_Q,
script = c.LXB_TAG_SCRIPT,
select = c.LXB_TAG_SELECT,
source = c.LXB_TAG_SOURCE,
span = c.LXB_TAG_SPAN,
style = c.LXB_TAG_STYLE,
table = c.LXB_TAG_TABLE,
caption = c.LXB_TAG_CAPTION,
th = c.LXB_TAG_TH,
td = c.LXB_TAG_TD,
col = c.LXB_TAG_COL,
tr = c.LXB_TAG_TR,
thead = c.LXB_TAG_THEAD,
tbody = c.LXB_TAG_TBODY,
tfoot = c.LXB_TAG_TFOOT,
template = c.LXB_TAG_TEMPLATE,
textarea = c.LXB_TAG_TEXTAREA,
time = c.LXB_TAG_TIME,
title = c.LXB_TAG_TITLE,
track = c.LXB_TAG_TRACK,
ul = c.LXB_TAG_UL,
video = c.LXB_TAG_VIDEO,
undef = c.LXB_TAG__UNDEF,
pub fn all() []Tag {
comptime {
const info = @typeInfo(Tag).Enum;
comptime var l: [info.fields.len]Tag = undefined;
inline for (info.fields, 0..) |field, i| {
l[i] = @as(Tag, @enumFromInt(field.value));
}
return &l;
}
}
pub fn allElements() [][]const u8 {
comptime {
const tags = all();
var names: [tags.len][]const u8 = undefined;
inline for (tags, 0..) |tag, i| {
names[i] = tag.elementName();
}
return &names;
}
}
fn upperName(comptime name: []const u8) []const u8 {
comptime {
var upper_name: [name.len]u8 = undefined;
for (name, 0..) |char, i| {
var to_upper = false;
if (i == 0) {
to_upper = true;
} else if (i == 1 and name.len == 2) {
to_upper = true;
}
if (to_upper) {
upper_name[i] = std.ascii.toUpper(char);
} else {
upper_name[i] = char;
}
}
return &upper_name;
}
}
fn elementName(comptime tag: Tag) []const u8 {
return switch (tag) {
.a => "Anchor",
.dl => "DList",
.fieldset => "FieldSet",
.frameset => "FrameSet",
.h1, .h2, .h3, .h4, .h5, .h6 => "Heading",
.iframe => "IFrame",
.img => "Image",
.ins, .del => "Mod",
.ol => "OList",
.optgroup => "OptGroup",
.p => "Paragraph",
.blockquote, .q => "Quote",
.caption => "TableCaption",
.th, .td => "TableCell",
.col => "TableCol",
.tr => "TableRow",
.thead, .tbody, .tfoot => "TableSection",
.textarea => "TextArea",
.ul => "UList",
.undef => "Unknown",
else => upperName(@tagName(tag)),
};
}
};
// EventTarget
pub const EventTarget = c.lxb_dom_event_target_t;
// Node
pub const Node = c.lxb_dom_node_t;
pub const NodeType = enum(u4) {
undef,
element,
attribute,
text,
cdata_section,
entity_reference,
entity,
processing_instruction,
comment,
document,
document_type,
document_fragment,
notation,
last_entry,
};
pub inline fn nodeEventTarget(node: *Node) *EventTarget {
return c.lxb_dom_interface_event_target(node);
}
pub inline fn nodeTag(node: *Node) Tag {
// FIXME: lxb_dom_node_tag_id returns a big number if element is unknwon
// while it should return 0 (value of enum LXB_TAG__UNDEF).
// This fix the problem by assuming that a value greater than an u8 (the basis
// of Tag enum) is 0.
var val = c.lxb_dom_node_tag_id(node);
if (val > 256) {
val = 0;
}
return @as(Tag, @enumFromInt(val));
}
pub const nodeWalker = (fn (node: ?*Node, _: ?*anyopaque) callconv(.C) Action);
pub inline fn nodeName(node: *Node) [*c]const u8 {
var s: usize = undefined;
return c.lxb_dom_node_name(node, &s);
}
pub inline fn nodeType(node: *Node) NodeType {
return @as(NodeType, @enumFromInt(node.*.type));
}
pub inline fn nodeWalk(node: *Node, comptime walker: nodeWalker) !void {
c.lxb_dom_node_simple_walk(node, walker, null);
}
// Element
pub const Element = c.lxb_dom_element_t;
pub inline fn elementNode(element: *Element) *Node {
return c.lxb_dom_interface_node(element);
}
pub inline fn elementLocalName(element: *Element) []const u8 {
var size: usize = undefined;
const local_name = c.lxb_dom_element_local_name(element, &size);
return std.mem.sliceTo(local_name, 0);
}
pub inline fn elementsByAttr(
element: *Element,
collection: *Collection,
attr: []const u8,
value: []const u8,
case_sensitve: bool,
) !void {
const status = c.lxb_dom_elements_by_attr(
element,
collection,
attr.ptr,
attr.len,
value.ptr,
value.len,
case_sensitve,
);
if (status != 0) {
return error.ElementsByAttr;
}
}
// DocumentHTML
pub const DocumentHTML = c.lxb_html_document_t;
pub inline fn documentHTMLInit() *DocumentHTML {
return c.lxb_html_document_create();
}
pub inline fn documentHTMLDeinit(document_html: *DocumentHTML) void {
_ = c.lxb_html_document_destroy(document_html);
}
pub inline fn documentHTMLParse(document_html: *DocumentHTML, html: []const u8) !void {
const status = c.lxb_html_document_parse(document_html, html.ptr, html.len - 1);
if (status != 0) {
return error.DocumentHTMLParse;
}
}
pub inline fn documentHTMLToNode(document_html: *DocumentHTML) *Node {
return c.lxb_dom_interface_node(document_html);
}
pub inline fn documentHTMLToDocument(document_html: *DocumentHTML) *Document {
return &document_html.dom_document;
}
pub inline fn documentHTMLBody(document_html: *DocumentHTML) *Body {
return document_html.body;
}
// Document
pub const Document = c.lxb_dom_document_t;
pub inline fn documentCreateElement(document: *Document, tag_name: []const u8) *Element {
return c.lxb_dom_document_create_element(document, tag_name.ptr, tag_name.len, null);
}
// Collection
pub const Collection = c.lxb_dom_collection_t;
pub inline fn collectionInit(document: *Document, size: usize) *Collection {
return c.lxb_dom_collection_make(document, size);
}
pub inline fn collectionDeinit(collection: *Collection) void {
_ = c.lxb_dom_collection_destroy(collection, true);
}
pub inline fn collectionElement(collection: *Collection, index: usize) *Element {
return c.lxb_dom_collection_element(collection, index);
}
// HTML Elements
pub const HTMLElement = c.lxb_html_element_t;
pub const MediaElement = c.lxb_html_media_element_t;
pub const Unknown = c.lxb_html_unknown_element_t;
pub const Anchor = c.lxb_html_anchor_element_t;
pub const Area = c.lxb_html_area_element_t;
pub const Audio = c.lxb_html_audio_element_t;
pub const BR = c.lxb_html_br_element_t;
pub const Base = c.lxb_html_base_element_t;
pub const Body = c.lxb_html_body_element_t;
pub const Button = c.lxb_html_button_element_t;
pub const Canvas = c.lxb_html_canvas_element_t;
pub const DList = c.lxb_html_d_list_element_t;
pub const Data = c.lxb_html_data_element_t;
pub const Dialog = c.lxb_html_dialog_element_t;
pub const Div = c.lxb_html_div_element_t;
pub const Embed = c.lxb_html_embed_element_t;
pub const FieldSet = c.lxb_html_field_set_element_t;
pub const Form = c.lxb_html_form_element_t;
pub const FrameSet = c.lxb_html_frame_set_element_t;
pub const HR = c.lxb_html_hr_element_t;
pub const Head = c.lxb_html_head_element_t;
pub const Heading = c.lxb_html_heading_element_t;
pub const Html = c.lxb_html_html_element_t;
pub const IFrame = c.lxb_html_iframe_element_t;
pub const Image = c.lxb_html_image_element_t;
pub const Input = c.lxb_html_input_element_t;
pub const LI = c.lxb_html_li_element_t;
pub const Label = c.lxb_html_label_element_t;
pub const Legend = c.lxb_html_legend_element_t;
pub const Link = c.lxb_html_link_element_t;
pub const Map = c.lxb_html_map_element_t;
pub const Meta = c.lxb_html_meta_element_t;
pub const Meter = c.lxb_html_meter_element_t;
pub const Mod = c.lxb_html_mod_element_t;
pub const OList = c.lxb_html_o_list_element_t;
pub const Object = c.lxb_html_object_element_t;
pub const OptGroup = c.lxb_html_opt_group_element_t;
pub const Option = c.lxb_html_option_element_t;
pub const Output = c.lxb_html_output_element_t;
pub const Paragraph = c.lxb_html_paragraph_element_t;
pub const Picture = c.lxb_html_picture_element_t;
pub const Pre = c.lxb_html_pre_element_t;
pub const Progress = c.lxb_html_progress_element_t;
pub const Quote = c.lxb_html_quote_element_t;
pub const Script = c.lxb_html_script_element_t;
pub const Select = c.lxb_html_select_element_t;
pub const Source = c.lxb_html_source_element_t;
pub const Span = c.lxb_html_span_element_t;
pub const Style = c.lxb_html_style_element_t;
pub const Table = c.lxb_html_table_element_t;
pub const TableCaption = c.lxb_html_table_caption_element_t;
pub const TableCell = c.lxb_html_table_cell_element_t;
pub const TableCol = c.lxb_html_table_col_element_t;
pub const TableRow = c.lxb_html_table_row_element_t;
pub const TableSection = c.lxb_html_table_section_element_t;
pub const Template = c.lxb_html_template_element_t;
pub const TextArea = c.lxb_html_text_area_element_t;
pub const Time = c.lxb_html_time_element_t;
pub const Title = c.lxb_html_title_element_t;
pub const Track = c.lxb_html_track_element_t;
pub const UList = c.lxb_html_u_list_element_t;
pub const Video = c.lxb_html_video_element_t;
// Base
pub const Action = c.lexbor_action_t;
// TODO: use enum?
pub const ActionStop = c.LEXBOR_ACTION_STOP;
pub const ActionNext = c.LEXBOR_ACTION_NEXT;
pub const ActionOk = c.LEXBOR_ACTION_OK;
// Playground
// ----------
fn serialize_callback(_: [*c]const u8, _: usize, _: ?*anyopaque) callconv(.C) c_uint {
return 0;
}
fn walker_play(nn: ?*c.lxb_dom_node_t, _: ?*anyopaque) callconv(.C) c.lexbor_action_t {
if (nn == null) {
return c.LEXBOR_ACTION_STOP;
}
const n = nn.?;
var s: usize = undefined;
const name = c.lxb_dom_node_name(n, &s);
std.debug.print("type: {d}, name: {s}\n", .{ n.*.type, name });
if (n.*.local_name == c.LXB_TAG_A) {
const element = c.lxb_dom_interface_element(n);
const attr = element.*.first_attr;
std.debug.print("link, attr: {any}\n", .{attr.*.upper_name});
}
return c.LEXBOR_ACTION_OK;
}
pub fn parse_document() void {
const html = "<div><a href='foo'>OK</a><p>blah-blah-blah</p></div>";
const html_len = html.len - 1;
// parse
const doc = c.lxb_html_document_create();
const status_parse = c.lxb_html_document_parse(doc, html, html_len);
std.debug.print("status parse: {any}\n", .{status_parse});
// tree
const document_node = c.lxb_dom_interface_node(doc);
std.debug.print("document node is empty: {any}\n", .{c.lxb_dom_node_is_empty(document_node)});
std.debug.print("document node type: {any}\n", .{document_node.*.type});
std.debug.print("document node name: {any}\n", .{document_node.*.local_name});
c.lxb_dom_node_simple_walk(document_node, walker_play, null);
const first_child = c.lxb_dom_node_last_child(document_node);
if (first_child == null) {
std.debug.print("hummm is null\n", .{});
}
std.debug.print("first child type: {any}\n", .{first_child.*.type});
std.debug.print("first child name: {any}\n", .{first_child.*.local_name});
const tt = c.lxb_dom_node_first_child(first_child);
std.debug.print("tt type: {any}\n", .{tt.*.type});
std.debug.print("tt name: {any}\n", .{tt.*.local_name});
std.debug.print("{any}\n", .{c.LXB_DOM_NODE_TYPE_TEXT});
var s: usize = undefined;
const tt_name = c.lxb_dom_node_name(tt, &s);
std.debug.print("tt name: {s}\n", .{tt_name});
const nn = tt.*.first_child;
if (nn == null) {
std.debug.print("is null\n", .{});
}
// text
var text_len: usize = undefined;
var text = c.lxb_dom_node_text_content(tt, &text_len);
std.debug.print("size: {d}\n", .{text_len});
std.debug.print("text: {s}\n", .{text});
// serialize
const status_serialize = c.lxb_html_serialize_pretty_tree_cb(
document_node,
c.LXB_HTML_SERIALIZE_OPT_UNDEF,
0,
serialize_callback,
null,
);
std.debug.print("status serialize: {any}\n", .{status_serialize});
// destroy
_ = c.lxb_html_document_destroy(doc);
// _ = c.lxb_dom_document_destroy_text(first_child.*.owner_document, &text);
// _ = c.lxb_dom_document_destroy_text(c.lxb_dom_interface_document(document), text);
std.debug.print("text2: {s}\n", .{text}); // should not work
}

View File

@@ -1,32 +1,54 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
const parser = @import("netsurf.zig");
const parser = @import("netsurf");
const apiweb = @import("apiweb.zig");
const Window = @import("html/window.zig").Window;
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
const socket_path = "/tmp/browsercore-server.sock";
var doc: *parser.DocumentHTML = undefined;
var server: std.net.StreamServer = undefined;
var server: std.net.Server = undefined;
fn execJS(
alloc: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
// start JS env
try js_env.start(alloc);
try js_env.start();
defer js_env.stop();
// alias global as self and window
try js_env.attachObject(try js_env.getGlobal(), "self", null);
try js_env.attachObject(try js_env.getGlobal(), "window", null);
var window = Window.create(null);
window.replaceDocument(doc);
try js_env.bindGlobal(window);
// add document object
try js_env.addObject(doc, "document");
// try catch
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(js_env.*);
defer try_catch.deinit();
while (true) {
@@ -40,11 +62,12 @@ fn execJS(
break;
}
const res = try js_env.execTryCatch(alloc, cmd, "cdp");
if (res.success) {
std.debug.print("-> {s}\n", .{res.result});
}
_ = try conn.stream.write(res.result);
const res = try js_env.exec(cmd, "cdp");
const res_str = try res.toString(alloc, js_env.*);
defer alloc.free(res_str);
std.debug.print("-> {s}\n", .{res_str});
_ = try conn.stream.write(res_str);
}
}
@@ -58,6 +81,9 @@ pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
try parser.init();
defer parser.deinit();
// document
const file = try std.fs.cwd().openFile("test.html", .{});
defer file.close();
@@ -71,7 +97,7 @@ pub fn main() !void {
// reuse_address (SO_REUSEADDR flag) does not seems to work on unix socket
// see: https://gavv.net/articles/unix-socket-reuse/
// TODO: use a lock file instead
std.os.unlink(socket_path) catch |err| {
std.posix.unlink(socket_path) catch |err| {
if (err != error.FileNotFound) {
return err;
}
@@ -79,10 +105,9 @@ pub fn main() !void {
// server
const addr = try std.net.Address.initUnix(socket_path);
server = std.net.StreamServer.init(.{});
server = try addr.listen(.{});
defer server.deinit();
try server.listen(addr);
std.debug.print("Listening on: {s}...\n", .{socket_path});
try jsruntime.loadEnv(&arena, execJS);
try jsruntime.loadEnv(&arena, null, execJS);
}

View File

@@ -1,12 +1,32 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Browser = @import("browser/browser.zig").Browser;
const jsruntime = @import("jsruntime");
const apiweb = @import("apiweb.zig");
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const std_options = struct {
pub const log_level = .debug;
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
pub const std_options = std.Options{
.log_level = .debug,
};
const usage =
@@ -38,7 +58,7 @@ pub fn main() !void {
while (args.next()) |arg| {
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
try std.io.getStdErr().writer().print(usage, .{execname});
std.os.exit(0);
std.posix.exit(0);
}
if (std.mem.eql(u8, "--dump", arg)) {
dump = true;
@@ -47,14 +67,14 @@ pub fn main() !void {
// allow only one url
if (url.len != 0) {
try std.io.getStdErr().writer().print(usage, .{execname});
std.os.exit(1);
std.posix.exit(1);
}
url = arg;
}
if (url.len == 0) {
try std.io.getStdErr().writer().print(usage, .{execname});
std.os.exit(1);
std.posix.exit(1);
}
const vm = jsruntime.VM.init();
@@ -64,8 +84,12 @@ pub fn main() !void {
defer browser.deinit();
var page = try browser.currentSession().createPage();
defer page.end();
defer page.deinit();
try page.navigate(url);
defer page.end();
try page.wait();
if (dump) {
try page.dump(std.io.getStdOut());

View File

@@ -1,13 +1,35 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
const parser = @import("netsurf.zig");
const parser = @import("netsurf");
const apiweb = @import("apiweb.zig");
const Window = @import("html/window.zig").Window;
const storage = @import("storage/storage.zig");
const html_test = @import("html_test.zig").html;
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
const Client = @import("async/Client.zig");
var doc: *parser.DocumentHTML = undefined;
@@ -15,17 +37,26 @@ fn execJS(
alloc: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
// start JS env
try js_env.start(alloc);
try js_env.start();
defer js_env.stop();
// alias global as self and window
try js_env.attachObject(try js_env.getGlobal(), "self", null);
try js_env.attachObject(try js_env.getGlobal(), "window", null);
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
defer cli.deinit();
// add document object
try js_env.addObject(doc, "document");
try js_env.setUserContext(UserContext{
.document = doc,
.httpClient = &cli,
});
var storageShelf = storage.Shelf.init(alloc);
defer storageShelf.deinit();
// alias global as self and window
var window = Window.create(null);
window.replaceDocument(doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(window);
// launch shellExec
try jsruntime.shellExec(alloc, js_env);
@@ -39,6 +70,9 @@ pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
try parser.init();
defer parser.deinit();
// document
const file = try std.fs.cwd().openFile("test.html", .{});
defer file.close();

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
@@ -30,6 +48,8 @@ const Out = enum {
};
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const GlobalType = apiweb.GlobalType;
pub const UserContext = apiweb.UserContext;
// TODO For now the WPT tests run is specific to WPT.
// It manually load js framwork libs, and run the first script w/ js content in
@@ -56,7 +76,7 @@ pub fn main() !void {
while (args.next()) |arg| {
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
try std.io.getStdErr().writer().print(usage, .{execname});
std.os.exit(0);
std.posix.exit(0);
}
if (std.mem.eql(u8, "--json", arg)) {
out = .json;
@@ -122,7 +142,7 @@ pub fn main() !void {
defer arena.deinit();
const res = wpt.run(&arena, wpt_dir, tc, &loader) catch |err| {
const suite = try Suite.init(alloc, tc, false, @errorName(err), null);
const suite = try Suite.init(alloc, tc, false, @errorName(err));
try results.append(suite);
if (out == .text) {
@@ -131,9 +151,9 @@ pub fn main() !void {
failures += 1;
continue;
};
// no need to call res.deinit() thanks to the arena allocator.
defer res.deinit(arena.allocator());
const suite = try Suite.init(alloc, tc, res.success, res.result, res.stack);
const suite = try Suite.init(alloc, tc, res.ok, res.msg orelse "");
try results.append(suite);
if (out == .json) {
@@ -176,7 +196,7 @@ pub fn main() !void {
try cases.append(Case{
.pass = suite.pass,
.name = suite.name,
.message = suite.stack orelse suite.message,
.message = suite.message,
});
}
@@ -194,12 +214,12 @@ pub fn main() !void {
}
try std.json.stringify(output.items, .{ .whitespace = .indent_2 }, std.io.getStdOut().writer());
std.os.exit(0);
std.posix.exit(0);
}
if (out == .text and failures > 0) {
std.debug.print("{d}/{d} tests suites failures\n", .{ failures, run });
std.os.exit(1);
std.posix.exit(1);
}
}
@@ -268,7 +288,7 @@ fn runSafe(
argv.appendAssumeCapacity(tc);
defer _ = argv.pop();
const run = try std.ChildProcess.run(.{
const run = try std.process.Child.run(.{
.allocator = alloc,
.argv = argv.items,
.max_output_bytes = 1024 * 1024,
@@ -303,7 +323,7 @@ fn runSafe(
if (c.pass) pass += 1;
}
}
const status = if (pass == all) "Pass" else "Fail";
const status = if (all > 0 and pass == all) "Pass" else "Fail";
std.debug.print("{s} {d}/{d}", .{ status, pass, all });
continue;
@@ -346,7 +366,8 @@ fn runSafe(
if (c.pass) pass += 1;
}
}
std.debug.print("{d}/{d}\n\n", .{ pass, all });
const status = if (all > 0 and pass == all) "Pass" else "Fail";
std.debug.print("{s} {d}/{d}\n\n", .{ status, pass, all });
}
if (out == .json) {

76
src/mimalloc/mimalloc.zig Normal file
View File

@@ -0,0 +1,76 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// This file makes the glue between mimalloc heap allocation and libdom memory
// management.
// We replace the libdom default usage of allocations with mimalloc heap
// allocation to be able to free all memory used at once, like an arena usage.
const std = @import("std");
const c = @cImport({
@cInclude("mimalloc.h");
});
const Error = error{
HeapNotNull,
HeapNull,
};
var heap: ?*c.mi_heap_t = null;
pub fn create() Error!void {
if (heap != null) return Error.HeapNotNull;
heap = c.mi_heap_new();
if (heap == null) return Error.HeapNull;
}
pub fn destroy() void {
if (heap == null) return;
c.mi_heap_destroy(heap.?);
heap = null;
}
pub export fn m_alloc(size: usize) callconv(.C) ?*anyopaque {
if (heap == null) return null;
return c.mi_heap_malloc(heap.?, size);
}
pub export fn re_alloc(ptr: ?*anyopaque, size: usize) callconv(.C) ?*anyopaque {
if (heap == null) return null;
return c.mi_heap_realloc(heap.?, ptr, size);
}
pub export fn c_alloc(nmemb: usize, size: usize) callconv(.C) ?*anyopaque {
if (heap == null) return null;
return c.mi_heap_calloc(heap.?, nmemb, size);
}
pub export fn str_dup(s: [*c]const u8) callconv(.C) [*c]u8 {
if (heap == null) return null;
return c.mi_heap_strdup(heap.?, s);
}
pub export fn strn_dup(s: [*c]const u8, size: usize) callconv(.C) [*c]u8 {
if (heap == null) return null;
return c.mi_heap_strndup(heap.?, s, size);
}
// NOOP, use destroy to clear all the memory allocated at once.
pub export fn f_ree(_: ?*anyopaque) callconv(.C) void {
return;
}

View File

@@ -1,14 +1,52 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const c = @cImport({
@cInclude("dom/dom.h");
@cInclude("core/pi.h");
@cInclude("dom/bindings/hubbub/parser.h");
@cInclude("events/event_target.h");
@cInclude("events/event.h");
});
const mimalloc = @import("mimalloc");
const Callback = @import("jsruntime").Callback;
const EventToInterface = @import("events/event.zig").Event.toInterface;
// init initializes netsurf lib.
// init starts a mimalloc heap arena for the netsurf session. The caller must
// call deinit() to free the arena memory.
pub fn init() !void {
try mimalloc.create();
}
// deinit frees the mimalloc heap arena memory.
// It also clean dom namespaces and lwc strings.
pub fn deinit() void {
_ = c.dom_namespace_finalise();
// destroy all lwc strings.
c.lwc_deinit_strings();
mimalloc.destroy();
}
// Vtable
// ------
@@ -223,8 +261,8 @@ pub const Tag = enum(u8) {
pub fn all() []Tag {
comptime {
const info = @typeInfo(Tag).Enum;
comptime var l: [info.fields.len]Tag = undefined;
inline for (info.fields, 0..) |field, i| {
var l: [info.fields.len]Tag = undefined;
for (info.fields, 0..) |field, i| {
l[i] = @as(Tag, @enumFromInt(field.value));
}
return &l;
@@ -235,7 +273,7 @@ pub const Tag = enum(u8) {
comptime {
const tags = all();
var names: [tags.len][]const u8 = undefined;
inline for (tags, 0..) |tag, i| {
for (tags, 0..) |tag, i| {
names[i] = tag.elementName();
}
return &names;
@@ -318,6 +356,12 @@ pub const DOMError = error{
Timeout,
InvalidNodeType,
DataClone,
// custom netsurf error
UnspecifiedEventType,
DispatchRequest,
NoMemory,
AttributeWrongType,
};
const DOMException = c.dom_exception;
@@ -342,6 +386,13 @@ fn DOMErr(except: DOMException) DOMError!void {
c.DOM_INVALID_ACCESS_ERR => DOMError.InvalidAccess,
c.DOM_VALIDATION_ERR => DOMError.Validation,
c.DOM_TYPE_MISMATCH_ERR => DOMError.TypeMismatch,
// custom netsurf error
c.DOM_UNSPECIFIED_EVENT_TYPE_ERR => DOMError.UnspecifiedEventType,
c.DOM_DISPATCH_REQUEST_ERR => DOMError.DispatchRequest,
c.DOM_NO_MEM_ERR => DOMError.NoMemory,
c.DOM_ATTR_WRONG_TYPE_ERR => DOMError.AttributeWrongType,
else => unreachable,
};
}
@@ -376,6 +427,10 @@ pub fn eventType(evt: *Event) ![]const u8 {
var s: ?*String = undefined;
const err = c._dom_event_get_type(evt, &s);
try DOMErr(err);
// if the event type is null, return a empty string.
if (s == null) return "";
return strToData(s.?);
}
@@ -467,30 +522,34 @@ pub const EventType = enum(u8) {
progress_event = 1,
};
// EventHandler
fn event_handler_cbk(data: *anyopaque) *Callback {
const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(data);
return @as(*Callback, @ptrCast(ptr));
pub const MutationEvent = c.dom_mutation_event;
pub fn eventToMutationEvent(evt: *Event) *MutationEvent {
return @as(*MutationEvent, @ptrCast(evt));
}
const event_handler = struct {
fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void {
if (data) |d| {
const func = event_handler_cbk(d);
pub fn mutationEventAttributeName(evt: *MutationEvent) ![]const u8 {
var s: ?*String = undefined;
const err = c._dom_mutation_event_get_attr_name(evt, &s);
try DOMErr(err);
return strToData(s.?);
}
if (event) |evt| {
func.call(.{
EventToInterface(evt) catch unreachable,
}) catch unreachable;
} else {
func.call(.{event}) catch unreachable;
}
// NOTE: we can not call func.deinit here
// b/c the handler can be called several times
// either on this dispatch event or in anoter one
}
}
}.handle;
pub fn mutationEventPrevValue(evt: *MutationEvent) !?[]const u8 {
var s: ?*String = undefined;
const err = c._dom_mutation_event_get_prev_value(evt, &s);
try DOMErr(err);
if (s == null) return null;
return strToData(s.?);
}
pub fn mutationEventRelatedNode(evt: *MutationEvent) !?*Node {
var n: NodeExternal = undefined;
const err = c._dom_mutation_event_get_related_node(evt, &n);
try DOMErr(err);
if (n == null) return null;
return @as(*Node, @ptrCast(n));
}
// EventListener
pub const EventListener = c.dom_event_listener;
@@ -503,6 +562,10 @@ fn eventListenerGetData(lst: *EventListener) ?*anyopaque {
// EventTarget
pub const EventTarget = c.dom_event_target;
pub fn eventTargetToNode(et: *EventTarget) *Node {
return @as(*Node, @ptrCast(et));
}
fn eventTargetVtable(et: *EventTarget) c.dom_event_target_vtable {
// retrieve the vtable
const vtable = et.*.vtable.?;
@@ -551,10 +614,9 @@ pub fn eventTargetHasListener(
// and capture property,
// let's check if the callback handler is the same
defer c.dom_event_listener_unref(listener);
const data = eventListenerGetData(listener);
if (data) |d| {
const cbk = event_handler_cbk(d);
if (cbk_id == cbk.id()) {
const ehd = EventHandlerDataInternal.fromListener(listener);
if (ehd) |d| {
if (cbk_id == d.data.cbk.id()) {
return lst;
}
}
@@ -572,21 +634,99 @@ pub fn eventTargetHasListener(
return null;
}
// EventHandlerFunc is a zig function called when the event is dispatched to a
// listener.
// The EventHandlerFunc is responsible to call the callback included into the
// EventHandlerData.
pub const EventHandlerFunc = *const fn (event: ?*Event, data: EventHandlerData) void;
// EventHandler implements the function exposed in C and called by libdom.
// It retrieves the EventHandlerInternalData and call the EventHandlerFunc with
// the EventHandlerData in parameter.
const EventHandler = struct {
fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void {
if (data) |d| {
const ehd = EventHandlerDataInternal.get(d);
ehd.handler(event, ehd.data);
// NOTE: we can not call func.deinit here
// b/c the handler can be called several times
// either on this dispatch event or in anoter one
}
}
}.handle;
// EventHandlerData contains a JS callback and the data associated to the
// handler.
// If given, deinitFunc is called with the data pointer to allow the creator to
// clean memory.
// The callback is deinit by EventHandlerDataInternal. It must NOT be deinit
// into deinitFunc.
pub const EventHandlerData = struct {
cbk: Callback,
data: ?*anyopaque = null,
// deinitFunc implements the data deinitialization.
deinitFunc: ?DeinitFunc = null,
pub const DeinitFunc = *const fn (data: ?*anyopaque, alloc: std.mem.Allocator) void;
};
// EventHandlerDataInternal groups the EventHandlerFunc and the EventHandlerData.
const EventHandlerDataInternal = struct {
data: EventHandlerData,
handler: EventHandlerFunc,
fn init(alloc: std.mem.Allocator, handler: EventHandlerFunc, data: EventHandlerData) !*EventHandlerDataInternal {
const ptr = try alloc.create(EventHandlerDataInternal);
ptr.* = .{
.data = data,
.handler = handler,
};
return ptr;
}
fn deinit(self: *EventHandlerDataInternal, alloc: std.mem.Allocator) void {
if (self.data.deinitFunc) |d| d(self.data.data, alloc);
self.data.cbk.deinit(alloc);
alloc.destroy(self);
}
fn get(data: *anyopaque) *EventHandlerDataInternal {
const ptr: *align(@alignOf(*EventHandlerDataInternal)) anyopaque = @alignCast(data);
return @as(*EventHandlerDataInternal, @ptrCast(ptr));
}
// retrieve a EventHandlerDataInternal from a listener.
fn fromListener(lst: *EventListener) ?*EventHandlerDataInternal {
const data = eventListenerGetData(lst);
// free cbk allocation made on eventTargetAddEventListener
if (data == null) return null;
return get(data.?);
}
};
pub fn eventTargetAddEventListener(
et: *EventTarget,
alloc: std.mem.Allocator,
typ: []const u8,
cbk: Callback,
handlerFunc: EventHandlerFunc,
data: EventHandlerData,
capture: bool,
) !void {
// this allocation will be removed either on
// eventTargetRemoveEventListener or eventTargetRemoveAllEventListeners
const cbk_ptr = try alloc.create(Callback);
cbk_ptr.* = cbk;
const ehd = try EventHandlerDataInternal.init(alloc, handlerFunc, data);
errdefer ehd.deinit(alloc);
const ctx = @as(*anyopaque, @ptrCast(cbk_ptr));
// When a function is used as an event handler, its this parameter is bound
// to the DOM element on which the listener is placed.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#this_in_dom_event_handlers
try ehd.data.cbk.setThisArg(et);
const ctx = @as(*anyopaque, @ptrCast(ehd));
var listener: ?*EventListener = undefined;
const errLst = c.dom_event_listener_create(event_handler, ctx, &listener);
const errLst = c.dom_event_listener_create(EventHandler, ctx, &listener);
try DOMErr(errLst);
defer c.dom_event_listener_unref(listener);
@@ -602,13 +742,9 @@ pub fn eventTargetRemoveEventListener(
lst: *EventListener,
capture: bool,
) !void {
const data = eventListenerGetData(lst);
// free cbk allocation made on eventTargetAddEventListener
if (data) |d| {
const cbk_ptr = event_handler_cbk(d);
cbk_ptr.deinit(alloc);
alloc.destroy(cbk_ptr);
}
// free data allocation made on eventTargetAddEventListener
const ehd = EventHandlerDataInternal.fromListener(lst);
if (ehd) |d| d.deinit(alloc);
const s = try strFromData(typ);
const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture);
@@ -636,13 +772,10 @@ pub fn eventTargetRemoveAllEventListeners(
if (lst) |listener| {
defer c.dom_event_listener_unref(listener);
const data = eventListenerGetData(listener);
if (data) |d| {
// free cbk allocation made on eventTargetAddEventListener
const cbk = event_handler_cbk(d);
cbk.deinit(alloc);
alloc.destroy(cbk);
}
const ehd = EventHandlerDataInternal.fromListener(listener);
if (ehd) |d| d.deinit(alloc);
const err = eventTargetVtable(et).remove_event_listener.?(
et,
null,
@@ -1235,6 +1368,18 @@ pub const Comment = c.dom_comment;
// ProcessingInstruction
pub const ProcessingInstruction = c.dom_processing_instruction;
// processingInstructionToNode is an helper to convert an ProcessingInstruction to a node.
pub inline fn processingInstructionToNode(pi: *ProcessingInstruction) *Node {
return @as(*Node, @ptrCast(pi));
}
pub fn processInstructionCopy(pi: *ProcessingInstruction) !*ProcessingInstruction {
var res: ?*Node = undefined;
const err = c._dom_pi_copy(processingInstructionToNode(pi), &res);
try DOMErr(err);
return @as(*ProcessingInstruction, @ptrCast(res.?));
}
// Attribute
pub const Attribute = c.dom_attr;
@@ -1299,6 +1444,20 @@ pub fn elementGetAttribute(elem: *Element, name: []const u8) !?[]const u8 {
return strToData(s.?);
}
pub fn elementGetAttributeNS(elem: *Element, ns: []const u8, name: []const u8) !?[]const u8 {
var s: ?*String = undefined;
const err = elementVtable(elem).dom_element_get_attribute_ns.?(
elem,
try strFromData(ns),
try strFromData(name),
&s,
);
try DOMErr(err);
if (s == null) return null;
return strToData(s.?);
}
pub fn elementSetAttribute(elem: *Element, qname: []const u8, value: []const u8) !void {
const err = elementVtable(elem).dom_element_set_attribute.?(
elem,
@@ -1308,11 +1467,35 @@ pub fn elementSetAttribute(elem: *Element, qname: []const u8, value: []const u8)
try DOMErr(err);
}
pub fn elementSetAttributeNS(
elem: *Element,
ns: []const u8,
qname: []const u8,
value: []const u8,
) !void {
const err = elementVtable(elem).dom_element_set_attribute_ns.?(
elem,
try strFromData(ns),
try strFromData(qname),
try strFromData(value),
);
try DOMErr(err);
}
pub fn elementRemoveAttribute(elem: *Element, qname: []const u8) !void {
const err = elementVtable(elem).dom_element_remove_attribute.?(elem, try strFromData(qname));
try DOMErr(err);
}
pub fn elementRemoveAttributeNS(elem: *Element, ns: []const u8, qname: []const u8) !void {
const err = elementVtable(elem).dom_element_remove_attribute_ns.?(
elem,
try strFromData(ns),
try strFromData(qname),
);
try DOMErr(err);
}
pub fn elementHasAttribute(elem: *Element, qname: []const u8) !bool {
var res: bool = undefined;
const err = elementVtable(elem).dom_element_has_attribute.?(elem, try strFromData(qname), &res);
@@ -1440,6 +1623,85 @@ pub fn elementHTMLGetTagType(elem_html: *ElementHTML) !Tag {
return @as(Tag, @enumFromInt(tag_type));
}
// HTMLScriptElement
// scriptToElt is an helper to convert an script to an element.
pub inline fn scriptToElt(s: *Script) *Element {
return @as(*Element, @ptrCast(s));
}
// HTMLAnchorElement
// anchorToNode is an helper to convert an anchor to a node.
pub inline fn anchorToNode(a: *Anchor) *Node {
return @as(*Node, @ptrCast(a));
}
pub fn anchorGetTarget(a: *Anchor) ![]const u8 {
var res: ?*String = undefined;
const err = c.dom_html_anchor_element_get_target(a, &res);
try DOMErr(err);
if (res == null) return "";
return strToData(res.?);
}
pub fn anchorSetTarget(a: *Anchor, target: []const u8) !void {
const err = c.dom_html_anchor_element_set_target(a, try strFromData(target));
try DOMErr(err);
}
pub fn anchorGetHref(a: *Anchor) ![]const u8 {
var res: ?*String = undefined;
const err = c.dom_html_anchor_element_get_href(a, &res);
try DOMErr(err);
if (res == null) return "";
return strToData(res.?);
}
pub fn anchorSetHref(a: *Anchor, href: []const u8) !void {
const err = c.dom_html_anchor_element_set_href(a, try strFromData(href));
try DOMErr(err);
}
pub fn anchorGetHrefLang(a: *Anchor) ![]const u8 {
var res: ?*String = undefined;
const err = c.dom_html_anchor_element_get_hreflang(a, &res);
try DOMErr(err);
if (res == null) return "";
return strToData(res.?);
}
pub fn anchorSetHrefLang(a: *Anchor, href: []const u8) !void {
const err = c.dom_html_anchor_element_set_hreflang(a, try strFromData(href));
try DOMErr(err);
}
pub fn anchorGetType(a: *Anchor) ![]const u8 {
var res: ?*String = undefined;
const err = c.dom_html_anchor_element_get_type(a, &res);
try DOMErr(err);
if (res == null) return "";
return strToData(res.?);
}
pub fn anchorSetType(a: *Anchor, t: []const u8) !void {
const err = c.dom_html_anchor_element_set_type(a, try strFromData(t));
try DOMErr(err);
}
pub fn anchorGetRel(a: *Anchor) ![]const u8 {
var res: ?*String = undefined;
const err = c.dom_html_anchor_element_get_rel(a, &res);
try DOMErr(err);
if (res == null) return "";
return strToData(res.?);
}
pub fn anchorSetRel(a: *Anchor, rel: []const u8) !void {
const err = c.dom_html_anchor_element_set_rel(a, try strFromData(rel));
try DOMErr(err);
}
// ElementsHTML
pub const MediaElement = struct { base: *c.dom_html_element };
@@ -1514,6 +1776,22 @@ pub const Video = struct { base: *c.dom_html_element };
// Document Fragment
pub const DocumentFragment = c.dom_document_fragment;
pub inline fn documentFragmentToNode(doc: *DocumentFragment) *Node {
return @as(*Node, @ptrCast(doc));
}
pub fn documentFragmentBodyChildren(doc: *DocumentFragment) !?*NodeList {
const node = documentFragmentToNode(doc);
const html = try nodeFirstChild(node) orelse return null;
// TODO unref
const head = try nodeFirstChild(html) orelse return null;
// TODO unref
const body = try nodeNextSibling(head) orelse return null;
// TODO unref
return try nodeGetChildNodes(body);
}
// Document Position
pub const DocumentPosition = enum(u2) {
@@ -1595,21 +1873,29 @@ pub inline fn domImplementationCreateDocumentType(
return dt.?;
}
pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8) !*Document {
var doc: ?*Document = undefined;
const err = c.dom_implementation_create_document(
c.DOM_IMPLEMENTATION_HTML,
null,
null,
null,
null,
null,
&doc,
);
try DOMErr(err);
// TODO set title
_ = title;
return doc.?;
pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8) !*DocumentHTML {
const doc_html = try documentCreateDocument(title);
const doc = documentHTMLToDocument(doc_html);
// add hierarchy: html, head, body.
const html = try documentCreateElement(doc, "html");
_ = try nodeAppendChild(documentToNode(doc), elementToNode(html));
const head = try documentCreateElement(doc, "head");
_ = try nodeAppendChild(elementToNode(html), elementToNode(head));
if (title) |t| {
try documentHTMLSetTitle(doc_html, t);
const htitle = try documentCreateElement(doc, "title");
const txt = try documentCreateTextNode(doc, t);
_ = try nodeAppendChild(elementToNode(htitle), @as(*Node, @ptrCast(txt)));
_ = try nodeAppendChild(elementToNode(head), elementToNode(htitle));
}
const body = try documentCreateElement(doc, "body");
_ = try nodeAppendChild(elementToNode(html), elementToNode(body));
return doc_html;
}
// Document
@@ -1653,6 +1939,11 @@ pub inline fn documentGetDocumentURI(doc: *Document) ![]const u8 {
return strToData(s.?);
}
pub fn documentSetDocumentURI(doc: *Document, uri: []const u8) !void {
const err = documentVtable(doc).dom_document_set_uri.?(doc, try strFromData(uri));
try DOMErr(err);
}
pub inline fn documentGetInputEncoding(doc: *Document) ![]const u8 {
var s: ?*String = undefined;
const err = documentVtable(doc).dom_document_get_input_encoding.?(doc, &s);
@@ -1660,6 +1951,28 @@ pub inline fn documentGetInputEncoding(doc: *Document) ![]const u8 {
return strToData(s.?);
}
pub inline fn documentSetInputEncoding(doc: *Document, enc: []const u8) !void {
const err = documentVtable(doc).dom_document_set_input_encoding.?(doc, try strFromData(enc));
try DOMErr(err);
}
pub inline fn documentCreateDocument(title: ?[]const u8) !*DocumentHTML {
var doc: ?*Document = undefined;
const err = c.dom_implementation_create_document(
c.DOM_IMPLEMENTATION_HTML,
null,
null,
null,
null,
null,
&doc,
);
try DOMErr(err);
const doc_html = @as(*DocumentHTML, @ptrCast(doc.?));
if (title) |t| try documentHTMLSetTitle(doc_html, t);
return doc_html;
}
pub inline fn documentCreateElement(doc: *Document, tag_name: []const u8) !*Element {
var elem: ?*Element = undefined;
const err = documentVtable(doc).dom_document_create_element.?(doc, try strFromData(tag_name), &elem);
@@ -1821,9 +2134,40 @@ pub fn documentHTMLParse(reader: anytype, enc: ?[:0]const u8) !*DocumentHTML {
var parser: ?*c.dom_hubbub_parser = undefined;
var doc: ?*c.dom_document = undefined;
var err: c.hubbub_error = undefined;
var params = parseParams(enc);
var params = c.dom_hubbub_parser_params{
.enc = null,
err = c.dom_hubbub_parser_create(&params, &parser, &doc);
try parserErr(err);
defer c.dom_hubbub_parser_destroy(parser);
try parseData(parser.?, reader);
return @as(*DocumentHTML, @ptrCast(doc.?));
}
pub fn documentParseFragmentFromStr(self: *Document, str: []const u8) !*DocumentFragment {
var fbs = std.io.fixedBufferStream(str);
return try documentParseFragment(self, fbs.reader(), "UTF-8");
}
pub fn documentParseFragment(self: *Document, reader: anytype, enc: ?[:0]const u8) !*DocumentFragment {
var parser: ?*c.dom_hubbub_parser = undefined;
var fragment: ?*c.dom_document_fragment = undefined;
var err: c.hubbub_error = undefined;
var params = parseParams(enc);
err = c.dom_hubbub_fragment_parser_create(&params, self, &parser, &fragment);
try parserErr(err);
defer c.dom_hubbub_parser_destroy(parser);
try parseData(parser.?, reader);
return @as(*DocumentFragment, @ptrCast(fragment.?));
}
fn parseParams(enc: ?[:0]const u8) c.dom_hubbub_parser_params {
return .{
.enc = enc orelse null,
.fix_enc = true,
.msg = null,
.script = null,
@@ -1831,13 +2175,10 @@ pub fn documentHTMLParse(reader: anytype, enc: ?[:0]const u8) !*DocumentHTML {
.ctx = null,
.daf = null,
};
}
if (enc) |e| params.enc = e;
err = c.dom_hubbub_parser_create(&params, &parser, &doc);
try parserErr(err);
defer c.dom_hubbub_parser_destroy(parser);
fn parseData(parser: *c.dom_hubbub_parser, reader: anytype) !void {
var err: c.hubbub_error = undefined;
var buffer: [1024]u8 = undefined;
var ln = buffer.len;
while (ln > 0) {
@@ -1855,8 +2196,6 @@ pub fn documentHTMLParse(reader: anytype, enc: ?[:0]const u8) !*DocumentHTML {
err = c.dom_hubbub_parser_completed(parser);
try parserErr(err);
return @as(*DocumentHTML, @ptrCast(doc.?));
}
// documentHTMLClose closes the document.

View File

@@ -1,13 +1,36 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const jsruntime = @import("jsruntime");
const generate = @import("generate.zig");
const pretty = @import("pretty");
const parser = @import("netsurf.zig");
const parser = @import("netsurf");
const apiweb = @import("apiweb.zig");
const Window = @import("html/window.zig").Window;
const xhr = @import("xhr/xhr.zig");
const storage = @import("storage/storage.zig");
const url = @import("url/url.zig");
const urlquery = @import("url/query.zig");
const Client = @import("async/Client.zig");
const documentTestExecFn = @import("dom/document.zig").testExecFn;
const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn;
@@ -23,11 +46,19 @@ const DOMTokenListExecFn = @import("dom/token_list.zig").testExecFn;
const NodeListTestExecFn = @import("dom/nodelist.zig").testExecFn;
const AttrTestExecFn = @import("dom/attribute.zig").testExecFn;
const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn;
const ProcessingInstructionTestExecFn = @import("dom/processing_instruction.zig").testExecFn;
const CommentTestExecFn = @import("dom/comment.zig").testExecFn;
const DocumentFragmentTestExecFn = @import("dom/document_fragment.zig").testExecFn;
const EventTestExecFn = @import("events/event.zig").testExecFn;
const XHRTestExecFn = xhr.testExecFn;
const ProgressEventTestExecFn = @import("xhr/progress_event.zig").testExecFn;
const StorageTestExecFn = storage.testExecFn;
const URLTestExecFn = url.testExecFn;
const HTMLElementTestExecFn = @import("html/elements.zig").testExecFn;
const MutationObserverTestExecFn = @import("dom/mutation_observer.zig").testExecFn;
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = @import("user_context.zig").UserContext;
var doc: *parser.DocumentHTML = undefined;
@@ -36,14 +67,15 @@ fn testExecFn(
js_env: *jsruntime.Env,
comptime execFn: jsruntime.ContextExecFn,
) anyerror!void {
try parser.init();
defer parser.deinit();
// start JS env
try js_env.start(alloc);
try js_env.start();
defer js_env.stop();
// alias global as self and window
try js_env.attachObject(try js_env.getGlobal(), "self", null);
try js_env.attachObject(try js_env.getGlobal(), "window", null);
var storageShelf = storage.Shelf.init(alloc);
defer storageShelf.deinit();
// document
const file = try std.fs.cwd().openFile("test.html", .{});
@@ -54,8 +86,21 @@ fn testExecFn(
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
};
// add document object
try js_env.addObject(doc, "document");
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
defer cli.deinit();
try js_env.setUserContext(.{
.document = doc,
.httpClient = &cli,
});
// alias global as self and window
var window = Window.create(null);
window.replaceDocument(doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(window);
// run test
try execFn(alloc, js_env);
@@ -79,10 +124,17 @@ fn testsAllExecFn(
DOMTokenListExecFn,
NodeListTestExecFn,
AttrTestExecFn,
CommentTestExecFn,
DocumentFragmentTestExecFn,
EventTargetTestExecFn,
EventTestExecFn,
XHRTestExecFn,
ProgressEventTestExecFn,
ProcessingInstructionTestExecFn,
StorageTestExecFn,
URLTestExecFn,
HTMLElementTestExecFn,
MutationObserverTestExecFn,
};
inline for (testFns) |testFn| {
@@ -90,20 +142,185 @@ fn testsAllExecFn(
}
}
const usage =
\\usage: test [options]
\\ Run the tests. By default the command will run both js and unit tests.
\\
\\ -h, --help Print this help message and exit.
\\ --browser run only browser js tests
\\ --unit run only js unit tests
\\ --json bench result is formatted in JSON.
\\ only browser tests are benchmarked.
\\
;
// Out list all the ouputs handled by benchmark result and written on stdout.
const Out = enum {
text,
json,
};
// Which tests must be run.
const Run = enum {
all,
browser,
unit,
};
pub fn main() !void {
std.debug.print("\n", .{});
for (builtin.test_functions) |test_fn| {
try test_fn.func();
std.debug.print("{s}\tOK\n", .{test_fn.name});
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const gpa_alloc = gpa.allocator();
var args = try std.process.argsWithAllocator(gpa_alloc);
defer args.deinit();
// ignore the exec name.
_ = args.next().?;
var out: Out = .text;
var run: Run = .all;
while (args.next()) |arg| {
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
try std.io.getStdErr().writer().print(usage, .{});
std.posix.exit(0);
}
if (std.mem.eql(u8, "--json", arg)) {
out = .json;
continue;
}
if (std.mem.eql(u8, "--browser", arg)) {
run = .browser;
continue;
}
if (std.mem.eql(u8, "--unit", arg)) {
run = .unit;
continue;
}
}
// run js tests
if (run == .all or run == .browser) try run_js(out);
// run standard unit tests.
if (run == .all or run == .unit) {
std.debug.print("\n", .{});
for (builtin.test_functions) |test_fn| {
try parser.init();
defer parser.deinit();
try test_fn.func();
std.debug.print("{s}\tOK\n", .{test_fn.name});
}
}
}
test {
const TestAsync = @import("async/test.zig");
std.testing.refAllDecls(TestAsync);
// Run js test and display the output depending of the output parameter.
fn run_js(out: Out) !void {
var bench_alloc = jsruntime.bench_allocator(std.testing.allocator);
const start = try std.time.Instant.now();
// run js exectuion tests
try testJSRuntime(bench_alloc.allocator());
const duration = std.time.Instant.since(try std.time.Instant.now(), start);
const stats = bench_alloc.stats();
// get and display the results
if (out == .json) {
const res = [_]struct {
name: []const u8,
bench: struct {
duration: u64,
alloc_nb: usize,
realloc_nb: usize,
alloc_size: usize,
},
}{
.{ .name = "browser", .bench = .{
.duration = duration,
.alloc_nb = stats.alloc_nb,
.realloc_nb = stats.realloc_nb,
.alloc_size = stats.alloc_size,
} },
// TODO get libdom bench info.
.{ .name = "libdom", .bench = .{
.duration = duration,
.alloc_nb = 0,
.realloc_nb = 0,
.alloc_size = 0,
} },
// TODO get v8 bench info.
.{ .name = "v8", .bench = .{
.duration = duration,
.alloc_nb = 0,
.realloc_nb = 0,
.alloc_size = 0,
} },
// TODO get main bench info.
.{ .name = "main", .bench = .{
.duration = duration,
.alloc_nb = 0,
.realloc_nb = 0,
.alloc_size = 0,
} },
};
try std.json.stringify(res, .{ .whitespace = .indent_2 }, std.io.getStdOut().writer());
return;
}
// display console result by default
const dur = pretty.Measure{ .unit = "ms", .value = duration / ms };
const size = pretty.Measure{ .unit = "kb", .value = stats.alloc_size / kb };
const zerosize = pretty.Measure{ .unit = "kb", .value = 0 };
// benchmark table
const row_shape = .{ []const u8, pretty.Measure, u64, u64, pretty.Measure };
const table = try pretty.GenerateTable(4, row_shape, pretty.TableConf{ .margin_left = " " });
const header = .{ "FUNCTION", "DURATION", "ALLOCATIONS (nb)", "RE-ALLOCATIONS (nb)", "HEAP SIZE" };
var t = table.init("Benchmark browsercore 🚀", header);
try t.addRow(.{ "browser", dur, stats.alloc_nb, stats.realloc_nb, size });
try t.addRow(.{ "libdom", dur, 0, 0, zerosize }); // TODO get libdom bench info.
try t.addRow(.{ "v8", dur, 0, 0, zerosize }); // TODO get v8 bench info.
try t.addRow(.{ "main", dur, 0, 0, zerosize }); // TODO get main bench info.
try t.render(std.io.getStdOut().writer());
}
test "jsruntime" {
const kb = 1024;
const ms = std.time.ns_per_ms;
test {
const asyncTest = @import("async/test.zig");
std.testing.refAllDecls(asyncTest);
const dumpTest = @import("browser/dump.zig");
std.testing.refAllDecls(dumpTest);
const mimeTest = @import("browser/mime.zig");
std.testing.refAllDecls(mimeTest);
const cssTest = @import("css/css.zig");
std.testing.refAllDecls(cssTest);
const cssParserTest = @import("css/parser.zig");
std.testing.refAllDecls(cssParserTest);
const cssMatchTest = @import("css/match_test.zig");
std.testing.refAllDecls(cssMatchTest);
const cssLibdomTest = @import("css/libdom_test.zig");
std.testing.refAllDecls(cssLibdomTest);
const queryTest = @import("url/query.zig");
std.testing.refAllDecls(queryTest);
}
fn testJSRuntime(alloc: std.mem.Allocator) !void {
// generate tests
try generate.tests();
@@ -111,11 +328,10 @@ test "jsruntime" {
const vm = jsruntime.VM.init();
defer vm.deinit();
var bench_alloc = jsruntime.bench_allocator(std.testing.allocator);
var arena_alloc = std.heap.ArenaAllocator.init(bench_alloc.allocator());
var arena_alloc = std.heap.ArenaAllocator.init(alloc);
defer arena_alloc.deinit();
try jsruntime.loadEnv(&arena_alloc, testsAllExecFn);
try jsruntime.loadEnv(&arena_alloc, null, testsAllExecFn);
}
test "DocumentHTMLParseFromStr" {
@@ -138,15 +354,6 @@ test "bug document html parsing #4" {
parser.documentHTMLClose(doc) catch {};
}
const dump = @import("browser/dump.zig");
test "run browser tests" {
// const out = std.io.getStdOut();
const out = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only });
defer out.close();
try dump.HTMLFileTestFn(out);
}
test "Window is a libdom event target" {
var window = Window.create(null);

250
src/storage/storage.zig Normal file
View File

@@ -0,0 +1,250 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const DOMError = @import("netsurf").DOMError;
const log = std.log.scoped(.storage);
pub const Interfaces = generate.Tuple(.{
Bottle,
});
// See https://storage.spec.whatwg.org/#model for storage hierarchy.
// A Shed contains map of Shelves. The key is the document's origin.
// A Shelf contains on default Bucket (it could contain many in the future).
// A Bucket contains a local and a session Bottle.
// A Bottle stores a map of strings and is exposed to the JS.
pub const Shed = struct {
const Map = std.StringHashMapUnmanaged(Shelf);
alloc: std.mem.Allocator,
map: Map,
pub fn init(alloc: std.mem.Allocator) Shed {
return .{
.alloc = alloc,
.map = .{},
};
}
pub fn deinit(self: *Shed) void {
// loop hover each KV and free the memory.
var it = self.map.iterator();
while (it.next()) |entry| {
entry.value_ptr.deinit();
self.alloc.free(entry.key_ptr.*);
}
self.map.deinit(self.alloc);
}
pub fn getOrPut(self: *Shed, origin: []const u8) !*Shelf {
const shelf = self.map.getPtr(origin);
if (shelf) |s| return s;
const oorigin = try self.alloc.dupe(u8, origin);
try self.map.put(self.alloc, oorigin, Shelf.init(self.alloc));
return self.map.getPtr(origin).?;
}
};
pub const Shelf = struct {
bucket: Bucket,
pub fn init(alloc: std.mem.Allocator) Shelf {
return .{ .bucket = Bucket.init(alloc) };
}
pub fn deinit(self: *Shelf) void {
self.bucket.deinit();
}
};
pub const Bucket = struct {
local: Bottle,
session: Bottle,
pub fn init(alloc: std.mem.Allocator) Bucket {
return .{
.local = Bottle.init(alloc),
.session = Bottle.init(alloc),
};
}
pub fn deinit(self: *Bucket) void {
self.local.deinit();
self.session.deinit();
}
};
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface
pub const Bottle = struct {
pub const mem_guarantied = true;
const Map = std.StringHashMapUnmanaged([]const u8);
// allocator is stored. we don't use the JS env allocator b/c the storage
// data could exists longer than a js env lifetime.
alloc: std.mem.Allocator,
map: Map,
pub fn init(alloc: std.mem.Allocator) Bottle {
return .{
.alloc = alloc,
.map = .{},
};
}
// loop hover each KV and free the memory.
fn free(self: *Bottle) void {
var it = self.map.iterator();
while (it.next()) |entry| {
self.alloc.free(entry.key_ptr.*);
self.alloc.free(entry.value_ptr.*);
}
}
pub fn deinit(self: *Bottle) void {
self.free();
self.map.deinit(self.alloc);
}
pub fn get_length(self: *Bottle) u32 {
return @intCast(self.map.count());
}
pub fn _key(self: *Bottle, idx: u32) ?[]const u8 {
if (idx >= self.map.count()) return null;
var it = self.map.valueIterator();
var i: u32 = 0;
while (it.next()) |v| {
if (i == idx) return v.*;
i += 1;
}
unreachable;
}
pub fn _getItem(self: *Bottle, k: []const u8) ?[]const u8 {
return self.map.get(k);
}
pub fn _setItem(self: *Bottle, k: []const u8, v: []const u8) !void {
const old = self.map.get(k);
if (old != null and std.mem.eql(u8, v, old.?)) return;
// owns k and v by copying them.
const kk = try self.alloc.dupe(u8, k);
errdefer self.alloc.free(kk);
const vv = try self.alloc.dupe(u8, v);
errdefer self.alloc.free(vv);
self.map.put(self.alloc, kk, vv) catch |e| {
log.debug("set item: {any}", .{e});
return DOMError.QuotaExceeded;
};
// > Broadcast this with key, oldValue, and value.
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
//
// > The storage event of the Window interface fires when a storage
// > area (localStorage or sessionStorage) has been modified in the
// > context of another document.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
//
// So for now, we won't impement the feature.
}
pub fn _removeItem(self: *Bottle, k: []const u8) !void {
const old = self.map.fetchRemove(k);
if (old == null) return;
// > Broadcast this with key, oldValue, and null.
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
//
// > The storage event of the Window interface fires when a storage
// > area (localStorage or sessionStorage) has been modified in the
// > context of another document.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
//
// So for now, we won't impement the feature.
}
pub fn _clear(self: *Bottle) void {
self.free();
self.map.clearRetainingCapacity();
// > Broadcast this with null, null, and null.
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
//
// > The storage event of the Window interface fires when a storage
// > area (localStorage or sessionStorage) has been modified in the
// > context of another document.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
//
// So for now, we won't impement the feature.
}
};
// Tests
// -----
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var storage = [_]Case{
.{ .src = "localStorage.length", .ex = "0" },
.{ .src = "localStorage.setItem('foo', 'bar')", .ex = "undefined" },
.{ .src = "localStorage.length", .ex = "1" },
.{ .src = "localStorage.getItem('foo')", .ex = "bar" },
.{ .src = "localStorage.removeItem('foo')", .ex = "undefined" },
.{ .src = "localStorage.length", .ex = "0" },
// .{ .src = "localStorage['foo'] = 'bar'", .ex = "undefined" },
// .{ .src = "localStorage['foo']", .ex = "bar" },
// .{ .src = "localStorage.length", .ex = "1" },
.{ .src = "localStorage.clear()", .ex = "undefined" },
.{ .src = "localStorage.length", .ex = "0" },
};
try checkCases(js_env, &storage);
}
test "storage bottle" {
var bottle = Bottle.init(std.testing.allocator);
defer bottle.deinit();
try std.testing.expect(0 == bottle.get_length());
try std.testing.expect(null == bottle._getItem("foo"));
try bottle._setItem("foo", "bar");
try std.testing.expect(std.mem.eql(u8, "bar", bottle._getItem("foo").?));
try bottle._removeItem("foo");
try std.testing.expect(0 == bottle.get_length());
try std.testing.expect(null == bottle._getItem("foo"));
}

106
src/str/parser.zig Normal file
View File

@@ -0,0 +1,106 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// some utils to parser strings.
const std = @import("std");
const testing = std.testing;
pub const Reader = struct {
s: []const u8,
i: usize = 0,
pub fn until(self: *Reader, c: u8) []const u8 {
const ln = self.s.len;
const start = self.i;
while (self.i < ln) {
if (c == self.s[self.i]) return self.s[start..self.i];
self.i += 1;
}
return self.s[start..self.i];
}
pub fn tail(self: *Reader) []const u8 {
if (self.i > self.s.len) return "";
defer self.i = self.s.len;
return self.s[self.i..];
}
pub fn skip(self: *Reader) bool {
if (self.i >= self.s.len) return false;
self.i += 1;
return true;
}
};
test "Reader.skip" {
var r = Reader{ .s = "foo" };
try testing.expect(r.skip());
try testing.expect(r.skip());
try testing.expect(r.skip());
try testing.expect(!r.skip());
try testing.expect(!r.skip());
}
test "Reader.tail" {
var r = Reader{ .s = "foo" };
try testing.expectEqualStrings("foo", r.tail());
try testing.expectEqualStrings("", r.tail());
}
test "Reader.until" {
var r = Reader{ .s = "foo.bar.baz" };
try testing.expectEqualStrings("foo", r.until('.'));
_ = r.skip();
try testing.expectEqualStrings("bar", r.until('.'));
_ = r.skip();
try testing.expectEqualStrings("baz", r.until('.'));
r = Reader{ .s = "foo" };
try testing.expectEqualStrings("foo", r.until('.'));
r = Reader{ .s = "" };
try testing.expectEqualStrings("", r.until('.'));
}
pub fn trim(s: []const u8) []const u8 {
const ln = s.len;
if (ln == 0) {
return "";
}
var start: usize = 0;
while (start < ln) {
if (!std.ascii.isWhitespace(s[start])) break;
start += 1;
}
var end: usize = ln;
while (end > 0) {
if (!std.ascii.isWhitespace(s[end - 1])) break;
end -= 1;
}
return s[start..end];
}
test "trim" {
try testing.expectEqualStrings("", trim(""));
try testing.expectEqualStrings("foo", trim("foo"));
try testing.expectEqualStrings("foo", trim(" \n\tfoo"));
try testing.expectEqualStrings("foo", trim("foo \n\t"));
}

View File

@@ -1,8 +1,27 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const tests = @import("run_tests.zig");
pub const Types = tests.Types;
pub const UserContext = tests.UserContext;
pub fn main() !void {
try tests.main();

283
src/url/query.zig Normal file
View File

@@ -0,0 +1,283 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Reader = @import("../str/parser.zig").Reader;
// Values is a map with string key of string values.
pub const Values = struct {
alloc: std.mem.Allocator,
map: std.StringArrayHashMapUnmanaged(List),
const List = std.ArrayListUnmanaged([]const u8);
pub fn init(alloc: std.mem.Allocator) Values {
return .{
.alloc = alloc,
.map = .{},
};
}
pub fn deinit(self: *Values) void {
var it = self.map.iterator();
while (it.next()) |entry| {
for (entry.value_ptr.items) |v| self.alloc.free(v);
entry.value_ptr.deinit(self.alloc);
self.alloc.free(entry.key_ptr.*);
}
self.map.deinit(self.alloc);
}
// add the key value couple to the values.
// the key and the value are duplicated.
pub fn append(self: *Values, k: []const u8, v: []const u8) !void {
const vv = try self.alloc.dupe(u8, v);
if (self.map.getPtr(k)) |list| {
return try list.append(self.alloc, vv);
}
const kk = try self.alloc.dupe(u8, k);
var list = List{};
try list.append(self.alloc, vv);
try self.map.put(self.alloc, kk, list);
}
// append by taking the ownership of the key and the value
fn appendOwned(self: *Values, k: []const u8, v: []const u8) !void {
if (self.map.getPtr(k)) |list| {
return try list.append(self.alloc, v);
}
var list = List{};
try list.append(self.alloc, v);
try self.map.put(self.alloc, k, list);
}
pub fn get(self: *Values, k: []const u8) [][]const u8 {
if (self.map.get(k)) |list| {
return list.items;
}
return &[_][]const u8{};
}
pub fn first(self: *Values, k: []const u8) []const u8 {
if (self.map.getPtr(k)) |list| {
if (list.items.len == 0) return "";
return list.items[0];
}
return "";
}
pub fn delete(self: *Values, k: []const u8) void {
if (self.map.getPtr(k)) |list| {
list.deinit(self.alloc);
_ = self.map.fetchSwapRemove(k);
}
}
pub fn deleteValue(self: *Values, k: []const u8, v: []const u8) void {
const list = self.map.getPtr(k) orelse return;
for (list.items, 0..) |vv, i| {
if (std.mem.eql(u8, v, vv)) {
_ = list.swapRemove(i);
return;
}
}
}
pub fn count(self: *Values) usize {
return self.map.count();
}
// the caller owned the returned string.
pub fn encode(self: *Values, writer: anytype) !void {
var i: usize = 0;
var it = self.map.iterator();
while (it.next()) |entry| {
defer i += 1;
if (i > 0) try writer.writeByte('&');
if (entry.value_ptr.items.len == 0) {
try escape(writer, entry.key_ptr.*);
continue;
}
const start = i;
for (entry.value_ptr.items) |v| {
defer i += 1;
if (start < i) try writer.writeByte('&');
try escape(writer, entry.key_ptr.*);
if (v.len > 0) try writer.writeByte('=');
try escape(writer, v);
}
}
}
};
fn unhex(c: u8) u8 {
if ('0' <= c and c <= '9') return c - '0';
if ('a' <= c and c <= 'f') return c - 'a' + 10;
if ('A' <= c and c <= 'F') return c - 'A' + 10;
return 0;
}
// unescape decodes a percent encoded string.
// The caller owned the returned string.
pub fn unescape(alloc: std.mem.Allocator, s: []const u8) ![]const u8 {
var buf: std.ArrayListUnmanaged(u8) = .{};
defer buf.deinit(alloc);
var i: usize = 0;
while (i < s.len) {
defer i += 1;
switch (s[i]) {
'%' => {
if (i + 2 > s.len) return error.EscapeError;
if (!std.ascii.isHex(s[i + 1])) return error.EscapeError;
if (!std.ascii.isHex(s[i + 2])) return error.EscapeError;
try buf.append(alloc, unhex(s[i + 1]) << 4 | unhex(s[i + 2]));
i += 2;
},
'+' => try buf.append(alloc, ' '), // TODO should we decode or keep as it?
else => try buf.append(alloc, s[i]),
}
}
return try buf.toOwnedSlice(alloc);
}
test "unescape" {
var v: []const u8 = undefined;
const alloc = std.testing.allocator;
v = try unescape(alloc, "%7E");
try std.testing.expect(std.mem.eql(u8, "~", v));
alloc.free(v);
}
pub fn escape(writer: anytype, raw: []const u8) !void {
var start: usize = 0;
for (raw, 0..) |char, index| {
if ('a' <= char and char <= 'z' or 'A' <= char and char <= 'Z' or '0' <= char and char <= '9') {
continue;
}
try writer.print("{s}%{X:0>2}", .{ raw[start..index], char });
start = index + 1;
}
try writer.writeAll(raw[start..]);
}
// Parse the given query.
pub fn parseQuery(alloc: std.mem.Allocator, s: []const u8) !Values {
var values = Values.init(alloc);
errdefer values.deinit();
const ln = s.len;
if (ln == 0) return values;
var r = Reader{ .s = s };
while (true) {
const param = r.until('&');
if (param.len == 0) break;
var rr = Reader{ .s = param };
const k = rr.until('=');
if (k.len == 0) continue;
_ = rr.skip();
const v = rr.tail();
// decode k and v
const kk = try unescape(alloc, k);
const vv = try unescape(alloc, v);
try values.appendOwned(kk, vv);
if (!r.skip()) break;
}
return values;
}
test "parse empty query" {
var values = try parseQuery(std.testing.allocator, "");
defer values.deinit();
try std.testing.expect(values.count() == 0);
}
test "parse empty query &" {
var values = try parseQuery(std.testing.allocator, "&");
defer values.deinit();
try std.testing.expect(values.count() == 0);
}
test "parse query" {
var values = try parseQuery(std.testing.allocator, "a=b&b=c");
defer values.deinit();
try std.testing.expect(values.count() == 2);
try std.testing.expect(values.get("a").len == 1);
try std.testing.expect(std.mem.eql(u8, values.get("a")[0], "b"));
try std.testing.expect(std.mem.eql(u8, values.first("a"), "b"));
try std.testing.expect(values.get("b").len == 1);
try std.testing.expect(std.mem.eql(u8, values.get("b")[0], "c"));
try std.testing.expect(std.mem.eql(u8, values.first("b"), "c"));
}
test "parse query no value" {
var values = try parseQuery(std.testing.allocator, "a");
defer values.deinit();
try std.testing.expect(values.count() == 1);
try std.testing.expect(std.mem.eql(u8, values.first("a"), ""));
}
test "parse query dup" {
var values = try parseQuery(std.testing.allocator, "a=b&a=c");
defer values.deinit();
try std.testing.expect(values.count() == 1);
try std.testing.expect(std.mem.eql(u8, values.first("a"), "b"));
try std.testing.expect(values.get("a").len == 2);
}
test "encode query" {
var values = try parseQuery(std.testing.allocator, "a=b&b=c");
defer values.deinit();
try values.append("a", "~");
var buf: std.ArrayListUnmanaged(u8) = .{};
defer buf.deinit(std.testing.allocator);
try values.encode(buf.writer(std.testing.allocator));
try std.testing.expect(std.mem.eql(u8, buf.items, "a=b&a=%7E&b=c"));
}

318
src/url/url.zig Normal file
View File

@@ -0,0 +1,318 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const query = @import("query.zig");
pub const Interfaces = generate.Tuple(.{
URL,
URLSearchParams,
});
// https://url.spec.whatwg.org/#url
//
// TODO we could avoid many of these getter string allocation in two differents
// way:
//
// 1. We can eventually get the slice of scheme *with* the following char in
// the underlying string. But I don't know if it's possible and how to do that.
// I mean, if the rawuri contains `https://foo.bar`, uri.scheme is a slice
// containing only `https`. I want `https:` so, in theory, I don't need to
// allocate data, I should be able to retrieve the scheme + the following `:`
// from rawuri.
//
// 2. The other way would bu to copy the `std.Uri` code to ahve a dedicated
// parser including the characters we want for the web API.
pub const URL = struct {
rawuri: []const u8,
uri: std.Uri,
search_params: URLSearchParams,
pub const mem_guarantied = true;
pub fn constructor(alloc: std.mem.Allocator, url: []const u8, base: ?[]const u8) !URL {
const raw = try std.mem.concat(alloc, u8, &[_][]const u8{ url, base orelse "" });
errdefer alloc.free(raw);
const uri = std.Uri.parse(raw) catch {
return error.TypeError;
};
return .{
.rawuri = raw,
.uri = uri,
.search_params = try URLSearchParams.constructor(
alloc,
uriComponentNullStr(uri.query),
),
};
}
pub fn deinit(self: *URL, alloc: std.mem.Allocator) void {
self.search_params.deinit(alloc);
alloc.free(self.rawuri);
}
// the caller must free the returned string.
// TODO return a disposable string
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
pub fn get_origin(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try self.uri.writeToStream(.{
.scheme = true,
.authentication = false,
.authority = true,
.path = false,
.query = false,
.fragment = false,
}, buf.writer());
return try buf.toOwnedSlice();
}
// get_href returns the URL by writing all its components.
// The query is replaced by a dump of search params.
//
// the caller must free the returned string.
// TODO return a disposable string
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
pub fn get_href(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
// retrieve the query search from search_params.
const cur = self.uri.query;
defer self.uri.query = cur;
var q = std.ArrayList(u8).init(alloc);
defer q.deinit();
try self.search_params.values.encode(q.writer());
self.uri.query = .{ .percent_encoded = q.items };
return try self.format(alloc);
}
// format the url with all its components.
pub fn format(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try self.uri.writeToStream(.{
.scheme = true,
.authentication = true,
.authority = true,
.path = uriComponentNullStr(self.uri.path).len > 0,
.query = uriComponentNullStr(self.uri.query).len > 0,
.fragment = uriComponentNullStr(self.uri.fragment).len > 0,
}, buf.writer());
return try buf.toOwnedSlice();
}
// the caller must free the returned string.
// TODO return a disposable string
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
pub fn get_protocol(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
return try std.mem.concat(alloc, u8, &[_][]const u8{ self.uri.scheme, ":" });
}
pub fn get_username(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.user);
}
pub fn get_password(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.password);
}
// the caller must free the returned string.
// TODO return a disposable string
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
pub fn get_host(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try self.uri.writeToStream(.{
.scheme = false,
.authentication = false,
.authority = true,
.path = false,
.query = false,
.fragment = false,
}, buf.writer());
return try buf.toOwnedSlice();
}
pub fn get_hostname(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.host);
}
// the caller must free the returned string.
// TODO return a disposable string
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
pub fn get_port(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
if (self.uri.port == null) return try alloc.dupe(u8, "");
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try std.fmt.formatInt(self.uri.port.?, 10, .lower, .{}, buf.writer());
return try buf.toOwnedSlice();
}
pub fn get_pathname(self: *URL) []const u8 {
if (uriComponentStr(self.uri.path).len == 0) return "/";
return uriComponentStr(self.uri.path);
}
// the caller must free the returned string.
// TODO return a disposable string
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
pub fn get_search(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
if (self.search_params.get_size() == 0) return try alloc.dupe(u8, "");
var buf: std.ArrayListUnmanaged(u8) = .{};
defer buf.deinit(alloc);
try buf.append(alloc, '?');
try self.search_params.values.encode(buf.writer(alloc));
return buf.toOwnedSlice(alloc);
}
// the caller must free the returned string.
// TODO return a disposable string
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
pub fn get_hash(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
if (self.uri.fragment == null) return try alloc.dupe(u8, "");
return try std.mem.concat(alloc, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) });
}
pub fn get_searchParams(self: *URL) *URLSearchParams {
return &self.search_params;
}
pub fn _toJSON(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
return try self.get_href(alloc);
}
};
// uriComponentNullStr converts an optional std.Uri.Component to string value.
// The string value can be undecoded.
fn uriComponentNullStr(c: ?std.Uri.Component) []const u8 {
if (c == null) return "";
return uriComponentStr(c.?);
}
fn uriComponentStr(c: std.Uri.Component) []const u8 {
return switch (c) {
.raw => |v| v,
.percent_encoded => |v| v,
};
}
// https://url.spec.whatwg.org/#interface-urlsearchparams
// TODO array like
pub const URLSearchParams = struct {
values: query.Values,
pub const mem_guarantied = true;
pub fn constructor(alloc: std.mem.Allocator, init: ?[]const u8) !URLSearchParams {
return .{
.values = try query.parseQuery(alloc, init orelse ""),
};
}
pub fn deinit(self: *URLSearchParams, _: std.mem.Allocator) void {
self.values.deinit();
}
pub fn get_size(self: *URLSearchParams) u32 {
return @intCast(self.values.count());
}
pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8) !void {
try self.values.append(name, value);
}
pub fn _delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8) !void {
if (value) |v| return self.values.deleteValue(name, v);
self.values.delete(name);
}
pub fn _get(self: *URLSearchParams, name: []const u8) ?[]const u8 {
return self.values.first(name);
}
// TODO return generates an error: caught unexpected error 'TypeLookup'
// pub fn _getAll(self: *URLSearchParams, name: []const u8) [][]const u8 {
// try self.values.get(name);
// }
// TODO
pub fn _sort(_: *URLSearchParams) void {}
};
// Tests
// -----
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var url = [_]Case{
.{ .src = "var url = new URL('https://foo.bar/path?query#fragment')", .ex = "undefined" },
.{ .src = "url.origin", .ex = "https://foo.bar" },
.{ .src = "url.href", .ex = "https://foo.bar/path?query#fragment" },
.{ .src = "url.protocol", .ex = "https:" },
.{ .src = "url.username", .ex = "" },
.{ .src = "url.password", .ex = "" },
.{ .src = "url.host", .ex = "foo.bar" },
.{ .src = "url.hostname", .ex = "foo.bar" },
.{ .src = "url.port", .ex = "" },
.{ .src = "url.pathname", .ex = "/path" },
.{ .src = "url.search", .ex = "?query" },
.{ .src = "url.hash", .ex = "#fragment" },
.{ .src = "url.searchParams.get('query')", .ex = "" },
};
try checkCases(js_env, &url);
var qs = [_]Case{
.{ .src = "var url = new URL('https://foo.bar/path?a=~&b=%7E#fragment')", .ex = "undefined" },
.{ .src = "url.searchParams.get('a')", .ex = "~" },
.{ .src = "url.searchParams.get('b')", .ex = "~" },
.{ .src = "url.searchParams.append('c', 'foo')", .ex = "undefined" },
.{ .src = "url.searchParams.get('c')", .ex = "foo" },
.{ .src = "url.searchParams.size", .ex = "3" },
// search is dynamic
.{ .src = "url.search", .ex = "?a=%7E&b=%7E&c=foo" },
// href is dynamic
.{ .src = "url.href", .ex = "https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment" },
.{ .src = "url.searchParams.delete('c', 'foo')", .ex = "undefined" },
.{ .src = "url.searchParams.get('c')", .ex = "" },
.{ .src = "url.searchParams.delete('a')", .ex = "undefined" },
.{ .src = "url.searchParams.get('a')", .ex = "" },
};
try checkCases(js_env, &qs);
}

8
src/user_context.zig Normal file
View File

@@ -0,0 +1,8 @@
const std = @import("std");
const parser = @import("netsurf");
const Client = @import("async/Client.zig");
pub const UserContext = struct {
document: *parser.DocumentHTML,
httpClient: *Client,
};

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const fspath = std.fs.path;

View File

@@ -1,91 +1,109 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const fspath = std.fs.path;
const FileLoader = @import("fileloader.zig").FileLoader;
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Loop = jsruntime.Loop;
const Env = jsruntime.Env;
const Window = @import("../html/window.zig").Window;
const storage = @import("../storage/storage.zig");
const Types = @import("../main_wpt.zig").Types;
const UserContext = @import("../main_wpt.zig").UserContext;
const Client = @import("../async/Client.zig");
// runWPT parses the given HTML file, starts a js env and run the first script
// tags containing javascript sources.
// It loads first the js libs files.
pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const u8, loader: *FileLoader) !jsruntime.JSResult {
pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const u8, loader: *FileLoader) !Res {
const alloc = arena.allocator();
try parser.init();
defer parser.deinit();
// document
const file = try std.fs.cwd().openFile(f, .{});
defer file.close();
const html_doc = try parser.documentHTMLParse(file.reader(), "UTF-8");
const doc = parser.documentHTMLToDocument(html_doc);
const dirname = fspath.dirname(f[dir.len..]) orelse unreachable;
// create JS env
var loop = try Loop.init(alloc);
defer loop.deinit();
var js_env = try Env.init(alloc, &loop);
var cli = Client{ .allocator = alloc, .loop = &loop };
defer cli.deinit();
var js_env = try Env.init(alloc, &loop, UserContext{
.document = html_doc,
.httpClient = &cli,
});
defer js_env.deinit();
var storageShelf = storage.Shelf.init(alloc);
defer storageShelf.deinit();
// load user-defined types in JS env
var js_types: [Types.len]usize = undefined;
try js_env.load(&js_types);
// start JS env
try js_env.start(alloc);
try js_env.start();
defer js_env.stop();
// display console logs
defer {
var res = evalJS(js_env, alloc, "console.join('\\n');", "console") catch unreachable;
const res = evalJS(js_env, alloc, "console.join('\\n');", "console") catch unreachable;
defer res.deinit(alloc);
if (res.result.len > 0) {
std.debug.print("-- CONSOLE LOG\n{s}\n--\n", .{res.result});
if (res.msg != null and res.msg.?.len > 0) {
std.debug.print("-- CONSOLE LOG\n{s}\n--\n", .{res.msg.?});
}
}
// setup global env vars.
try js_env.attachObject(try js_env.getGlobal(), "self", null);
try js_env.attachObject(try js_env.getGlobal(), "window", null);
try js_env.addObject(html_doc, "document");
// thanks to the arena, we don't need to deinit res.
var res: jsruntime.JSResult = undefined;
var window = Window.create(null);
window.replaceDocument(html_doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(&window);
const init =
\\window.listeners = [];
\\window.document = document;
\\window.parent = window;
\\window.addEventListener = function (type, listener, options) {
\\ window.listeners.push({type: type, listener: listener, options: options});
\\};
\\window.dispatchEvent = function (event) {
\\ len = window.listeners.length;
\\ for (var i = 0; i < len; i++) {
\\ if (window.listeners[i].type == event.target) {
\\ window.listeners[i].listener(event);
\\ }
\\ }
\\ return true;
\\};
\\window.removeEventListener = function () {};
\\
\\console = [];
\\console.log = function () {
\\ console.push(...arguments);
\\};
\\console.debug = function () {
\\ console.push("debug", ...arguments);
\\};
;
res = try evalJS(js_env, alloc, init, "init");
if (!res.success) {
return res;
}
var res = try evalJS(js_env, alloc, init, "init");
if (!res.ok) return res;
res.deinit(alloc);
// loop hover the scripts.
const doc = parser.documentHTMLToDocument(html_doc);
const scripts = try parser.documentGetElementsByTagName(doc, "script");
const slen = try parser.nodeListLength(scripts);
for (0..slen) |i| {
@@ -100,41 +118,74 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
}
res = try evalJS(js_env, alloc, try loader.get(path), src);
if (!res.success) {
return res;
}
if (!res.ok) return res;
res.deinit(alloc);
}
// If the script as a source text, execute it.
const src = try parser.nodeTextContent(s) orelse continue;
res = try evalJS(js_env, alloc, src, "");
// return the first failure.
if (!res.success) {
return res;
}
if (!res.ok) return res;
res.deinit(alloc);
}
// Mark tests as ready to run.
res = try evalJS(js_env, alloc, "window.dispatchEvent({target: 'load'});", "ready");
if (!res.success) {
return res;
}
const loadevt = try parser.eventCreate();
defer parser.eventDestroy(loadevt);
try parser.eventInit(loadevt, "load", .{});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, &window),
loadevt,
);
// wait for all async executions
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(js_env);
defer try_catch.deinit();
js_env.wait() catch {
return .{
.ok = false,
.msg = try try_catch.err(alloc, js_env),
};
};
// Check the final test status.
res = try evalJS(js_env, alloc, "report.status;", "teststatus");
if (!res.success) {
return res;
}
if (!res.ok) return res;
res.deinit(alloc);
// return the detailed result.
return try evalJS(js_env, alloc, "report.log", "teststatus");
}
fn evalJS(env: jsruntime.Env, alloc: std.mem.Allocator, script: []const u8, name: ?[]const u8) !jsruntime.JSResult {
var res = jsruntime.JSResult{};
try env.run(alloc, script, name, &res, null);
return res;
pub const Res = struct {
ok: bool,
msg: ?[]const u8,
pub fn deinit(res: Res, alloc: std.mem.Allocator) void {
if (res.msg) |msg| {
alloc.free(msg);
}
}
};
fn evalJS(env: jsruntime.Env, alloc: std.mem.Allocator, script: []const u8, name: ?[]const u8) !Res {
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(env);
defer try_catch.deinit();
const v = env.exec(script, name) catch {
return .{
.ok = false,
.msg = try try_catch.err(alloc, env),
};
};
return .{
.ok = true,
.msg = try v.toString(alloc, env),
};
}
// browse the path to find the tests list.

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const testing = std.testing;
@@ -49,28 +67,22 @@ pub const Suite = struct {
pass: bool,
name: []const u8,
message: ?[]const u8,
stack: ?[]const u8,
cases: ?[]Case,
// caller owns the wpt.Suite.
// owner must call deinit().
pub fn init(alloc: std.mem.Allocator, name: []const u8, pass: bool, res: []const u8, stack: ?[]const u8) !Suite {
pub fn init(alloc: std.mem.Allocator, name: []const u8, pass: bool, res: []const u8) !Suite {
var suite = Suite{
.alloc = alloc,
.pass = false,
.name = try alloc.dupe(u8, name),
.message = null,
.stack = null,
.cases = null,
};
// handle JS error.
if (!pass) {
suite.message = try alloc.dupe(u8, res);
if (stack) |st| {
suite.stack = try alloc.dupe(u8, st);
}
return suite;
}
@@ -119,6 +131,16 @@ pub const Suite = struct {
try cases.append(case);
}
if (cases.items.len == 0) {
// no test case, create a failed one.
suite.pass = false;
try cases.append(.{
.pass = false,
.name = "no test case",
.message = "no test case",
});
}
suite.cases = try cases.toOwnedSlice();
return suite;
@@ -127,10 +149,6 @@ pub const Suite = struct {
pub fn deinit(self: Suite) void {
self.alloc.free(self.name);
if (self.stack) |stack| {
self.alloc.free(stack);
}
if (self.message) |res| {
self.alloc.free(res);
}
@@ -147,9 +165,6 @@ pub const Suite = struct {
if (self.message) |v| {
return v;
}
if (self.stack) |v| {
return v;
}
return "";
}
};
@@ -171,7 +186,7 @@ test "success test case" {
,
};
const suite = Suite.init(alloc, "foo", res.pass, res.result, null) catch unreachable; // TODO
const suite = Suite.init(alloc, "foo", res.pass, res.result) catch unreachable; // TODO
defer suite.deinit();
try testing.expect(suite.pass == true);
@@ -198,7 +213,7 @@ test "failed test case" {
,
};
const suite = Suite.init(alloc, "foo", res.pass, res.result, null) catch unreachable; // TODO
const suite = Suite.init(alloc, "foo", res.pass, res.result) catch unreachable; // TODO
defer suite.deinit();
try testing.expect(suite.pass == false);
@@ -223,7 +238,7 @@ test "invalid result" {
,
};
const suite = Suite.init(alloc, "foo", res.pass, res.result, null) catch unreachable; // TODO
const suite = Suite.init(alloc, "foo", res.pass, res.result) catch unreachable; // TODO
defer suite.deinit();
try testing.expect(suite.pass == false);
@@ -238,7 +253,7 @@ test "invalid result" {
,
};
const suite2 = Suite.init(alloc, "foo", res2.pass, res2.result, null) catch unreachable; // TODO
const suite2 = Suite.init(alloc, "foo", res2.pass, res2.result) catch unreachable; // TODO
defer suite2.deinit();
try testing.expect(suite2.pass == false);

View File

@@ -1,11 +1,30 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
const Callback = jsruntime.Callback;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler;
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const log = std.log.scoped(.xhr);
@@ -23,8 +42,20 @@ pub const XMLHttpRequestEventTarget = struct {
ontimeout_cbk: ?Callback = null,
onloadend_cbk: ?Callback = null,
fn register(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
try parser.eventTargetAddEventListener(@as(*parser.EventTarget, @ptrCast(self)), alloc, typ, cbk, false);
fn register(
self: *XMLHttpRequestEventTarget,
alloc: std.mem.Allocator,
typ: []const u8,
cbk: Callback,
) !void {
try parser.eventTargetAddEventListener(
@as(*parser.EventTarget, @ptrCast(self)),
alloc,
typ,
EventHandler,
.{ .cbk = cbk },
false,
);
}
fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
const et = @as(*parser.EventTarget, @ptrCast(self));

View File

@@ -1,10 +1,28 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const Event = @import("../events/event.zig").Event;
const DOMException = @import("../dom/exceptions.zig").DOMException;

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const jsruntime = @import("jsruntime");
@@ -5,7 +23,7 @@ const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const DOMError = @import("../netsurf.zig").DOMError;
const DOMError = @import("netsurf").DOMError;
const DOMException = @import("../dom/exceptions.zig").DOMException;
const ProgressEvent = @import("progress_event.zig").ProgressEvent;
@@ -17,7 +35,9 @@ const Loop = jsruntime.Loop;
const YieldImpl = Loop.Yield(XMLHttpRequest);
const Client = @import("../async/Client.zig");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf");
const UserContext = @import("../user_context.zig").UserContext;
const log = std.log.scoped(.xhr);
@@ -75,6 +95,53 @@ pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) {
};
pub const XMLHttpRequest = struct {
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
alloc: std.mem.Allocator,
cli: *Client,
impl: YieldImpl,
priv_state: PrivState = .new,
req: ?Client.Request = null,
method: std.http.Method,
state: u16,
url: ?[]const u8,
uri: std.Uri,
// request headers
headers: Headers,
sync: bool = true,
err: ?anyerror = null,
// TODO uncomment this field causes casting issue with
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
// not sure. see
// https://lightpanda.slack.com/archives/C05TRU6RBM1/p1707819010681019
// upload: ?XMLHttpRequestUpload = null,
// TODO uncomment this field causes casting issue with
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
// not sure. see
// https://lightpanda.slack.com/archives/C05TRU6RBM1/p1707819010681019
// timeout: u32 = 0,
withCredentials: bool = false,
// TODO: response readonly attribute any response;
response_bytes: ?[]const u8 = null,
response_type: ResponseType = .Empty,
response_headers: Headers,
// used by zig client to parse response headers.
// use 16KB for headers buffer size.
response_header_buffer: [1024 * 16]u8 = undefined,
response_status: u10 = 0,
response_override_mime_type: ?[]const u8 = null,
response_mime: Mime = undefined,
response_obj: ?ResponseObj = null,
send_flag: bool = false,
payload: ?[]const u8 = null,
pub const prototype = *XMLHttpRequestEventTarget;
pub const mem_guarantied = true;
@@ -94,10 +161,92 @@ pub const XMLHttpRequest = struct {
JSON,
};
// TODO use std.json.Value instead, but it causes comptime error.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/204
// const JSONValue = std.json.Value;
const JSONValue = u8;
const JSONValue = std.json.Value;
const Headers = struct {
alloc: std.mem.Allocator,
list: List,
const List = std.ArrayListUnmanaged(std.http.Header);
fn init(alloc: std.mem.Allocator) Headers {
return .{
.alloc = alloc,
.list = List{},
};
}
fn deinit(self: *Headers) void {
self.free();
self.list.deinit(self.alloc);
}
fn append(self: *Headers, k: []const u8, v: []const u8) !void {
// duplicate strings
const kk = try self.alloc.dupe(u8, k);
const vv = try self.alloc.dupe(u8, v);
try self.list.append(self.alloc, .{ .name = kk, .value = vv });
}
// free all strings allocated.
fn free(self: *Headers) void {
for (self.list.items) |h| {
self.alloc.free(h.name);
self.alloc.free(h.value);
}
}
fn clearAndFree(self: *Headers) void {
self.free();
self.list.clearAndFree(self.alloc);
}
fn has(self: Headers, k: []const u8) bool {
for (self.list.items) |h| {
if (std.ascii.eqlIgnoreCase(k, h.name)) {
return true;
}
}
return false;
}
fn getFirstValue(self: Headers, k: []const u8) ?[]const u8 {
for (self.list.items) |h| {
if (std.ascii.eqlIgnoreCase(k, h.name)) {
return h.value;
}
}
return null;
}
// replace any existing header with the same key
fn set(self: *Headers, k: []const u8, v: []const u8) !void {
for (self.list.items, 0..) |h, i| {
if (std.ascii.eqlIgnoreCase(k, h.name)) {
const hh = self.list.swapRemove(i);
self.alloc.free(hh.name);
self.alloc.free(hh.value);
}
}
self.append(k, v);
}
// TODO
fn sort(_: *Headers) void {}
fn all(self: Headers) []std.http.Header {
return self.list.items;
}
fn load(self: *Headers, it: *std.http.HeaderIterator) !void {
while (true) {
const h = it.next() orelse break;
_ = try self.append(h.name, h.value);
}
}
};
const Response = union(ResponseType) {
Empty: void,
@@ -132,54 +281,19 @@ pub const XMLHttpRequest = struct {
const PrivState = enum { new, open, send, write, finish, wait, done };
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
alloc: std.mem.Allocator,
cli: Client,
impl: YieldImpl,
const min_delay: u64 = 50000000; // 50ms
priv_state: PrivState = .new,
req: ?Client.Request = null,
method: std.http.Method,
state: u16,
url: ?[]const u8,
uri: std.Uri,
headers: std.http.Headers,
sync: bool = true,
err: ?anyerror = null,
// TODO uncomment this field causes casting issue with
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
// not sure. see
// https://lightpanda.slack.com/archives/C05TRU6RBM1/p1707819010681019
// upload: ?XMLHttpRequestUpload = null,
timeout: u32 = 0,
withCredentials: bool = false,
// TODO: response readonly attribute any response;
response_bytes: ?[]const u8 = null,
response_type: ResponseType = .Empty,
response_headers: std.http.Headers,
response_status: u10 = 0,
response_override_mime_type: ?[]const u8 = null,
response_mime: Mime = undefined,
response_obj: ?ResponseObj = null,
send_flag: bool = false,
payload: ?[]const u8 = null,
pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest {
pub fn constructor(alloc: std.mem.Allocator, loop: *Loop, userctx: UserContext) !XMLHttpRequest {
return .{
.alloc = alloc,
.headers = .{ .allocator = alloc, .owned = true },
.response_headers = .{ .allocator = alloc, .owned = true },
.headers = Headers.init(alloc),
.response_headers = Headers.init(alloc),
.impl = YieldImpl.init(loop),
.method = undefined,
.url = null,
.uri = undefined,
.state = UNSENT,
// TODO retrieve the HTTP client globally to reuse existing connections.
.cli = .{ .allocator = alloc, .loop = loop },
.cli = userctx.httpClient,
};
}
@@ -218,25 +332,22 @@ pub const XMLHttpRequest = struct {
self.response_headers.deinit();
self.proto.deinit(alloc);
// TODO the client must be shared between requests.
self.cli.deinit();
}
pub fn get_readyState(self: *XMLHttpRequest) u16 {
return self.state;
}
pub fn get_timeout(self: *XMLHttpRequest) u32 {
return self.timeout;
pub fn get_timeout(_: *XMLHttpRequest) u32 {
return 0;
}
pub fn set_timeout(self: *XMLHttpRequest, timeout: u32) !void {
// TODO, the value is ignored for now.
pub fn set_timeout(_: *XMLHttpRequest, _: u32) !void {
// TODO If the current global object is a Window object and thiss
// synchronous flag is set, then throw an "InvalidAccessError"
// DOMException.
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-timeout
self.timeout = timeout;
}
pub fn get_withCredentials(self: *XMLHttpRequest) bool {
@@ -301,6 +412,7 @@ pub const XMLHttpRequest = struct {
typ: []const u8,
opts: ProgressEvent.EventInit,
) void {
log.debug("dispatch progress event: {s}", .{typ});
var evt = ProgressEvent.constructor(typ, .{
// https://xhr.spec.whatwg.org/#firing-events-using-the-progressevent-interface
.lengthComputable = opts.total > 0,
@@ -369,7 +481,7 @@ pub const XMLHttpRequest = struct {
const body_init = XMLHttpRequestBodyInit{ .String = body.? };
// keep the user content type from request headers.
if (self.headers.getFirstEntry("Content-Type") == null) {
if (self.headers.has("Content-Type")) {
// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
try self.headers.append("Content-Type", try body_init.contentType());
}
@@ -395,14 +507,17 @@ pub const XMLHttpRequest = struct {
switch (self.priv_state) {
.new => {
self.priv_state = .open;
self.req = self.cli.open(self.method, self.uri, self.headers, .{}) catch |e| return self.onErr(e);
self.req = self.cli.open(self.method, self.uri, .{
.server_header_buffer = &self.response_header_buffer,
.extra_headers = self.headers.all(),
}) catch |e| return self.onErr(e);
},
.open => {
// prepare payload transfert.
if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len };
self.priv_state = .send;
self.req.?.send(.{}) catch |e| return self.onErr(e);
self.req.?.send() catch |e| return self.onErr(e);
},
.send => {
if (self.payload) |payload| {
@@ -425,7 +540,8 @@ pub const XMLHttpRequest = struct {
log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status });
self.priv_state = .done;
self.response_headers = self.req.?.response.headers.clone(self.response_headers.allocator) catch |e| return self.onErr(e);
var it = self.req.?.response.iterateHeaders();
self.response_headers.load(&it) catch |e| return self.onErr(e);
// extract a mime type from headers.
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml";
@@ -450,6 +566,7 @@ pub const XMLHttpRequest = struct {
const reader = self.req.?.reader();
var buffer: [1024]u8 = undefined;
var ln = buffer.len;
var prev_dispatch: ?std.time.Instant = null;
while (ln > 0) {
ln = reader.read(&buffer) catch |e| {
buf.deinit(self.alloc);
@@ -461,7 +578,13 @@ pub const XMLHttpRequest = struct {
};
loaded = loaded + ln;
// TODO dispatch only if 50ms have passed.
// Dispatch only if 50ms have passed.
const now = std.time.Instant.now() catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
if (prev_dispatch != null and now.since(prev_dispatch.?) < min_delay) continue;
defer prev_dispatch = now;
self.state = LOADING;
self.dispatchEvt("readystatechange");
@@ -803,31 +926,43 @@ pub fn testExecFn(
};
try checkCases(js_env, &document);
// var json = [_]Case{
// .{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
// .{ .src = "req3.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
// .{ .src = "req3.responseType = 'json'", .ex = "json" },
// .{ .src = "req3.send()", .ex = "undefined" },
// // Each case executed waits for all loop callaback calls.
// // So the url has been retrieved.
// .{ .src = "req3.status", .ex = "200" },
// .{ .src = "req3.statusText", .ex = "OK" },
// .{ .src = "req3.response", .ex = "" },
// };
// try checkCases(js_env, &json);
//
var post = [_]Case{
var json = [_]Case{
.{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req3.open('POST', 'http://httpbin.io/post')", .ex = "undefined" },
.{ .src = "req3.send('foo')", .ex = "undefined" },
.{ .src = "req3.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
.{ .src = "req3.responseType = 'json'", .ex = "json" },
.{ .src = "req3.send()", .ex = "undefined" },
// Each case executed waits for all loop callaback calls.
// So the url has been retrieved.
.{ .src = "req3.status", .ex = "200" },
.{ .src = "req3.statusText", .ex = "OK" },
.{ .src = "req3.responseText.length > 64", .ex = "true" },
.{ .src = "req3.response.slideshow.author", .ex = "Yours Truly" },
};
try checkCases(js_env, &json);
var post = [_]Case{
.{ .src = "const req4 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req4.open('POST', 'http://httpbin.io/post')", .ex = "undefined" },
.{ .src = "req4.send('foo')", .ex = "undefined" },
// Each case executed waits for all loop callaback calls.
// So the url has been retrieved.
.{ .src = "req4.status", .ex = "200" },
.{ .src = "req4.statusText", .ex = "OK" },
.{ .src = "req4.responseText.length > 64", .ex = "true" },
};
try checkCases(js_env, &post);
var cbk = [_]Case{
.{ .src = "const req5 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req5.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
.{ .src = "var status = 0; req5.onload = function () { status = this.status };", .ex = "function () { status = this.status }" },
.{ .src = "req5.send()", .ex = "undefined" },
// Each case executed waits for all loop callaback calls.
// So the url has been retrieved.
.{ .src = "status", .ex = "200" },
};
try checkCases(js_env, &cbk);
}

Submodule vendor/jsruntime-lib deleted from 2d7b816f48

1
vendor/lexbor-src vendored

Submodule vendor/lexbor-src deleted from b2c0a617f3

1
vendor/mimalloc vendored Submodule

Submodule vendor/mimalloc added at 8f7d1e9a41

1
vendor/tls.zig vendored Submodule

Submodule vendor/tls.zig added at 0ea9e6d769

1
vendor/zig-js-runtime vendored Submodule

Submodule vendor/zig-js-runtime added at f2a6e94a18