575 Commits

Author SHA1 Message Date
nikneym
c9cd0fad40 add custom mouse event test 2025-09-10 11:27:41 +03:00
nikneym
8f96ea457f remove unknown mouse event warning 2025-09-10 11:27:20 +03:00
nikneym
9c83b268b9 persist the detail if provided 2025-09-10 09:49:31 +03:00
nikneym
10fc056184 createEvent should increase tag count by 1 2025-09-09 21:56:10 +03:00
nikneym
7517937155 add createEvent tests 2025-09-09 21:45:09 +03:00
nikneym
a86fa8cc37 add support for CustomEvent#initCustomEvent 2025-09-09 21:44:51 +03:00
nikneym
e1c765e78a support CustomEvent in createEvent 2025-09-09 21:44:09 +03:00
Karl Seguin
2ed8a1c0ec Merge pull request #1030 from lightpanda-io/update_libdom
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
this was already updated, but subsequent PR (by me) accidentally reve…
2025-09-09 19:53:54 +08:00
Pierre Tachoire
389dff7031 Merge pull request #1029 from lightpanda-io/remove_telemetry_debug_output
Remove a std.debug.print
2025-09-09 13:48:20 +02:00
Karl Seguin
123d69e595 this was already updated, but subsequent PR (by me) accidentally reverted it 2025-09-09 19:44:54 +08:00
Karl Seguin
4ab7fe26fc Merge pull request #1025 from lightpanda-io/migrate_some_tests_7
migrate more tests to htmlRunner
2025-09-09 19:41:56 +08:00
Karl Seguin
7136851893 Remove a std.debug.print
Probably added in the Zig 0.15 migration. Sorry.
2025-09-09 19:19:36 +08:00
Pierre Tachoire
85f60cbc7b Merge pull request #1027 from lightpanda-io/libcurl-readme
add libcurl in the readme
2025-09-09 11:24:55 +02:00
Pierre Tachoire
9c35f8a24e add libcurl in the readme 2025-09-09 11:22:56 +02:00
Karl Seguin
1ca8dc0ac0 Merge pull request #1022 from lightpanda-io/slot
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Start working on HTMLSlotElement
2025-09-09 11:52:04 +08:00
Karl Seguin
85d148822e migrate more tests to htmlRunner 2025-09-09 11:48:08 +08:00
Karl Seguin
1e738dcf79 Merge pull request #1023 from lightpanda-io/migrate_some_tests_6
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
migrate more tests to htmlRunner
2025-09-08 20:58:41 +08:00
Karl Seguin
b5ffd8d046 Merge pull request #1024 from lightpanda-io/run_distant_tasks
Ability to run tasks even in the "distant" future.
2025-09-08 20:58:30 +08:00
Karl Seguin
21e354d252 Ability to run tasks even in the "distant" future.
We previously ignored tasks scheduled more than 5 seconds away. These tasks are
now scheduled on the low priority queue. This means that they won't stop a
page.wait for returning, but they'll still [eventually] be run if page.wait is
called multiple times.

Practically, this means that they'll never be run in `fetch` mode, but they
might be run from CDP if the driver waits.

Make queue names consistent, primary => high_priority, secondary => low_priority
(the same names used by the page)
2025-09-08 18:55:48 +08:00
Karl Seguin
15628d9b07 migrate more tests to htmlRunner 2025-09-08 18:40:59 +08:00
Karl Seguin
950182986a Start working on HTMLSlotElement 2025-09-08 17:36:45 +08:00
Pierre Tachoire
bc82023878 Merge pull request #1020 from lightpanda-io/inline_script_ignore_defer
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Inline script tags ignore defer/async
2025-09-05 17:44:45 +02:00
Pierre Tachoire
d5363e5993 Merge pull request #1018 from lightpanda-io/fix_screen_event_target_prototype
Fix the Screen and ScreenOrientation prototype
2025-09-05 17:44:09 +02:00
Pierre Tachoire
80adee8558 Merge pull request #1017 from lightpanda-io/fix_async_script_processing
Fix blockingGet during blockingGet
2025-09-05 17:43:40 +02:00
Pierre Tachoire
37fe6a661b Merge pull request #1013 from lightpanda-io/reset_request_method
Reset CURLOPT_CUSTOMREQUEST for each request
2025-09-05 17:43:30 +02:00
Karl Seguin
eb453f471b Inline script tags ignore defer/async
According to MDN, inline script tags should not have defer/async attributes. But
some do. This ignores those attributes for inline script tags.

(Previously, we were only half ignoring them. We were treating them as inline,
but flagging them as deferred or async, which was causing issues with cleanup)

Fixes: https://github.com/lightpanda-io/browser/issues/1014
2025-09-05 23:23:31 +08:00
Karl Seguin
afd278ca4e Fix the Screen and ScreenOrientation prototype 2025-09-05 19:08:07 +08:00
Karl Seguin
ca8877da2d Fix blockingGet during blockingGet
ScriptManager should only ever has one in-flight blockingGet. The is_blocking
flag is used to assert this, as well as enforce it in evaluate(). If is_blocking
is true, evaluate() exits.

This doesn't work for async scripts however, as they aren't executed via
evaluate(), but rather execute directly once complete.

This PR changes the execution behavior of async scripts. They are now only
executed in evaluate() (and thus won't execute when is_blocking == true).
However, unlike normal/deferred scripts, async scripts continue to execute in
their completion order (not their declared order).

Fixes https://github.com/lightpanda-io/browser/issues/1016
2025-09-05 18:17:55 +08:00
Pierre Tachoire
42828c64fb Merge pull request #1012 from lightpanda-io/cdp_detached
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Don't assume that page events means the BrowserContext has a page
2025-09-05 10:19:18 +02:00
Karl Seguin
6600626f4f Reset CURLOPT_CUSTOMREQUEST for each request 2025-09-05 15:45:28 +08:00
Karl Seguin
ac10d5b2a3 Don't assume that page events means the BrowserContext has a page
CDP currently assumes that if we get a page-related notification (like a
request interception, or page lifecycle event), then we must have a session
and page.

But, Target.detachFromTarget can remove the session from the BrowserContext
while still having the page run (I wonder if we should stop the page at this
point??). So, remove these assumptions and make sure we have a page/session
in the handling of page events.
2025-09-05 15:07:30 +08:00
Pierre Tachoire
9f040025e7 Merge pull request #1010 from lightpanda-io/update_transfer_uri_on_redirect
Update the transfer.uri on redirect
2025-09-05 08:35:13 +02:00
Karl Seguin
2522e7fe9c Merge pull request #1011 from lightpanda-io/migrate_some_tests_5
migrate to htmlRunne (plus zig fmt)
2025-09-05 14:16:10 +08:00
Karl Seguin
dd22c55d23 migrate to htmlRunne (plus zig fmt) 2025-09-05 13:52:08 +08:00
Karl Seguin
a6efa9e9b2 Update the transfer.uri on redirect
Ensures that cookies set on the redirect page use the correct host and we don't
incorrectly reject cookies.

https://github.com/lightpanda-io/browser/issues/947
2025-09-05 08:55:36 +08:00
Karl Seguin
5087b8004a Merge pull request #1009 from lightpanda-io/migrate_some_tests_4
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
migrate to htmlRunner
2025-09-04 18:32:17 +08:00
Karl Seguin
d4c40242d0 Merge pull request #1008 from lightpanda-io/network_idle_page_lifecycle
Emit networkIdle and networkAlmostIdle Page.lifecycleEvent
2025-09-04 17:48:02 +08:00
Karl Seguin
5af55f1d5d migrate to htmlRunner 2025-09-04 17:46:42 +08:00
Karl Seguin
55ef0a5e9e fix some spelling in comments 2025-09-04 16:44:00 +08:00
Karl Seguin
5dda86bf4a Emit networkIdle and networkAlmostIdle Page.lifecycleEvent
Most CDP drivers have a mechanism to wait for idle network, or an almost idle
network (sometimes called networkIdle2). These are events the browser must emit.

The page will now emit `networkIdle` when we are reasonably sure there's no more
network activity (this requires some slight changes to request interception,
since, I believe, intercepted requests should be considered).

`networkAlmostIdle` is currently _always_ emitted prior to emitting
`networkIdle`. We should tweak this but I can't, at a glance, think of a great
heuristic for when this should be emitted.
2025-09-04 16:36:29 +08:00
Karl Seguin
d81377b10d Merge pull request #1007 from lightpanda-io/timeout_limit
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Limit serve timeout to 1 week
2025-09-04 16:02:39 +08:00
Karl Seguin
da128f5d49 remove unecessary @intCast 2025-09-04 15:52:08 +08:00
Karl Seguin
6e5fe8e4a2 Add timeout limit to --help text 2025-09-04 15:48:01 +08:00
Karl Seguin
b3d350d41e Limit serve timeout to 1 week 2025-09-04 15:27:03 +08:00
Karl Seguin
7c6870f8eb Merge pull request #1006 from lightpanda-io/migrate_some_tests_3
migrate to htmlRunner
2025-09-04 13:18:44 +08:00
Karl Seguin
327b4e4e37 migrate to htmlRunner 2025-09-04 13:11:15 +08:00
Karl Seguin
7fdc857326 Merge pull request #1004 from lightpanda-io/migrate_some_tests_2
Migrate some tests 2
2025-09-04 12:19:36 +08:00
Karl Seguin
0382c2775e Migrate more tests to html runner
Implement LocalStorage named get/set (i.e. localStorage["hi"])
2025-09-03 22:54:41 +08:00
Karl Seguin
a0374133cd migrate tests to new html runner 2025-09-03 22:54:40 +08:00
Karl Seguin
055f697340 Merge pull request #1005 from lightpanda-io/e2e-output
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
ci: remove go runner verbose mode
2025-09-03 22:44:35 +08:00
Pierre Tachoire
ec8a9862c7 ci: remove go runner verbose mode 2025-09-03 15:42:35 +02:00
Karl Seguin
f393eb7b7d Merge pull request #1003 from lightpanda-io/http_always_monitor_cdp
Http always monitor cdp
2025-09-03 20:35:49 +08:00
Karl Seguin
78285d7b1e fix tests 2025-09-03 20:23:59 +08:00
Karl Seguin
b6137b03cd Rework page wait again
Further reducing bouncing between page and server for loop polling. If there is
a page, the page polls. If there isn't a page, the server polls. Simpler.
2025-09-03 19:38:01 +08:00
Karl Seguin
e237e709b6 Change loader id on navigation
This appears to be what chrome is doing. I don't know why we weren't before.
2025-09-03 08:17:14 +08:00
Karl Seguin
2ac9b2088a Always monitor the CDP client socket, even on page.wait 2025-09-03 08:17:13 +08:00
Karl Seguin
a791212d89 Merge pull request #1002 from lightpanda-io/nix-0.15.1
Some checks failed
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Update Nix Flake (Zig 0.15.1)
2025-09-03 08:07:34 +08:00
Muki Kiboigo
5cc5f45ef3 update zig-v8-fork 2025-09-02 09:25:33 -07:00
Muki Kiboigo
a11e50c087 nix flake for zig 0.15.1 2025-09-02 08:58:31 -07:00
Pierre Tachoire
4dc09360a1 Merge pull request #1001 from lightpanda-io/fix_abort_crash
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
fix segfault on abort if there are queued transfers
2025-09-02 15:45:51 +02:00
Karl Seguin
3a5528cc4d Merge pull request #1000 from lightpanda-io/log_unhandled_promise_rejections
Log unhandled promise rejection
2025-09-02 21:18:28 +08:00
Karl Seguin
de533755e5 fix segfault on abort if there are queued transfers 2025-09-02 21:18:02 +08:00
Karl Seguin
024b69ee3e update v8 dep 2025-09-02 19:48:56 +08:00
Karl Seguin
d7e7832e9f Log unhandled promise rejection 2025-09-02 18:19:28 +08:00
Karl Seguin
8d4d72bf15 Merge pull request #998 from lightpanda-io/migrate_some_tests_1
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Migrate some tests to the new htmlRunner
2025-09-02 16:11:08 +08:00
Pierre Tachoire
86a82d55fa Merge pull request #999 from lightpanda-io/handle_no_certs
Don't panic if no certs are available
2025-09-02 08:03:44 +02:00
Karl Seguin
5a15066da3 Don't panic if no certs are available
https://github.com/lightpanda-io/browser/issues/982
2025-09-02 13:50:53 +08:00
Karl Seguin
81766c8517 Migrate some tests to the new htmlRunner
Fix events.get_timeStamp (was events.get_timestamp, wrong casing).

Rename `newRunner` to `htmlRunner`.

move tests to src/tests (from src/browser/tests). src/runtime and possibly other
parts might want to have html tests too.
2025-09-02 10:40:04 +08:00
Karl Seguin
e486f28a41 Merge pull request #995 from lightpanda-io/improved_test_runner
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Looking for feedback on new test runner
2025-09-02 07:45:41 +08:00
Karl Seguin
8a9cbaf413 explicitly load testing.js 2025-09-02 07:38:03 +08:00
Karl Seguin
3a0a930b79 don't log 'long timeout ignored' during testing 2025-09-02 07:38:03 +08:00
Karl Seguin
c40704d2f3 Prototype new test runner
Follows up on https://github.com/lightpanda-io/browser/pull/994 and replaces
the jsRunner with a new page.navigation-based test runner.

Currently only implemented for the Window tests, looking for feedback and
converting every existing test will take time - so for a while, newRunner (to be
renamed) will sit side-by-side with jsRunner.

In addition to the benefits outlined in 994, largely around code simplicity and
putting more of the actual code under tests, I think our WebAPI tests
particularly benefit from:
1 - No need to recompile when modifying the html tests
2 - Much better assertions, e.g. you can assert that something is actually an
    array, not just a string representation of an array
3 - Ability to test some edge cases (e.g. dynamic script loading)

I've put some effort into testing.js to make sure that, if the encapsulating
zig test passes, it's because it actually passed, not because it didn't run.

For the time being, console tests are removed. I think it's more useful to have
access to the console within tests, than it is to test the console (which is
just a wrapper around log, which is both tested and heavily used).
2025-09-02 07:38:02 +08:00
Karl Seguin
c0f0630e17 Merge pull request #997 from lightpanda-io/fix_build
fix build
2025-09-02 07:24:02 +08:00
Karl Seguin
19dbb3a778 fix build 2025-09-02 07:06:57 +08:00
Karl Seguin
d4fc6f1b35 Merge pull request #996 from lightpanda-io/revert-document-element
Revert "document.documentElement returns a *parser.Element"
2025-09-02 06:52:16 +08:00
Karl Seguin
7c82942912 Merge pull request #994 from lightpanda-io/test_http_server
Test http server
2025-09-02 06:51:52 +08:00
Karl Seguin
87d48b028b Merge pull request #992 from lightpanda-io/http_buffer_presize
Pre-size the destination buffer when we know the response content length
2025-09-02 06:51:15 +08:00
Pierre Tachoire
d6640f4d15 Revert "document.documentElement returns a *parser.Element"
This reverts commit c1752ae5eb.
2025-09-01 15:46:16 +02:00
Karl Seguin
785a8da623 remove content-length limit 2025-09-01 18:53:00 +08:00
Karl Seguin
57dc303d90 Make getContentLength work on fulfilled responses 2025-09-01 18:40:50 +08:00
Pierre Tachoire
ce08cc9659 Merge pull request #993 from lightpanda-io/remove_unsafe_undefine
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Remove [some] unsafe undefines from netsurf wrapper
2025-09-01 08:26:41 +02:00
Pierre Tachoire
866393743c Merge pull request #991 from lightpanda-io/mimalloc_assertions
Switch mimalloc guards to assertions
2025-09-01 08:12:21 +02:00
Pierre Tachoire
ba255aa653 Merge pull request #989 from lightpanda-io/clocks
Improve clocks
2025-09-01 08:11:05 +02:00
Karl Seguin
7d46e8fe80 Start unifying test and code
Depends on https://github.com/lightpanda-io/browser/pull/993

There's currently 3 ways to execute a page:
1 - page.navigate (as used in both the 'fetch' and 'serve' commands)
2 - jsRunner as used in unit tests
3 - main_wpt as used in the WPT runner

Both jsRunner and main_wpt replicate the page.navigate code, but in their own
hack-ish way. main_wpt re-implements the DOM walking in order to extract and
execute <script> tags, as well as the needed page lifecycle events.

This PR replaces the existing main_wpt loader with a call to page.navigate. To
support this, a test HTTP server was added. (The test HTTP server is extracted
from the existing unit test test server, and re-used between the two).

There are benefits to this approach:
1 - The code is simpler
2 - More of the actual code and flow is tested
3 - There's 1 way to do things (page.navigate)
4 - Having an HTTP server might unlock some WPT tests

Technically, we're replacing file IO with network IO i.e. http requests). This
has potential downsides:
1 - The tests might be more brittle
2 - The tests might be slower

I think we need to run it for a while to see if we get flaky behavior.

The goal for following PRs is to bring this unification to the jsRunner.
2025-09-01 13:01:08 +08:00
Karl Seguin
6c41245c73 Remove [some] unsafe undefines from netsurf wrapper
Code like this:

```
var body: ?*ElementHTML = undefined;
const err = documentHTMLVtable(doc_html).get_body.?(doc_html, &body);
try DOMErr(err);
if (body == null) return null;
return @as(*Body, @ptrCast(body.?));
```

Isn't safe. It assumes that libdom will either return an error, or set body
to null or a value. However, there are cases (specifically for this API) where
libdom returns DOM_NO_ERR without ever setting body. In such cases, we return
a `body` initialized to a random (invalid) value.

This PR replaces the initial value of optional types from undefined to null
(within the libdom wrapper).
2025-09-01 11:41:37 +08:00
Karl Seguin
2a8e51c2d2 Pre-size the destination buffer when we know the response content length 2025-08-31 20:14:55 +08:00
Karl Seguin
b2cf5df612 Switch mimalloc guards to assertions
The thin mimalloc API is currently defensive around incorrect setup/teardown by
guarding against using/destroying the arena when the heap is null, or creating
an arena when it already exists.

The only time these checks will fail is when the code is wrong, e.g. trying
to use libdom before or after freeing the arena. The current behavior can mask
these errors, plus add runtime overhead.
2025-08-31 19:35:53 +08:00
Karl Seguin
ada9ddd5b8 Improve clocks
There's a flaky performance test that I wanted to fix (1). This led to a couple
changes.

1 - Add timestamp() and milliTimestamp() to datetime.zig. Reduce some code
    duplication and use better clock_ids where available

2 - Change Performance API to use milliTimestamp and store a u64 instead of a
    f64. While the spec says a float, Firefox deals with u64 and implicit
    conversion is always available. Makes our APIs simpler.

(1) - https://github.com/lightpanda-io/browser/actions/runs/17313296490/job/49151366798#step:4:131
2025-08-30 13:45:12 +08:00
Karl Seguin
f66f4d9aeb Merge pull request #987 from lightpanda-io/improve_server_shutdown
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Ignore ConnectionClosed error on server shutdown
2025-08-30 12:35:12 +08:00
Pierre Tachoire
d02ba777f2 Merge pull request #984 from lightpanda-io/zig.0.15.1
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Zig 0.15.1
2025-08-29 10:33:00 +02:00
Karl Seguin
aef614823b Ignore ConnectionClosed error on server shutdown
Our shutdown could be cleaner, but this at least removes a meaningless (because
we're shutting down) log.err that was happening on every test run.
2025-08-29 16:21:26 +08:00
Karl Seguin
431db85ecb Merge pull request #978 from lightpanda-io/dynamic_cdp_read_buffer
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Make the CDP read buffer heap allocated & dynamic
2025-08-29 12:20:58 +08:00
Karl Seguin
1ebac06f4b add debug line on cdp buffer growth 2025-08-29 10:55:36 +08:00
Karl Seguin
c7c5af4708 zig fmt 2025-08-29 10:51:19 +08:00
Karl Seguin
0b6a9d3a0b use llvm. The new x86 backend crashes with v8. 2025-08-29 10:42:07 +08:00
Karl Seguin
23d6362058 fix telemetry, link libc and libcpp 2025-08-29 10:42:06 +08:00
Karl Seguin
1443f38e5f Zig 0.15.1
Depends on https://github.com/lightpanda-io/zig-v8-fork/pull/89
2025-08-29 10:42:06 +08:00
Karl Seguin
94960cc842 Merge pull request #979 from lightpanda-io/app_owns_platform
App owns platform
2025-08-29 10:33:55 +08:00
Karl Seguin
efc983b009 Start with 16K buffer (down from 32K). Use array list growth algorithm 2025-08-29 10:33:27 +08:00
Karl Seguin
74d90f2892 fix tests 2025-08-29 10:14:59 +08:00
Karl Seguin
56f1b6cc19 Make the CDP read buffer heap allocated & dynamic
Rather than stack-allocating MAX_MESSAGE_SIZE upfront, we now allocate 32KB
and grow the buffer as needed for larger messages, up to MAX_MESSAGE_SIZE.

This will reduce memory usage for drivers that don't send huge payloads (like
playwright does).

While not implemented, this would also enable us to set the MAX_MESSAGE_SIZE
at runtime (e.g. via a command line option).
2025-08-29 10:14:58 +08:00
Karl Seguin
fa2cd9dfd9 Ability to start/stop CDP server.
Exists for cleaning up after tests.
2025-08-29 10:14:08 +08:00
Karl Seguin
687f09d952 Make the App own the Platform
Removes optional platform, which only existed for tests.

There is now a global `@import("testing.zig").test_app` available. This is setup
when the test runner starts, and cleaned up at the end of tests. Individual
tests don't have to worry about creating app, which I assume was the reason I
Platform optional, since that woul dhave been something else that needed to be
setup.
2025-08-29 10:14:06 +08:00
Karl Seguin
67b479b5c8 Merge pull request #983 from lightpanda-io/sigint
exit the browser on SIGINT signal
2025-08-29 10:10:49 +08:00
Pierre Tachoire
eac2140693 Merge pull request #986 from lightpanda-io/readme-interception
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
README: check request interception
2025-08-28 17:19:32 +02:00
Pierre Tachoire
7a3f5de9c2 Merge pull request #985 from lightpanda-io/fulfill-content-type-len
http: set content_type len on fulfill request
2025-08-28 17:19:23 +02:00
Pierre Tachoire
7005bf2481 README: check request interception 2025-08-28 17:18:42 +02:00
Pierre Tachoire
b80ee3342c http: set content_type len on fulfill request 2025-08-28 16:28:41 +02:00
Pierre Tachoire
4c7b7b1e60 handle graceful shutdown 2025-08-28 12:44:16 +02:00
Pierre Tachoire
1a4a3608c8 exit the browser on SIGINT signal 2025-08-28 12:44:12 +02:00
Pierre Tachoire
6800d50339 Merge pull request #981 from lightpanda-io/page-navigate-event
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
page: ensure page navigate events order
2025-08-27 18:23:22 +02:00
Pierre Tachoire
036f808ec6 page: ensure page navigate events order 2025-08-27 17:36:36 +02:00
Pierre Tachoire
7647ce9e6d Merge pull request #960 from lightpanda-io/auth-challenge
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
auth required interception
2025-08-27 15:34:51 +02:00
Karl Seguin
545d3f81ce Merge pull request #977 from lightpanda-io/selector_by_ref
Select is relatively large (64 bytes), pass it by ref
2025-08-27 19:37:36 +08:00
Pierre Tachoire
455615b9c1 Merge pull request #980 from lightpanda-io/update-docker-readme
Update docker readme
2025-08-27 09:32:41 +02:00
Pierre Tachoire
d0e2a03da5 README: proxy support is ready 2025-08-27 09:30:43 +02:00
Pierre Tachoire
fa408e644c cs fix 2025-08-27 09:26:10 +02:00
Pierre Tachoire
a22416584d README: --privileged is not needed anymore 2025-08-27 09:25:51 +02:00
Karl Seguin
b8fc60df45 Merge pull request #975 from lightpanda-io/dynamic_script
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Support dynamic scripts which are added to the DOM before src is set
2025-08-27 05:59:28 +08:00
Karl Seguin
c6455cf02e Select is relatively large (64 bytes), pass it by ref 2025-08-27 05:55:04 +08:00
Pierre Tachoire
2ac1d39367 Merge pull request #976 from lightpanda-io/webapi_file_placeholder
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
The most basic File implementation.
2025-08-26 18:20:53 +02:00
Pierre Tachoire
041e014d68 Merge pull request #970 from lightpanda-io/remove_loop
Remove the loop
2025-08-26 18:17:32 +02:00
Pierre Tachoire
5defb5c442 http: build headers when auth challenge fails 2025-08-26 18:05:45 +02:00
Pierre Tachoire
520a572bb4 http: add reset and tries for transfer 2025-08-26 18:05:45 +02:00
Pierre Tachoire
4c602256da http: remove useless field 2025-08-26 18:05:45 +02:00
Pierre Tachoire
5a40cbd655 cdp: use enum for AuthChallengeResponse 2025-08-26 18:05:45 +02:00
Pierre Tachoire
a75f9dd48d cdp: set default username/passwd for authChallengeResponse 2025-08-26 18:05:44 +02:00
Pierre Tachoire
6b47aa2446 cdp: add auth required interception process 2025-08-26 18:05:44 +02:00
Pierre Tachoire
a847a1faae http: replace _forbidden with _auth_challenge struct 2025-08-26 18:05:44 +02:00
Pierre Tachoire
bb381e522c http: add creds into request 2025-08-26 18:05:39 +02:00
Karl Seguin
6962cfb91a Merge pull request #973 from lightpanda-io/no-body-response
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Handle response without body
2025-08-26 18:44:22 +08:00
Pierre Tachoire
302c50a90e Merge pull request #964 from lightpanda-io/proxy-header
http: refacto headerCallback and get proxy CONNECT request details
2025-08-26 10:53:43 +02:00
sjorsdonkers
e2d47e1c86 fix merge conflict 2025-08-26 10:12:07 +02:00
Pierre Tachoire
7d51da1efb Merge pull request #974 from lightpanda-io/ignore_non_js_script_tags
Removes the log for unknown script tags
2025-08-26 08:53:29 +02:00
Karl Seguin
c7674926c3 The most basic File implementation.
Almost silly as-is, but handles this case:

```
if (input instanceof File) {
   throw Error('file not supported')
}
```

as seen on reddit.
2025-08-26 13:25:30 +08:00
Karl Seguin
f0ca9728ae Support dynamic scripts which are added to the DOM before src is set
This should load the "src.js":

```
const s = document.createElement('script');
document.getElementsByTagName('body')[0].appendChild(s);
s.src = "src.js"
```

Notice that src is set AFTER the element is added to the DOM. This PR enables
the above, by
1 - skipping dynamically added scripts which don't have a src
2 - trying to load a script whenever `set_src` is called.

(2) is safe because the ScriptManager already prevents scripts from being
processed multiple times.

Additionally, not only can the src be set after the script is added to the DOM,
but onload and onerror can be set after the src:

```
s.src = "src.js"
s.onload = ...;
s.onerror = ...;
```

This PR also delays reading the onload/onerror callbacks until the script is
done loading.

This behavior is seen on reddit.
2025-08-26 13:10:55 +08:00
Karl Seguin
5fa8567801 Removes the log for unknown script tags
Some sites have a lot of text/template or application/json, and it just adds
noise to the logs.
2025-08-26 08:48:29 +08:00
sjorsdonkers
e5b1acb6e1 Handle response without body 2025-08-25 18:07:02 +02:00
Karl Seguin
8fdbaef4c7 Use posix.TCP.NODELAY now that it's available in MacOS also 2025-08-25 22:03:58 +08:00
Pierre Tachoire
7869159657 add e2e test through proxy 2025-08-25 14:18:15 +02:00
Pierre Tachoire
7046e18d7e http: simplify header parsing 2025-08-25 14:18:14 +02:00
Pierre Tachoire
a7516061d0 http: move use_proxy from connection to client 2025-08-25 14:18:14 +02:00
Pierre Tachoire
e61d787ff0 http: move header done callback in its own func
And call it only after the headers are parsed, either from data callback
or end of the request.
2025-08-25 14:18:14 +02:00
Pierre Tachoire
25ad420f85 http: ajust header callback according to review 2025-08-25 14:18:14 +02:00
Pierre Tachoire
fcd49c000f page: avoid crash on empty body 2025-08-25 14:18:13 +02:00
Pierre Tachoire
e2320ebe66 http: handle proxy's request header callback 2025-08-25 14:18:13 +02:00
Pierre Tachoire
5e78a26e3d http: refacto http header parsing 2025-08-25 14:18:13 +02:00
Pierre Tachoire
159bd06a56 http: add use_proxy bool in connection 2025-08-25 14:18:12 +02:00
Pierre Tachoire
bc7e1e07f4 typo fix 2025-08-25 14:18:08 +02:00
Karl Seguin
ccc9618102 Merge pull request #971 from lightpanda-io/fix-send-error-json-format
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Fix sendError message's format
2025-08-25 19:05:47 +08:00
sjorsdonkers
0ad09cca9d Fix sendError message's format 2025-08-25 12:51:47 +02:00
Karl Seguin
0959eea677 Remove the loop
Previously, the IO loop was doing three things:
1 - Managing timeouts (either from scripts or for our own needs)
2 - Handling browser IO events (page/script/xhr)
3 - Handling CDP events (accept, read, write, timeout)

With the libcurl merge, 1 was moved to an in-process scheduler and 2 was moved
to libcurl's own event loop. That means the entire loop code, including
the dependency on tigerbeetle-io existed for handling a single TCP client.
Not only is that a lot of code, there was also friction between the two loops
(the libcurl one and our IO loop), which would result in latency - while one
loop is waiting for the events, any events on the other loop go un-processed.

This PR removes our IO loop. To accomplish this:

1 - The main accept loop is blocking. This is simpler and works perfectly well,
given we only allow 1 active connection.
2 - The client socket is passed to libcurl - yes, libcurl's loop can take
arbitrary FDs and poll them along with its own.

In addition to having one less dependency, the CDP code is quite a bit simpler,
especially around shutdowns and writes. This also removes _some_ of the latency
caused by the friction between page process and CDP processing. Specifically,
when CDP now blocks for input, http page events (script loading, xhr, ...) will
still be processed.

There's still friction. For one, the reverse isn't true: when the page is
waiting for events, CDP events aren't going to be processed. But the page.wait
already have some sensitivity to this (e.g. the page.request_intercepted flag).
Also, when CDP waits, while we will process network events, page timeouts are
still not processed. Because of both these remaining issues, we still need to
jump between the two loops - but being able to block on CDP (even for a short
time) WITHOUT stopping the page's network I/O, should reduce some latency.
2025-08-25 17:27:28 +08:00
Pierre Tachoire
3316f2fcf4 Merge pull request #968 from lightpanda-io/normalize_cdp_response_headers
Normalize CDP response headers
2025-08-25 09:31:56 +02:00
Karl Seguin
390a21e4aa Merge pull request #969 from lightpanda-io/fix_wpt_runner
Handle all case status (not just Pass and Fail)
2025-08-25 10:49:46 +08:00
Karl Seguin
70ce54a5cd Handle all case status (not just Pass and Fail) 2025-08-25 10:40:23 +08:00
Karl Seguin
087e42a641 Normalize CDP response headers
chromedb doesn't support duplicate header names. Although servers _will_ send
this (e.g. Cache-Control: public\r\nCache-Control: max-age=60\r\n), Chrome
seems to join them with a "\n". So we do the same.

A note on curl_easy_nextheader, which this code ultimately uses to iterate
and collect the headers. The documentation says:

    Applications must copy the data if they want it to survive subsequent API
    calls or the life-time of the easy handle.

As-is, I'd understand this to mean that a given header name/value is only
valid until any API call, including another call to curl_easy_nextheader. So,
from this comment, we _should_ be duping the name/value. But we don't. Why?
Because, despite the note in the documentation, this doesn't appear to be how
it actually works, nor does it really make sense. If it's just a linked list,
there's no reason curl_easy_nextheader should invalidate previous results. I'm
guessing this is just a general lack of guarantee libcurl is willing to make re
lifetimes.

https://github.com/lightpanda-io/browser/issues/966
2025-08-25 09:25:15 +08:00
Karl Seguin
e26d4afce2 Merge pull request #963 from lightpanda-io/wpt_runner_fix_and_nodeiterator_tweak
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Improves correctness of NodeIterator
2025-08-22 15:29:42 +08:00
Karl Seguin
b9ae4c6077 Update src/runtime/js.zig
Co-authored-by: Pierre Tachoire <pierre@lightpanda.io>
2025-08-22 15:17:59 +08:00
Pierre Tachoire
11485d24f5 Merge pull request #962 from lightpanda-io/compareBoundaryPoints
Add Range.compareBoundaryPoints
2025-08-22 09:08:22 +02:00
Karl Seguin
ce14f0b380 Improves correctness of NodeIterator
Minor improvement to correctness of TreeWalker.

Fun fact, this is the first time, that I've run into, where we have to default
null and undefined to different values.

Also, tweaked the WPT test runner. WPT test results use | as a field delimiter.
But a WPT test (and, I assume a message) can contain a |. So we had at least
a few tests that were being reported as failed, only because the result line
was weird / unexpected. No great robust way to parse this, but I changed it
to look explicitly for |Pass or |Fail and use those positions as anchor points.
2025-08-21 18:11:48 +08:00
Karl Seguin
8bb2158a2a Add Range.compareBoundaryPoints
Also rename start_container and end_container to start_node and end_node.
2025-08-21 16:47:33 +08:00
Karl Seguin
1a9d4af565 Merge pull request #961 from lightpanda-io/cdp_getResponseBody
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Implement Network.getResponseBody
2025-08-21 16:19:07 +08:00
Pierre Tachoire
a6f37633a1 Merge pull request #959 from lightpanda-io/html-pre
handle text content type with HTML
2025-08-21 09:38:36 +02:00
Pierre Tachoire
3182a47858 typo fix 2025-08-21 08:52:35 +02:00
Pierre Tachoire
7335b1d0a4 escape incoming plain text 2025-08-21 08:52:34 +02:00
Karl Seguin
cd33e9ad0e Implement Network.getResponseBody
Add response_data event, CDP now captures the full body so that it can respond
to the Network.getResponseBody. This isn't memory efficient, but I don't see
another way to do it. At least this way, it's only capturing/storing every
response body when (a) CDP is used and (b) Network.enabled is called. That is,
as opposed to baking this into Http/Client.zig, which would force the memory
consumption for all use-cases.

There's arguably some optimizations we could make for XHR requests, which also
dupe/own the response. As of now, the response is dupe'd separately for CDP
and XHR.
2025-08-21 10:33:53 +08:00
Karl Seguin
557f8444b2 Merge pull request #955 from lightpanda-io/replace_deprecated_build
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Makes build.zig Zig 0.15 ready
2025-08-21 10:09:59 +08:00
Karl Seguin
65088b8644 swap unnecessary addModule with createModule 2025-08-21 09:59:42 +08:00
Karl Seguin
7cc9521cbb Merge pull request #958 from lightpanda-io/http_request_done_notification
Emits a http_request_done internal notification.
2025-08-21 09:23:41 +08:00
Karl Seguin
4ad19fc4d8 use merged v8 commit 2025-08-21 09:23:11 +08:00
Pierre Tachoire
ec71f8e2d9 handle text content type with HTML
For text content type (and application/json) we create a pseudo HTML
tree with the text value in a <pre> tag.

It allows CDP clients to interact with text content easily.
2025-08-20 15:27:15 +02:00
Pierre Tachoire
ff8a847795 Merge pull request #957 from lightpanda-io/remove_header_callback
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Remove the http/Client.zig header_callback.
2025-08-20 14:35:06 +02:00
Karl Seguin
6b001c50a4 Emits a http_request_done internal notification.
With networking enabled, CDP listens to this event and emits a
`Network.loadingFinished` event. This is event is used by puppeteer to know that
details about the response (i.e. the body) can be queries.

Added dummy handling for the Network.getResponseBody message. Returns an
empty body. Needed because we emit the loadingFinished event which signals
to drivers that they can ask for the body.
2025-08-20 19:32:19 +08:00
Karl Seguin
5759c88932 Remove the http/Client.zig header_callback.
The callback which was called on a per-header basis is removed. Only XHR was
using this, and it was created before the HeaderIterator existed (because I
didn't know we could iterate through the response headers in curl after the fact).

The header_done_callback remains, but is now called header_callback (a bit
confusing in the short term).

The only difficulty was with fulfilled requests, which do not have an easy
handle for our HeaderIterator. The existing code would segfault if
transfer.responseHeaderIterator() was called on a fulfilled requests.
The HeaderIterator is now a tagged union that abstracts whether the source of
the response header is a curl easy, or just an injected list from the fulfilled
requests.
2025-08-20 17:49:37 +08:00
Karl Seguin
00c11d9bd4 Merge pull request #956 from lightpanda-io/typo-fix
typo fix
2025-08-20 17:06:16 +08:00
Pierre Tachoire
ed99acebfe typo fix 2025-08-20 09:25:47 +02:00
Pierre Tachoire
bade412d30 Merge pull request #953 from lightpanda-io/is_navigation_and_arena
Use Transfer.arena in a few more places, correctly set is_navigation …
2025-08-20 08:59:32 +02:00
Pierre Tachoire
191e9ba073 Merge pull request #954 from lightpanda-io/remove_managed_arraylist
Replace all std.ArrayList with std.ArrayListUnmanaged
2025-08-20 08:55:03 +02:00
Karl Seguin
b21688a0ac Makes build.zig Zig 0.15 ready
Our build.zig is using a number of deprecated features, which are removed in
0.15.

This updates build.zig so that it still works in 0.14 and [hopefully] will work
in 0.15.

Related: https://github.com/lightpanda-io/zig-v8-fork/pull/87
2025-08-20 14:54:27 +08:00
Karl Seguin
a4d4da4d96 Replace all std.ArrayList with std.ArrayListUnmanaged
Very minor, but the more we can do upfront to align the code for Zig 0.15, the
better.
2025-08-20 12:30:39 +08:00
Karl Seguin
16c85c5b8a Use Transfer.arena in a few more places, correctly set is_navigation on redirect
Following up to Request Interception PR (1) and Cookie Redirect PR (2) which
both introduced features that were useful to the other. This PR closes that
loop.

(1) https://github.com/lightpanda-io/browser/pull/946
(2) https://github.com/lightpanda-io/browser/pull/948
2025-08-20 11:39:38 +08:00
Karl Seguin
2c7b39927a Merge pull request #952 from lightpanda-io/fix_compilation_error
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Fix compilation error
2025-08-20 10:28:54 +08:00
Karl Seguin
7f47692ad4 Fix compilation error
bad auto merge?
2025-08-20 10:04:15 +08:00
Karl Seguin
af4066da87 Merge pull request #946 from lightpanda-io/request_interception
Request Interception
2025-08-20 07:53:08 +08:00
Pierre Tachoire
4de4e7504d Merge pull request #951 from lightpanda-io/wpt_range
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Improve correctness of Node.compareDocumentPosition and Range api.
2025-08-19 17:23:39 +02:00
Karl Seguin
b46c181b07 zig fmt 2025-08-19 22:01:14 +08:00
Karl Seguin
e4f89092b3 add Range.intersectsNode and cover a few more edge-cases 2025-08-19 22:00:59 +08:00
Pierre Tachoire
4fbedf5b20 Merge pull request #948 from lightpanda-io/redirect-cookies
handle cookies on redirection manually
2025-08-19 14:48:37 +02:00
Karl Seguin
d51a03f1b6 Improve correctness of Node.compareDocumentPosition and Range api.
Should fix a good chunk (~20K I think) of the recently broken WPT tests.
2025-08-19 18:23:54 +08:00
Pierre Tachoire
f7eee0d461 http: add an arena to Transfer 2025-08-19 11:10:52 +02:00
Pierre Tachoire
39178d8d2b http: remove uselesss Client.arena 2025-08-19 11:10:25 +02:00
Pierre Tachoire
7795916c08 apply review comments 2025-08-19 10:01:35 +02:00
Pierre Tachoire
0e2a3d8009 handle cookies on redirection manually 2025-08-19 10:01:11 +02:00
Karl Seguin
38a0b6905e Merge pull request #949 from lightpanda-io/network_path_reference_stitch
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Fix network-path reference stitching
2025-08-19 15:38:43 +08:00
Karl Seguin
8797549369 Fix network-path reference stitching 2025-08-18 23:05:11 +08:00
Karl Seguin
f5ec74252d Add fulfillRequest and more complete continueRequest 2025-08-18 18:29:10 +08:00
Karl Seguin
211012d367 move intercept_state and extra_headers from CDP instance to BrowserContext 2025-08-18 13:23:17 +08:00
Karl Seguin
c1319d1f27 add proper resourceType 2025-08-18 12:42:18 +08:00
Pierre Tachoire
d4d8773fd1 Merge pull request #927 from lightpanda-io/window-frames
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Partial window.frames implementation
2025-08-15 14:54:29 +02:00
Karl Seguin
01223601f2 Reduce allocations made during request interception
Stream (to json) the Transfer as a request and response object in the various
network interception-related events (e.g. Network.responseReceived).

Add a page.request_intercepted boolean flag for CDP to signal the page that
requests have been intercepted, allowing Page.wait to prioritize intercept
handling (or, at least, not block it).
2025-08-15 14:01:57 +08:00
Karl Seguin
d9ed4cfca8 Merge pull request #940 from lightpanda-io/redirect-cookies
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
enable curl cookie engine
2025-08-15 08:50:25 +08:00
Pierre Tachoire
7d0e4b6270 use CURLOPT_COOKIE to set cookies 2025-08-14 15:33:02 +02:00
Pierre Tachoire
b2f645a5ce enable curl cookie engine
Enabling Curl cookie engine brings advantage:
* handle cookies during a redirection: when a srv redirects including
  cookies, curl sends back the cookies correctly during the next request
2025-08-14 15:32:56 +02:00
Karl Seguin
6a29d6711c Merge pull request #945 from lightpanda-io/remove_unecessary_content_type_parse
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Remove unecessary content type parse
2025-08-14 19:07:41 +08:00
Karl Seguin
5b2806a784 expose response header amount 2025-08-14 18:57:57 +08:00
Karl Seguin
a2f15ce0b2 Remove unecessary content type parse
getResponseHeader takes header index
2025-08-14 18:26:01 +08:00
Karl Seguin
68400f3bcf Merge pull request #944 from lightpanda-io/fix_memory_leak
fix memory leak
2025-08-14 18:20:53 +08:00
Karl Seguin
31f3c2771a fix build error...sorry 2025-08-14 18:07:14 +08:00
Karl Seguin
f9352e26cb same memory leak, different place 2025-08-14 18:00:56 +08:00
Karl Seguin
4fa542bc38 fix memory leak 2025-08-14 17:51:40 +08:00
Pierre Tachoire
a707e10af7 Merge pull request #922 from lightpanda-io/nonblocking_libcurl
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Nonblocking libcurl
2025-08-14 11:43:19 +02:00
Pierre Tachoire
1e095fede5 zig fmt build.zig 2025-08-14 11:29:56 +02:00
Karl Seguin
96b10f4b85 Optimize Network.responseReceived
Add a header iterator to the transfer. This removes the need for NetworkState,
duping header name/values, and the http_header_received event.
2025-08-14 15:50:56 +08:00
Karl Seguin
5100e06f38 fix header done callback 2025-08-14 14:51:02 +08:00
Karl Seguin
35e2fa5058 Merge pull request #943 from lightpanda-io/integer-overflow
fix integer overflow for sleeping delay
2025-08-14 05:43:49 +08:00
Pierre Tachoire
8d2d4ffdd2 fix integer overflow for sleeping delay 2025-08-13 19:44:06 +02:00
sjorsdonkers
7d05712f40 setExtraHTTPHeaders 2025-08-13 14:54:59 +02:00
sjorsdonkers
c0106a238b http_headers_done_receiving 2025-08-13 14:29:23 +02:00
Karl Seguin
f6c68e4580 fix release build (constness via telemetry, not seen in debug) 2025-08-13 20:16:14 +08:00
Karl Seguin
3c8065fdee fix fmt 2025-08-13 20:12:39 +08:00
Karl Seguin
9bd8b2fc43 fix wpt runner 2025-08-13 19:39:49 +08:00
Karl Seguin
5a3d5f5512 improve elapsed display for larger numbers 2025-08-13 18:17:59 +08:00
Karl Seguin
ca9e850ac7 Create Client.Transfer earlier.
On client.request(req) we now immediately wrap the request into a Transfer. This
results in less copying of the Request object. It also makes the transfer.uri
available, so CDP no longer needs to std.Uri(request.url) anymore.

The main advantage is that it's easier to manage resources. There was a use-
after free before due to the sensitive nature of the tranfer's lifetime. There
were also corner cases where some resources might not be freed. This is
hopefully fixed with the lifetime of Transfer being extended.
2025-08-13 18:05:00 +08:00
Karl Seguin
2dc09c799f Merge pull request #930 from lightpanda-io/request_interception
request interception
2025-08-13 14:44:26 +08:00
sjorsdonkers
a49154acf4 http_request_fail 2025-08-12 15:20:48 +02:00
sjorsdonkers
77eee7f087 Cookies 2025-08-12 14:40:23 +02:00
sjorsdonkers
03694b54f0 3# This is a combination of 3 commits.
intercept continue and abort

feedback

First version of headers, no cookies yet
2025-08-12 13:49:20 +02:00
Karl Seguin
bed320204d Merge pull request #939 from lightpanda-io/raw-done
finalize document loading with non-HTML pages
2025-08-12 19:09:31 +08:00
Pierre Tachoire
971524fa3b finalize document loading with non-HTML pages
Avoid infinite the loop of loading non-HTML documents with CDP.
2025-08-12 12:55:44 +02:00
Karl Seguin
4758456069 Merge pull request #938 from lightpanda-io/node_isConnected
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Fix Node.isConnected
2025-08-12 18:17:28 +08:00
Karl Seguin
3ef4ba6b8b Fix Node.isConnected
The previous implementation just checked if a node had a parent. But it should
check the node has a document ancestor:

```
const d1 = document.createElement('div');
document.createElement('div').appendChild(d1);
d1.isConnected
```

should be `false`.

In addition to this fix, also added support for DocumentFragments which are
part of the ShadowRoot. This, like events, is one of those apis that DOES break
the ShadowRoot isolation.
2025-08-12 17:23:38 +08:00
Karl Seguin
a504f051e7 Merge pull request #937 from lightpanda-io/event_composedPath
Add ShadowRoot get/set innerHTML
2025-08-12 17:22:25 +08:00
Karl Seguin
ea0bbaf332 Revert "Treat pending requests as active"
This reverts commit 19c908035b.
2025-08-12 11:27:28 +08:00
Karl Seguin
19c908035b Treat pending requests as active
This ensures that page.wait won't unblock too early. As-is, this isn't an issue
since active can only be 0 if there are no active OR pending requests. However,
with request interception (https://github.com/lightpanda-io/browser/pull/930)
it's possible to have no active requests and no pending requests - from the
http client's point of view - but still have pending-on-intercept requests.

An alternative to this would be to undo these changes, and instead change
Page.wait to be intercept-aware. That is, Page.wait would continue to block on
http activity and scheduled tasks, as well as intercepted requests. However,
since the Page doesn't know anything about CDP right now, and it does know
about the http client, maybe doing this in the client is fine.
2025-08-12 11:13:19 +08:00
Muki Kiboigo
05192b6850 update flake 2025-08-11 12:09:22 -07:00
Karl Seguin
079ce5e9de whitelist application/ld+json 2025-08-11 21:38:36 +08:00
Karl Seguin
ff742c0169 don't allow concurrent blocking calls 2025-08-11 21:38:36 +08:00
Karl Seguin
332e264437 remove unimportant todos 2025-08-11 21:38:34 +08:00
Karl Seguin
3554634c1c cleanup optional request headers 2025-08-11 21:37:03 +08:00
Karl Seguin
c96fb3c2f2 support CDP proxy override 2025-08-11 21:37:03 +08:00
Karl Seguin
1e612e4166 Add command line options to control HTTP client
http_timeout_ms
http_connect_timeout_ms
http_max_host_open
http_max_concurrent
2025-08-11 21:37:03 +08:00
Karl Seguin
06984ace21 fix overflow and debug units 2025-08-11 21:37:03 +08:00
Karl Seguin
cabd4fa718 re-enable datauris 2025-08-11 21:37:03 +08:00
Karl Seguin
ddb549cb45 cookie support 2025-08-11 21:37:02 +08:00
Karl Seguin
c7484c69c0 Increase max concurrent request to 10
Improve wait analysis dump.

De-prioritize secondary schedules.

Don't log warning for application/json scripts

Change pretty log timer to display time from start.
2025-08-11 21:37:02 +08:00
Karl Seguin
9876d79680 Add Accept-Encoding
This is necessary because of CloudFront which will send gzip content even if
we don't ask for it.

Properly handle scripts that are both async and defer.

Add a helper to print state of page wait. This can be helpful in identifying
what's causing the page to hang on page.wait.
2025-08-11 21:37:02 +08:00
Karl Seguin
32566ccc80 Set window location on load
Set SUPPRESS_CONNECT_HEADERS option.
2025-08-11 21:37:02 +08:00
Karl Seguin
7f9e309ae8 Shutdown clean async scripts
Set parent current script
2025-08-11 21:37:02 +08:00
Karl Seguin
7831aabe5a connect proxy 2025-08-11 21:37:02 +08:00
Karl Seguin
74b40b97ec fix ScriptManager wrong order execution 2025-08-11 21:37:02 +08:00
Karl Seguin
f45726d61f ScriptManager & HttpClient support for JS modules
Improve cleanup/shutdown (*cough* memory leaks *cough*)
2025-08-11 21:37:01 +08:00
Karl Seguin
3c0d027306 dynamic script support 2025-08-11 21:37:01 +08:00
Karl Seguin
dc83765808 fix build 2025-08-11 21:37:01 +08:00
Karl Seguin
4244b572d1 Improve page.wait
Allow page.wait to transition page mode.

Optimize initial page load. No point running scheduler until the initial
page is loaded.

Support ISO-8859-1 charset
2025-08-11 21:37:01 +08:00
Karl Seguin
77475ca5e4 Re-enable --insecure_disable_tls_host_verification
Better error logs on http callback error

Fix wait timing
2025-08-11 21:37:01 +08:00
Karl Seguin
3555680335 Working navigation events (clicks, form submission) 2025-08-11 21:37:01 +08:00
Karl Seguin
f65a39a3e3 Re-enable telemetry
Start work on supporting navigation events (clicks, form submission).
2025-08-11 21:37:00 +08:00
Karl Seguin
94e8964f69 add custom scheduler 2025-08-11 21:37:00 +08:00
Karl Seguin
254d22e2cc don't poll libcurl if we have no running transfers 2025-08-11 21:37:00 +08:00
Karl Seguin
54ab1326e5 Switch XHR to new http client
get puppeteer/cdp.js working again

make test are all passing
2025-08-11 21:37:00 +08:00
Karl Seguin
b0fe5d60ab Initial work on integrating libcurl and making all http nonblocking 2025-08-11 21:36:56 +08:00
Karl Seguin
4b1eb2794f Add ShadowRoot get/set innerHTML
Adds event.composedPath()

This depends on https://github.com/lightpanda-io/libdom/pull/34
2025-08-11 16:32:08 +08:00
Karl Seguin
6a2dd1111c Merge pull request #928 from lightpanda-io/lit_compat
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
lit compatibility
2025-08-11 08:30:34 +08:00
Karl Seguin
f5da89b50b lit compatibility
Aims to improve compatibility for the lit framework (e.g. what Reddit is using).

1 - Adds support for adoptedStyleSheets to the Document and ShadowRoot
2 - Adds mock support for replace and replaceSync to the CSSStyleSheet
3 - Optionally include shadowroot in dump
4 - Special-case setting innerHTML on a TemplateElement
2025-08-09 07:43:27 +08:00
Karl Seguin
bede244598 Merge pull request #934 from lightpanda-io/with-base
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
add a --with_base option to fetch
2025-08-09 07:37:32 +08:00
Karl Seguin
4df48c9695 Merge pull request #935 from lightpanda-io/mouse-event-log
use internal logger instead of std.log
2025-08-09 07:36:35 +08:00
Karl Seguin
05ad77ffbe Merge pull request #936 from lightpanda-io/runtime-empty-array
Fix crashes with empty array
2025-08-09 07:36:09 +08:00
Pierre Tachoire
dc23a74e7b add <base> in the DOM tree 2025-08-08 18:34:14 +02:00
Pierre Tachoire
f463cb16da runtime: handle empty array parameter 2025-08-08 17:50:18 +02:00
Pierre Tachoire
b785884cd8 runtime: fix returning an empty array crash 2025-08-08 17:26:39 +02:00
Pierre Tachoire
f09caec09a use internal logger instead of std.log 2025-08-08 16:21:23 +02:00
Pierre Tachoire
5e30a3997e typo fix
Co-authored-by: Karl Seguin <karlseguin@users.noreply.github.com>
2025-08-08 16:17:52 +02:00
Karl Seguin
8552a5797c Merge pull request #933 from lightpanda-io/document_fragment_get_element_by_id
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Add DocumentFragment getElementById
2025-08-08 22:06:02 +08:00
Karl Seguin
a0d528981e Merge pull request #932 from lightpanda-io/libdom_element_attributes
Updates libdom
2025-08-08 22:05:42 +08:00
Pierre Tachoire
7ffdee0d7f node: add baseURI getter 2025-08-08 15:21:20 +02:00
Pierre Tachoire
3d0928a449 add a --with_base option to fetch
with_base option adds a <base> tag to the dump for better offline preview.
2025-08-08 15:18:11 +02:00
Pierre Tachoire
ea1bca05c7 fix no-script default value 2025-08-08 14:30:41 +02:00
Karl Seguin
df292a2103 Add DocumentFragment getElementById 2025-08-08 17:05:22 +08:00
Karl Seguin
7f2c360f33 Updates libdom
libdom's parsing is now less strict with respect to attribute names. See:
https://github.com/lightpanda-io/libdom/pull/33

However, the attribute name in setAttribute has stricter validation applied to
it, which we now handle directly.
2025-08-08 16:22:25 +08:00
Karl Seguin
fbd40a6514 Merge pull request #931 from lightpanda-io/element_getAttributeNames
add element.getAttributeNames()
2025-08-08 15:13:03 +08:00
Karl Seguin
9dd02ec67d add element.getAttributeNames() 2025-08-08 10:23:45 +08:00
Karl Seguin
8e55082d4e Merge pull request #929 from lightpanda-io/fix-webcomponents
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
webcomponent must be cast as HTMLElement
2025-08-07 19:04:31 +08:00
Pierre Tachoire
29378c57ea node: cast the libdom document depending its type 2025-08-07 12:54:18 +02:00
Pierre Tachoire
16c74cf3b4 element: fix toInterface for webcomponents
The webcomponents tag can be anything. But we must return them as
HTMLElement for HTML documents.
2025-08-07 12:47:02 +02:00
Pierre Tachoire
b199925f91 iframe: move HTMLIFrameElement in its own file 2025-08-07 10:35:04 +02:00
Pierre Tachoire
28397bf9d0 window: frame is obsolete, ignore them from frames list 2025-08-07 10:04:42 +02:00
Pierre Tachoire
1b7abf9972 window: partial implementation for indexed_get 2025-08-06 18:29:26 +02:00
Pierre Tachoire
b98bdeaae7 window.length dynamically 2025-08-06 18:29:25 +02:00
Pierre Tachoire
221274b473 first change to start support frames 2025-08-06 16:19:52 +02:00
Karl Seguin
cc6d443113 Merge pull request #926 from lightpanda-io/noscript_exclude_preload
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
When --noscript is specified, also exclude <link rel=preload as=script>
2025-08-06 19:45:52 +08:00
Karl Seguin
b3c81c9e55 When --noscript is specified, also exclude <link rel=preload as=script> 2025-08-06 18:04:57 +08:00
Pierre Tachoire
84d07f3f18 Merge pull request #919 from lightpanda-io/html_element-and-element
Create HTMLElement instead of pure Element
2025-08-06 10:55:46 +02:00
Pierre Tachoire
0fee2bbf28 upgrade netsurf/libdom 2025-08-06 10:42:54 +02:00
Pierre Tachoire
ea38845622 detect HTML document 2025-08-06 10:42:54 +02:00
Pierre Tachoire
81a0e95916 netsurf: remove inline for documentCreateHTMLElement* 2025-08-06 10:42:54 +02:00
Pierre Tachoire
2a9feee476 init default HTML doc and Image w/ HTML Elements 2025-08-06 10:42:53 +02:00
Pierre Tachoire
c38c1fa93a remove netsurf.elementHTMLGetTagType 2025-08-06 10:42:53 +02:00
Pierre Tachoire
8d7c35d034 refacto and use Element.toInterface 2025-08-06 10:42:53 +02:00
Pierre Tachoire
425f62607b add Tag.fromString to get element tag from tagname 2025-08-06 10:42:52 +02:00
Pierre Tachoire
c1752ae5eb document.documentElement returns a *parser.Element
For XML documents, the documentElement could be another element than
HTMLElement. So we don't want to pass to through the toInterface.
2025-08-06 10:42:52 +02:00
Pierre Tachoire
d61e91b949 Merge pull request #924 from lightpanda-io/fix-null-owner
node: check owner null before using it
2025-08-06 10:38:59 +02:00
Pierre Tachoire
090c0f8857 node: check owner null before using it 2025-08-05 18:23:41 +02:00
Pierre Tachoire
c453dd2b3c Merge pull request #923 from lightpanda-io/doc-owner-next
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
node: don't call owner twice in _insertBefore
2025-08-05 15:59:16 +02:00
Pierre Tachoire
b2b2e97edc zig fmt 2025-08-05 14:47:25 +02:00
Pierre Tachoire
bd9e4dbc79 node: don't call owner twice in _insertBefore
When the ref_node_ is null, call directly _appendChild w/o fixing the
node's owner.
2025-08-05 14:45:25 +02:00
Pierre Tachoire
0c19070800 Merge pull request #920 from SrikanthKumarC/main
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Fix: Properly handle node ownership when using appendChild and insertBefore
2025-08-05 14:45:18 +02:00
Karl Seguin
07e37b257f Merge pull request #921 from lightpanda-io/cdp-agent-commt
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
cdp: add comment for CDP_USER_AGENT
2025-08-05 07:38:43 +08:00
Srikanth
0a5f060d1b add tests and simplify walker traversal 2025-08-04 23:53:29 +05:30
muki
6fcfcb630d Merge pull request #916 from lightpanda-io/allow-nullable-listener
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
2025-08-04 06:15:39 -07:00
Pierre Tachoire
7aff90aec7 cdp: add comment for CDP_USER_AGENT 2025-08-04 14:40:44 +02:00
Srikanth
f1e513443b refactor: use walker to traverse the nodes 2025-08-04 14:27:39 +05:30
Srikanth
c533b10e19 fix: traverse all children correctly 2025-08-04 13:00:03 +05:30
Srikanth
b4014e8c24 Fix: Properly handle node ownership when using appendChild and insertBefore 2025-08-03 20:27:32 +05:30
sjorsdonkers
478f3a5308 simplify statusText
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
2025-07-29 09:53:54 +02:00
sjorsdonkers
b98edf3d76 CDP response statusText 2025-07-29 09:53:54 +02:00
Karl Seguin
02fe46de58 Merge pull request #915 from lightpanda-io/css_tweaks
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Tweak cssom
2025-07-24 19:28:21 +08:00
Karl Seguin
ab2fd0ad36 Merge pull request #911 from lightpanda-io/select_options
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Implement select.options
2025-07-24 07:48:12 +08:00
Muki Kiboigo
88655d877b handle null event listener 2025-07-23 06:53:44 -07:00
Karl Seguin
6e94affea6 Update src/browser/dom/html_collection.zig
Co-authored-by: Sjors <72333389+sjorsdonkers@users.noreply.github.com>
2025-07-23 21:34:42 +08:00
Karl Seguin
f7f382275a Merge pull request #908 from lightpanda-io/guard_against_double_script_execution
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Prevent double-execution of script tags.
2025-07-23 21:33:52 +08:00
Karl Seguin
23f3bf43c2 Merge pull request #910 from lightpanda-io/performance_getEntriesByX
Add placeholder performance getEntriesByName and Type
2025-07-23 21:19:01 +08:00
Karl Seguin
8a0c4909b9 fix file casing 2025-07-23 16:06:07 +08:00
Karl Seguin
2aeaf02d05 Tweak cssom
The only functionality change is adding a `named_set` to the CSSStyleDeclaration
so that styles can be set (`named_get` was already defined)

Combine the StringHashMapUnmanaged + ArrayListUnmanaged into a single
StringArrayHashMapUnmanaged.

Use file structs, because @import("css_style_declaration.zig").CSSStyleDeclaration
is a bit tedious.

Various micro-optimization around parsing CSS, e.g. ascii.eqlIgnoreCase in loops
replaced by 1 lowercase + N*mem.eql.
2025-07-23 15:34:32 +08:00
Karl Seguin
f4a6e34713 update libdom 2025-07-23 07:51:33 +08:00
Karl Seguin
3eb85da02c Implement select.options
Add HTMLOptionsCollection and enhance HTMLOptionElement API.

Amazon.
2025-07-23 07:39:53 +08:00
Karl Seguin
6533456472 Add placeholder performance getEntriesByName and Type 2025-07-22 08:05:52 +08:00
Karl Seguin
7969e047c7 Merge pull request #909 from lightpanda-io/zig_fmt
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig fmt
2025-07-22 08:05:50 +08:00
Karl Seguin
f0d6d9d177 zig fmt 2025-07-22 07:57:17 +08:00
Karl Seguin
ca574df3be Prevent double-execution of script tags.
Depends on https://github.com/lightpanda-io/libdom/pull/31
2025-07-22 07:54:39 +08:00
Karl Seguin
0b793d82fe Merge pull request #907 from lightpanda-io/array_buffer_as_u8_slice
Map ArrayBuffer and ArrayBufferView to u8.
2025-07-22 07:13:57 +08:00
Karl Seguin
f6d51462eb Merge pull request #906 from lightpanda-io/text_decoder
Add TextDecoder (utf8 support only)
2025-07-22 07:13:21 +08:00
Karl Seguin
5bdacbab61 Merge pull request #903 from lightpanda-io/MessageChannel
Add MessageChannel
2025-07-22 07:13:07 +08:00
Karl Seguin
e239cc962b Merge pull request #904 from lightpanda-io/minor-refactor-prep-for-tls
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Minor refactor prep for tls
2025-07-21 20:55:35 +08:00
sjorsdonkers
6ebd4fcf5b fix unencrypted keepalive 2025-07-21 14:28:53 +02:00
Karl Seguin
ef427fa966 Map ArrayBuffer and ArrayBufferView to u8.
Depends on https://github.com/lightpanda-io/zig-v8-fork/pull/86

Built ontop of https://github.com/lightpanda-io/browser/pull/906 just because
this is the feature that uses it.
2025-07-21 19:46:57 +08:00
Karl Seguin
f4383a11d7 Merge pull request #905 from lightpanda-io/scheme_only_url
Allow scheme-only URLs
2025-07-21 19:36:24 +08:00
Karl Seguin
77b6377473 Add TextDecoder (utf8 support only) 2025-07-21 16:29:42 +08:00
Karl Seguin
7bf3cf999f Allow scheme-only URLs
new URL('sveltekit-internal://') is valid. Used by amazon.
2025-07-21 15:46:23 +08:00
sjorsdonkers
4ab611de0c minor refactor prep for tls 2025-07-21 09:30:22 +02:00
Karl Seguin
d7745a418f Merge pull request #902 from lightpanda-io/window_DOMContentLoaded
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Trigger the DOMContentLoaded on the Window
2025-07-19 08:51:12 +08:00
Karl Seguin
058a5a43ba Add MessageChannel 2025-07-18 16:47:04 +08:00
Karl Seguin
878dbd81b1 Merge pull request #901 from lightpanda-io/url_stitch
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Rework URL.stitch, handle ../ (for yahoo)
2025-07-17 21:44:24 +08:00
Karl Seguin
3c64ed1eb2 Merge pull request #899 from lightpanda-io/element_remove
Add element.remove()  (needed by reddit)
2025-07-17 21:44:08 +08:00
Karl Seguin
ee50f1238c Trigger the DOMContentLoaded on the Window
This is hacky, but it's inspired by how NetSurf does it. While the Window isn't
the parent of the Document, many events should bubble from the Document to the
Window. libdom simply doesn't handle this (it has no concept of a Window, and
the Document has no parent).

We potentially need to do this for multiple event types (NetSurf only does it
for the 'load' event as far as I can tell). It would be nice to find a generic
way to do this...maybe intercept any addEventListener on the body and
registering special events on the Window? For now, `DOMContentLoaded` is the
blocking (for finance.yahoo.com) and we can see if this is really an issue for
other event types.
2025-07-17 21:38:54 +08:00
Karl Seguin
1af2513fc0 zig fmt 2025-07-17 20:52:15 +08:00
Karl Seguin
9c0d26bc84 add note about incomplete removal 2025-07-17 20:51:05 +08:00
Karl Seguin
4d9053a83b Update src/url.zig
Co-authored-by: Sjors <72333389+sjorsdonkers@users.noreply.github.com>
2025-07-17 20:45:42 +08:00
Karl Seguin
3f7e98c277 Update src/url.zig
Co-authored-by: Sjors <72333389+sjorsdonkers@users.noreply.github.com>
2025-07-17 20:45:35 +08:00
Karl Seguin
aebc877e7b Merge pull request #900 from lightpanda-io/getDocument_depth
support `depth` parameter for DOM.getDocument
2025-07-17 20:44:58 +08:00
Karl Seguin
eef5f3fec2 support null params to CDP DOM.getDocument 2025-07-17 19:05:17 +08:00
Karl Seguin
16a1677fde Rework URL.stitch, handle ../ (for yahoo)
Also handle ./ anywhere in the path.
2025-07-17 17:54:00 +08:00
Karl Seguin
f199816fcd support depth parameter for DOM.getDocument 2025-07-17 14:17:33 +08:00
Karl Seguin
5e74e17b41 Merge pull request #888 from lightpanda-io/cdp_dom_requestChildNodes
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Add support for CDP's DOM.requestChildNodes
2025-07-17 10:48:24 +08:00
Karl Seguin
98b041e84a requestChildNode cannot have a depth of 0 2025-07-17 10:36:20 +08:00
Karl Seguin
bba9c8f652 Add element.remove() (needed by reddit) 2025-07-17 10:00:38 +08:00
Karl Seguin
1a05fe6ae1 Merge pull request #887 from lightpanda-io/go_rod
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Noop CDP methods that go-rod requires
2025-07-16 20:01:03 +08:00
sjorsdonkers
16fcbf66ee http_proxy_before ?? comment 2025-07-16 11:20:00 +02:00
Karl Seguin
b7fd4e90e2 Merge pull request #894 from lightpanda-io/HTMLStyleElement_get_sheet
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
add HTMLStyleElement.get_sheet
2025-07-16 10:34:37 +08:00
Karl Seguin
b6341c10cc Merge pull request #892 from lightpanda-io/set_timeout_params
Support params for setTimeout and setInterval
2025-07-16 08:17:11 +08:00
Karl Seguin
08487b0fcc Merge pull request #891 from lightpanda-io/reattach_shadowroot
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Add childElementCount and children to DocumentFragment
2025-07-15 23:36:26 +08:00
muki
b084dde22a Merge pull request #872 from lightpanda-io/dynamic-import 2025-07-15 08:31:52 -07:00
Karl Seguin
5229a7c997 Merge pull request #897 from lightpanda-io/animate
Add Element.animate and Animation
2025-07-15 23:09:08 +08:00
Karl Seguin
e56c56e2fe Merge pull request #895 from lightpanda-io/performance_clear
dummy performance clearMarks and clearMeasures
2025-07-15 21:40:31 +08:00
Karl Seguin
7374f956cf Merge pull request #896 from lightpanda-io/dont_send_after_disconnect
Don't queue data to send after we've initiated a disconnect of the cl…
2025-07-15 21:29:01 +08:00
Muki Kiboigo
287df42994 log module specifier on dynamic import stages 2025-07-15 06:22:52 -07:00
Muki Kiboigo
06e514cc2e use resource_str for stitching url 2025-07-15 06:22:52 -07:00
Muki Kiboigo
dffd8b5fec use module() for dynamic imports 2025-07-15 06:22:52 -07:00
Muki Kiboigo
2a87337875 dynamicImportCallback in JsContext 2025-07-15 06:22:50 -07:00
Karl Seguin
a74f79118f Merge pull request #893 from lightpanda-io/dump_noscript
Add a --noscript option to "improve" --dump
2025-07-15 21:22:33 +08:00
Muki Kiboigo
a13ed0bec3 add dynamic import callback to isolate 2025-07-15 06:22:13 -07:00
Karl Seguin
f8ca45f0f2 Add Element.animate and Animation
These are dummy implementations, but they do expose the ready and finished
promise, and do resolve the finished promise, so it should unblock basic cases.
2025-07-15 18:58:58 +08:00
Karl Seguin
4bf92a34f6 Don't queue data to send after we've initiated a disconnect of the client 2025-07-15 17:58:57 +08:00
Karl Seguin
4f1c84004a dummy performance clearMarks and clearMeasures 2025-07-15 12:11:28 +08:00
Karl Seguin
1bd430598d add HTMLStyleElement.get_sheet 2025-07-15 10:59:59 +08:00
Karl Seguin
3bc654bf97 Merge pull request #890 from lightpanda-io/xhr_cant_block_sync_requests
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Always make sure we have 1 free http state available for synchronous …
2025-07-15 10:08:54 +08:00
Karl Seguin
3906acb83d fix test 2025-07-14 18:42:25 +08:00
Karl Seguin
cfd62ac137 Add a --noscript option to "improve" --dump
Currently, fetch --dump includes <script> tag (either inline or with src). I
don't know what use-case this is the desired behavior. Excluding them, via the
new --noscript option has benefit that if you --dump --noscript and open the
resulting page in the browser, you don't re-execute JavaScript, which is
likely to break the page.

For example, opening a --dump of github makes it look like the page is broken
because it re-executes JavaScript that isn't meant to be re-executed.

Similarly, opening a --dump in a browser might execute JavaScript that
lightpanda browser failed to execute, making it looks like it worked better
than it did.
2025-07-14 18:24:36 +08:00
Karl Seguin
cd540dfae9 Support params for setTimeout and setInterval 2025-07-14 17:42:53 +08:00
Karl Seguin
74ad9ec8bf Add childElementCount and children to DocumentFragment
Also, when shadowRoot is re-attached to an element, clear all existing children
(like we're supposed to)
2025-07-14 17:01:11 +08:00
Karl Seguin
4f8a3fe5b9 Always make sure we have 1 free http state available for synchronous requests
If it wasn't for the fact that the HTTP client is likely going to see a major
refactor, it would definitely be time to create a specific state instance for
synchronous requests.
2025-07-14 16:41:26 +08:00
Karl Seguin
09ca0e6ef0 Add support for CDP's DOM.requestChildNodes
https://github.com/lightpanda-io/browser/issues/866
2025-07-14 15:13:01 +08:00
Karl Seguin
fae2b5acfa Noop CDP methods that go-rod requires
go-rod appears to stop processing when it receives an error, such as
UnknownMethod. Added placeholder handlers for Network.setUserAgentOverride and
Page.stopLoading.

Setting a custom user agent is something still being discussed, so no-oping it
seems reasonable. And, due to the currently synchronous nature of the initial
page load, no-oping stopLoading also seems reasonable.

https://github.com/lightpanda-io/browser/issues/867
2025-07-14 11:21:02 +08:00
Karl Seguin
d35a3eab6c Merge pull request #880 from lightpanda-io/webcomponents
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Add basic ShadowRoot implementation, polyfill webcomponents
2025-07-14 11:10:40 +08:00
Karl Seguin
7f7f47497a Merge pull request #886 from lightpanda-io/scriptcompiler-compile
use ScriptCompiler to compile script
2025-07-14 11:07:29 +08:00
Karl Seguin
eb14ac3741 update build.zig.zon v8 version 2025-07-14 11:00:01 +08:00
Karl Seguin
22334faba3 update zig-v8-fork lib version 2025-07-14 10:51:27 +08:00
Karl Seguin
d08fd297e8 Merge pull request #881 from lightpanda-io/window_queueMicrotask
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
add window.queueMicrotask
2025-07-13 09:03:57 +08:00
Pierre Tachoire
0dd664bfbf use ScriptCompiler to compile script 2025-07-12 12:09:16 -07:00
Karl Seguin
1602932d72 Add a "pre" polyfill
This is always run, but only the full webcomponents polyfill, it's very
small and isn't intrusive. This introduces a layer of indirection so that,
if the full polyfill is loaded, its monkeypatched constructor will be called
2025-07-12 19:49:19 +08:00
Karl Seguin
98c8b7d2b0 Merge pull request #875 from lightpanda-io/async_forward_proxy_to_tls
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Fix async https requests over a http forward proxy
2025-07-11 17:53:53 +08:00
Karl Seguin
b9ae24c42d add window.queueMicrotask 2025-07-11 17:46:39 +08:00
Karl Seguin
b387fd2bd4 Update src/http/client.zig
Co-authored-by: Sjors <72333389+sjorsdonkers@users.noreply.github.com>
2025-07-11 17:38:31 +08:00
Karl Seguin
818f4540fd Add basic ShadowRoot implementation, polyfill webcomponents 2025-07-11 17:32:01 +08:00
sjorsdonkers
49a97dbb66 fix callback crash with Node.Union
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
2025-07-11 10:05:44 +02:00
sjorsdonkers
a8b72c1d5f Separate NodeIterator impl, fix _filter 2025-07-11 10:05:44 +02:00
sjorsdonkers
765b8dc97b NodeIterator 2025-07-11 10:05:44 +02:00
sjorsdonkers
5123697afe EventTarget internal type for all 2025-07-11 09:55:16 +02:00
sjorsdonkers
2a2a9d7941 EventTarget InternalType 2025-07-11 09:55:16 +02:00
sjorsdonkers
2873aa5f81 EventTarget constructor 2025-07-11 09:55:16 +02:00
Karl Seguin
795c925ba1 Revert "Update src/http/client.zig"
This reverts commit 4a12d045e4.
2025-07-11 09:49:40 +08:00
Karl Seguin
d6ace3f695 Merge pull request #863 from lightpanda-io/innerHTML_head
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Setting innerHTML now captures head elements
2025-07-11 08:03:14 +08:00
Karl Seguin
dd04759de7 Merge pull request #869 from lightpanda-io/performance_observer
more PerformnaceObserver placeholders
2025-07-11 08:03:01 +08:00
Pierre Tachoire
10fbde84ba Merge pull request #879 from lightpanda-io/css-parser-error
Fix parser identifier with escaped string
2025-07-10 16:10:14 -07:00
Pierre Tachoire
2b5652e1e4 wip 2025-07-10 16:01:36 -07:00
Pierre Tachoire
18796ae44e css: allow escaped first char in identifier name 2025-07-10 15:44:04 -07:00
Pierre Tachoire
a67692dc29 Merge pull request #877 from lightpanda-io/visible-pseudoclass
Visible Psuedoclass
2025-07-10 14:18:10 -07:00
Muki Kiboigo
1efd756a55 add visible pseudoclass 2025-07-10 12:40:44 -07:00
Pierre Tachoire
29671acdb6 Merge pull request #847 from lightpanda-io/name-property-handler
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
enable conditionnal loading for polyfill
2025-07-10 09:18:29 -07:00
Karl Seguin
e82240a60e Setting innerHTML now captures head elements
I couldn't find where the behavior is described. AND, browsers seem to behave
differently depending on the state of the page (blank document vs actual page).

Still, some sites use innerHTML to load <script> tags, and, in libdom at least,
these are created in the implicit head. We cannot just copy the body nodes. To
keep it simple, I now copy all head and body elements.
2025-07-10 22:19:53 +08:00
Karl Seguin
72083c8614 Merge pull request #868 from lightpanda-io/element_hasAttributes_fix
Fix element.hasAttributes
2025-07-10 21:46:33 +08:00
Karl Seguin
8c2c1e534c Merge pull request #865 from lightpanda-io/document_domain
Fix document.domain
2025-07-10 21:46:15 +08:00
Karl Seguin
bfc01d957b Merge pull request #874 from lightpanda-io/document_styleSheets
add dummy document.get_styleSheets
2025-07-10 21:46:00 +08:00
Karl Seguin
4a12d045e4 Update src/http/client.zig
Co-authored-by: Sjors <72333389+sjorsdonkers@users.noreply.github.com>
2025-07-10 17:10:58 +08:00
Karl Seguin
2d78b2c219 add TODO note for dummy implementation 2025-07-10 17:03:51 +08:00
Karl Seguin
3049bb0b9f Fix async https requests over a http forward proxy
XHR requests to https (which is most XHR requests) currently don't work with
the implementation proxy because of this.
2025-07-10 16:27:09 +08:00
Karl Seguin
34ab8152fb add dummy document.get_styleSheets 2025-07-10 13:45:49 +08:00
Karl Seguin
fb58c50fb7 Merge pull request #870 from lightpanda-io/popover_open_pseudo_selector
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Accept popover-over pseudo selector
2025-07-10 08:27:43 +08:00
Pierre Tachoire
955f917015 Merge pull request #873 from lightpanda-io/macos-build
ci: fix macos version for building
2025-07-09 15:35:09 -07:00
Pierre Tachoire
12c7df98e4 ci: fix macos version for building 2025-07-09 15:26:07 -07:00
Pierre Tachoire
889c29a163 Merge pull request #871 from lightpanda-io/ws-http-max
ws: increase max http message from 2kb to 4kb
2025-07-09 15:13:50 -07:00
Pierre Tachoire
886c1370e7 ws: increase max http message from 2kb to 4kb 2025-07-09 15:02:40 -07:00
Karl Seguin
febcc0a673 Merge pull request #864 from lightpanda-io/link_href
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
add HTMLElementLink get/set href
2025-07-09 18:48:17 +08:00
Karl Seguin
98cad6bf8d Accept popover-over pseudo selector
Optimize pseudo-selector parsing. Make comparison case insensitive, bucket
comparisons by length, and process input as integers.
2025-07-09 18:45:28 +08:00
Karl Seguin
7e5daedc8c more PerformnaceObserver placeholders 2025-07-09 18:10:23 +08:00
Karl Seguin
da3fe6f7ea fix test 2025-07-09 17:41:05 +08:00
Karl Seguin
f612ce262f Update src/browser/html/elements.zig
Co-authored-by: Sjors <72333389+sjorsdonkers@users.noreply.github.com>
2025-07-09 16:16:45 +08:00
Karl Seguin
24ccfca279 Fix element.hasAttributes
libdom's hasAttributes is based on the type. Elements, according to libdom,
always have attributes, thus hasAttributes always return true, even when the
element in question has no attribute. Change our _hasAttributes to only return
true if the attribute count > 0.
2025-07-09 16:14:53 +08:00
Karl Seguin
34b3c3982b Fix document.domain
Currently seems to always return null. Doesn't seem to be a way in libdom to
change this. The property is deprecated, and MDN recommends using location.host
instead, so change document.get_domain to wrap location.host.
2025-07-09 14:29:05 +08:00
Karl Seguin
7f732c94da add HTMLElementLink get/set href 2025-07-09 13:28:32 +08:00
Karl Seguin
bdc49a65aa Merge pull request #859 from lightpanda-io/document_fragment_query_selector
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Add querySelect and querySelectorAll to DocumentFragment
2025-07-09 10:25:35 +08:00
Karl Seguin
73d82dd0ba I guess we can't use the call_arena for querySelectorAll 2025-07-09 10:19:16 +08:00
Karl Seguin
dfa4403c8a arena -> call_arena for querySelectorAll 2025-07-09 10:11:26 +08:00
Karl Seguin
b8f3b19499 Merge pull request #857 from lightpanda-io/improved_native_proto
Improve prototype resolution for native types
2025-07-09 10:01:38 +08:00
Karl Seguin
448718d112 Merge pull request #858 from lightpanda-io/callback_with_new_this
Allow JS Callback to be called with a previously-unseen this.
2025-07-09 09:34:14 +08:00
Pierre Tachoire
6de55df4bc Merge pull request #856 from lightpanda-io/resize_observer
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
add dummy ResizeObserver
2025-07-08 15:50:08 -07:00
Pierre Tachoire
189fe26667 Merge pull request #862 from lightpanda-io/macos-14
ci: use macos-14 for nightly builds
2025-07-08 15:49:47 -07:00
Pierre Tachoire
7230884116 ci: use macos-14 for nightly builds 2025-07-08 08:27:45 -07:00
Karl Seguin
d7fba81f8f Add querySelect and querySelectorAll to DocumentFragment 2025-07-08 19:24:35 +08:00
Karl Seguin
29ac13185c Allow JS Callback to be called with a previously-unseen this. 2025-07-08 19:17:59 +08:00
Karl Seguin
3a49ee83ce Improve prototype resolution for native types
Prototype resolution of Zig types previously had 2 limitations (bug?). The first
was that the Zig prototype chain could only be 1 deep. You couldn't do A->B->C
where each of those was a Zig type (but you could do A->B->C->D->E ... so long
as every other type was a C opaque value).

The other limitation was that Zig prototypes only worked when the nested field
was directly embedded in the struct (i.e. not a pointer). So you could do:

```zig
const X = struct {
   proto: XParent,
};
```

But not:

```zig
const X = struct {
   proto: *XParent,
};
```

This addresses both limitations. The first issue is solved by keeping track
of the cumulative offset
2025-07-08 18:37:24 +08:00
Karl Seguin
95cbbc3b45 add dummy ResizeObserver 2025-07-08 18:35:25 +08:00
Karl Seguin
2a5c7d139f Merge pull request #855 from lightpanda-io/zig_fmt
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig fmt
2025-07-08 18:34:14 +08:00
Karl Seguin
b74863873b zig fmt 2025-07-08 18:28:21 +08:00
Karl Seguin
7b46fe9cc8 Merge pull request #848 from lightpanda-io/fix_insecure_forward_proxy
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Fix non-tls forward-proxy
2025-07-08 09:52:23 +08:00
Karl Seguin
afc8c69a82 Merge pull request #854 from lightpanda-io/remove_debug_log
remove std.debug.print
2025-07-08 09:39:19 +08:00
Karl Seguin
38bbad6e88 Revert "fix secure connection logic"
This reverts commit b6132f2497.
2025-07-08 09:33:53 +08:00
Karl Seguin
1df47fd415 remove std.debug.print 2025-07-08 09:33:19 +08:00
Pierre Tachoire
faf21c5fff Merge pull request #853 from lightpanda-io/typo-fix
typo fix
2025-07-07 17:24:28 -07:00
Karl Seguin
2aee580795 Merge pull request #849 from lightpanda-io/mutation_observer_loop
Rework MutationObserver callback.
2025-07-08 08:15:02 +08:00
Pierre Tachoire
404c027546 typo fix 2025-07-07 17:14:52 -07:00
Karl Seguin
04e59c6df2 Merge pull request #850 from lightpanda-io/set_attribute_value
Attribute.set_value uses element, if possible
2025-07-08 08:14:52 +08:00
Karl Seguin
835042b794 Merge pull request #851 from lightpanda-io/add_event_listener_signal
Add support for the signal option of addEventListener
2025-07-08 08:14:38 +08:00
Pierre Tachoire
907490e266 Merge pull request #852 from lightpanda-io/katie-lpd-patch-1
Some checks failed
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Update README.md
2025-07-07 17:01:09 -07:00
Pierre Tachoire
80fe167646 Update README.md 2025-07-07 17:00:54 -07:00
katie-lpd
d30631f991 Apply suggestions from code review
Co-authored-by: Pierre Tachoire <pierre@lightpanda.io>
2025-07-07 16:59:07 -07:00
katie-lpd
8956ab85f9 Update README.md 2025-07-07 16:50:32 -07:00
Pierre Tachoire
2cdc9e9f5f cdp: use a polyfill loader per isolate 2025-07-07 16:31:54 -07:00
Pierre Tachoire
13c623755c js: remove existing unknown property debug
Because it will be displayed only if the property is non-native.
So if your property is set in pureJS, you will still have the log...
2025-07-07 16:31:54 -07:00
Pierre Tachoire
bdfceec520 refacto a bit the missing callback into polyfill
Add a debug global unknown property
2025-07-07 16:31:53 -07:00
Pierre Tachoire
941dace7f9 enable conditionnal loading for polyfill 2025-07-07 16:31:53 -07:00
Karl Seguin
07693e54af Add support for the signal option of addEventListener 2025-07-07 20:56:19 +08:00
Karl Seguin
b6132f2497 fix secure connection logic 2025-07-07 19:56:21 +08:00
Karl Seguin
b3fe3d02c9 Attribute.set_value uses element, if possible
Only when setAttribute is called directly on the element, does libdom raise
a `DOMAttrModified` event (which MutationObserver uses).

From what I can tell, libdom's element set attribute _does_ rely on the
underlying attribute set value, so the behavior should be pretty close, it just
does extra things on top of that.
2025-07-07 19:47:17 +08:00
Karl Seguin
e880b18bb1 Rework MutationObserver callback.
Previously, MutationObserver callbacks where called using the `jsCallScopeEnd`
mechanism. This was slow and resulted in records split in a way that callers
might not expect. `jsCallScopeEnd` has been removed.

The new approach uses the loop.timeout mechanism, much like a window.setTimeout
and only registers a timeout when events have been handled. It should perform
much better.

Exactly how MutationRecords are supposed to be grouped is still a mystery to me.
This new grouping is still wrong in many cases (according to WPT), but appears
slightly less wrong; I'm pretty hopeful clients don't really have hard-coded
expectations for this though.

Also implement the attributeFilter option of MutationObserver. (Github)
2025-07-07 19:29:10 +08:00
Karl Seguin
74a299eef7 Fix non-tls forward-proxy 2025-07-07 11:03:04 +08:00
Karl Seguin
300428ddfb Merge pull request #840 from lightpanda-io/xhr_readystatechange
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Add readystate change event to XHR
2025-07-06 08:59:19 +08:00
Pierre Tachoire
1c27f8251e Merge pull request #846 from lightpanda-io/e2e-draft
ci: don't run 2e2 on draft
2025-07-05 16:46:04 -07:00
Pierre Tachoire
92badd3722 ci: don't run 2e2 on draft 2025-07-05 14:22:21 -07:00
Karl Seguin
8a80f0b3dd Merge pull request #843 from lightpanda-io/empty_anchor_fix
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
An empty anchor should return empty strings for its getters
2025-07-04 23:43:10 +08:00
Karl Seguin
fcc74b63d3 correct comment 2025-07-04 23:17:48 +08:00
Karl Seguin
d7155e6662 An empty anchor should return empty strings for its getters
document.createElement('a').host  or .href or .. should return an empty string.

However, URL.constructor(document.createElement('a')) should fail.

Because HTMLAnchorElement uses URL.constructor, we have the wrong behavior.

This adds a guard for an empty anchor. This might not cover all of the cases
which are valid for an anchor but invalid for a URL.constructor, but it's
the most common.
2025-07-04 19:23:19 +08:00
Karl Seguin
42c3841639 Merge pull request #842 from lightpanda-io/fix_elementFromPoint_crash
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Rely on js.zig for float->int translation
2025-07-04 19:12:46 +08:00
Karl Seguin
c331713401 set correct state on xhr.abort and send correct events 2025-07-04 19:12:26 +08:00
Karl Seguin
002d9c1747 Merge pull request #841 from lightpanda-io/scroll_events
make window.scrollTo triggers scroll and scrollend events
2025-07-04 19:00:28 +08:00
Karl Seguin
2885ceceb1 document use of i32 2025-07-04 18:55:14 +08:00
sjorsdonkers
22a644ba01 rename tls_in_tls to tlsproxy 2025-07-04 10:00:22 +02:00
sjorsdonkers
bab120a75d secure changes 2025-07-04 10:00:22 +02:00
Francis Bouvier
7a07c82f06 https-proxy: update upstream tls.zig 2025-07-04 10:00:22 +02:00
sjorsdonkers
e881d2f6cf tls proxy tweaks 2025-07-04 10:00:22 +02:00
Francis Bouvier
c8d003a08f https-proxy: update tls.zig 2025-07-04 10:00:22 +02:00
Francis Bouvier
e2cc404571 Handle TLS proxy, both for HTTP and HTTPS (tls in tls) endpoints 2025-07-04 10:00:22 +02:00
sjorsdonkers
be71eaae47 TLS connect proxy WIP 2025-07-04 10:00:22 +02:00
Karl Seguin
ed31a452b2 Rely on js.zig for float->int translation
Not only does this ensure compatibility with browsers, it doesn't crash when
the value is NaN of Infinity.
2025-07-04 11:34:34 +08:00
Pierre Tachoire
f51ee7f3a0 Merge pull request #829 from lightpanda-io/pumpmessageloop
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
add pump message loop calls
2025-07-03 10:11:26 -07:00
Pierre Tachoire
9d1dc97766 remove useless debug log 2025-07-03 09:49:01 -07:00
Pierre Tachoire
b78729f685 test: inject platform to the serveCDP app 2025-07-03 09:49:00 -07:00
Pierre Tachoire
44a76e59f9 run pumpmessageloop in its own loop 2025-07-03 09:49:00 -07:00
Pierre Tachoire
1504e36a68 use comptime test for platform existence 2025-07-03 09:49:00 -07:00
Pierre Tachoire
80348ef190 fix wpt tests with platform requirement 2025-07-03 09:48:59 -07:00
Pierre Tachoire
a3c14748d3 fix unit testing with platform deps requirement 2025-07-03 09:48:59 -07:00
Pierre Tachoire
3c0143af92 add runIdleTasks 2025-07-03 09:48:57 -07:00
Pierre Tachoire
22a93a9c39 add pump message loop calls 2025-07-03 09:47:50 -07:00
Karl Seguin
e8866a6431 Merge pull request #838 from lightpanda-io/improved_js_value_printing
Improve JS value printing
2025-07-04 00:30:35 +08:00
Karl Seguin
455ed79872 Remove HTTP client generic Loop parameter
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
I think we initially thought we might need different clients for different
parts of the system, each with a unique loop  (e.g. we thought telemetry might
need some isolation). But that never happened, so it's just needless now,
especially since the async connect uses the non-generic *Loop type directly.
2025-07-03 15:10:47 +02:00
Karl Seguin
3d17c531d7 make window.scrollTo triggers scroll and scrollend events 2025-07-03 19:37:07 +08:00
Karl Seguin
dfe90243d6 Add readystate change event to XHR
Deal with non-node current target crashing. Builds ontop of abort_signal, but
with the new event-target specific union, I think this will work in for all
future cases.
2025-07-03 19:32:24 +08:00
Karl Seguin
bf1db50667 Merge pull request #839 from lightpanda-io/build_time
improve build times (a little)
2025-07-03 15:34:53 +08:00
Pierre Tachoire
a2565a7c83 range: add detach function 2025-07-03 09:16:36 +02:00
Pierre Tachoire
947d01a3c0 range starts and ends with the global document by default 2025-07-03 09:16:36 +02:00
Karl Seguin
be11d82c9c improve build times (a little) 2025-07-03 13:56:01 +08:00
Karl Seguin
7a0e7fff13 Improve JS value printing
Don't error on JSON.stringify failure (likely caused by circular reference).

In debug mode, try to print [slightly] more meaningful value representation
when default serialization results in [object Object].
2025-07-03 10:35:09 +08:00
Karl Seguin
81fb71b7f7 Merge pull request #830 from lightpanda-io/SetHostInitializeImportMetaObjectCallback
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Implement ImportMeta callback
2025-07-03 09:17:47 +08:00
Karl Seguin
b10f5ec99f bump zig-v8-fork version 2025-07-02 13:38:01 +08:00
Karl Seguin
5abe7bdeef Merge pull request #831 from lightpanda-io/log_invalid_cookie_expiry
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
use logger for invalid cookie expiry
2025-07-02 10:11:20 +08:00
Karl Seguin
54be651415 Merge pull request #832 from lightpanda-io/range_selectNodeContents
range.selectNodeContents
2025-07-02 10:11:01 +08:00
Pierre Tachoire
cdbf6d7ae7 Merge pull request #834 from lightpanda-io/arm-generic
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
ARM generic
2025-07-01 12:08:30 -07:00
Pierre Tachoire
50349edf4d ci: use a generic target for arm build 2025-07-01 07:55:32 -07:00
sjorsdonkers
da307c1b40 range.selectNodeContents 2025-07-01 15:11:01 +02:00
Karl Seguin
b50b96bd1d Implement ImportMeta callback
The first time `import.meta` is called within a module, this callback is called
and we can populate it with whatever fields we want. For WebAPI, the important
field is `url`:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta

Depends on: https://github.com/lightpanda-io/zig-v8-fork/pull/80
2025-07-01 15:59:24 +08:00
Karl Seguin
92654fc5aa use logger for invalid cookie expiry 2025-07-01 12:25:25 +08:00
Pierre Tachoire
36b2de216b Merge pull request #828 from lightpanda-io/arm-compat
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Arm compat
2025-06-30 11:53:06 -07:00
Pierre Tachoire
8745c1016e ci: use cpu cortex_a72 for arm build 2025-06-30 10:58:15 -07:00
Pierre Tachoire
f5a58c1ff0 Merge pull request #826 from lightpanda-io/upgrade-v8
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Upgrade v8
2025-06-29 10:54:46 -07:00
Pierre Tachoire
d9e72049ae ci: use ubuntu 22.04 for arm64 build 2025-06-29 10:48:04 -07:00
Pierre Tachoire
927ca01161 upgrade zig v8 version 2025-06-29 10:47:03 -07:00
Pierre Tachoire
3ea8d0b01c Merge pull request #824 from lightpanda-io/dom-non-html
create a DOM tree for non-html files
2025-06-29 10:44:26 -07:00
Karl Seguin
c52d33e331 Merge pull request #822 from lightpanda-io/undefined_or
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Add UndefinedOr(T) union
2025-06-28 09:09:45 +08:00
Karl Seguin
fd36606acc change field order 2025-06-28 09:02:12 +08:00
Karl Seguin
1c6f4a79e0 Merge pull request #821 from lightpanda-io/abort_controller
Abort controller
2025-06-28 09:00:07 +08:00
Pierre Tachoire
7896d274a3 create a DOM tree for non-html files too. 2025-06-27 12:17:03 -07:00
Pierre Tachoire
6937c8ecb4 Merge pull request #823 from lightpanda-io/atob_btoa
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
add atob and btoa
2025-06-27 09:21:41 -07:00
Karl Seguin
f02b9566c5 add atob and btoa 2025-06-27 18:36:29 +08:00
Karl Seguin
c9936c2b7e Add UndefinedOr(T) union
Some apis want a value or undefined. For these, we can't use an Optional
return type, null maps to JS null. Adds an Env.UndefinedOr(T) generic
union for such return types.
2025-06-27 17:55:13 +08:00
Karl Seguin
bbd9e5e07c add AbortController API 2025-06-27 17:31:25 +08:00
sjorsdonkers
476fb7ec4e DOMException constructor
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
2025-06-27 08:13:43 +02:00
Karl Seguin
7435274be2 Merge pull request #819 from lightpanda-io/update_tls_lib
Upgrade tlz.zig to latest version
2025-06-27 13:45:28 +08:00
Karl Seguin
08d2ea6a10 abort controller 2025-06-27 13:14:35 +08:00
Karl Seguin
41b7ed6938 Upgrade tlz.zig to latest version
Was seeing pretty frequent TLS errors on reddit. I think I had the wrong max
TLS record size, but figured this was an opportunity to upgrade tls.zig, which
has seen quite a few changes since our last upgrade.

Specifically, the nonblocking TLS logic has been split into two structs: one
for handshaking, and then another to be used to encrypt/decrypt after the h
andshake is complete. The biggest impact here is with respect to keepalive,
since what we want to keepalive is the connection post-handshake, but we don't
have this object until much later.

There was also some general API changes, with respect to state and partially
encrypted/decrypted data which we must now maintain.
2025-06-27 13:14:12 +08:00
Pierre Tachoire
7a311a181b Merge pull request #820 from lightpanda-io/ci-e2e
ci: use hetzner for 2e2 regression perf
2025-06-26 22:12:34 -07:00
Pierre Tachoire
ddcb597710 ci: use hetzner for 2e2 regression perf 2025-06-26 17:37:35 -07:00
Pierre Tachoire
9c75f29875 ci: optimize 2e2 build 2025-06-26 17:04:05 -07:00
Pierre Tachoire
343f3885f7 Merge pull request #817 from lightpanda-io/script_tag_dump
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
dump script tag's text content as-is
2025-06-26 11:27:04 -07:00
Karl Seguin
ed7dfeab84 dump script tag's text content as-is 2025-06-26 12:41:22 +08:00
Karl Seguin
8de27b3674 Merge pull request #813 from lightpanda-io/crypto_get_random_values_fix
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Crypto.getRandomValues consistency
2025-06-26 11:43:39 +08:00
Karl Seguin
f56b0a5f6d Merge branch 'main' into crypto_get_random_values_fix 2025-06-26 10:25:53 +08:00
Karl Seguin
0a27e1254f Merge pull request #814 from lightpanda-io/root_module_nested_modules
Allow root modules to imported modules
2025-06-26 10:25:10 +08:00
Karl Seguin
3f9b256fcb Merge pull request #812 from lightpanda-io/identity_map_collision
We cannot have empty Zig structs mapping to JS instances
2025-06-26 10:24:09 +08:00
Karl Seguin
9ea9859150 Merge pull request #809 from lightpanda-io/html_element_dataset
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Add element.dataset API
2025-06-26 09:18:19 +08:00
Pierre Tachoire
03e3f95d2e Merge pull request #810 from lightpanda-io/proxy-authentication
basic/bearer proxy authentication
2025-06-25 17:31:47 -07:00
Pierre Tachoire
e721b0af92 Merge pull request #816 from lightpanda-io/connect_proxy
Connect proxy
2025-06-25 17:31:27 -07:00
Karl Seguin
e18c589de3 Allow root modules to imported modules
Root modules (non-cacheable) should register their module_id -> URL so that,
if they load a nested module, we can get the full URL of the nested module.
2025-06-25 18:20:55 +08:00
sjorsdonkers
aea34264a9 basic/bearer testing 2025-06-25 12:04:38 +02:00
Karl Seguin
8d3a04235d Crypto.getRandomValues consistency
Crypto.getRandomValues should mutate the given parameter as well as return
the value. This return value must be the same (JsObject) as the input parameter.

There might be more magical ways to solve this, but I opted for both the
simplest and most flexible: adding a `toZig` function to JsObject which does
what js.zig does internally when mapping js values to Zig (and, of course, it
uses the same code).

This allows a caller to receive a JsObject (not too common, but we already do
that in a few places) and return that same JsObject (again, not too common, but
we do have support for returning JsObject directly already). With the main
addition that the JsObjet can now be turned into a Zig type by the caller.
2025-06-25 18:03:26 +08:00
Karl Seguin
9c4088b24c We cannot have empty Zig structs mapping to JS instances
An empty struct will share the same address as its sibling (1) which will cause
an collision in the identity map.

(1) - This depends on Zig's non-guaranteed layout, so the collision might not
be with its sibling, but rather some other [seemingly random] field.
2025-06-25 14:58:09 +08:00
Karl Seguin
1e7ee4e0a1 proxy_type 'simple' renamed to 'forward' 2025-06-25 12:21:44 +08:00
Karl Seguin
ec92f110b3 Change dataset to work directly off DOM element 2025-06-25 12:16:08 +08:00
Karl Seguin
2aa5eb85ad Add element.dataset API
Uses the State to store the dataset, but, on first load, loads the data
attributes from the DOM.
2025-06-25 12:16:08 +08:00
Karl Seguin
2815f02382 Merge pull request #811 from lightpanda-io/crypto-getrandomvalues-return
Some checks failed
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Return Random Array from crypto.GetRandomValues
2025-06-25 07:45:04 +08:00
Karl Seguin
8bd7c8dd41 Merge pull request #807 from lightpanda-io/css-util-iface
add CSS utility interface
2025-06-25 07:44:15 +08:00
Karl Seguin
269dcf071f Merge pull request #806 from lightpanda-io/document-range
add AbstractRange and Range
2025-06-25 07:41:52 +08:00
Karl Seguin
997ec7f0bc Merge pull request #805 from lightpanda-io/performance-mark
add PerformanceEntry and PerformanceMark
2025-06-25 07:41:19 +08:00
Muki Kiboigo
d9c26bb77f return array in crypto.getRandomValues 2025-06-24 15:01:32 -07:00
Muki Kiboigo
c0fc3a19c8 add CSS utility interface 2025-06-24 13:55:47 -07:00
Muki Kiboigo
ce638c39e3 add AbstractRange and Range 2025-06-24 12:06:50 -07:00
Muki Kiboigo
6b651cd5e4 add PerformanceEntry and PerformanceMark 2025-06-24 12:04:28 -07:00
sjorsdonkers
4560f31010 basic/bearer proxy authentication 2025-06-24 16:38:58 +02:00
Karl Seguin
c97a32e24b Initial work on CONNECT proxy.
Cannot currently connect to the proxy over TLS (though, once connected, it can
connect to the actual site over TLS). No support for authentication.
2025-06-24 15:10:20 +08:00
Karl Seguin
8a005bc5a1 Merge pull request #808 from lightpanda-io/accept-header
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
http: send an Accept: */* header
2025-06-24 09:42:14 +08:00
Pierre Tachoire
20aabee72e http: send an Accept: */* header 2025-06-23 18:18:04 -07:00
Karl Seguin
a00c2345ee Merge pull request #802 from lightpanda-io/endless_loop_fix_and_dot_slash_stitch
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Don't allow timeouts to be registered when shutting down
2025-06-24 08:57:59 +08:00
Karl Seguin
cb35b3624a Merge pull request #803 from lightpanda-io/inline_module_no_cache
Fix module caching
2025-06-24 08:55:23 +08:00
Karl Seguin
c6f59a7aa6 Merge pull request #804 from lightpanda-io/add_error_event_web_api
add ErrorEvent webapi
2025-06-24 08:54:01 +08:00
Karl Seguin
bf296ad797 add ErrorEvent webapi 2025-06-23 19:04:59 +08:00
Karl Seguin
256540934b reject long timeouts as we're shutting down 2025-06-23 17:27:22 +08:00
Karl Seguin
3c07c0818d improve variable names 2025-06-23 17:19:30 +08:00
Karl Seguin
a01d18ace1 Fix module caching
In https://github.com/lightpanda-io/browser/pull/798 module caching was added.
This was necessary as the same module loaded multiple time should result in the
same v8 module instance.

To make this work, modules became cached by their full URL. The full URL of one
module was also used to determine the full URL of nested modules (full url +
specifier).

With inline scripts, the page URL was used as the full URL. While this is
correct when resolving nested modules, it's incorrect for caching the module
itself. Two inline modules on a page share the same URL, but they aren't the
same and should be cached.

To fix this, inline modules still inherit the page URL, in order to resolve the
correct URL for nested modules, but are themselves never cached.
2025-06-23 17:10:54 +08:00
Karl Seguin
55e02f01dc fix wpt runner 2025-06-23 16:47:31 +08:00
Karl Seguin
fe6ccad485 loop.run now takes a maximum wait time 2025-06-23 16:43:28 +08:00
Karl Seguin
11fe79312d Don't allow timeouts to be registered when shutting down
Currently, a timeout that sets a timeout can cause loop.run to block forever.

Also, cleanup URL stitch with leading './'. The resulting URL was valid, but
since we use the URL as the module cache key, it's important to resolve various
representations of the same URL in the same way.
2025-06-23 15:01:58 +08:00
Karl Seguin
bdb2338b5b Merge pull request #796 from lightpanda-io/docker
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
update dockerfile to multi-arch
2025-06-23 12:51:20 +08:00
Karl Seguin
bbafb048d0 Merge pull request #798 from lightpanda-io/module_loading
Fix module loading
2025-06-23 08:54:17 +08:00
Karl Seguin
9fc2fa51bd Merge pull request #797 from lightpanda-io/template_content
add HTML Template's content attribute
2025-06-23 08:54:00 +08:00
Karl Seguin
d8ec50345a Fix module loading
When V8 calls the ResolveModuleCallback that we give it, it passes the specifier
which is essentially the string given to `from`:

```
import {x} from './blah.js';
```

We were taking that specifier and giving it to the page. The page knew the
currently executing script, an thus could resolve the full URL. Given the full
URL, it could either return the JS content from its module cache or fetch
the source.

At best though, this isn't efficient. If two files import the same module, yes
we cache the src, but we still ask v8 to re-compile it. At worse, it crashes
due to resource exhaustion in the case of cyclical dependencies.

ResolveModuleCallback should instead detect that it has already loaded the
module and return the previously loaded module. Essentially, we shouldn't be
caching the JavaScript source, we should be caching the v8 module.

However, in order to do this, we need more than the specifier, which might only
be a relative path (and thus isn't unique). So, in addition to a module cache,
we now also maintain an module identifier lookup. Given a module, we can get
its full path. Thankfully ResolveModuleCallback gives us the referring module,
so we can look up that modules URL, stitch it to the specifier, and get the
full url (the unique identifier) within the JS runtime.

Need more real world testing, and a fully working example before I celebrate,
but for sites with many import, this appears to improve performance by many
orders of magnitude.
2025-06-20 19:17:55 +08:00
Karl Seguin
9f1cc09ca8 add HTML Template's content attribute 2025-06-20 14:36:56 +08:00
Pierre Tachoire
898b73ffc8 update dockerfile to multi-arch 2025-06-19 10:10:14 -07:00
186 changed files with 26935 additions and 21463 deletions

View File

@@ -5,7 +5,7 @@ inputs:
zig:
description: 'Zig version to install'
required: false
default: '0.14.1'
default: '0.15.1'
arch:
description: 'CPU arch used to select the v8 lib'
required: false
@@ -17,7 +17,7 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.1.24'
default: 'v0.1.28'
v8:
description: 'v8 version to install'
required: false

View File

@@ -60,7 +60,7 @@ jobs:
ARCH: aarch64
OS: linux
runs-on: ubuntu-24.04-arm
runs-on: ubuntu-22.04-arm
timeout-minutes: 15
steps:
@@ -76,7 +76,7 @@ jobs:
arch: ${{env.ARCH}}
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -98,7 +98,9 @@ jobs:
ARCH: aarch64
OS: macos
runs-on: macos-latest
# macos-14 runs on arm CPU. see
# https://github.com/actions/runner-images?tab=readme-ov-file
runs-on: macos-14
timeout-minutes: 15
steps:
@@ -136,6 +138,11 @@ jobs:
ARCH: x86_64
OS: macos
# macos-13 runs on x86 CPU. see
# https://github.com/actions/runner-images?tab=readme-ov-file
# If we want to build for macos-14 or superior, we need to switch to
# macos-14-large.
# No need for now, but maybe we will need it in the short term.
runs-on: macos-13
timeout-minutes: 15

View File

@@ -45,6 +45,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
# Don't run the CI with draft PR.
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
with:
@@ -65,56 +68,6 @@ jobs:
zig-out/bin/lightpanda
retention-days: 1
puppeteer-perf:
name: puppeteer-perf
needs: zig-build-release
env:
MAX_MEMORY: 30000
MAX_AVG_DURATION: 24
LIGHTPANDA_DISABLE_TELEMETRY: true
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
repository: 'lightpanda-io/demo'
fetch-depth: 0
- run: npm install
- name: download artifact
uses: actions/download-artifact@v4
with:
name: lightpanda-build-release
- run: chmod a+x ./lightpanda
- name: run puppeteer
run: |
python3 -m http.server 1234 -d ./public & echo $! > PYTHON.pid
./lightpanda serve & echo $! > LPD.pid
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
kill `cat LPD.pid` `cat PYTHON.pid`
- name: puppeteer result
run: cat puppeteer.out
- name: memory regression
run: |
export LPD_VmHWM=`cat LPD.VmHWM`
echo "Peak resident set size: $LPD_VmHWM"
test "$LPD_VmHWM" -le "$MAX_MEMORY"
- name: duration regression
run: |
export PUPPETEER_AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
echo "puppeteer avg duration: $PUPPETEER_AVG_DURATION"
test "$PUPPETEER_AVG_DURATION" -le "$MAX_AVG_DURATION"
demo-scripts:
name: demo-scripts
needs: zig-build-release
@@ -140,15 +93,29 @@ jobs:
- name: run end to end tests
run: |
./lightpanda serve & echo $! > LPD.pid
go run runner/main.go --verbose
go run runner/main.go
kill `cat LPD.pid`
- name: build proxy
run: |
cd proxy
go build
- name: run end to end tests through proxy
run: |
./proxy/proxy & echo $! > PROXY.id
./lightpanda serve --http_proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid
go run runner/main.go
kill `cat LPD.pid` `cat PROXY.id`
cdp-and-hyperfine-bench:
name: cdp-and-hyperfine-bench
needs: zig-build-release
# Don't execute on PR
if: github.event_name != 'pull_request'
env:
MAX_MEMORY: 27000
MAX_AVG_DURATION: 23
LIGHTPANDA_DISABLE_TELEMETRY: true
# use a self host runner.
runs-on: lpd-bench-hetzner
@@ -185,6 +152,18 @@ jobs:
- name: puppeteer result
run: cat puppeteer.out
- name: memory regression
run: |
export LPD_VmHWM=`cat LPD.VmHWM`
echo "Peak resident set size: $LPD_VmHWM"
test "$LPD_VmHWM" -le "$MAX_MEMORY"
- name: duration regression
run: |
export PUPPETEER_AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
echo "puppeteer avg duration: $PUPPETEER_AVG_DURATION"
test "$PUPPETEER_AVG_DURATION" -le "$MAX_AVG_DURATION"
- name: json output
run: |
export AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`

View File

@@ -1,7 +1,7 @@
name: zig-fmt
env:
ZIG_VERSION: 0.14.1
ZIG_VERSION: 0.15.1
on:
pull_request:

12
.gitmodules vendored
View File

@@ -19,3 +19,15 @@
[submodule "vendor/mimalloc"]
path = vendor/mimalloc
url = https://github.com/microsoft/mimalloc.git/
[submodule "vendor/nghttp2"]
path = vendor/nghttp2
url = https://github.com/nghttp2/nghttp2.git
[submodule "vendor/mbedtls"]
path = vendor/mbedtls
url = https://github.com/Mbed-TLS/mbedtls.git
[submodule "vendor/zlib"]
path = vendor/zlib
url = https://github.com/madler/zlib.git
[submodule "vendor/curl"]
path = vendor/curl
url = https://github.com/curl/curl.git

View File

@@ -1,11 +1,11 @@
FROM ubuntu:24.04
FROM debian:stable
ARG MINISIG=0.12
ARG ZIG=0.14.1
ARG ZIG=0.15.1
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG ARCH=x86_64
ARG V8=13.6.233.8
ARG ZIG_V8=v0.1.24
ARG ZIG_V8=v0.1.28
ARG TARGETPLATFORM
RUN apt-get update -yq && \
apt-get install -yq xz-utils \
@@ -20,30 +20,19 @@ RUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${M
tar xvzf minisign-${MINISIG}-linux.tar.gz
# install zig
RUN curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz
RUN curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig
RUN minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG}
# clean minisg
RUN rm -fr minisign-0.11-linux.tar.gz minisign-linux
# install zig
RUN tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
RUN case $TARGETPLATFORM in \
"linux/arm64") ARCH="aarch64" ;; \
*) ARCH="x86_64" ;; \
esac && \
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz && \
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig && \
minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG} && \
tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
# clean up zig install
RUN rm -fr zig-${ARCH}-linux-${ZIG}.tar.xz zig-${ARCH}-linux-${ZIG}.tar.xz.minisig
# force use of http instead of ssh with github
RUN cat <<EOF > /root/.gitconfig
[url "https://github.com/"]
insteadOf="git@github.com:"
EOF
# clone lightpanda
RUN git clone git@github.com:lightpanda-io/browser.git
RUN git clone https://github.com/lightpanda-io/browser.git
WORKDIR /browser
@@ -56,14 +45,18 @@ RUN make install-libiconv && \
make install-mimalloc
# download and install v8
RUN curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
RUN case $TARGETPLATFORM in \
"linux/arm64") ARCH="aarch64" ;; \
*) ARCH="x86_64" ;; \
esac && \
curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
mkdir -p v8/out/linux/release/obj/zig/ && \
mv libc_v8.a v8/out/linux/release/obj/zig/libc_v8.a
# build release
RUN make build
FROM ubuntu:24.04
FROM debian:stable-slim
# copy ca certificates
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt

View File

@@ -18,7 +18,7 @@ Lightpanda is the open-source browser made for headless usage:
- Javascript execution
- Support of Web APIs (partial, WIP)
- Compatible with Playwright[^1], Puppeteer through CDP (WIP)
- Compatible with Playwright[^1], Puppeteer, chromedp through CDP
Fast web automation for AI agents, LLM training, scraping and testing:
@@ -41,7 +41,8 @@ Due to the nature of Playwright, a script that works with the current version of
## Quick start
### Install from the nightly builds
### Install
**Install from the nightly builds**
You can download the last binary from the [nightly
builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for
@@ -64,6 +65,16 @@ chmod a+x ./lightpanda
The Lightpanda browser is compatible to run on windows inside WSL. Follow the Linux instruction for installation from a WSL terminal.
It is recommended to install clients like Puppeteer on the Windows host.
**Install from Docker**
Lightpanda provides [official Docker
images](https://hub.docker.com/r/lightpanda/browser) for both Linux amd64 and
arm64 architectures.
The following command fetches the Docker image and starts a new container exposing Lightpanda's CDP server on port `9222`.
```console
docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
```
### Dump a URL
```console
@@ -124,21 +135,26 @@ By default, Lightpanda collects and sends usage telemetry. This can be disabled
## Status
Lightpanda is still a work in progress and is currently at a Beta stage.
:warning: You should expect most websites to fail or crash.
Lightpanda is in Beta and currently a work in progress. Stability and coverage are improving and many websites now work.
You may still encounter errors or crashes. Please open an issue with specifics if so.
Here are the key features we have implemented:
- [x] HTTP loader
- [x] HTTP loader (based on Libcurl)
- [x] HTML parser and DOM tree (based on Netsurf libs)
- [x] Javascript support (v8)
- [x] Basic DOM APIs
- [x] DOM APIs
- [x] Ajax
- [x] XHR API
- [x] Fetch API
- [x] Fetch API (polyfill)
- [x] DOM dump
- [x] Basic CDP/websockets server
- [x] CDP/websockets server
- [x] Click
- [x] Input form
- [x] Cookies
- [x] Custom HTTP headers
- [x] Proxy support
- [x] Network interception
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
@@ -148,11 +164,12 @@ You can also follow the progress of our Javascript support in our dedicated [zig
### Prerequisites
Lightpanda is written with [Zig](https://ziglang.org/) `0.14.1`. You have to
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to
install it with the right version in order to build the project.
Lightpanda also depends on
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
[Libcurl](https://curl.se/libcurl/),
[Netsurf libs](https://www.netsurf-browser.org/) and
[Mimalloc](https://microsoft.github.io/mimalloc).

737
build.zig
View File

@@ -19,11 +19,13 @@
const std = @import("std");
const builtin = @import("builtin");
const Build = std.Build;
/// Do not rename this constant. It is scanned by some scripts to determine
/// which zig version to install.
const recommended_zig_version = "0.14.1";
const recommended_zig_version = "0.15.1";
pub fn build(b: *std.Build) !void {
pub fn build(b: *Build) !void {
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
.eq => {},
.lt => {
@@ -47,6 +49,18 @@ pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// We're still using llvm because the new x86 backend seems to crash
// with v8. This can be reproduced in zig-v8-fork.
const lightpanda_module = b.addModule("lightpanda", .{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.link_libcpp = true,
});
try addDependencies(b, lightpanda_module, opts);
{
// browser
// -------
@@ -54,12 +68,9 @@ pub fn build(b: *std.Build) !void {
// compile and install
const exe = b.addExecutable(.{
.name = "lightpanda",
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/main.zig"),
.use_llvm = true,
.root_module = lightpanda_module,
});
try common(b, opts, exe);
b.installArtifact(exe);
// run
@@ -73,6 +84,54 @@ pub fn build(b: *std.Build) !void {
run_step.dependOn(&run_cmd.step);
}
{
// tests
// ----
// compile
const tests = b.addTest(.{
.root_module = lightpanda_module,
.use_llvm = true,
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
});
const run_tests = b.addRunArtifact(tests);
if (b.args) |args| {
run_tests.addArgs(args);
}
// step
const tests_step = b.step("test", "Run unit tests");
tests_step.dependOn(&run_tests.step);
}
{
// wpt
// -----
const wpt_module = b.createModule(.{
.root_source_file = b.path("src/main_wpt.zig"),
.target = target,
.optimize = optimize,
});
try addDependencies(b, wpt_module, opts);
// compile and install
const wpt = b.addExecutable(.{
.name = "lightpanda-wpt",
.use_llvm = true,
.root_module = wpt_module,
});
// run
const wpt_cmd = b.addRunArtifact(wpt);
if (b.args) |args| {
wpt_cmd.addArgs(args);
}
// step
const wpt_step = b.step("wpt", "WPT tests");
wpt_step.dependOn(&wpt_cmd.step);
}
{
// get v8
// -------
@@ -90,63 +149,19 @@ pub fn build(b: *std.Build) !void {
const build_step = b.step("build-v8", "Build v8");
build_step.dependOn(&build_v8.step);
}
{
// tests
// ----
// compile
const tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
.target = target,
.optimize = optimize,
});
try common(b, opts, tests);
const run_tests = b.addRunArtifact(tests);
if (b.args) |args| {
run_tests.addArgs(args);
}
// step
const tests_step = b.step("test", "Run unit tests");
tests_step.dependOn(&run_tests.step);
}
{
// wpt
// -----
// compile and install
const wpt = b.addExecutable(.{
.name = "lightpanda-wpt",
.root_source_file = b.path("src/main_wpt.zig"),
.target = target,
.optimize = optimize,
});
try common(b, opts, wpt);
// run
const wpt_cmd = b.addRunArtifact(wpt);
if (b.args) |args| {
wpt_cmd.addArgs(args);
}
// step
const wpt_step = b.step("wpt", "WPT tests");
wpt_step.dependOn(&wpt_cmd.step);
}
}
fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Compile) !void {
const mod = step.root_module;
const target = mod.resolved_target.?;
const optimize = mod.optimize.?;
const dep_opts = .{ .target = target, .optimize = optimize };
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !void {
try moduleNetSurf(b, mod);
mod.addImport("build_config", opts.createModule());
try moduleNetSurf(b, step, target);
mod.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
mod.addImport("tigerbeetle-io", b.dependency("tigerbeetle_io", .{}).module("tigerbeetle_io"));
const target = mod.resolved_target.?;
const dep_opts = .{
.target = target,
.optimize = mod.optimize.?,
};
mod.addIncludePath(b.path("vendor/lightpanda"));
{
// v8
@@ -156,11 +171,7 @@ fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Co
const v8_mod = b.dependency("v8", dep_opts).module("v8");
v8_mod.addOptions("default_exports", v8_opts);
mod.addImport("v8", v8_mod);
}
mod.link_libcpp = true;
{
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
const os = switch (target.result.os.tag) {
.linux => "linux",
@@ -181,21 +192,211 @@ fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Co
);
};
mod.addObjectFile(mod.owner.path(lib_path));
switch (target.result.os.tag) {
.macos => {
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
mod.linkFramework("CoreFoundation", .{});
},
else => {},
}
}
switch (target.result.os.tag) {
.macos => {
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
mod.linkFramework("CoreFoundation", .{});
},
else => {},
}
{
//curl
{
const is_linux = target.result.os.tag == .linux;
if (is_linux) {
mod.addCMacro("HAVE_LINUX_TCP_H", "1");
mod.addCMacro("HAVE_MSG_NOSIGNAL", "1");
mod.addCMacro("HAVE_GETHOSTBYNAME_R", "1");
}
mod.addCMacro("_FILE_OFFSET_BITS", "64");
mod.addCMacro("BUILDING_LIBCURL", "1");
mod.addCMacro("CURL_DISABLE_AWS", "1");
mod.addCMacro("CURL_DISABLE_DICT", "1");
mod.addCMacro("CURL_DISABLE_DOH", "1");
mod.addCMacro("CURL_DISABLE_FILE", "1");
mod.addCMacro("CURL_DISABLE_FTP", "1");
mod.addCMacro("CURL_DISABLE_GOPHER", "1");
mod.addCMacro("CURL_DISABLE_KERBEROS", "1");
mod.addCMacro("CURL_DISABLE_IMAP", "1");
mod.addCMacro("CURL_DISABLE_IPFS", "1");
mod.addCMacro("CURL_DISABLE_LDAP", "1");
mod.addCMacro("CURL_DISABLE_LDAPS", "1");
mod.addCMacro("CURL_DISABLE_MQTT", "1");
mod.addCMacro("CURL_DISABLE_NTLM", "1");
mod.addCMacro("CURL_DISABLE_PROGRESS_METER", "1");
mod.addCMacro("CURL_DISABLE_POP3", "1");
mod.addCMacro("CURL_DISABLE_RTSP", "1");
mod.addCMacro("CURL_DISABLE_SMB", "1");
mod.addCMacro("CURL_DISABLE_SMTP", "1");
mod.addCMacro("CURL_DISABLE_TELNET", "1");
mod.addCMacro("CURL_DISABLE_TFTP", "1");
mod.addCMacro("CURL_EXTERN_SYMBOL", "__attribute__ ((__visibility__ (\"default\"))");
mod.addCMacro("CURL_OS", if (is_linux) "\"Linux\"" else "\"mac\"");
mod.addCMacro("CURL_STATICLIB", "1");
mod.addCMacro("ENABLE_IPV6", "1");
mod.addCMacro("HAVE_ALARM", "1");
mod.addCMacro("HAVE_ALLOCA_H", "1");
mod.addCMacro("HAVE_ARPA_INET_H", "1");
mod.addCMacro("HAVE_ARPA_TFTP_H", "1");
mod.addCMacro("HAVE_ASSERT_H", "1");
mod.addCMacro("HAVE_BASENAME", "1");
mod.addCMacro("HAVE_BOOL_T", "1");
mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1");
mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
mod.addCMacro("HAVE_DLFCN_H", "1");
mod.addCMacro("HAVE_ERRNO_H", "1");
mod.addCMacro("HAVE_FCNTL", "1");
mod.addCMacro("HAVE_FCNTL_H", "1");
mod.addCMacro("HAVE_FCNTL_O_NONBLOCK", "1");
mod.addCMacro("HAVE_FREEADDRINFO", "1");
mod.addCMacro("HAVE_FSETXATTR", "1");
mod.addCMacro("HAVE_FSETXATTR_5", "1");
mod.addCMacro("HAVE_FTRUNCATE", "1");
mod.addCMacro("HAVE_GETADDRINFO", "1");
mod.addCMacro("HAVE_GETEUID", "1");
mod.addCMacro("HAVE_GETHOSTBYNAME", "1");
mod.addCMacro("HAVE_GETHOSTBYNAME_R_6", "1");
mod.addCMacro("HAVE_GETHOSTNAME", "1");
mod.addCMacro("HAVE_GETPEERNAME", "1");
mod.addCMacro("HAVE_GETPPID", "1");
mod.addCMacro("HAVE_GETPPID", "1");
mod.addCMacro("HAVE_GETPROTOBYNAME", "1");
mod.addCMacro("HAVE_GETPWUID", "1");
mod.addCMacro("HAVE_GETPWUID_R", "1");
mod.addCMacro("HAVE_GETRLIMIT", "1");
mod.addCMacro("HAVE_GETSOCKNAME", "1");
mod.addCMacro("HAVE_GETTIMEOFDAY", "1");
mod.addCMacro("HAVE_GMTIME_R", "1");
mod.addCMacro("HAVE_IDN2_H", "1");
mod.addCMacro("HAVE_IF_NAMETOINDEX", "1");
mod.addCMacro("HAVE_IFADDRS_H", "1");
mod.addCMacro("HAVE_INET_ADDR", "1");
mod.addCMacro("HAVE_INET_PTON", "1");
mod.addCMacro("HAVE_INTTYPES_H", "1");
mod.addCMacro("HAVE_IOCTL", "1");
mod.addCMacro("HAVE_IOCTL_FIONBIO", "1");
mod.addCMacro("HAVE_IOCTL_SIOCGIFADDR", "1");
mod.addCMacro("HAVE_LDAP_URL_PARSE", "1");
mod.addCMacro("HAVE_LIBGEN_H", "1");
mod.addCMacro("HAVE_LIBZ", "1");
mod.addCMacro("HAVE_LL", "1");
mod.addCMacro("HAVE_LOCALE_H", "1");
mod.addCMacro("HAVE_LOCALTIME_R", "1");
mod.addCMacro("HAVE_LONGLONG", "1");
mod.addCMacro("HAVE_MALLOC_H", "1");
mod.addCMacro("HAVE_MEMORY_H", "1");
mod.addCMacro("HAVE_NET_IF_H", "1");
mod.addCMacro("HAVE_NETDB_H", "1");
mod.addCMacro("HAVE_NETINET_IN_H", "1");
mod.addCMacro("HAVE_NETINET_TCP_H", "1");
mod.addCMacro("HAVE_PIPE", "1");
mod.addCMacro("HAVE_POLL", "1");
mod.addCMacro("HAVE_POLL_FINE", "1");
mod.addCMacro("HAVE_POLL_H", "1");
mod.addCMacro("HAVE_POSIX_STRERROR_R", "1");
mod.addCMacro("HAVE_PTHREAD_H", "1");
mod.addCMacro("HAVE_PWD_H", "1");
mod.addCMacro("HAVE_RECV", "1");
mod.addCMacro("HAVE_SA_FAMILY_T", "1");
mod.addCMacro("HAVE_SELECT", "1");
mod.addCMacro("HAVE_SEND", "1");
mod.addCMacro("HAVE_SETJMP_H", "1");
mod.addCMacro("HAVE_SETLOCALE", "1");
mod.addCMacro("HAVE_SETRLIMIT", "1");
mod.addCMacro("HAVE_SETSOCKOPT", "1");
mod.addCMacro("HAVE_SIGACTION", "1");
mod.addCMacro("HAVE_SIGINTERRUPT", "1");
mod.addCMacro("HAVE_SIGNAL", "1");
mod.addCMacro("HAVE_SIGNAL_H", "1");
mod.addCMacro("HAVE_SIGSETJMP", "1");
mod.addCMacro("HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID", "1");
mod.addCMacro("HAVE_SOCKET", "1");
mod.addCMacro("HAVE_STDBOOL_H", "1");
mod.addCMacro("HAVE_STDINT_H", "1");
mod.addCMacro("HAVE_STDIO_H", "1");
mod.addCMacro("HAVE_STDLIB_H", "1");
mod.addCMacro("HAVE_STRCASECMP", "1");
mod.addCMacro("HAVE_STRDUP", "1");
mod.addCMacro("HAVE_STRERROR_R", "1");
mod.addCMacro("HAVE_STRING_H", "1");
mod.addCMacro("HAVE_STRINGS_H", "1");
mod.addCMacro("HAVE_STRSTR", "1");
mod.addCMacro("HAVE_STRTOK_R", "1");
mod.addCMacro("HAVE_STRTOLL", "1");
mod.addCMacro("HAVE_STRUCT_SOCKADDR_STORAGE", "1");
mod.addCMacro("HAVE_STRUCT_TIMEVAL", "1");
mod.addCMacro("HAVE_SYS_IOCTL_H", "1");
mod.addCMacro("HAVE_SYS_PARAM_H", "1");
mod.addCMacro("HAVE_SYS_POLL_H", "1");
mod.addCMacro("HAVE_SYS_RESOURCE_H", "1");
mod.addCMacro("HAVE_SYS_SELECT_H", "1");
mod.addCMacro("HAVE_SYS_SOCKET_H", "1");
mod.addCMacro("HAVE_SYS_STAT_H", "1");
mod.addCMacro("HAVE_SYS_TIME_H", "1");
mod.addCMacro("HAVE_SYS_TYPES_H", "1");
mod.addCMacro("HAVE_SYS_UIO_H", "1");
mod.addCMacro("HAVE_SYS_UN_H", "1");
mod.addCMacro("HAVE_TERMIO_H", "1");
mod.addCMacro("HAVE_TERMIOS_H", "1");
mod.addCMacro("HAVE_TIME_H", "1");
mod.addCMacro("HAVE_UNAME", "1");
mod.addCMacro("HAVE_UNISTD_H", "1");
mod.addCMacro("HAVE_UTIME", "1");
mod.addCMacro("HAVE_UTIME_H", "1");
mod.addCMacro("HAVE_UTIMES", "1");
mod.addCMacro("HAVE_VARIADIC_MACROS_C99", "1");
mod.addCMacro("HAVE_VARIADIC_MACROS_GCC", "1");
mod.addCMacro("HAVE_ZLIB_H", "1");
mod.addCMacro("RANDOM_FILE", "\"/dev/urandom\"");
mod.addCMacro("RECV_TYPE_ARG1", "int");
mod.addCMacro("RECV_TYPE_ARG2", "void *");
mod.addCMacro("RECV_TYPE_ARG3", "size_t");
mod.addCMacro("RECV_TYPE_ARG4", "int");
mod.addCMacro("RECV_TYPE_RETV", "ssize_t");
mod.addCMacro("SEND_QUAL_ARG2", "const");
mod.addCMacro("SEND_TYPE_ARG1", "int");
mod.addCMacro("SEND_TYPE_ARG2", "void *");
mod.addCMacro("SEND_TYPE_ARG3", "size_t");
mod.addCMacro("SEND_TYPE_ARG4", "int");
mod.addCMacro("SEND_TYPE_RETV", "ssize_t");
mod.addCMacro("SIZEOF_CURL_OFF_T", "8");
mod.addCMacro("SIZEOF_INT", "4");
mod.addCMacro("SIZEOF_LONG", "8");
mod.addCMacro("SIZEOF_OFF_T", "8");
mod.addCMacro("SIZEOF_SHORT", "2");
mod.addCMacro("SIZEOF_SIZE_T", "8");
mod.addCMacro("SIZEOF_TIME_T", "8");
mod.addCMacro("STDC_HEADERS", "1");
mod.addCMacro("TIME_WITH_SYS_TIME", "1");
mod.addCMacro("USE_NGHTTP2", "1");
mod.addCMacro("USE_MBEDTLS", "1");
mod.addCMacro("USE_THREADS_POSIX", "1");
mod.addCMacro("USE_UNIX_SOCKETS", "1");
}
mod.addImport("build_config", opts.createModule());
try buildZlib(b, mod);
try buildMbedtls(b, mod);
try buildNghttp2(b, mod);
try buildCurl(b, mod);
switch (target.result.os.tag) {
.macos => {
// needed for proxying on mac
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
mod.linkFramework("CoreFoundation", .{});
mod.linkFramework("SystemConfiguration", .{});
},
else => {},
}
}
}
fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build.ResolvedTarget) !void {
fn moduleNetSurf(b: *Build, mod: *Build.Module) !void {
const target = mod.resolved_target.?;
const os = target.result.os.tag;
const arch = target.result.cpu.arch;
@@ -210,8 +411,8 @@ fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
.{ @tagName(os), @tagName(arch) },
);
step.addObjectFile(b.path(libiconv_lib_path));
step.addIncludePath(b.path(libiconv_include_path));
mod.addObjectFile(b.path(libiconv_lib_path));
mod.addIncludePath(b.path(libiconv_include_path));
{
// mimalloc
@@ -221,8 +422,8 @@ fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
.{ @tagName(os), @tagName(arch) },
);
step.addObjectFile(b.path(lib_path));
step.addIncludePath(b.path(mimalloc ++ "/include"));
mod.addObjectFile(b.path(lib_path));
mod.addIncludePath(b.path(mimalloc ++ "/include"));
}
// netsurf libs
@@ -232,7 +433,7 @@ fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build
ns ++ "/out/{s}-{s}/include",
.{ @tagName(os), @tagName(arch) },
);
step.addIncludePath(b.path(ns_include_path));
mod.addIncludePath(b.path(ns_include_path));
const libs: [4][]const u8 = .{
"libdom",
@@ -246,7 +447,379 @@ fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build
ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a",
.{ @tagName(os), @tagName(arch) },
);
step.addObjectFile(b.path(ns_lib_path));
step.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
mod.addObjectFile(b.path(ns_lib_path));
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
}
}
fn buildZlib(b: *Build, m: *Build.Module) !void {
const zlib = b.addLibrary(.{
.name = "zlib",
.root_module = m,
});
const root = "vendor/zlib/";
zlib.installHeader(b.path(root ++ "zlib.h"), "zlib.h");
zlib.installHeader(b.path(root ++ "zconf.h"), "zconf.h");
zlib.addCSourceFiles(.{ .flags = &.{
"-DHAVE_SYS_TYPES_H",
"-DHAVE_STDINT_H",
"-DHAVE_STDDEF_H",
}, .files = &.{
root ++ "adler32.c",
root ++ "compress.c",
root ++ "crc32.c",
root ++ "deflate.c",
root ++ "gzclose.c",
root ++ "gzlib.c",
root ++ "gzread.c",
root ++ "gzwrite.c",
root ++ "inflate.c",
root ++ "infback.c",
root ++ "inftrees.c",
root ++ "inffast.c",
root ++ "trees.c",
root ++ "uncompr.c",
root ++ "zutil.c",
} });
}
fn buildMbedtls(b: *Build, m: *Build.Module) !void {
const mbedtls = b.addLibrary(.{
.name = "mbedtls",
.root_module = m,
});
const root = "vendor/mbedtls/";
mbedtls.addIncludePath(b.path(root ++ "include"));
mbedtls.addIncludePath(b.path(root ++ "library"));
mbedtls.addCSourceFiles(.{ .flags = &.{}, .files = &.{
root ++ "library/aes.c",
root ++ "library/aesni.c",
root ++ "library/aesce.c",
root ++ "library/aria.c",
root ++ "library/asn1parse.c",
root ++ "library/asn1write.c",
root ++ "library/base64.c",
root ++ "library/bignum.c",
root ++ "library/bignum_core.c",
root ++ "library/bignum_mod.c",
root ++ "library/bignum_mod_raw.c",
root ++ "library/camellia.c",
root ++ "library/ccm.c",
root ++ "library/chacha20.c",
root ++ "library/chachapoly.c",
root ++ "library/cipher.c",
root ++ "library/cipher_wrap.c",
root ++ "library/constant_time.c",
root ++ "library/cmac.c",
root ++ "library/ctr_drbg.c",
root ++ "library/des.c",
root ++ "library/dhm.c",
root ++ "library/ecdh.c",
root ++ "library/ecdsa.c",
root ++ "library/ecjpake.c",
root ++ "library/ecp.c",
root ++ "library/ecp_curves.c",
root ++ "library/entropy.c",
root ++ "library/entropy_poll.c",
root ++ "library/error.c",
root ++ "library/gcm.c",
root ++ "library/hkdf.c",
root ++ "library/hmac_drbg.c",
root ++ "library/lmots.c",
root ++ "library/lms.c",
root ++ "library/md.c",
root ++ "library/md5.c",
root ++ "library/memory_buffer_alloc.c",
root ++ "library/nist_kw.c",
root ++ "library/oid.c",
root ++ "library/padlock.c",
root ++ "library/pem.c",
root ++ "library/pk.c",
root ++ "library/pk_ecc.c",
root ++ "library/pk_wrap.c",
root ++ "library/pkcs12.c",
root ++ "library/pkcs5.c",
root ++ "library/pkparse.c",
root ++ "library/pkwrite.c",
root ++ "library/platform.c",
root ++ "library/platform_util.c",
root ++ "library/poly1305.c",
root ++ "library/psa_crypto.c",
root ++ "library/psa_crypto_aead.c",
root ++ "library/psa_crypto_cipher.c",
root ++ "library/psa_crypto_client.c",
root ++ "library/psa_crypto_ffdh.c",
root ++ "library/psa_crypto_driver_wrappers_no_static.c",
root ++ "library/psa_crypto_ecp.c",
root ++ "library/psa_crypto_hash.c",
root ++ "library/psa_crypto_mac.c",
root ++ "library/psa_crypto_pake.c",
root ++ "library/psa_crypto_rsa.c",
root ++ "library/psa_crypto_se.c",
root ++ "library/psa_crypto_slot_management.c",
root ++ "library/psa_crypto_storage.c",
root ++ "library/psa_its_file.c",
root ++ "library/psa_util.c",
root ++ "library/ripemd160.c",
root ++ "library/rsa.c",
root ++ "library/rsa_alt_helpers.c",
root ++ "library/sha1.c",
root ++ "library/sha3.c",
root ++ "library/sha256.c",
root ++ "library/sha512.c",
root ++ "library/threading.c",
root ++ "library/timing.c",
root ++ "library/version.c",
root ++ "library/version_features.c",
root ++ "library/pkcs7.c",
root ++ "library/x509.c",
root ++ "library/x509_create.c",
root ++ "library/x509_crl.c",
root ++ "library/x509_crt.c",
root ++ "library/x509_csr.c",
root ++ "library/x509write.c",
root ++ "library/x509write_crt.c",
root ++ "library/x509write_csr.c",
root ++ "library/debug.c",
root ++ "library/mps_reader.c",
root ++ "library/mps_trace.c",
root ++ "library/net_sockets.c",
root ++ "library/ssl_cache.c",
root ++ "library/ssl_ciphersuites.c",
root ++ "library/ssl_client.c",
root ++ "library/ssl_cookie.c",
root ++ "library/ssl_debug_helpers_generated.c",
root ++ "library/ssl_msg.c",
root ++ "library/ssl_ticket.c",
root ++ "library/ssl_tls.c",
root ++ "library/ssl_tls12_client.c",
root ++ "library/ssl_tls12_server.c",
root ++ "library/ssl_tls13_keys.c",
root ++ "library/ssl_tls13_server.c",
root ++ "library/ssl_tls13_client.c",
root ++ "library/ssl_tls13_generic.c",
} });
}
fn buildNghttp2(b: *Build, m: *Build.Module) !void {
const nghttp2 = b.addLibrary(.{
.name = "nghttp2",
.root_module = m,
});
const root = "vendor/nghttp2/";
nghttp2.addIncludePath(b.path(root ++ "lib"));
nghttp2.addIncludePath(b.path(root ++ "lib/includes"));
nghttp2.addCSourceFiles(.{ .flags = &.{
"-DNGHTTP2_STATICLIB",
"-DHAVE_NETINET_IN",
"-DHAVE_TIME_H",
}, .files = &.{
root ++ "lib/sfparse.c",
root ++ "lib/nghttp2_alpn.c",
root ++ "lib/nghttp2_buf.c",
root ++ "lib/nghttp2_callbacks.c",
root ++ "lib/nghttp2_debug.c",
root ++ "lib/nghttp2_extpri.c",
root ++ "lib/nghttp2_frame.c",
root ++ "lib/nghttp2_hd.c",
root ++ "lib/nghttp2_hd_huffman.c",
root ++ "lib/nghttp2_hd_huffman_data.c",
root ++ "lib/nghttp2_helper.c",
root ++ "lib/nghttp2_http.c",
root ++ "lib/nghttp2_map.c",
root ++ "lib/nghttp2_mem.c",
root ++ "lib/nghttp2_option.c",
root ++ "lib/nghttp2_outbound_item.c",
root ++ "lib/nghttp2_pq.c",
root ++ "lib/nghttp2_priority_spec.c",
root ++ "lib/nghttp2_queue.c",
root ++ "lib/nghttp2_rcbuf.c",
root ++ "lib/nghttp2_session.c",
root ++ "lib/nghttp2_stream.c",
root ++ "lib/nghttp2_submit.c",
root ++ "lib/nghttp2_version.c",
root ++ "lib/nghttp2_ratelim.c",
root ++ "lib/nghttp2_time.c",
} });
}
fn buildCurl(b: *Build, m: *Build.Module) !void {
const curl = b.addLibrary(.{
.name = "curl",
.root_module = m,
});
const root = "vendor/curl/";
curl.addIncludePath(b.path(root ++ "lib"));
curl.addIncludePath(b.path(root ++ "include"));
curl.addCSourceFiles(.{
.flags = &.{},
.files = &.{
root ++ "lib/altsvc.c",
root ++ "lib/amigaos.c",
root ++ "lib/asyn-ares.c",
root ++ "lib/asyn-base.c",
root ++ "lib/asyn-thrdd.c",
root ++ "lib/bufq.c",
root ++ "lib/bufref.c",
root ++ "lib/cf-h1-proxy.c",
root ++ "lib/cf-h2-proxy.c",
root ++ "lib/cf-haproxy.c",
root ++ "lib/cf-https-connect.c",
root ++ "lib/cf-socket.c",
root ++ "lib/cfilters.c",
root ++ "lib/conncache.c",
root ++ "lib/connect.c",
root ++ "lib/content_encoding.c",
root ++ "lib/cookie.c",
root ++ "lib/cshutdn.c",
root ++ "lib/curl_addrinfo.c",
root ++ "lib/curl_des.c",
root ++ "lib/curl_endian.c",
root ++ "lib/curl_fnmatch.c",
root ++ "lib/curl_get_line.c",
root ++ "lib/curl_gethostname.c",
root ++ "lib/curl_gssapi.c",
root ++ "lib/curl_memrchr.c",
root ++ "lib/curl_ntlm_core.c",
root ++ "lib/curl_range.c",
root ++ "lib/curl_rtmp.c",
root ++ "lib/curl_sasl.c",
root ++ "lib/curl_sha512_256.c",
root ++ "lib/curl_sspi.c",
root ++ "lib/curl_threads.c",
root ++ "lib/curl_trc.c",
root ++ "lib/cw-out.c",
root ++ "lib/cw-pause.c",
root ++ "lib/dict.c",
root ++ "lib/doh.c",
root ++ "lib/dynhds.c",
root ++ "lib/easy.c",
root ++ "lib/easygetopt.c",
root ++ "lib/easyoptions.c",
root ++ "lib/escape.c",
root ++ "lib/fake_addrinfo.c",
root ++ "lib/file.c",
root ++ "lib/fileinfo.c",
root ++ "lib/fopen.c",
root ++ "lib/formdata.c",
root ++ "lib/ftp.c",
root ++ "lib/ftplistparser.c",
root ++ "lib/getenv.c",
root ++ "lib/getinfo.c",
root ++ "lib/gopher.c",
root ++ "lib/hash.c",
root ++ "lib/headers.c",
root ++ "lib/hmac.c",
root ++ "lib/hostip.c",
root ++ "lib/hostip4.c",
root ++ "lib/hostip6.c",
root ++ "lib/hsts.c",
root ++ "lib/http.c",
root ++ "lib/http1.c",
root ++ "lib/http2.c",
root ++ "lib/http_aws_sigv4.c",
root ++ "lib/http_chunks.c",
root ++ "lib/http_digest.c",
root ++ "lib/http_negotiate.c",
root ++ "lib/http_ntlm.c",
root ++ "lib/http_proxy.c",
root ++ "lib/httpsrr.c",
root ++ "lib/idn.c",
root ++ "lib/if2ip.c",
root ++ "lib/imap.c",
root ++ "lib/krb5.c",
root ++ "lib/ldap.c",
root ++ "lib/llist.c",
root ++ "lib/macos.c",
root ++ "lib/md4.c",
root ++ "lib/md5.c",
root ++ "lib/memdebug.c",
root ++ "lib/mime.c",
root ++ "lib/mprintf.c",
root ++ "lib/mqtt.c",
root ++ "lib/multi.c",
root ++ "lib/multi_ev.c",
root ++ "lib/netrc.c",
root ++ "lib/noproxy.c",
root ++ "lib/openldap.c",
root ++ "lib/parsedate.c",
root ++ "lib/pingpong.c",
root ++ "lib/pop3.c",
root ++ "lib/progress.c",
root ++ "lib/psl.c",
root ++ "lib/rand.c",
root ++ "lib/rename.c",
root ++ "lib/request.c",
root ++ "lib/rtsp.c",
root ++ "lib/select.c",
root ++ "lib/sendf.c",
root ++ "lib/setopt.c",
root ++ "lib/sha256.c",
root ++ "lib/share.c",
root ++ "lib/slist.c",
root ++ "lib/smb.c",
root ++ "lib/smtp.c",
root ++ "lib/socketpair.c",
root ++ "lib/socks.c",
root ++ "lib/socks_gssapi.c",
root ++ "lib/socks_sspi.c",
root ++ "lib/speedcheck.c",
root ++ "lib/splay.c",
root ++ "lib/strcase.c",
root ++ "lib/strdup.c",
root ++ "lib/strequal.c",
root ++ "lib/strerror.c",
root ++ "lib/system_win32.c",
root ++ "lib/telnet.c",
root ++ "lib/tftp.c",
root ++ "lib/transfer.c",
root ++ "lib/uint-bset.c",
root ++ "lib/uint-hash.c",
root ++ "lib/uint-spbset.c",
root ++ "lib/uint-table.c",
root ++ "lib/url.c",
root ++ "lib/urlapi.c",
root ++ "lib/version.c",
root ++ "lib/ws.c",
root ++ "lib/curlx/base64.c",
root ++ "lib/curlx/dynbuf.c",
root ++ "lib/curlx/inet_ntop.c",
root ++ "lib/curlx/nonblock.c",
root ++ "lib/curlx/strparse.c",
root ++ "lib/curlx/timediff.c",
root ++ "lib/curlx/timeval.c",
root ++ "lib/curlx/wait.c",
root ++ "lib/curlx/warnless.c",
root ++ "lib/vquic/curl_ngtcp2.c",
root ++ "lib/vquic/curl_osslq.c",
root ++ "lib/vquic/curl_quiche.c",
root ++ "lib/vquic/vquic.c",
root ++ "lib/vquic/vquic-tls.c",
root ++ "lib/vauth/cleartext.c",
root ++ "lib/vauth/cram.c",
root ++ "lib/vauth/digest.c",
root ++ "lib/vauth/digest_sspi.c",
root ++ "lib/vauth/gsasl.c",
root ++ "lib/vauth/krb5_gssapi.c",
root ++ "lib/vauth/krb5_sspi.c",
root ++ "lib/vauth/ntlm.c",
root ++ "lib/vauth/ntlm_sspi.c",
root ++ "lib/vauth/oauth2.c",
root ++ "lib/vauth/spnego_gssapi.c",
root ++ "lib/vauth/spnego_sspi.c",
root ++ "lib/vauth/vauth.c",
root ++ "lib/vtls/cipher_suite.c",
root ++ "lib/vtls/mbedtls.c",
root ++ "lib/vtls/mbedtls_threadlock.c",
root ++ "lib/vtls/vtls.c",
root ++ "lib/vtls/vtls_scache.c",
root ++ "lib/vtls/x509asn1.c",
},
});
}

View File

@@ -4,19 +4,10 @@
.version = "0.0.0",
.fingerprint = 0xda130f3af836cea0,
.dependencies = .{
.tls = .{
.url = "https://github.com/ianic/tls.zig/archive/b29a8b45fc59fc2d202769c4f54509bb9e17d0a2.tar.gz",
.hash = "tls-0.1.0-ER2e0uAxBQDm_TmSDdbiiyvAZoh4ejlDD4hW8Fl813xE",
},
.tigerbeetle_io = .{
.url = "https://github.com/lightpanda-io/tigerbeetle-io/archive/61d9652f1a957b7f4db723ea6aa0ce9635e840ce.tar.gz",
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
},
.v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/1d25fcf3ced688adca3c7a95a138771e4ebba692.tar.gz",
.hash = "v8-0.0.0-xddH61eyAwDICIkLAkfQcxsX4TMCKY80QiSUgNBQqx-u",
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d3040a953e5a37290dae20e7ddf138b7aeb5e67d.tar.gz",
.hash = "v8-0.0.0-xddH6w_EAwA8vK0NAxfxfI7IcbnpkUAcXKNujn7qwnmY",
},
//.v8 = .{ .path = "../zig-v8-fork" },
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
//.v8 = .{ .path = "../zig-v8-fork" }
},
}

127
flake.lock generated
View File

@@ -1,5 +1,21 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
@@ -18,13 +34,52 @@
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"zlsPkg",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1748964450,
"narHash": "sha256-ZouDiXkUk8mkMnah10QcoQ9Nu6UW6AFAHLScS3En6aI=",
"lastModified": 1756822655,
"narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9ff500cd9e123f46c55855eca64beccead29b152",
"rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
"type": "github"
},
"original": {
@@ -37,7 +92,9 @@
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"zigPkgs": "zigPkgs",
"zlsPkg": "zlsPkg"
}
},
"systems": {
@@ -54,6 +111,68 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"zigPkgs": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1756555914,
"narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=",
"owner": "mitchellh",
"repo": "zig-overlay",
"rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6",
"type": "github"
},
"original": {
"owner": "mitchellh",
"repo": "zig-overlay",
"type": "github"
}
},
"zlsPkg": {
"inputs": {
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"zig-overlay": [
"zigPkgs"
]
},
"locked": {
"lastModified": 1756048867,
"narHash": "sha256-GFzSHUljcxy7sM1PaabbkQUdUnLwpherekPWJFxXtnk=",
"owner": "zigtools",
"repo": "zls",
"rev": "ce6c8f02c78e622421cfc2405c67c5222819ec03",
"type": "github"
},
"original": {
"owner": "zigtools",
"ref": "0.15.0",
"repo": "zls",
"type": "github"
}
}
},
"root": "root",

View File

@@ -3,20 +3,37 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/release-25.05";
zigPkgs.url = "github:mitchellh/zig-overlay";
zigPkgs.inputs.nixpkgs.follows = "nixpkgs";
zlsPkg.url = "github:zigtools/zls/0.15.0";
zlsPkg.inputs.zig-overlay.follows = "zigPkgs";
zlsPkg.inputs.nixpkgs.follows = "nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
nixpkgs,
zigPkgs,
zlsPkg,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let
overlays = [
(final: prev: {
zigpkgs = zigPkgs.packages.${prev.system};
zls = zlsPkg.packages.${prev.system}.default;
})
];
pkgs = import nixpkgs {
inherit system;
inherit system overlays;
};
# We need crtbeginS.o for building.
@@ -32,7 +49,7 @@
targetPkgs =
pkgs: with pkgs; [
# Build Tools
zig
zigpkgs."0.15.1"
zls
python3
pkg-config
@@ -49,6 +66,7 @@
glib.dev
glibc.dev
zlib
zlib.dev
];
};
in

117
src/TestHTTPServer.zig Normal file
View File

@@ -0,0 +1,117 @@
const std = @import("std");
const TestHTTPServer = @This();
shutdown: bool,
listener: ?std.net.Server,
handler: Handler,
const Handler = *const fn (req: *std.http.Server.Request) anyerror!void;
pub fn init(handler: Handler) TestHTTPServer {
return .{
.shutdown = true,
.listener = null,
.handler = handler,
};
}
pub fn deinit(self: *TestHTTPServer) void {
self.shutdown = true;
if (self.listener) |*listener| {
listener.deinit();
}
}
pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
self.listener = try address.listen(.{ .reuse_address = true });
var listener = &self.listener.?;
wg.finish();
while (true) {
const conn = listener.accept() catch |err| {
if (self.shutdown) {
return;
}
return err;
};
const thrd = try std.Thread.spawn(.{}, handleConnection, .{ self, conn });
thrd.detach();
}
}
fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void {
defer conn.stream.close();
var req_buf: [2048]u8 = undefined;
var conn_reader = conn.stream.reader(&req_buf);
var conn_writer = conn.stream.writer(&req_buf);
var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface);
while (true) {
var req = http_server.receiveHead() catch |err| switch (err) {
error.ReadFailed => continue,
error.HttpConnectionClosing => continue,
else => {
std.debug.print("Test HTTP Server error: {}\n", .{err});
return err;
},
};
self.handler(&req) catch |err| {
std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err });
try req.respond("server error", .{ .status = .internal_server_error });
return;
};
}
}
pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) {
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
else => return err,
};
const stat = try file.stat();
var send_buffer: [4096]u8 = undefined;
var res = try req.respondStreaming(&send_buffer, .{
.content_length = stat.size,
.respond_options = .{
.extra_headers = &.{
.{ .name = "content-type", .value = getContentType(file_path) },
},
},
});
var read_buffer: [4096]u8 = undefined;
var reader = file.reader(&read_buffer);
_ = try res.writer.sendFileAll(&reader, .unlimited);
try res.writer.flush();
try res.end();
}
fn getContentType(file_path: []const u8) []const u8 {
if (std.mem.endsWith(u8, file_path, ".js")) {
return "application/json";
}
if (std.mem.endsWith(u8, file_path, ".html")) {
return "text/html";
}
if (std.mem.endsWith(u8, file_path, ".htm")) {
return "text/html";
}
if (std.mem.endsWith(u8, file_path, ".xml")) {
// some wpt tests do this
return "text/xml";
}
std.debug.print("TestHTTPServer asked to serve an unknown file type: {s}\n", .{file_path});
return "text/html";
}

View File

@@ -1,20 +1,22 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const log = @import("log.zig");
const Loop = @import("runtime/loop.zig").Loop;
const HttpClient = @import("http/client.zig").Client;
const Http = @import("http/Http.zig");
const Platform = @import("runtime/js.zig").Platform;
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
const Notification = @import("notification.zig").Notification;
// Container for global state / objects that various parts of the system
// might need.
pub const App = struct {
loop: *Loop,
http: Http,
config: Config,
platform: Platform,
allocator: Allocator,
telemetry: Telemetry,
http_client: HttpClient,
app_dir_path: ?[]const u8,
notification: *Notification,
@@ -28,38 +30,50 @@ pub const App = struct {
pub const Config = struct {
run_mode: RunMode,
tls_verify_host: bool = true,
http_proxy: ?std.Uri = null,
http_proxy: ?[:0]const u8 = null,
proxy_bearer_token: ?[:0]const u8 = null,
http_timeout_ms: ?u31 = null,
http_connect_timeout_ms: ?u31 = null,
http_max_host_open: ?u8 = null,
http_max_concurrent: ?u8 = null,
};
pub fn init(allocator: Allocator, config: Config) !*App {
const app = try allocator.create(App);
errdefer allocator.destroy(app);
const loop = try allocator.create(Loop);
errdefer allocator.destroy(loop);
loop.* = try Loop.init(allocator);
errdefer loop.deinit();
const notification = try Notification.init(allocator, null);
errdefer notification.deinit();
var http = try Http.init(allocator, .{
.max_host_open = config.http_max_host_open orelse 4,
.max_concurrent = config.http_max_concurrent orelse 10,
.timeout_ms = config.http_timeout_ms orelse 5000,
.connect_timeout_ms = config.http_connect_timeout_ms orelse 0,
.http_proxy = config.http_proxy,
.tls_verify_host = config.tls_verify_host,
.proxy_bearer_token = config.proxy_bearer_token,
});
errdefer http.deinit();
const platform = try Platform.init();
errdefer platform.deinit();
const app_dir_path = getAndMakeAppDir(allocator);
app.* = .{
.loop = loop,
.http = http,
.allocator = allocator,
.telemetry = undefined,
.platform = platform,
.app_dir_path = app_dir_path,
.notification = notification,
.http_client = try HttpClient.init(allocator, .{
.max_concurrent = 3,
.http_proxy = config.http_proxy,
.tls_verify_host = config.tls_verify_host,
}),
.config = config,
};
app.telemetry = Telemetry.init(app, config.run_mode);
app.telemetry = try Telemetry.init(app, config.run_mode);
errdefer app.telemetry.deinit();
try app.telemetry.register(app.notification);
return app;
@@ -71,10 +85,9 @@ pub const App = struct {
allocator.free(app_dir_path);
}
self.telemetry.deinit();
self.loop.deinit();
allocator.destroy(self.loop);
self.http_client.deinit();
self.notification.deinit();
self.http.deinit();
self.platform.deinit();
allocator.destroy(self);
}
};

52
src/browser/DataURI.zig Normal file
View File

@@ -0,0 +1,52 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
// Parses data:[<media-type>][;base64],<data>
pub fn parse(allocator: Allocator, src: []const u8) !?[]const u8 {
if (!std.mem.startsWith(u8, src, "data:")) {
return null;
}
const uri = src[5..];
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
var data = uri[data_starts + 1 ..];
// Extract the encoding.
const metadata = uri[0..data_starts];
if (std.mem.endsWith(u8, metadata, ";base64")) {
const decoder = std.base64.standard.Decoder;
const decoded_size = try decoder.calcSizeForSlice(data);
const buffer = try allocator.alloc(u8, decoded_size);
errdefer allocator.free(buffer);
try decoder.decode(buffer, data);
data = buffer;
}
return data;
}
const testing = @import("../testing.zig");
test "DataURI: parse valid" {
try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo");
try test_valid("data:text/javascript; charset=utf-8;,foo", "foo");
try test_valid("data:,foo", "foo");
}
test "DataURI: parse invalid" {
try test_cannot_parse("atad:,foo");
try test_cannot_parse("data:foo");
try test_cannot_parse("data:");
}
fn test_valid(uri: []const u8, expected: []const u8) !void {
defer testing.reset();
const data_uri = try parse(testing.arena_allocator, uri) orelse return error.TestFailed;
try testing.expectEqual(expected, data_uri);
}
fn test_cannot_parse(uri: []const u8) !void {
try testing.expectEqual(null, parse(undefined, uri));
}

174
src/browser/Scheduler.zig Normal file
View File

@@ -0,0 +1,174 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../log.zig");
const Allocator = std.mem.Allocator;
const Scheduler = @This();
high_priority: Queue,
// For repeating tasks. We only want to run these if there are other things to
// do. We don't, for example, want a window.setInterval or the page.runMicrotasks
// to block the page.wait.
low_priority: Queue,
// we expect allocator to be the page arena, hence we never call high_priority.deinit
pub fn init(allocator: Allocator) Scheduler {
return .{
.high_priority = Queue.init(allocator, {}),
.low_priority = Queue.init(allocator, {}),
};
}
pub fn reset(self: *Scheduler) void {
self.high_priority.clearRetainingCapacity();
self.low_priority.clearRetainingCapacity();
}
const AddOpts = struct {
name: []const u8 = "",
low_priority: bool = false,
};
pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: AddOpts) !void {
var low_priority = opts.low_priority;
if (ms > 5_000) {
// we don't want tasks in the far future to block page.wait from
// completing. However, if page.wait is called multiple times (maybe
// a CDP driver is wait for something to happen), then we do want
// to [eventually] run these when their time is up.
low_priority = true;
}
var q = if (low_priority) &self.low_priority else &self.high_priority;
return q.add(.{
.ms = std.time.milliTimestamp() + ms,
.ctx = ctx,
.func = func,
.name = opts.name,
});
}
pub fn runHighPriority(self: *Scheduler) !?i32 {
return self.runQueue(&self.high_priority);
}
pub fn runLowPriority(self: *Scheduler) !?i32 {
return self.runQueue(&self.low_priority);
}
fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
// this is O(1)
if (queue.count() == 0) {
return null;
}
const now = std.time.milliTimestamp();
var next = queue.peek();
while (next) |task| {
const time_to_next = task.ms - now;
if (time_to_next > 0) {
// @intCast is petty safe since we limit tasks to just 5 seconds
// in the future
return @intCast(time_to_next);
}
if (task.func(task.ctx)) |repeat_delay| {
// if we do (now + 0) then our WHILE loop will run endlessly.
// no task should ever return 0
std.debug.assert(repeat_delay != 0);
var copy = task;
copy.ms = now + repeat_delay;
try self.low_priority.add(copy);
}
_ = queue.remove();
next = queue.peek();
}
return null;
}
const Task = struct {
ms: i64,
func: Func,
ctx: *anyopaque,
name: []const u8,
const Func = *const fn (ctx: *anyopaque) ?u32;
};
const Queue = std.PriorityQueue(Task, void, struct {
fn compare(_: void, a: Task, b: Task) std.math.Order {
return std.math.order(a.ms, b.ms);
}
}.compare);
const testing = @import("../testing.zig");
test "Scheduler" {
defer testing.reset();
var task = TestTask{ .allocator = testing.arena_allocator };
var s = Scheduler.init(testing.arena_allocator);
try testing.expectEqual(null, s.runHighPriority());
try testing.expectEqual(0, task.calls.items.len);
try s.add(&task, TestTask.run1, 3, .{});
try testing.expectDelta(3, try s.runHighPriority(), 1);
try testing.expectEqual(0, task.calls.items.len);
std.Thread.sleep(std.time.ns_per_ms * 5);
try testing.expectEqual(null, s.runHighPriority());
try testing.expectEqualSlices(u32, &.{1}, task.calls.items);
try s.add(&task, TestTask.run2, 3, .{});
try s.add(&task, TestTask.run1, 2, .{});
std.Thread.sleep(std.time.ns_per_ms * 5);
try testing.expectDelta(null, try s.runHighPriority(), 1);
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
std.Thread.sleep(std.time.ns_per_ms * 5);
// won't run low_priority
try testing.expectEqual(null, try s.runHighPriority());
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
//runs low_priority
try testing.expectDelta(2, try s.runLowPriority(), 1);
try testing.expectEqualSlices(u32, &.{ 1, 1, 2, 2 }, task.calls.items);
}
const TestTask = struct {
allocator: Allocator,
calls: std.ArrayListUnmanaged(u32) = .{},
fn run1(ctx: *anyopaque) ?u32 {
var self: *TestTask = @ptrCast(@alignCast(ctx));
self.calls.append(self.allocator, 1) catch unreachable;
return null;
}
fn run2(ctx: *anyopaque) ?u32 {
var self: *TestTask = @ptrCast(@alignCast(ctx));
self.calls.append(self.allocator, 2) catch unreachable;
return 2;
}
};

View File

@@ -0,0 +1,835 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../log.zig");
const parser = @import("netsurf.zig");
const Env = @import("env.zig").Env;
const Page = @import("page.zig").Page;
const DataURI = @import("DataURI.zig");
const Http = @import("../http/Http.zig");
const Browser = @import("browser.zig").Browser;
const URL = @import("../url.zig").URL;
const Allocator = std.mem.Allocator;
const ArrayListUnmanaged = std.ArrayListUnmanaged;
const ScriptManager = @This();
page: *Page,
// used to prevent recursive evalutaion
is_evaluating: bool,
// used to prevent executing scripts while we're doing a blocking load
is_blocking: bool = false,
// Only once this is true can deferred scripts be run
static_scripts_done: bool,
// List of async scripts. We don't care about the execution order of these, but
// on shutdown/abort, we need to cleanup any pending ones.
asyncs: OrderList,
// When an async script is ready to be evaluated, it's moved from asyncs to
// this list. You might think we can evaluate an async script as soon as it's
// done, but we can only evaluate scripts when `is_blocking == false`. So this
// becomes a list of scripts to execute on the next evaluate().
asyncs_ready: OrderList,
// Normal scripts (non-deferred & non-async). These must be executed in order
scripts: OrderList,
// List of deferred scripts. These must be executed in order, but only once
// dom_loaded == true,
deferreds: OrderList,
shutdown: bool = false,
client: *Http.Client,
allocator: Allocator,
buffer_pool: BufferPool,
script_pool: std.heap.MemoryPool(PendingScript),
const OrderList = std.DoublyLinkedList;
pub fn init(browser: *Browser, page: *Page) ScriptManager {
// page isn't fully initialized, we can setup our reference, but that's it.
const allocator = browser.allocator;
return .{
.page = page,
.asyncs = .{},
.scripts = .{},
.deferreds = .{},
.asyncs_ready = .{},
.is_evaluating = false,
.allocator = allocator,
.client = browser.http_client,
.static_scripts_done = false,
.buffer_pool = BufferPool.init(allocator, 5),
.script_pool = std.heap.MemoryPool(PendingScript).init(allocator),
};
}
pub fn deinit(self: *ScriptManager) void {
self.reset();
self.buffer_pool.deinit();
self.script_pool.deinit();
}
pub fn reset(self: *ScriptManager) void {
self.clearList(&self.asyncs);
self.clearList(&self.scripts);
self.clearList(&self.deferreds);
self.clearList(&self.asyncs_ready);
self.static_scripts_done = false;
}
fn clearList(_: *const ScriptManager, list: *OrderList) void {
while (list.first) |node| {
const pending_script: *PendingScript = @fieldParentPtr("node", node);
// this removes it from the list
pending_script.deinit();
}
std.debug.assert(list.first == null);
}
pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
if (try parser.elementGetAttribute(element, "nomodule") != null) {
// these scripts should only be loaded if we don't support modules
// but since we do support modules, we can just skip them.
return;
}
// If a script tag gets dynamically created and added to the dom:
// document.getElementsByTagName('head')[0].appendChild(script)
// that script tag will immediately get executed by our scriptAddedCallback.
// However, if the location where the script tag is inserted happens to be
// below where processHTMLDoc currently is, then we'll re-run that same script
// again in processHTMLDoc. This flag is used to let us know if a specific
// <script> has already been processed.
if (try parser.scriptGetProcessed(@ptrCast(element))) {
return;
}
try parser.scriptSetProcessed(@ptrCast(element), true);
const kind: Script.Kind = blk: {
const script_type = try parser.elementGetAttribute(element, "type") orelse break :blk .javascript;
if (script_type.len == 0) {
break :blk .javascript;
}
if (std.ascii.eqlIgnoreCase(script_type, "application/javascript")) {
break :blk .javascript;
}
if (std.ascii.eqlIgnoreCase(script_type, "text/javascript")) {
break :blk .javascript;
}
if (std.ascii.eqlIgnoreCase(script_type, "module")) {
break :blk .module;
}
// "type" could be anything, but only the above are ones we need to process.
// Common other ones are application/json, application/ld+json, text/template
return;
};
const page = self.page;
var source: Script.Source = undefined;
var remote_url: ?[:0]const u8 = null;
if (try parser.elementGetAttribute(element, "src")) |src| {
if (try DataURI.parse(page.arena, src)) |data_uri| {
source = .{ .@"inline" = data_uri };
}
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true });
source = .{ .remote = .{} };
} else {
const inline_source = try parser.nodeTextContent(@ptrCast(element)) orelse return;
source = .{ .@"inline" = inline_source };
}
var script = Script{
.kind = kind,
.element = element,
.source = source,
.url = remote_url orelse page.url.raw,
.is_defer = if (remote_url == null) false else try parser.elementGetAttribute(element, "defer") != null,
.is_async = if (remote_url == null) false else try parser.elementGetAttribute(element, "async") != null,
};
if (source == .@"inline" and self.scripts.first == null) {
// inline script with no pending scripts, execute it immediately.
// (if there is a pending script, then we cannot execute this immediately
// as it needs to best executed in order)
return script.eval(page);
}
const pending_script = try self.script_pool.create();
errdefer self.script_pool.destroy(pending_script);
pending_script.* = .{
.script = script,
.complete = false,
.manager = self,
.node = .{},
};
if (source == .@"inline") {
// if we're here, it means that we have pending scripts (i.e. self.scripts
// is not empty). Because the script is inline, it's complete/ready, but
// we need to process them in order
pending_script.complete = true;
self.scripts.append(&pending_script.node);
return;
} else {
log.debug(.http, "script queue", .{ .url = remote_url.? });
}
pending_script.getList().append(&pending_script.node);
errdefer pending_script.deinit();
var headers = try Http.Headers.init();
try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers);
try self.client.request(.{
.url = remote_url.?,
.ctx = pending_script,
.method = .GET,
.headers = headers,
.cookie_jar = page.cookie_jar,
.resource_type = .script,
.start_callback = if (log.enabled(.http, .debug)) startCallback else null,
.header_callback = headerCallback,
.data_callback = dataCallback,
.done_callback = doneCallback,
.error_callback = errorCallback,
});
}
// @TODO: Improving this would have the simplest biggest performance improvement
// for most sites.
//
// For JS imports (both static and dynamic), we currently block to get the
// result (the content of the file).
//
// For static imports, this is necessary, since v8 is expecting the compiled module
// as part of the function return. (we should try to pre-load the JavaScript
// source via module.GetModuleRequests(), but that's for a later time).
//
// For dynamic dynamic imports, this is not strictly necessary since the v8
// call returns a Promise; we could make this a normal get call, associated with
// the promise, and when done, resolve the promise.
//
// In both cases, for now at least, we just issue a "blocking" request. We block
// by ticking the http client until the script is complete.
//
// This uses the client.blockingRequest call which has a dedicated handle for
// these blocking requests. Because they are blocking, we're guaranteed to have
// only 1 at a time, thus the 1 reserved handle.
//
// You almost don't need the http client's blocking handle. In most cases, you
// should always have 1 free handle whenever you get here, because we always
// release the handle before executing the doneCallback. So, if a module does:
// import * as x from 'blah'
// And we need to load 'blah', there should always be 1 free handle - the handle
// of the http GET we just completed before executing the module.
// The exception to this, and the reason we need a special blocking handle, is
// for inline modules within the HTML page itself:
// <script type=module>import ....</script>
// Unlike external modules which can only ever be executed after releasing an
// http handle, these are executed without there necessarily being a free handle.
// Thus, Http/Client.zig maintains a dedicated handle for these calls.
pub fn blockingGet(self: *ScriptManager, url: [:0]const u8) !BlockingResult {
std.debug.assert(self.is_blocking == false);
self.is_blocking = true;
defer {
self.is_blocking = false;
// we blocked evaluation while loading this script, there could be
// scripts ready to process.
self.evaluate();
}
var blocking = Blocking{
.allocator = self.allocator,
.buffer_pool = &self.buffer_pool,
};
var headers = try Http.Headers.init();
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
var client = self.client;
try client.blockingRequest(.{
.url = url,
.method = .GET,
.headers = headers,
.cookie_jar = self.page.cookie_jar,
.ctx = &blocking,
.resource_type = .script,
.start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null,
.header_callback = Blocking.headerCallback,
.data_callback = Blocking.dataCallback,
.done_callback = Blocking.doneCallback,
.error_callback = Blocking.errorCallback,
});
// rely on http's timeout settings to avoid an endless/long loop.
while (true) {
_ = try client.tick(200);
switch (blocking.state) {
.running => {},
.done => |result| return result,
.err => |err| return err,
}
}
}
pub fn staticScriptsDone(self: *ScriptManager) void {
std.debug.assert(self.static_scripts_done == false);
self.static_scripts_done = true;
}
// try to evaluate completed scripts (in order). This is called whenever a script
// is completed.
fn evaluate(self: *ScriptManager) void {
if (self.is_evaluating) {
// It's possible for a script.eval to cause evaluate to be called again.
// This is particularly true with blockingGet, but even without this,
// it's theoretically possible (but unlikely). We could make this work
// but there's little reason to support the complexity.
return;
}
if (self.is_blocking) {
// Cannot evaluate scripts while a blocking-load is in progress. Not
// only could that result in incorrect evaluation order, it could
// trigger another blocking request, while we're doing a blocking request.
return;
}
const page = self.page;
self.is_evaluating = true;
defer self.is_evaluating = false;
// every script in asyncs_ready is ready to be evaluated.
while (self.asyncs_ready.first) |n| {
var pending_script: *PendingScript = @fieldParentPtr("node", n);
defer pending_script.deinit();
pending_script.script.eval(page);
}
while (self.scripts.first) |n| {
var pending_script: *PendingScript = @fieldParentPtr("node", n);
if (pending_script.complete == false) {
return;
}
defer pending_script.deinit();
pending_script.script.eval(page);
}
if (self.static_scripts_done == false) {
// We can only execute deferred scripts if
// 1 - all the normal scripts are done
// 2 - we've finished parsing the HTML and at least queued all the scripts
// The last one isn't obvious, but it's possible for self.scripts to
// be empty not because we're done executing all the normal scripts
// but because we're done executing some (or maybe none), but we're still
// parsing the HTML.
return;
}
while (self.deferreds.first) |n| {
var pending_script: *PendingScript = @fieldParentPtr("node", n);
if (pending_script.complete == false) {
return;
}
defer pending_script.deinit();
pending_script.script.eval(page);
}
// When all scripts (normal and deferred) are done loading, the document
// state changes (this ultimately triggers the DOMContentLoaded event)
page.documentIsLoaded();
if (self.asyncs.first == null) {
// 1 - there are no async scripts pending
// 2 - we checkecked static_scripts_done == true above
// 3 - we drained self.scripts above
// 4 - we drained self.deferred above
page.documentIsComplete();
}
}
pub fn isDone(self: *const ScriptManager) bool {
return self.asyncs.first == null and // there are no more async scripts
self.static_scripts_done and // and we've finished parsing the HTML to queue all <scripts>
self.scripts.first == null and // and there are no more <script src=> to wait for
self.deferreds.first == null; // and there are no more <script defer src=> to wait for
}
fn startCallback(transfer: *Http.Transfer) !void {
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
script.startCallback(transfer) catch |err| {
log.err(.http, "SM.startCallback", .{ .err = err, .transfer = transfer });
return err;
};
}
fn headerCallback(transfer: *Http.Transfer) !void {
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
try script.headerCallback(transfer);
}
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
script.dataCallback(transfer, data) catch |err| {
log.err(.http, "SM.dataCallback", .{ .err = err, .transfer = transfer, .len = data.len });
return err;
};
}
fn doneCallback(ctx: *anyopaque) !void {
const script: *PendingScript = @ptrCast(@alignCast(ctx));
script.doneCallback();
}
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
const script: *PendingScript = @ptrCast(@alignCast(ctx));
script.errorCallback(err);
}
// A script which is pending execution.
// It could be pending because:
// (a) we're still downloading its content or
// (b) this is a non-async script that has to be executed in order
pub const PendingScript = struct {
script: Script,
complete: bool,
node: OrderList.Node,
manager: *ScriptManager,
fn deinit(self: *PendingScript) void {
const script = &self.script;
const manager = self.manager;
if (script.source == .remote) {
manager.buffer_pool.release(script.source.remote);
}
self.getList().remove(&self.node);
}
fn remove(self: *PendingScript) void {
if (self.node) |*node| {
self.getList().remove(node);
self.node = null;
}
}
fn startCallback(self: *PendingScript, transfer: *Http.Transfer) !void {
_ = self;
log.debug(.http, "script fetch start", .{ .req = transfer });
}
fn headerCallback(self: *PendingScript, transfer: *Http.Transfer) !void {
const header = &transfer.response_header.?;
if (header.status != 200) {
log.info(.http, "script header", .{
.req = transfer,
.status = header.status,
.content_type = header.contentType(),
});
return;
}
log.debug(.http, "script header", .{
.req = transfer,
.status = header.status,
.content_type = header.contentType(),
});
// If this isn't true, then we'll likely leak memory. If you don't
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
// will fail. This assertion exists to catch incorrect assumptions about
// how libcurl works, or about how we've configured it.
std.debug.assert(self.script.source.remote.capacity == 0);
var buffer = self.manager.buffer_pool.get();
if (transfer.getContentLength()) |cl| {
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
}
self.script.source = .{ .remote = buffer };
}
fn dataCallback(self: *PendingScript, transfer: *Http.Transfer, data: []const u8) !void {
_ = transfer;
// too verbose
// log.debug(.http, "script data chunk", .{
// .req = transfer,
// .len = data.len,
// });
try self.script.source.remote.appendSlice(self.manager.allocator, data);
}
fn doneCallback(self: *PendingScript) void {
log.debug(.http, "script fetch complete", .{ .req = self.script.url });
const manager = self.manager;
self.complete = true;
if (self.script.is_async) {
manager.asyncs.remove(&self.node);
manager.asyncs_ready.append(&self.node);
}
manager.evaluate();
}
fn errorCallback(self: *PendingScript, err: anyerror) void {
log.warn(.http, "script fetch error", .{ .req = self.script.url, .err = err });
const manager = self.manager;
self.deinit();
if (manager.shutdown) {
return;
}
manager.evaluate();
}
fn getList(self: *const PendingScript) *OrderList {
// When a script has both the async and defer flag set, it should be
// treated as async. Async is newer, so some websites use both so that
// if async isn't known, it'll fallback to defer.
const script = &self.script;
if (script.is_async) {
return if (self.complete) &self.manager.asyncs_ready else &self.manager.asyncs;
}
if (script.is_defer) {
return &self.manager.deferreds;
}
return &self.manager.scripts;
}
};
const Script = struct {
kind: Kind,
url: []const u8,
is_async: bool,
is_defer: bool,
source: Source,
element: *parser.Element,
const Kind = enum {
module,
javascript,
};
const Callback = union(enum) {
string: []const u8,
function: Env.Function,
};
const Source = union(enum) {
@"inline": []const u8,
remote: std.ArrayListUnmanaged(u8),
fn content(self: Source) []const u8 {
return switch (self) {
.remote => |buf| buf.items,
.@"inline" => |c| c,
};
}
};
fn eval(self: *Script, page: *Page) void {
page.setCurrentScript(@ptrCast(self.element)) catch |err| {
log.err(.browser, "set document script", .{ .err = err });
return;
};
defer page.setCurrentScript(null) catch |err| {
log.err(.browser, "clear document script", .{ .err = err });
};
// inline scripts aren't cached. remote ones are.
const cacheable = self.source == .remote;
const url = self.url;
log.info(.browser, "executing script", .{
.src = url,
.kind = self.kind,
.cacheable = cacheable,
});
const js_context = page.main_context;
var try_catch: Env.TryCatch = undefined;
try_catch.init(js_context);
defer try_catch.deinit();
const success = blk: {
const content = self.source.content();
switch (self.kind) {
.javascript => _ = js_context.eval(content, url) catch break :blk false,
.module => {
// We don't care about waiting for the evaluation here.
_ = js_context.module(content, url, cacheable) catch break :blk false;
},
}
break :blk true;
};
if (success) {
self.executeCallback("onload", page);
return;
}
if (page.delayed_navigation) {
// If we're navigating to another page, an error is expected
// since we probably terminated the script forcefully.
return;
}
const msg = try_catch.err(page.arena) catch |err| @errorName(err) orelse "unknown";
log.warn(.user_script, "eval script", .{
.url = url,
.err = msg,
.cacheable = cacheable,
});
self.executeCallback("onerror", page);
}
fn executeCallback(self: *const Script, comptime typ: []const u8, page: *Page) void {
const callback = self.getCallback(typ, page) orelse return;
switch (callback) {
.string => |str| {
var try_catch: Env.TryCatch = undefined;
try_catch.init(page.main_context);
defer try_catch.deinit();
_ = page.main_context.exec(str, typ) catch |err| {
const msg = try_catch.err(page.arena) catch @errorName(err) orelse "unknown";
log.warn(.user_script, "script callback", .{
.url = self.url,
.err = msg,
.type = typ,
.@"inline" = true,
});
};
},
.function => |f| {
const Event = @import("events/event.zig").Event;
const loadevt = parser.eventCreate() catch |err| {
log.err(.browser, "SM event creation", .{ .err = err });
return;
};
defer parser.eventDestroy(loadevt);
var result: Env.Function.Result = undefined;
const iface = Event.toInterface(loadevt) catch |err| {
log.err(.browser, "SM event interface", .{ .err = err });
return;
};
f.tryCall(void, .{iface}, &result) catch {
log.warn(.user_script, "script callback", .{
.url = self.url,
.type = typ,
.err = result.exception,
.stack = result.stack,
.@"inline" = false,
});
};
},
}
}
fn getCallback(self: *const Script, comptime typ: []const u8, page: *Page) ?Callback {
const element = self.element;
// first we check if there was an el.onload set directly on the
// element in JavaScript (if so, it'd be stored in the node state)
if (page.getNodeState(@ptrCast(element))) |se| {
if (@field(se, typ)) |function| {
return .{ .function = function };
}
}
// if we have no node state, or if the node state has no onload/onerror
// then check for the onload/onerror attribute
if (parser.elementGetAttribute(element, typ) catch null) |string| {
return .{ .string = string };
}
return null;
}
};
const BufferPool = struct {
count: usize,
available: List = .{},
allocator: Allocator,
max_concurrent_transfers: u8,
mem_pool: std.heap.MemoryPool(Container),
const List = std.DoublyLinkedList;
const Container = struct {
node: List.Node,
buf: std.ArrayListUnmanaged(u8),
};
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
return .{
.available = .{},
.count = 0,
.allocator = allocator,
.max_concurrent_transfers = max_concurrent_transfers,
.mem_pool = std.heap.MemoryPool(Container).init(allocator),
};
}
fn deinit(self: *BufferPool) void {
const allocator = self.allocator;
var node = self.available.first;
while (node) |n| {
const container: *Container = @fieldParentPtr("node", n);
container.buf.deinit(allocator);
node = n.next;
}
self.mem_pool.deinit();
}
fn get(self: *BufferPool) std.ArrayListUnmanaged(u8) {
const node = self.available.popFirst() orelse {
// return a new buffer
return .{};
};
self.count -= 1;
const container: *Container = @fieldParentPtr("node", node);
defer self.mem_pool.destroy(container);
return container.buf;
}
fn release(self: *BufferPool, buffer: ArrayListUnmanaged(u8)) void {
// create mutable copy
var b = buffer;
if (self.count == self.max_concurrent_transfers) {
b.deinit(self.allocator);
return;
}
const container = self.mem_pool.create() catch |err| {
b.deinit(self.allocator);
log.err(.http, "SM BufferPool release", .{ .err = err });
return;
};
b.clearRetainingCapacity();
container.* = .{ .buf = b, .node = .{} };
self.count += 1;
self.available.append(&container.node);
}
};
const Blocking = struct {
allocator: Allocator,
buffer_pool: *BufferPool,
state: State = .{ .running = {} },
buffer: std.ArrayListUnmanaged(u8) = .{},
const State = union(enum) {
running: void,
err: anyerror,
done: BlockingResult,
};
fn startCallback(transfer: *Http.Transfer) !void {
log.debug(.http, "script fetch start", .{ .req = transfer, .blocking = true });
}
fn headerCallback(transfer: *Http.Transfer) !void {
const header = &transfer.response_header.?;
log.debug(.http, "script header", .{
.req = transfer,
.blocking = true,
.status = header.status,
.content_type = header.contentType(),
});
if (header.status != 200) {
return error.InvalidStatusCode;
}
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
self.buffer = self.buffer_pool.get();
}
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
// too verbose
// log.debug(.http, "script data chunk", .{
// .req = transfer,
// .blocking = true,
// });
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
self.buffer.appendSlice(self.allocator, data) catch |err| {
log.err(.http, "SM.dataCallback", .{
.err = err,
.len = data.len,
.blocking = true,
.transfer = transfer,
});
return err;
};
}
fn doneCallback(ctx: *anyopaque) !void {
var self: *Blocking = @ptrCast(@alignCast(ctx));
self.state = .{ .done = .{
.buffer = self.buffer,
.buffer_pool = self.buffer_pool,
} };
}
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
var self: *Blocking = @ptrCast(@alignCast(ctx));
self.state = .{ .err = err };
self.buffer_pool.release(self.buffer);
}
};
pub const BlockingResult = struct {
buffer: std.ArrayListUnmanaged(u8),
buffer_pool: *BufferPool,
pub fn deinit(self: *BlockingResult) void {
self.buffer_pool.release(self.buffer);
}
pub fn src(self: *const BlockingResult) []const u8 {
return self.buffer.items;
}
};

View File

@@ -28,7 +28,11 @@
const Env = @import("env.zig").Env;
const parser = @import("netsurf.zig");
const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration;
const DataSet = @import("html/DataSet.zig");
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
const StyleSheet = @import("cssom/StyleSheet.zig");
const CSSStyleSheet = @import("cssom/CSSStyleSheet.zig");
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");
// for HTMLScript (but probably needs to be added to more)
onload: ?Env.Function = null,
@@ -36,12 +40,21 @@ onerror: ?Env.Function = null,
// for HTMLElement
style: CSSStyleDeclaration = .empty,
dataset: ?DataSet = null,
template_content: ?*parser.DocumentFragment = null,
// For dom/element
shadow_root: ?*ShadowRoot = null,
// for html/document
ready_state: ReadyState = .loading,
// for html/HTMLStyleElement
style_sheet: ?*StyleSheet = null,
// for dom/document
active_element: ?*parser.Element = null,
adopted_style_sheets: ?Env.JsObject = null,
// for HTMLSelectElement
// By default, if no option is explicitly selected, the first option should

View File

@@ -27,7 +27,8 @@ const App = @import("../app.zig").App;
const Session = @import("session.zig").Session;
const Notification = @import("../notification.zig").Notification;
const http = @import("../http/client.zig");
const log = @import("../log.zig");
const HttpClient = @import("../http/Client.zig");
// Browser is an instance of the browser.
// You can create multiple browser instances.
@@ -37,7 +38,7 @@ pub const Browser = struct {
app: *App,
session: ?Session,
allocator: Allocator,
http_client: *http.Client,
http_client: *HttpClient,
page_arena: ArenaAllocator,
session_arena: ArenaAllocator,
transfer_arena: ArenaAllocator,
@@ -47,10 +48,12 @@ pub const Browser = struct {
pub fn init(app: *App) !Browser {
const allocator = app.allocator;
const env = try Env.init(allocator, .{});
const env = try Env.init(allocator, &app.platform, .{});
errdefer env.deinit();
const notification = try Notification.init(allocator, app.notification);
app.http.client.notification = notification;
app.http.client.next_request_id = 0; // Should we track ids in CDP only?
errdefer notification.deinit();
return .{
@@ -59,7 +62,7 @@ pub const Browser = struct {
.session = null,
.allocator = allocator,
.notification = notification,
.http_client = &app.http_client,
.http_client = app.http.client,
.page_arena = ArenaAllocator.init(allocator),
.session_arena = ArenaAllocator.init(allocator),
.transfer_arena = ArenaAllocator.init(allocator),
@@ -73,6 +76,7 @@ pub const Browser = struct {
self.page_arena.deinit();
self.session_arena.deinit();
self.transfer_arena.deinit();
self.http_client.notification = null;
self.notification.deinit();
self.state_pool.deinit();
}
@@ -95,17 +99,18 @@ pub const Browser = struct {
}
pub fn runMicrotasks(self: *const Browser) void {
return self.env.runMicrotasks();
self.env.runMicrotasks();
}
pub fn runMessageLoop(self: *const Browser) void {
while (self.env.pumpMessageLoop()) {
log.debug(.browser, "pumpMessageLoop", .{});
}
self.env.runIdleTasks();
}
};
const testing = @import("../testing.zig");
test "Browser" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
// this will crash if ICU isn't properly configured / ininitialized
try runner.testCases(&.{
.{ "new Intl.DateTimeFormat()", "[object Intl.DateTimeFormat]" },
}, .{});
try testing.htmlRunner("browser.html");
}

View File

@@ -18,62 +18,61 @@
const std = @import("std");
const builtin = @import("builtin");
const log = @import("../../log.zig");
const Allocator = std.mem.Allocator;
const Page = @import("../page.zig").Page;
const JsObject = @import("../env.zig").Env.JsObject;
const log = if (builtin.is_test) &test_capture else @import("../../log.zig");
pub const Console = struct {
// TODO: configurable writer
timers: std.StringHashMapUnmanaged(u32) = .{},
counts: std.StringHashMapUnmanaged(u32) = .{},
pub fn static_lp(values: []JsObject, page: *Page) !void {
pub fn _lp(values: []JsObject, page: *Page) !void {
if (values.len == 0) {
return;
}
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
}
pub fn static_log(values: []JsObject, page: *Page) !void {
pub fn _log(values: []JsObject, page: *Page) !void {
if (values.len == 0) {
return;
}
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
}
pub fn static_info(values: []JsObject, page: *Page) !void {
return static_log(values, page);
pub fn _info(values: []JsObject, page: *Page) !void {
return _log(values, page);
}
pub fn static_debug(values: []JsObject, page: *Page) !void {
pub fn _debug(values: []JsObject, page: *Page) !void {
if (values.len == 0) {
return;
}
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
}
pub fn static_warn(values: []JsObject, page: *Page) !void {
pub fn _warn(values: []JsObject, page: *Page) !void {
if (values.len == 0) {
return;
}
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
}
pub fn static_error(values: []JsObject, page: *Page) !void {
pub fn _error(values: []JsObject, page: *Page) !void {
if (values.len == 0) {
return;
}
log.info(.console, "error", .{
log.warn(.console, "error", .{
.args = try serializeValues(values, page),
.stack = page.stackTrace() catch "???",
});
}
pub fn static_clear() void {}
pub fn _clear() void {}
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
const label = label_ orelse "default";
@@ -134,7 +133,7 @@ pub const Console = struct {
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
}
pub fn static_assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
if (assertion.isTruthy()) {
return;
}
@@ -165,165 +164,76 @@ pub const Console = struct {
};
fn timestamp() u32 {
const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch unreachable;
return @intCast(ts.sec);
return @import("../../datetime.zig").timestamp();
}
var test_capture = TestCapture{};
const testing = @import("../../testing.zig");
test "Browser.Console" {
defer testing.reset();
// const testing = @import("../../testing.zig");
// test "Browser.Console" {
// defer testing.reset();
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
// var runner = try testing.jsRunner(testing.tracking_allocator, .{});
// defer runner.deinit();
{
try runner.testCases(&.{
.{ "console.log('a')", "undefined" },
.{ "console.warn('hello world', 23, true, new Object())", "undefined" },
}, .{});
// {
// try runner.testCases(&.{
// .{ "console.log('a')", "undefined" },
// .{ "console.warn('hello world', 23, true, new Object())", "undefined" },
// }, .{});
const captured = test_capture.captured.items;
try testing.expectEqual("[info] args= 1: a", captured[0]);
try testing.expectEqual("[warn] args= 1: hello world 2: 23 3: true 4: #<Object>", captured[1]);
}
// const captured = test_capture.captured.items;
// try testing.expectEqual("[info] args= 1: a", captured[0]);
// try testing.expectEqual("[warn] args= 1: hello world 2: 23 3: true 4: #<Object>", captured[1]);
// }
{
test_capture.reset();
try runner.testCases(&.{
.{ "console.countReset()", "undefined" },
.{ "console.count()", "undefined" },
.{ "console.count('teg')", "undefined" },
.{ "console.count('teg')", "undefined" },
.{ "console.count('teg')", "undefined" },
.{ "console.count()", "undefined" },
.{ "console.countReset('teg')", "undefined" },
.{ "console.countReset()", "undefined" },
.{ "console.count()", "undefined" },
}, .{});
// {
// test_capture.reset();
// try runner.testCases(&.{
// .{ "console.countReset()", "undefined" },
// .{ "console.count()", "undefined" },
// .{ "console.count('teg')", "undefined" },
// .{ "console.count('teg')", "undefined" },
// .{ "console.count('teg')", "undefined" },
// .{ "console.count()", "undefined" },
// .{ "console.countReset('teg')", "undefined" },
// .{ "console.countReset()", "undefined" },
// .{ "console.count()", "undefined" },
// }, .{});
const captured = test_capture.captured.items;
try testing.expectEqual("[invalid counter] label=default", captured[0]);
try testing.expectEqual("[count] label=default count=1", captured[1]);
try testing.expectEqual("[count] label=teg count=1", captured[2]);
try testing.expectEqual("[count] label=teg count=2", captured[3]);
try testing.expectEqual("[count] label=teg count=3", captured[4]);
try testing.expectEqual("[count] label=default count=2", captured[5]);
try testing.expectEqual("[count reset] label=teg count=3", captured[6]);
try testing.expectEqual("[count reset] label=default count=2", captured[7]);
try testing.expectEqual("[count] label=default count=1", captured[8]);
}
// const captured = test_capture.captured.items;
// try testing.expectEqual("[invalid counter] label=default", captured[0]);
// try testing.expectEqual("[count] label=default count=1", captured[1]);
// try testing.expectEqual("[count] label=teg count=1", captured[2]);
// try testing.expectEqual("[count] label=teg count=2", captured[3]);
// try testing.expectEqual("[count] label=teg count=3", captured[4]);
// try testing.expectEqual("[count] label=default count=2", captured[5]);
// try testing.expectEqual("[count reset] label=teg count=3", captured[6]);
// try testing.expectEqual("[count reset] label=default count=2", captured[7]);
// try testing.expectEqual("[count] label=default count=1", captured[8]);
// }
{
test_capture.reset();
try runner.testCases(&.{
.{ "console.assert(true)", "undefined" },
.{ "console.assert('a', 2, 3, 4)", "undefined" },
.{ "console.assert('')", "undefined" },
.{ "console.assert('', 'x', true)", "undefined" },
.{ "console.assert(false, 'x')", "undefined" },
}, .{});
// {
// test_capture.reset();
// try runner.testCases(&.{
// .{ "console.assert(true)", "undefined" },
// .{ "console.assert('a', 2, 3, 4)", "undefined" },
// .{ "console.assert('')", "undefined" },
// .{ "console.assert('', 'x', true)", "undefined" },
// .{ "console.assert(false, 'x')", "undefined" },
// }, .{});
const captured = test_capture.captured.items;
try testing.expectEqual("[assertion failed] values=", captured[0]);
try testing.expectEqual("[assertion failed] values= 1: x 2: true", captured[1]);
try testing.expectEqual("[assertion failed] values= 1: x", captured[2]);
}
// const captured = test_capture.captured.items;
// try testing.expectEqual("[assertion failed] values=", captured[0]);
// try testing.expectEqual("[assertion failed] values= 1: x 2: true", captured[1]);
// try testing.expectEqual("[assertion failed] values= 1: x", captured[2]);
// }
{
test_capture.reset();
try runner.testCases(&.{
.{ "[1].forEach(console.log)", null },
}, .{});
// {
// test_capture.reset();
// try runner.testCases(&.{
// .{ "[1].forEach(console.log)", null },
// }, .{});
const captured = test_capture.captured.items;
try testing.expectEqual("[info] args= 1: 1 2: 0 3: [1]", captured[0]);
}
}
const TestCapture = struct {
captured: std.ArrayListUnmanaged([]const u8) = .{},
fn separator(_: *const TestCapture) []const u8 {
return " ";
}
fn reset(self: *TestCapture) void {
self.captured = .{};
}
fn debug(
self: *TestCapture,
comptime scope: @Type(.enum_literal),
comptime msg: []const u8,
args: anytype,
) void {
self.capture(scope, msg, args);
}
fn info(
self: *TestCapture,
comptime scope: @Type(.enum_literal),
comptime msg: []const u8,
args: anytype,
) void {
self.capture(scope, msg, args);
}
fn warn(
self: *TestCapture,
comptime scope: @Type(.enum_literal),
comptime msg: []const u8,
args: anytype,
) void {
self.capture(scope, msg, args);
}
fn err(
self: *TestCapture,
comptime scope: @Type(.enum_literal),
comptime msg: []const u8,
args: anytype,
) void {
self.capture(scope, msg, args);
}
fn fatal(
self: *TestCapture,
comptime scope: @Type(.enum_literal),
comptime msg: []const u8,
args: anytype,
) void {
self.capture(scope, msg, args);
}
fn capture(
self: *TestCapture,
comptime scope: @Type(.enum_literal),
comptime msg: []const u8,
args: anytype,
) void {
self._capture(scope, msg, args) catch unreachable;
}
fn _capture(
self: *TestCapture,
comptime scope: @Type(.enum_literal),
comptime msg: []const u8,
args: anytype,
) !void {
std.debug.assert(scope == .console);
const allocator = testing.arena_allocator;
var buf: std.ArrayListUnmanaged(u8) = .empty;
try buf.appendSlice(allocator, "[" ++ msg ++ "] ");
inline for (@typeInfo(@TypeOf(args)).@"struct".fields) |f| {
try buf.appendSlice(allocator, f.name);
try buf.append(allocator, '=');
try @import("../../log.zig").writeValue(.pretty, @field(args, f.name), buf.writer(allocator));
try buf.append(allocator, ' ');
}
self.captured.append(testing.arena_allocator, std.mem.trimRight(u8, buf.items, " ")) catch unreachable;
}
};
// const captured = test_capture.captured.items;
// try testing.expectEqual("[info] args= 1: 1 2: 0 3: [1]", captured[0]);
// }
// }

View File

@@ -17,16 +17,21 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Env = @import("../env.zig").Env;
const uuidv4 = @import("../../id.zig").uuidv4;
// https://w3c.github.io/webcrypto/#crypto-interface
pub const Crypto = struct {
pub fn _getRandomValues(_: *const Crypto, into: RandomValues) !void {
_not_empty: bool = true,
pub fn _getRandomValues(_: *const Crypto, js_obj: Env.JsObject) !Env.JsObject {
var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues);
const buf = into.asBuffer();
if (buf.len > 65_536) {
return error.QuotaExceededError;
}
std.crypto.random.bytes(buf);
return js_obj;
}
pub fn _randomUUID(_: *const Crypto) [36]u8 {
@@ -47,36 +52,20 @@ const RandomValues = union(enum) {
uint64: []u64,
fn asBuffer(self: RandomValues) []u8 {
switch (self) {
.int8 => |b| return (@as([]u8, @ptrCast(b)))[0..b.len],
.uint8 => |b| return (@as([]u8, @ptrCast(b)))[0..b.len],
.int16 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
.uint16 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
.int32 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
.uint32 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
.int64 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
.uint64 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
}
return switch (self) {
.int8 => |b| (@as([]u8, @ptrCast(b)))[0..b.len],
.uint8 => |b| (@as([]u8, @ptrCast(b)))[0..b.len],
.int16 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
.uint16 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
.int32 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
.uint32 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
.int64 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
.uint64 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
};
}
};
const testing = @import("../../testing.zig");
test "Browser.Crypto" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "const a = crypto.randomUUID();", "undefined" },
.{ "const b = crypto.randomUUID();", "undefined" },
.{ "a.length;", "36" },
.{ "a.length;", "36" },
.{ "a == b;", "false" },
}, .{});
try runner.testCases(&.{
.{ "try { crypto.getRandomValues(new BigUint64Array(8193)) } catch(e) { e.message == 'QuotaExceededError' }", "true" },
.{ "let r1 = new Int32Array(5)", "undefined" },
.{ "crypto.getRandomValues(r1)", "undefined" },
.{ "new Set(r1).size", "5" },
}, .{});
test "Browser: Crypto" {
try testing.htmlRunner("crypto.html");
}

View File

@@ -23,6 +23,20 @@ const std = @import("std");
const Selector = @import("selector.zig").Selector;
const parser = @import("parser.zig");
pub const Interfaces = .{
Css,
};
// https://developer.mozilla.org/en-US/docs/Web/API/CSS
pub const Css = struct {
_not_empty: bool = true,
pub fn _supports(_: *Css, _: []const u8, _: ?[]const u8) bool {
// TODO: Actually respond with which CSS features we support.
return true;
}
};
// parse parse a selector string and returns the parsed result or an error.
pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions) parser.ParseError!Selector {
var p = parser.Parser{ .s = s, .i = 0, .opts = opts };
@@ -31,7 +45,7 @@ pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions)
// matchFirst call m.match with the first node that matches the selector s, from the
// descendants of n and returns true. If none matches, it returns false.
pub fn matchFirst(s: Selector, node: anytype, m: anytype) !bool {
pub fn matchFirst(s: *const Selector, node: anytype, m: anytype) !bool {
var c = try node.firstChild();
while (true) {
if (c == null) break;
@@ -49,7 +63,7 @@ pub fn matchFirst(s: Selector, node: anytype, m: anytype) !bool {
// matchAll call m.match with the all the nodes that matches the selector s, from the
// descendants of n.
pub fn matchAll(s: Selector, node: anytype, m: anytype) !void {
pub fn matchAll(s: *const Selector, node: anytype, m: anytype) !void {
var c = try node.firstChild();
while (true) {
if (c == null) break;
@@ -174,3 +188,8 @@ test "parse" {
defer s.deinit(alloc);
}
}
const testing = @import("../../testing.zig");
test "Browser: CSS" {
try testing.htmlRunner("css.html");
}

View File

@@ -20,18 +20,23 @@ const std = @import("std");
const css = @import("css.zig");
const Node = @import("libdom.zig").Node;
const parser = @import("../netsurf.zig");
const Allocator = std.mem.Allocator;
const Matcher = struct {
const Nodes = std.ArrayList(Node);
const Nodes = std.ArrayListUnmanaged(Node);
nodes: Nodes,
allocator: Allocator,
fn init(alloc: std.mem.Allocator) Matcher {
return .{ .nodes = Nodes.init(alloc) };
fn init(allocator: Allocator) Matcher {
return .{
.nodes = .empty,
.allocator = allocator,
};
}
fn deinit(m: *Matcher) void {
m.nodes.deinit();
m.nodes.deinit(m.allocator);
}
fn reset(m: *Matcher) void {
@@ -39,7 +44,7 @@ const Matcher = struct {
}
pub fn match(m: *Matcher, n: Node) !void {
try m.nodes.append(n);
try m.nodes.append(m.allocator, n);
}
};

View File

@@ -84,16 +84,20 @@ pub const Node = struct {
};
const Matcher = struct {
const Nodes = std.ArrayList(*const Node);
const Nodes = std.ArrayListUnmanaged(*const Node);
nodes: Nodes,
allocator: Allocator,
fn init(alloc: std.mem.Allocator) Matcher {
return .{ .nodes = Nodes.init(alloc) };
fn init(allocator: Allocator) Matcher {
return .{
.nodes = .empty,
.allocator = allocator,
};
}
fn deinit(m: *Matcher) void {
m.nodes.deinit();
m.nodes.deinit(self.allocator);
}
fn reset(m: *Matcher) void {
@@ -101,7 +105,7 @@ const Matcher = struct {
}
pub fn match(m: *Matcher, n: *const Node) !void {
try m.nodes.append(n);
try m.nodes.append(self.allocator, n);
}
};

View File

@@ -22,6 +22,7 @@
// see https://github.com/andybalholm/cascadia/blob/master/parser.go
const std = @import("std");
const ascii = std.ascii;
const Allocator = std.mem.Allocator;
const selector = @import("selector.zig");
const Selector = selector.Selector;
@@ -77,8 +78,8 @@ pub const Parser = struct {
opts: ParseOptions,
pub fn parse(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
return p.parseSelectorGroup(alloc);
pub fn parse(p: *Parser, allocator: Allocator) ParseError!Selector {
return p.parseSelectorGroup(allocator);
}
// skipWhitespace consumes whitespace characters and comments.
@@ -115,13 +116,13 @@ pub const Parser = struct {
// parseSimpleSelectorSequence parses a selector sequence that applies to
// a single element.
fn parseSimpleSelectorSequence(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
fn parseSimpleSelectorSequence(p: *Parser, allocator: Allocator) ParseError!Selector {
if (p.i >= p.s.len) {
return ParseError.ExpectedSelector;
}
var buf = std.ArrayList(Selector).init(alloc);
defer buf.deinit();
var buf: std.ArrayListUnmanaged(Selector) = .empty;
defer buf.deinit(allocator);
switch (p.s[p.i]) {
'*' => {
@@ -138,20 +139,20 @@ pub const Parser = struct {
// There's no type selector. Wait to process the other till the
// main loop.
},
else => try buf.append(try p.parseTypeSelector(alloc)),
else => try buf.append(allocator, try p.parseTypeSelector(allocator)),
}
var pseudo_elt: ?PseudoClass = null;
loop: while (p.i < p.s.len) {
var ns: Selector = switch (p.s[p.i]) {
'#' => try p.parseIDSelector(alloc),
'.' => try p.parseClassSelector(alloc),
'[' => try p.parseAttributeSelector(alloc),
':' => try p.parsePseudoclassSelector(alloc),
'#' => try p.parseIDSelector(allocator),
'.' => try p.parseClassSelector(allocator),
'[' => try p.parseAttributeSelector(allocator),
':' => try p.parsePseudoclassSelector(allocator),
else => break :loop,
};
errdefer ns.deinit(alloc);
errdefer ns.deinit(allocator);
// From https://drafts.csswg.org/selectors-3/#pseudo-elements :
// "Only one pseudo-element may appear per selector, and if present
@@ -165,28 +166,32 @@ pub const Parser = struct {
if (!p.opts.accept_pseudo_elts) return ParseError.PseudoElementDisabled;
pseudo_elt = e;
ns.deinit(alloc);
ns.deinit(allocator);
},
else => {
if (pseudo_elt != null) return ParseError.PseudoElementNotAtSelectorEnd;
try buf.append(ns);
try buf.append(allocator, ns);
},
}
}
// no need wrap the selectors in compoundSelector
if (buf.items.len == 1 and pseudo_elt == null) return buf.items[0];
if (buf.items.len == 1 and pseudo_elt == null) {
return buf.items[0];
}
return .{ .compound = .{ .selectors = try buf.toOwnedSlice(), .pseudo_elt = pseudo_elt } };
return .{
.compound = .{ .selectors = try buf.toOwnedSlice(allocator), .pseudo_elt = pseudo_elt },
};
}
// parseTypeSelector parses a type selector (one that matches by tag name).
fn parseTypeSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try p.parseIdentifier(buf.writer());
fn parseTypeSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(allocator);
try p.parseIdentifier(buf.writer(allocator));
return .{ .tag = try buf.toOwnedSlice() };
return .{ .tag = try buf.toOwnedSlice(allocator) };
}
// parseIdentifier parses an identifier.
@@ -204,7 +209,7 @@ pub const Parser = struct {
}
const c = p.s[p.i];
if (!nameStart(c) or c == '\\') {
if (!(nameStart(c) or c == '\\')) {
return ParseError.ExpectedSelector;
}
@@ -314,47 +319,47 @@ pub const Parser = struct {
}
// parseIDSelector parses a selector that matches by id attribute.
fn parseIDSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
fn parseIDSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
if (p.i >= p.s.len) return ParseError.ExpectedIDSelector;
if (p.s[p.i] != '#') return ParseError.ExpectedIDSelector;
p.i += 1;
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(allocator);
try p.parseName(buf.writer());
return .{ .id = try buf.toOwnedSlice() };
try p.parseName(buf.writer(allocator));
return .{ .id = try buf.toOwnedSlice(allocator) };
}
// parseClassSelector parses a selector that matches by class attribute.
fn parseClassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
fn parseClassSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
if (p.i >= p.s.len) return ParseError.ExpectedClassSelector;
if (p.s[p.i] != '.') return ParseError.ExpectedClassSelector;
p.i += 1;
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(allocator);
try p.parseIdentifier(buf.writer());
return .{ .class = try buf.toOwnedSlice() };
try p.parseIdentifier(buf.writer(allocator));
return .{ .class = try buf.toOwnedSlice(allocator) };
}
// parseAttributeSelector parses a selector that matches by attribute value.
fn parseAttributeSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
fn parseAttributeSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
if (p.s[p.i] != '[') return ParseError.ExpectedAttributeSelector;
p.i += 1;
_ = p.skipWhitespace();
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(allocator);
try p.parseIdentifier(buf.writer());
const key = try buf.toOwnedSlice();
errdefer alloc.free(key);
try p.parseIdentifier(buf.writer(allocator));
const key = try buf.toOwnedSlice(allocator);
errdefer allocator.free(key);
lowerstr(key);
@@ -377,12 +382,12 @@ pub const Parser = struct {
var is_val: bool = undefined;
if (op == .regexp) {
is_val = false;
try p.parseRegex(buf.writer());
try p.parseRegex(buf.writer(allocator));
} else {
is_val = true;
switch (p.s[p.i]) {
'\'', '"' => try p.parseString(buf.writer()),
else => try p.parseIdentifier(buf.writer()),
'\'', '"' => try p.parseString(buf.writer(allocator)),
else => try p.parseIdentifier(buf.writer(allocator)),
}
}
@@ -404,8 +409,8 @@ pub const Parser = struct {
return .{ .attribute = .{
.key = key,
.val = if (is_val) try buf.toOwnedSlice() else null,
.regexp = if (!is_val) try buf.toOwnedSlice() else null,
.val = if (is_val) try buf.toOwnedSlice(allocator) else null,
.regexp = if (!is_val) try buf.toOwnedSlice(allocator) else null,
.op = op,
.ci = ci,
} };
@@ -498,7 +503,7 @@ pub const Parser = struct {
// parsePseudoclassSelector parses a pseudoclass selector like :not(p) or a pseudo-element
// For backwards compatibility, both ':' and '::' prefix are allowed for pseudo-elements.
// https://drafts.csswg.org/selectors-3/#pseudo-elements
fn parsePseudoclassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
fn parsePseudoclassSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
if (p.i >= p.s.len) return ParseError.ExpectedPseudoClassSelector;
if (p.s[p.i] != ':') return ParseError.ExpectedPseudoClassSelector;
@@ -511,10 +516,10 @@ pub const Parser = struct {
p.i += 1;
}
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(allocator);
try p.parseIdentifier(buf.writer());
try p.parseIdentifier(buf.writer(allocator));
const pseudo_class = try PseudoClass.parse(buf.items);
@@ -527,11 +532,11 @@ pub const Parser = struct {
.not, .has, .haschild => {
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
const sel = try p.parseSelectorGroup(alloc);
const sel = try p.parseSelectorGroup(allocator);
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
const s = try alloc.create(Selector);
errdefer alloc.destroy(s);
const s = try allocator.create(Selector);
errdefer allocator.destroy(s);
s.* = sel;
return .{ .pseudo_class_relative = .{ .pseudo_class = pseudo_class, .match = s } };
@@ -541,16 +546,16 @@ pub const Parser = struct {
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
switch (p.s[p.i]) {
'\'', '"' => try p.parseString(buf.writer()),
else => try p.parseString(buf.writer()),
'\'', '"' => try p.parseString(buf.writer(allocator)),
else => try p.parseString(buf.writer(allocator)),
}
_ = p.skipWhitespace();
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
const val = try buf.toOwnedSlice();
errdefer alloc.free(val);
const val = try buf.toOwnedSlice(allocator);
errdefer allocator.free(val);
lowerstr(val);
@@ -559,15 +564,15 @@ pub const Parser = struct {
.matches, .matchesown => {
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
try p.parseRegex(buf.writer());
try p.parseRegex(buf.writer(allocator));
if (p.i >= p.s.len) return ParseError.InvalidPseudoClassSelector;
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
return .{ .pseudo_class_regexp = .{ .own = pseudo_class == .matchesown, .regexp = try buf.toOwnedSlice() } };
return .{ .pseudo_class_regexp = .{ .own = pseudo_class == .matchesown, .regexp = try buf.toOwnedSlice(allocator) } };
},
.nth_child, .nth_last_child, .nth_of_type, .nth_last_of_type => {
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
const nth = try p.parseNth(alloc);
const nth = try p.parseNth(allocator);
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
const last = pseudo_class == .nth_last_child or pseudo_class == .nth_last_of_type;
@@ -582,18 +587,19 @@ pub const Parser = struct {
.only_of_type => return .{ .pseudo_class_only_child = true },
.input, .empty, .root, .link => return .{ .pseudo_class = pseudo_class },
.enabled, .disabled, .checked => return .{ .pseudo_class = pseudo_class },
.visible => return .{ .pseudo_class = pseudo_class },
.lang => {
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
try p.parseIdentifier(buf.writer());
try p.parseIdentifier(buf.writer(allocator));
_ = p.skipWhitespace();
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
const val = try buf.toOwnedSlice();
errdefer alloc.free(val);
const val = try buf.toOwnedSlice(allocator);
errdefer allocator.free(val);
lowerstr(val);
return .{ .pseudo_class_lang = val };
@@ -605,7 +611,7 @@ pub const Parser = struct {
.after, .backdrop, .before, .cue, .first_letter => return .{ .pseudo_element = pseudo_class },
.first_line, .grammar_error, .marker, .placeholder => return .{ .pseudo_element = pseudo_class },
.selection, .spelling_error => return .{ .pseudo_element = pseudo_class },
.modal => return .{ .pseudo_element = pseudo_class },
.modal, .popover_open => return .{ .pseudo_element = pseudo_class },
}
}
@@ -621,30 +627,32 @@ pub const Parser = struct {
}
// parseSelectorGroup parses a group of selectors, separated by commas.
fn parseSelectorGroup(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
const s = try p.parseSelector(alloc);
fn parseSelectorGroup(p: *Parser, allocator: Allocator) ParseError!Selector {
const s = try p.parseSelector(allocator);
var buf = std.ArrayList(Selector).init(alloc);
defer buf.deinit();
var buf: std.ArrayListUnmanaged(Selector) = .empty;
defer buf.deinit(allocator);
try buf.append(s);
try buf.append(allocator, s);
while (p.i < p.s.len) {
if (p.s[p.i] != ',') break;
p.i += 1;
const ss = try p.parseSelector(alloc);
try buf.append(ss);
const ss = try p.parseSelector(allocator);
try buf.append(allocator, ss);
}
if (buf.items.len == 1) return buf.items[0];
if (buf.items.len == 1) {
return buf.items[0];
}
return .{ .group = try buf.toOwnedSlice() };
return .{ .group = try buf.toOwnedSlice(allocator) };
}
// parseSelector parses a selector that may include combinators.
fn parseSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
fn parseSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
_ = p.skipWhitespace();
var s = try p.parseSimpleSelectorSequence(alloc);
var s = try p.parseSimpleSelectorSequence(allocator);
while (true) {
var combinator: Combinator = .empty;
@@ -672,17 +680,21 @@ pub const Parser = struct {
return s;
}
const c = try p.parseSimpleSelectorSequence(alloc);
const c = try p.parseSimpleSelectorSequence(allocator);
const first = try alloc.create(Selector);
errdefer alloc.destroy(first);
const first = try allocator.create(Selector);
errdefer allocator.destroy(first);
first.* = s;
const second = try alloc.create(Selector);
errdefer alloc.destroy(second);
const second = try allocator.create(Selector);
errdefer allocator.destroy(second);
second.* = c;
s = Selector{ .combined = .{ .first = first, .second = second, .combinator = combinator } };
s = Selector{ .combined = .{
.first = first,
.second = second,
.combinator = combinator,
} };
}
return s;
@@ -775,7 +787,7 @@ pub const Parser = struct {
}
// parseNth parses the argument for :nth-child (normally of the form an+b).
fn parseNth(p: *Parser, alloc: std.mem.Allocator) ParseError![2]isize {
fn parseNth(p: *Parser, allocator: Allocator) ParseError![2]isize {
// initial state
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
return switch (p.s[p.i]) {
@@ -793,10 +805,10 @@ pub const Parser = struct {
return p.parseNthReadN(1);
},
'o', 'O', 'e', 'E' => {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(allocator);
try p.parseName(buf.writer());
try p.parseName(buf.writer(allocator));
if (std.ascii.eqlIgnoreCase("odd", buf.items)) return .{ 2, 1 };
if (std.ascii.eqlIgnoreCase("even", buf.items)) return .{ 2, 0 };
@@ -872,7 +884,7 @@ test "parser.skipWhitespace" {
}
test "parser.parseIdentifier" {
const alloc = std.testing.allocator;
const allocator = std.testing.allocator;
const testcases = [_]struct {
s: []const u8, // given value
@@ -888,14 +900,14 @@ test "parser.parseIdentifier" {
.{ .s = "a\\\"b", .exp = "a\"b" },
};
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(allocator);
for (testcases) |tc| {
buf.clearRetainingCapacity();
var p = Parser{ .s = tc.s, .opts = .{} };
p.parseIdentifier(buf.writer()) catch |e| {
p.parseIdentifier(buf.writer(allocator)) catch |e| {
// if error was expected, continue.
if (tc.err) continue;
@@ -910,7 +922,7 @@ test "parser.parseIdentifier" {
}
test "parser.parseString" {
const alloc = std.testing.allocator;
const allocator = std.testing.allocator;
const testcases = [_]struct {
s: []const u8, // given value
@@ -929,14 +941,14 @@ test "parser.parseString" {
.{ .s = "\"hello world\"", .exp = "hello world" },
};
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(allocator);
for (testcases) |tc| {
buf.clearRetainingCapacity();
var p = Parser{ .s = tc.s, .opts = .{} };
p.parseString(buf.writer()) catch |e| {
p.parseString(buf.writer(allocator)) catch |e| {
// if error was expected, continue.
if (tc.err) continue;
@@ -949,3 +961,36 @@ test "parser.parseString" {
};
}
}
test "parser.parse" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
const testcases = [_]struct {
s: []const u8, // given value
exp: Selector, // expected value
err: bool = false,
}{
.{ .s = "root", .exp = .{ .tag = "root" } },
.{ .s = ".root", .exp = .{ .class = "root" } },
.{ .s = ":root", .exp = .{ .pseudo_class = .root } },
.{ .s = ".\\:bar", .exp = .{ .class = ":bar" } },
.{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } },
};
for (testcases) |tc| {
var p = Parser{ .s = tc.s, .opts = .{} };
const sel = p.parse(allocator) catch |e| {
// if error was expected, continue.
if (tc.err) continue;
std.debug.print("test case {s}\n", .{tc.s});
return e;
};
std.testing.expectEqualDeep(tc.exp, sel) catch |e| {
std.debug.print("test case {s} : {}\n", .{ tc.s, sel });
return e;
};
}
}

View File

@@ -99,6 +99,8 @@ pub const PseudoClass = enum {
selection,
spelling_error,
modal,
popover_open,
visible,
pub const Error = error{
InvalidPseudoClass,
@@ -114,52 +116,108 @@ pub const PseudoClass = enum {
}
pub fn parse(s: []const u8) Error!PseudoClass {
if (std.ascii.eqlIgnoreCase(s, "not")) return .not;
if (std.ascii.eqlIgnoreCase(s, "has")) return .has;
if (std.ascii.eqlIgnoreCase(s, "haschild")) return .haschild;
if (std.ascii.eqlIgnoreCase(s, "contains")) return .contains;
if (std.ascii.eqlIgnoreCase(s, "containsown")) return .containsown;
if (std.ascii.eqlIgnoreCase(s, "matches")) return .matches;
if (std.ascii.eqlIgnoreCase(s, "matchesown")) return .matchesown;
if (std.ascii.eqlIgnoreCase(s, "nth-child")) return .nth_child;
if (std.ascii.eqlIgnoreCase(s, "nth-last-child")) return .nth_last_child;
if (std.ascii.eqlIgnoreCase(s, "nth-of-type")) return .nth_of_type;
if (std.ascii.eqlIgnoreCase(s, "nth-last-of-type")) return .nth_last_of_type;
if (std.ascii.eqlIgnoreCase(s, "first-child")) return .first_child;
if (std.ascii.eqlIgnoreCase(s, "last-child")) return .last_child;
if (std.ascii.eqlIgnoreCase(s, "first-of-type")) return .first_of_type;
if (std.ascii.eqlIgnoreCase(s, "last-of-type")) return .last_of_type;
if (std.ascii.eqlIgnoreCase(s, "only-child")) return .only_child;
if (std.ascii.eqlIgnoreCase(s, "only-of-type")) return .only_of_type;
if (std.ascii.eqlIgnoreCase(s, "input")) return .input;
if (std.ascii.eqlIgnoreCase(s, "empty")) return .empty;
if (std.ascii.eqlIgnoreCase(s, "root")) return .root;
if (std.ascii.eqlIgnoreCase(s, "link")) return .link;
if (std.ascii.eqlIgnoreCase(s, "lang")) return .lang;
if (std.ascii.eqlIgnoreCase(s, "enabled")) return .enabled;
if (std.ascii.eqlIgnoreCase(s, "disabled")) return .disabled;
if (std.ascii.eqlIgnoreCase(s, "checked")) return .checked;
if (std.ascii.eqlIgnoreCase(s, "visited")) return .visited;
if (std.ascii.eqlIgnoreCase(s, "hover")) return .hover;
if (std.ascii.eqlIgnoreCase(s, "active")) return .active;
if (std.ascii.eqlIgnoreCase(s, "focus")) return .focus;
if (std.ascii.eqlIgnoreCase(s, "target")) return .target;
if (std.ascii.eqlIgnoreCase(s, "after")) return .after;
if (std.ascii.eqlIgnoreCase(s, "backdrop")) return .backdrop;
if (std.ascii.eqlIgnoreCase(s, "before")) return .before;
if (std.ascii.eqlIgnoreCase(s, "cue")) return .cue;
if (std.ascii.eqlIgnoreCase(s, "first-letter")) return .first_letter;
if (std.ascii.eqlIgnoreCase(s, "first-line")) return .first_line;
if (std.ascii.eqlIgnoreCase(s, "grammar-error")) return .grammar_error;
if (std.ascii.eqlIgnoreCase(s, "marker")) return .marker;
if (std.ascii.eqlIgnoreCase(s, "placeholder")) return .placeholder;
if (std.ascii.eqlIgnoreCase(s, "selection")) return .selection;
if (std.ascii.eqlIgnoreCase(s, "spelling-error")) return .spelling_error;
if (std.ascii.eqlIgnoreCase(s, "modal")) return .modal;
const longest_selector = "nth-last-of-type";
if (s.len > longest_selector.len) {
return Error.InvalidPseudoClass;
}
var buf: [longest_selector.len]u8 = undefined;
const selector = std.ascii.lowerString(&buf, s);
switch (selector.len) {
3 => switch (@as(u24, @bitCast(selector[0..3].*))) {
asUint(u24, "cue") => return .cue,
asUint(u24, "has") => return .has,
asUint(u24, "not") => return .not,
else => {},
},
4 => switch (@as(u32, @bitCast(selector[0..4].*))) {
asUint(u32, "lang") => return .lang,
asUint(u32, "link") => return .link,
asUint(u32, "root") => return .root,
else => {},
},
5 => switch (@as(u40, @bitCast(selector[0..5].*))) {
asUint(u40, "after") => return .after,
asUint(u40, "empty") => return .empty,
asUint(u40, "focus") => return .focus,
asUint(u40, "hover") => return .hover,
asUint(u40, "input") => return .input,
asUint(u40, "modal") => return .modal,
else => {},
},
6 => switch (@as(u48, @bitCast(selector[0..6].*))) {
asUint(u48, "active") => return .active,
asUint(u48, "before") => return .before,
asUint(u48, "marker") => return .marker,
asUint(u48, "target") => return .target,
else => {},
},
7 => switch (@as(u56, @bitCast(selector[0..7].*))) {
asUint(u56, "checked") => return .checked,
asUint(u56, "enabled") => return .enabled,
asUint(u56, "matches") => return .matches,
asUint(u56, "visited") => return .visited,
asUint(u56, "visible") => return .visible,
else => {},
},
8 => switch (@as(u64, @bitCast(selector[0..8].*))) {
asUint(u64, "backdrop") => return .backdrop,
asUint(u64, "contains") => return .contains,
asUint(u64, "disabled") => return .disabled,
asUint(u64, "haschild") => return .haschild,
else => {},
},
9 => switch (@as(u72, @bitCast(selector[0..9].*))) {
asUint(u72, "nth-child") => return .nth_child,
asUint(u72, "selection") => return .selection,
else => {},
},
10 => switch (@as(u80, @bitCast(selector[0..10].*))) {
asUint(u80, "first-line") => return .first_line,
asUint(u80, "last-child") => return .last_child,
asUint(u80, "matchesown") => return .matchesown,
asUint(u80, "only-child") => return .only_child,
else => {},
},
11 => switch (@as(u88, @bitCast(selector[0..11].*))) {
asUint(u88, "containsown") => return .containsown,
asUint(u88, "first-child") => return .first_child,
asUint(u88, "nth-of-type") => return .nth_of_type,
asUint(u88, "placeholder") => return .placeholder,
else => {},
},
12 => switch (@as(u96, @bitCast(selector[0..12].*))) {
asUint(u96, "first-letter") => return .first_letter,
asUint(u96, "last-of-type") => return .last_of_type,
asUint(u96, "only-of-type") => return .only_of_type,
asUint(u96, "popover-open") => return .popover_open,
else => {},
},
13 => switch (@as(u104, @bitCast(selector[0..13].*))) {
asUint(u104, "first-of-type") => return .first_of_type,
asUint(u104, "grammar-error") => return .grammar_error,
else => {},
},
14 => switch (@as(u112, @bitCast(selector[0..14].*))) {
asUint(u112, "nth-last-child") => return .nth_last_child,
asUint(u112, "spelling-error") => return .spelling_error,
else => {},
},
16 => switch (@as(u128, @bitCast(selector[0..16].*))) {
asUint(u128, "nth-last-of-type") => return .nth_last_of_type,
else => {},
},
else => {},
}
return Error.InvalidPseudoClass;
}
};
fn asUint(comptime T: type, comptime string: []const u8) T {
return @bitCast(string[0..string.len].*);
}
pub const Selector = union(enum) {
pub const Error = error{
UnknownCombinedCombinator,
@@ -248,8 +306,8 @@ pub const Selector = union(enum) {
}
// match returns true if the node matches the selector query.
pub fn match(s: Selector, n: anytype) !bool {
return switch (s) {
pub fn match(s: *const Selector, n: anytype) !bool {
return switch (s.*) {
.tag => |v| n.isElement() and std.ascii.eqlIgnoreCase(v, try n.tag()),
.id => |v| return n.isElement() and std.mem.eql(u8, v, try n.attr("id") orelse return false),
.class => |v| return n.isElement() and word(try n.attr("class") orelse return false, v, false),
@@ -511,6 +569,8 @@ pub const Selector = union(enum) {
// TODO implement using the url fragment.
// see https://developer.mozilla.org/en-US/docs/Web/CSS/:target
.target => return false,
// visible always returns true.
.visible => return true,
// all others pseudo class are handled by specialized
// pseudo_class_X selectors.
@@ -734,8 +794,8 @@ pub const Selector = union(enum) {
return false;
}
pub fn deinit(sel: Selector, alloc: std.mem.Allocator) void {
switch (sel) {
pub fn deinit(sel: *const Selector, alloc: std.mem.Allocator) void {
switch (sel.*) {
.group => |v| {
for (v) |vv| vv.deinit(alloc);
alloc.free(v);

View File

@@ -0,0 +1,289 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const CSSConstants = struct {
const IMPORTANT = "!important";
const URL_PREFIX = "url(";
};
const CSSParserState = enum {
seek_name,
in_name,
seek_colon,
seek_value,
in_value,
in_quoted_value,
in_single_quoted_value,
in_url,
in_important,
};
const CSSDeclaration = struct {
name: []const u8,
value: []const u8,
is_important: bool,
};
const CSSParser = @This();
state: CSSParserState,
name_start: usize,
name_end: usize,
value_start: usize,
position: usize,
paren_depth: usize,
escape_next: bool,
pub fn init() CSSParser {
return .{
.state = .seek_name,
.name_start = 0,
.name_end = 0,
.value_start = 0,
.position = 0,
.paren_depth = 0,
.escape_next = false,
};
}
pub fn parseDeclarations(arena: Allocator, text: []const u8) ![]CSSDeclaration {
var parser = init();
var declarations: std.ArrayListUnmanaged(CSSDeclaration) = .empty;
while (parser.position < text.len) {
const c = text[parser.position];
switch (parser.state) {
.seek_name => {
if (!std.ascii.isWhitespace(c)) {
parser.name_start = parser.position;
parser.state = .in_name;
continue;
}
},
.in_name => {
if (c == ':') {
parser.name_end = parser.position;
parser.state = .seek_value;
} else if (std.ascii.isWhitespace(c)) {
parser.name_end = parser.position;
parser.state = .seek_colon;
}
},
.seek_colon => {
if (c == ':') {
parser.state = .seek_value;
} else if (!std.ascii.isWhitespace(c)) {
parser.state = .seek_name;
continue;
}
},
.seek_value => {
if (!std.ascii.isWhitespace(c)) {
parser.value_start = parser.position;
if (c == '"') {
parser.state = .in_quoted_value;
} else if (c == '\'') {
parser.state = .in_single_quoted_value;
} else if (c == 'u' and parser.position + CSSConstants.URL_PREFIX.len <= text.len and std.mem.startsWith(u8, text[parser.position..], CSSConstants.URL_PREFIX)) {
parser.state = .in_url;
parser.paren_depth = 1;
parser.position += 3;
} else {
parser.state = .in_value;
continue;
}
}
},
.in_value => {
if (parser.escape_next) {
parser.escape_next = false;
} else if (c == '\\') {
parser.escape_next = true;
} else if (c == '(') {
parser.paren_depth += 1;
} else if (c == ')' and parser.paren_depth > 0) {
parser.paren_depth -= 1;
} else if (c == ';' and parser.paren_depth == 0) {
try parser.finishDeclaration(arena, &declarations, text);
parser.state = .seek_name;
}
},
.in_quoted_value => {
if (parser.escape_next) {
parser.escape_next = false;
} else if (c == '\\') {
parser.escape_next = true;
} else if (c == '"') {
parser.state = .in_value;
}
},
.in_single_quoted_value => {
if (parser.escape_next) {
parser.escape_next = false;
} else if (c == '\\') {
parser.escape_next = true;
} else if (c == '\'') {
parser.state = .in_value;
}
},
.in_url => {
if (parser.escape_next) {
parser.escape_next = false;
} else if (c == '\\') {
parser.escape_next = true;
} else if (c == '(') {
parser.paren_depth += 1;
} else if (c == ')') {
parser.paren_depth -= 1;
if (parser.paren_depth == 0) {
parser.state = .in_value;
}
}
},
.in_important => {},
}
parser.position += 1;
}
try parser.finalize(arena, &declarations, text);
return declarations.items;
}
fn finishDeclaration(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
const name = std.mem.trim(u8, text[self.name_start..self.name_end], &std.ascii.whitespace);
if (name.len == 0) return;
const raw_value = text[self.value_start..self.position];
const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace);
var final_value = value;
var is_important = false;
if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT)) {
is_important = true;
final_value = std.mem.trimRight(u8, value[0 .. value.len - CSSConstants.IMPORTANT.len], &std.ascii.whitespace);
}
try declarations.append(arena, .{
.name = name,
.value = final_value,
.is_important = is_important,
});
}
fn finalize(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
if (self.state != .in_value) {
return;
}
return self.finishDeclaration(arena, declarations, text);
}
const testing = @import("../../testing.zig");
test "Browser: CSS.Parser - Simple property" {
defer testing.reset();
const text = "color: red;";
const allocator = testing.arena_allocator;
const declarations = try CSSParser.parseDeclarations(allocator, text);
try testing.expectEqual(1, declarations.len);
try testing.expectEqual("color", declarations[0].name);
try testing.expectEqual("red", declarations[0].value);
try testing.expectEqual(false, declarations[0].is_important);
}
test "Browser: CSS.Parser - Property with !important" {
defer testing.reset();
const text = "margin: 10px !important;";
const allocator = testing.arena_allocator;
const declarations = try CSSParser.parseDeclarations(allocator, text);
try testing.expectEqual(1, declarations.len);
try testing.expectEqual("margin", declarations[0].name);
try testing.expectEqual("10px", declarations[0].value);
try testing.expectEqual(true, declarations[0].is_important);
}
test "Browser: CSS.Parser - Multiple properties" {
defer testing.reset();
const text = "color: red; font-size: 12px; margin: 5px !important;";
const allocator = testing.arena_allocator;
const declarations = try CSSParser.parseDeclarations(allocator, text);
try testing.expect(declarations.len == 3);
try testing.expectEqual("color", declarations[0].name);
try testing.expectEqual("red", declarations[0].value);
try testing.expectEqual(false, declarations[0].is_important);
try testing.expectEqual("font-size", declarations[1].name);
try testing.expectEqual("12px", declarations[1].value);
try testing.expectEqual(false, declarations[1].is_important);
try testing.expectEqual("margin", declarations[2].name);
try testing.expectEqual("5px", declarations[2].value);
try testing.expectEqual(true, declarations[2].is_important);
}
test "Browser: CSS.Parser - Quoted value with semicolon" {
defer testing.reset();
const text = "content: \"Hello; world!\";";
const allocator = testing.arena_allocator;
const declarations = try CSSParser.parseDeclarations(allocator, text);
try testing.expectEqual(1, declarations.len);
try testing.expectEqual("content", declarations[0].name);
try testing.expectEqual("\"Hello; world!\"", declarations[0].value);
try testing.expectEqual(false, declarations[0].is_important);
}
test "Browser: CSS.Parser - URL value" {
defer testing.reset();
const text = "background-image: url(\"test.png\");";
const allocator = testing.arena_allocator;
const declarations = try CSSParser.parseDeclarations(allocator, text);
try testing.expectEqual(1, declarations.len);
try testing.expectEqual("background-image", declarations[0].name);
try testing.expectEqual("url(\"test.png\")", declarations[0].value);
try testing.expectEqual(false, declarations[0].is_important);
}
test "Browser: CSS.Parser - Whitespace handling" {
defer testing.reset();
const text = " color : purple ; margin : 10px ; ";
const allocator = testing.arena_allocator;
const declarations = try CSSParser.parseDeclarations(allocator, text);
try testing.expectEqual(2, declarations.len);
try testing.expectEqual("color", declarations[0].name);
try testing.expectEqual("purple", declarations[0].value);
try testing.expectEqual("margin", declarations[1].name);
try testing.expectEqual("10px", declarations[1].value);
}

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const CSSStyleSheet = @import("css_stylesheet.zig").CSSStyleSheet;
const CSSStyleSheet = @import("CSSStyleSheet.zig");
pub const Interfaces = .{
CSSRule,
@@ -26,11 +26,10 @@ pub const Interfaces = .{
};
// https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
pub const CSSRule = struct {
css_text: []const u8,
parent_rule: ?*CSSRule = null,
parent_stylesheet: ?*CSSStyleSheet = null,
};
const CSSRule = @This();
css_text: []const u8,
parent_rule: ?*CSSRule = null,
parent_stylesheet: ?*CSSStyleSheet = null,
pub const CSSImportRule = struct {
pub const prototype = *CSSRule;

View File

@@ -0,0 +1,52 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CSSRule = @import("CSSRule.zig");
const StyleSheet = @import("StyleSheet.zig").StyleSheet;
const CSSImportRule = CSSRule.CSSImportRule;
const CSSRuleList = @This();
list: std.ArrayListUnmanaged([]const u8),
pub fn constructor() CSSRuleList {
return .{ .list = .empty };
}
pub fn _item(self: *CSSRuleList, _index: u32) ?CSSRule {
const index: usize = @intCast(_index);
if (index > self.list.items.len) {
return null;
}
// todo: for now, just return null.
// this depends on properly parsing CSSRule
return null;
}
pub fn get_length(self: *CSSRuleList) u32 {
return @intCast(self.list.items.len);
}
const testing = @import("../../testing.zig");
test "Browser: CSS.CSSRuleList" {
try testing.htmlRunner("cssom/css_rule_list.html");
}

View File

@@ -0,0 +1,958 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Page = @import("../page.zig").Page;
const CSSRule = @import("CSSRule.zig");
const CSSParser = @import("CSSParser.zig");
const Property = struct {
value: []const u8,
priority: bool,
};
const CSSStyleDeclaration = @This();
properties: std.StringArrayHashMapUnmanaged(Property),
pub const empty: CSSStyleDeclaration = .{
.properties = .empty,
};
pub fn get_cssFloat(self: *const CSSStyleDeclaration) []const u8 {
return self._getPropertyValue("float");
}
pub fn set_cssFloat(self: *CSSStyleDeclaration, value: ?[]const u8, page: *Page) !void {
const final_value = value orelse "";
return self._setProperty("float", final_value, null, page);
}
pub fn get_cssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 {
var buffer: std.ArrayListUnmanaged(u8) = .empty;
const writer = buffer.writer(page.call_arena);
var it = self.properties.iterator();
while (it.next()) |entry| {
const name = entry.key_ptr.*;
const property = entry.value_ptr;
const escaped = try escapeCSSValue(page.call_arena, property.value);
try writer.print("{s}: {s}", .{ name, escaped });
if (property.priority) {
try writer.writeAll(" !important; ");
} else {
try writer.writeAll("; ");
}
}
return buffer.items;
}
// TODO Propagate also upward to parent node
pub fn set_cssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void {
self.properties.clearRetainingCapacity();
// call_arena is safe here, because _setProperty will dupe the name
// using the page's longer-living arena.
const declarations = try CSSParser.parseDeclarations(page.call_arena, text);
for (declarations) |decl| {
if (!isValidPropertyName(decl.name)) {
continue;
}
const priority: ?[]const u8 = if (decl.is_important) "important" else null;
try self._setProperty(decl.name, decl.value, priority, page);
}
}
pub fn get_length(self: *const CSSStyleDeclaration) usize {
return self.properties.count();
}
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
return null;
}
pub fn _getPropertyPriority(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
const property = self.properties.getPtr(name) orelse return "";
return if (property.priority) "important" else "";
}
// TODO should handle properly shorthand properties and canonical forms
pub fn _getPropertyValue(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
if (self.properties.getPtr(name)) |property| {
return property.value;
}
// default to everything being visible (unless it's been explicitly set)
if (std.mem.eql(u8, name, "visibility")) {
return "visible";
}
return "";
}
pub fn _item(self: *const CSSStyleDeclaration, index: usize) []const u8 {
const values = self.properties.entries.items(.key);
if (index >= values.len) {
return "";
}
return values[index];
}
pub fn _removeProperty(self: *CSSStyleDeclaration, name: []const u8) ![]const u8 {
const property = self.properties.fetchOrderedRemove(name) orelse return "";
return property.value.value;
}
pub fn _setProperty(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, priority: ?[]const u8, page: *Page) !void {
const gop = try self.properties.getOrPut(page.arena, name);
if (!gop.found_existing) {
const owned_name = try page.arena.dupe(u8, name);
gop.key_ptr.* = owned_name;
}
const owned_value = try page.arena.dupe(u8, value);
const is_important = priority != null and std.ascii.eqlIgnoreCase(priority.?, "important");
gop.value_ptr.* = .{ .value = owned_value, .priority = is_important };
}
pub fn named_get(self: *const CSSStyleDeclaration, name: []const u8, _: *bool) []const u8 {
return self._getPropertyValue(name);
}
pub fn named_set(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, _: *bool, page: *Page) !void {
return self._setProperty(name, value, null, page);
}
fn isNumericWithUnit(value: []const u8) bool {
if (value.len == 0) {
return false;
}
const first = value[0];
if (!std.ascii.isDigit(first) and first != '+' and first != '-' and first != '.') {
return false;
}
var i: usize = 0;
var has_digit = false;
var decimal_point = false;
while (i < value.len) : (i += 1) {
const c = value[i];
if (std.ascii.isDigit(c)) {
has_digit = true;
} else if (c == '.' and !decimal_point) {
decimal_point = true;
} else if ((c == 'e' or c == 'E') and has_digit) {
if (i + 1 >= value.len) return false;
if (value[i + 1] != '+' and value[i + 1] != '-' and !std.ascii.isDigit(value[i + 1])) break;
i += 1;
if (value[i] == '+' or value[i] == '-') {
i += 1;
}
var has_exp_digits = false;
while (i < value.len and std.ascii.isDigit(value[i])) : (i += 1) {
has_exp_digits = true;
}
if (!has_exp_digits) return false;
break;
} else if (c != '-' and c != '+') {
break;
}
}
if (!has_digit) {
return false;
}
if (i == value.len) {
return true;
}
const unit = value[i..];
return CSSKeywords.isValidUnit(unit);
}
fn isHexColor(value: []const u8) bool {
if (value.len == 0) {
return false;
}
if (value[0] != '#') {
return false;
}
const hex_part = value[1..];
if (hex_part.len != 3 and hex_part.len != 6 and hex_part.len != 8) {
return false;
}
for (hex_part) |c| {
if (!std.ascii.isHex(c)) {
return false;
}
}
return true;
}
fn isMultiValueProperty(value: []const u8) bool {
var parts = std.mem.splitAny(u8, value, " ");
var multi_value_parts: usize = 0;
var all_parts_valid = true;
while (parts.next()) |part| {
if (part.len == 0) continue;
multi_value_parts += 1;
if (isNumericWithUnit(part)) {
continue;
}
if (isHexColor(part)) {
continue;
}
if (CSSKeywords.isKnownKeyword(part)) {
continue;
}
if (CSSKeywords.startsWithFunction(part)) {
continue;
}
all_parts_valid = false;
break;
}
return multi_value_parts >= 2 and all_parts_valid;
}
fn isAlreadyQuoted(value: []const u8) bool {
return value.len >= 2 and ((value[0] == '"' and value[value.len - 1] == '"') or
(value[0] == '\'' and value[value.len - 1] == '\''));
}
fn isValidPropertyName(name: []const u8) bool {
if (name.len == 0) return false;
if (std.mem.startsWith(u8, name, "--")) {
if (name.len == 2) return false;
for (name[2..]) |c| {
if (!std.ascii.isAlphanumeric(c) and c != '-' and c != '_') {
return false;
}
}
return true;
}
const first_char = name[0];
if (!std.ascii.isAlphabetic(first_char) and first_char != '-') {
return false;
}
if (first_char == '-') {
if (name.len < 2) return false;
if (!std.ascii.isAlphabetic(name[1])) {
return false;
}
for (name[2..]) |c| {
if (!std.ascii.isAlphanumeric(c) and c != '-') {
return false;
}
}
} else {
for (name[1..]) |c| {
if (!std.ascii.isAlphanumeric(c) and c != '-') {
return false;
}
}
}
return true;
}
fn extractImportant(value: []const u8) struct { value: []const u8, is_important: bool } {
const trimmed = std.mem.trim(u8, value, &std.ascii.whitespace);
if (std.mem.endsWith(u8, trimmed, "!important")) {
const clean_value = std.mem.trimRight(u8, trimmed[0 .. trimmed.len - 10], &std.ascii.whitespace);
return .{ .value = clean_value, .is_important = true };
}
return .{ .value = trimmed, .is_important = false };
}
fn needsQuotes(value: []const u8) bool {
if (value.len == 0) return true;
if (isAlreadyQuoted(value)) return false;
if (CSSKeywords.containsSpecialChar(value)) {
return true;
}
if (std.mem.indexOfScalar(u8, value, ' ') == null) {
return false;
}
const is_url = std.mem.startsWith(u8, value, "url(");
const is_function = CSSKeywords.startsWithFunction(value);
return !isMultiValueProperty(value) and
!is_url and
!is_function;
}
fn escapeCSSValue(arena: std.mem.Allocator, value: []const u8) ![]const u8 {
if (!needsQuotes(value)) {
return value;
}
var out: std.ArrayListUnmanaged(u8) = .empty;
// We'll need at least this much space, +2 for the quotes
try out.ensureTotalCapacity(arena, value.len + 2);
const writer = out.writer(arena);
try writer.writeByte('"');
for (value, 0..) |c, i| {
switch (c) {
'"' => try writer.writeAll("\\\""),
'\\' => try writer.writeAll("\\\\"),
'\n' => try writer.writeAll("\\A "),
'\r' => try writer.writeAll("\\D "),
'\t' => try writer.writeAll("\\9 "),
0...8, 11, 12, 14...31, 127 => {
try writer.print("\\{x}", .{c});
if (i + 1 < value.len and std.ascii.isHex(value[i + 1])) {
try writer.writeByte(' ');
}
},
else => try writer.writeByte(c),
}
}
try writer.writeByte('"');
return out.items;
}
fn isKnownKeyword(value: []const u8) bool {
return CSSKeywords.isKnownKeyword(value);
}
fn containsSpecialChar(value: []const u8) bool {
return CSSKeywords.containsSpecialChar(value);
}
const CSSKeywords = struct {
const BORDER_STYLES = [_][]const u8{
"none", "solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset",
};
const COLOR_NAMES = [_][]const u8{
"black", "white", "red", "green", "blue", "yellow", "purple", "gray", "transparent",
"currentColor", "inherit",
};
const POSITION_KEYWORDS = [_][]const u8{
"auto", "center", "left", "right", "top", "bottom",
};
const BACKGROUND_REPEAT = [_][]const u8{
"repeat", "no-repeat", "repeat-x", "repeat-y", "space", "round",
};
const FONT_STYLES = [_][]const u8{
"normal", "italic", "oblique", "bold", "bolder", "lighter",
};
const FONT_SIZES = [_][]const u8{
"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large",
"smaller", "larger",
};
const FONT_FAMILIES = [_][]const u8{
"serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
};
const CSS_GLOBAL = [_][]const u8{
"initial", "inherit", "unset", "revert",
};
const DISPLAY_VALUES = [_][]const u8{
"block", "inline", "inline-block", "flex", "grid", "none",
};
const UNITS = [_][]const u8{
// LENGTH
"px", "em", "rem", "vw", "vh", "vmin", "vmax", "%", "pt", "pc", "in", "cm", "mm",
"ex", "ch", "fr",
// ANGLE
"deg", "rad", "grad", "turn",
// TIME
"s", "ms",
// FREQUENCY
"hz", "khz",
// RESOLUTION
"dpi", "dpcm",
"dppx",
};
const SPECIAL_CHARS = [_]u8{
'"', '\'', ';', '{', '}', '\\', '<', '>', '/', '\n', '\t', '\r', '\x00', '\x7F',
};
const FUNCTIONS = [_][]const u8{
"rgb(", "rgba(", "hsl(", "hsla(", "url(", "calc(", "var(", "attr(",
"linear-gradient(", "radial-gradient(", "conic-gradient(", "translate(", "rotate(", "scale(", "skew(", "matrix(",
};
const KEYWORDS = BORDER_STYLES ++ COLOR_NAMES ++ POSITION_KEYWORDS ++
BACKGROUND_REPEAT ++ FONT_STYLES ++ FONT_SIZES ++ FONT_FAMILIES ++
CSS_GLOBAL ++ DISPLAY_VALUES;
const MAX_KEYWORD_LEN = lengthOfLongestValue(&KEYWORDS);
pub fn isKnownKeyword(value: []const u8) bool {
if (value.len > MAX_KEYWORD_LEN) {
return false;
}
var buf: [MAX_KEYWORD_LEN]u8 = undefined;
const normalized = std.ascii.lowerString(&buf, value);
for (KEYWORDS) |keyword| {
if (std.ascii.eqlIgnoreCase(normalized, keyword)) {
return true;
}
}
return false;
}
pub fn containsSpecialChar(value: []const u8) bool {
return std.mem.indexOfAny(u8, value, &SPECIAL_CHARS) != null;
}
const MAX_UNIT_LEN = lengthOfLongestValue(&UNITS);
pub fn isValidUnit(unit: []const u8) bool {
if (unit.len > MAX_UNIT_LEN) {
return false;
}
var buf: [MAX_UNIT_LEN]u8 = undefined;
const normalized = std.ascii.lowerString(&buf, unit);
for (UNITS) |u| {
if (std.mem.eql(u8, normalized, u)) {
return true;
}
}
return false;
}
pub fn startsWithFunction(value: []const u8) bool {
const pos = std.mem.indexOfScalar(u8, value, '(') orelse return false;
if (pos == 0) return false;
if (std.mem.indexOfScalarPos(u8, value, pos, ')') == null) {
return false;
}
const function_name = value[0..pos];
return isValidFunctionName(function_name);
}
fn isValidFunctionName(name: []const u8) bool {
if (name.len == 0) return false;
const first = name[0];
if (!std.ascii.isAlphabetic(first) and first != '_' and first != '-') {
return false;
}
for (name[1..]) |c| {
if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '-') {
return false;
}
}
return true;
}
};
fn lengthOfLongestValue(values: []const []const u8) usize {
var max: usize = 0;
for (values) |v| {
max = @max(v.len, max);
}
return max;
}
const testing = @import("../../testing.zig");
test "Browser: CSS.StyleDeclaration" {
try testing.htmlRunner("cssom/css_style_declaration.html");
}
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - valid numbers with units" {
try testing.expect(isNumericWithUnit("10px"));
try testing.expect(isNumericWithUnit("3.14em"));
try testing.expect(isNumericWithUnit("-5rem"));
try testing.expect(isNumericWithUnit("+12.5%"));
try testing.expect(isNumericWithUnit("0vh"));
try testing.expect(isNumericWithUnit(".5vw"));
}
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - scientific notation" {
try testing.expect(isNumericWithUnit("1e5px"));
try testing.expect(isNumericWithUnit("2.5E-3em"));
try testing.expect(isNumericWithUnit("1e+2rem"));
try testing.expect(isNumericWithUnit("-3.14e10px"));
}
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - edge cases and invalid inputs" {
try testing.expect(!isNumericWithUnit(""));
try testing.expect(!isNumericWithUnit("px"));
try testing.expect(!isNumericWithUnit("--px"));
try testing.expect(!isNumericWithUnit(".px"));
try testing.expect(!isNumericWithUnit("1e"));
try testing.expect(!isNumericWithUnit("1epx"));
try testing.expect(!isNumericWithUnit("1e+"));
try testing.expect(!isNumericWithUnit("1e+px"));
try testing.expect(!isNumericWithUnit("1.2.3px"));
try testing.expect(!isNumericWithUnit("10xyz"));
try testing.expect(!isNumericWithUnit("5invalid"));
try testing.expect(isNumericWithUnit("10"));
try testing.expect(isNumericWithUnit("3.14"));
try testing.expect(isNumericWithUnit("-5"));
}
test "Browser: CSS.StyleDeclaration: isHexColor - valid hex colors" {
try testing.expect(isHexColor("#000"));
try testing.expect(isHexColor("#fff"));
try testing.expect(isHexColor("#123456"));
try testing.expect(isHexColor("#abcdef"));
try testing.expect(isHexColor("#ABCDEF"));
try testing.expect(isHexColor("#12345678"));
}
test "Browser: CSS.StyleDeclaration: isHexColor - invalid hex colors" {
try testing.expect(!isHexColor(""));
try testing.expect(!isHexColor("#"));
try testing.expect(!isHexColor("000"));
try testing.expect(!isHexColor("#00"));
try testing.expect(!isHexColor("#0000"));
try testing.expect(!isHexColor("#00000"));
try testing.expect(!isHexColor("#0000000"));
try testing.expect(!isHexColor("#000000000"));
try testing.expect(!isHexColor("#gggggg"));
try testing.expect(!isHexColor("#123xyz"));
}
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - valid multi-value properties" {
try testing.expect(isMultiValueProperty("10px 20px"));
try testing.expect(isMultiValueProperty("solid red"));
try testing.expect(isMultiValueProperty("#fff black"));
try testing.expect(isMultiValueProperty("1em 2em 3em 4em"));
try testing.expect(isMultiValueProperty("rgb(255,0,0) solid"));
}
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - invalid multi-value properties" {
try testing.expect(!isMultiValueProperty(""));
try testing.expect(!isMultiValueProperty("10px"));
try testing.expect(!isMultiValueProperty("invalid unknown"));
try testing.expect(!isMultiValueProperty("10px invalid"));
try testing.expect(!isMultiValueProperty(" "));
}
test "Browser: CSS.StyleDeclaration: isAlreadyQuoted - various quoting scenarios" {
try testing.expect(isAlreadyQuoted("\"hello\""));
try testing.expect(isAlreadyQuoted("'world'"));
try testing.expect(isAlreadyQuoted("\"\""));
try testing.expect(isAlreadyQuoted("''"));
try testing.expect(!isAlreadyQuoted(""));
try testing.expect(!isAlreadyQuoted("hello"));
try testing.expect(!isAlreadyQuoted("\""));
try testing.expect(!isAlreadyQuoted("'"));
try testing.expect(!isAlreadyQuoted("\"hello'"));
try testing.expect(!isAlreadyQuoted("'hello\""));
try testing.expect(!isAlreadyQuoted("\"hello"));
try testing.expect(!isAlreadyQuoted("hello\""));
}
test "Browser: CSS.StyleDeclaration: isValidPropertyName - valid property names" {
try testing.expect(isValidPropertyName("color"));
try testing.expect(isValidPropertyName("background-color"));
try testing.expect(isValidPropertyName("-webkit-transform"));
try testing.expect(isValidPropertyName("font-size"));
try testing.expect(isValidPropertyName("margin-top"));
try testing.expect(isValidPropertyName("z-index"));
try testing.expect(isValidPropertyName("line-height"));
}
test "Browser: CSS.StyleDeclaration: isValidPropertyName - invalid property names" {
try testing.expect(!isValidPropertyName(""));
try testing.expect(!isValidPropertyName("123color"));
try testing.expect(!isValidPropertyName("color!"));
try testing.expect(!isValidPropertyName("color space"));
try testing.expect(!isValidPropertyName("@color"));
try testing.expect(!isValidPropertyName("color.test"));
try testing.expect(!isValidPropertyName("color_test"));
}
test "Browser: CSS.StyleDeclaration: extractImportant - with and without !important" {
var result = extractImportant("red !important");
try testing.expect(result.is_important);
try testing.expectEqual("red", result.value);
result = extractImportant("blue");
try testing.expect(!result.is_important);
try testing.expectEqual("blue", result.value);
result = extractImportant(" green !important ");
try testing.expect(result.is_important);
try testing.expectEqual("green", result.value);
result = extractImportant("!important");
try testing.expect(result.is_important);
try testing.expectEqual("", result.value);
result = extractImportant("important");
try testing.expect(!result.is_important);
try testing.expectEqual("important", result.value);
}
test "Browser: CSS.StyleDeclaration: needsQuotes - various scenarios" {
try testing.expect(needsQuotes(""));
try testing.expect(needsQuotes("hello world"));
try testing.expect(needsQuotes("test;"));
try testing.expect(needsQuotes("a{b}"));
try testing.expect(needsQuotes("test\"quote"));
try testing.expect(!needsQuotes("\"already quoted\""));
try testing.expect(!needsQuotes("'already quoted'"));
try testing.expect(!needsQuotes("url(image.png)"));
try testing.expect(!needsQuotes("rgb(255, 0, 0)"));
try testing.expect(!needsQuotes("10px 20px"));
try testing.expect(!needsQuotes("simple"));
}
test "Browser: CSS.StyleDeclaration: escapeCSSValue - escaping various characters" {
const allocator = testing.arena_allocator;
var result = try escapeCSSValue(allocator, "simple");
try testing.expectEqual("simple", result);
result = try escapeCSSValue(allocator, "\"already quoted\"");
try testing.expectEqual("\"already quoted\"", result);
result = try escapeCSSValue(allocator, "test\"quote");
try testing.expectEqual("\"test\\\"quote\"", result);
result = try escapeCSSValue(allocator, "test\nline");
try testing.expectEqual("\"test\\A line\"", result);
result = try escapeCSSValue(allocator, "test\\back");
try testing.expectEqual("\"test\\\\back\"", result);
}
test "Browser: CSS.StyleDeclaration: CSSKeywords.isKnownKeyword - case sensitivity" {
try testing.expect(CSSKeywords.isKnownKeyword("red"));
try testing.expect(CSSKeywords.isKnownKeyword("solid"));
try testing.expect(CSSKeywords.isKnownKeyword("center"));
try testing.expect(CSSKeywords.isKnownKeyword("inherit"));
try testing.expect(CSSKeywords.isKnownKeyword("RED"));
try testing.expect(CSSKeywords.isKnownKeyword("Red"));
try testing.expect(CSSKeywords.isKnownKeyword("SOLID"));
try testing.expect(CSSKeywords.isKnownKeyword("Center"));
try testing.expect(!CSSKeywords.isKnownKeyword("invalid"));
try testing.expect(!CSSKeywords.isKnownKeyword("unknown"));
try testing.expect(!CSSKeywords.isKnownKeyword(""));
}
test "Browser: CSS.StyleDeclaration: CSSKeywords.containsSpecialChar - various special characters" {
try testing.expect(CSSKeywords.containsSpecialChar("test\"quote"));
try testing.expect(CSSKeywords.containsSpecialChar("test'quote"));
try testing.expect(CSSKeywords.containsSpecialChar("test;end"));
try testing.expect(CSSKeywords.containsSpecialChar("test{brace"));
try testing.expect(CSSKeywords.containsSpecialChar("test}brace"));
try testing.expect(CSSKeywords.containsSpecialChar("test\\back"));
try testing.expect(CSSKeywords.containsSpecialChar("test<angle"));
try testing.expect(CSSKeywords.containsSpecialChar("test>angle"));
try testing.expect(CSSKeywords.containsSpecialChar("test/slash"));
try testing.expect(!CSSKeywords.containsSpecialChar("normal-text"));
try testing.expect(!CSSKeywords.containsSpecialChar("text123"));
try testing.expect(!CSSKeywords.containsSpecialChar(""));
}
test "Browser: CSS.StyleDeclaration: CSSKeywords.isValidUnit - various units" {
try testing.expect(CSSKeywords.isValidUnit("px"));
try testing.expect(CSSKeywords.isValidUnit("em"));
try testing.expect(CSSKeywords.isValidUnit("rem"));
try testing.expect(CSSKeywords.isValidUnit("%"));
try testing.expect(CSSKeywords.isValidUnit("deg"));
try testing.expect(CSSKeywords.isValidUnit("rad"));
try testing.expect(CSSKeywords.isValidUnit("s"));
try testing.expect(CSSKeywords.isValidUnit("ms"));
try testing.expect(CSSKeywords.isValidUnit("PX"));
try testing.expect(!CSSKeywords.isValidUnit("invalid"));
try testing.expect(!CSSKeywords.isValidUnit(""));
}
test "Browser: CSS.StyleDeclaration: CSSKeywords.startsWithFunction - function detection" {
try testing.expect(CSSKeywords.startsWithFunction("rgb(255, 0, 0)"));
try testing.expect(CSSKeywords.startsWithFunction("rgba(255, 0, 0, 0.5)"));
try testing.expect(CSSKeywords.startsWithFunction("url(image.png)"));
try testing.expect(CSSKeywords.startsWithFunction("calc(100% - 20px)"));
try testing.expect(CSSKeywords.startsWithFunction("var(--custom-property)"));
try testing.expect(CSSKeywords.startsWithFunction("linear-gradient(to right, red, blue)"));
try testing.expect(CSSKeywords.startsWithFunction("custom-function(args)"));
try testing.expect(CSSKeywords.startsWithFunction("unknown(test)"));
try testing.expect(!CSSKeywords.startsWithFunction("not-a-function"));
try testing.expect(!CSSKeywords.startsWithFunction("missing-paren)"));
try testing.expect(!CSSKeywords.startsWithFunction("missing-close("));
try testing.expect(!CSSKeywords.startsWithFunction(""));
try testing.expect(!CSSKeywords.startsWithFunction("rgb"));
}
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - whitespace handling" {
try testing.expect(!isNumericWithUnit(" 10px"));
try testing.expect(!isNumericWithUnit("10 px"));
try testing.expect(!isNumericWithUnit("10px "));
try testing.expect(!isNumericWithUnit(" 10 px "));
}
test "Browser: CSS.StyleDeclaration: extractImportant - whitespace edge cases" {
var result = extractImportant(" ");
try testing.expect(!result.is_important);
try testing.expectEqual("", result.value);
result = extractImportant("\t\n\r !important\t\n");
try testing.expect(result.is_important);
try testing.expectEqual("", result.value);
result = extractImportant("red\t!important");
try testing.expect(result.is_important);
try testing.expectEqual("red", result.value);
}
test "Browser: CSS.StyleDeclaration: isHexColor - mixed case handling" {
try testing.expect(isHexColor("#AbC"));
try testing.expect(isHexColor("#123aBc"));
try testing.expect(isHexColor("#FFffFF"));
try testing.expect(isHexColor("#000FFF"));
}
test "Browser: CSS.StyleDeclaration: edge case - very long inputs" {
const long_valid = "a" ** 1000 ++ "px";
try testing.expect(!isNumericWithUnit(long_valid)); // not numeric
const long_property = "a-" ** 100 ++ "property";
try testing.expect(isValidPropertyName(long_property));
const long_hex = "#" ++ "a" ** 20;
try testing.expect(!isHexColor(long_hex));
}
test "Browser: CSS.StyleDeclaration: boundary conditions - numeric parsing" {
try testing.expect(isNumericWithUnit("0px"));
try testing.expect(isNumericWithUnit("0.0px"));
try testing.expect(isNumericWithUnit(".0px"));
try testing.expect(isNumericWithUnit("0.px"));
try testing.expect(isNumericWithUnit("999999999px"));
try testing.expect(isNumericWithUnit("1.7976931348623157e+308px"));
try testing.expect(isNumericWithUnit("0.000000001px"));
try testing.expect(isNumericWithUnit("1e-100px"));
}
test "Browser: CSS.StyleDeclaration: extractImportant - malformed important declarations" {
var result = extractImportant("red ! important");
try testing.expect(!result.is_important);
try testing.expectEqual("red ! important", result.value);
result = extractImportant("red !Important");
try testing.expect(!result.is_important);
try testing.expectEqual("red !Important", result.value);
result = extractImportant("red !IMPORTANT");
try testing.expect(!result.is_important);
try testing.expectEqual("red !IMPORTANT", result.value);
result = extractImportant("!importantred");
try testing.expect(!result.is_important);
try testing.expectEqual("!importantred", result.value);
result = extractImportant("red !important !important");
try testing.expect(result.is_important);
try testing.expectEqual("red !important", result.value);
}
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - complex spacing scenarios" {
try testing.expect(isMultiValueProperty("10px 20px"));
try testing.expect(isMultiValueProperty("solid red"));
try testing.expect(isMultiValueProperty(" 10px 20px "));
try testing.expect(!isMultiValueProperty("10px\t20px"));
try testing.expect(!isMultiValueProperty("10px\n20px"));
try testing.expect(isMultiValueProperty("10px 20px 30px"));
}
test "Browser: CSS.StyleDeclaration: isAlreadyQuoted - edge cases with quotes" {
try testing.expect(isAlreadyQuoted("\"'hello'\""));
try testing.expect(isAlreadyQuoted("'\"hello\"'"));
try testing.expect(isAlreadyQuoted("\"hello\\\"world\""));
try testing.expect(isAlreadyQuoted("'hello\\'world'"));
try testing.expect(!isAlreadyQuoted("\"hello"));
try testing.expect(!isAlreadyQuoted("hello\""));
try testing.expect(!isAlreadyQuoted("'hello"));
try testing.expect(!isAlreadyQuoted("hello'"));
try testing.expect(isAlreadyQuoted("\"a\""));
try testing.expect(isAlreadyQuoted("'b'"));
}
test "Browser: CSS.StyleDeclaration: needsQuotes - function and URL edge cases" {
try testing.expect(!needsQuotes("rgb(255, 0, 0)"));
try testing.expect(!needsQuotes("calc(100% - 20px)"));
try testing.expect(!needsQuotes("url(path with spaces.jpg)"));
try testing.expect(!needsQuotes("linear-gradient(to right, red, blue)"));
try testing.expect(needsQuotes("rgb(255, 0, 0"));
}
test "Browser: CSS.StyleDeclaration: escapeCSSValue - control characters and Unicode" {
const allocator = testing.arena_allocator;
var result = try escapeCSSValue(allocator, "test\ttab");
try testing.expectEqual("\"test\\9 tab\"", result);
result = try escapeCSSValue(allocator, "test\rreturn");
try testing.expectEqual("\"test\\D return\"", result);
result = try escapeCSSValue(allocator, "test\x00null");
try testing.expectEqual("\"test\\0null\"", result);
result = try escapeCSSValue(allocator, "test\x7Fdel");
try testing.expectEqual("\"test\\7f del\"", result);
result = try escapeCSSValue(allocator, "test\"quote\nline\\back");
try testing.expectEqual("\"test\\\"quote\\A line\\\\back\"", result);
}
test "Browser: CSS.StyleDeclaration: isValidPropertyName - CSS custom properties and vendor prefixes" {
try testing.expect(isValidPropertyName("--custom-color"));
try testing.expect(isValidPropertyName("--my-variable"));
try testing.expect(isValidPropertyName("--123"));
try testing.expect(isValidPropertyName("-webkit-transform"));
try testing.expect(isValidPropertyName("-moz-border-radius"));
try testing.expect(isValidPropertyName("-ms-filter"));
try testing.expect(isValidPropertyName("-o-transition"));
try testing.expect(!isValidPropertyName("-123invalid"));
try testing.expect(!isValidPropertyName("--"));
try testing.expect(!isValidPropertyName("-"));
}
test "Browser: CSS.StyleDeclaration: startsWithFunction - case sensitivity and partial matches" {
try testing.expect(CSSKeywords.startsWithFunction("RGB(255, 0, 0)"));
try testing.expect(CSSKeywords.startsWithFunction("Rgb(255, 0, 0)"));
try testing.expect(CSSKeywords.startsWithFunction("URL(image.png)"));
try testing.expect(CSSKeywords.startsWithFunction("rg(something)"));
try testing.expect(CSSKeywords.startsWithFunction("ur(something)"));
try testing.expect(CSSKeywords.startsWithFunction("rgb(1,2,3)"));
try testing.expect(CSSKeywords.startsWithFunction("rgba(1,2,3,4)"));
try testing.expect(CSSKeywords.startsWithFunction("my-custom-function(args)"));
try testing.expect(CSSKeywords.startsWithFunction("function-with-dashes(test)"));
try testing.expect(!CSSKeywords.startsWithFunction("123function(test)"));
}
test "Browser: CSS.StyleDeclaration: isHexColor - Unicode and invalid characters" {
try testing.expect(!isHexColor("#ghijkl"));
try testing.expect(!isHexColor("#12345g"));
try testing.expect(!isHexColor("#xyz"));
try testing.expect(!isHexColor("#АВС"));
try testing.expect(!isHexColor("#1234567g"));
try testing.expect(!isHexColor("#g2345678"));
}
test "Browser: CSS.StyleDeclaration: complex integration scenarios" {
const allocator = testing.arena_allocator;
try testing.expect(isMultiValueProperty("rgb(255,0,0) url(bg.jpg)"));
try testing.expect(!needsQuotes("calc(100% - 20px)"));
const result = try escapeCSSValue(allocator, "fake(function with spaces");
try testing.expectEqual("\"fake(function with spaces\"", result);
const important_result = extractImportant("rgb(255,0,0) !important");
try testing.expect(important_result.is_important);
try testing.expectEqual("rgb(255,0,0)", important_result.value);
}
test "Browser: CSS.StyleDeclaration: performance edge cases - empty and minimal inputs" {
try testing.expect(!isNumericWithUnit(""));
try testing.expect(!isHexColor(""));
try testing.expect(!isMultiValueProperty(""));
try testing.expect(!isAlreadyQuoted(""));
try testing.expect(!isValidPropertyName(""));
try testing.expect(needsQuotes(""));
try testing.expect(!CSSKeywords.isKnownKeyword(""));
try testing.expect(!CSSKeywords.containsSpecialChar(""));
try testing.expect(!CSSKeywords.isValidUnit(""));
try testing.expect(!CSSKeywords.startsWithFunction(""));
try testing.expect(!isNumericWithUnit("a"));
try testing.expect(!isHexColor("a"));
try testing.expect(!isMultiValueProperty("a"));
try testing.expect(!isAlreadyQuoted("a"));
try testing.expect(isValidPropertyName("a"));
try testing.expect(!needsQuotes("a"));
}

View File

@@ -0,0 +1,97 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const StyleSheet = @import("StyleSheet.zig");
const CSSRuleList = @import("CSSRuleList.zig");
const CSSImportRule = @import("CSSRule.zig").CSSImportRule;
const CSSStyleSheet = @This();
pub const prototype = *StyleSheet;
proto: StyleSheet,
css_rules: CSSRuleList,
owner_rule: ?*CSSImportRule,
const CSSStyleSheetOpts = struct {
base_url: ?[]const u8 = null,
// TODO: Suupport media
disabled: bool = false,
};
pub fn constructor(_opts: ?CSSStyleSheetOpts) !CSSStyleSheet {
const opts = _opts orelse CSSStyleSheetOpts{};
return .{
.proto = .{ .disabled = opts.disabled },
.css_rules = .constructor(),
.owner_rule = null,
};
}
pub fn get_ownerRule(_: *CSSStyleSheet) ?*CSSImportRule {
return null;
}
pub fn get_cssRules(self: *CSSStyleSheet) *CSSRuleList {
return &self.css_rules;
}
pub fn _insertRule(self: *CSSStyleSheet, rule: []const u8, _index: ?usize, page: *Page) !usize {
const index = _index orelse 0;
if (index > self.css_rules.list.items.len) {
return error.IndexSize;
}
const arena = page.arena;
try self.css_rules.list.insert(arena, index, try arena.dupe(u8, rule));
return index;
}
pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
if (index > self.css_rules.list.items.len) {
return error.IndexSize;
}
_ = self.css_rules.list.orderedRemove(index);
}
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !Env.Promise {
_ = self;
_ = text;
// TODO: clear self.css_rules
// parse text and re-populate self.css_rules
const resolver = page.main_context.createPromiseResolver();
try resolver.resolve({});
return resolver.promise();
}
pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
_ = self;
_ = text;
// TODO: clear self.css_rules
// parse text and re-populate self.css_rules
}
const testing = @import("../../testing.zig");
test "Browser: CSS.StyleSheet" {
try testing.htmlRunner("cssom/css_stylesheet.html");
}

View File

@@ -19,37 +19,37 @@
const parser = @import("../netsurf.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/StyleSheet#specifications
pub const StyleSheet = struct {
disabled: bool = false,
href: []const u8 = "",
owner_node: ?*parser.Node = null,
parent_stylesheet: ?*StyleSheet = null,
title: []const u8 = "",
type: []const u8 = "text/css",
const StyleSheet = @This();
pub fn get_disabled(self: *const StyleSheet) bool {
return self.disabled;
}
disabled: bool = false,
href: []const u8 = "",
owner_node: ?*parser.Node = null,
parent_stylesheet: ?*StyleSheet = null,
title: []const u8 = "",
type: []const u8 = "text/css",
pub fn get_href(self: *const StyleSheet) []const u8 {
return self.href;
}
pub fn get_disabled(self: *const StyleSheet) bool {
return self.disabled;
}
// TODO: media
pub fn get_href(self: *const StyleSheet) []const u8 {
return self.href;
}
pub fn get_ownerNode(self: *const StyleSheet) ?*parser.Node {
return self.owner_node;
}
// TODO: media
pub fn get_parentStyleSheet(self: *const StyleSheet) ?*StyleSheet {
return self.parent_stylesheet;
}
pub fn get_ownerNode(self: *const StyleSheet) ?*parser.Node {
return self.owner_node;
}
pub fn get_title(self: *const StyleSheet) []const u8 {
return self.title;
}
pub fn get_parentStyleSheet(self: *const StyleSheet) ?*StyleSheet {
return self.parent_stylesheet;
}
pub fn get_type(self: *const StyleSheet) []const u8 {
return self.type;
}
};
pub fn get_title(self: *const StyleSheet) []const u8 {
return self.title;
}
pub fn get_type(self: *const StyleSheet) []const u8 {
return self.type;
}

View File

@@ -1,291 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const CSSConstants = struct {
const IMPORTANT = "!important";
const URL_PREFIX = "url(";
};
pub const CSSParserState = enum {
seek_name,
in_name,
seek_colon,
seek_value,
in_value,
in_quoted_value,
in_single_quoted_value,
in_url,
in_important,
};
pub const CSSDeclaration = struct {
name: []const u8,
value: []const u8,
is_important: bool,
};
pub const CSSParser = struct {
state: CSSParserState,
name_start: usize,
name_end: usize,
value_start: usize,
position: usize,
paren_depth: usize,
escape_next: bool,
pub fn init() CSSParser {
return .{
.state = .seek_name,
.name_start = 0,
.name_end = 0,
.value_start = 0,
.position = 0,
.paren_depth = 0,
.escape_next = false,
};
}
pub fn parseDeclarations(arena: Allocator, text: []const u8) ![]CSSDeclaration {
var parser = init();
var declarations: std.ArrayListUnmanaged(CSSDeclaration) = .empty;
while (parser.position < text.len) {
const c = text[parser.position];
switch (parser.state) {
.seek_name => {
if (!std.ascii.isWhitespace(c)) {
parser.name_start = parser.position;
parser.state = .in_name;
continue;
}
},
.in_name => {
if (c == ':') {
parser.name_end = parser.position;
parser.state = .seek_value;
} else if (std.ascii.isWhitespace(c)) {
parser.name_end = parser.position;
parser.state = .seek_colon;
}
},
.seek_colon => {
if (c == ':') {
parser.state = .seek_value;
} else if (!std.ascii.isWhitespace(c)) {
parser.state = .seek_name;
continue;
}
},
.seek_value => {
if (!std.ascii.isWhitespace(c)) {
parser.value_start = parser.position;
if (c == '"') {
parser.state = .in_quoted_value;
} else if (c == '\'') {
parser.state = .in_single_quoted_value;
} else if (c == 'u' and parser.position + CSSConstants.URL_PREFIX.len <= text.len and std.mem.startsWith(u8, text[parser.position..], CSSConstants.URL_PREFIX)) {
parser.state = .in_url;
parser.paren_depth = 1;
parser.position += 3;
} else {
parser.state = .in_value;
continue;
}
}
},
.in_value => {
if (parser.escape_next) {
parser.escape_next = false;
} else if (c == '\\') {
parser.escape_next = true;
} else if (c == '(') {
parser.paren_depth += 1;
} else if (c == ')' and parser.paren_depth > 0) {
parser.paren_depth -= 1;
} else if (c == ';' and parser.paren_depth == 0) {
try parser.finishDeclaration(arena, &declarations, text);
parser.state = .seek_name;
}
},
.in_quoted_value => {
if (parser.escape_next) {
parser.escape_next = false;
} else if (c == '\\') {
parser.escape_next = true;
} else if (c == '"') {
parser.state = .in_value;
}
},
.in_single_quoted_value => {
if (parser.escape_next) {
parser.escape_next = false;
} else if (c == '\\') {
parser.escape_next = true;
} else if (c == '\'') {
parser.state = .in_value;
}
},
.in_url => {
if (parser.escape_next) {
parser.escape_next = false;
} else if (c == '\\') {
parser.escape_next = true;
} else if (c == '(') {
parser.paren_depth += 1;
} else if (c == ')') {
parser.paren_depth -= 1;
if (parser.paren_depth == 0) {
parser.state = .in_value;
}
}
},
.in_important => {},
}
parser.position += 1;
}
try parser.finalize(arena, &declarations, text);
return declarations.items;
}
fn finishDeclaration(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
const name = std.mem.trim(u8, text[self.name_start..self.name_end], &std.ascii.whitespace);
if (name.len == 0) return;
const raw_value = text[self.value_start..self.position];
const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace);
var final_value = value;
var is_important = false;
if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT)) {
is_important = true;
final_value = std.mem.trimRight(u8, value[0 .. value.len - CSSConstants.IMPORTANT.len], &std.ascii.whitespace);
}
try declarations.append(arena, .{
.name = name,
.value = final_value,
.is_important = is_important,
});
}
fn finalize(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
if (self.state != .in_value) {
return;
}
return self.finishDeclaration(arena, declarations, text);
}
};
const testing = @import("../../testing.zig");
test "CSSParser - Simple property" {
defer testing.reset();
const text = "color: red;";
const allocator = testing.arena_allocator;
const declarations = try CSSParser.parseDeclarations(allocator, text);
try testing.expectEqual(1, declarations.len);
try testing.expectEqual("color", declarations[0].name);
try testing.expectEqual("red", declarations[0].value);
try testing.expectEqual(false, declarations[0].is_important);
}
test "CSSParser - Property with !important" {
defer testing.reset();
const text = "margin: 10px !important;";
const allocator = testing.arena_allocator;
const declarations = try CSSParser.parseDeclarations(allocator, text);
try testing.expectEqual(1, declarations.len);
try testing.expectEqual("margin", declarations[0].name);
try testing.expectEqual("10px", declarations[0].value);
try testing.expectEqual(true, declarations[0].is_important);
}
test "CSSParser - Multiple properties" {
defer testing.reset();
const text = "color: red; font-size: 12px; margin: 5px !important;";
const allocator = testing.arena_allocator;
const declarations = try CSSParser.parseDeclarations(allocator, text);
try testing.expect(declarations.len == 3);
try testing.expectEqual("color", declarations[0].name);
try testing.expectEqual("red", declarations[0].value);
try testing.expectEqual(false, declarations[0].is_important);
try testing.expectEqual("font-size", declarations[1].name);
try testing.expectEqual("12px", declarations[1].value);
try testing.expectEqual(false, declarations[1].is_important);
try testing.expectEqual("margin", declarations[2].name);
try testing.expectEqual("5px", declarations[2].value);
try testing.expectEqual(true, declarations[2].is_important);
}
test "CSSParser - Quoted value with semicolon" {
defer testing.reset();
const text = "content: \"Hello; world!\";";
const allocator = testing.arena_allocator;
const declarations = try CSSParser.parseDeclarations(allocator, text);
try testing.expectEqual(1, declarations.len);
try testing.expectEqual("content", declarations[0].name);
try testing.expectEqual("\"Hello; world!\"", declarations[0].value);
try testing.expectEqual(false, declarations[0].is_important);
}
test "CSSParser - URL value" {
defer testing.reset();
const text = "background-image: url(\"test.png\");";
const allocator = testing.arena_allocator;
const declarations = try CSSParser.parseDeclarations(allocator, text);
try testing.expectEqual(1, declarations.len);
try testing.expectEqual("background-image", declarations[0].name);
try testing.expectEqual("url(\"test.png\")", declarations[0].value);
try testing.expectEqual(false, declarations[0].is_important);
}
test "CSSParser - Whitespace handling" {
defer testing.reset();
const text = " color : purple ; margin : 10px ; ";
const allocator = testing.arena_allocator;
const declarations = try CSSParser.parseDeclarations(allocator, text);
try testing.expectEqual(2, declarations.len);
try testing.expectEqual("color", declarations[0].name);
try testing.expectEqual("purple", declarations[0].value);
try testing.expectEqual("margin", declarations[1].name);
try testing.expectEqual("10px", declarations[1].value);
}

View File

@@ -1,60 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const StyleSheet = @import("stylesheet.zig").StyleSheet;
const CSSRule = @import("css_rule.zig").CSSRule;
const CSSImportRule = @import("css_rule.zig").CSSImportRule;
pub const CSSRuleList = struct {
list: std.ArrayListUnmanaged([]const u8),
pub fn constructor() CSSRuleList {
return .{ .list = .empty };
}
pub fn _item(self: *CSSRuleList, _index: u32) ?CSSRule {
const index: usize = @intCast(_index);
if (index > self.list.items.len) {
return null;
}
// todo: for now, just return null.
// this depends on properly parsing CSSRule
return null;
}
pub fn get_length(self: *CSSRuleList) u32 {
return @intCast(self.list.items.len);
}
};
const testing = @import("../../testing.zig");
test "Browser.CSS.CSSRuleList" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let list = new CSSRuleList()", "undefined" },
.{ "list instanceof CSSRuleList", "true" },
.{ "list.length", "0" },
.{ "list.item(0)", "null" },
}, .{});
}

View File

@@ -1,241 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CSSParser = @import("./css_parser.zig").CSSParser;
const CSSValueAnalyzer = @import("./css_value_analyzer.zig").CSSValueAnalyzer;
const CSSRule = @import("css_rule.zig").CSSRule;
const Page = @import("../page.zig").Page;
pub const CSSStyleDeclaration = struct {
store: std.StringHashMapUnmanaged(Property),
order: std.ArrayListUnmanaged([]const u8),
pub const empty: CSSStyleDeclaration = .{
.store = .empty,
.order = .empty,
};
const Property = struct {
value: []const u8,
priority: bool,
};
pub fn get_cssFloat(self: *const CSSStyleDeclaration) []const u8 {
return self._getPropertyValue("float");
}
pub fn set_cssFloat(self: *CSSStyleDeclaration, value: ?[]const u8, page: *Page) !void {
const final_value = value orelse "";
return self._setProperty("float", final_value, null, page);
}
pub fn get_cssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 {
var buffer: std.ArrayListUnmanaged(u8) = .empty;
const writer = buffer.writer(page.call_arena);
for (self.order.items) |name| {
const prop = self.store.get(name).?;
const escaped = try CSSValueAnalyzer.escapeCSSValue(page.call_arena, prop.value);
try writer.print("{s}: {s}", .{ name, escaped });
if (prop.priority) try writer.writeAll(" !important");
try writer.writeAll("; ");
}
return buffer.items;
}
// TODO Propagate also upward to parent node
pub fn set_cssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void {
self.store.clearRetainingCapacity();
self.order.clearRetainingCapacity();
// call_arena is safe here, because _setProperty will dupe the name
// using the page's longer-living arena.
const declarations = try CSSParser.parseDeclarations(page.call_arena, text);
for (declarations) |decl| {
if (!CSSValueAnalyzer.isValidPropertyName(decl.name)) continue;
const priority: ?[]const u8 = if (decl.is_important) "important" else null;
try self._setProperty(decl.name, decl.value, priority, page);
}
}
pub fn get_length(self: *const CSSStyleDeclaration) usize {
return self.order.items.len;
}
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
return null;
}
pub fn _getPropertyPriority(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
return if (self.store.get(name)) |prop| (if (prop.priority) "important" else "") else "";
}
// TODO should handle properly shorthand properties and canonical forms
pub fn _getPropertyValue(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
if (self.store.get(name)) |prop| {
return prop.value;
}
// default to everything being visible (unless it's been explicitly set)
if (std.mem.eql(u8, name, "visibility")) {
return "visible";
}
return "";
}
pub fn _item(self: *const CSSStyleDeclaration, index: usize) []const u8 {
return if (index < self.order.items.len) self.order.items[index] else "";
}
pub fn _removeProperty(self: *CSSStyleDeclaration, name: []const u8) ![]const u8 {
const prop = self.store.fetchRemove(name) orelse return "";
for (self.order.items, 0..) |item, i| {
if (std.mem.eql(u8, item, name)) {
_ = self.order.orderedRemove(i);
break;
}
}
// safe to return, since it's in our page.arena
return prop.value.value;
}
pub fn _setProperty(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, priority: ?[]const u8, page: *Page) !void {
const owned_value = try page.arena.dupe(u8, value);
const is_important = priority != null and std.ascii.eqlIgnoreCase(priority.?, "important");
const gop = try self.store.getOrPut(page.arena, name);
if (!gop.found_existing) {
const owned_name = try page.arena.dupe(u8, name);
gop.key_ptr.* = owned_name;
try self.order.append(page.arena, owned_name);
}
gop.value_ptr.* = .{ .value = owned_value, .priority = is_important };
}
pub fn named_get(self: *const CSSStyleDeclaration, name: []const u8, _: *bool) []const u8 {
return self._getPropertyValue(name);
}
};
const testing = @import("../../testing.zig");
test "CSSOM.CSSStyleDeclaration" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let style = document.getElementById('content').style", "undefined" },
.{ "style.cssText = 'color: red; font-size: 12px; margin: 5px !important;'", "color: red; font-size: 12px; margin: 5px !important;" },
.{ "style.length", "3" },
}, .{});
try runner.testCases(&.{
.{ "style.getPropertyValue('color')", "red" },
.{ "style.getPropertyValue('font-size')", "12px" },
.{ "style.getPropertyValue('unknown-property')", "" },
.{ "style.getPropertyPriority('margin')", "important" },
.{ "style.getPropertyPriority('color')", "" },
.{ "style.getPropertyPriority('unknown-property')", "" },
.{ "style.item(0)", "color" },
.{ "style.item(1)", "font-size" },
.{ "style.item(2)", "margin" },
.{ "style.item(3)", "" },
}, .{});
try runner.testCases(&.{
.{ "style.setProperty('background-color', 'blue')", "undefined" },
.{ "style.getPropertyValue('background-color')", "blue" },
.{ "style.length", "4" },
.{ "style.setProperty('color', 'green')", "undefined" },
.{ "style.getPropertyValue('color')", "green" },
.{ "style.length", "4" },
.{ "style.color", "green" },
.{ "style.setProperty('padding', '10px', 'important')", "undefined" },
.{ "style.getPropertyValue('padding')", "10px" },
.{ "style.getPropertyPriority('padding')", "important" },
.{ "style.setProperty('border', '1px solid black', 'IMPORTANT')", "undefined" },
.{ "style.getPropertyPriority('border')", "important" },
}, .{});
try runner.testCases(&.{
.{ "style.removeProperty('color')", "green" },
.{ "style.getPropertyValue('color')", "" },
.{ "style.length", "5" },
.{ "style.removeProperty('unknown-property')", "" },
}, .{});
try runner.testCases(&.{
.{ "style.cssText.includes('font-size: 12px;')", "true" },
.{ "style.cssText.includes('margin: 5px !important;')", "true" },
.{ "style.cssText.includes('padding: 10px !important;')", "true" },
.{ "style.cssText.includes('border: 1px solid black !important;')", "true" },
.{ "style.cssText = 'color: purple; text-align: center;'", "color: purple; text-align: center;" },
.{ "style.length", "2" },
.{ "style.getPropertyValue('color')", "purple" },
.{ "style.getPropertyValue('text-align')", "center" },
.{ "style.getPropertyValue('font-size')", "" },
.{ "style.setProperty('cont', 'Hello; world!')", "undefined" },
.{ "style.getPropertyValue('cont')", "Hello; world!" },
.{ "style.cssText = 'content: \"Hello; world!\"; background-image: url(\"test.png\");'", "content: \"Hello; world!\"; background-image: url(\"test.png\");" },
.{ "style.getPropertyValue('content')", "\"Hello; world!\"" },
.{ "style.getPropertyValue('background-image')", "url(\"test.png\")" },
}, .{});
try runner.testCases(&.{
.{ "style.cssFloat", "" },
.{ "style.cssFloat = 'left'", "left" },
.{ "style.cssFloat", "left" },
.{ "style.getPropertyValue('float')", "left" },
.{ "style.cssFloat = 'right'", "right" },
.{ "style.cssFloat", "right" },
.{ "style.cssFloat = null", "null" },
.{ "style.cssFloat", "" },
}, .{});
try runner.testCases(&.{
.{ "style.setProperty('display', '')", "undefined" },
.{ "style.getPropertyValue('display')", "" },
.{ "style.cssText = ' color : purple ; margin : 10px ; '", " color : purple ; margin : 10px ; " },
.{ "style.getPropertyValue('color')", "purple" },
.{ "style.getPropertyValue('margin')", "10px" },
.{ "style.setProperty('border-bottom-left-radius', '5px')", "undefined" },
.{ "style.getPropertyValue('border-bottom-left-radius')", "5px" },
}, .{});
try runner.testCases(&.{
.{ "style.visibility", "visible" },
.{ "style.getPropertyValue('visibility')", "visible" },
}, .{});
}

View File

@@ -1,91 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Page = @import("../page.zig").Page;
const StyleSheet = @import("stylesheet.zig").StyleSheet;
const CSSRuleList = @import("css_rule_list.zig").CSSRuleList;
const CSSImportRule = @import("css_rule.zig").CSSImportRule;
pub const CSSStyleSheet = struct {
pub const prototype = *StyleSheet;
proto: StyleSheet,
css_rules: CSSRuleList,
owner_rule: ?*CSSImportRule,
const CSSStyleSheetOpts = struct {
base_url: ?[]const u8 = null,
// TODO: Suupport media
disabled: bool = false,
};
pub fn constructor(_opts: ?CSSStyleSheetOpts) !CSSStyleSheet {
const opts = _opts orelse CSSStyleSheetOpts{};
return .{
.proto = StyleSheet{ .disabled = opts.disabled },
.css_rules = .constructor(),
.owner_rule = null,
};
}
pub fn get_ownerRule(_: *CSSStyleSheet) ?*CSSImportRule {
return null;
}
pub fn get_cssRules(self: *CSSStyleSheet) *CSSRuleList {
return &self.css_rules;
}
pub fn _insertRule(self: *CSSStyleSheet, rule: []const u8, _index: ?usize, page: *Page) !usize {
const index = _index orelse 0;
if (index > self.css_rules.list.items.len) {
return error.IndexSize;
}
const arena = page.arena;
try self.css_rules.list.insert(arena, index, try arena.dupe(u8, rule));
return index;
}
pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
if (index > self.css_rules.list.items.len) {
return error.IndexSize;
}
_ = self.css_rules.list.orderedRemove(index);
}
};
const testing = @import("../../testing.zig");
test "Browser.CSS.StyleSheet" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let css = new CSSStyleSheet()", "undefined" },
.{ "css instanceof CSSStyleSheet", "true" },
.{ "css.cssRules.length", "0" },
.{ "css.ownerRule", "null" },
.{ "let index1 = css.insertRule('body { color: red; }', 0)", "undefined" },
.{ "index1", "0" },
.{ "css.cssRules.length", "1" },
}, .{});
}

View File

@@ -1,811 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
pub const CSSValueAnalyzer = struct {
pub fn isNumericWithUnit(value: []const u8) bool {
if (value.len == 0) return false;
if (!std.ascii.isDigit(value[0]) and
value[0] != '+' and value[0] != '-' and value[0] != '.')
{
return false;
}
var i: usize = 0;
var has_digit = false;
var decimal_point = false;
while (i < value.len) : (i += 1) {
const c = value[i];
if (std.ascii.isDigit(c)) {
has_digit = true;
} else if (c == '.' and !decimal_point) {
decimal_point = true;
} else if ((c == 'e' or c == 'E') and has_digit) {
if (i + 1 >= value.len) return false;
if (value[i + 1] != '+' and value[i + 1] != '-' and !std.ascii.isDigit(value[i + 1])) break;
i += 1;
if (value[i] == '+' or value[i] == '-') {
i += 1;
}
var has_exp_digits = false;
while (i < value.len and std.ascii.isDigit(value[i])) : (i += 1) {
has_exp_digits = true;
}
if (!has_exp_digits) return false;
break;
} else if (c != '-' and c != '+') {
break;
}
}
if (!has_digit) return false;
if (i == value.len) return true;
const unit = value[i..];
return CSSKeywords.isValidUnit(unit);
}
pub fn isHexColor(value: []const u8) bool {
if (!std.mem.startsWith(u8, value, "#")) return false;
const hex_part = value[1..];
if (hex_part.len != 3 and hex_part.len != 6 and hex_part.len != 8) return false;
for (hex_part) |c| {
if (!std.ascii.isHex(c)) return false;
}
return true;
}
pub fn isMultiValueProperty(value: []const u8) bool {
var parts = std.mem.splitAny(u8, value, " ");
var multi_value_parts: usize = 0;
var all_parts_valid = true;
while (parts.next()) |part| {
if (part.len == 0) continue;
multi_value_parts += 1;
const is_numeric = isNumericWithUnit(part);
const is_hex_color = isHexColor(part);
const is_known_keyword = CSSKeywords.isKnownKeyword(part);
const is_function = CSSKeywords.startsWithFunction(part);
if (!is_numeric and !is_hex_color and !is_known_keyword and !is_function) {
all_parts_valid = false;
break;
}
}
return multi_value_parts >= 2 and all_parts_valid;
}
pub fn isAlreadyQuoted(value: []const u8) bool {
return value.len >= 2 and ((value[0] == '"' and value[value.len - 1] == '"') or
(value[0] == '\'' and value[value.len - 1] == '\''));
}
pub fn isValidPropertyName(name: []const u8) bool {
if (name.len == 0) return false;
if (std.mem.startsWith(u8, name, "--")) {
if (name.len == 2) return false;
for (name[2..]) |c| {
if (!std.ascii.isAlphanumeric(c) and c != '-' and c != '_') {
return false;
}
}
return true;
}
const first_char = name[0];
if (!std.ascii.isAlphabetic(first_char) and first_char != '-') {
return false;
}
if (first_char == '-') {
if (name.len < 2) return false;
if (!std.ascii.isAlphabetic(name[1])) {
return false;
}
for (name[2..]) |c| {
if (!std.ascii.isAlphanumeric(c) and c != '-') {
return false;
}
}
} else {
for (name[1..]) |c| {
if (!std.ascii.isAlphanumeric(c) and c != '-') {
return false;
}
}
}
return true;
}
pub fn extractImportant(value: []const u8) struct { value: []const u8, is_important: bool } {
const trimmed = std.mem.trim(u8, value, &std.ascii.whitespace);
if (std.mem.endsWith(u8, trimmed, "!important")) {
const clean_value = std.mem.trimRight(u8, trimmed[0 .. trimmed.len - 10], &std.ascii.whitespace);
return .{ .value = clean_value, .is_important = true };
}
return .{ .value = trimmed, .is_important = false };
}
pub fn needsQuotes(value: []const u8) bool {
if (value.len == 0) return true;
if (isAlreadyQuoted(value)) return false;
if (CSSKeywords.containsSpecialChar(value)) {
return true;
}
if (std.mem.indexOfScalar(u8, value, ' ') == null) {
return false;
}
const is_url = std.mem.startsWith(u8, value, "url(");
const is_function = CSSKeywords.startsWithFunction(value);
return !isMultiValueProperty(value) and
!is_url and
!is_function;
}
pub fn escapeCSSValue(arena: std.mem.Allocator, value: []const u8) ![]const u8 {
if (!needsQuotes(value)) {
return value;
}
var out: std.ArrayListUnmanaged(u8) = .empty;
// We'll need at least this much space, +2 for the quotes
try out.ensureTotalCapacity(arena, value.len + 2);
const writer = out.writer(arena);
try writer.writeByte('"');
for (value, 0..) |c, i| {
switch (c) {
'"' => try writer.writeAll("\\\""),
'\\' => try writer.writeAll("\\\\"),
'\n' => try writer.writeAll("\\A "),
'\r' => try writer.writeAll("\\D "),
'\t' => try writer.writeAll("\\9 "),
0...8, 11, 12, 14...31, 127 => {
try writer.print("\\{x}", .{c});
if (i + 1 < value.len and std.ascii.isHex(value[i + 1])) {
try writer.writeByte(' ');
}
},
else => try writer.writeByte(c),
}
}
try writer.writeByte('"');
return out.items;
}
pub fn isKnownKeyword(value: []const u8) bool {
return CSSKeywords.isKnownKeyword(value);
}
pub fn containsSpecialChar(value: []const u8) bool {
return CSSKeywords.containsSpecialChar(value);
}
};
const CSSKeywords = struct {
const border_styles = [_][]const u8{
"none", "solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset",
};
const color_names = [_][]const u8{
"black", "white", "red", "green", "blue", "yellow", "purple", "gray", "transparent",
"currentColor", "inherit",
};
const position_keywords = [_][]const u8{
"auto", "center", "left", "right", "top", "bottom",
};
const background_repeat = [_][]const u8{
"repeat", "no-repeat", "repeat-x", "repeat-y", "space", "round",
};
const font_styles = [_][]const u8{
"normal", "italic", "oblique", "bold", "bolder", "lighter",
};
const font_sizes = [_][]const u8{
"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large",
"smaller", "larger",
};
const font_families = [_][]const u8{
"serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
};
const css_global = [_][]const u8{
"initial", "inherit", "unset", "revert",
};
const display_values = [_][]const u8{
"block", "inline", "inline-block", "flex", "grid", "none",
};
const length_units = [_][]const u8{
"px", "em", "rem", "vw", "vh", "vmin", "vmax", "%", "pt", "pc", "in", "cm", "mm",
"ex", "ch", "fr",
};
const angle_units = [_][]const u8{
"deg", "rad", "grad", "turn",
};
const time_units = [_][]const u8{
"s", "ms",
};
const frequency_units = [_][]const u8{
"Hz", "kHz",
};
const resolution_units = [_][]const u8{
"dpi", "dpcm", "dppx",
};
const special_chars = [_]u8{
'"', '\'', ';', '{', '}', '\\', '<', '>', '/', '\n', '\t', '\r', '\x00', '\x7F',
};
const functions = [_][]const u8{
"rgb(", "rgba(", "hsl(", "hsla(", "url(", "calc(", "var(", "attr(",
"linear-gradient(", "radial-gradient(", "conic-gradient(", "translate(", "rotate(", "scale(", "skew(", "matrix(",
};
pub fn isKnownKeyword(value: []const u8) bool {
const all_categories = [_][]const []const u8{
&border_styles, &color_names, &position_keywords, &background_repeat,
&font_styles, &font_sizes, &font_families, &css_global,
&display_values,
};
for (all_categories) |category| {
for (category) |keyword| {
if (std.ascii.eqlIgnoreCase(value, keyword)) {
return true;
}
}
}
return false;
}
pub fn containsSpecialChar(value: []const u8) bool {
for (value) |c| {
for (special_chars) |special| {
if (c == special) {
return true;
}
}
}
return false;
}
pub fn isValidUnit(unit: []const u8) bool {
const all_units = [_][]const []const u8{
&length_units, &angle_units, &time_units, &frequency_units, &resolution_units,
};
for (all_units) |category| {
for (category) |valid_unit| {
if (std.ascii.eqlIgnoreCase(unit, valid_unit)) {
return true;
}
}
}
return false;
}
pub fn startsWithFunction(value: []const u8) bool {
const pos = std.mem.indexOfScalar(u8, value, '(') orelse return false;
if (pos == 0) return false;
if (std.mem.indexOfScalarPos(u8, value, pos, ')') == null) {
return false;
}
const function_name = value[0..pos];
return isValidFunctionName(function_name);
}
fn isValidFunctionName(name: []const u8) bool {
if (name.len == 0) return false;
const first = name[0];
if (!std.ascii.isAlphabetic(first) and first != '_' and first != '-') {
return false;
}
for (name[1..]) |c| {
if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '-') {
return false;
}
}
return true;
}
};
const testing = @import("../../testing.zig");
test "CSSValueAnalyzer: isNumericWithUnit - valid numbers with units" {
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("10px"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("3.14em"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-5rem"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("+12.5%"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0vh"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit(".5vw"));
}
test "CSSValueAnalyzer: isNumericWithUnit - scientific notation" {
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e5px"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("2.5E-3em"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e+2rem"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-3.14e10px"));
}
test "CSSValueAnalyzer: isNumericWithUnit - edge cases and invalid inputs" {
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(""));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("px"));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("--px"));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(".px"));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e"));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1epx"));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e+"));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e+px"));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1.2.3px"));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10xyz"));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("5invalid"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("10"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("3.14"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-5"));
}
test "CSSValueAnalyzer: isHexColor - valid hex colors" {
try testing.expect(CSSValueAnalyzer.isHexColor("#000"));
try testing.expect(CSSValueAnalyzer.isHexColor("#fff"));
try testing.expect(CSSValueAnalyzer.isHexColor("#123456"));
try testing.expect(CSSValueAnalyzer.isHexColor("#abcdef"));
try testing.expect(CSSValueAnalyzer.isHexColor("#ABCDEF"));
try testing.expect(CSSValueAnalyzer.isHexColor("#12345678"));
}
test "CSSValueAnalyzer: isHexColor - invalid hex colors" {
try testing.expect(!CSSValueAnalyzer.isHexColor(""));
try testing.expect(!CSSValueAnalyzer.isHexColor("#"));
try testing.expect(!CSSValueAnalyzer.isHexColor("000"));
try testing.expect(!CSSValueAnalyzer.isHexColor("#00"));
try testing.expect(!CSSValueAnalyzer.isHexColor("#0000"));
try testing.expect(!CSSValueAnalyzer.isHexColor("#00000"));
try testing.expect(!CSSValueAnalyzer.isHexColor("#0000000"));
try testing.expect(!CSSValueAnalyzer.isHexColor("#000000000"));
try testing.expect(!CSSValueAnalyzer.isHexColor("#gggggg"));
try testing.expect(!CSSValueAnalyzer.isHexColor("#123xyz"));
}
test "CSSValueAnalyzer: isMultiValueProperty - valid multi-value properties" {
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px"));
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("solid red"));
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("#fff black"));
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("1em 2em 3em 4em"));
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("rgb(255,0,0) solid"));
}
test "CSSValueAnalyzer: isMultiValueProperty - invalid multi-value properties" {
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(""));
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px"));
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("invalid unknown"));
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px invalid"));
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(" "));
}
test "CSSValueAnalyzer: isAlreadyQuoted - various quoting scenarios" {
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"hello\""));
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'world'"));
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"\""));
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("''"));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted(""));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello"));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\""));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'"));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello'"));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'hello\""));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello"));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello\""));
}
test "CSSValueAnalyzer: isValidPropertyName - valid property names" {
try testing.expect(CSSValueAnalyzer.isValidPropertyName("color"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("background-color"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-webkit-transform"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("font-size"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("margin-top"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("z-index"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("line-height"));
}
test "CSSValueAnalyzer: isValidPropertyName - invalid property names" {
try testing.expect(!CSSValueAnalyzer.isValidPropertyName(""));
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("123color"));
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color!"));
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color space"));
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("@color"));
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color.test"));
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color_test"));
}
test "CSSValueAnalyzer: extractImportant - with and without !important" {
var result = CSSValueAnalyzer.extractImportant("red !important");
try testing.expect(result.is_important);
try testing.expectEqual("red", result.value);
result = CSSValueAnalyzer.extractImportant("blue");
try testing.expect(!result.is_important);
try testing.expectEqual("blue", result.value);
result = CSSValueAnalyzer.extractImportant(" green !important ");
try testing.expect(result.is_important);
try testing.expectEqual("green", result.value);
result = CSSValueAnalyzer.extractImportant("!important");
try testing.expect(result.is_important);
try testing.expectEqual("", result.value);
result = CSSValueAnalyzer.extractImportant("important");
try testing.expect(!result.is_important);
try testing.expectEqual("important", result.value);
}
test "CSSValueAnalyzer: needsQuotes - various scenarios" {
try testing.expect(CSSValueAnalyzer.needsQuotes(""));
try testing.expect(CSSValueAnalyzer.needsQuotes("hello world"));
try testing.expect(CSSValueAnalyzer.needsQuotes("test;"));
try testing.expect(CSSValueAnalyzer.needsQuotes("a{b}"));
try testing.expect(CSSValueAnalyzer.needsQuotes("test\"quote"));
try testing.expect(!CSSValueAnalyzer.needsQuotes("\"already quoted\""));
try testing.expect(!CSSValueAnalyzer.needsQuotes("'already quoted'"));
try testing.expect(!CSSValueAnalyzer.needsQuotes("url(image.png)"));
try testing.expect(!CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0)"));
try testing.expect(!CSSValueAnalyzer.needsQuotes("10px 20px"));
try testing.expect(!CSSValueAnalyzer.needsQuotes("simple"));
}
test "CSSValueAnalyzer: escapeCSSValue - escaping various characters" {
const allocator = testing.arena_allocator;
var result = try CSSValueAnalyzer.escapeCSSValue(allocator, "simple");
try testing.expectEqual("simple", result);
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "\"already quoted\"");
try testing.expectEqual("\"already quoted\"", result);
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\"quote");
try testing.expectEqual("\"test\\\"quote\"", result);
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\nline");
try testing.expectEqual("\"test\\A line\"", result);
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\\back");
try testing.expectEqual("\"test\\\\back\"", result);
}
test "CSSValueAnalyzer: CSSKeywords.isKnownKeyword - case sensitivity" {
try testing.expect(CSSKeywords.isKnownKeyword("red"));
try testing.expect(CSSKeywords.isKnownKeyword("solid"));
try testing.expect(CSSKeywords.isKnownKeyword("center"));
try testing.expect(CSSKeywords.isKnownKeyword("inherit"));
try testing.expect(CSSKeywords.isKnownKeyword("RED"));
try testing.expect(CSSKeywords.isKnownKeyword("Red"));
try testing.expect(CSSKeywords.isKnownKeyword("SOLID"));
try testing.expect(CSSKeywords.isKnownKeyword("Center"));
try testing.expect(!CSSKeywords.isKnownKeyword("invalid"));
try testing.expect(!CSSKeywords.isKnownKeyword("unknown"));
try testing.expect(!CSSKeywords.isKnownKeyword(""));
}
test "CSSValueAnalyzer: CSSKeywords.containsSpecialChar - various special characters" {
try testing.expect(CSSKeywords.containsSpecialChar("test\"quote"));
try testing.expect(CSSKeywords.containsSpecialChar("test'quote"));
try testing.expect(CSSKeywords.containsSpecialChar("test;end"));
try testing.expect(CSSKeywords.containsSpecialChar("test{brace"));
try testing.expect(CSSKeywords.containsSpecialChar("test}brace"));
try testing.expect(CSSKeywords.containsSpecialChar("test\\back"));
try testing.expect(CSSKeywords.containsSpecialChar("test<angle"));
try testing.expect(CSSKeywords.containsSpecialChar("test>angle"));
try testing.expect(CSSKeywords.containsSpecialChar("test/slash"));
try testing.expect(!CSSKeywords.containsSpecialChar("normal-text"));
try testing.expect(!CSSKeywords.containsSpecialChar("text123"));
try testing.expect(!CSSKeywords.containsSpecialChar(""));
}
test "CSSValueAnalyzer: CSSKeywords.isValidUnit - various units" {
try testing.expect(CSSKeywords.isValidUnit("px"));
try testing.expect(CSSKeywords.isValidUnit("em"));
try testing.expect(CSSKeywords.isValidUnit("rem"));
try testing.expect(CSSKeywords.isValidUnit("%"));
try testing.expect(CSSKeywords.isValidUnit("deg"));
try testing.expect(CSSKeywords.isValidUnit("rad"));
try testing.expect(CSSKeywords.isValidUnit("s"));
try testing.expect(CSSKeywords.isValidUnit("ms"));
try testing.expect(CSSKeywords.isValidUnit("PX"));
try testing.expect(!CSSKeywords.isValidUnit("invalid"));
try testing.expect(!CSSKeywords.isValidUnit(""));
}
test "CSSValueAnalyzer: CSSKeywords.startsWithFunction - function detection" {
try testing.expect(CSSKeywords.startsWithFunction("rgb(255, 0, 0)"));
try testing.expect(CSSKeywords.startsWithFunction("rgba(255, 0, 0, 0.5)"));
try testing.expect(CSSKeywords.startsWithFunction("url(image.png)"));
try testing.expect(CSSKeywords.startsWithFunction("calc(100% - 20px)"));
try testing.expect(CSSKeywords.startsWithFunction("var(--custom-property)"));
try testing.expect(CSSKeywords.startsWithFunction("linear-gradient(to right, red, blue)"));
try testing.expect(CSSKeywords.startsWithFunction("custom-function(args)"));
try testing.expect(CSSKeywords.startsWithFunction("unknown(test)"));
try testing.expect(!CSSKeywords.startsWithFunction("not-a-function"));
try testing.expect(!CSSKeywords.startsWithFunction("missing-paren)"));
try testing.expect(!CSSKeywords.startsWithFunction("missing-close("));
try testing.expect(!CSSKeywords.startsWithFunction(""));
try testing.expect(!CSSKeywords.startsWithFunction("rgb"));
}
test "CSSValueAnalyzer: isNumericWithUnit - whitespace handling" {
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(" 10px"));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10 px"));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10px "));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(" 10 px "));
}
test "CSSValueAnalyzer: extractImportant - whitespace edge cases" {
var result = CSSValueAnalyzer.extractImportant(" ");
try testing.expect(!result.is_important);
try testing.expectEqual("", result.value);
result = CSSValueAnalyzer.extractImportant("\t\n\r !important\t\n");
try testing.expect(result.is_important);
try testing.expectEqual("", result.value);
result = CSSValueAnalyzer.extractImportant("red\t!important");
try testing.expect(result.is_important);
try testing.expectEqual("red", result.value);
}
test "CSSValueAnalyzer: isHexColor - mixed case handling" {
try testing.expect(CSSValueAnalyzer.isHexColor("#AbC"));
try testing.expect(CSSValueAnalyzer.isHexColor("#123aBc"));
try testing.expect(CSSValueAnalyzer.isHexColor("#FFffFF"));
try testing.expect(CSSValueAnalyzer.isHexColor("#000FFF"));
}
test "CSSValueAnalyzer: edge case - very long inputs" {
const long_valid = "a" ** 1000 ++ "px";
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(long_valid)); // not numeric
const long_property = "a-" ** 100 ++ "property";
try testing.expect(CSSValueAnalyzer.isValidPropertyName(long_property));
const long_hex = "#" ++ "a" ** 20;
try testing.expect(!CSSValueAnalyzer.isHexColor(long_hex));
}
test "CSSValueAnalyzer: boundary conditions - numeric parsing" {
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0px"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.0px"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit(".0px"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.px"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("999999999px"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1.7976931348623157e+308px"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.000000001px"));
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e-100px"));
}
test "CSSValueAnalyzer: extractImportant - malformed important declarations" {
var result = CSSValueAnalyzer.extractImportant("red ! important");
try testing.expect(!result.is_important);
try testing.expectEqual("red ! important", result.value);
result = CSSValueAnalyzer.extractImportant("red !Important");
try testing.expect(!result.is_important);
try testing.expectEqual("red !Important", result.value);
result = CSSValueAnalyzer.extractImportant("red !IMPORTANT");
try testing.expect(!result.is_important);
try testing.expectEqual("red !IMPORTANT", result.value);
result = CSSValueAnalyzer.extractImportant("!importantred");
try testing.expect(!result.is_important);
try testing.expectEqual("!importantred", result.value);
result = CSSValueAnalyzer.extractImportant("red !important !important");
try testing.expect(result.is_important);
try testing.expectEqual("red !important", result.value);
}
test "CSSValueAnalyzer: isMultiValueProperty - complex spacing scenarios" {
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px"));
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("solid red"));
try testing.expect(CSSValueAnalyzer.isMultiValueProperty(" 10px 20px "));
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px\t20px"));
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px\n20px"));
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px 30px"));
}
test "CSSValueAnalyzer: isAlreadyQuoted - edge cases with quotes" {
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"'hello'\""));
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'\"hello\"'"));
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"hello\\\"world\""));
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'hello\\'world'"));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello"));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello\""));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'hello"));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello'"));
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"a\""));
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'b'"));
}
test "CSSValueAnalyzer: needsQuotes - function and URL edge cases" {
try testing.expect(!CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0)"));
try testing.expect(!CSSValueAnalyzer.needsQuotes("calc(100% - 20px)"));
try testing.expect(!CSSValueAnalyzer.needsQuotes("url(path with spaces.jpg)"));
try testing.expect(!CSSValueAnalyzer.needsQuotes("linear-gradient(to right, red, blue)"));
try testing.expect(CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0"));
}
test "CSSValueAnalyzer: escapeCSSValue - control characters and Unicode" {
const allocator = testing.arena_allocator;
var result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\ttab");
try testing.expectEqual("\"test\\9 tab\"", result);
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\rreturn");
try testing.expectEqual("\"test\\D return\"", result);
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\x00null");
try testing.expectEqual("\"test\\0null\"", result);
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\x7Fdel");
try testing.expectEqual("\"test\\7f del\"", result);
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\"quote\nline\\back");
try testing.expectEqual("\"test\\\"quote\\A line\\\\back\"", result);
}
test "CSSValueAnalyzer: isValidPropertyName - CSS custom properties and vendor prefixes" {
try testing.expect(CSSValueAnalyzer.isValidPropertyName("--custom-color"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("--my-variable"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("--123"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-webkit-transform"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-moz-border-radius"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-ms-filter"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-o-transition"));
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("-123invalid"));
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("--"));
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("-"));
}
test "CSSValueAnalyzer: startsWithFunction - case sensitivity and partial matches" {
try testing.expect(CSSKeywords.startsWithFunction("RGB(255, 0, 0)"));
try testing.expect(CSSKeywords.startsWithFunction("Rgb(255, 0, 0)"));
try testing.expect(CSSKeywords.startsWithFunction("URL(image.png)"));
try testing.expect(CSSKeywords.startsWithFunction("rg(something)"));
try testing.expect(CSSKeywords.startsWithFunction("ur(something)"));
try testing.expect(CSSKeywords.startsWithFunction("rgb(1,2,3)"));
try testing.expect(CSSKeywords.startsWithFunction("rgba(1,2,3,4)"));
try testing.expect(CSSKeywords.startsWithFunction("my-custom-function(args)"));
try testing.expect(CSSKeywords.startsWithFunction("function-with-dashes(test)"));
try testing.expect(!CSSKeywords.startsWithFunction("123function(test)"));
}
test "CSSValueAnalyzer: isHexColor - Unicode and invalid characters" {
try testing.expect(!CSSValueAnalyzer.isHexColor("#ghijkl"));
try testing.expect(!CSSValueAnalyzer.isHexColor("#12345g"));
try testing.expect(!CSSValueAnalyzer.isHexColor("#xyz"));
try testing.expect(!CSSValueAnalyzer.isHexColor("#АВС"));
try testing.expect(!CSSValueAnalyzer.isHexColor("#1234567g"));
try testing.expect(!CSSValueAnalyzer.isHexColor("#g2345678"));
}
test "CSSValueAnalyzer: complex integration scenarios" {
const allocator = testing.arena_allocator;
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("rgb(255,0,0) url(bg.jpg)"));
try testing.expect(!CSSValueAnalyzer.needsQuotes("calc(100% - 20px)"));
const result = try CSSValueAnalyzer.escapeCSSValue(allocator, "fake(function with spaces");
try testing.expectEqual("\"fake(function with spaces\"", result);
const important_result = CSSValueAnalyzer.extractImportant("rgb(255,0,0) !important");
try testing.expect(important_result.is_important);
try testing.expectEqual("rgb(255,0,0)", important_result.value);
}
test "CSSValueAnalyzer: performance edge cases - empty and minimal inputs" {
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(""));
try testing.expect(!CSSValueAnalyzer.isHexColor(""));
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(""));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted(""));
try testing.expect(!CSSValueAnalyzer.isValidPropertyName(""));
try testing.expect(CSSValueAnalyzer.needsQuotes(""));
try testing.expect(!CSSKeywords.isKnownKeyword(""));
try testing.expect(!CSSKeywords.containsSpecialChar(""));
try testing.expect(!CSSKeywords.isValidUnit(""));
try testing.expect(!CSSKeywords.startsWithFunction(""));
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("a"));
try testing.expect(!CSSValueAnalyzer.isHexColor("a"));
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("a"));
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("a"));
try testing.expect(CSSValueAnalyzer.isValidPropertyName("a"));
try testing.expect(!CSSValueAnalyzer.needsQuotes("a"));
}

View File

@@ -16,15 +16,10 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub const Stylesheet = @import("stylesheet.zig").StyleSheet;
pub const CSSStylesheet = @import("css_stylesheet.zig").CSSStyleSheet;
pub const CSSStyleDeclaration = @import("css_style_declaration.zig").CSSStyleDeclaration;
pub const CSSRuleList = @import("css_rule_list.zig").CSSRuleList;
pub const Interfaces = .{
Stylesheet,
CSSStylesheet,
CSSStyleDeclaration,
CSSRuleList,
@import("css_rule.zig").Interfaces,
@import("StyleSheet.zig"),
@import("CSSStyleSheet.zig"),
@import("CSSStyleDeclaration.zig"),
@import("CSSRuleList.zig"),
@import("CSSRule.zig").Interfaces,
};

View File

@@ -1,79 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
// Represents https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data
pub const DataURI = struct {
was_base64_encoded: bool,
// The contents in the uri. It will be base64 decoded but not prepared in
// any way for mime.charset.
data: []const u8,
// Parses data:[<media-type>][;base64],<data>
pub fn parse(allocator: Allocator, src: []const u8) !?DataURI {
if (!std.mem.startsWith(u8, src, "data:")) {
return null;
}
const uri = src[5..];
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
// Extract the encoding.
var metadata = uri[0..data_starts];
var base64_encoded = false;
if (std.mem.endsWith(u8, metadata, ";base64")) {
base64_encoded = true;
metadata = metadata[0 .. metadata.len - 7];
}
// TODO: Extract mime type. This not trivial because Mime.parse requires
// a []u8 and might mutate the src. And, the DataURI.parse references atm
// do not have deinit calls.
// Prepare the data.
var data = uri[data_starts + 1 ..];
if (base64_encoded) {
const decoder = std.base64.standard.Decoder;
const decoded_size = try decoder.calcSizeForSlice(data);
const buffer = try allocator.alloc(u8, decoded_size);
errdefer allocator.free(buffer);
try decoder.decode(buffer, data);
data = buffer;
}
return .{
.was_base64_encoded = base64_encoded,
.data = data,
};
}
pub fn deinit(self: *const DataURI, allocator: Allocator) void {
if (self.was_base64_encoded) {
allocator.free(self.data);
}
}
};
const testing = std.testing;
test "DataURI: parse valid" {
try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo");
try test_valid("data:text/javascript; charset=utf-8;,foo", "foo");
try test_valid("data:,foo", "foo");
}
test "DataURI: parse invalid" {
try test_cannot_parse("atad:,foo");
try test_cannot_parse("data:foo");
try test_cannot_parse("data:");
}
fn test_valid(uri: []const u8, expected: []const u8) !void {
const data_uri = try DataURI.parse(std.testing.allocator, uri) orelse return error.TestFailed;
defer data_uri.deinit(testing.allocator);
try testing.expectEqualStrings(expected, data_uri.data);
}
fn test_cannot_parse(uri: []const u8) !void {
try testing.expectEqual(null, DataURI.parse(std.testing.allocator, uri));
}

View File

@@ -0,0 +1,109 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Page = @import("../page.zig").Page;
const JsObject = @import("../env.zig").JsObject;
const Promise = @import("../env.zig").Promise;
const PromiseResolver = @import("../env.zig").PromiseResolver;
const Animation = @This();
effect: ?JsObject,
timeline: ?JsObject,
ready_resolver: ?PromiseResolver,
finished_resolver: ?PromiseResolver,
pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !Animation {
return .{
.effect = if (effect) |eo| try eo.persist() else null,
.timeline = if (timeline) |to| try to.persist() else null,
.ready_resolver = null,
.finished_resolver = null,
};
}
pub fn get_playState(self: *const Animation) []const u8 {
_ = self;
return "finished";
}
pub fn get_pending(self: *const Animation) bool {
_ = self;
return false;
}
pub fn get_finished(self: *Animation, page: *Page) !Promise {
if (self.finished_resolver == null) {
const resolver = page.main_context.createPromiseResolver();
try resolver.resolve(self);
self.finished_resolver = resolver;
}
return self.finished_resolver.?.promise();
}
pub fn get_ready(self: *Animation, page: *Page) !Promise {
// never resolved, because we're always "finished"
if (self.ready_resolver == null) {
const resolver = page.main_context.createPromiseResolver();
self.ready_resolver = resolver;
}
return self.ready_resolver.?.promise();
}
pub fn get_effect(self: *const Animation) ?JsObject {
return self.effect;
}
pub fn set_effect(self: *Animation, effect: JsObject) !void {
self.effect = try effect.persist();
}
pub fn get_timeline(self: *const Animation) ?JsObject {
return self.timeline;
}
pub fn set_timeline(self: *Animation, timeline: JsObject) !void {
self.timeline = try timeline.persist();
}
pub fn _play(self: *const Animation) void {
_ = self;
}
pub fn _pause(self: *const Animation) void {
_ = self;
}
pub fn _cancel(self: *const Animation) void {
_ = self;
}
pub fn _finish(self: *const Animation) void {
_ = self;
}
pub fn _reverse(self: *const Animation) void {
_ = self;
}
const testing = @import("../../testing.zig");
test "Browser: DOM.Animation" {
try testing.htmlRunner("dom/animation.html");
}

View File

@@ -0,0 +1,291 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler;
const JsObject = Env.JsObject;
const Function = Env.Function;
const Allocator = std.mem.Allocator;
const MAX_QUEUE_SIZE = 10;
pub const Interfaces = .{ MessageChannel, MessagePort };
const MessageChannel = @This();
port1: *MessagePort,
port2: *MessagePort,
pub fn constructor(page: *Page) !MessageChannel {
// Why do we allocate this rather than storing directly in the struct?
// https://github.com/lightpanda-io/project/discussions/165
const port1 = try page.arena.create(MessagePort);
const port2 = try page.arena.create(MessagePort);
port1.* = .{
.pair = port2,
};
port2.* = .{
.pair = port1,
};
return .{
.port1 = port1,
.port2 = port2,
};
}
pub fn get_port1(self: *const MessageChannel) *MessagePort {
return self.port1;
}
pub fn get_port2(self: *const MessageChannel) *MessagePort {
return self.port2;
}
pub const MessagePort = struct {
pub const prototype = *EventTarget;
proto: parser.EventTargetTBase = .{ .internal_target_type = .message_port },
pair: *MessagePort,
closed: bool = false,
started: bool = false,
onmessage_cbk: ?Function = null,
onmessageerror_cbk: ?Function = null,
// This is the queue of messages to dispatch to THIS MessagePort when the
// MessagePort is started.
queue: std.ArrayListUnmanaged(JsObject) = .empty,
pub const PostMessageOption = union(enum) {
transfer: JsObject,
options: Opts,
pub const Opts = struct {
transfer: JsObject,
};
};
pub fn _postMessage(self: *MessagePort, obj: JsObject, opts_: ?PostMessageOption, page: *Page) !void {
if (self.closed) {
return;
}
if (opts_ != null) {
log.warn(.web_api, "not implemented", .{ .feature = "MessagePort postMessage options" });
return error.NotImplemented;
}
try self.pair.dispatchOrQueue(obj, page.arena);
}
// Start impacts the ability to receive a message.
// Given pair1 (started) and pair2 (not started), then:
// pair2.postMessage('x'); //will be dispatched to pair1.onmessage
// pair1.postMessage('x'); // will be queued until pair2 is started
pub fn _start(self: *MessagePort) !void {
if (self.started) {
return;
}
self.started = true;
for (self.queue.items) |data| {
try self.dispatch(data);
}
// we'll never use this queue again, but it's allocated with an arena
// we don't even need to clear it, but it seems a bit safer to do at
// least that
self.queue.clearRetainingCapacity();
}
// Closing seems to stop both the publishing and receiving of messages,
// effectively rendering the channel useless. It cannot be reversed.
pub fn _close(self: *MessagePort) void {
self.closed = true;
self.pair.closed = true;
}
pub fn get_onmessage(self: *MessagePort) ?Function {
return self.onmessage_cbk;
}
pub fn get_onmessageerror(self: *MessagePort) ?Function {
return self.onmessageerror_cbk;
}
pub fn set_onmessage(self: *MessagePort, listener: EventHandler.Listener, page: *Page) !void {
if (self.onmessage_cbk) |cbk| {
try self.unregister("message", cbk.id);
}
self.onmessage_cbk = try self.register(page.arena, "message", listener);
// When onmessage is set directly, then it's like start() was called.
// If addEventListener('message') is used, the app has to call start()
// explicitly.
try self._start();
}
pub fn set_onmessageerror(self: *MessagePort, listener: EventHandler.Listener, page: *Page) !void {
if (self.onmessageerror_cbk) |cbk| {
try self.unregister("messageerror", cbk.id);
}
self.onmessageerror_cbk = try self.register(page.arena, "messageerror", listener);
}
// called from our pair. If port1.postMessage("x") is called, then this
// will be called on port2.
fn dispatchOrQueue(self: *MessagePort, obj: JsObject, arena: Allocator) !void {
// our pair should have checked this already
std.debug.assert(self.closed == false);
if (self.started) {
return self.dispatch(try obj.persist());
}
if (self.queue.items.len > MAX_QUEUE_SIZE) {
// This isn't part of the spec, but not putting a limit is reckless
return error.MessageQueueLimit;
}
return self.queue.append(arena, try obj.persist());
}
fn dispatch(self: *MessagePort, obj: JsObject) !void {
// obj is already persisted, don't use `MessageEvent.constructor`, but
// go directly to `init`, which assumes persisted objects.
var evt = try MessageEvent.init(.{ .data = obj });
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(MessagePort, self),
@as(*parser.Event, @ptrCast(&evt)),
);
}
fn register(
self: *MessagePort,
alloc: Allocator,
typ: []const u8,
listener: EventHandler.Listener,
) !?Function {
const target = @as(*parser.EventTarget, @ptrCast(self));
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
return eh.callback;
}
fn unregister(self: *MessagePort, typ: []const u8, cbk_id: usize) !void {
const et = @as(*parser.EventTarget, @ptrCast(self));
const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id);
if (lst == null) {
return;
}
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
}
};
pub const MessageEvent = struct {
const Event = @import("../events/event.zig").Event;
const DOMException = @import("exceptions.zig").DOMException;
pub const prototype = *Event;
pub const Exception = DOMException;
pub const union_make_copy = true;
proto: parser.Event,
data: ?JsObject,
// You would think if port1 sends to port2, the source would be port2
// (which is how I read the documentation), but it appears to always be
// null. It can always be set explicitly via the constructor;
source: ?JsObject,
origin: []const u8,
// This is used for Server-Sent events. Appears to always be an empty
// string for MessagePort messages.
last_event_id: []const u8,
// This might be related to the "transfer" option of postMessage which
// we don't yet support. For "normal" message, it's always an empty array.
// Though it could be set explicitly via the constructor
ports: []*MessagePort,
const Options = struct {
data: ?JsObject = null,
source: ?JsObject = null,
origin: []const u8 = "",
lastEventId: []const u8 = "",
ports: []*MessagePort = &.{},
};
pub fn constructor(opts: Options) !MessageEvent {
return init(.{
.data = if (opts.data) |obj| try obj.persist() else null,
.source = if (opts.source) |obj| try obj.persist() else null,
.ports = opts.ports,
.origin = opts.origin,
.lastEventId = opts.lastEventId,
});
}
// This is like "constructor", but it assumes JsObjects have already been
// persisted. Necessary because this `new MessageEvent()` can be called
// directly from JS OR from a port.postMessage. In the latter case, data
// may have already been persisted (as it might need to be queued);
fn init(opts: Options) !MessageEvent {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, "message", .{});
try parser.eventSetInternalType(event, .message_event);
return .{
.proto = event.*,
.data = opts.data,
.source = opts.source,
.ports = opts.ports,
.origin = opts.origin,
.last_event_id = opts.lastEventId,
};
}
pub fn get_data(self: *const MessageEvent) !?JsObject {
return self.data;
}
pub fn get_origin(self: *const MessageEvent) []const u8 {
return self.origin;
}
pub fn get_source(self: *const MessageEvent) ?JsObject {
return self.source;
}
pub fn get_ports(self: *const MessageEvent) []*MessagePort {
return self.ports;
}
pub fn get_lastEventId(self: *const MessageEvent) []const u8 {
return self.last_event_id;
}
};
const testing = @import("../../testing.zig");
test "Browser: DOM.MessageChannel" {
try testing.htmlRunner("dom/message_channel.html");
}

View File

@@ -15,7 +15,6 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const parser = @import("../netsurf.zig");
const Node = @import("node.zig").Node;
@@ -47,7 +46,14 @@ pub const Attr = struct {
}
pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 {
try parser.attributeSetValue(self, v);
if (try parser.attributeGetOwnerElement(self)) |el| {
// if possible, go through the element, as that triggers a
// DOMAttrModified event (which MutationObserver cares about)
const name = try parser.attributeGetName(self);
try parser.elementSetAttribute(el, name, v);
} else {
try parser.attributeSetValue(self, v);
}
return v;
}
@@ -64,32 +70,6 @@ pub const Attr = struct {
// -----
const testing = @import("../../testing.zig");
test "Browser.DOM.Attribute" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let a = document.createAttributeNS('foo', 'bar')", "undefined" },
.{ "a.namespaceURI", "foo" },
.{ "a.prefix", "null" },
.{ "a.localName", "bar" },
.{ "a.name", "bar" },
.{ "a.value", "" },
// TODO: libdom has a bug here: the created attr has no parent, it
// causes a panic w/ libdom when setting the value.
//.{ "a.value = 'nok'", "nok" },
.{ "a.ownerElement", "null" },
}, .{});
try runner.testCases(&.{
.{ "let b = document.getElementById('link').getAttributeNode('class')", "undefined" },
.{ "b.name", "class" },
.{ "b.value", "ok" },
.{ "b.value = 'nok'", "nok" },
.{ "b.value", "nok" },
.{ "b.value = null", "null" },
.{ "b.value", "null" },
.{ "b.value = 'ok'", "ok" },
.{ "b.ownerElement.id", "link" },
}, .{});
test "Browser: DOM.Attribute" {
try testing.htmlRunner("dom/attribute.html");
}

View File

@@ -24,7 +24,8 @@ const Node = @import("node.zig").Node;
const Comment = @import("comment.zig").Comment;
const Text = @import("text.zig");
const ProcessingInstruction = @import("processing_instruction.zig").ProcessingInstruction;
const HTMLElem = @import("../html/elements.zig");
const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union;
// CharacterData interfaces
pub const Interfaces = .{
@@ -49,20 +50,20 @@ pub const CharacterData = struct {
return try parser.characterDataLength(self);
}
pub fn get_nextElementSibling(self: *parser.CharacterData) !?HTMLElem.Union {
pub fn get_nextElementSibling(self: *parser.CharacterData) !?ElementUnion {
const res = try parser.nodeNextElementSibling(parser.characterDataToNode(self));
if (res == null) {
return null;
}
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
return try Element.toInterface(res.?);
}
pub fn get_previousElementSibling(self: *parser.CharacterData) !?HTMLElem.Union {
pub fn get_previousElementSibling(self: *parser.CharacterData) !?ElementUnion {
const res = try parser.nodePreviousElementSibling(parser.characterDataToNode(self));
if (res == null) {
return null;
}
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
return try Element.toInterface(res.?);
}
// Read/Write attributes
@@ -101,7 +102,7 @@ pub const CharacterData = struct {
// netsurf's CharacterData (text, comment) doesn't implement the
// dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode.
pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) !bool {
if (try parser.nodeType(@alignCast(@ptrCast(self))) != try parser.nodeType(other_node)) {
if (try parser.nodeType(@ptrCast(@alignCast(self))) != try parser.nodeType(other_node)) {
return false;
}
@@ -128,69 +129,6 @@ pub const CharacterData = struct {
// -----
const testing = @import("../../testing.zig");
test "Browser.DOM.CharacterData" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let link = document.getElementById('link')", "undefined" },
.{ "let cdata = link.firstChild", "undefined" },
.{ "cdata.data", "OK" },
}, .{});
try runner.testCases(&.{
.{ "cdata.data = 'OK modified'", "OK modified" },
.{ "cdata.data === 'OK modified'", "true" },
.{ "cdata.data = 'OK'", "OK" },
}, .{});
try runner.testCases(&.{
.{ "cdata.length === 2", "true" },
}, .{});
try runner.testCases(&.{
.{ "cdata.nextElementSibling === null", "true" },
// create a next element
.{ "let next = document.createElement('a')", "undefined" },
.{ "link.appendChild(next, cdata) !== undefined", "true" },
.{ "cdata.nextElementSibling.localName === 'a' ", "true" },
}, .{});
try runner.testCases(&.{
.{ "cdata.previousElementSibling === null", "true" },
// create a prev element
.{ "let prev = document.createElement('div')", "undefined" },
.{ "link.insertBefore(prev, cdata) !== undefined", "true" },
.{ "cdata.previousElementSibling.localName === 'div' ", "true" },
}, .{});
try runner.testCases(&.{
.{ "cdata.appendData(' modified')", "undefined" },
.{ "cdata.data === 'OK modified' ", "true" },
}, .{});
try runner.testCases(&.{
.{ "cdata.deleteData('OK'.length, ' modified'.length)", "undefined" },
.{ "cdata.data == 'OK'", "true" },
}, .{});
try runner.testCases(&.{
.{ "cdata.insertData('OK'.length-1, 'modified')", "undefined" },
.{ "cdata.data == 'OmodifiedK'", "true" },
}, .{});
try runner.testCases(&.{
.{ "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", "undefined" },
.{ "cdata.data == 'OreplacedK'", "true" },
}, .{});
try runner.testCases(&.{
.{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
.{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
}, .{});
try runner.testCases(&.{
.{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
.{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
}, .{});
test "Browser: DOM.CharacterData" {
try testing.htmlRunner("dom/character_data.html");
}

View File

@@ -40,15 +40,6 @@ pub const Comment = struct {
// -----
const testing = @import("../../testing.zig");
test "Browser.DOM.Comment" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let comment = new Comment('foo')", "undefined" },
.{ "comment.data", "foo" },
.{ "let emptycomment = new Comment()", "undefined" },
.{ "emptycomment.data", "" },
}, .{});
test "Browser: DOM.Comment" {
try testing.htmlRunner("dom/comment.html");
}

View File

@@ -38,7 +38,7 @@ pub fn querySelector(alloc: std.mem.Allocator, n: *parser.Node, selector: []cons
var m = MatchFirst{};
_ = try css.matchFirst(ps, Node{ .node = n }, &m);
_ = try css.matchFirst(&ps, Node{ .node = n }, &m);
return m.n;
}
@@ -75,6 +75,6 @@ pub fn querySelectorAll(alloc: std.mem.Allocator, n: *parser.Node, selector: []c
var m = MatchAll.init(alloc);
defer m.deinit();
try css.matchAll(ps, Node{ .node = n }, &m);
try css.matchAll(&ps, Node{ .node = n }, &m);
return m.toOwnedList();
}

View File

@@ -32,6 +32,11 @@ const css = @import("css.zig");
const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union;
const TreeWalker = @import("tree_walker.zig").TreeWalker;
const CSSStyleSheet = @import("../cssom/CSSStyleSheet.zig");
const NodeIterator = @import("node_iterator.zig").NodeIterator;
const Range = @import("range.zig").Range;
const CustomEvent = @import("../events/custom_event.zig").CustomEvent;
const Env = @import("../env.zig").Env;
@@ -107,13 +112,21 @@ pub const Document = struct {
return try parser.documentGetDoctype(self);
}
pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !*parser.Event {
// TODO: for now only "Event" constructor is supported
// see table on https://dom.spec.whatwg.org/#dom-document-createevent $2
pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !union(enum) {
base: *parser.Event,
custom: CustomEvent,
} {
if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) {
return try parser.eventCreate();
return .{ .base = try parser.eventCreate() };
}
return parser.DOMError.NotSupported;
// Not documented in MDN but supported in Chrome.
// This is actually both instance of `Event` and `CustomEvent`.
if (std.ascii.eqlIgnoreCase(eventCstr, "CustomEvent")) {
return .{ .custom = try CustomEvent.constructor(eventCstr, null) };
}
return error.NotSupported;
}
pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion {
@@ -121,28 +134,11 @@ pub const Document = struct {
return try Element.toInterface(e);
}
const CreateElementResult = union(enum) {
element: ElementUnion,
custom: Env.JsObject,
};
pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !CreateElementResult {
const custom_element = page.window.custom_elements._get(tag_name) orelse {
const e = try parser.documentCreateElement(self, tag_name);
return .{ .element = try Element.toInterface(e) };
};
var result: Env.Function.Result = undefined;
const js_obj = custom_element.newInstance(&result) catch |err| {
log.fatal(.user_script, "newInstance error", .{
.err = result.exception,
.stack = result.stack,
.tag_name = tag_name,
.source = "createElement",
});
return err;
};
return .{ .custom = js_obj };
pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
// The elements namespace is the HTML namespace when document is an HTML document
// https://dom.spec.whatwg.org/#ref-for-dom-document-createelement%E2%91%A0
const e = try parser.documentCreateElementNS(self, "http://www.w3.org/1999/xhtml", tag_name);
return Element.toInterface(e);
}
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {
@@ -163,7 +159,9 @@ pub const Document = struct {
tag_name: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, true);
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, .{
.include_root = true,
});
}
pub fn _getElementsByClassName(
@@ -171,7 +169,9 @@ pub const Document = struct {
classNames: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, true);
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, .{
.include_root = true,
});
}
pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment {
@@ -215,7 +215,9 @@ pub const Document = struct {
// ParentNode
// https://dom.spec.whatwg.org/#parentnode
pub fn get_children(self: *parser.Document) !collection.HTMLCollection {
return try collection.HTMLCollectionChildren(parser.documentToNode(self), false);
return collection.HTMLCollectionChildren(parser.documentToNode(self), .{
.include_root = false,
});
}
pub fn get_firstElementChild(self: *parser.Document) !?ElementUnion {
@@ -236,7 +238,7 @@ pub const Document = struct {
pub fn _querySelector(self: *parser.Document, selector: []const u8, page: *Page) !?ElementUnion {
if (selector.len == 0) return null;
const n = try css.querySelector(page.arena, parser.documentToNode(self), selector);
const n = try css.querySelector(page.call_arena, parser.documentToNode(self), selector);
if (n == null) return null;
@@ -259,19 +261,23 @@ pub const Document = struct {
return Node.replaceChildren(parser.documentToNode(self), nodes);
}
pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?TreeWalker.TreeWalkerOpts) !TreeWalker {
return try TreeWalker.init(root, what_to_show, filter);
pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?TreeWalker.WhatToShow, filter: ?TreeWalker.TreeWalkerOpts) !TreeWalker {
return TreeWalker.init(root, what_to_show, filter);
}
pub fn _createNodeIterator(_: *parser.Document, root: *parser.Node, what_to_show: ?NodeIterator.WhatToShow, filter: ?NodeIterator.NodeIteratorOpts) !NodeIterator {
return NodeIterator.init(root, what_to_show, filter);
}
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| {
if (page.getNodeState(@ptrCast(@alignCast(self)))) |state| {
if (state.active_element) |ae| {
return ae;
}
}
if (try parser.documentHTMLBody(page.window.document)) |body| {
return @alignCast(@ptrCast(body));
return @ptrCast(@alignCast(body));
}
return try parser.documentGetDocumentElement(self);
@@ -287,206 +293,37 @@ pub const Document = struct {
// we could look for the "disabled" attribute, but that's only meaningful
// on certain types, and libdom's vtable doesn't seem to expose this.
pub fn setFocus(self: *parser.Document, e: *parser.ElementHTML, page: *Page) !void {
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.active_element = @ptrCast(e);
}
pub fn _createRange(_: *parser.Document, page: *Page) Range {
return Range.constructor(page);
}
// TODO: dummy implementation
pub fn get_styleSheets(_: *parser.Document) []CSSStyleSheet {
return &.{};
}
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !Env.JsObject {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
if (state.adopted_style_sheets) |obj| {
return obj;
}
const obj = try page.main_context.newArray(0).persist();
state.adopted_style_sheets = obj;
return obj;
}
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: Env.JsObject, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.adopted_style_sheets = try sheets.persist();
}
};
const testing = @import("../../testing.zig");
test "Browser.DOM.Document" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{
.url = "about:blank",
});
defer runner.deinit();
try runner.testCases(&.{
.{ "document.__proto__.__proto__.constructor.name", "Document" },
.{ "document.__proto__.__proto__.__proto__.constructor.name", "Node" },
.{ "document.__proto__.__proto__.__proto__.__proto__.constructor.name", "EventTarget" },
.{ "let newdoc = new Document()", "undefined" },
.{ "newdoc.documentElement", "null" },
.{ "newdoc.children.length", "0" },
.{ "newdoc.getElementsByTagName('*').length", "0" },
.{ "newdoc.getElementsByTagName('*').item(0)", "null" },
.{ "newdoc.inputEncoding === document.inputEncoding", "true" },
.{ "newdoc.documentURI === document.documentURI", "true" },
.{ "newdoc.URL === document.URL", "true" },
.{ "newdoc.compatMode === document.compatMode", "true" },
.{ "newdoc.characterSet === document.characterSet", "true" },
.{ "newdoc.charset === document.charset", "true" },
.{ "newdoc.contentType === document.contentType", "true" },
}, .{});
try runner.testCases(&.{
.{ "let getElementById = document.getElementById('content')", "undefined" },
.{ "getElementById.constructor.name", "HTMLDivElement" },
.{ "getElementById.localName", "div" },
}, .{});
try runner.testCases(&.{
.{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
.{ "getElementsByTagName.length", "2" },
.{ "getElementsByTagName.item(0).localName", "p" },
.{ "getElementsByTagName.item(1).localName", "p" },
.{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
.{ "getElementsByTagNameAll.length", "8" },
.{ "getElementsByTagNameAll.item(0).localName", "html" },
.{ "getElementsByTagNameAll.item(7).localName", "p" },
.{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
}, .{});
try runner.testCases(&.{
.{ "let ok = document.getElementsByClassName('ok')", "undefined" },
.{ "ok.length", "2" },
.{ "let empty = document.getElementsByClassName('empty')", "undefined" },
.{ "empty.length", "1" },
.{ "let emptyok = document.getElementsByClassName('empty ok')", "undefined" },
.{ "emptyok.length", "1" },
}, .{});
try runner.testCases(&.{
.{ "let e = document.documentElement", "undefined" },
.{ "e.localName", "html" },
}, .{});
try runner.testCases(&.{
.{ "document.characterSet", "UTF-8" },
.{ "document.charset", "UTF-8" },
.{ "document.inputEncoding", "UTF-8" },
}, .{});
try runner.testCases(&.{
.{ "document.compatMode", "CSS1Compat" },
}, .{});
try runner.testCases(&.{
.{ "document.contentType", "text/html" },
}, .{});
try runner.testCases(&.{
.{ "document.documentURI", "about:blank" },
.{ "document.URL", "about:blank" },
}, .{});
try runner.testCases(&.{
.{ "let impl = document.implementation", "undefined" },
}, .{});
try runner.testCases(&.{
.{ "let d = new Document()", "undefined" },
.{ "d.characterSet", "UTF-8" },
.{ "d.URL", "about:blank" },
.{ "d.documentURI", "about:blank" },
.{ "d.compatMode", "CSS1Compat" },
.{ "d.contentType", "text/html" },
}, .{});
try runner.testCases(&.{
.{ "var v = document.createDocumentFragment()", "undefined" },
.{ "v.nodeName", "#document-fragment" },
}, .{});
try runner.testCases(&.{
.{ "var v = document.createTextNode('foo')", "undefined" },
.{ "v.nodeName", "#text" },
}, .{});
try runner.testCases(&.{
.{ "var v = document.createCDATASection('foo')", "undefined" },
.{ "v.nodeName", "#cdata-section" },
}, .{});
try runner.testCases(&.{
.{ "var v = document.createComment('foo')", "undefined" },
.{ "v.nodeName", "#comment" },
.{ "let v2 = v.cloneNode()", "undefined" },
}, .{});
try runner.testCases(&.{
.{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" },
.{ "pi.target", "foo" },
.{ "let pi2 = pi.cloneNode()", "undefined" },
}, .{});
try runner.testCases(&.{
.{ "let nimp = document.getElementById('content')", "undefined" },
.{ "var v = document.importNode(nimp)", "undefined" },
.{ "v.nodeName", "DIV" },
}, .{});
try runner.testCases(&.{
.{ "var v = document.createAttribute('foo')", "undefined" },
.{ "v.nodeName", "foo" },
}, .{});
try runner.testCases(&.{
.{ "document.children.length", "1" },
.{ "document.children.item(0).nodeName", "HTML" },
.{ "document.firstElementChild.nodeName", "HTML" },
.{ "document.lastElementChild.nodeName", "HTML" },
.{ "document.childElementCount", "1" },
.{ "let nd = new Document()", "undefined" },
.{ "nd.children.length", "0" },
.{ "nd.children.item(0)", "null" },
.{ "nd.firstElementChild", "null" },
.{ "nd.lastElementChild", "null" },
.{ "nd.childElementCount", "0" },
.{ "let emptydoc = document.createElement('html')", "undefined" },
.{ "emptydoc.prepend(document.createElement('html'))", "undefined" },
.{ "let emptydoc2 = document.createElement('html')", "undefined" },
.{ "emptydoc2.append(document.createElement('html'))", "undefined" },
}, .{});
try runner.testCases(&.{
.{ "document.querySelector('')", "null" },
.{ "document.querySelector('*').nodeName", "HTML" },
.{ "document.querySelector('#content').id", "content" },
.{ "document.querySelector('#para').id", "para" },
.{ "document.querySelector('.ok').id", "link" },
.{ "document.querySelector('a ~ p').id", "para-empty" },
.{ "document.querySelector(':root').nodeName", "HTML" },
.{ "document.querySelectorAll('p').length", "2" },
.{
\\ Array.from(document.querySelectorAll('#content > p#para-empty'))
\\ .map(row => row.querySelector('span').textContent)
\\ .length;
,
"1",
},
}, .{});
try runner.testCases(&.{
.{ "document.activeElement === document.body", "true" },
.{ "document.getElementById('link').focus()", "undefined" },
.{ "document.activeElement === document.getElementById('link')", "true" },
}, .{});
// this test breaks the doc structure, keep it at the end of the test
// suite.
try runner.testCases(&.{
.{ "let nadop = document.getElementById('content')", "undefined" },
.{ "var v = document.adoptNode(nadop)", "undefined" },
.{ "v.nodeName", "DIV" },
}, .{});
const Case = testing.JsRunner.Case;
const tags = comptime parser.Tag.all();
var createElements: [(tags.len) * 2]Case = undefined;
inline for (tags, 0..) |tag, i| {
const tag_name = @tagName(tag);
createElements[i * 2] = Case{
"var " ++ tag_name ++ "Elem = document.createElement('" ++ tag_name ++ "')",
"undefined",
};
createElements[(i * 2) + 1] = Case{
tag_name ++ "Elem.localName",
tag_name,
};
}
try runner.testCases(&createElements, .{});
test "Browser: DOM.Document" {
try testing.htmlRunner("dom/document.html");
}

View File

@@ -16,8 +16,13 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const css = @import("css.zig");
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const NodeList = @import("nodelist.zig").NodeList;
const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union;
const collection = @import("html_collection.zig");
const Node = @import("node.zig").Node;
@@ -53,22 +58,39 @@ pub const DocumentFragment = struct {
pub fn _replaceChildren(self: *parser.DocumentFragment, nodes: []const Node.NodeOrText) !void {
return Node.replaceChildren(parser.documentFragmentToNode(self), nodes);
}
pub fn _querySelector(self: *parser.DocumentFragment, selector: []const u8, page: *Page) !?ElementUnion {
if (selector.len == 0) return null;
const n = try css.querySelector(page.call_arena, parser.documentFragmentToNode(self), selector);
if (n == null) return null;
return try Element.toInterface(parser.nodeToElement(n.?));
}
pub fn _querySelectorAll(self: *parser.DocumentFragment, selector: []const u8, page: *Page) !NodeList {
return css.querySelectorAll(page.arena, parser.documentFragmentToNode(self), selector);
}
pub fn get_childElementCount(self: *parser.DocumentFragment) !u32 {
var children = try get_children(self);
return children.get_length();
}
pub fn get_children(self: *parser.DocumentFragment) !collection.HTMLCollection {
return collection.HTMLCollectionChildren(parser.documentFragmentToNode(self), .{
.include_root = false,
});
}
pub fn _getElementById(self: *parser.DocumentFragment, id: []const u8) !?ElementUnion {
const e = try parser.nodeGetElementById(@ptrCast(@alignCast(self)), id) orelse return null;
return try Element.toInterface(e);
}
};
const testing = @import("../../testing.zig");
test "Browser.DOM.DocumentFragment" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "const dc = new DocumentFragment()", "undefined" },
.{ "dc.constructor.name", "DocumentFragment" },
}, .{});
try runner.testCases(&.{
.{ "const dc1 = new DocumentFragment()", "undefined" },
.{ "const dc2 = new DocumentFragment()", "undefined" },
.{ "dc1.isEqualNode(dc1)", "true" },
.{ "dc1.isEqualNode(dc2)", "true" },
}, .{});
test "Browser: DOM.DocumentFragment" {
try testing.htmlRunner("dom/document_fragment.html");
}

View File

@@ -62,19 +62,6 @@ pub const DocumentType = struct {
};
const testing = @import("../../testing.zig");
test "Browser.DOM.DocumentType" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let dt1 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');", "undefined" },
.{ "let dt2 = document.implementation.createDocumentType('qname2', 'pid2', 'sys2');", "undefined" },
.{ "let dt3 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');", "undefined" },
.{ "dt1.isEqualNode(dt1)", "true" },
.{ "dt1.isEqualNode(dt3)", "true" },
.{ "dt1.isEqualNode(dt2)", "false" },
.{ "dt2.isEqualNode(dt3)", "false" },
.{ "dt1.isEqualNode(document)", "false" },
.{ "document.isEqualNode(dt1)", "false" },
}, .{});
test "Browser: DOM.DocumentType" {
try testing.htmlRunner("dom/document_type.html");
}

View File

@@ -23,12 +23,13 @@ const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
const DOMTokenList = @import("token_list.zig");
const NodeList = @import("nodelist.zig");
const Node = @import("node.zig");
const ResizeObserver = @import("resize_observer.zig");
const MutationObserver = @import("mutation_observer.zig");
const IntersectionObserver = @import("intersection_observer.zig");
const DOMParser = @import("dom_parser.zig").DOMParser;
const TreeWalker = @import("tree_walker.zig").TreeWalker;
const NodeIterator = @import("node_iterator.zig").NodeIterator;
const NodeFilter = @import("node_filter.zig").NodeFilter;
const Performance = @import("performance.zig").Performance;
const PerformanceObserver = @import("performance_observer.zig").PerformanceObserver;
pub const Interfaces = .{
@@ -41,11 +42,16 @@ pub const Interfaces = .{
NodeList.Interfaces,
Node.Node,
Node.Interfaces,
ResizeObserver.Interfaces,
MutationObserver.Interfaces,
IntersectionObserver.Interfaces,
DOMParser,
TreeWalker,
NodeIterator,
NodeFilter,
Performance,
@import("performance.zig").Interfaces,
PerformanceObserver,
@import("range.zig").Interfaces,
@import("Animation.zig"),
@import("MessageChannel.zig").Interfaces,
};

View File

@@ -36,12 +36,6 @@ pub const DOMParser = struct {
};
const testing = @import("../../testing.zig");
test "Browser.DOM.DOMParser" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "const dp = new DOMParser()", "undefined" },
.{ "dp.parseFromString('<div>abc</div>', 'text/html')", "[object HTMLDocument]" },
}, .{});
test "Browser: DOM.Parser" {
try testing.htmlRunner("dom/dom_parser.html");
}

View File

@@ -30,6 +30,11 @@ const Node = @import("node.zig").Node;
const Walker = @import("walker.zig").WalkerDepthFirst;
const NodeList = @import("nodelist.zig").NodeList;
const HTMLElem = @import("../html/elements.zig");
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
const Animation = @import("Animation.zig");
const JsObject = @import("../env.zig").JsObject;
pub const Union = @import("../html/elements.zig").Union;
// WEB IDL https://dom.spec.whatwg.org/#element
@@ -50,8 +55,33 @@ pub const Element = struct {
};
pub fn toInterface(e: *parser.Element) !Union {
return try HTMLElem.toInterface(Union, e);
// SVGElement and MathML are not supported yet.
return toInterfaceT(Union, e);
}
pub fn toInterfaceT(comptime T: type, e: *parser.Element) !T {
const tagname = try parser.elementGetTagName(e) orelse {
// If the owner's document is HTML, assume we have an HTMLElement.
const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
if (doc != null and !doc.?.is_html) {
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
}
return .{ .Element = e };
};
// TODO SVGElement and MathML are not supported yet.
const tag = parser.Tag.fromString(tagname) catch {
// If the owner's document is HTML, assume we have an HTMLElement.
const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
if (doc != null and doc.?.is_html) {
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
}
return .{ .Element = e };
};
return HTMLElem.toInterfaceFromTag(T, e, tag);
}
// JS funcs
@@ -107,18 +137,18 @@ pub const Element = struct {
}
pub fn get_innerHTML(self: *parser.Element, page: *Page) ![]const u8 {
var buf = std.ArrayList(u8).init(page.arena);
try dump.writeChildren(parser.elementToNode(self), buf.writer());
return buf.items;
var aw = std.Io.Writer.Allocating.init(page.call_arena);
try dump.writeChildren(parser.elementToNode(self), .{}, &aw.writer);
return aw.written();
}
pub fn get_outerHTML(self: *parser.Element, page: *Page) ![]const u8 {
var buf = std.ArrayList(u8).init(page.arena);
try dump.writeNode(parser.elementToNode(self), buf.writer());
return buf.items;
var aw = std.Io.Writer.Allocating.init(page.call_arena);
try dump.writeNode(parser.elementToNode(self), .{}, &aw.writer);
return aw.written();
}
pub fn set_innerHTML(self: *parser.Element, str: []const u8) !void {
pub fn set_innerHTML(self: *parser.Element, str: []const u8, page: *Page) !void {
const node = parser.elementToNode(self);
const doc = try parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
// parse the fragment
@@ -127,16 +157,64 @@ pub const Element = struct {
// remove existing children
try Node.removeChildren(node);
// get fragment body children
const children = try parser.documentFragmentBodyChildren(fragment) orelse return;
const fragment_node = parser.documentFragmentToNode(fragment);
// append children to the node
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because ndoeAppendChild moves the node out of
// the nodeList and into the new tree
const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(node, child);
// I'm not sure what the exact behavior is supposed to be. Initially,
// we were only copying the body of the document fragment. But it seems
// like head elements should be copied too. Specifically, some sites
// create script tags via innerHTML, which we need to capture.
// If you play with this in a browser, you should notice that the
// behavior is different depending on whether you're in a blank page
// or an actual document. In a blank page, something like:
// x.innerHTML = '<script></script>';
// does _not_ create an empty script, but in a real page, it does. Weird.
const html = try parser.nodeFirstChild(fragment_node) orelse return;
const head = try parser.nodeFirstChild(html) orelse return;
const body = try parser.nodeNextSibling(head) orelse return;
if (try parser.elementTag(self) == .template) {
// HTMLElementTemplate is special. We don't append these as children
// of the template, but instead set its content as the body of the
// fragment. Simpler to do this by copying the body children into
// a new fragment
const clean = try parser.documentCreateDocumentFragment(doc);
const children = try parser.nodeGetChildNodes(body);
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child);
}
const state = try page.getOrCreateNodeState(node);
state.template_content = clean;
return;
}
// For any node other than a template, we copy the head and body elements
// as child nodes of the element
{
// First, copy some of the head element
const children = try parser.nodeGetChildNodes(head);
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(node, child);
}
}
{
const children = try parser.nodeGetChildNodes(body);
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(node, child);
}
}
}
@@ -160,8 +238,14 @@ pub const Element = struct {
}
}
// don't use parser.nodeHasAttributes(...) because that returns true/false
// based on the type, e.g. a node never as attributes, an element always has
// attributes. But, Element.hasAttributes is supposed to return true only
// if the element has at least 1 attribute.
pub fn _hasAttributes(self: *parser.Element) !bool {
return try parser.nodeHasAttributes(parser.elementToNode(self));
// an element _must_ have at least an empty attribute
const node_map = try parser.nodeGetAttributes(parser.elementToNode(self)) orelse unreachable;
return try parser.namedNodeMapGetLength(node_map) > 0;
}
pub fn _getAttribute(self: *parser.Element, qname: []const u8) !?[]const u8 {
@@ -230,6 +314,22 @@ pub const Element = struct {
return true;
}
pub fn _getAttributeNames(self: *parser.Element, page: *Page) ![]const []const u8 {
const attributes = try parser.nodeGetAttributes(@ptrCast(self)) orelse return &.{};
const ln = try parser.namedNodeMapGetLength(attributes);
const names = try page.call_arena.alloc([]const u8, ln);
var at: usize = 0;
for (0..ln) |i| {
const attribute = try parser.namedNodeMapItem(attributes, @intCast(i)) orelse break;
names[at] = try parser.attributeGetName(attribute);
at += 1;
}
return names[0..at];
}
pub fn _getAttributeNode(self: *parser.Element, name: []const u8) !?*parser.Attribute {
return try parser.elementGetAttributeNode(self, name);
}
@@ -259,7 +359,7 @@ pub const Element = struct {
page.arena,
parser.elementToNode(self),
tag_name,
false,
.{ .include_root = false },
);
}
@@ -272,14 +372,16 @@ pub const Element = struct {
page.arena,
parser.elementToNode(self),
classNames,
false,
.{ .include_root = false },
);
}
// ParentNode
// https://dom.spec.whatwg.org/#parentnode
pub fn get_children(self: *parser.Element) !collection.HTMLCollection {
return try collection.HTMLCollectionChildren(parser.elementToNode(self), false);
return collection.HTMLCollectionChildren(parser.elementToNode(self), .{
.include_root = false,
});
}
pub fn get_firstElementChild(self: *parser.Element) !?Union {
@@ -307,13 +409,13 @@ pub const Element = struct {
pub fn get_previousElementSibling(self: *parser.Element) !?Union {
const res = try parser.nodePreviousElementSibling(parser.elementToNode(self));
if (res == null) return null;
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
return try toInterface(res.?);
}
pub fn get_nextElementSibling(self: *parser.Element) !?Union {
const res = try parser.nodeNextElementSibling(parser.elementToNode(self));
if (res == null) return null;
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
return try toInterface(res.?);
}
fn getElementById(self: *parser.Element, id: []const u8) !?*parser.Node {
@@ -335,7 +437,7 @@ pub const Element = struct {
pub fn _querySelector(self: *parser.Element, selector: []const u8, page: *Page) !?Union {
if (selector.len == 0) return null;
const n = try css.querySelector(page.arena, parser.elementToNode(self), selector);
const n = try css.querySelector(page.call_arena, parser.elementToNode(self), selector);
if (n == null) return null;
@@ -429,254 +531,79 @@ pub const Element = struct {
_ = opts;
return true;
}
const AttachShadowOpts = struct {
mode: []const u8, // must be specified
};
pub fn _attachShadow(self: *parser.Element, opts: AttachShadowOpts, page: *Page) !*ShadowRoot {
const mode = std.meta.stringToEnum(ShadowRoot.Mode, opts.mode) orelse return error.InvalidArgument;
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
if (state.shadow_root) |sr| {
if (mode != sr.mode) {
// this is the behavior per the spec
return error.NotSupportedError;
}
try Node.removeChildren(@ptrCast(@alignCast(sr.proto)));
return sr;
}
// Not sure what to do if there is no owner document
const doc = try parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument;
const fragment = try parser.documentCreateDocumentFragment(doc);
const sr = try page.arena.create(ShadowRoot);
sr.* = .{
.host = self,
.mode = mode,
.proto = fragment,
};
state.shadow_root = sr;
parser.documentFragmentSetHost(sr.proto, @ptrCast(@alignCast(self)));
// Storing the ShadowRoot on the element makes sense, it's the ShadowRoot's
// parent. When we render, we go top-down, so we'll have the element, get
// its shadowroot, and go on. that's what the above code does.
// But we sometimes need to go bottom-up, e.g when we have a slot element
// and want to find the containing parent. Unforatunately , we don't have
// that link, so we need to create it. In the DOM, the ShadowRoot is
// represented by this DocumentFragment (it's the ShadowRoot's base prototype)
// So we can also store the ShadowRoot in the DocumentFragment's state.
const fragment_state = try page.getOrCreateNodeState(@ptrCast(@alignCast(fragment)));
fragment_state.shadow_root = sr;
return sr;
}
pub fn get_shadowRoot(self: *parser.Element, page: *Page) ?*ShadowRoot {
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
const sr = state.shadow_root orelse return null;
if (sr.mode == .closed) {
return null;
}
return sr;
}
pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation {
_ = self;
_ = opts;
return Animation.constructor(effect, null);
}
pub fn _remove(self: *parser.Element) !void {
// TODO: This hasn't been tested to make sure all references to this
// node are properly updated. A lot of libdom is lazy and will look
// for related elements JIT by walking the tree, but there could be
// cases in libdom or the Zig WebAPI where this reference is kept
const as_node: *parser.Node = @ptrCast(self);
const parent = try parser.nodeParentNode(as_node) orelse return;
_ = try Node._removeChild(parent, as_node);
}
};
// Tests
// -----
const testing = @import("../../testing.zig");
test "Browser.DOM.Element" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let g = document.getElementById('content')", "undefined" },
.{ "g.namespaceURI", "http://www.w3.org/1999/xhtml" },
.{ "g.prefix", "null" },
.{ "g.localName", "div" },
.{ "g.tagName", "DIV" },
}, .{});
try runner.testCases(&.{
.{ "let gs = document.getElementById('content')", "undefined" },
.{ "gs.id", "content" },
.{ "gs.id = 'foo'", "foo" },
.{ "gs.id", "foo" },
.{ "gs.id = 'content'", "content" },
.{ "gs.className", "" },
.{ "let gs2 = document.getElementById('para-empty')", "undefined" },
.{ "gs2.className", "ok empty" },
.{ "gs2.className = 'foo bar baz'", "foo bar baz" },
.{ "gs2.className", "foo bar baz" },
.{ "gs2.className = 'ok empty'", "ok empty" },
.{ "let cl = gs2.classList", "undefined" },
.{ "cl.length", "2" },
}, .{});
try runner.testCases(&.{
.{ "const el2 = document.createElement('div');", "undefined" },
.{ "el2.id = 'closest'; el2.className = 'ok';", "ok" },
.{ "el2.closest('#closest')", "[object HTMLDivElement]" },
.{ "el2.closest('.ok')", "[object HTMLDivElement]" },
.{ "el2.closest('#9000')", "null" },
.{ "el2.closest('.notok')", "null" },
.{ "const sp = document.createElement('span');", "undefined" },
.{ "el2.appendChild(sp);", "[object HTMLSpanElement]" },
.{ "sp.closest('#closest')", "[object HTMLDivElement]" },
.{ "sp.closest('#9000')", "null" },
}, .{});
try runner.testCases(&.{
.{ "let a = document.getElementById('content')", "undefined" },
.{ "a.hasAttributes()", "true" },
.{ "a.attributes.length", "1" },
.{ "a.getAttribute('id')", "content" },
.{ "a.attributes['id'].value", "content" },
.{
\\ let x = '';
\\ for (const attr of a.attributes) {
\\ x += attr.name + '=' + attr.value;
\\ }
\\ x;
,
"id=content",
},
.{ "a.hasAttribute('foo')", "false" },
.{ "a.getAttribute('foo')", "null" },
.{ "a.setAttribute('foo', 'bar')", "undefined" },
.{ "a.hasAttribute('foo')", "true" },
.{ "a.getAttribute('foo')", "bar" },
.{ "a.setAttribute('foo', 'baz')", "undefined" },
.{ "a.hasAttribute('foo')", "true" },
.{ "a.getAttribute('foo')", "baz" },
.{ "a.removeAttribute('foo')", "undefined" },
.{ "a.hasAttribute('foo')", "false" },
.{ "a.getAttribute('foo')", "null" },
}, .{});
try runner.testCases(&.{
.{ "let b = document.getElementById('content')", "undefined" },
.{ "b.toggleAttribute('foo')", "true" },
.{ "b.hasAttribute('foo')", "true" },
.{ "b.getAttribute('foo')", "" },
.{ "b.toggleAttribute('foo')", "false" },
.{ "b.hasAttribute('foo')", "false" },
}, .{});
try runner.testCases(&.{
.{ "let c = document.getElementById('content')", "undefined" },
.{ "c.children.length", "3" },
.{ "c.firstElementChild.nodeName", "A" },
.{ "c.lastElementChild.nodeName", "P" },
.{ "c.childElementCount", "3" },
.{ "c.prepend(document.createTextNode('foo'))", "undefined" },
.{ "c.append(document.createTextNode('bar'))", "undefined" },
}, .{});
try runner.testCases(&.{
.{ "let d = document.getElementById('para')", "undefined" },
.{ "d.previousElementSibling.nodeName", "P" },
.{ "d.nextElementSibling", "null" },
}, .{});
try runner.testCases(&.{
.{ "let e = document.getElementById('content')", "undefined" },
.{ "e.querySelector('foo')", "null" },
.{ "e.querySelector('#foo')", "null" },
.{ "e.querySelector('#link').id", "link" },
.{ "e.querySelector('#para').id", "para" },
.{ "e.querySelector('*').id", "link" },
.{ "e.querySelector('')", "null" },
.{ "e.querySelector('*').id", "link" },
.{ "e.querySelector('#content')", "null" },
.{ "e.querySelector('#para').id", "para" },
.{ "e.querySelector('.ok').id", "link" },
.{ "e.querySelector('a ~ p').id", "para-empty" },
.{ "e.querySelectorAll('foo').length", "0" },
.{ "e.querySelectorAll('#foo').length", "0" },
.{ "e.querySelectorAll('#link').length", "1" },
.{ "e.querySelectorAll('#link').item(0).id", "link" },
.{ "e.querySelectorAll('#para').length", "1" },
.{ "e.querySelectorAll('#para').item(0).id", "para" },
.{ "e.querySelectorAll('*').length", "4" },
.{ "e.querySelectorAll('p').length", "2" },
.{ "e.querySelectorAll('.ok').item(0).id", "link" },
}, .{});
try runner.testCases(&.{
.{ "let f = document.getElementById('content')", "undefined" },
.{ "let ff = document.createAttribute('foo')", "undefined" },
.{ "f.setAttributeNode(ff)", "null" },
.{ "f.getAttributeNode('foo').name", "foo" },
.{ "f.removeAttributeNode(ff).name", "foo" },
.{ "f.getAttributeNode('bar')", "null" },
}, .{});
try runner.testCases(&.{
.{ "document.getElementById('para').innerHTML", " And" },
.{ "document.getElementById('para-empty').innerHTML.trim()", "<span id=\"para-empty-child\"></span>" },
.{ "let h = document.getElementById('para-empty')", "undefined" },
.{ "const prev = h.innerHTML", "undefined" },
.{ "h.innerHTML = '<p id=\"hello\">hello world</p>'", "<p id=\"hello\">hello world</p>" },
.{ "h.innerHTML", "<p id=\"hello\">hello world</p>" },
.{ "h.firstChild.nodeName", "P" },
.{ "h.firstChild.id", "hello" },
.{ "h.firstChild.textContent", "hello world" },
.{ "h.innerHTML = prev; true", "true" },
.{ "document.getElementById('para-empty').innerHTML.trim()", "<span id=\"para-empty-child\"></span>" },
}, .{});
try runner.testCases(&.{
.{ "document.getElementById('para').outerHTML", "<p id=\"para\"> And</p>" },
}, .{});
try runner.testCases(&.{
.{ "document.getElementById('para').clientWidth", "1" },
.{ "document.getElementById('para').clientHeight", "1" },
.{ "let r1 = document.getElementById('para').getBoundingClientRect()", "undefined" },
.{ "r1.x", "0" },
.{ "r1.y", "0" },
.{ "r1.width", "1" },
.{ "r1.height", "1" },
.{ "let r2 = document.getElementById('content').getBoundingClientRect()", "undefined" },
.{ "r2.x", "1" },
.{ "r2.y", "0" },
.{ "r2.width", "1" },
.{ "r2.height", "1" },
.{ "let r3 = document.getElementById('para').getBoundingClientRect()", "undefined" },
.{ "r3.x", "0" },
.{ "r3.y", "0" },
.{ "r3.width", "1" },
.{ "r3.height", "1" },
.{ "document.getElementById('para').clientWidth", "2" },
.{ "document.getElementById('para').clientHeight", "1" },
.{ "let r4 = document.createElement('div').getBoundingClientRect()", null },
.{ "r4.x", "0" },
.{ "r4.y", "0" },
.{ "r4.width", "0" },
.{ "r4.height", "0" },
// Test setup causes WrongDocument or HierarchyRequest error unlike in chrome/firefox
// .{ // An element of another document, even if created from the main document, is not rendered.
// \\ let div5 = document.createElement('div');
// \\ const newDoc = document.implementation.createHTMLDocument("New Document");
// \\ newDoc.body.appendChild(div5);
// \\ let r5 = div5.getBoundingClientRect();
// ,
// null,
// },
// .{ "r5.x", "0" },
// .{ "r5.y", "0" },
// .{ "r5.width", "0" },
// .{ "r5.height", "0" },
}, .{});
try runner.testCases(&.{
.{ "const el = document.createElement('div');", "undefined" },
.{ "el.id = 'matches'; el.className = 'ok';", "ok" },
.{ "el.matches('#matches')", "true" },
.{ "el.matches('.ok')", "true" },
.{ "el.matches('#9000')", "false" },
.{ "el.matches('.notok')", "false" },
}, .{});
try runner.testCases(&.{
.{ "const el3 = document.createElement('div');", "undefined" },
.{ "el3.scrollIntoViewIfNeeded();", "undefined" },
.{ "el3.scrollIntoViewIfNeeded(false);", "undefined" },
}, .{});
// before
try runner.testCases(&.{
.{ "const before_container = document.createElement('div');", "undefined" },
.{ "document.append(before_container);", "undefined" },
.{ "const b1 = document.createElement('div');", "undefined" },
.{ "before_container.append(b1);", "undefined" },
.{ "const b1_a = document.createElement('p');", "undefined" },
.{ "b1.before(b1_a, 'over 9000');", "undefined" },
.{ "before_container.innerHTML", "<p></p>over 9000<div></div>" },
}, .{});
// after
try runner.testCases(&.{
.{ "const after_container = document.createElement('div');", "undefined" },
.{ "document.append(after_container);", "undefined" },
.{ "const a1 = document.createElement('div');", "undefined" },
.{ "after_container.append(a1);", "undefined" },
.{ "const a1_a = document.createElement('p');", "undefined" },
.{ "a1.after('over 9000', a1_a);", "undefined" },
.{ "after_container.innerHTML", "<div></div>over 9000<p></p>" },
}, .{});
try runner.testCases(&.{
.{ "var div1 = document.createElement('div');", null },
.{ "div1.innerHTML = \" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>\"", null },
.{ "div1.getElementsByTagName('a').length", "1" },
}, .{});
test "Browser: DOM.Element" {
try testing.htmlRunner("dom/element.html");
}

View File

@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Env = @import("../env.zig").Env;
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
@@ -23,30 +24,75 @@ const Page = @import("../page.zig").Page;
const EventHandler = @import("../events/event.zig").EventHandler;
const DOMException = @import("exceptions.zig").DOMException;
const Nod = @import("node.zig");
const nod = @import("node.zig");
// EventTarget interfaces
pub const Union = Nod.Union;
pub const Union = union(enum) {
node: nod.Union,
xhr: *@import("../xhr/xhr.zig").XMLHttpRequest,
plain: *parser.EventTarget,
message_port: *@import("MessageChannel.zig").MessagePort,
screen: *@import("../html/screen.zig").Screen,
screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
performance: *@import("performance.zig").Performance,
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
};
// EventTarget implementation
pub const EventTarget = struct {
pub const Self = parser.EventTarget;
pub const Exception = DOMException;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain },
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
// Not all targets are *parser.Nodes. page.zig emits a "load" event
// where the target is a Window, which cannot be cast directly to a node.
// Ideally, we'd remove this duality. Failing that, we'll need to embed
// data into the *parser.EventTarget should we need this for other types.
// For now, for the Window, which is a singleton, we can do this:
if (@intFromPtr(et) == @intFromPtr(&page.window.base)) {
return .{ .Window = &page.window };
// libdom assumes that all event targets are libdom nodes. They are not.
switch (try parser.eventTargetInternalType(et)) {
.libdom_node => {
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
},
.plain => return .{ .plain = et },
.abort_signal => {
// AbortSignal is a special case, it has its own internal type.
// We return it as a node, but we need to handle it differently.
return .{ .node = .{ .AbortSignal = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) } };
},
.window => {
// The window is a common non-node target, but it's easy to handle as its a singleton.
std.debug.assert(@intFromPtr(et) == @intFromPtr(&page.window.base));
return .{ .node = .{ .Window = &page.window } };
},
.xhr => {
const XMLHttpRequestEventTarget = @import("../xhr/event_target.zig").XMLHttpRequestEventTarget;
const base: *XMLHttpRequestEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
return .{ .xhr = @fieldParentPtr("proto", base) };
},
.message_port => {
return .{ .message_port = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
},
.screen => {
return .{ .screen = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
},
.screen_orientation => {
return .{ .screen_orientation = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
},
.performance => {
return .{ .performance = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
},
.media_query_list => {
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
},
}
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
}
// JS funcs
// --------
pub fn constructor(page: *Page) !*parser.EventTarget {
const et = try page.arena.create(EventTarget);
return @ptrCast(&et.base);
}
pub fn _addEventListener(
self: *parser.EventTarget,
typ: []const u8,
@@ -108,130 +154,6 @@ pub const EventTarget = struct {
};
const testing = @import("../../testing.zig");
test "Browser.DOM.EventTarget" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let content = document.getElementById('content')", "undefined" },
.{ "let para = document.getElementById('para')", "undefined" },
// NOTE: as some event properties will change during the event dispatching phases
// we need to copy thoses values in order to check them afterwards
.{
\\ var nb = 0; var evt; var phase; var cur;
\\ function cbk(event) {
\\ evt = event;
\\ phase = event.eventPhase;
\\ cur = event.currentTarget;
\\ nb ++;
\\ }
,
"undefined",
},
}, .{});
try runner.testCases(&.{
.{ "content.addEventListener('basic', cbk)", "undefined" },
.{ "content.dispatchEvent(new Event('basic'))", "true" },
.{ "nb", "1" },
.{ "evt instanceof Event", "true" },
.{ "evt.type", "basic" },
.{ "phase", "2" },
.{ "cur.getAttribute('id')", "content" },
}, .{});
try runner.testCases(&.{
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
.{ "para.dispatchEvent(new Event('basic'))", "true" },
.{ "nb", "0" }, // handler is not called, no capture, not the target, no bubbling
.{ "evt === undefined", "true" },
}, .{});
try runner.testCases(&.{
.{ "nb = 0", "0" },
.{ "content.addEventListener('basic', cbk)", "undefined" },
.{ "content.dispatchEvent(new Event('basic'))", "true" },
.{ "nb", "1" },
}, .{});
try runner.testCases(&.{
.{ "nb = 0", "0" },
.{ "content.addEventListener('basic', cbk, true)", "undefined" },
.{ "content.dispatchEvent(new Event('basic'))", "true" },
.{ "nb", "2" },
}, .{});
try runner.testCases(&.{
.{ "nb = 0", "0" },
.{ "content.removeEventListener('basic', cbk)", "undefined" },
.{ "content.dispatchEvent(new Event('basic'))", "true" },
.{ "nb", "1" },
}, .{});
try runner.testCases(&.{
.{ "nb = 0", "0" },
.{ "content.removeEventListener('basic', cbk, {capture: true})", "undefined" },
.{ "content.dispatchEvent(new Event('basic'))", "true" },
.{ "nb", "0" },
}, .{});
try runner.testCases(&.{
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
.{ "content.addEventListener('capture', cbk, true)", "undefined" },
.{ "content.dispatchEvent(new Event('capture'))", "true" },
.{ "nb", "1" },
.{ "evt instanceof Event", "true" },
.{ "evt.type", "capture" },
.{ "phase", "2" },
.{ "cur.getAttribute('id')", "content" },
}, .{});
try runner.testCases(&.{
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
.{ "para.dispatchEvent(new Event('capture'))", "true" },
.{ "nb", "1" },
.{ "evt instanceof Event", "true" },
.{ "evt.type", "capture" },
.{ "phase", "1" },
.{ "cur.getAttribute('id')", "content" },
}, .{});
try runner.testCases(&.{
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
.{ "content.addEventListener('bubbles', cbk)", "undefined" },
.{ "content.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" },
.{ "nb", "1" },
.{ "evt instanceof Event", "true" },
.{ "evt.type", "bubbles" },
.{ "evt.bubbles", "true" },
.{ "phase", "2" },
.{ "cur.getAttribute('id')", "content" },
}, .{});
try runner.testCases(&.{
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
.{ "para.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" },
.{ "nb", "1" },
.{ "evt instanceof Event", "true" },
.{ "evt.type", "bubbles" },
.{ "phase", "3" },
.{ "cur.getAttribute('id')", "content" },
}, .{});
try runner.testCases(&.{
.{ "const obj1 = {calls: 0, handleEvent: function() { this.calls += 1; } };", null },
.{ "content.addEventListener('he', obj1);", null },
.{ "content.dispatchEvent(new Event('he'));", null },
.{ "obj1.calls", "1" },
.{ "content.removeEventListener('he', obj1);", null },
.{ "content.dispatchEvent(new Event('he'));", null },
.{ "obj1.calls", "1" },
}, .{});
// doesn't crash on null receiver
try runner.testCases(&.{
.{ "content.addEventListener('he2', null);", null },
.{ "content.dispatchEvent(new Event('he2'));", null },
}, .{});
test "Browser: DOM.EventTarget" {
try testing.htmlRunner("dom/event_target.html");
}

View File

@@ -20,10 +20,11 @@ const std = @import("std");
const allocPrint = std.fmt.allocPrint;
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
// https://webidl.spec.whatwg.org/#idl-DOMException
pub const DOMException = struct {
err: parser.DOMError,
err: ?parser.DOMError,
str: []const u8,
pub const ErrorSet = parser.DOMError;
@@ -55,34 +56,84 @@ pub const DOMException = struct {
pub const _INVALID_NODE_TYPE_ERR = 24;
pub const _DATA_CLONE_ERR = 25;
pub fn constructor(message_: ?[]const u8, name_: ?[]const u8, page: *const Page) !DOMException {
const message = message_ orelse "";
const err = if (name_) |n| error_from_str(n) else null;
const fixed_name = name(err);
if (message.len == 0) return .{ .err = err, .str = fixed_name };
const str = try allocPrint(page.arena, "{s}: {s}", .{ fixed_name, message });
return .{ .err = err, .str = str };
}
// TODO: deinit
pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) !DOMException {
const errCast = @as(parser.DOMError, @errorCast(err));
const errName = DOMException.name(errCast);
const str = switch (errCast) {
pub fn init(alloc: std.mem.Allocator, err: anyerror, caller_name: []const u8) !DOMException {
const dom_error = @as(parser.DOMError, @errorCast(err));
const error_name = DOMException.name(dom_error);
const str = switch (dom_error) {
error.HierarchyRequest => try allocPrint(
alloc,
"{s}: Failed to execute '{s}' on 'Node': The new child element contains the parent.",
.{ errName, callerName },
.{ error_name, caller_name },
),
error.NoError => unreachable,
// todo add more custom error messages
else => try allocPrint(
alloc,
"{s}: TODO message", // TODO: implement other messages
.{DOMException.name(errCast)},
"{s}: Failed to execute '{s}' : {s}",
.{ error_name, caller_name, error_name },
),
error.NoError => unreachable,
};
return .{ .err = errCast, .str = str };
return .{ .err = dom_error, .str = str };
}
fn name(err: parser.DOMError) []const u8 {
fn error_from_str(name_: []const u8) ?parser.DOMError {
// @speed: Consider length first, left as is for maintainability, awaiting switch on string support
if (std.mem.eql(u8, name_, "IndexSizeError")) return error.IndexSize;
if (std.mem.eql(u8, name_, "StringSizeError")) return error.StringSize;
if (std.mem.eql(u8, name_, "HierarchyRequestError")) return error.HierarchyRequest;
if (std.mem.eql(u8, name_, "WrongDocumentError")) return error.WrongDocument;
if (std.mem.eql(u8, name_, "InvalidCharacterError")) return error.InvalidCharacter;
if (std.mem.eql(u8, name_, "NoDataAllowedError")) return error.NoDataAllowed;
if (std.mem.eql(u8, name_, "NoModificationAllowedError")) return error.NoModificationAllowed;
if (std.mem.eql(u8, name_, "NotFoundError")) return error.NotFound;
if (std.mem.eql(u8, name_, "NotSupportedError")) return error.NotSupported;
if (std.mem.eql(u8, name_, "InuseAttributeError")) return error.InuseAttribute;
if (std.mem.eql(u8, name_, "InvalidStateError")) return error.InvalidState;
if (std.mem.eql(u8, name_, "SyntaxError")) return error.Syntax;
if (std.mem.eql(u8, name_, "InvalidModificationError")) return error.InvalidModification;
if (std.mem.eql(u8, name_, "NamespaceError")) return error.Namespace;
if (std.mem.eql(u8, name_, "InvalidAccessError")) return error.InvalidAccess;
if (std.mem.eql(u8, name_, "ValidationError")) return error.Validation;
if (std.mem.eql(u8, name_, "TypeMismatchError")) return error.TypeMismatch;
if (std.mem.eql(u8, name_, "SecurityError")) return error.Security;
if (std.mem.eql(u8, name_, "NetworkError")) return error.Network;
if (std.mem.eql(u8, name_, "AbortError")) return error.Abort;
if (std.mem.eql(u8, name_, "URLismatchError")) return error.URLismatch;
if (std.mem.eql(u8, name_, "QuotaExceededError")) return error.QuotaExceeded;
if (std.mem.eql(u8, name_, "TimeoutError")) return error.Timeout;
if (std.mem.eql(u8, name_, "InvalidNodeTypeError")) return error.InvalidNodeType;
if (std.mem.eql(u8, name_, "DataCloneError")) return error.DataClone;
// custom netsurf error
if (std.mem.eql(u8, name_, "UnspecifiedEventTypeError")) return error.UnspecifiedEventType;
if (std.mem.eql(u8, name_, "DispatchRequestError")) return error.DispatchRequest;
if (std.mem.eql(u8, name_, "NoMemoryError")) return error.NoMemory;
if (std.mem.eql(u8, name_, "AttributeWrongTypeError")) return error.AttributeWrongType;
return null;
}
fn name(err_: ?parser.DOMError) []const u8 {
const err = err_ orelse return "Error";
return switch (err) {
error.IndexSize => "IndexSizeError",
error.StringSize => "StringSizeError",
error.StringSize => "StringSizeError", // Legacy: DOMSTRING_SIZE_ERR
error.HierarchyRequest => "HierarchyRequestError",
error.WrongDocument => "WrongDocumentError",
error.InvalidCharacter => "InvalidCharacterError",
error.NoDataAllowed => "NoDataAllowedError",
error.NoDataAllowed => "NoDataAllowedError", // Legacy: NO_DATA_ALLOWED_ERR
error.NoModificationAllowed => "NoModificationAllowedError",
error.NotFound => "NotFoundError",
error.NotSupported => "NotSupportedError",
@@ -92,7 +143,7 @@ pub const DOMException = struct {
error.InvalidModification => "InvalidModificationError",
error.Namespace => "NamespaceError",
error.InvalidAccess => "InvalidAccessError",
error.Validation => "ValidationError",
error.Validation => "ValidationError", // Legacy: VALIDATION_ERR
error.TypeMismatch => "TypeMismatchError",
error.Security => "SecurityError",
error.Network => "NetworkError",
@@ -115,7 +166,8 @@ pub const DOMException = struct {
// JS properties and methods
pub fn get_code(self: *const DOMException) u8 {
return switch (self.err) {
const err = self.err orelse return 0;
return switch (err) {
error.IndexSize => 1,
error.StringSize => 2,
error.HierarchyRequest => 3,
@@ -157,7 +209,8 @@ pub const DOMException = struct {
pub fn get_message(self: *const DOMException) []const u8 {
const errName = DOMException.name(self.err);
return self.str[errName.len + 2 ..];
if (self.str.len <= errName.len + 2) return "";
return self.str[errName.len + 2 ..]; // ! Requires str is formatted as "{name}: {message}"
}
pub fn _toString(self: *const DOMException) []const u8 {
@@ -166,26 +219,6 @@ pub const DOMException = struct {
};
const testing = @import("../../testing.zig");
test "Browser.DOM.Exception" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
const err = "Failed to execute 'appendChild' on 'Node': The new child element contains the parent.";
try runner.testCases(&.{
.{ "let content = document.getElementById('content')", "undefined" },
.{ "let link = document.getElementById('link')", "undefined" },
// HierarchyRequestError
.{
\\ var he;
\\ try { link.appendChild(content) } catch (error) { he = error}
\\ he.name
,
"HierarchyRequestError",
},
.{ "he.code", "3" },
.{ "he.message", err },
.{ "he.toString()", "HierarchyRequestError: " ++ err },
.{ "he instanceof DOMException", "true" },
.{ "he instanceof Error", "true" },
}, .{});
test "Browser: DOM.Exceptions" {
try testing.htmlRunner("dom/exceptions.html");
}

View File

@@ -72,13 +72,14 @@ pub fn HTMLCollectionByTagName(
arena: Allocator,
root: ?*parser.Node,
tag_name: []const u8,
include_root: bool,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) },
.include_root = include_root,
.mutable = opts.mutable,
.include_root = opts.include_root,
};
}
@@ -109,13 +110,14 @@ pub fn HTMLCollectionByClassName(
arena: Allocator,
root: ?*parser.Node,
classNames: []const u8,
include_root: bool,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) },
.include_root = include_root,
.mutable = opts.mutable,
.include_root = opts.include_root,
};
}
@@ -139,13 +141,14 @@ pub fn HTMLCollectionByName(
arena: Allocator,
root: ?*parser.Node,
name: []const u8,
include_root: bool,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByName = try MatchByName.init(arena, name) },
.include_root = include_root,
.mutable = opts.mutable,
.include_root = opts.include_root,
};
}
@@ -189,13 +192,14 @@ pub const HTMLAllCollection = struct {
pub fn HTMLCollectionChildren(
root: ?*parser.Node,
include_root: bool,
) !HTMLCollection {
opts: Opts,
) HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerChildren = .{} },
.matcher = .{ .matchTrue = .{} },
.include_root = include_root,
.mutable = opts.mutable,
.include_root = opts.include_root,
};
}
@@ -224,13 +228,14 @@ pub const MatchByLinks = struct {
pub fn HTMLCollectionByLinks(
root: ?*parser.Node,
include_root: bool,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByLinks = MatchByLinks{} },
.include_root = include_root,
.mutable = opts.mutable,
.include_root = opts.include_root,
};
}
@@ -249,13 +254,14 @@ pub const MatchByAnchors = struct {
pub fn HTMLCollectionByAnchors(
root: ?*parser.Node,
include_root: bool,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByAnchors = MatchByAnchors{} },
.include_root = include_root,
.mutable = opts.mutable,
.include_root = opts.include_root,
};
}
@@ -285,6 +291,11 @@ pub const HTMLCollectionIterator = struct {
}
};
const Opts = struct {
include_root: bool,
mutable: bool = false,
};
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection
// HTMLCollection is re implemented in zig here because libdom
// dom_html_collection expects a comparison function callback as arguement.
@@ -300,6 +311,8 @@ pub const HTMLCollection = struct {
// itself.
include_root: bool = false,
mutable: bool = false,
// save a state for the collection to improve the _item speed.
cur_idx: ?u32 = null,
cur_node: ?*parser.Node = null,
@@ -350,7 +363,7 @@ pub const HTMLCollection = struct {
var node: *parser.Node = undefined;
// Use the current state to improve speed if possible.
if (self.cur_idx != null and index >= self.cur_idx.?) {
if (self.mutable == false and self.cur_idx != null and index >= self.cur_idx.?) {
i = self.cur_idx.?;
node = self.cur_node.?;
} else {
@@ -449,52 +462,6 @@ pub const HTMLCollection = struct {
};
const testing = @import("../../testing.zig");
test "Browser.DOM.HTMLCollection" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
.{ "getElementsByTagName.length", "2" },
.{ "let getElementsByTagNameCI = document.getElementsByTagName('P')", "undefined" },
.{ "getElementsByTagNameCI.length", "2" },
.{ "getElementsByTagName.item(0).localName", "p" },
.{ "getElementsByTagName.item(1).localName", "p" },
.{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
.{ "getElementsByTagNameAll.length", "8" },
.{ "getElementsByTagNameAll.item(0).localName", "html" },
.{ "getElementsByTagNameAll.item(0).localName", "html" },
.{ "getElementsByTagNameAll.item(1).localName", "head" },
.{ "getElementsByTagNameAll.item(0).localName", "html" },
.{ "getElementsByTagNameAll.item(2).localName", "body" },
.{ "getElementsByTagNameAll.item(3).localName", "div" },
.{ "getElementsByTagNameAll.item(7).localName", "p" },
.{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
// array like
.{ "getElementsByTagNameAll[0].localName", "html" },
.{ "getElementsByTagNameAll[7].localName", "p" },
.{ "getElementsByTagNameAll[8]", "undefined" },
.{ "getElementsByTagNameAll['para-empty-child'].localName", "span" },
.{ "getElementsByTagNameAll['foo']", "undefined" },
.{ "document.getElementById('content').getElementsByTagName('*').length", "4" },
.{ "document.getElementById('content').getElementsByTagName('p').length", "2" },
.{ "document.getElementById('content').getElementsByTagName('div').length", "0" },
.{ "document.children.length", "1" },
.{ "document.getElementById('content').children.length", "3" },
// check liveness
.{ "let content = document.getElementById('content')", "undefined" },
.{ "let pe = document.getElementById('para-empty')", "undefined" },
.{ "let p = document.createElement('p')", "undefined" },
.{ "p.textContent = 'OK live'", "OK live" },
.{ "getElementsByTagName.item(1).textContent", " And" },
.{ "content.appendChild(p) != undefined", "true" },
.{ "getElementsByTagName.length", "3" },
.{ "getElementsByTagName.item(2).textContent", "OK live" },
.{ "content.insertBefore(p, pe) != undefined", "true" },
.{ "getElementsByTagName.item(0).textContent", "OK live" },
}, .{});
test "Browser: DOM.HTMLCollection" {
try testing.htmlRunner("dom/html_collection.html");
}

View File

@@ -50,23 +50,7 @@ pub const DOMImplementation = struct {
}
};
// Tests
// -----
const testing = @import("../../testing.zig");
test "Browser.DOM.Implementation" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let impl = document.implementation", "undefined" },
.{ "impl.createHTMLDocument();", "[object HTMLDocument]" },
.{ "const doc = impl.createHTMLDocument('foo');", "undefined" },
.{ "doc", "[object HTMLDocument]" },
.{ "doc.title", "foo" },
.{ "doc.body", "[object HTMLBodyElement]" },
.{ "impl.createDocument(null, 'foo');", "[object Document]" },
.{ "impl.createDocumentType('foo', 'bar', 'baz')", "[object DocumentType]" },
.{ "impl.hasFeature()", "true" },
}, .{});
test "Browser: DOM.Implementation" {
try testing.htmlRunner("dom/implementation.html");
}

View File

@@ -181,110 +181,6 @@ pub const IntersectionObserverEntry = struct {
};
const testing = @import("../../testing.zig");
test "Browser.DOM.IntersectionObserver" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "new IntersectionObserver(() => {}).observe(document.documentElement);", "undefined" },
}, .{});
try runner.testCases(&.{
.{ "let count_a = 0;", "undefined" },
.{ "const a1 = document.createElement('div');", "undefined" },
.{ "new IntersectionObserver(entries => {count_a += 1;}).observe(a1);", "undefined" },
.{ "count_a;", "1" },
}, .{});
// This test is documenting current behavior, not correct behavior.
// Currently every time observe is called, the callback is called with all entries.
try runner.testCases(&.{
.{ "let count_b = 0;", "undefined" },
.{ "let observer_b = new IntersectionObserver(entries => {count_b = entries.length;});", "undefined" },
.{ "const b1 = document.createElement('div');", "undefined" },
.{ "observer_b.observe(b1);", "undefined" },
.{ "count_b;", "1" },
.{ "const b2 = document.createElement('div');", "undefined" },
.{ "observer_b.observe(b2);", "undefined" },
.{ "count_b;", "2" },
}, .{});
// Re-observing is a no-op
try runner.testCases(&.{
.{ "let count_bb = 0;", "undefined" },
.{ "let observer_bb = new IntersectionObserver(entries => {count_bb = entries.length;});", "undefined" },
.{ "const bb1 = document.createElement('div');", "undefined" },
.{ "observer_bb.observe(bb1);", "undefined" },
.{ "count_bb;", "1" },
.{ "observer_bb.observe(bb1);", "undefined" },
.{ "count_bb;", "1" }, // Still 1, not 2
}, .{});
// Unobserve
try runner.testCases(&.{
.{ "let count_c = 0;", "undefined" },
.{ "let observer_c = new IntersectionObserver(entries => { count_c = entries.length;});", "undefined" },
.{ "const c1 = document.createElement('div');", "undefined" },
.{ "observer_c.observe(c1);", "undefined" },
.{ "count_c;", "1" },
.{ "observer_c.unobserve(c1);", "undefined" },
.{ "const c2 = document.createElement('div');", "undefined" },
.{ "observer_c.observe(c2);", "undefined" },
.{ "count_c;", "1" },
}, .{});
// Disconnect
try runner.testCases(&.{
.{ "let observer_d = new IntersectionObserver(entries => {});", "undefined" },
.{ "let d1 = document.createElement('div');", "undefined" },
.{ "observer_d.observe(d1);", "undefined" },
.{ "observer_d.disconnect();", "undefined" },
.{ "observer_d.takeRecords().length;", "0" },
}, .{});
// takeRecords
try runner.testCases(&.{
.{ "let observer_e = new IntersectionObserver(entries => {});", "undefined" },
.{ "let e1 = document.createElement('div');", "undefined" },
.{ "observer_e.observe(e1);", "undefined" },
.{ "const e2 = document.createElement('div');", "undefined" },
.{ "observer_e.observe(e2);", "undefined" },
.{ "observer_e.takeRecords().length;", "2" },
}, .{});
// Entry
try runner.testCases(&.{
.{ "let entry;", "undefined" },
.{ "let div1 = document.createElement('div')", null },
.{ "document.body.appendChild(div1);", null },
.{ "new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);", null },
.{ "entry.boundingClientRect.x;", "0" },
.{ "entry.intersectionRatio;", "1" },
.{ "entry.intersectionRect.x;", "0" },
.{ "entry.intersectionRect.y;", "0" },
.{ "entry.intersectionRect.width;", "1" },
.{ "entry.intersectionRect.height;", "1" },
.{ "entry.isIntersecting;", "true" },
.{ "entry.rootBounds.x;", "0" },
.{ "entry.rootBounds.y;", "0" },
.{ "entry.rootBounds.width;", "1" },
.{ "entry.rootBounds.height;", "1" },
.{ "entry.target;", "[object HTMLDivElement]" },
}, .{});
// Options
try runner.testCases(&.{
.{ "const new_root = document.createElement('span');", null },
.{ "document.body.appendChild(new_root);", null },
.{ "let new_entry;", "undefined" },
.{
\\ const new_observer = new IntersectionObserver(
\\ entries => { new_entry = entries[0]; },
\\ {root: new_root, rootMargin: '0px 0px 0px 0px', threshold: [0]});
,
"undefined",
},
.{ "new_observer.observe(document.createElement('div'));", "undefined" },
.{ "new_entry.rootBounds.x;", "1" },
}, .{});
test "Browser: DOM.IntersectionObserver" {
try testing.htmlRunner("dom/intersection_observer.html");
}

View File

@@ -35,25 +35,33 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
pub const MutationObserver = struct {
page: *Page,
cbk: Env.Function,
arena: Allocator,
connected: bool,
scheduled: bool,
// List of records which were observed. When the call scope ends, we need to
// execute our callback with it.
observed: std.ArrayListUnmanaged(*MutationRecord),
observed: std.ArrayListUnmanaged(MutationRecord),
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
return .{
.cbk = cbk,
.page = page,
.observed = .{},
.arena = page.arena,
.connected = true,
.scheduled = false,
};
}
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?MutationObserverInit) !void {
const options = options_ orelse MutationObserverInit{};
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?Options) !void {
const arena = self.page.arena;
var options = options_ orelse Options{};
if (options.attributeFilter.len > 0) {
options.attributeFilter = try arena.dupe([]const u8, options.attributeFilter);
}
const observer = try self.arena.create(Observer);
const observer = try arena.create(Observer);
observer.* = .{
.node = node,
.options = options,
@@ -102,30 +110,35 @@ pub const MutationObserver = struct {
}
}
pub fn jsCallScopeEnd(self: *MutationObserver) void {
const record = self.observed.items;
if (record.len == 0) {
return;
fn callback(ctx: *anyopaque) ?u32 {
const self: *MutationObserver = @ptrCast(@alignCast(ctx));
if (self.connected == false) {
self.scheduled = true;
return null;
}
self.scheduled = false;
const records = self.observed.items;
if (records.len == 0) {
return null;
}
defer self.observed.clearRetainingCapacity();
for (record) |r| {
const records = [_]MutationRecord{r.*};
var result: Env.Function.Result = undefined;
self.cbk.tryCall(void, .{records}, &result) catch {
log.debug(.user_script, "callback error", .{
.err = result.exception,
.stack = result.stack,
.source = "mutation observer",
});
};
}
var result: Env.Function.Result = undefined;
self.cbk.tryCall(void, .{records}, &result) catch {
log.debug(.user_script, "callback error", .{
.err = result.exception,
.stack = result.stack,
.source = "mutation observer",
});
};
return null;
}
// TODO
pub fn _disconnect(_: *MutationObserver) !void {
// TODO unregister listeners.
pub fn _disconnect(self: *MutationObserver) !void {
self.connected = false;
}
// TODO
@@ -182,31 +195,27 @@ pub const MutationRecord = struct {
}
};
const MutationObserverInit = struct {
const Options = struct {
childList: bool = false,
attributes: bool = false,
characterData: bool = false,
subtree: bool = false,
attributeOldValue: bool = false,
characterDataOldValue: bool = false,
// TODO
// attributeFilter: [][]const u8,
attributeFilter: [][]const u8 = &.{},
fn attr(self: MutationObserverInit) bool {
return self.attributes or self.attributeOldValue;
fn attr(self: Options) bool {
return self.attributes or self.attributeOldValue or self.attributeFilter.len > 0;
}
fn cdata(self: MutationObserverInit) bool {
fn cdata(self: Options) bool {
return self.characterData or self.characterDataOldValue;
}
};
const Observer = struct {
node: *parser.Node,
options: MutationObserverInit,
// record of the mutation, all observed changes in 1 call are batched
record: ?MutationRecord = null,
options: Options,
// reference back to the MutationObserver so that we can access the arena
// and batch the mutation records.
@@ -214,19 +223,34 @@ const Observer = struct {
event_node: parser.EventNode,
fn appliesTo(o: *const Observer, target: *parser.Node) bool {
fn appliesTo(
self: *const Observer,
target: *parser.Node,
event_type: MutationEventType,
event: *parser.MutationEvent,
) !bool {
if (event_type == .DOMAttrModified and self.options.attributeFilter.len > 0) {
const attribute_name = try parser.mutationEventAttributeName(event);
for (self.options.attributeFilter) |needle| blk: {
if (std.mem.eql(u8, attribute_name, needle)) {
break :blk;
}
}
return false;
}
// mutation on any target is always ok.
if (o.options.subtree) {
if (self.options.subtree) {
return true;
}
// if target equals node, alway ok.
if (target == o.node) {
if (target == self.node) {
return true;
}
// no subtree, no same target and no childlist, always noky.
if (!o.options.childList) {
if (!self.options.childList) {
return false;
}
@@ -234,7 +258,7 @@ const Observer = struct {
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = walker.get_next(o.node, next) catch break orelse break;
next = walker.get_next(self.node, next) catch break orelse break;
if (next.? == target) {
return true;
}
@@ -258,27 +282,22 @@ const Observer = struct {
break :blk parser.eventTargetToNode(event_target);
};
if (self.appliesTo(node) == false) {
return;
}
const mutation_event = parser.eventToMutationEvent(event);
const event_type = blk: {
const t = try parser.eventType(event);
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
};
const arena = mutation_observer.arena;
if (self.record == null) {
self.record = .{
.target = self.node,
.type = event_type.recordType(),
};
try mutation_observer.observed.append(arena, &self.record.?);
if (try self.appliesTo(node, event_type, mutation_event) == false) {
return;
}
var record = &self.record.?;
const mutation_event = parser.eventToMutationEvent(event);
var record = MutationRecord{
.target = self.node,
.type = event_type.recordType(),
};
const arena = mutation_observer.page.arena;
switch (event_type) {
.DOMAttrModified => {
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
@@ -302,6 +321,18 @@ const Observer = struct {
}
},
}
try mutation_observer.observed.append(arena, record);
if (mutation_observer.scheduled == false) {
mutation_observer.scheduled = true;
try mutation_observer.page.scheduler.add(
mutation_observer,
MutationObserver.callback,
0,
.{ .name = "mutation_observer" },
);
}
}
};
@@ -322,68 +353,6 @@ const MutationEventType = enum {
};
const testing = @import("../../testing.zig");
test "Browser.DOM.MutationObserver" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "new MutationObserver(() => {}).observe(document, { childList: true });", "undefined" },
}, .{});
try runner.testCases(&.{
.{
\\ var nb = 0;
\\ var mrs;
\\ new MutationObserver((mu) => {
\\ mrs = mu;
\\ nb++;
\\ }).observe(document.firstElementChild, { attributes: true, attributeOldValue: true });
\\ document.firstElementChild.setAttribute("foo", "bar");
\\ // ignored b/c it's about another target.
\\ document.firstElementChild.firstChild.setAttribute("foo", "bar");
\\ nb;
,
"1",
},
.{ "mrs[0].type", "attributes" },
.{ "mrs[0].target == document.firstElementChild", "true" },
.{ "mrs[0].target.getAttribute('foo')", "bar" },
.{ "mrs[0].attributeName", "foo" },
.{ "mrs[0].oldValue", "null" },
}, .{});
try runner.testCases(&.{
.{
\\ var node = document.getElementById("para").firstChild;
\\ var nb2 = 0;
\\ var mrs2;
\\ new MutationObserver((mu) => {
\\ mrs2 = mu;
\\ nb2++;
\\ }).observe(node, { characterData: true, characterDataOldValue: true });
\\ node.data = "foo";
\\ nb2;
,
"1",
},
.{ "mrs2[0].type", "characterData" },
.{ "mrs2[0].target == node", "true" },
.{ "mrs2[0].target.data", "foo" },
.{ "mrs2[0].oldValue", " And" },
}, .{});
// tests that mutation observers that have a callback which trigger the
// mutation observer don't crash.
// https://github.com/lightpanda-io/browser/issues/550
try runner.testCases(&.{
.{
\\ var node = document.getElementById("para");
\\ new MutationObserver(() => {
\\ node.innerText = 'a';
\\ }).observe(document, { subtree:true,childList:true });
\\ node.innerText = "2";
,
"2",
},
}, .{});
test "Browser: DOM.MutationObserver" {
try testing.htmlRunner("dom/mutation_observer.html");
}

View File

@@ -115,24 +115,7 @@ pub const NamedNodeMapIterator = struct {
}
};
// Tests
// -----
const testing = @import("../../testing.zig");
test "Browser.DOM.NamedNodeMap" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let a = document.getElementById('content').attributes", "undefined" },
.{ "a.length", "1" },
.{ "a.item(0)", "[object Attr]" },
.{ "a.item(1)", "null" },
.{ "a.getNamedItem('id')", "[object Attr]" },
.{ "a.getNamedItem('foo')", "null" },
.{ "a.setNamedItem(a.getNamedItem('id'))", "[object Attr]" },
.{ "a['id'].name", "id" },
.{ "a['id'].value", "content" },
.{ "a['other']", "undefined" },
}, .{});
test "Browser: DOM.NamedNodeMap" {
try testing.htmlRunner("dom/named_node_map.html");
}

View File

@@ -29,6 +29,7 @@ const EventTarget = @import("event_target.zig").EventTarget;
const Attr = @import("attribute.zig").Attr;
const CData = @import("character_data.zig");
const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union;
const NodeList = @import("nodelist.zig").NodeList;
const Document = @import("document.zig").Document;
const DocumentType = @import("document_type.zig").DocumentType;
@@ -36,11 +37,11 @@ const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
const HTMLCollection = @import("html_collection.zig").HTMLCollection;
const HTMLAllCollection = @import("html_collection.zig").HTMLAllCollection;
const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator;
const ShadowRoot = @import("shadow_root.zig").ShadowRoot;
const Walker = @import("walker.zig").WalkerDepthFirst;
// HTML
const HTML = @import("../html/html.zig");
const HTMLElem = @import("../html/elements.zig");
// Node interfaces
pub const Interfaces = .{
@@ -67,7 +68,7 @@ pub const Node = struct {
pub fn toInterface(node: *parser.Node) !Union {
return switch (try parser.nodeType(node)) {
.element => try HTMLElem.toInterface(
.element => try Element.toInterfaceT(
Union,
@as(*parser.Element, @ptrCast(node)),
),
@@ -75,7 +76,14 @@ pub const Node = struct {
.text => .{ .Text = @as(*parser.Text, @ptrCast(node)) },
.cdata_section => .{ .CDATASection = @as(*parser.CDATASection, @ptrCast(node)) },
.processing_instruction => .{ .ProcessingInstruction = @as(*parser.ProcessingInstruction, @ptrCast(node)) },
.document => .{ .HTMLDocument = @as(*parser.DocumentHTML, @ptrCast(node)) },
.document => blk: {
const doc: *parser.Document = @ptrCast(node);
if (doc.is_html) {
break :blk .{ .HTMLDocument = @as(*parser.DocumentHTML, @ptrCast(node)) };
}
break :blk .{ .Document = doc };
},
.document_type => .{ .DocumentType = @as(*parser.DocumentType, @ptrCast(node)) },
.attribute => .{ .Attr = @as(*parser.Attribute, @ptrCast(node)) },
.document_fragment => .{ .DocumentFragment = @as(*parser.DocumentFragment, @ptrCast(node)) },
@@ -100,10 +108,20 @@ pub const Node = struct {
pub const _ENTITY_NODE = @intFromEnum(parser.NodeType.entity);
pub const _NOTATION_NODE = @intFromEnum(parser.NodeType.notation);
pub const _DOCUMENT_POSITION_DISCONNECTED = @intFromEnum(parser.DocumentPosition.disconnected);
pub const _DOCUMENT_POSITION_PRECEDING = @intFromEnum(parser.DocumentPosition.preceding);
pub const _DOCUMENT_POSITION_FOLLOWING = @intFromEnum(parser.DocumentPosition.following);
pub const _DOCUMENT_POSITION_CONTAINS = @intFromEnum(parser.DocumentPosition.contains);
pub const _DOCUMENT_POSITION_CONTAINED_BY = @intFromEnum(parser.DocumentPosition.contained_by);
pub const _DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = @intFromEnum(parser.DocumentPosition.implementation_specific);
// JS funcs
// --------
// Read-only attributes
pub fn get_baseURI(_: *parser.Node, page: *Page) ![]const u8 {
return page.url.raw;
}
pub fn get_firstChild(self: *parser.Node) !?Union {
const res = try parser.nodeFirstChild(self);
@@ -145,12 +163,12 @@ pub const Node = struct {
return try Node.toInterface(res.?);
}
pub fn get_parentElement(self: *parser.Node) !?HTMLElem.Union {
pub fn get_parentElement(self: *parser.Node) !?ElementUnion {
const res = try parser.nodeParentElement(self);
if (res == null) {
return null;
}
return try HTMLElem.toInterface(HTMLElem.Union, @as(*parser.Element, @ptrCast(res.?)));
return try Element.toInterface(res.?);
}
pub fn get_nodeName(self: *parser.Node) ![]const u8 {
@@ -170,11 +188,35 @@ pub const Node = struct {
}
pub fn get_isConnected(self: *parser.Node) !bool {
// TODO: handle Shadow DOM
if (try parser.nodeType(self) == .document) {
return true;
var node = self;
while (true) {
const node_type = try parser.nodeType(node);
if (node_type == .document) {
return true;
}
if (try parser.nodeParentNode(node)) |parent| {
// didn't find a document, but node has a parent, let's see
// if it's connected;
node = parent;
continue;
}
if (node_type != .document_fragment) {
// doesn't have a parent and isn't a document_fragment
// can't be connected
return false;
}
if (parser.documentFragmentGetHost(@ptrCast(node))) |host| {
// node doesn't have a parent, but it's a document fragment
// with a host. The host is like the parent, but we only want to
// traverse up (or down) to it in specific cases, like isConnected.
node = host;
continue;
}
return false;
}
return try Node.get_parentNode(self) != null;
}
// Read/Write attributes
@@ -198,6 +240,23 @@ pub const Node = struct {
// Methods
pub fn _appendChild(self: *parser.Node, child: *parser.Node) !Union {
const self_owner = try parser.nodeOwnerDocument(self);
const child_owner = try parser.nodeOwnerDocument(child);
// If the node to be inserted has a different ownerDocument than the parent node,
// modern browsers automatically adopt the node and its descendants into
// the parent's ownerDocument.
// This process is known as adoption.
// (7.1) https://dom.spec.whatwg.org/#concept-node-insert
if (child_owner == null or (self_owner != null and child_owner.? != self_owner.?)) {
const w = Walker{};
var current = child;
while (true) {
current.owner = self_owner;
current = try w.get_next(child, current) orelse break;
}
}
// TODO: DocumentFragment special case
const res = try parser.nodeAppendChild(self, child);
return try Node.toInterface(res);
@@ -209,14 +268,43 @@ pub const Node = struct {
}
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) !u32 {
if (self == other) return 0;
if (self == other) {
return 0;
}
const docself = try parser.nodeOwnerDocument(self);
const docother = try parser.nodeOwnerDocument(other);
const docself = try parser.nodeOwnerDocument(self) orelse blk: {
if (try parser.nodeType(self) == .document) {
break :blk @as(*parser.Document, @ptrCast(self));
}
break :blk null;
};
const docother = try parser.nodeOwnerDocument(other) orelse blk: {
if (try parser.nodeType(other) == .document) {
break :blk @as(*parser.Document, @ptrCast(other));
}
break :blk null;
};
// Both are in different document.
if (docself == null or docother == null or docother.? != docself.?) {
return @intFromEnum(parser.DocumentPosition.disconnected);
if (docself == null or docother == null or docself.? != docother.?) {
return @intFromEnum(parser.DocumentPosition.disconnected) +
@intFromEnum(parser.DocumentPosition.implementation_specific) +
@intFromEnum(parser.DocumentPosition.preceding);
}
if (@intFromPtr(self) == @intFromPtr(docself.?)) {
// if self is the document, and we already know other is in the
// document, then other is contained by and following self.
return @intFromEnum(parser.DocumentPosition.following) +
@intFromEnum(parser.DocumentPosition.contained_by);
}
const rootself = try parser.nodeGetRootNode(self);
const rootother = try parser.nodeGetRootNode(other);
if (rootself != rootother) {
return @intFromEnum(parser.DocumentPosition.disconnected) +
@intFromEnum(parser.DocumentPosition.implementation_specific) +
@intFromEnum(parser.DocumentPosition.preceding);
}
// TODO Both are in a different trees in the same document.
@@ -267,11 +355,23 @@ pub const Node = struct {
// - An Element inside a standard web page will return an HTMLDocument object representing the entire page (or <iframe>).
// - An Element inside a shadow DOM will return the associated ShadowRoot.
// - An Element that is not attached to a document or a shadow tree will return the root of the DOM tree it belongs to
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }) !Union {
const GetRootNodeResult = union(enum) {
shadow_root: *ShadowRoot,
node: Union,
};
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
if (options) |options_| if (options_.composed) {
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
};
return try Node.toInterface(try parser.nodeGetRootNode(self));
const root = try parser.nodeGetRootNode(self);
if (page.getNodeState(root)) |state| {
if (state.shadow_root) |sr| {
return .{ .shadow_root = sr };
}
}
return .{ .node = try Node.toInterface(root) };
}
pub fn _hasChildNodes(self: *parser.Node) !bool {
@@ -290,10 +390,28 @@ pub const Node = struct {
}
pub fn _insertBefore(self: *parser.Node, new_node: *parser.Node, ref_node_: ?*parser.Node) !Union {
if (ref_node_) |ref_node| {
return Node.toInterface(try parser.nodeInsertBefore(self, new_node, ref_node));
if (ref_node_ == null) {
return _appendChild(self, new_node);
}
return _appendChild(self, new_node);
const self_owner = try parser.nodeOwnerDocument(self);
const new_node_owner = try parser.nodeOwnerDocument(new_node);
// If the node to be inserted has a different ownerDocument than the parent node,
// modern browsers automatically adopt the node and its descendants into
// the parent's ownerDocument.
// This process is known as adoption.
// (7.1) https://dom.spec.whatwg.org/#concept-node-insert
if (new_node_owner == null or (self_owner != null and new_node_owner.? != self_owner.?)) {
const w = Walker{};
var current = new_node;
while (true) {
current.owner = self_owner;
current = try w.get_next(new_node, current) orelse break;
}
}
return Node.toInterface(try parser.nodeInsertBefore(self, new_node, ref_node_.?));
}
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
@@ -496,7 +614,7 @@ pub const Node = struct {
fn toNode(self: NodeOrText, doc: *parser.Document) !*parser.Node {
return switch (self) {
.node => |n| n,
.text => |txt| @alignCast(@ptrCast(try parser.documentCreateTextNode(doc, txt))),
.text => |txt| @ptrCast(@alignCast(try parser.documentCreateTextNode(doc, txt))),
};
}
@@ -607,7 +725,13 @@ test "Browser.DOM.node" {
try runner.testCases(&.{
.{ "content.isConnected", "true" },
.{ "document.isConnected", "true" },
.{ "document.createElement('div').isConnected", "false" },
.{ "const connDiv = document.createElement('div')", null },
.{ "connDiv.isConnected", "false" },
.{ "const connParentDiv = document.createElement('div')", null },
.{ "connParentDiv.appendChild(connDiv)", null },
.{ "connDiv.isConnected", "false" },
.{ "content.appendChild(connParentDiv)", null },
.{ "connDiv.isConnected", "true" },
}, .{});
try runner.testCases(&.{
@@ -695,6 +819,10 @@ test "Browser.DOM.node" {
.{ "link.normalize()", "undefined" },
}, .{});
try runner.testCases(&.{
.{ "link.baseURI", "https://lightpanda.io/opensource-browser/" },
}, .{});
try runner.testCases(&.{
.{ "content.removeChild(append) !== undefined", "true" },
.{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" },
@@ -720,3 +848,45 @@ test "Browser.DOM.node" {
.{ "Node.NOTATION_NODE", "12" },
}, .{});
}
test "Browser.DOM.node.owner" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
\\ <div id="target-container">
\\ <p id="reference-node">
\\ I am the original reference node.
\\ </p>
\\ </div>"
});
defer runner.deinit();
try runner.testCases(&.{
.{
\\ const parser = new DOMParser();
\\ const newDoc = parser.parseFromString('<div id="new-node"><p>Hey</p><span>Marked</span></div>', 'text/html');
\\ const newNode = newDoc.getElementById('new-node');
\\ const parent = document.getElementById('target-container');
\\ const referenceNode = document.getElementById('reference-node');
\\ parent.insertBefore(newNode, referenceNode);
\\ const k = document.getElementById('new-node');
\\ const ptag = k.querySelector('p');
\\ const spanTag = k.querySelector('span');
\\ const anotherDoc = parser.parseFromString('<div id="another-new-node"></div>', 'text/html');
\\ const anotherNewNode = anotherDoc.getElementById('another-new-node');
\\
\\ parent.appendChild(anotherNewNode)
,
"[object HTMLDivElement]",
},
.{ "parent.ownerDocument === newNode.ownerDocument", "true" },
.{ "parent.ownerDocument === anotherNewNode.ownerDocument", "true" },
.{ "newNode.firstChild.nodeName", "P" },
.{ "ptag.ownerDocument === parent.ownerDocument", "true" },
.{ "spanTag.ownerDocument === parent.ownerDocument", "true" },
.{ "parent.contains(newNode)", "true" },
.{ "parent.contains(anotherNewNode)", "true" },
.{ "anotherDoc.contains(anotherNewNode)", "false" },
.{ "newDoc.contains(newNode)", "false" },
}, .{});
}

View File

@@ -17,11 +17,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Node = @import("node.zig").Node;
pub const NodeFilter = struct {
pub const _FILTER_ACCEPT: u16 = 1;
pub const _FILTER_REJECT: u16 = 2;
pub const _FILTER_SKIP: u16 = 3;
pub const _SHOW_ALL: u32 = std.math.maxInt(u32);
pub const _SHOW_ELEMENT: u32 = 0b1;
pub const _SHOW_ATTRIBUTE: u32 = 0b10;
@@ -37,6 +41,39 @@ pub const NodeFilter = struct {
pub const _SHOW_NOTATION: u32 = 0b100000000000;
};
const VerifyResult = enum { accept, skip, reject };
pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !VerifyResult {
const node_type = try parser.nodeType(node);
// Verify that we can show this node type.
if (!switch (node_type) {
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
.comment => what_to_show & NodeFilter._SHOW_COMMENT != 0,
.document => what_to_show & NodeFilter._SHOW_DOCUMENT != 0,
.document_fragment => what_to_show & NodeFilter._SHOW_DOCUMENT_FRAGMENT != 0,
.document_type => what_to_show & NodeFilter._SHOW_DOCUMENT_TYPE != 0,
.element => what_to_show & NodeFilter._SHOW_ELEMENT != 0,
.entity => what_to_show & NodeFilter._SHOW_ENTITY != 0,
.entity_reference => what_to_show & NodeFilter._SHOW_ENTITY_REFERENCE != 0,
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
.text => what_to_show & NodeFilter._SHOW_TEXT != 0,
}) return .reject;
// Verify that we aren't filtering it out.
if (filter) |f| {
const acceptance = try f.call(u16, .{try Node.toInterface(node)});
return switch (acceptance) {
NodeFilter._FILTER_ACCEPT => .accept,
NodeFilter._FILTER_REJECT => .reject,
NodeFilter._FILTER_SKIP => .skip,
else => .reject,
};
} else return .accept;
}
const testing = @import("../../testing.zig");
test "Browser.DOM.NodeFilter" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});

View File

@@ -0,0 +1,338 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const NodeFilter = @import("node_filter.zig");
const Node = @import("node.zig").Node;
const NodeUnion = @import("node.zig").Union;
const DOMException = @import("exceptions.zig").DOMException;
// https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator
// While this is similar to TreeWalker it has its own implementation as there are several subtle differences
// For example:
// - nextNode returns the reference node, whereas TreeWalker returns the next node
// - Skip and reject are equivalent for NodeIterator, for TreeWalker they are different
pub const NodeIterator = struct {
pub const Exception = DOMException;
root: *parser.Node,
reference_node: *parser.Node,
what_to_show: u32,
filter: ?NodeIteratorOpts,
filter_func: ?Env.Function,
pointer_before_current: bool = true,
// used to track / block recursive filters
is_in_callback: bool = false,
// One of the few cases where null and undefined resolve to different default.
// We need the raw JsObject so that we can probe the tri state:
// null, undefined or i32.
pub const WhatToShow = Env.JsObject;
pub const NodeIteratorOpts = union(enum) {
function: Env.Function,
object: struct { acceptNode: Env.Function },
};
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator {
var filter_func: ?Env.Function = null;
if (filter) |f| {
filter_func = switch (f) {
.function => |func| func,
.object => |o| o.acceptNode,
};
}
var what_to_show: u32 = undefined;
if (what_to_show_) |wts| {
switch (try wts.triState(NodeIterator, "what_to_show", u32)) {
.null => what_to_show = 0,
.undefined => what_to_show = NodeFilter.NodeFilter._SHOW_ALL,
.value => |v| what_to_show = v,
}
} else {
what_to_show = NodeFilter.NodeFilter._SHOW_ALL;
}
return .{
.root = node,
.reference_node = node,
.what_to_show = what_to_show,
.filter = filter,
.filter_func = filter_func,
};
}
pub fn get_filter(self: *const NodeIterator) ?NodeIteratorOpts {
return self.filter;
}
pub fn get_pointerBeforeReferenceNode(self: *const NodeIterator) bool {
return self.pointer_before_current;
}
pub fn get_referenceNode(self: *const NodeIterator) !NodeUnion {
return try Node.toInterface(self.reference_node);
}
pub fn get_root(self: *const NodeIterator) !NodeUnion {
return try Node.toInterface(self.root);
}
pub fn get_whatToShow(self: *const NodeIterator) u32 {
return self.what_to_show;
}
pub fn _nextNode(self: *NodeIterator) !?NodeUnion {
try self.callbackStart();
defer self.callbackEnd();
if (self.pointer_before_current) {
// Unlike TreeWalker, NodeIterator starts at the first node
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
self.pointer_before_current = false;
return try Node.toInterface(self.reference_node);
}
}
if (try self.firstChild(self.reference_node)) |child| {
self.reference_node = child;
return try Node.toInterface(child);
}
var current = self.reference_node;
while (current != self.root) {
if (try self.nextSibling(current)) |sibling| {
self.reference_node = sibling;
return try Node.toInterface(sibling);
}
current = (try parser.nodeParentNode(current)) orelse break;
}
return null;
}
pub fn _previousNode(self: *NodeIterator) !?NodeUnion {
try self.callbackStart();
defer self.callbackEnd();
if (!self.pointer_before_current) {
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
self.pointer_before_current = true;
// Still need to verify as last may be first as well
return try Node.toInterface(self.reference_node);
}
}
if (self.reference_node == self.root) {
return null;
}
var current = self.reference_node;
while (try parser.nodePreviousSibling(current)) |previous| {
current = previous;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => {
// Get last child if it has one.
if (try self.lastChild(current)) |child| {
self.reference_node = child;
return try Node.toInterface(child);
}
// Otherwise, this node is our previous one.
self.reference_node = current;
return try Node.toInterface(current);
},
.reject, .skip => {
// Get last child if it has one.
if (try self.lastChild(current)) |child| {
self.reference_node = child;
return try Node.toInterface(child);
}
},
}
}
if (current != self.root) {
if (try self.parentNode(current)) |parent| {
self.reference_node = parent;
return try Node.toInterface(parent);
}
}
return null;
}
pub fn _detach(self: *const NodeIterator) void {
// no-op as per spec
_ = self;
}
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node);
const child_count = try parser.nodeListLength(children);
for (0..child_count) |i| {
const index: u32 = @intCast(i);
const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
.reject, .skip => if (try self.firstChild(child)) |gchild| return gchild,
}
}
return null;
}
fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node);
const child_count = try parser.nodeListLength(children);
var index: u32 = child_count;
while (index > 0) {
index -= 1;
const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
.reject, .skip => if (try self.lastChild(child)) |gchild| return gchild,
}
}
return null;
}
// This implementation is actually the same as :TreeWalker
fn parentNode(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
if (self.root == node) return null;
var current = node;
while (true) {
if (current == self.root) return null;
current = (try parser.nodeParentNode(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current,
.reject, .skip => continue,
}
}
}
// This implementation is actually the same as :TreeWalker
fn nextSibling(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
var current = node;
while (true) {
current = (try parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current,
.skip, .reject => continue,
}
}
return null;
}
fn callbackStart(self: *NodeIterator) !void {
if (self.is_in_callback) {
// this is the correct DOMExeption
return error.InvalidState;
}
self.is_in_callback = true;
}
fn callbackEnd(self: *NodeIterator) void {
self.is_in_callback = false;
}
};
const testing = @import("../../testing.zig");
test "Browser.DOM.NodeFilter" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{
\\ const nodeIterator = document.createNodeIterator(
\\ document.body,
\\ NodeFilter.SHOW_ELEMENT,
\\ {
\\ acceptNode(node) {
\\ return NodeFilter.FILTER_ACCEPT;
\\ },
\\ },
\\ );
\\ nodeIterator.nextNode().nodeName;
,
"BODY",
},
.{ "nodeIterator.nextNode().nodeName", "DIV" },
.{ "nodeIterator.nextNode().nodeName", "A" },
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
.{ "nodeIterator.nextNode().nodeName", "A" }, // pointer_before_current flips
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
.{ "nodeIterator.previousNode().nodeName", "DIV" },
.{ "nodeIterator.previousNode().nodeName", "BODY" },
.{ "nodeIterator.previousNode()", "null" }, // Not HEAD since body is root
.{ "nodeIterator.previousNode()", "null" }, // Keeps returning null
.{ "nodeIterator.nextNode().nodeName", "BODY" },
.{ "nodeIterator.nextNode().nodeName", null },
.{ "nodeIterator.nextNode().nodeName", null },
.{ "nodeIterator.nextNode().nodeName", null },
.{ "nodeIterator.nextNode().nodeName", "SPAN" },
.{ "nodeIterator.nextNode().nodeName", "P" },
.{ "nodeIterator.nextNode()", "null" }, // Just the last one
.{ "nodeIterator.nextNode()", "null" }, // Keeps returning null
.{ "nodeIterator.previousNode().nodeName", "P" },
}, .{});
try runner.testCases(&.{
.{
\\ const notationIterator = document.createNodeIterator(
\\ document.body,
\\ NodeFilter.SHOW_NOTATION,
\\ );
\\ notationIterator.nextNode();
,
"null",
},
.{ "notationIterator.previousNode()", "null" },
}, .{});
try runner.testCases(&.{
.{ "nodeIterator.filter.acceptNode(document.body)", "1" },
.{ "notationIterator.filter", "null" },
.{
\\ const rejectIterator = document.createNodeIterator(
\\ document.body,
\\ NodeFilter.SHOW_ALL,
\\ (e => { return NodeFilter.FILTER_REJECT}),
\\ );
\\ rejectIterator.filter(document.body);
,
"2",
},
}, .{});
}

View File

@@ -110,7 +110,7 @@ pub const NodeList = struct {
try self.nodes.append(alloc, node);
}
pub fn get_length(self: *NodeList) u32 {
pub fn get_length(self: *const NodeList) u32 {
return @intCast(self.nodes.items.len);
}

View File

@@ -20,53 +20,174 @@ const std = @import("std");
const parser = @import("../netsurf.zig");
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
pub const Interfaces = .{
Performance,
PerformanceEntry,
PerformanceMark,
};
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
pub const Performance = struct {
pub const prototype = *EventTarget;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{},
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .performance },
time_origin: std.time.Timer,
time_origin: u64,
// if (Window.crossOriginIsolated) -> Resolution in isolated contexts: 5 microseconds
// else -> Resolution in non-isolated contexts: 100 microseconds
const ms_resolution = 100;
fn limitedResolutionMs(nanoseconds: u64) f64 {
const elapsed_at_resolution = ((nanoseconds / std.time.ns_per_us) + ms_resolution / 2) / ms_resolution * ms_resolution;
const elapsed = @as(f64, @floatFromInt(elapsed_at_resolution));
return elapsed / @as(f64, std.time.us_per_ms);
}
pub fn get_timeOrigin(self: *const Performance) f64 {
const is_posix = switch (@import("builtin").os.tag) { // From std.time.zig L125
.windows, .uefi, .wasi => false,
else => true,
pub fn init() Performance {
return .{
.time_origin = milliTimestamp(),
};
const zero = std.time.Instant{ .timestamp = if (!is_posix) 0 else .{ .sec = 0, .nsec = 0 } };
const started = self.time_origin.started.since(zero);
return limitedResolutionMs(started);
}
pub fn _now(self: *Performance) f64 {
return limitedResolutionMs(self.time_origin.read());
pub fn get_timeOrigin(self: *const Performance) u64 {
return self.time_origin;
}
pub fn reset(self: *Performance) void {
self.time_origin = milliTimestamp();
}
pub fn _now(self: *const Performance) u64 {
return milliTimestamp() - self.time_origin;
}
pub fn _mark(_: *Performance, name: []const u8, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
const mark: PerformanceMark = try .constructor(name, _options, page);
// TODO: Should store this in an entries list
return mark;
}
// TODO: fn _mark should record the marks in a lookup
pub fn _clearMarks(_: *Performance, name: ?[]const u8) void {
_ = name;
}
// TODO: fn _measures should record the marks in a lookup
pub fn _clearMeasures(_: *Performance, name: ?[]const u8) void {
_ = name;
}
// TODO: fn _measures should record the marks in a lookup
pub fn _getEntriesByName(_: *Performance, name: []const u8, typ: ?[]const u8) []PerformanceEntry {
_ = name;
_ = typ;
return &.{};
}
// TODO: fn _measures should record the marks in a lookup
pub fn _getEntriesByType(_: *Performance, typ: []const u8) []PerformanceEntry {
_ = typ;
return &.{};
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry
pub const PerformanceEntry = struct {
const PerformanceEntryType = enum {
element,
event,
first_input,
largest_contentful_paint,
layout_shift,
long_animation_frame,
longtask,
mark,
measure,
navigation,
paint,
resource,
taskattribution,
visibility_state,
pub fn toString(self: PerformanceEntryType) []const u8 {
return switch (self) {
.first_input => "first-input",
.largest_contentful_paint => "largest-contentful-paint",
.layout_shift => "layout-shift",
.long_animation_frame => "long-animation-frame",
.visibility_state => "visibility-state",
else => @tagName(self),
};
}
};
duration: f64 = 0.0,
entry_type: PerformanceEntryType,
name: []const u8,
start_time: f64 = 0.0,
pub fn get_duration(self: *const PerformanceEntry) f64 {
return self.duration;
}
pub fn get_entryType(self: *const PerformanceEntry) PerformanceEntryType {
return self.entry_type;
}
pub fn get_name(self: *const PerformanceEntry) []const u8 {
return self.name;
}
pub fn get_startTime(self: *const PerformanceEntry) f64 {
return self.start_time;
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMark
pub const PerformanceMark = struct {
pub const prototype = *PerformanceEntry;
proto: PerformanceEntry,
detail: ?Env.JsObject,
const Options = struct {
detail: ?Env.JsObject = null,
startTime: ?f64 = null,
};
pub fn constructor(name: []const u8, _options: ?Options, page: *Page) !PerformanceMark {
const perf = &page.window.performance;
const options = _options orelse Options{};
const start_time = options.startTime orelse @as(f64, @floatFromInt(perf._now()));
if (start_time < 0.0) {
return error.TypeError;
}
const detail = if (options.detail) |d| try d.persist() else null;
const duped_name = try page.arena.dupe(u8, name);
const proto = PerformanceEntry{ .name = duped_name, .entry_type = .mark, .start_time = start_time };
return .{ .proto = proto, .detail = detail };
}
pub fn get_detail(self: *const PerformanceMark) ?Env.JsObject {
return self.detail;
}
};
const testing = @import("./../../testing.zig");
test "Performance: get_timeOrigin" {
var perf = Performance{ .time_origin = try std.time.Timer.start() };
var perf = Performance.init();
const time_origin = perf.get_timeOrigin();
try testing.expect(time_origin >= 0);
// Check resolution
try testing.expectDelta(@rem(time_origin * std.time.us_per_ms, 100.0), 0.0, 0.1);
}
test "Performance: now" {
var perf = Performance{ .time_origin = try std.time.Timer.start() };
var perf = Performance.init();
// Monotonically increasing
var now = perf._now();
@@ -74,14 +195,30 @@ test "Performance: now" {
try testing.expectEqual(now, 0);
now = perf._now();
}
// Check resolution
try testing.expectDelta(@rem(now * std.time.us_per_ms, 100.0), 0.0, 0.1);
var after = perf._now();
while (after <= now) { // Loop untill after > now
try testing.expectEqual(after, now);
after = perf._now();
}
// Check resolution
try testing.expectDelta(@rem(after * std.time.us_per_ms, 100.0), 0.0, 0.1);
}
test "Browser.Performance.Mark" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let performance = window.performance", null },
.{ "performance instanceof Performance", "true" },
.{ "let mark1 = performance.mark(\"start\")", null },
.{ "mark1 instanceof PerformanceMark", "true" },
.{ "mark1.name", "start" },
.{ "mark1.entryType", "mark" },
.{ "mark1.duration", "0" },
.{ "mark1.detail", "null" },
.{ "let mark2 = performance.mark(\"start\", {startTime: 32939393.9})", null },
.{ "mark2.startTime", "32939393.9" },
}, .{});
}

View File

@@ -17,10 +17,39 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Env = @import("../env.zig").Env;
const PerformanceEntry = @import("performance.zig").PerformanceEntry;
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver
pub const PerformanceObserver = struct {
pub const _supportedEntryTypes = [0][]const u8{};
pub fn constructor(cbk: Env.Function) PerformanceObserver {
_ = cbk;
return .{};
}
pub fn _observe(self: *const PerformanceObserver, options_: ?Options) void {
_ = self;
_ = options_;
return;
}
pub fn _disconnect(self: *PerformanceObserver) void {
_ = self;
}
pub fn _takeRecords(_: *const PerformanceObserver) []PerformanceEntry {
return &[_]PerformanceEntry{};
}
};
const Options = struct {
buffered: ?bool = null,
durationThreshold: ?f64 = null,
entryTypes: ?[]const []const u8 = null,
type: ?[]const u8 = null,
};
const testing = @import("../../testing.zig");

428
src/browser/dom/range.zig Normal file
View File

@@ -0,0 +1,428 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const Node = @import("node.zig").Node;
const NodeUnion = @import("node.zig").Union;
const DOMException = @import("exceptions.zig").DOMException;
pub const Interfaces = .{
AbstractRange,
Range,
};
pub const AbstractRange = struct {
collapsed: bool,
end_node: *parser.Node,
end_offset: u32,
start_node: *parser.Node,
start_offset: u32,
pub fn updateCollapsed(self: *AbstractRange) void {
// TODO: Eventually, compare properly.
self.collapsed = false;
}
pub fn get_collapsed(self: *const AbstractRange) bool {
return self.collapsed;
}
pub fn get_endContainer(self: *const AbstractRange) !NodeUnion {
return Node.toInterface(self.end_node);
}
pub fn get_endOffset(self: *const AbstractRange) u32 {
return self.end_offset;
}
pub fn get_startContainer(self: *const AbstractRange) !NodeUnion {
return Node.toInterface(self.start_node);
}
pub fn get_startOffset(self: *const AbstractRange) u32 {
return self.start_offset;
}
};
pub const Range = struct {
pub const Exception = DOMException;
pub const prototype = *AbstractRange;
proto: AbstractRange,
pub const _START_TO_START = 0;
pub const _START_TO_END = 1;
pub const _END_TO_END = 2;
pub const _END_TO_START = 3;
// The Range() constructor returns a newly created Range object whose start
// and end is the global Document object.
// https://developer.mozilla.org/en-US/docs/Web/API/Range/Range
pub fn constructor(page: *Page) Range {
const proto: AbstractRange = .{
.collapsed = true,
.end_node = parser.documentHTMLToNode(page.window.document),
.end_offset = 0,
.start_node = parser.documentHTMLToNode(page.window.document),
.start_offset = 0,
};
return .{ .proto = proto };
}
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
try ensureValidOffset(node, offset_);
const offset: u32 = @intCast(offset_);
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
error.WrongDocument => blk: {
// allow a node with a different root than the current, or
// a disconnected one. Treat it as if it's "after", so that
// we also update the end_offset and end_node.
break :blk 1;
},
else => return err,
};
if (position == 1) {
// if we're setting the node after the current start, the end must
// be set too.
self.proto.end_offset = offset;
self.proto.end_node = node;
}
self.proto.start_node = node;
self.proto.start_offset = offset;
self.proto.updateCollapsed();
}
pub fn _setStartBefore(self: *Range, node: *parser.Node) !void {
const parent, const index = try getParentAndIndex(node);
self.proto.start_node = parent;
self.proto.start_offset = index;
}
pub fn _setStartAfter(self: *Range, node: *parser.Node) !void {
const parent, const index = try getParentAndIndex(node);
self.proto.start_node = parent;
self.proto.start_offset = index + 1;
}
pub fn _setEnd(self: *Range, node: *parser.Node, offset_: i32) !void {
try ensureValidOffset(node, offset_);
const offset: u32 = @intCast(offset_);
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
error.WrongDocument => blk: {
// allow a node with a different root than the current, or
// a disconnected one. Treat it as if it's "before", so that
// we also update the end_offset and end_node.
break :blk -1;
},
else => return err,
};
if (position == -1) {
// if we're setting the node before the current start, the start
// must be set too.
self.proto.start_offset = offset;
self.proto.start_node = node;
}
self.proto.end_node = node;
self.proto.end_offset = offset;
self.proto.updateCollapsed();
}
pub fn _setEndBefore(self: *Range, node: *parser.Node) !void {
const parent, const index = try getParentAndIndex(node);
self.proto.end_node = parent;
self.proto.end_offset = index;
}
pub fn _setEndAfter(self: *Range, node: *parser.Node) !void {
const parent, const index = try getParentAndIndex(node);
self.proto.end_node = parent;
self.proto.end_offset = index + 1;
}
pub fn _createContextualFragment(_: *Range, fragment: []const u8, page: *Page) !*parser.DocumentFragment {
const document_html = page.window.document;
const document = parser.documentHTMLToDocument(document_html);
const doc_frag = try parser.documentParseFragmentFromStr(document, fragment);
return doc_frag;
}
pub fn _selectNodeContents(self: *Range, node: *parser.Node) !void {
self.proto.start_node = node;
self.proto.start_offset = 0;
self.proto.end_node = node;
// Set end_offset
switch (try parser.nodeType(node)) {
.text, .cdata_section, .comment, .processing_instruction => {
// For text-like nodes, end_offset should be the length of the text data
if (try parser.nodeValue(node)) |text_data| {
self.proto.end_offset = @intCast(text_data.len);
} else {
self.proto.end_offset = 0;
}
},
else => {
// For element and other nodes, end_offset is the number of children
const child_nodes = try parser.nodeGetChildNodes(node);
const child_count = try parser.nodeListLength(child_nodes);
self.proto.end_offset = @intCast(child_count);
},
}
self.proto.updateCollapsed();
}
// creates a copy
pub fn _cloneRange(self: *const Range) Range {
return .{
.proto = .{
.collapsed = self.proto.collapsed,
.end_node = self.proto.end_node,
.end_offset = self.proto.end_offset,
.start_node = self.proto.start_node,
.start_offset = self.proto.start_offset,
},
};
}
pub fn _comparePoint(self: *const Range, node: *parser.Node, offset_: i32) !i32 {
const start = self.proto.start_node;
if (try parser.nodeGetRootNode(start) != try parser.nodeGetRootNode(node)) {
// WPT really wants this error to be first. Later, when we check
// if the relative position is 'disconnected', it'll also catch this
// case, but WPT will complain because it sometimes also sends
// invalid offsets, and it wants WrongDocument to be raised.
return error.WrongDocument;
}
if (try parser.nodeType(node) == .document_type) {
return error.InvalidNodeType;
}
try ensureValidOffset(node, offset_);
const offset: u32 = @intCast(offset_);
if (try compare(node, offset, start, self.proto.start_offset) == -1) {
return -1;
}
if (try compare(node, offset, self.proto.end_node, self.proto.end_offset) == 1) {
return 1;
}
return 0;
}
pub fn _isPointInRange(self: *const Range, node: *parser.Node, offset_: i32) !bool {
return self._comparePoint(node, offset_) catch |err| switch (err) {
error.WrongDocument => return false,
else => return err,
} == 0;
}
pub fn _intersectsNode(self: *const Range, node: *parser.Node) !bool {
const start_root = try parser.nodeGetRootNode(self.proto.start_node);
const node_root = try parser.nodeGetRootNode(node);
if (start_root != node_root) {
return false;
}
const parent, const index = getParentAndIndex(node) catch |err| switch (err) {
error.InvalidNodeType => return true, // if node has no parent, we return true.
else => return err,
};
if (try compare(parent, index + 1, self.proto.start_node, self.proto.start_offset) != 1) {
// node isn't after start, can't intersect
return false;
}
if (try compare(parent, index, self.proto.end_node, self.proto.end_offset) != -1) {
// node isn't before end, can't intersect
return false;
}
return true;
}
pub fn _compareBoundaryPoints(self: *const Range, how: i32, other: *const Range) !i32 {
return switch (how) {
_START_TO_START => compare(self.proto.start_node, self.proto.start_offset, other.proto.start_node, other.proto.start_offset),
_START_TO_END => compare(self.proto.start_node, self.proto.start_offset, other.proto.end_node, other.proto.end_offset),
_END_TO_END => compare(self.proto.end_node, self.proto.end_offset, other.proto.end_node, other.proto.end_offset),
_END_TO_START => compare(self.proto.end_node, self.proto.end_offset, other.proto.start_node, other.proto.start_offset),
else => error.NotSupported, // this is the correct DOM Exception to return
};
}
// The Range.detach() method does nothing. It used to disable the Range
// object and enable the browser to release associated resources. The
// method has been kept for compatibility.
// https://developer.mozilla.org/en-US/docs/Web/API/Range/detach
pub fn _detach(_: *Range) void {}
};
fn ensureValidOffset(node: *parser.Node, offset: i32) !void {
if (offset < 0) {
return error.IndexSize;
}
// not >= because 0 seems to represent the node itself.
if (offset > try nodeLength(node)) {
return error.IndexSize;
}
}
fn nodeLength(node: *parser.Node) !usize {
switch (try isTextual(node)) {
true => return ((try parser.nodeTextContent(node)) orelse "").len,
false => {
const children = try parser.nodeGetChildNodes(node);
return @intCast(try parser.nodeListLength(children));
},
}
}
fn isTextual(node: *parser.Node) !bool {
return switch (try parser.nodeType(node)) {
.text, .comment, .cdata_section => true,
else => false,
};
}
fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } {
const parent = (try parser.nodeParentNode(child)) orelse return error.InvalidNodeType;
const children = try parser.nodeGetChildNodes(parent);
const ln = try parser.nodeListLength(children);
var i: u32 = 0;
while (i < ln) {
defer i += 1;
const c = try parser.nodeListItem(children, i) orelse continue;
if (c == child) {
return .{ parent, i };
}
}
// should not be possible to reach this point
return error.InvalidNodeType;
}
// implementation is largely copied from the WPT helper called getPosition in
// the common.js of the dom folder.
fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b: u32) !i32 {
if (node_a == node_b) {
// This is a simple and common case, where the two nodes are the same
// We just need to compare their offsets
if (offset_a == offset_b) {
return 0;
}
return if (offset_a < offset_b) -1 else 1;
}
// We're probably comparing two different nodes. "Probably", because the
// above case on considered the offset if the two nodes were the same
// as-is. They could still be the same here, if we first consider the
// offset.
const position = try Node._compareDocumentPosition(node_b, node_a);
if (position & @intFromEnum(parser.DocumentPosition.disconnected) == @intFromEnum(parser.DocumentPosition.disconnected)) {
return error.WrongDocument;
}
if (position & @intFromEnum(parser.DocumentPosition.following) == @intFromEnum(parser.DocumentPosition.following)) {
return switch (try compare(node_b, offset_b, node_a, offset_a)) {
-1 => 1,
1 => -1,
else => unreachable,
};
}
if (position & @intFromEnum(parser.DocumentPosition.contains) == @intFromEnum(parser.DocumentPosition.contains)) {
// node_a contains node_b
var child = node_b;
while (try parser.nodeParentNode(child)) |parent| {
if (parent == node_a) {
// child.parentNode == node_a
break;
}
child = parent;
} else {
// this should not happen, because Node._compareDocumentPosition
// has told us that node_a contains node_b, so one of node_b's
// parent's MUST be node_a. But somehow we do end up here sometimes.
return -1;
}
const child_parent, const child_index = try getParentAndIndex(child);
std.debug.assert(node_a == child_parent);
return if (child_index < offset_a) -1 else 1;
}
return -1;
}
const testing = @import("../../testing.zig");
test "Browser.Range" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
// Test Range constructor
.{ "let range = new Range()", "undefined" },
.{ "range instanceof Range", "true" },
.{ "range instanceof AbstractRange", "true" },
// Test initial state - collapsed range
.{ "range.collapsed", "true" },
.{ "range.startOffset", "0" },
.{ "range.endOffset", "0" },
.{ "range.startContainer instanceof HTMLDocument", "true" },
.{ "range.endContainer instanceof HTMLDocument", "true" },
// Test document.createRange()
.{ "let docRange = document.createRange()", "undefined" },
.{ "docRange instanceof Range", "true" },
.{ "docRange.collapsed", "true" },
}, .{});
try runner.testCases(&.{
.{ "const container = document.getElementById('content');", null },
// Test text range
.{ "const commentNode = container.childNodes[7];", null },
.{ "commentNode.nodeValue", "comment" },
.{ "const textRange = document.createRange();", null },
.{ "textRange.selectNodeContents(commentNode)", "undefined" },
.{ "textRange.startOffset", "0" },
.{ "textRange.endOffset", "7" }, // length of `comment`
// Test Node range
.{ "const nodeRange = document.createRange();", null },
.{ "nodeRange.selectNodeContents(container)", "undefined" },
.{ "nodeRange.startOffset", "0" },
.{ "nodeRange.endOffset", "9" }, // length of container.childNodes
}, .{});
}

View File

@@ -0,0 +1,54 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const Env = @import("../env.zig").Env;
const parser = @import("../netsurf.zig");
pub const Interfaces = .{
ResizeObserver,
};
// WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface
pub const ResizeObserver = struct {
pub fn constructor(cbk: Env.Function) ResizeObserver {
_ = cbk;
return .{};
}
pub fn _observe(self: *const ResizeObserver, element: *parser.Element, options_: ?Options) void {
_ = self;
_ = element;
_ = options_;
return;
}
pub fn _unobserve(self: *const ResizeObserver, element: *parser.Element) void {
_ = self;
_ = element;
return;
}
// TODO
pub fn _disconnect(self: *ResizeObserver) void {
_ = self;
}
};
const Options = struct {
box: []const u8,
};

View File

@@ -0,0 +1,155 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const dump = @import("../dump.zig");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const Node = @import("node.zig").Node;
const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union;
// WEB IDL https://dom.spec.whatwg.org/#interface-shadowroot
pub const ShadowRoot = struct {
pub const prototype = *parser.DocumentFragment;
pub const subtype = .node;
mode: Mode,
host: *parser.Element,
proto: *parser.DocumentFragment,
adopted_style_sheets: ?Env.JsObject = null,
pub const Mode = enum {
open,
closed,
};
pub fn get_host(self: *const ShadowRoot) !ElementUnion {
return Element.toInterface(self.host);
}
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject {
if (self.adopted_style_sheets) |obj| {
return obj;
}
const obj = try page.main_context.newArray(0).persist();
self.adopted_style_sheets = obj;
return obj;
}
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
self.adopted_style_sheets = try sheets.persist();
}
pub fn get_innerHTML(self: *ShadowRoot, page: *Page) ![]const u8 {
var aw = std.Io.Writer.Allocating.init(page.call_arena);
try dump.writeChildren(parser.documentFragmentToNode(self.proto), .{}, &aw.writer);
return aw.written();
}
pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void {
const sr_doc = parser.documentFragmentToNode(self.proto);
const doc = try parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument;
try Node.removeChildren(sr_doc);
const str = str_ orelse return;
const fragment = try parser.documentParseFragmentFromStr(doc, str);
const fragment_node = parser.documentFragmentToNode(fragment);
// Element.set_innerHTML also has some weirdness here. It isn't clear
// what should and shouldn't be set. Whatever string you pass to libdom,
// it always creates a full HTML document, with an html, head and body
// element.
// For ShadowRoot, it appears the only the children within the body should
// be set.
const html = try parser.nodeFirstChild(fragment_node) orelse return;
const head = try parser.nodeFirstChild(html) orelse return;
const body = try parser.nodeNextSibling(head) orelse return;
const children = try parser.nodeGetChildNodes(body);
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(sr_doc, child);
}
}
};
const testing = @import("../../testing.zig");
test "Browser.DOM.ShadowRoot" {
defer testing.reset();
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
\\ <div id=conflict>nope</div>
});
defer runner.deinit();
try runner.testCases(&.{
.{ "const div1 = document.createElement('div');", null },
.{ "let sr1 = div1.attachShadow({mode: 'open'})", null },
.{ "sr1.host == div1", "true" },
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
.{ "div1.shadowRoot == sr1", "true" },
.{ "try { div1.attachShadow({mode: 'closed'}) } catch (e) { e }", "Error: NotSupportedError" },
.{ " sr1.append(document.createElement('div'))", null },
.{ " sr1.append(document.createElement('span'))", null },
.{ "sr1.childElementCount", "2" },
// re-attaching clears it
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
.{ "sr1.childElementCount", "0" },
}, .{});
try runner.testCases(&.{
.{ "const div2 = document.createElement('di2');", null },
.{ "let sr2 = div2.attachShadow({mode: 'closed'})", null },
.{ "sr2.host == div2", "true" },
.{ "div2.shadowRoot", "null" }, // null when attached with 'closed'
}, .{});
try runner.testCases(&.{
.{ "sr2.getElementById('conflict')", "null" },
.{ "const n1 = document.createElement('div')", null },
.{ "n1.id = 'conflict'", null },
.{ "sr2.append(n1)", null },
.{ "sr2.getElementById('conflict') == n1", "true" },
}, .{});
try runner.testCases(&.{
.{ "const acss = sr2.adoptedStyleSheets", null },
.{ "acss.length", "0" },
.{ "acss.push(new CSSStyleSheet())", null },
.{ "sr2.adoptedStyleSheets.length", "1" },
}, .{});
try runner.testCases(&.{
.{ "sr1.innerHTML = '<p>hello</p>'", null },
.{ "sr1.innerHTML", "<p>hello</p>" },
.{ "sr1.querySelector('*')", "[object HTMLParagraphElement]" },
.{ "sr1.innerHTML = null", null },
.{ "sr1.innerHTML", "" },
.{ "sr1.querySelector('*')", "null" },
}, .{});
}

View File

@@ -19,23 +19,31 @@
const std = @import("std");
const parser = @import("../netsurf.zig");
const NodeFilter = @import("node_filter.zig").NodeFilter;
const NodeFilter = @import("node_filter.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const Node = @import("node.zig").Node;
const NodeUnion = @import("node.zig").Union;
// https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
pub const TreeWalker = struct {
root: *parser.Node,
current_node: *parser.Node,
what_to_show: u32,
filter: ?Env.Function,
filter: ?TreeWalkerOpts,
filter_func: ?Env.Function,
// One of the few cases where null and undefined resolve to different default.
// We need the raw JsObject so that we can probe the tri state:
// null, undefined or i32.
pub const WhatToShow = Env.JsObject;
pub const TreeWalkerOpts = union(enum) {
function: Env.Function,
object: struct { acceptNode: Env.Function },
};
pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?TreeWalkerOpts) !TreeWalker {
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker {
var filter_func: ?Env.Function = null;
if (filter) |f| {
@@ -45,61 +53,39 @@ pub const TreeWalker = struct {
};
}
var what_to_show: u32 = undefined;
if (what_to_show_) |wts| {
switch (try wts.triState(TreeWalker, "what_to_show", u32)) {
.null => what_to_show = 0,
.undefined => what_to_show = NodeFilter.NodeFilter._SHOW_ALL,
.value => |v| what_to_show = v,
}
} else {
what_to_show = NodeFilter.NodeFilter._SHOW_ALL;
}
return .{
.root = node,
.current_node = node,
.what_to_show = what_to_show orelse NodeFilter._SHOW_ALL,
.filter = filter_func,
.what_to_show = what_to_show,
.filter = filter,
.filter_func = filter_func,
};
}
const VerifyResult = enum { accept, skip, reject };
fn verify(self: *const TreeWalker, node: *parser.Node) !VerifyResult {
const node_type = try parser.nodeType(node);
const what_to_show = self.what_to_show;
// Verify that we can show this node type.
if (!switch (node_type) {
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
.comment => what_to_show & NodeFilter._SHOW_COMMENT != 0,
.document => what_to_show & NodeFilter._SHOW_DOCUMENT != 0,
.document_fragment => what_to_show & NodeFilter._SHOW_DOCUMENT_FRAGMENT != 0,
.document_type => what_to_show & NodeFilter._SHOW_DOCUMENT_TYPE != 0,
.element => what_to_show & NodeFilter._SHOW_ELEMENT != 0,
.entity => what_to_show & NodeFilter._SHOW_ENTITY != 0,
.entity_reference => what_to_show & NodeFilter._SHOW_ENTITY_REFERENCE != 0,
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
.text => what_to_show & NodeFilter._SHOW_TEXT != 0,
}) return .reject;
// Verify that we aren't filtering it out.
if (self.filter) |f| {
const filter = try f.call(u32, .{node});
return switch (filter) {
NodeFilter._FILTER_ACCEPT => .accept,
NodeFilter._FILTER_REJECT => .reject,
NodeFilter._FILTER_SKIP => .skip,
else => .reject,
};
} else return .accept;
pub fn get_root(self: *TreeWalker) !NodeUnion {
return try Node.toInterface(self.root);
}
pub fn get_root(self: *TreeWalker) *parser.Node {
return self.root;
}
pub fn get_currentNode(self: *TreeWalker) *parser.Node {
return self.current_node;
pub fn get_currentNode(self: *TreeWalker) !NodeUnion {
return try Node.toInterface(self.current_node);
}
pub fn get_whatToShow(self: *TreeWalker) u32 {
return self.what_to_show;
}
pub fn get_filter(self: *TreeWalker) ?Env.Function {
pub fn get_filter(self: *TreeWalker) ?TreeWalkerOpts {
return self.filter;
}
@@ -115,7 +101,7 @@ pub const TreeWalker = struct {
const index: u32 = @intCast(i);
const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try self.verify(child)) {
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
.accept => return child,
.reject => continue,
.skip => if (try self.firstChild(child)) |gchild| return gchild,
@@ -134,7 +120,7 @@ pub const TreeWalker = struct {
index -= 1;
const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try self.verify(child)) {
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
.accept => return child,
.reject => continue,
.skip => if (try self.lastChild(child)) |gchild| return gchild,
@@ -150,7 +136,7 @@ pub const TreeWalker = struct {
while (true) {
current = (try parser.nodeNextSibling(current)) orelse return null;
switch (try self.verify(current)) {
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current,
.skip, .reject => continue,
}
@@ -165,7 +151,7 @@ pub const TreeWalker = struct {
while (true) {
current = (try parser.nodePreviousSibling(current)) orelse return null;
switch (try self.verify(current)) {
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current,
.skip, .reject => continue,
}
@@ -182,42 +168,42 @@ pub const TreeWalker = struct {
if (current == self.root) return null;
current = (try parser.nodeParentNode(current)) orelse return null;
switch (try self.verify(current)) {
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current,
.reject, .skip => continue,
}
}
}
pub fn _firstChild(self: *TreeWalker) !?*parser.Node {
pub fn _firstChild(self: *TreeWalker) !?NodeUnion {
if (try self.firstChild(self.current_node)) |child| {
self.current_node = child;
return child;
return try Node.toInterface(child);
}
return null;
}
pub fn _lastChild(self: *TreeWalker) !?*parser.Node {
pub fn _lastChild(self: *TreeWalker) !?NodeUnion {
if (try self.lastChild(self.current_node)) |child| {
self.current_node = child;
return child;
return try Node.toInterface(child);
}
return null;
}
pub fn _nextNode(self: *TreeWalker) !?*parser.Node {
pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
if (try self.firstChild(self.current_node)) |child| {
self.current_node = child;
return child;
return try Node.toInterface(child);
}
var current = self.current_node;
while (current != self.root) {
if (try self.nextSibling(current)) |sibling| {
self.current_node = sibling;
return sibling;
return try Node.toInterface(sibling);
}
current = (try parser.nodeParentNode(current)) orelse break;
@@ -226,47 +212,49 @@ pub const TreeWalker = struct {
return null;
}
pub fn _nextSibling(self: *TreeWalker) !?*parser.Node {
pub fn _nextSibling(self: *TreeWalker) !?NodeUnion {
if (try self.nextSibling(self.current_node)) |sibling| {
self.current_node = sibling;
return sibling;
return try Node.toInterface(sibling);
}
return null;
}
pub fn _parentNode(self: *TreeWalker) !?*parser.Node {
pub fn _parentNode(self: *TreeWalker) !?NodeUnion {
if (try self.parentNode(self.current_node)) |parent| {
self.current_node = parent;
return parent;
return try Node.toInterface(parent);
}
return null;
}
pub fn _previousNode(self: *TreeWalker) !?*parser.Node {
pub fn _previousNode(self: *TreeWalker) !?NodeUnion {
if (self.current_node == self.root) return null;
var current = self.current_node;
while (try parser.nodePreviousSibling(current)) |previous| {
current = previous;
switch (try self.verify(current)) {
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => {
// Get last child if it has one.
if (try self.lastChild(current)) |child| {
self.current_node = child;
return child;
return try Node.toInterface(child);
}
// Otherwise, this node is our previous one.
self.current_node = current;
return current;
return try Node.toInterface(current);
},
.reject => continue,
.skip => {
// Get last child if it has one.
if (try self.lastChild(current)) |child| {
self.current_node = child;
return child;
return try Node.toInterface(child);
}
},
}
@@ -275,17 +263,17 @@ pub const TreeWalker = struct {
if (current != self.root) {
if (try self.parentNode(current)) |parent| {
self.current_node = parent;
return parent;
return try Node.toInterface(parent);
}
}
return null;
}
pub fn _previousSibling(self: *TreeWalker) !?*parser.Node {
pub fn _previousSibling(self: *TreeWalker) !?NodeUnion {
if (try self.previousSibling(self.current_node)) |sibling| {
self.current_node = sibling;
return sibling;
return try Node.toInterface(sibling);
}
return null;

View File

@@ -19,17 +19,25 @@
const std = @import("std");
const parser = @import("netsurf.zig");
const Page = @import("page.zig").Page;
const Walker = @import("dom/walker.zig").WalkerChildren;
pub const Opts = struct {
// set to include element shadowroots in the dump
page: ?*const Page = null,
exclude_scripts: bool = false,
};
// writer must be a std.io.Writer
pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {
pub fn writeHTML(doc: *parser.Document, opts: Opts, writer: *std.Io.Writer) !void {
try writer.writeAll("<!DOCTYPE html>\n");
try writeChildren(parser.documentToNode(doc), writer);
try writeChildren(parser.documentToNode(doc), opts, writer);
try writer.writeAll("\n");
}
// Spec: https://www.w3.org/TR/xml/#sec-prolog-dtd
pub fn writeDocType(doc_type: *parser.DocumentType, writer: anytype) !void {
pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !void {
try writer.writeAll("<!DOCTYPE ");
try writer.writeAll(try parser.documentTypeGetName(doc_type));
@@ -54,10 +62,15 @@ pub fn writeDocType(doc_type: *parser.DocumentType, writer: anytype) !void {
try writer.writeAll(">");
}
pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerror!void {
switch (try parser.nodeType(node)) {
.element => {
// open the tag
const tag_type = try parser.nodeHTMLGetTagType(node) orelse .undef;
if (opts.exclude_scripts and try isScriptOrRelated(tag_type, node)) {
return;
}
const tag = try parser.nodeLocalName(node);
try writer.writeAll("<");
try writer.writeAll(tag);
@@ -79,12 +92,24 @@ pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
try writer.writeAll(">");
if (opts.page) |page| {
if (page.getNodeState(node)) |state| {
if (state.shadow_root) |sr| {
try writeChildren(@ptrCast(@alignCast(sr.proto)), opts, writer);
}
}
}
// void elements can't have any content.
if (try isVoid(parser.nodeToElement(node))) return;
// write the children
// TODO avoid recursion
try writeChildren(node, writer);
if (tag_type == .script) {
try writer.writeAll(try parser.nodeTextContent(node) orelse "");
} else {
// write the children
// TODO avoid recursion
try writeChildren(node, opts, writer);
}
// close the tag
try writer.writeAll("</");
@@ -125,19 +150,38 @@ pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
}
// writer must be a std.io.Writer
pub fn writeChildren(root: *parser.Node, writer: anytype) !void {
pub fn writeChildren(root: *parser.Node, opts: Opts, writer: *std.Io.Writer) !void {
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(root, next) orelse break;
try writeNode(next.?, writer);
try writeNode(next.?, opts, writer);
}
}
// When `exclude_scripts` is passed to dump, we don't include <script> tags.
// We also want to omit <link rel=preload as=ascript>
fn isScriptOrRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
if (tag_type == .script) {
return true;
}
if (tag_type == .link) {
const el = parser.nodeToElement(node);
const as = try parser.elementGetAttribute(el, "as") orelse return false;
if (!std.ascii.eqlIgnoreCase(as, "script")) {
return false;
}
const rel = try parser.elementGetAttribute(el, "rel") orelse return false;
return std.ascii.eqlIgnoreCase(rel, "preload");
}
return false;
}
// area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
// https://html.spec.whatwg.org/#void-elements
fn isVoid(elem: *parser.Element) !bool {
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(elem)));
const tag = try parser.elementTag(elem);
return switch (tag) {
.area, .base, .br, .col, .embed, .hr, .img, .input, .link => true,
.meta, .source, .track, .wbr => true,
@@ -211,6 +255,11 @@ test "dump.writeHTML" {
\\</head><body>9000</body></html>
\\
, "<html><title>It's over what?</title><meta name=a value=\"b\">\n<body>9000");
try testWriteHTML(
"<p>hi</p><script>alert(power > 9000)</script>",
"<p>hi</p><script>alert(power > 9000)</script>",
);
}
fn testWriteHTML(comptime expected_body: []const u8, src: []const u8) !void {
@@ -222,13 +271,13 @@ fn testWriteHTML(comptime expected_body: []const u8, src: []const u8) !void {
}
fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
var buf = std.ArrayListUnmanaged(u8){};
defer buf.deinit(testing.allocator);
var aw = std.Io.Writer.Allocating.init(testing.allocator);
defer aw.deinit();
const doc_html = try parser.documentHTMLParseFromStr(src);
defer parser.documentHTMLClose(doc_html) catch {};
const doc = parser.documentHTMLToDocument(doc_html);
try writeHTML(doc, buf.writer(testing.allocator));
try testing.expectEqualStrings(expected, buf.items);
try writeHTML(doc, .{}, &aw.writer);
try testing.expectEqualStrings(expected, aw.written());
}

View File

@@ -0,0 +1,84 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const Env = @import("../env.zig").Env;
// https://encoding.spec.whatwg.org/#interface-textdecoder
const TextDecoder = @This();
const SupportedLabels = enum {
utf8,
@"utf-8",
@"unicode-1-1-utf-8",
};
const Options = struct {
fatal: bool = false,
ignoreBOM: bool = false,
};
fatal: bool,
ignore_bom: bool,
pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
if (label_) |l| {
_ = std.meta.stringToEnum(SupportedLabels, l) orelse {
log.warn(.web_api, "not implemented", .{ .feature = "TextDecoder label", .label = l });
return error.NotImplemented;
};
}
const opts = opts_ orelse Options{};
return .{
.fatal = opts.fatal,
.ignore_bom = opts.ignoreBOM,
};
}
pub fn get_encoding(_: *const TextDecoder) []const u8 {
return "utf-8";
}
pub fn get_ignoreBOM(self: *const TextDecoder) bool {
return self.ignore_bom;
}
pub fn get_fatal(self: *const TextDecoder) bool {
return self.fatal;
}
// TODO: Should accept an ArrayBuffer, TypedArray or DataView
// js.zig will currently only map a TypedArray to our []const u8.
pub fn _decode(self: *const TextDecoder, v: []const u8) ![]const u8 {
if (self.fatal and !std.unicode.utf8ValidateSlice(v)) {
return error.InvalidUtf8;
}
if (self.ignore_bom == false and std.mem.startsWith(u8, v, &.{ 0xEF, 0xBB, 0xBF })) {
return v[3..];
}
return v;
}
const testing = @import("../../testing.zig");
test "Browser: Encoding.TextDecoder" {
try testing.htmlRunner("encoding/decoder.html");
}

View File

@@ -0,0 +1,48 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Env = @import("../env.zig").Env;
// https://encoding.spec.whatwg.org/#interface-textencoder
const TextEncoder = @This();
pub fn constructor() !TextEncoder {
return .{};
}
pub fn get_encoding(_: *const TextEncoder) []const u8 {
return "utf-8";
}
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
// Ensure the input is a valid utf-8
// It seems chrome accepts invalid utf-8 sequence.
//
if (!std.unicode.utf8ValidateSlice(v)) {
return error.InvalidUtf8;
}
return .{ .values = v };
}
const testing = @import("../../testing.zig");
test "Browser: Encoding.TextEncoder" {
try testing.htmlRunner("encoding/encoder.html");
}

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -16,8 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const CustomElementRegistry = @import("custom_element_registry.zig").CustomElementRegistry;
pub const Interfaces = .{
CustomElementRegistry,
@import("TextDecoder.zig"),
@import("TextEncoder.zig"),
};

View File

@@ -1,64 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Env = @import("../env.zig").Env;
pub const Interfaces = .{
TextEncoder,
};
// https://encoding.spec.whatwg.org/#interface-textencoder
pub const TextEncoder = struct {
pub fn constructor() !TextEncoder {
return .{};
}
pub fn get_encoding(_: *const TextEncoder) []const u8 {
return "utf-8";
}
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
// Ensure the input is a valid utf-8
// It seems chrome accepts invalid utf-8 sequence.
//
if (!std.unicode.utf8ValidateSlice(v)) {
return error.InvalidUtf8;
}
return .{ .values = v };
}
};
const testing = @import("../../testing.zig");
test "Browser.Encoding.TextEncoder" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "var encoder = new TextEncoder();", "undefined" },
.{ "encoder.encoding;", "utf-8" },
.{ "encoder.encode('€');", "226,130,172" },
// Invalid utf-8 sequence.
// Result with chrome:
// .{ "encoder.encode(new Uint8Array([0xE2,0x28,0xA1]))", "50,50,54,44,52,48,44,49,54,49" },
.{ "try {encoder.encode(new Uint8Array([0xE2,0x28,0xA1])) } catch (e) { e };", "Error: InvalidUtf8" },
}, .{});
}

View File

@@ -22,9 +22,11 @@ const WebApis = struct {
pub const Interfaces = generate.Tuple(.{
@import("crypto/crypto.zig").Crypto,
@import("console/console.zig").Console,
@import("css/css.zig").Interfaces,
@import("cssom/cssom.zig").Interfaces,
@import("dom/dom.zig").Interfaces,
@import("encoding/text_encoder.zig").Interfaces,
@import("dom/shadow_root.zig").ShadowRoot,
@import("encoding/encoding.zig").Interfaces,
@import("events/event.zig").Interfaces,
@import("html/html.zig").Interfaces,
@import("iterator/iterator.zig").Interfaces,
@@ -32,13 +34,16 @@ const WebApis = struct {
@import("url/url.zig").Interfaces,
@import("xhr/xhr.zig").Interfaces,
@import("xhr/form_data.zig").Interfaces,
@import("xhr/File.zig"),
@import("xmlserializer/xmlserializer.zig").Interfaces,
@import("webcomponents/webcomponents.zig").Interfaces,
});
};
pub const JsThis = Env.JsThis;
pub const JsObject = Env.JsObject;
pub const Function = Env.Function;
pub const Promise = Env.Promise;
pub const PromiseResolver = Env.PromiseResolver;
pub const Env = js.Env(*Page, WebApis);
pub const Global = @import("html/window.zig").Window;

View File

@@ -19,6 +19,7 @@
const parser = @import("../netsurf.zig");
const Event = @import("event.zig").Event;
const JsObject = @import("../env.zig").JsObject;
const netsurf = @import("../netsurf.zig");
// https://dom.spec.whatwg.org/#interface-customevent
pub const CustomEvent = struct {
@@ -55,26 +56,30 @@ pub const CustomEvent = struct {
pub fn get_detail(self: *CustomEvent) ?JsObject {
return self.detail;
}
// Initializes an already created `CustomEvent`.
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/initCustomEvent
pub fn _initCustomEvent(
self: *CustomEvent,
event_type: []const u8,
can_bubble: bool,
cancelable: bool,
maybe_detail: ?JsObject,
) !void {
// This function can only be called after the constructor has called.
// So we assume proto is initialized already by constructor.
self.proto.type = try netsurf.strFromData(event_type);
self.proto.bubble = can_bubble;
self.proto.cancelable = cancelable;
self.proto.is_initialised = true;
// Detail is stored separately.
if (maybe_detail) |detail| {
self.detail = try detail.persist();
}
}
};
const testing = @import("../../testing.zig");
test "Browser.CustomEvent" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let capture = null", "undefined" },
.{ "const el = document.createElement('div');", "undefined" },
.{ "el.addEventListener('c1', (e) => { capture = 'c1-' + new String(e.detail)})", "undefined" },
.{ "el.addEventListener('c2', (e) => { capture = 'c2-' + new String(e.detail.over)})", "undefined" },
.{ "el.dispatchEvent(new CustomEvent('c1'));", "true" },
.{ "capture", "c1-null" },
.{ "el.dispatchEvent(new CustomEvent('c1', {detail: '123'}));", "true" },
.{ "capture", "c1-123" },
.{ "el.dispatchEvent(new CustomEvent('c2', {detail: {over: 9000}}));", "true" },
.{ "capture", "c2-9000" },
}, .{});
test "Browser: Events.Custom" {
try testing.htmlRunner("events/custom.html");
}

View File

@@ -24,21 +24,20 @@ const parser = @import("../netsurf.zig");
const generate = @import("../../runtime/generate.zig");
const Page = @import("../page.zig").Page;
const Node = @import("../dom/node.zig").Node;
const DOMException = @import("../dom/exceptions.zig").DOMException;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventTargetUnion = @import("../dom/event_target.zig").Union;
const AbortSignal = @import("../html/AbortController.zig").AbortSignal;
const CustomEvent = @import("custom_event.zig").CustomEvent;
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
const MouseEvent = @import("mouse_event.zig").MouseEvent;
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
// Event interfaces
pub const Interfaces = .{
Event,
CustomEvent,
ProgressEvent,
MouseEvent,
};
pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent };
pub const Union = generate.Union(Interfaces);
@@ -58,10 +57,12 @@ pub const Event = struct {
pub fn toInterface(evt: *parser.Event) !Union {
return switch (try parser.eventGetInternalType(evt)) {
.event => .{ .Event = evt },
.event, .abort_signal, .xhr_event => .{ .Event = evt },
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
};
}
@@ -109,8 +110,11 @@ pub const Event = struct {
return try parser.eventIsTrusted(self);
}
pub fn get_timestamp(self: *parser.Event) !u32 {
return try parser.eventTimestamp(self);
// Even though this is supposed to to provide microsecond resolution, browser
// return coarser values to protect against fingerprinting. libdom returns
// seconds, which is good enough.
pub fn get_timeStamp(self: *parser.Event) !u32 {
return parser.eventTimestamp(self);
}
// Methods
@@ -139,6 +143,64 @@ pub const Event = struct {
pub fn _preventDefault(self: *parser.Event) !void {
return try parser.eventPreventDefault(self);
}
pub fn _composedPath(self: *parser.Event, page: *Page) ![]const EventTargetUnion {
const et_ = try parser.eventTarget(self);
const et = et_ orelse return &.{};
var node: ?*parser.Node = switch (try parser.eventTargetInternalType(et)) {
.libdom_node => @as(*parser.Node, @ptrCast(et)),
.plain => parser.eventTargetToNode(et),
else => {
// Window, XHR, MessagePort, etc...no path beyond the event itself
return &.{try EventTarget.toInterface(et, page)};
},
};
const arena = page.call_arena;
var path: std.ArrayListUnmanaged(EventTargetUnion) = .empty;
while (node) |n| {
try path.append(arena, .{
.node = try Node.toInterface(n),
});
node = try parser.nodeParentNode(n);
if (node == null and try parser.nodeType(n) == .document_fragment) {
// we have a non-continuous hook from a shadowroot to its host (
// it's parent element). libdom doesn't really support ShdowRoots
// and, for the most part, that works out well since it naturally
// provides isolation. But events don't follow the same
// shadowroot isolation as most other things, so, if this is
// a parent-less document fragment, we need to check if it has
// a host.
if (parser.documentFragmentGetHost(@ptrCast(n))) |host| {
node = host;
// If a document fragment has a host, then that host
// _has_ to have a state and that state _has_ to have
// a shadow_root field. All of this is set in Element._attachShadow
if (page.getNodeState(host).?.shadow_root.?.mode == .closed) {
// if the shadow root is closed, then the composedPath
// starts at the host element.
path.clearRetainingCapacity();
}
} else {
// Our document fragement has no parent and no host, we
// can break out of the loop.
break;
}
}
}
if (path.getLastOrNull()) |last| {
// the Window isn't part of the DOM hierarchy, but for events, it
// is, so we need to glue it on.
if (last.node == .HTMLDocument and last.node.HTMLDocument == page.window.document) {
try path.append(arena, .{ .node = .{ .Window = &page.window } });
}
}
return path.items;
}
};
pub const EventHandler = struct {
@@ -178,7 +240,7 @@ pub const EventHandler = struct {
// that the listener won't call preventDefault() and thus can safely
// run the default as needed).
passive: ?bool,
signal: ?bool, // currently does nothing
signal: ?*AbortSignal, // currently does nothing
};
};
@@ -191,25 +253,42 @@ pub const EventHandler = struct {
) !?*EventHandler {
var once = false;
var capture = false;
var signal: ?*AbortSignal = null;
if (opts_) |opts| {
switch (opts) {
.capture => |c| capture = c,
.flags => |f| {
// Done this way so that, for common cases that _only_ set
// capture, i.e. {captrue: true}, it works.
// But for any case that sets any of the other flags, we
// error. If we don't error, this function call would succeed
// but the behavior might be wrong. At this point, it's
// better to be explicit and error.
if (f.signal orelse false) return error.NotImplemented;
once = f.once orelse false;
signal = f.signal orelse null;
capture = f.capture orelse false;
},
}
}
const callback = (try listener.callback(target)) orelse return null;
if (signal) |s| {
const signal_target = parser.toEventTarget(AbortSignal, s);
const scb = try allocator.create(SignalCallback);
scb.* = .{
.target = target,
.capture = capture,
.callback_id = callback.id,
.typ = try allocator.dupe(u8, typ),
.signal_target = signal_target,
.signal_listener = undefined,
.node = .{ .func = SignalCallback.handle },
};
scb.signal_listener = try parser.eventTargetAddEventListener(
signal_target,
"abort",
&scb.node,
false,
);
}
// check if event target has already this listener
if (try parser.eventTargetHasListener(target, typ, capture, callback.id) != null) {
return null;
@@ -265,110 +344,51 @@ pub const EventHandler = struct {
}
};
const SignalCallback = struct {
typ: []const u8,
capture: bool,
callback_id: usize,
node: parser.EventNode,
target: *parser.EventTarget,
signal_target: *parser.EventTarget,
signal_listener: *parser.EventListener,
fn handle(node: *parser.EventNode, _: *parser.Event) void {
const self: *SignalCallback = @fieldParentPtr("node", node);
self._handle() catch |err| {
log.err(.app, "event signal handler", .{ .err = err });
};
}
fn _handle(self: *SignalCallback) !void {
const lst = try parser.eventTargetHasListener(
self.target,
self.typ,
self.capture,
self.callback_id,
);
if (lst == null) {
return;
}
try parser.eventTargetRemoveEventListener(
self.target,
self.typ,
lst.?,
self.capture,
);
// remove the abort signal listener itself
try parser.eventTargetRemoveEventListener(
self.signal_target,
"abort",
self.signal_listener,
false,
);
}
};
const testing = @import("../../testing.zig");
test "Browser.Event" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let content = document.getElementById('content')", "undefined" },
.{ "let para = document.getElementById('para')", "undefined" },
.{ "var nb = 0; var evt", "undefined" },
}, .{});
try runner.testCases(&.{
.{
\\ content.addEventListener('target', function(e) {
\\ evt = e; nb = nb + 1;
\\ e.preventDefault();
\\ })
,
"undefined",
},
.{ "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", "false" },
.{ "nb", "1" },
.{ "evt.target === content", "true" },
.{ "evt.bubbles", "true" },
.{ "evt.cancelable", "true" },
.{ "evt.defaultPrevented", "true" },
.{ "evt.isTrusted", "true" },
.{ "evt.timestamp > 1704063600", "true" }, // 2024/01/01 00:00
// event.type, event.currentTarget, event.phase checked in EventTarget
}, .{});
try runner.testCases(&.{
.{ "nb = 0", "0" },
.{
\\ content.addEventListener('stop',function(e) {
\\ e.stopPropagation();
\\ nb = nb + 1;
\\ }, true)
,
"undefined",
},
// the following event listener will not be invoked
.{
\\ para.addEventListener('stop',function(e) {
\\ nb = nb + 1;
\\ })
,
"undefined",
},
.{ "para.dispatchEvent(new Event('stop'))", "true" },
.{ "nb", "1" }, // will be 2 if event was not stopped at content event listener
}, .{});
try runner.testCases(&.{
.{ "nb = 0", "0" },
.{
\\ content.addEventListener('immediate', function(e) {
\\ e.stopImmediatePropagation();
\\ nb = nb + 1;
\\ })
,
"undefined",
},
// the following event listener will not be invoked
.{
\\ content.addEventListener('immediate', function(e) {
\\ nb = nb + 1;
\\ })
,
"undefined",
},
.{ "content.dispatchEvent(new Event('immediate'))", "true" },
.{ "nb", "1" }, // will be 2 if event was not stopped at first content event listener
}, .{});
try runner.testCases(&.{
.{ "nb = 0", "0" },
.{
\\ content.addEventListener('legacy', function(e) {
\\ evt = e; nb = nb + 1;
\\ })
,
"undefined",
},
.{ "let evtLegacy = document.createEvent('Event')", "undefined" },
.{ "evtLegacy.initEvent('legacy')", "undefined" },
.{ "content.dispatchEvent(evtLegacy)", "true" },
.{ "nb", "1" },
}, .{});
try runner.testCases(&.{
.{ "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", "undefined" },
.{ "document.addEventListener('count', cbk)", "undefined" },
.{ "document.removeEventListener('count', cbk)", "undefined" },
.{ "document.dispatchEvent(new Event('count'))", "true" },
.{ "nb", "0" },
}, .{});
try runner.testCases(&.{
.{ "nb = 0; function cbk(event) { nb ++; }", null },
.{ "document.addEventListener('count', cbk, {once: true})", null },
.{ "document.dispatchEvent(new Event('count'))", "true" },
.{ "document.dispatchEvent(new Event('count'))", "true" },
.{ "document.dispatchEvent(new Event('count'))", "true" },
.{ "nb", "1" },
}, .{});
test "Browser: Event" {
try testing.htmlRunner("events/event.html");
}

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = std.log.scoped(.mouse_event);
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Event = @import("event.zig").Event;
@@ -68,10 +68,6 @@ pub const MouseEvent = struct {
.button = @intFromEnum(opts.button),
});
if (!std.mem.eql(u8, event_type, "click")) {
log.warn("MouseEvent currently only supports listeners for 'click' events!", .{});
}
return mouse_event;
}
@@ -107,34 +103,6 @@ pub const MouseEvent = struct {
};
const testing = @import("../../testing.zig");
test "Browser.MouseEvent" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
// Default MouseEvent
.{ "let event = new MouseEvent('click')", "undefined" },
.{ "event.type", "click" },
.{ "event instanceof MouseEvent", "true" },
.{ "event instanceof Event", "true" },
.{ "event.clientX", "0" },
.{ "event.clientY", "0" },
.{ "event.screenX", "0" },
.{ "event.screenY", "0" },
// MouseEvent with parameters
.{ "let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20 })", "undefined" },
.{ "new_event.button", "0" },
.{ "new_event.x", "10" },
.{ "new_event.y", "20" },
.{ "new_event.screenX", "10" },
.{ "new_event.screenY", "20" },
// MouseEvent Listener
.{ "let me = new MouseEvent('click')", "undefined" },
.{ "me instanceof Event", "true" },
.{ "var eevt = null; function ccbk(event) { eevt = event; }", "undefined" },
.{ "document.addEventListener('click', ccbk)", "undefined" },
.{ "document.dispatchEvent(me)", "true" },
.{ "eevt.type", "click" },
.{ "eevt instanceof MouseEvent", "true" },
}, .{});
test "Browser: Events.Mouse" {
try testing.htmlRunner("events/mouse.html");
}

View File

@@ -0,0 +1,181 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
pub const Interfaces = .{
AbortController,
AbortSignal,
};
const AbortController = @This();
signal: *AbortSignal,
pub fn constructor(page: *Page) !AbortController {
// Why do we allocate this rather than storing directly in the struct?
// https://github.com/lightpanda-io/project/discussions/165
const signal = try page.arena.create(AbortSignal);
signal.* = .init;
return .{
.signal = signal,
};
}
pub fn get_signal(self: *AbortController) *AbortSignal {
return self.signal;
}
pub fn _abort(self: *AbortController, reason_: ?[]const u8) !void {
return self.signal.abort(reason_);
}
pub const AbortSignal = struct {
const DEFAULT_REASON = "AbortError";
pub const prototype = *EventTarget;
proto: parser.EventTargetTBase = .{ .internal_target_type = .abort_signal },
aborted: bool,
reason: ?[]const u8,
pub const init: AbortSignal = .{
.reason = null,
.aborted = false,
};
pub fn static_abort(reason_: ?[]const u8) AbortSignal {
return .{
.aborted = true,
.reason = reason_ orelse DEFAULT_REASON,
};
}
pub fn static_timeout(delay: u32, page: *Page) !*AbortSignal {
const callback = try page.arena.create(TimeoutCallback);
callback.* = .{
.signal = .init,
};
try page.scheduler.add(callback, TimeoutCallback.run, delay, .{ .name = "abort_signal" });
return &callback.signal;
}
pub fn get_aborted(self: *const AbortSignal) bool {
return self.aborted;
}
fn abort(self: *AbortSignal, reason_: ?[]const u8) !void {
self.aborted = true;
self.reason = reason_ orelse DEFAULT_REASON;
const abort_event = try parser.eventCreate();
try parser.eventSetInternalType(abort_event, .abort_signal);
defer parser.eventDestroy(abort_event);
try parser.eventInit(abort_event, "abort", .{});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(AbortSignal, self),
abort_event,
);
}
const Reason = union(enum) {
reason: []const u8,
undefined: void,
};
pub fn get_reason(self: *const AbortSignal) Reason {
if (self.reason) |r| {
return .{ .reason = r };
}
return .{ .undefined = {} };
}
const ThrowIfAborted = union(enum) {
exception: Env.Exception,
undefined: void,
};
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
if (self.aborted) {
const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON);
return .{ .exception = ex };
}
return .{ .undefined = {} };
}
};
const TimeoutCallback = struct {
signal: AbortSignal,
fn run(ctx: *anyopaque) ?u32 {
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
self.signal.abort("TimeoutError") catch |err| {
log.warn(.app, "abort signal timeout", .{ .err = err });
};
return null;
}
};
const testing = @import("../../testing.zig");
test "Browser.HTML.AbortController" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "var called = 0", null },
.{ "var a1 = new AbortController()", null },
.{ "var s1 = a1.signal", null },
.{ "s1.throwIfAborted()", "undefined" },
.{ "s1.reason", "undefined" },
.{ "var target;", null },
.{
\\ s1.addEventListener('abort', (e) => {
\\ called += 1;
\\ target = e.target;
\\
\\ });
,
null,
},
.{ "a1.abort()", null },
.{ "s1.aborted", "true" },
.{ "target == s1", "true" },
.{ "s1.reason", "AbortError" },
.{ "called", "1" },
}, .{});
try runner.testCases(&.{
.{ "var s2 = AbortSignal.abort('over 9000')", null },
.{ "s2.aborted", "true" },
.{ "s2.reason", "over 9000" },
.{ "AbortSignal.abort().reason", "AbortError" },
}, .{});
try runner.testCases(&.{
.{ "var s3 = AbortSignal.timeout(10)", null },
.{ "s3.aborted", "true" },
.{ "s3.reason", "TimeoutError" },
.{ "try { s3.throwIfAborted() } catch (e) { e }", "Error: TimeoutError" },
}, .{});
}

View File

@@ -0,0 +1,97 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const Allocator = std.mem.Allocator;
const DataSet = @This();
element: *parser.Element,
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !Env.UndefinedOr([]const u8) {
const normalized_name = try normalize(page.call_arena, name);
if (try parser.elementGetAttribute(self.element, normalized_name)) |value| {
return .{ .value = value };
}
return .undefined;
}
pub fn named_set(self: *DataSet, name: []const u8, value: []const u8, _: *bool, page: *Page) !void {
const normalized_name = try normalize(page.call_arena, name);
try parser.elementSetAttribute(self.element, normalized_name, value);
}
pub fn named_delete(self: *DataSet, name: []const u8, _: *bool, page: *Page) !void {
const normalized_name = try normalize(page.call_arena, name);
try parser.elementRemoveAttribute(self.element, normalized_name);
}
fn normalize(allocator: Allocator, name: []const u8) ![]const u8 {
var upper_count: usize = 0;
for (name) |c| {
if (std.ascii.isUpper(c)) {
upper_count += 1;
}
}
// for every upper-case letter, we'll probably need a dash before it
// and we need the 'data-' prefix
var normalized = try allocator.alloc(u8, name.len + upper_count + 5);
@memcpy(normalized[0..5], "data-");
if (upper_count == 0) {
@memcpy(normalized[5..], name);
return normalized;
}
var pos: usize = 5;
for (name) |c| {
if (std.ascii.isUpper(c)) {
normalized[pos] = '-';
pos += 1;
normalized[pos] = c + 32;
} else {
normalized[pos] = c;
}
pos += 1;
}
return normalized;
}
const testing = @import("../../testing.zig");
test "Browser.HTML.DataSet" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
defer runner.deinit();
try runner.testCases(&.{
.{ "let el1 = document.createElement('div')", null },
.{ "el1.dataset.x", "undefined" },
.{ "el1.dataset.x = '123'", "123" },
.{ "delete el1.dataset.x", "true" },
.{ "el1.dataset.x", "undefined" },
.{ "delete el1.dataset.other", "true" }, // yes, this is right
.{ "let ds1 = el1.dataset", null },
.{ "ds1.helloWorld = 'yes'", null },
.{ "el1.getAttribute('data-hello-world')", "yes" },
.{ "el1.setAttribute('data-this-will-work', 'positive')", null },
.{ "ds1.thisWillWork", "positive" },
}, .{});
}

View File

@@ -42,8 +42,12 @@ pub const HTMLDocument = struct {
// JS funcs
// --------
pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 {
return try parser.documentHTMLGetDomain(self);
pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
// libdom's document_html get_domain always returns null, this is
// the way MDN recommends getting the domain anyways, since document.domain
// is deprecated.
const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
return location.get_host(page);
}
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
@@ -81,7 +85,10 @@ pub const HTMLDocument = struct {
pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 {
var buf: std.ArrayListUnmanaged(u8) = .{};
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true, .is_http = false });
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{
.is_http = false,
.is_navigation = true,
});
return buf.items;
}
@@ -114,7 +121,9 @@ pub const HTMLDocument = struct {
if (name.len == 0) return list;
const root = parser.documentHTMLToNode(self);
var c = try collection.HTMLCollectionByName(arena, root, name, false);
var c = try collection.HTMLCollectionByName(arena, root, name, .{
.include_root = false,
});
const ln = try c.get_length();
var i: u32 = 0;
@@ -128,11 +137,15 @@ pub const HTMLDocument = struct {
}
pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", false);
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", .{
.include_root = false,
});
}
pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", false);
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", .{
.include_root = false,
});
}
pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
@@ -140,11 +153,15 @@ pub const HTMLDocument = struct {
}
pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", false);
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", .{
.include_root = false,
});
}
pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", false);
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", .{
.include_root = false,
});
}
pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection {
@@ -152,11 +169,15 @@ pub const HTMLDocument = struct {
}
pub fn get_links(self: *parser.DocumentHTML) !collection.HTMLCollection {
return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), false);
return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{
.include_root = false,
});
}
pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection {
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), false);
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{
.include_root = false,
});
}
pub fn get_all(self: *parser.DocumentHTML) collection.HTMLAllCollection {
@@ -188,7 +209,7 @@ pub const HTMLDocument = struct {
}
pub fn get_readyState(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
return @tagName(state.ready_state);
}
@@ -233,19 +254,23 @@ pub const HTMLDocument = struct {
// Since LightPanda requires the client to know what they are clicking on we do not return the underlying element at this moment
// This can currenty only happen if the first pixel is clicked without having rendered any element. This will change when css properties are supported.
// This returns an ElementUnion instead of a *Parser.Element in case the element somehow hasn't passed through the js runtime yet.
pub fn _elementFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, page: *Page) !?ElementUnion {
const ix: i32 = @intFromFloat(@floor(x));
const iy: i32 = @intFromFloat(@floor(y));
const element = page.renderer.getElementAtPosition(ix, iy) orelse return null;
// While x and y should be f32, here we take i32 since that's what our
// "renderer" uses. By specifying i32 here, rather than f32 and doing the
// conversion ourself, we rely on v8's type conversion which is both more
// flexible (e.g. handles NaN) and will be more consistent with a browser.
pub fn _elementFromPoint(_: *parser.DocumentHTML, x: i32, y: i32, page: *Page) !?ElementUnion {
const element = page.renderer.getElementAtPosition(x, y) orelse return null;
// TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?)
return try Element.toInterface(element);
}
// Returns an array of all elements at the specified coordinates (relative to the viewport). The elements are ordered from the topmost to the bottommost box of the viewport.
pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, page: *Page) ![]ElementUnion {
const ix: i32 = @intFromFloat(@floor(x));
const iy: i32 = @intFromFloat(@floor(y));
const element = page.renderer.getElementAtPosition(ix, iy) orelse return &.{};
// While x and y should be f32, here we take i32 since that's what our
// "renderer" uses. By specifying i32 here, rather than f32 and doing the
// conversion ourself, we rely on v8's type conversion which is both more
// flexible (e.g. handles NaN) and will be more consistent with a browser.
pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: i32, y: i32, page: *Page) ![]ElementUnion {
const element = page.renderer.getElementAtPosition(x, y) orelse return &.{};
// TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?)
var list: std.ArrayListUnmanaged(ElementUnion) = .empty;
@@ -267,22 +292,24 @@ pub const HTMLDocument = struct {
}
pub fn documentIsLoaded(self: *parser.DocumentHTML, page: *Page) !void {
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.ready_state = .interactive;
const evt = try parser.eventCreate();
defer parser.eventDestroy(evt);
log.debug(.script_event, "dispatch event", .{
.type = "DOMContentLoaded",
.source = "document",
});
const evt = try parser.eventCreate();
defer parser.eventDestroy(evt);
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, self), evt);
try page.window.dispatchForDocumentTarget(evt);
}
pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void {
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.ready_state = .complete;
}
};
@@ -303,7 +330,7 @@ test "Browser.HTML.Document" {
}, .{});
try runner.testCases(&.{
.{ "document.domain", "" },
.{ "document.domain", "lightpanda.io" },
.{ "document.referrer", "" },
.{ "document.title", "" },
.{ "document.body.localName", "body" },

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const Env = @import("../env.zig").Env;
const parser = @import("../netsurf.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
pub const ErrorEvent = struct {
pub const prototype = *parser.Event;
pub const union_make_copy = true;
proto: parser.Event,
message: []const u8,
filename: []const u8,
lineno: i32,
colno: i32,
@"error": ?Env.JsObject,
const ErrorEventInit = struct {
message: []const u8 = "",
filename: []const u8 = "",
lineno: i32 = 0,
colno: i32 = 0,
@"error": ?Env.JsObject = null,
};
pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
try parser.eventSetInternalType(event, .event);
const o = opts orelse ErrorEventInit{};
return .{
.proto = event.*,
.message = o.message,
.filename = o.filename,
.lineno = o.lineno,
.colno = o.colno,
.@"error" = if (o.@"error") |e| try e.persist() else null,
};
}
pub fn get_message(self: *const ErrorEvent) []const u8 {
return self.message;
}
pub fn get_filename(self: *const ErrorEvent) []const u8 {
return self.filename;
}
pub fn get_lineno(self: *const ErrorEvent) i32 {
return self.lineno;
}
pub fn get_colno(self: *const ErrorEvent) i32 {
return self.colno;
}
pub fn get_error(self: *const ErrorEvent) Env.UndefinedOr(Env.JsObject) {
if (self.@"error") |e| {
return .{ .value = e };
}
return .undefined;
}
};
const testing = @import("../../testing.zig");
test "Browser: HTML.ErrorEvent" {
try testing.htmlRunner("html/error_event.html");
}

View File

@@ -36,5 +36,8 @@ pub const Interfaces = .{
History,
Location,
MediaQueryList,
@import("DataSet.zig"),
@import("screen.zig").Interfaces,
@import("error_event.zig").ErrorEvent,
@import("AbortController.zig").Interfaces,
};

View File

@@ -0,0 +1,30 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const HTMLElement = @import("elements.zig").HTMLElement;
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#htmliframeelement
pub const HTMLIFrameElement = struct {
pub const Self = parser.IFrame;
pub const prototype = *HTMLElement;
pub const subtype = .node;
};

View File

@@ -26,7 +26,7 @@ pub const MediaQueryList = struct {
// Extend libdom event target for pure zig struct.
// This is not safe as it relies on a structure layout that isn't guaranteed
base: parser.EventTargetTBase = parser.EventTargetTBase{},
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .media_query_list },
matches: bool,
media: []const u8,

View File

@@ -18,6 +18,7 @@
const std = @import("std");
const parser = @import("../netsurf.zig");
const EventTarget = @import("../dom/event_target.zig").EventTarget;
pub const Interfaces = .{
@@ -29,6 +30,8 @@ pub const Interfaces = .{
pub const Screen = struct {
pub const prototype = *EventTarget;
proto: parser.EventTargetTBase = .{ .internal_target_type = .screen },
height: u32 = 1080,
width: u32 = 1920,
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/colorDepth
@@ -83,6 +86,7 @@ pub const ScreenOrientation = struct {
angle: u32 = 0,
type: ScreenOrientationType,
proto: parser.EventTargetTBase = .{ .internal_target_type = .screen_orientation },
pub fn get_angle(self: *const ScreenOrientation) u32 {
return self.angle;
@@ -94,16 +98,6 @@ pub const ScreenOrientation = struct {
};
const testing = @import("../../testing.zig");
test "Browser.HTML.Screen" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let screen = window.screen", "undefined" },
.{ "screen.width === 1920", "true" },
.{ "screen.height === 1080", "true" },
.{ "let orientation = screen.orientation", "undefined" },
.{ "orientation.angle === 0", "true" },
.{ "orientation.type === \"landscape-primary\"", "true" },
}, .{});
test "Browser: HTML.Screen" {
try testing.htmlRunner("html/screen.html");
}

View File

@@ -18,8 +18,16 @@
const std = @import("std");
const parser = @import("../netsurf.zig");
const HTMLElement = @import("elements.zig").HTMLElement;
const collection = @import("../dom/html_collection.zig");
const Page = @import("../page.zig").Page;
const HTMLElement = @import("elements.zig").HTMLElement;
pub const Interfaces = .{
HTMLSelectElement,
HTMLOptionElement,
HTMLOptionsCollection,
};
pub const HTMLSelectElement = struct {
pub const Self = parser.Select;
@@ -56,7 +64,7 @@ pub const HTMLSelectElement = struct {
}
pub fn get_selectedIndex(select: *parser.Select, page: *Page) !i32 {
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(select)));
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(select)));
const selected_index = try parser.selectGetSelectedIndex(select);
// See the explicit_index_set field documentation
@@ -75,7 +83,7 @@ pub const HTMLSelectElement = struct {
// Libdom's dom_html_select_select_set_selected_index will crash if index
// is out of range, and it doesn't properly unset options
pub fn set_selectedIndex(select: *parser.Select, index: i32, page: *Page) !void {
var state = try page.getOrCreateNodeState(@alignCast(@ptrCast(select)));
var state = try page.getOrCreateNodeState(@ptrCast(@alignCast(select)));
state.explicit_index_set = true;
const options = try parser.selectGetOptions(select);
@@ -89,6 +97,105 @@ pub const HTMLSelectElement = struct {
try parser.optionSetSelected(option, true);
}
}
pub fn get_options(select: *parser.Select) HTMLOptionsCollection {
return .{
.select = select,
.proto = collection.HTMLCollectionChildren(@ptrCast(@alignCast(select)), .{
.mutable = true,
.include_root = false,
}),
};
}
};
pub const HTMLOptionElement = struct {
pub const Self = parser.Option;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn get_value(self: *parser.Option) ![]const u8 {
return parser.optionGetValue(self);
}
pub fn set_value(self: *parser.Option, value: []const u8) !void {
return parser.optionSetValue(self, value);
}
pub fn get_label(self: *parser.Option) ![]const u8 {
return parser.optionGetLabel(self);
}
pub fn set_label(self: *parser.Option, label: []const u8) !void {
return parser.optionSetLabel(self, label);
}
pub fn get_selected(self: *parser.Option) !bool {
return parser.optionGetSelected(self);
}
pub fn set_selected(self: *parser.Option, value: bool) !void {
return parser.optionSetSelected(self, value);
}
pub fn get_disabled(self: *parser.Option) !bool {
return parser.optionGetDisabled(self);
}
pub fn set_disabled(self: *parser.Option, value: bool) !void {
return parser.optionSetDisabled(self, value);
}
pub fn get_text(self: *parser.Option) ![]const u8 {
return parser.optionGetText(self);
}
pub fn get_form(self: *parser.Option) !?*parser.Form {
return parser.optionGetForm(self);
}
};
pub const HTMLOptionsCollection = struct {
pub const prototype = *collection.HTMLCollection;
proto: collection.HTMLCollection,
select: *parser.Select,
pub fn get_selectedIndex(self: *HTMLOptionsCollection, page: *Page) !i32 {
return HTMLSelectElement.get_selectedIndex(self.select, page);
}
pub fn set_selectedIndex(self: *HTMLOptionsCollection, index: i32, page: *Page) !void {
return HTMLSelectElement.set_selectedIndex(self.select, index, page);
}
const BeforeOpts = union(enum) {
index: u32,
option: *parser.Option,
};
pub fn _add(self: *HTMLOptionsCollection, option: *parser.Option, before_: ?BeforeOpts) !void {
const Node = @import("../dom/node.zig").Node;
const before = before_ orelse {
return self.appendOption(option);
};
const insert_before: *parser.Node = switch (before) {
.option => |o| @ptrCast(@alignCast(o)),
.index => |i| (try self.proto.item(i)) orelse return self.appendOption(option),
};
return Node.before(insert_before, &.{
.{ .node = @ptrCast(@alignCast(option)) },
});
}
pub fn _remove(self: *HTMLOptionsCollection, index: u32) !void {
const Node = @import("../dom/node.zig").Node;
const option = (try self.proto.item(index)) orelse return;
_ = try Node._removeChild(@ptrCast(@alignCast(self.select)), option);
}
fn appendOption(self: *HTMLOptionsCollection, option: *parser.Option) !void {
const Node = @import("../dom/node.zig").Node;
return Node.append(@ptrCast(@alignCast(self.select)), &.{
.{ .node = @ptrCast(@alignCast(option)) },
});
}
};
const testing = @import("../../testing.zig");
@@ -140,5 +247,32 @@ test "Browser.HTML.Select" {
.{ "s.selectedIndex = -323", null },
.{ "s.selectedIndex", "-1" },
.{ "let options = s.options", null },
.{ "options.length", "2" },
.{ "options.item(1).value", "o2" },
.{ "options.selectedIndex", "-1" },
.{ "let o3 = document.createElement('option');", null },
.{ "o3.value = 'o3';", null },
.{ "options.add(o3)", null },
.{ "options.length", "3" },
.{ "options.item(2).value", "o3" },
.{ "let o4 = document.createElement('option');", null },
.{ "o4.value = 'o4';", null },
.{ "options.add(o4, 1)", null },
.{ "options.length", "4" },
.{ "options.item(1).value", "o4" },
.{ "let o5 = document.createElement('option');", null },
.{ "o5.value = 'o5';", null },
.{ "options.add(o5, o3)", null },
.{ "options.length", "5" },
.{ "options.item(3).value", "o5" },
.{ "options.remove(3)", null },
.{ "options.length", "4" },
.{ "options.item(3).value", "o3" },
}, .{});
}

View File

@@ -20,9 +20,8 @@ const std = @import("std");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Function = @import("../env.zig").Function;
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const Loop = @import("../../runtime/loop.zig").Loop;
const Navigator = @import("navigator.zig").Navigator;
const History = @import("history.zig").History;
@@ -32,9 +31,13 @@ const Console = @import("../console/console.zig").Console;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
const Performance = @import("../dom/performance.zig").Performance;
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
const CustomElementRegistry = @import("../webcomponents/custom_element_registry.zig").CustomElementRegistry;
const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
const Screen = @import("screen.zig").Screen;
const domcss = @import("../dom/css.zig");
const Css = @import("../css/css.zig").Css;
const Function = Env.Function;
const JsObject = Env.JsObject;
const storage = @import("../storage/storage.zig");
@@ -44,7 +47,7 @@ pub const Window = struct {
pub const prototype = *EventTarget;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{},
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .window },
document: *parser.DocumentHTML,
target: []const u8 = "",
@@ -54,14 +57,14 @@ pub const Window = struct {
// counter for having unique timer ids
timer_id: u30 = 0,
timers: std.AutoHashMapUnmanaged(u32, *TimerCallback) = .{},
timers: std.AutoHashMapUnmanaged(u32, void) = .{},
crypto: Crypto = .{},
console: Console = .{},
navigator: Navigator = .{},
performance: Performance,
custom_elements: CustomElementRegistry = .{},
screen: Screen = .{},
css: Css = .{},
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
var fbs = std.io.fixedBufferStream("");
@@ -73,7 +76,7 @@ pub const Window = struct {
.document = html_doc,
.target = target orelse "",
.navigator = navigator orelse .{},
.performance = .{ .time_origin = try std.time.Timer.start() },
.performance = Performance.init(),
};
}
@@ -83,7 +86,7 @@ pub const Window = struct {
}
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
self.performance.time_origin.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
self.document = doc;
try parser.documentHTMLSetLocation(Location, doc, &self.location);
}
@@ -124,7 +127,43 @@ pub const Window = struct {
return self;
}
// TODO: frames
// frames return the window itself, but accessing it via a pseudo
// array returns the Window object corresponding to the given frame or
// iframe.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/frames
pub fn get_frames(self: *Window) *Window {
return self;
}
pub fn indexed_get(self: *Window, index: u32, has_value: *bool, page: *Page) !*Window {
const frames = try domcss.querySelectorAll(
page.call_arena,
parser.documentHTMLToNode(self.document),
"iframe",
);
if (index >= frames.nodes.items.len) {
has_value.* = false;
return undefined;
}
has_value.* = true;
// TODO return the correct frame's window
// frames.nodes.items[indexed]
return error.TODO;
}
// Retrieve the numbre of frames/iframes from the DOM dynamically.
pub fn get_length(self: *const Window, page: *Page) !u32 {
const frames = try domcss.querySelectorAll(
page.call_arena,
parser.documentHTMLToNode(self.document),
"iframe",
);
return frames.get_length();
}
pub fn get_top(self: *Window) *Window {
return self;
}
@@ -167,41 +206,44 @@ pub const Window = struct {
return &self.performance;
}
pub fn get_customElements(self: *Window) *CustomElementRegistry {
return &self.custom_elements;
}
pub fn get_screen(self: *Window) *Screen {
return &self.screen;
}
pub fn get_CSS(self: *Window) *Css {
return &self.css;
}
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
return self.createTimeout(cbk, 5, page, .{ .animation_frame = true });
return self.createTimeout(cbk, 5, page, .{
.animation_frame = true,
.name = "animationFrame",
.low_priority = true,
});
}
pub fn _cancelAnimationFrame(self: *Window, id: u32, page: *Page) !void {
const kv = self.timers.fetchRemove(id) orelse return;
return page.loop.cancel(kv.value.loop_id);
pub fn _cancelAnimationFrame(self: *Window, id: u32) !void {
_ = self.timers.remove(id);
}
// TODO handle callback arguments.
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{});
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
}
// TODO handle callback arguments.
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .repeat = true });
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
}
pub fn _clearTimeout(self: *Window, id: u32, page: *Page) !void {
const kv = self.timers.fetchRemove(id) orelse return;
return page.loop.cancel(kv.value.loop_id);
pub fn _clearTimeout(self: *Window, id: u32) !void {
_ = self.timers.remove(id);
}
pub fn _clearInterval(self: *Window, id: u32, page: *Page) !void {
const kv = self.timers.fetchRemove(id) orelse return;
return page.loop.cancel(kv.value.loop_id);
pub fn _clearInterval(self: *Window, id: u32) !void {
_ = self.timers.remove(id);
}
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
}
pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
@@ -211,20 +253,30 @@ pub const Window = struct {
};
}
pub fn _btoa(_: *const Window, value: []const u8, page: *Page) ![]const u8 {
const Encoder = std.base64.standard.Encoder;
const out = try page.call_arena.alloc(u8, Encoder.calcSize(value.len));
return Encoder.encode(out, value);
}
pub fn _atob(_: *const Window, value: []const u8, page: *Page) ![]const u8 {
const Decoder = std.base64.standard.Decoder;
const size = Decoder.calcSizeForSlice(value) catch return error.InvalidCharacterError;
const out = try page.call_arena.alloc(u8, size);
Decoder.decode(out, value) catch return error.InvalidCharacterError;
return out;
}
const CreateTimeoutOpts = struct {
name: []const u8,
args: []Env.JsObject = &.{},
repeat: bool = false,
animation_frame: bool = false,
low_priority: bool = false,
};
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, comptime opts: CreateTimeoutOpts) !u32 {
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
const delay = delay_ orelse 0;
if (delay > 5000) {
log.warn(.user_script, "long timeout ignored", .{ .delay = delay, .interval = opts.repeat });
// self.timer_id is u30, so the largest value we can generate is
// 1_073_741_824. Returning 2_000_000_000 makes sure that clients
// can call cancelTimer/cancelInterval without breaking anything.
return 2_000_000_000;
}
if (self.timers.count() > 512) {
return error.TooManyTimeout;
}
@@ -237,24 +289,36 @@ pub const Window = struct {
if (gop.found_existing) {
// this can only happen if we've created 2^31 timeouts.
return error.TooManyTimeout;
} else {
gop.value_ptr.* = {};
}
errdefer _ = self.timers.remove(timer_id);
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
const callback = try arena.create(TimerCallback);
const args = opts.args;
var persisted_args: []Env.JsObject = &.{};
if (args.len > 0) {
persisted_args = try page.arena.alloc(Env.JsObject, args.len);
for (args, persisted_args) |a, *ca| {
ca.* = try a.persist();
}
}
const callback = try arena.create(TimerCallback);
callback.* = .{
.cbk = cbk,
.loop_id = 0, // we're going to set this to a real value shortly
.window = self,
.timer_id = timer_id,
.node = .{ .func = TimerCallback.run },
.repeat = if (opts.repeat) delay_ms else null,
.args = persisted_args,
.animation_frame = opts.animation_frame,
// setting a repeat time of 0 is illegal, doing + 1 is a simple way to avoid that
.repeat = if (opts.repeat) delay + 1 else null,
};
callback.loop_id = try page.loop.timeout(delay_ms, &callback.node);
gop.value_ptr.* = callback;
try page.scheduler.add(callback, TimerCallback.run, delay, .{
.name = opts.name,
.low_priority = opts.low_priority,
});
return timer_id;
}
@@ -277,35 +341,78 @@ pub const Window = struct {
behavior: []const u8,
};
};
pub fn _scrollTo(_: *const Window, opts: ScrollToOpts, y: ?u32) void {
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?u32) !void {
_ = opts;
_ = y;
{
const scroll_event = try parser.eventCreate();
defer parser.eventDestroy(scroll_event);
try parser.eventInit(scroll_event, "scroll", .{});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, self),
scroll_event,
);
}
{
const scroll_end = try parser.eventCreate();
defer parser.eventDestroy(scroll_end);
try parser.eventInit(scroll_end, "scrollend", .{});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(parser.DocumentHTML, self.document),
scroll_end,
);
}
}
// libdom's document doesn't have a parent, which is correct, but
// breaks the event bubbling that happens for many events from
// document -> window.
// We need to force dispatch this event on the window, with the
// document target.
// In theory, we should do this for a lot of events and might need
// to come up with a good way to solve this more generically. But
// this specific event, and maybe a few others in the near future,
// are blockers.
// Worth noting that NetSurf itself appears to do something similar:
// https://github.com/netsurf-browser/netsurf/blob/a32e1a03e1c91ee9f0aa211937dbae7a96831149/content/handlers/html/html.c#L380
pub fn dispatchForDocumentTarget(self: *Window, evt: *parser.Event) !void {
// we assume that this evt has already been dispatched on the document
// and thus the target has already been set to the document.
return self.base.redispatchEvent(evt);
}
};
const TimerCallback = struct {
// the internal loop id, need it when cancelling
loop_id: usize,
// the id of our timer (windows.timers key)
timer_id: u31,
// if false, we'll remove the timer_id from the window.timers lookup on run
repeat: ?u32,
// The JavaScript callback to execute
cbk: Function,
// This is the internal data that the event loop tracks. We'll get this
// back in run and, from it, can get our TimerCallback instance
node: Loop.CallbackNode = undefined,
// if the event should be repeated
repeat: ?u63 = null,
animation_frame: bool = false,
window: *Window,
fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
const self: *TimerCallback = @fieldParentPtr("node", node);
args: []Env.JsObject = &.{},
fn run(ctx: *anyopaque) ?u32 {
const self: *TimerCallback = @ptrCast(@alignCast(ctx));
if (self.repeat != null) {
if (self.window.timers.contains(self.timer_id) == false) {
// it was called
return null;
}
} else if (self.window.timers.remove(self.timer_id) == false) {
// it was cancelled
return null;
}
var result: Function.Result = undefined;
@@ -313,99 +420,23 @@ const TimerCallback = struct {
if (self.animation_frame) {
call = self.cbk.tryCall(void, .{self.window.performance._now()}, &result);
} else {
call = self.cbk.tryCall(void, .{}, &result);
call = self.cbk.tryCall(void, self.args, &result);
}
call catch {
log.debug(.user_script, "callback error", .{
log.warn(.user_script, "callback error", .{
.err = result.exception,
.stack = result.stack,
.source = "window timeout",
});
};
if (self.repeat) |r| {
// setInterval
repeat_delay.* = r;
return;
}
// setTimeout
_ = self.window.timers.remove(self.timer_id);
return self.repeat;
}
};
const testing = @import("../../testing.zig");
test "Browser.HTML.Window" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "window.parent === window", "true" },
.{ "window.top === window", "true" },
}, .{});
// requestAnimationFrame should be able to wait by recursively calling itself
// Note however that we in this test do not wait as the request is just send to the browser
try runner.testCases(&.{
.{
\\ let start = 0;
\\ function step(timestamp) {
\\ start = timestamp;
\\ }
,
null,
},
.{ "requestAnimationFrame(step);", null }, // returned id is checked in the next test
.{ " start > 0", "true" },
}, .{});
// cancelAnimationFrame should be able to cancel a request with the given id
try runner.testCases(&.{
.{ "let request_id = requestAnimationFrame(timestamp => {});", null },
.{ "cancelAnimationFrame(request_id);", "undefined" },
}, .{});
try runner.testCases(&.{
.{ "innerHeight", "1" },
.{ "innerWidth", "1" }, // Width is 1 even if there are no elements
.{
\\ let div1 = document.createElement('div');
\\ document.body.appendChild(div1);
\\ div1.getClientRects();
,
null,
},
.{
\\ let div2 = document.createElement('div');
\\ document.body.appendChild(div2);
\\ div2.getClientRects();
,
null,
},
.{ "innerHeight", "1" },
.{ "innerWidth", "2" },
}, .{});
// cancelAnimationFrame should be able to cancel a request with the given id
try runner.testCases(&.{
.{ "let longCall = false;", null },
.{ "window.setTimeout(() => {longCall = true}, 5001);", null },
.{ "longCall;", "false" },
}, .{});
// window event target
try runner.testCases(&.{
.{
\\ let called = false;
\\ window.addEventListener("ready", (e) => {
\\ called = (e.currentTarget == window);
\\ }, {capture: false, once: false});
\\ const evt = new Event("ready", { bubbles: true, cancelable: false });
\\ window.dispatchEvent(evt);
\\ called;
,
"true",
},
}, .{});
test "Browser: Window" {
try testing.htmlRunner("window/window.html");
try testing.htmlRunner("window/frames.html");
}

View File

@@ -1,337 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("netsurf.zig");
const Walker = @import("dom/walker.zig").WalkerChildren;
const URL = @import("../url.zig").URL;
const NP = "\n\n";
const Elem = struct {
inlin: bool = false,
list_order: ?u8 = null,
parent: ?*Elem = null,
};
const State = struct {
block: bool,
last_char: u8,
elem: ?*Elem = null,
fn is_inline(state: *State) bool {
if (state.elem == null) return false;
return state.elem.?.inlin;
}
fn last_char_space(state: *State) bool {
if (state.last_char == ' ' or state.last_char == '\n') return true;
return false;
}
};
// writer must be a std.io.Writer
pub fn writeMarkdown(url: URL, doc: *parser.Document, writer: anytype) !void {
var state = State{ .block = true, .last_char = '\n' };
_ = try writeChildren(url, parser.documentToNode(doc), &state, writer);
try writer.writeAll("\n");
}
fn writeChildren(url: URL, root: *parser.Node, state: *State, writer: anytype) !void {
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(root, next) orelse break;
try writeNode(url, next.?, state, writer);
}
}
fn ensureBlock(state: *State, writer: anytype) !void {
if (state.is_inline()) return;
if (!state.block) {
try writer.writeAll(NP);
state.last_char = '\n';
state.block = true;
}
}
fn writeInline(state: *State, text: []const u8, writer: anytype) !void {
try writer.writeAll(text);
state.last_char = text[text.len - 1];
if (state.block) state.block = false;
}
const order = [_][]const u8{
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10",
"11", "12", "13", "14", "15", "16", "17", "18", "19", "20",
"21", "22", "23", "24", "25", "26", "27", "28", "29", "30",
"31", "32", "33", "34", "35", "36", "37", "38", "39", "40",
"41", "42", "43", "44", "45", "46", "47", "48", "49", "50",
};
fn writeNode(url: URL, node: *parser.Node, state: *State, writer: anytype) anyerror!void {
switch (try parser.nodeType(node)) {
.element => {
const html_element: *parser.ElementHTML = @ptrCast(node);
const tag = try parser.elementHTMLGetTagType(html_element);
// debug
// try writer.writeAll("\nstart - ");
// try writer.writeAll(@tagName(tag));
// try writer.writeAll("\n");
switch (tag) {
// skip element, go to children
.html, .head, .meta, .link, .body, .span => {
try writeChildren(url, node, state, writer);
},
// skip element and children
.title, .i, .script, .noscript, .undef, .style => {},
// generic elements
.h1, .h2, .h3, .h4, .h5, .h6 => {
try ensureBlock(state, writer);
if (!state.is_inline()) {
switch (tag) {
.h1 => try writeInline(state, "# ", writer),
.h2 => try writeInline(state, "## ", writer),
.h3 => try writeInline(state, "### ", writer),
.h4 => try writeInline(state, "#### ", writer),
.h5 => try writeInline(state, "##### ", writer),
.h6 => try writeInline(state, "###### ", writer),
else => @panic("only headers tags are supported here"),
}
}
try writeChildren(url, node, state, writer);
try ensureBlock(state, writer);
},
// containers and dividers
.header, .footer, .nav, .section, .div, .article, .p, .button, .form => {
try ensureBlock(state, writer);
try writeChildren(url, node, state, writer);
try ensureBlock(state, writer);
},
.br => {
try ensureBlock(state, writer);
try writeChildren(url, node, state, writer);
},
.hr => {
try ensureBlock(state, writer);
try writeInline(state, "---", writer);
try ensureBlock(state, writer);
},
// styling
.b => {
var elem = Elem{ .parent = state.elem, .inlin = true };
state.elem = &elem;
defer state.elem = elem.parent;
try writeInline(state, "**", writer);
try writeChildren(url, node, state, writer);
try writeInline(state, "**", writer);
},
// specific elements
.a => {
if (!state.last_char_space()) try writeInline(state, " ", writer);
var elem = Elem{ .parent = state.elem, .inlin = true };
state.elem = &elem;
defer state.elem = elem.parent;
const element = parser.nodeToElement(node);
if (try getAttributeValue(element, "href")) |href| {
try writeInline(state, "[", writer);
try writeChildren(url, node, state, writer);
try writeInline(state, "](", writer);
// handle relative path
if (href[0] == '/') {
try writeInline(state, url.scheme(), writer);
try writeInline(state, "://", writer);
try writeInline(state, url.host(), writer);
}
try writeInline(state, href, writer);
try writeInline(state, ")", writer);
} else {
try writeChildren(url, node, state, writer);
}
},
.img => {
var elem = Elem{ .parent = state.elem, .inlin = true };
state.elem = &elem;
defer state.elem = elem.parent;
const element = parser.nodeToElement(node);
if (try getAttributeValue(element, "src")) |src| {
try writeInline(state, "![", writer);
if (try getAttributeValue(element, "alt")) |alt| {
try writeInline(state, alt, writer);
} else {
try writeInline(state, src, writer);
}
try writeInline(state, "](", writer);
// handle relative path
if (src[0] == '/') {
try writeInline(state, url.scheme(), writer);
try writeInline(state, "://", writer);
try writeInline(state, url.host(), writer);
}
try writeInline(state, src, writer);
try writeInline(state, ")", writer);
}
},
.ul => {
var elem = Elem{ .parent = state.elem, .list_order = 0 };
state.elem = &elem;
defer state.elem = elem.parent;
try ensureBlock(state, writer);
try writeChildren(url, node, state, writer);
try ensureBlock(state, writer);
},
.ol => {
var elem = Elem{ .parent = state.elem, .list_order = 1 };
state.elem = &elem;
defer state.elem = elem.parent;
try ensureBlock(state, writer);
try writeChildren(url, node, state, writer);
try ensureBlock(state, writer);
},
.li => blk: {
const parent = state.elem orelse break :blk;
const list_order = parent.list_order orelse break :blk;
if (!state.block) try writer.writeAll("\n");
if (list_order > 0) {
// ordered list
try writeInline(state, order[list_order - 1], writer);
try writeInline(state, ". ", writer);
parent.list_order = list_order + 1;
} else {
// unordered list
try writeInline(state, "- ", writer);
}
try writeChildren(url, node, state, writer);
},
.input => {
var elem = Elem{ .parent = state.elem, .inlin = true };
state.elem = &elem;
defer state.elem = elem.parent;
const element = parser.nodeToElement(node);
if (try getAttributeValue(element, "value")) |value| {
try writeInline(state, value, writer);
try writeInline(state, " ", writer);
}
},
else => {
try ensureBlock(state, writer);
try writer.writeAll(@tagName(tag));
try writer.writeAll(" not supported");
try ensureBlock(state, writer);
},
}
// try writer.writeAll("\nend - ");
// try writer.writeAll(@tagName(tag));
// try writer.writeAll("\n");
},
.text => {
const v = try parser.nodeValue(node) orelse return;
const printed = try writeText(state, v, writer);
if (printed) state.block = false;
},
.cdata_section => {},
.comment => {},
// TODO handle processing instruction dump
.processing_instruction => {},
// document fragment is outside of the main document DOM, so we
// don't output it.
.document_fragment => {},
// document will never be called, but required for completeness.
.document => {},
// done globally instead, but required for completeness. Only the outer DOCTYPE should be written
.document_type => {},
// deprecated
.attribute, .entity_reference, .entity, .notation => {},
}
}
// TODO: not sure about + - . ! as they are very common characters
// I fear that we add too much escape strings
// TODO: | (pipe)
const escape = [_]u8{ '\\', '`', '*', '_', '{', '}', '[', ']', '<', '>', '(', ')', '#' };
fn writeText(state: *State, value: []const u8, writer: anytype) !bool {
if (value.len == 0) return false;
var last_char: u8 = ' ';
var printed: u64 = 0;
for (value, 0..) |v, i| {
// do not print:
// - multiple spaces
// - return line
// - tabs
if (v == last_char and v == ' ') continue;
if (v == '\n') continue;
if (v == '\t') continue;
// escape char
for (escape) |esc| {
if (v == esc) try writer.writeAll("\\");
}
if (printed == 0 and !state.is_inline()) {
if (state.last_char != '\n' and state.last_char != ' ') {
try writer.writeAll(" ");
}
}
last_char = v;
printed += 1;
const x = [_]u8{v}; // TODO: do we have something better?
try writer.writeAll(&x);
if (i == value.len - 1) state.last_char = v;
}
if (printed > 0) return true;
return false;
}
fn getAttributeValue(elem: *parser.Element, attr: []const u8) !?[]const u8 {
if (try parser.elementGetAttribute(elem, attr)) |value| {
if (value.len > 0) return value;
}
return null;
}
fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
var v = value;
while (v.len > 0) {
try writer.writeAll("TEXT: ");
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
return writer.writeAll(v);
};
try writer.writeAll(v[0..index]);
switch (v[index]) {
'&' => try writer.writeAll("&amp;"),
'<' => try writer.writeAll("&lt;"),
'>' => try writer.writeAll("&gt;"),
else => unreachable,
}
v = v[index + 1 ..];
}
}

View File

@@ -20,6 +20,7 @@
// 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");
@@ -33,43 +34,43 @@ const Error = error{
var heap: ?*c.mi_heap_t = null;
pub fn create() Error!void {
if (heap != null) return Error.HeapNotNull;
std.debug.assert(heap == null);
heap = c.mi_heap_new();
if (heap == null) return Error.HeapNull;
std.debug.assert(heap != null);
}
pub fn destroy() void {
if (heap == null) return;
std.debug.assert(heap != null);
c.mi_heap_destroy(heap.?);
heap = null;
}
pub export fn m_alloc(size: usize) callconv(.C) ?*anyopaque {
if (heap == null) return null;
pub export fn m_alloc(size: usize) callconv(.c) ?*anyopaque {
std.debug.assert(heap != 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;
pub export fn re_alloc(ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque {
std.debug.assert(heap != 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;
pub export fn c_alloc(nmemb: usize, size: usize) callconv(.c) ?*anyopaque {
std.debug.assert(heap != 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;
pub export fn str_dup(s: [*c]const u8) callconv(.c) [*c]u8 {
std.debug.assert(heap != 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;
pub export fn strn_dup(s: [*c]const u8, size: usize) callconv(.c) [*c]u8 {
std.debug.assert(heap != 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 {
pub export fn f_ree(_: ?*anyopaque) callconv(.c) void {
return;
}

View File

@@ -22,11 +22,11 @@ const Allocator = std.mem.Allocator;
pub const Mime = struct {
content_type: ContentType,
params: []const u8 = "",
charset: ?[]const u8 = null,
charset: ?[:0]const u8 = null,
pub const unknown = Mime{
.params = "",
.charset = "",
.charset = null,
.content_type = .{ .unknown = {} },
};
@@ -35,6 +35,8 @@ pub const Mime = struct {
text_html,
text_javascript,
text_plain,
text_css,
application_json,
unknown,
other,
};
@@ -44,11 +46,13 @@ pub const Mime = struct {
text_html: void,
text_javascript: void,
text_plain: void,
text_css: void,
application_json: void,
unknown: void,
other: struct { type: []const u8, sub_type: []const u8 },
};
pub fn parse(arena: Allocator, input: []u8) !Mime {
pub fn parse(input: []u8) !Mime {
if (input.len > 255) {
return error.TooBig;
}
@@ -65,7 +69,7 @@ pub const Mime = struct {
const params = trimLeft(normalized[type_len..]);
var charset: ?[]const u8 = null;
var charset: ?[:0]const u8 = null;
var it = std.mem.splitScalar(u8, params, ';');
while (it.next()) |attr| {
@@ -82,7 +86,37 @@ pub const Mime = struct {
}, name) orelse continue;
switch (attribute_name) {
.charset => charset = try parseAttributeValue(arena, value),
.charset => {
// We used to have a proper value parser, but we currently
// only care about the charset attribute, plus only about
// the UTF-8 value. It's a lot easier to do it this way,
// and it doesn't require an allocation to (a) unescape the
// value or (b) ensure the correct lifetime.
if (value.len == 0) {
break;
}
var attribute_value = value;
if (value[0] == '"') {
if (value.len < 3 or value[value.len - 1] != '"') {
return error.Invalid;
}
attribute_value = value[1 .. value.len - 1];
}
if (std.ascii.eqlIgnoreCase(attribute_value, "utf-8")) {
charset = "UTF-8";
} else if (std.ascii.eqlIgnoreCase(attribute_value, "iso-8859-1")) {
charset = "ISO-8859-1";
} else {
// we only care about null (which we default to UTF-8)
// or UTF-8. If this is actually set (i.e. not null)
// and isn't UTF-8, we'll just put a dummy value. If
// we want to capture the actual value, we'll need to
// dupe/allocate it. Since, for now, we don't need that
// we can avoid the allocation.
charset = "lightpanda:UNSUPPORTED";
}
},
}
}
@@ -174,18 +208,22 @@ pub const Mime = struct {
if (std.meta.stringToEnum(enum {
@"text/xml",
@"text/html",
@"text/css",
@"text/plain",
@"text/javascript",
@"application/javascript",
@"application/x-javascript",
@"text/plain",
@"application/json",
}, type_name)) |known_type| {
const ct: ContentType = switch (known_type) {
.@"text/xml" => .{ .text_xml = {} },
.@"text/html" => .{ .text_html = {} },
.@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} },
.@"text/plain" => .{ .text_plain = {} },
.@"text/css" => .{ .text_css = {} },
.@"application/json" => .{ .application_json = {} },
};
return .{ ct, attribute_start };
}
@@ -216,56 +254,6 @@ pub const Mime = struct {
break :blk v;
};
fn parseAttributeValue(arena: Allocator, value: []const u8) ![]const u8 {
if (value[0] != '"') {
return value;
}
// 1 to skip the opening quote
var value_pos: usize = 1;
var unescaped_len: usize = 0;
const last = value.len - 1;
while (value_pos < value.len) {
switch (value[value_pos]) {
'"' => break,
'\\' => {
if (value_pos == last) {
return error.Invalid;
}
const next = value[value_pos + 1];
if (T_SPECIAL[next] == false) {
return error.Invalid;
}
value_pos += 2;
},
else => value_pos += 1,
}
unescaped_len += 1;
}
if (unescaped_len == 0) {
return error.Invalid;
}
value_pos = 1;
const owned = try arena.alloc(u8, unescaped_len);
for (0..unescaped_len) |i| {
switch (value[value_pos]) {
'"' => break,
'\\' => {
owned[i] = value[value_pos + 1];
value_pos += 2;
},
else => |c| {
owned[i] = c;
value_pos += 1;
},
}
}
return owned;
}
const VALID_CODEPOINTS = blk: {
var v: [256]bool = undefined;
for (0..256) |i| {
@@ -296,7 +284,7 @@ pub const Mime = struct {
};
const testing = @import("../testing.zig");
test "Mime: invalid " {
test "Mime: invalid" {
defer testing.reset();
const invalids = [_][]const u8{
@@ -314,12 +302,11 @@ test "Mime: invalid " {
"text/html; charset=\"\"",
"text/html; charset=\"",
"text/html; charset=\"\\",
"text/html; charset=\"\\a\"", // invalid to escape non special characters
};
for (invalids) |invalid| {
const mutable_input = try testing.arena_allocator.dupe(u8, invalid);
try testing.expectError(error.Invalid, Mime.parse(undefined, mutable_input));
try testing.expectError(error.Invalid, Mime.parse(mutable_input));
}
}
@@ -349,6 +336,9 @@ test "Mime: parse common" {
try expect(.{ .content_type = .{ .text_javascript = {} } }, "text/javascript");
try expect(.{ .content_type = .{ .text_javascript = {} } }, "Application/JavaScript");
try expect(.{ .content_type = .{ .text_javascript = {} } }, "application/x-javascript");
try expect(.{ .content_type = .{ .application_json = {} } }, "application/json");
try expect(.{ .content_type = .{ .text_css = {} } }, "text/css");
}
test "Mime: parse uncommon" {
@@ -373,19 +363,19 @@ test "Mime: parse charset" {
try expect(.{
.content_type = .{ .text_xml = {} },
.charset = "utf-8",
.charset = "UTF-8",
.params = "charset=utf-8",
}, "text/xml; charset=utf-8");
try expect(.{
.content_type = .{ .text_xml = {} },
.charset = "utf-8",
.charset = "UTF-8",
.params = "charset=\"utf-8\"",
}, "text/xml;charset=\"utf-8\"");
try expect(.{
.content_type = .{ .text_xml = {} },
.charset = "\\ \" ",
.charset = "lightpanda:UNSUPPORTED",
.params = "charset=\"\\\\ \\\" \"",
}, "text/xml;charset=\"\\\\ \\\" \" ");
}
@@ -396,7 +386,7 @@ test "Mime: isHTML" {
const isHTML = struct {
fn isHTML(expected: bool, input: []const u8) !void {
const mutable_input = try testing.arena_allocator.dupe(u8, input);
var mime = try Mime.parse(testing.arena_allocator, mutable_input);
var mime = try Mime.parse(mutable_input);
try testing.expectEqual(expected, mime.isHTML());
}
}.isHTML;
@@ -482,7 +472,7 @@ const Expectation = struct {
fn expect(expected: Expectation, input: []const u8) !void {
const mutable_input = try testing.arena_allocator.dupe(u8, input);
const actual = try Mime.parse(testing.arena_allocator, mutable_input);
const actual = try Mime.parse(mutable_input);
try testing.expectEqual(
std.meta.activeTag(expected.content_type),
std.meta.activeTag(actual.content_type),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -16,8 +16,6 @@ test "Browser.fetch" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try @import("polyfill.zig").load(testing.allocator, runner.page.main_context);
try runner.testCases(&.{
.{
\\ var ok = false;

View File

@@ -23,25 +23,98 @@ const log = @import("../../log.zig");
const Allocator = std.mem.Allocator;
const Env = @import("../env.zig").Env;
const modules = [_]struct {
name: []const u8,
source: []const u8,
}{
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
pub const Loader = struct {
state: enum { empty, loading } = .empty,
done: struct {
fetch: bool = false,
webcomponents: bool = false,
} = .{},
fn load(self: *Loader, comptime name: []const u8, source: []const u8, js_context: *Env.JsContext) void {
var try_catch: Env.TryCatch = undefined;
try_catch.init(js_context);
defer try_catch.deinit();
self.state = .loading;
defer self.state = .empty;
log.debug(.js, "polyfill load", .{ .name = name });
_ = js_context.exec(source, name) catch |err| {
log.fatal(.app, "polyfill error", .{
.name = name,
.err = try_catch.err(js_context.call_arena) catch @errorName(err) orelse @errorName(err),
});
};
@field(self.done, name) = true;
}
pub fn missing(self: *Loader, name: []const u8, js_context: *Env.JsContext) bool {
// Avoid recursive calls during polyfill loading.
if (self.state == .loading) {
return false;
}
if (!self.done.fetch and isFetch(name)) {
const source = @import("fetch.zig").source;
self.load("fetch", source, js_context);
// We return false here: We want v8 to continue the calling chain
// to finally find the polyfill we just inserted. If we want to
// return false and stops the call chain, we have to use
// `info.GetReturnValue.Set()` function, or `undefined` will be
// returned immediately.
return false;
}
if (!self.done.webcomponents and isWebcomponents(name)) {
const source = @import("webcomponents.zig").source;
self.load("webcomponents", source, js_context);
// We return false here: We want v8 to continue the calling chain
// to finally find the polyfill we just inserted. If we want to
// return false and stops the call chain, we have to use
// `info.GetReturnValue.Set()` function, or `undefined` will be
// returned immediately.
return false;
}
if (comptime builtin.mode == .Debug) {
log.debug(.unknown_prop, "unkown global property", .{
.info = "but the property can exist in pure JS",
.property = name,
});
}
return false;
}
fn isFetch(name: []const u8) bool {
if (std.mem.eql(u8, name, "fetch")) return true;
if (std.mem.eql(u8, name, "Request")) return true;
if (std.mem.eql(u8, name, "Response")) return true;
if (std.mem.eql(u8, name, "Headers")) return true;
return false;
}
fn isWebcomponents(name: []const u8) bool {
if (std.mem.eql(u8, name, "customElements")) return true;
return false;
}
};
pub fn load(allocator: Allocator, js_context: *Env.JsContext) !void {
pub fn preload(allocator: Allocator, js_context: *Env.JsContext) !void {
var try_catch: Env.TryCatch = undefined;
try_catch.init(js_context);
defer try_catch.deinit();
for (modules) |m| {
_ = js_context.exec(m.source, m.name) catch |err| {
if (try try_catch.err(allocator)) |msg| {
defer allocator.free(msg);
log.fatal(.app, "polyfill error", .{ .name = m.name, .err = msg });
}
return err;
};
}
const name = "webcomponents-pre";
const source = @import("webcomponents.zig").pre;
_ = js_context.exec(source, name) catch |err| {
if (try try_catch.err(allocator)) |msg| {
defer allocator.free(msg);
log.fatal(.app, "polyfill error", .{ .name = name, .err = msg });
}
return err;
};
}

View File

@@ -0,0 +1,61 @@
/**
@license @nocompile
Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
(function(){/*
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found
at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
Google as part of the polymer project is also subject to an additional IP
rights grant found at http://polymer.github.io/PATENTS.txt
*/
'use strict';var n=window.Document.prototype.createElement,p=window.Document.prototype.createElementNS,aa=window.Document.prototype.importNode,ba=window.Document.prototype.prepend,ca=window.Document.prototype.append,da=window.DocumentFragment.prototype.prepend,ea=window.DocumentFragment.prototype.append,q=window.Node.prototype.cloneNode,r=window.Node.prototype.appendChild,t=window.Node.prototype.insertBefore,u=window.Node.prototype.removeChild,v=window.Node.prototype.replaceChild,w=Object.getOwnPropertyDescriptor(window.Node.prototype,
"textContent"),y=window.Element.prototype.attachShadow,z=Object.getOwnPropertyDescriptor(window.Element.prototype,"innerHTML"),A=window.Element.prototype.getAttribute,B=window.Element.prototype.setAttribute,C=window.Element.prototype.removeAttribute,D=window.Element.prototype.toggleAttribute,E=window.Element.prototype.getAttributeNS,F=window.Element.prototype.setAttributeNS,G=window.Element.prototype.removeAttributeNS,H=window.Element.prototype.insertAdjacentElement,fa=window.Element.prototype.insertAdjacentHTML,
ha=window.Element.prototype.prepend,ia=window.Element.prototype.append,ja=window.Element.prototype.before,ka=window.Element.prototype.after,la=window.Element.prototype.replaceWith,ma=window.Element.prototype.remove,na=window.HTMLElement,I=Object.getOwnPropertyDescriptor(window.HTMLElement.prototype,"innerHTML"),oa=window.HTMLElement.prototype.insertAdjacentElement,pa=window.HTMLElement.prototype.insertAdjacentHTML;var qa=new Set;"annotation-xml color-profile font-face font-face-src font-face-uri font-face-format font-face-name missing-glyph".split(" ").forEach(function(a){return qa.add(a)});function ra(a){var b=qa.has(a);a=/^[a-z][.0-9_a-z]*-[-.0-9_a-z]*$/.test(a);return!b&&a}var sa=document.contains?document.contains.bind(document):document.documentElement.contains.bind(document.documentElement);
function J(a){var b=a.isConnected;if(void 0!==b)return b;if(sa(a))return!0;for(;a&&!(a.__CE_isImportDocument||a instanceof Document);)a=a.parentNode||(window.ShadowRoot&&a instanceof ShadowRoot?a.host:void 0);return!(!a||!(a.__CE_isImportDocument||a instanceof Document))}function K(a){var b=a.children;if(b)return Array.prototype.slice.call(b);b=[];for(a=a.firstChild;a;a=a.nextSibling)a.nodeType===Node.ELEMENT_NODE&&b.push(a);return b}
function L(a,b){for(;b&&b!==a&&!b.nextSibling;)b=b.parentNode;return b&&b!==a?b.nextSibling:null}
function M(a,b,d){for(var f=a;f;){if(f.nodeType===Node.ELEMENT_NODE){var c=f;b(c);var e=c.localName;if("link"===e&&"import"===c.getAttribute("rel")){f=c.import;void 0===d&&(d=new Set);if(f instanceof Node&&!d.has(f))for(d.add(f),f=f.firstChild;f;f=f.nextSibling)M(f,b,d);f=L(a,c);continue}else if("template"===e){f=L(a,c);continue}if(c=c.__CE_shadowRoot)for(c=c.firstChild;c;c=c.nextSibling)M(c,b,d)}f=f.firstChild?f.firstChild:L(a,f)}};function N(){var a=!(null===O||void 0===O||!O.noDocumentConstructionObserver),b=!(null===O||void 0===O||!O.shadyDomFastWalk);this.m=[];this.g=[];this.j=!1;this.shadyDomFastWalk=b;this.I=!a}function P(a,b,d,f){var c=window.ShadyDOM;if(a.shadyDomFastWalk&&c&&c.inUse){if(b.nodeType===Node.ELEMENT_NODE&&d(b),b.querySelectorAll)for(a=c.nativeMethods.querySelectorAll.call(b,"*"),b=0;b<a.length;b++)d(a[b])}else M(b,d,f)}function ta(a,b){a.j=!0;a.m.push(b)}function ua(a,b){a.j=!0;a.g.push(b)}
function Q(a,b){a.j&&P(a,b,function(d){return R(a,d)})}function R(a,b){if(a.j&&!b.__CE_patched){b.__CE_patched=!0;for(var d=0;d<a.m.length;d++)a.m[d](b);for(d=0;d<a.g.length;d++)a.g[d](b)}}function S(a,b){var d=[];P(a,b,function(c){return d.push(c)});for(b=0;b<d.length;b++){var f=d[b];1===f.__CE_state?a.connectedCallback(f):T(a,f)}}function U(a,b){var d=[];P(a,b,function(c){return d.push(c)});for(b=0;b<d.length;b++){var f=d[b];1===f.__CE_state&&a.disconnectedCallback(f)}}
function V(a,b,d){d=void 0===d?{}:d;var f=d.J,c=d.upgrade||function(g){return T(a,g)},e=[];P(a,b,function(g){a.j&&R(a,g);if("link"===g.localName&&"import"===g.getAttribute("rel")){var h=g.import;h instanceof Node&&(h.__CE_isImportDocument=!0,h.__CE_registry=document.__CE_registry);h&&"complete"===h.readyState?h.__CE_documentLoadHandled=!0:g.addEventListener("load",function(){var k=g.import;if(!k.__CE_documentLoadHandled){k.__CE_documentLoadHandled=!0;var l=new Set;f&&(f.forEach(function(m){return l.add(m)}),
l.delete(k));V(a,k,{J:l,upgrade:c})}})}else e.push(g)},f);for(b=0;b<e.length;b++)c(e[b])}
function T(a,b){try{var d=b.ownerDocument,f=d.__CE_registry;var c=f&&(d.defaultView||d.__CE_isImportDocument)?W(f,b.localName):void 0;if(c&&void 0===b.__CE_state){c.constructionStack.push(b);try{try{if(new c.constructorFunction!==b)throw Error("The custom element constructor did not produce the element being upgraded.");}finally{c.constructionStack.pop()}}catch(k){throw b.__CE_state=2,k;}b.__CE_state=1;b.__CE_definition=c;if(c.attributeChangedCallback&&b.hasAttributes()){var e=c.observedAttributes;
for(c=0;c<e.length;c++){var g=e[c],h=b.getAttribute(g);null!==h&&a.attributeChangedCallback(b,g,null,h,null)}}J(b)&&a.connectedCallback(b)}}catch(k){X(k)}}N.prototype.connectedCallback=function(a){var b=a.__CE_definition;if(b.connectedCallback)try{b.connectedCallback.call(a)}catch(d){X(d)}};N.prototype.disconnectedCallback=function(a){var b=a.__CE_definition;if(b.disconnectedCallback)try{b.disconnectedCallback.call(a)}catch(d){X(d)}};
N.prototype.attributeChangedCallback=function(a,b,d,f,c){var e=a.__CE_definition;if(e.attributeChangedCallback&&-1<e.observedAttributes.indexOf(b))try{e.attributeChangedCallback.call(a,b,d,f,c)}catch(g){X(g)}};
function va(a,b,d,f){var c=b.__CE_registry;if(c&&(null===f||"http://www.w3.org/1999/xhtml"===f)&&(c=W(c,d)))try{var e=new c.constructorFunction;if(void 0===e.__CE_state||void 0===e.__CE_definition)throw Error("Failed to construct '"+d+"': The returned value was not constructed with the HTMLElement constructor.");if("http://www.w3.org/1999/xhtml"!==e.namespaceURI)throw Error("Failed to construct '"+d+"': The constructed element's namespace must be the HTML namespace.");if(e.hasAttributes())throw Error("Failed to construct '"+
d+"': The constructed element must not have any attributes.");if(null!==e.firstChild)throw Error("Failed to construct '"+d+"': The constructed element must not have any children.");if(null!==e.parentNode)throw Error("Failed to construct '"+d+"': The constructed element must not have a parent node.");if(e.ownerDocument!==b)throw Error("Failed to construct '"+d+"': The constructed element's owner document is incorrect.");if(e.localName!==d)throw Error("Failed to construct '"+d+"': The constructed element's local name is incorrect.");
return e}catch(g){return X(g),b=null===f?n.call(b,d):p.call(b,f,d),Object.setPrototypeOf(b,HTMLUnknownElement.prototype),b.__CE_state=2,b.__CE_definition=void 0,R(a,b),b}b=null===f?n.call(b,d):p.call(b,f,d);R(a,b);return b}
function X(a){var b="",d="",f=0,c=0;a instanceof Error?(b=a.message,d=a.sourceURL||a.fileName||"",f=a.line||a.lineNumber||0,c=a.column||a.columnNumber||0):b="Uncaught "+String(a);var e=void 0;void 0===ErrorEvent.prototype.initErrorEvent?e=new ErrorEvent("error",{cancelable:!0,message:b,filename:d,lineno:f,colno:c,error:a}):(e=document.createEvent("ErrorEvent"),e.initErrorEvent("error",!1,!0,b,d,f),e.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{configurable:!0,get:function(){return!0}})});
void 0===e.error&&Object.defineProperty(e,"error",{configurable:!0,enumerable:!0,get:function(){return a}});window.dispatchEvent(e);e.defaultPrevented||console.error(a)};function wa(){var a=this;this.g=void 0;this.F=new Promise(function(b){a.l=b})}wa.prototype.resolve=function(a){if(this.g)throw Error("Already resolved.");this.g=a;this.l(a)};function xa(a){var b=document;this.l=void 0;this.h=a;this.g=b;V(this.h,this.g);"loading"===this.g.readyState&&(this.l=new MutationObserver(this.G.bind(this)),this.l.observe(this.g,{childList:!0,subtree:!0}))}function ya(a){a.l&&a.l.disconnect()}xa.prototype.G=function(a){var b=this.g.readyState;"interactive"!==b&&"complete"!==b||ya(this);for(b=0;b<a.length;b++)for(var d=a[b].addedNodes,f=0;f<d.length;f++)V(this.h,d[f])};function Y(a){this.s=new Map;this.u=new Map;this.C=new Map;this.A=!1;this.B=new Map;this.o=function(b){return b()};this.i=!1;this.v=[];this.h=a;this.D=a.I?new xa(a):void 0}Y.prototype.H=function(a,b){var d=this;if(!(b instanceof Function))throw new TypeError("Custom element constructor getters must be functions.");za(this,a);this.s.set(a,b);this.v.push(a);this.i||(this.i=!0,this.o(function(){return Aa(d)}))};
Y.prototype.define=function(a,b){var d=this;if(!(b instanceof Function))throw new TypeError("Custom element constructors must be functions.");za(this,a);Ba(this,a,b);this.v.push(a);this.i||(this.i=!0,this.o(function(){return Aa(d)}))};function za(a,b){if(!ra(b))throw new SyntaxError("The element name '"+b+"' is not valid.");if(W(a,b))throw Error("A custom element with name '"+(b+"' has already been defined."));if(a.A)throw Error("A custom element is already being defined.");}
function Ba(a,b,d){a.A=!0;var f;try{var c=d.prototype;if(!(c instanceof Object))throw new TypeError("The custom element constructor's prototype is not an object.");var e=function(m){var x=c[m];if(void 0!==x&&!(x instanceof Function))throw Error("The '"+m+"' callback must be a function.");return x};var g=e("connectedCallback");var h=e("disconnectedCallback");var k=e("adoptedCallback");var l=(f=e("attributeChangedCallback"))&&d.observedAttributes||[]}catch(m){throw m;}finally{a.A=!1}d={localName:b,
constructorFunction:d,connectedCallback:g,disconnectedCallback:h,adoptedCallback:k,attributeChangedCallback:f,observedAttributes:l,constructionStack:[]};a.u.set(b,d);a.C.set(d.constructorFunction,d);return d}Y.prototype.upgrade=function(a){V(this.h,a)};
function Aa(a){if(!1!==a.i){a.i=!1;for(var b=[],d=a.v,f=new Map,c=0;c<d.length;c++)f.set(d[c],[]);V(a.h,document,{upgrade:function(k){if(void 0===k.__CE_state){var l=k.localName,m=f.get(l);m?m.push(k):a.u.has(l)&&b.push(k)}}});for(c=0;c<b.length;c++)T(a.h,b[c]);for(c=0;c<d.length;c++){for(var e=d[c],g=f.get(e),h=0;h<g.length;h++)T(a.h,g[h]);(e=a.B.get(e))&&e.resolve(void 0)}d.length=0}}Y.prototype.get=function(a){if(a=W(this,a))return a.constructorFunction};
Y.prototype.whenDefined=function(a){if(!ra(a))return Promise.reject(new SyntaxError("'"+a+"' is not a valid custom element name."));var b=this.B.get(a);if(b)return b.F;b=new wa;this.B.set(a,b);var d=this.u.has(a)||this.s.has(a);a=-1===this.v.indexOf(a);d&&a&&b.resolve(void 0);return b.F};Y.prototype.polyfillWrapFlushCallback=function(a){this.D&&ya(this.D);var b=this.o;this.o=function(d){return a(function(){return b(d)})}};
function W(a,b){var d=a.u.get(b);if(d)return d;if(d=a.s.get(b)){a.s.delete(b);try{return Ba(a,b,d())}catch(f){X(f)}}}Y.prototype.define=Y.prototype.define;Y.prototype.upgrade=Y.prototype.upgrade;Y.prototype.get=Y.prototype.get;Y.prototype.whenDefined=Y.prototype.whenDefined;Y.prototype.polyfillDefineLazy=Y.prototype.H;Y.prototype.polyfillWrapFlushCallback=Y.prototype.polyfillWrapFlushCallback;function Z(a,b,d){function f(c){return function(e){for(var g=[],h=0;h<arguments.length;++h)g[h]=arguments[h];h=[];for(var k=[],l=0;l<g.length;l++){var m=g[l];m instanceof Element&&J(m)&&k.push(m);if(m instanceof DocumentFragment)for(m=m.firstChild;m;m=m.nextSibling)h.push(m);else h.push(m)}c.apply(this,g);for(g=0;g<k.length;g++)U(a,k[g]);if(J(this))for(g=0;g<h.length;g++)k=h[g],k instanceof Element&&S(a,k)}}void 0!==d.prepend&&(b.prepend=f(d.prepend));void 0!==d.append&&(b.append=f(d.append))};function Ca(a){Document.prototype.createElement=function(b){return va(a,this,b,null)};Document.prototype.importNode=function(b,d){b=aa.call(this,b,!!d);this.__CE_registry?V(a,b):Q(a,b);return b};Document.prototype.createElementNS=function(b,d){return va(a,this,d,b)};Z(a,Document.prototype,{prepend:ba,append:ca})};function Da(a){function b(f){return function(c){for(var e=[],g=0;g<arguments.length;++g)e[g]=arguments[g];g=[];for(var h=[],k=0;k<e.length;k++){var l=e[k];l instanceof Element&&J(l)&&h.push(l);if(l instanceof DocumentFragment)for(l=l.firstChild;l;l=l.nextSibling)g.push(l);else g.push(l)}f.apply(this,e);for(e=0;e<h.length;e++)U(a,h[e]);if(J(this))for(e=0;e<g.length;e++)h=g[e],h instanceof Element&&S(a,h)}}var d=Element.prototype;void 0!==ja&&(d.before=b(ja));void 0!==ka&&(d.after=b(ka));void 0!==la&&
(d.replaceWith=function(f){for(var c=[],e=0;e<arguments.length;++e)c[e]=arguments[e];e=[];for(var g=[],h=0;h<c.length;h++){var k=c[h];k instanceof Element&&J(k)&&g.push(k);if(k instanceof DocumentFragment)for(k=k.firstChild;k;k=k.nextSibling)e.push(k);else e.push(k)}h=J(this);la.apply(this,c);for(c=0;c<g.length;c++)U(a,g[c]);if(h)for(U(a,this),c=0;c<e.length;c++)g=e[c],g instanceof Element&&S(a,g)});void 0!==ma&&(d.remove=function(){var f=J(this);ma.call(this);f&&U(a,this)})};function Ea(a){function b(c,e){Object.defineProperty(c,"innerHTML",{enumerable:e.enumerable,configurable:!0,get:e.get,set:function(g){var h=this,k=void 0;J(this)&&(k=[],P(a,this,function(x){x!==h&&k.push(x)}));e.set.call(this,g);if(k)for(var l=0;l<k.length;l++){var m=k[l];1===m.__CE_state&&a.disconnectedCallback(m)}this.ownerDocument.__CE_registry?V(a,this):Q(a,this);return g}})}function d(c,e){c.insertAdjacentElement=function(g,h){var k=J(h);g=e.call(this,g,h);k&&U(a,h);J(g)&&S(a,h);return g}}function f(c,
e){function g(h,k){for(var l=[];h!==k;h=h.nextSibling)l.push(h);for(k=0;k<l.length;k++)V(a,l[k])}c.insertAdjacentHTML=function(h,k){h=h.toLowerCase();if("beforebegin"===h){var l=this.previousSibling;e.call(this,h,k);g(l||this.parentNode.firstChild,this)}else if("afterbegin"===h)l=this.firstChild,e.call(this,h,k),g(this.firstChild,l);else if("beforeend"===h)l=this.lastChild,e.call(this,h,k),g(l||this.firstChild,null);else if("afterend"===h)l=this.nextSibling,e.call(this,h,k),g(this.nextSibling,l);
else throw new SyntaxError("The value provided ("+String(h)+") is not one of 'beforebegin', 'afterbegin', 'beforeend', or 'afterend'.");}}y&&(Element.prototype.attachShadow=function(c){c=y.call(this,c);if(a.j&&!c.__CE_patched){c.__CE_patched=!0;for(var e=0;e<a.m.length;e++)a.m[e](c)}return this.__CE_shadowRoot=c});z&&z.get?b(Element.prototype,z):I&&I.get?b(HTMLElement.prototype,I):ua(a,function(c){b(c,{enumerable:!0,configurable:!0,get:function(){return q.call(this,!0).innerHTML},set:function(e){var g=
"template"===this.localName,h=g?this.content:this,k=p.call(document,this.namespaceURI,this.localName);for(k.innerHTML=e;0<h.childNodes.length;)u.call(h,h.childNodes[0]);for(e=g?k.content:k;0<e.childNodes.length;)r.call(h,e.childNodes[0])}})});Element.prototype.setAttribute=function(c,e){if(1!==this.__CE_state)return B.call(this,c,e);var g=A.call(this,c);B.call(this,c,e);e=A.call(this,c);a.attributeChangedCallback(this,c,g,e,null)};Element.prototype.setAttributeNS=function(c,e,g){if(1!==this.__CE_state)return F.call(this,
c,e,g);var h=E.call(this,c,e);F.call(this,c,e,g);g=E.call(this,c,e);a.attributeChangedCallback(this,e,h,g,c)};Element.prototype.removeAttribute=function(c){if(1!==this.__CE_state)return C.call(this,c);var e=A.call(this,c);C.call(this,c);null!==e&&a.attributeChangedCallback(this,c,e,null,null)};D&&(Element.prototype.toggleAttribute=function(c,e){if(1!==this.__CE_state)return D.call(this,c,e);var g=A.call(this,c),h=null!==g;e=D.call(this,c,e);h!==e&&a.attributeChangedCallback(this,c,g,e?"":null,null);
return e});Element.prototype.removeAttributeNS=function(c,e){if(1!==this.__CE_state)return G.call(this,c,e);var g=E.call(this,c,e);G.call(this,c,e);var h=E.call(this,c,e);g!==h&&a.attributeChangedCallback(this,e,g,h,c)};oa?d(HTMLElement.prototype,oa):H&&d(Element.prototype,H);pa?f(HTMLElement.prototype,pa):fa&&f(Element.prototype,fa);Z(a,Element.prototype,{prepend:ha,append:ia});Da(a)};var Fa={};function Ga(a){function b(){var d=this.constructor;var f=document.__CE_registry.C.get(d);if(!f)throw Error("Failed to construct a custom element: The constructor was not registered with `customElements`.");var c=f.constructionStack;if(0===c.length)return c=n.call(document,f.localName),Object.setPrototypeOf(c,d.prototype),c.__CE_state=1,c.__CE_definition=f,R(a,c),c;var e=c.length-1,g=c[e];if(g===Fa)throw Error("Failed to construct '"+f.localName+"': This element was already constructed.");c[e]=Fa;
Object.setPrototypeOf(g,d.prototype);R(a,g);return g}b.prototype=na.prototype;Object.defineProperty(HTMLElement.prototype,"constructor",{writable:!0,configurable:!0,enumerable:!1,value:b});window.HTMLElement=b};function Ha(a){function b(d,f){Object.defineProperty(d,"textContent",{enumerable:f.enumerable,configurable:!0,get:f.get,set:function(c){if(this.nodeType===Node.TEXT_NODE)f.set.call(this,c);else{var e=void 0;if(this.firstChild){var g=this.childNodes,h=g.length;if(0<h&&J(this)){e=Array(h);for(var k=0;k<h;k++)e[k]=g[k]}}f.set.call(this,c);if(e)for(c=0;c<e.length;c++)U(a,e[c])}}})}Node.prototype.insertBefore=function(d,f){if(d instanceof DocumentFragment){var c=K(d);d=t.call(this,d,f);if(J(this))for(f=
0;f<c.length;f++)S(a,c[f]);return d}c=d instanceof Element&&J(d);f=t.call(this,d,f);c&&U(a,d);J(this)&&S(a,d);return f};Node.prototype.appendChild=function(d){if(d instanceof DocumentFragment){var f=K(d);d=r.call(this,d);if(J(this))for(var c=0;c<f.length;c++)S(a,f[c]);return d}f=d instanceof Element&&J(d);c=r.call(this,d);f&&U(a,d);J(this)&&S(a,d);return c};Node.prototype.cloneNode=function(d){d=q.call(this,!!d);this.ownerDocument.__CE_registry?V(a,d):Q(a,d);return d};Node.prototype.removeChild=function(d){var f=
d instanceof Element&&J(d),c=u.call(this,d);f&&U(a,d);return c};Node.prototype.replaceChild=function(d,f){if(d instanceof DocumentFragment){var c=K(d);d=v.call(this,d,f);if(J(this))for(U(a,f),f=0;f<c.length;f++)S(a,c[f]);return d}c=d instanceof Element&&J(d);var e=v.call(this,d,f),g=J(this);g&&U(a,f);c&&U(a,d);g&&S(a,d);return e};w&&w.get?b(Node.prototype,w):ta(a,function(d){b(d,{enumerable:!0,configurable:!0,get:function(){for(var f=[],c=this.firstChild;c;c=c.nextSibling)c.nodeType!==Node.COMMENT_NODE&&
f.push(c.textContent);return f.join("")},set:function(f){for(;this.firstChild;)u.call(this,this.firstChild);null!=f&&""!==f&&r.call(this,document.createTextNode(f))}})})};var O=window.customElements;function Ia(){var a=new N;Ga(a);Ca(a);Z(a,DocumentFragment.prototype,{prepend:da,append:ea});Ha(a);Ea(a);window.CustomElementRegistry=Y;a=new Y(a);document.__CE_registry=a;Object.defineProperty(window,"customElements",{configurable:!0,enumerable:!0,value:a})}O&&!O.forcePolyfill&&"function"==typeof O.define&&"function"==typeof O.get||Ia();window.__CE_installPolyfill=Ia;/*
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
}).call(this);

View File

@@ -0,0 +1,42 @@
// webcomponents.js code comes from
// https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs
//
// The original code source is available in a "BSD style license".
//
// This is the `webcomponents-ce.js` bundle
pub const source = @embedFile("webcomponents.js");
// The main webcomponents.js is lazilly loaded when window.customElements is
// called. But, if you look at the test below, you'll notice that we declare
// our custom element (LightPanda) before we call `customElements.define`. We
// _have_ to declare it before we can register it.
// That causes an issue, because the LightPanda class extends HTMLElement, which
// hasn't been monkeypatched by the polyfill yet. If you were to try it as-is
// you'd get an "Illegal Constructor", because that's what the Zig HTMLElement
// constructor does (and that's correct).
// However, once HTMLElement is monkeypatched, it'll work. One simple solution
// is to run the webcomponents.js polyfill proactively on each page, ensuring
// that HTMLElement is monkeypatched before any other JavaScript is run. But
// that adds _a lot_ of overhead.
// So instead of always running the [large and intrusive] webcomponents.js
// polyfill, we'll always run this little snippet. It wraps the HTMLElement
// constructor. When the Lightpanda class is created, it'll extend our little
// wrapper. But, unlike the Zig default constructor which throws, our code
// calls the "real" constructor. That might seem like the same thing, but by the
// time our wrapper is called, the webcomponents.js polyfill will have been
// loaded and the "real" constructor will be the monkeypatched version.
// TL;DR creates a layer of indirection for the constructor, so that, when it's
// actually instantiated, the webcomponents.js polyfill will have been loaded.
pub const pre =
\\ (() => {
\\ const HE = window.HTMLElement;
\\ const b = function() { return HE.prototype.constructor.call(this); }
\\ b.prototype = HE.prototype;
\\ window.HTMLElement = b;
\\ })();
;
const testing = @import("../../testing.zig");
test "Browser: Polyfill.WebComponents" {
try testing.htmlRunner("polyfill/webcomponents.html");
}

View File

@@ -56,6 +56,12 @@ pub const Session = struct {
page: ?Page = null,
// If the current page want to navigate to a new page
// (form submit, link click, top.location = xxx)
// the details are stored here so that, on the next call to session.wait
// we can destroy the current page and start a new one.
queued_navigation: ?QueuedNavigation,
pub fn init(self: *Session, browser: *Browser) !void {
var executor = try browser.env.newExecutionWorld();
errdefer executor.deinit();
@@ -64,6 +70,7 @@ pub const Session = struct {
self.* = .{
.browser = browser,
.executor = executor,
.queued_navigation = null,
.arena = browser.session_arena.allocator(),
.storage_shed = storage.Shed.init(allocator),
.cookie_jar = storage.CookieJar.init(allocator),
@@ -111,23 +118,13 @@ pub const Session = struct {
std.debug.assert(self.page != null);
// Cleanup is a bit sensitive. We could still have inflight I/O. For
// example, we could have an XHR request which is still in the connect
// phase. It's important that we clean these up, as they're holding onto
// limited resources (like our fixed-sized http state pool).
//
// First thing we do, is removeJsContext() which will execute the destructor
// of any type that registered a destructor (e.g. XMLHttpRequest).
// This will shutdown any pending sockets, which begins our cleaning
// processed
// RemoveJsContext() will execute the destructor of any type that
// registered a destructor (e.g. XMLHttpRequest).
// Should be called before we deinit the page, because these objects
// could be referencing it.
self.executor.removeJsContext();
// Second thing we do is reset the loop. This increments the loop ctx_id
// so that any "stale" timeouts we process will get ignored. We need to
// do this BEFORE running the loop because, at this point, things like
// window.setTimeout and running microtasks should be ignored
self.browser.app.loop.reset();
self.page.?.deinit();
self.page = null;
// clear netsurf memory arena.
@@ -139,4 +136,47 @@ pub const Session = struct {
pub fn currentPage(self: *Session) ?*Page {
return &(self.page orelse return null);
}
pub const WaitResult = enum {
done,
no_page,
extra_socket,
};
pub fn wait(self: *Session, wait_ms: i32) WaitResult {
if (self.queued_navigation) |qn| {
// This was already aborted on the page, but it would be pretty
// bad if old requests went to the new page, so let's make double sure
self.browser.http_client.abort();
// Page.navigateFromWebAPI terminatedExecution. If we don't resume
// it before doing a shutdown we'll get an error.
self.executor.resumeExecution();
self.removePage();
self.queued_navigation = null;
const page = self.createPage() catch |err| {
log.err(.browser, "queued navigation page error", .{
.err = err,
.url = qn.url,
});
return .done;
};
page.navigate(qn.url, qn.opts) catch |err| {
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
return .done;
};
}
if (self.page) |*page| {
return page.wait(wait_ms);
}
return .no_page;
}
};
const QueuedNavigation = struct {
url: []const u8,
opts: NavigateOpts,
};

View File

@@ -4,15 +4,15 @@ const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const log = @import("../../log.zig");
const http = @import("../../http/client.zig");
const DateTime = @import("../../datetime.zig").DateTime;
const public_suffix_list = @import("../../data/public_suffix_list.zig").lookup;
pub const LookupOpts = struct {
request_time: ?i64 = null,
origin_uri: ?*const Uri = null,
navigation: bool = true,
is_http: bool,
is_navigation: bool = true,
prefix: ?[]const u8 = null,
};
pub const Jar = struct {
@@ -92,10 +92,15 @@ pub const Jar = struct {
var first = true;
for (self.cookies.items) |*cookie| {
if (!cookie.appliesTo(&target, same_site, opts.navigation, opts.is_http)) continue;
if (!cookie.appliesTo(&target, same_site, opts.is_navigation, opts.is_http)) {
continue;
}
// we have a match!
if (first) {
if (opts.prefix) |prefix| {
try writer.writeAll(prefix);
}
first = false;
} else {
try writer.writeAll("; ");
@@ -104,16 +109,14 @@ pub const Jar = struct {
}
}
pub fn populateFromResponse(self: *Jar, uri: *const Uri, header: *const http.ResponseHeader) !void {
pub fn populateFromResponse(self: *Jar, uri: *const Uri, set_cookie: []const u8) !void {
const c = Cookie.parse(self.allocator, uri, set_cookie) catch |err| {
log.warn(.web_api, "cookie parse failed", .{ .raw = set_cookie, .err = err });
return;
};
const now = std.time.timestamp();
var it = header.iterate("set-cookie");
while (it.next()) |set_cookie| {
const c = Cookie.parse(self.allocator, uri, set_cookie) catch |err| {
log.warn(.web_api, "cookie parse failed", .{ .raw = set_cookie, .err = err });
continue;
};
try self.add(c, now);
}
try self.add(c, now);
}
fn writeCookie(cookie: *const Cookie, writer: anytype) !void {
@@ -296,7 +299,12 @@ pub const Cookie = struct {
}
if (exp_dt) |dt| {
normalized_expires = @floatFromInt(dt.unix(.seconds));
} else std.debug.print("Invalid cookie expires value: {s}\n", .{expires_});
} else {
// Algolia, for example, will call document.setCookie with
// an expired value which is literally 'Invalid Date'
// (it's trying to do something like: `new Date() + undefined`).
log.debug(.web_api, "cookie expires date", .{ .date = expires_ });
}
}
}
@@ -350,12 +358,10 @@ pub const Cookie = struct {
if (domain.len > 0) {
const no_leading_dot = if (domain[0] == '.') domain[1..] else domain;
var list = std.ArrayList(u8).init(arena);
try list.ensureTotalCapacity(no_leading_dot.len + 1); // Expect no precents needed
list.appendAssumeCapacity('.');
try std.Uri.Component.percentEncode(list.writer(), no_leading_dot, isHostChar);
var owned_domain: []u8 = list.items; // @memory retains memory used before growing
_ = toLower(owned_domain);
var aw = try std.Io.Writer.Allocating.initCapacity(arena, no_leading_dot.len + 1);
try aw.writer.writeByte('.');
try std.Uri.Component.percentEncode(&aw.writer, no_leading_dot, isHostChar);
const owned_domain = toLower(aw.written());
if (std.mem.indexOfScalarPos(u8, owned_domain, 1, '.') == null and std.mem.eql(u8, "localhost", owned_domain[1..]) == false) {
// can't set a cookie for a TLD
@@ -377,10 +383,9 @@ pub const Cookie = struct {
pub fn percentEncode(arena: Allocator, component: std.Uri.Component, comptime isValidChar: fn (u8) bool) ![]u8 {
switch (component) {
.raw => |str| {
var list = std.ArrayList(u8).init(arena);
try list.ensureTotalCapacity(str.len); // Expect no precents needed
try std.Uri.Component.percentEncode(list.writer(), str, isValidChar);
return list.items; // @memory retains memory used before growing
var aw = try std.Io.Writer.Allocating.initCapacity(arena, str.len);
try std.Uri.Component.percentEncode(&aw.writer, str, isValidChar);
return aw.written(); // @memory retains memory used before growing
},
.percent_encoded => |str| {
return try arena.dupe(u8, str);
@@ -424,7 +429,7 @@ pub const Cookie = struct {
return .{ name, value, rest };
}
pub fn appliesTo(self: *const Cookie, url: *const PreparedUri, same_site: bool, navigation: bool, is_http: bool) bool {
pub fn appliesTo(self: *const Cookie, url: *const PreparedUri, same_site: bool, is_navigation: bool, is_http: bool) bool {
if (self.http_only and is_http == false) {
// http only cookies can be accessed from Javascript
return false;
@@ -443,7 +448,7 @@ pub const Cookie = struct {
// and cookie.same_site == .lax
switch (self.same_site) {
.strict => return false,
.lax => if (navigation == false) return false,
.lax => if (is_navigation == false) return false,
.none => {},
}
}
@@ -583,7 +588,7 @@ test "Jar: add" {
test "Jar: forRequest" {
const expectCookies = struct {
fn expect(expected: []const u8, jar: *Jar, target_uri: Uri, opts: LookupOpts) !void {
var arr: std.ArrayListUnmanaged(u8) = .{};
var arr: std.ArrayListUnmanaged(u8) = .empty;
defer arr.deinit(testing.allocator);
try jar.forRequest(&target_uri, arr.writer(testing.allocator), opts);
try testing.expectEqual(expected, arr.items);
@@ -614,7 +619,7 @@ test "Jar: forRequest" {
// nothing fancy here
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .is_http = true });
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .navigation = false, .is_http = true });
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .is_navigation = false, .is_http = true });
// We have a cookie where Domain=lightpanda.io
// This should _not_ match xyxlightpanda.io
@@ -680,22 +685,22 @@ test "Jar: forRequest" {
// non-navigational cross domain, insecure
try expectCookies("", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")),
.navigation = false,
.is_http = true,
.is_navigation = false,
});
// non-navigational cross domain, secure
try expectCookies("sitenone=6", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")),
.navigation = false,
.is_http = true,
.is_navigation = false,
});
// non-navigational same origin
try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://lightpanda.io/")),
.navigation = false,
.is_http = true,
.is_navigation = false,
});
// exact domain match + suffix

View File

@@ -130,7 +130,7 @@ pub const Bottle = struct {
return @intCast(self.map.count());
}
pub fn _key(self: *Bottle, idx: u32) ?[]const u8 {
pub fn _key(self: *const Bottle, idx: u32) ?[]const u8 {
if (idx >= self.map.count()) return null;
var it = self.map.valueIterator();
@@ -142,7 +142,7 @@ pub const Bottle = struct {
unreachable;
}
pub fn _getItem(self: *Bottle, k: []const u8) ?[]const u8 {
pub fn _getItem(self: *const Bottle, k: []const u8) ?[]const u8 {
return self.map.get(k);
}
@@ -202,35 +202,25 @@ pub const Bottle = struct {
//
// So for now, we won't impement the feature.
}
pub fn named_get(self: *const Bottle, name: []const u8, _: *bool) ?[]const u8 {
return self._getItem(name);
}
pub fn named_set(self: *Bottle, name: []const u8, value: []const u8, _: *bool) !void {
try self._setItem(name, value);
}
};
// Tests
// -----
const testing = @import("../../testing.zig");
test "Browser.Storage.LocalStorage" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "localStorage.length", "0" },
.{ "localStorage.setItem('foo', 'bar')", "undefined" },
.{ "localStorage.length", "1" },
.{ "localStorage.getItem('foo')", "bar" },
.{ "localStorage.removeItem('foo')", "undefined" },
.{ "localStorage.length", "0" },
// .{ "localStorage['foo'] = 'bar'", "undefined" },
// .{ "localStorage['foo']", "bar" },
// .{ "localStorage.length", "1" },
.{ "localStorage.clear()", "undefined" },
.{ "localStorage.length", "0" },
}, .{});
test "Browser: Storage.LocalStorage" {
try testing.htmlRunner("storage/local_storage.html");
}
test "storage bottle" {
test "Browser: Storage.Bottle" {
var bottle = Bottle.init(std.testing.allocator);
defer bottle.deinit();

View File

@@ -54,6 +54,11 @@ pub const URL = struct {
uri: std.Uri,
search_params: URLSearchParams,
pub const empty = URL{
.uri = .{ .scheme = "" },
.search_params = .{},
};
const URLArg = union(enum) {
url: *URL,
element: *parser.ElementHTML,
@@ -84,7 +89,17 @@ pub const URL = struct {
raw = if (url == .url) url_str else try arena.dupe(u8, url_str);
}
const uri = std.Uri.parse(raw.?) catch return error.TypeError;
const uri = std.Uri.parse(raw.?) catch blk: {
if (!std.mem.endsWith(u8, raw.?, "://")) {
return error.TypeError;
}
// schema only is valid!
break :blk std.Uri{
.scheme = raw.?[0 .. raw.?.len - 3],
.host = .{ .percent_encoded = "" },
};
};
return init(arena, uri);
}
@@ -99,16 +114,16 @@ pub const URL = struct {
}
pub fn get_origin(self: *URL, page: *Page) ![]const u8 {
var buf = std.ArrayList(u8).init(page.arena);
try self.uri.writeToStream(.{
var aw = std.Io.Writer.Allocating.init(page.arena);
try self.uri.writeToStream(&aw.writer, .{
.scheme = true,
.authentication = false,
.authority = true,
.path = false,
.query = false,
.fragment = false,
}, buf.writer());
return buf.items;
});
return aw.written();
}
// get_href returns the URL by writing all its components.
@@ -122,28 +137,28 @@ pub const URL = struct {
// format the url with all its components.
pub fn toString(self: *const URL, arena: Allocator) ![]const u8 {
var buf: std.ArrayListUnmanaged(u8) = .empty;
try self.uri.writeToStream(.{
var aw = std.Io.Writer.Allocating.init(arena);
try self.uri.writeToStream(&aw.writer, .{
.scheme = true,
.authentication = true,
.authority = true,
.path = uriComponentNullStr(self.uri.path).len > 0,
}, buf.writer(arena));
});
if (self.search_params.get_size() > 0) {
try buf.append(arena, '?');
try self.search_params.write(buf.writer(arena));
try aw.writer.writeByte('?');
try self.search_params.write(&aw.writer);
}
{
const fragment = uriComponentNullStr(self.uri.fragment);
if (fragment.len > 0) {
try buf.append(arena, '#');
try buf.appendSlice(arena, fragment);
try aw.writer.writeByte('#');
try aw.writer.writeAll(fragment);
}
}
return buf.items;
return aw.written();
}
pub fn get_protocol(self: *URL, page: *Page) ![]const u8 {
@@ -159,17 +174,16 @@ pub const URL = struct {
}
pub fn get_host(self: *URL, page: *Page) ![]const u8 {
var buf = std.ArrayList(u8).init(page.arena);
try self.uri.writeToStream(.{
var aw = std.Io.Writer.Allocating.init(page.arena);
try self.uri.writeToStream(&aw.writer, .{
.scheme = false,
.authentication = false,
.authority = true,
.path = false,
.query = false,
.fragment = false,
}, buf.writer());
return buf.items;
});
return aw.written();
}
pub fn get_hostname(self: *URL) []const u8 {
@@ -180,9 +194,9 @@ pub const URL = struct {
const arena = page.arena;
if (self.uri.port == null) return try arena.dupe(u8, "");
var buf = std.ArrayList(u8).init(arena);
try std.fmt.formatInt(self.uri.port.?, 10, .lower, .{}, buf.writer());
return buf.items;
var aw = std.Io.Writer.Allocating.init(arena);
try aw.writer.printInt(self.uri.port.?, 10, .lower, .{});
return aw.written();
}
pub fn get_pathname(self: *URL) []const u8 {
@@ -487,155 +501,10 @@ const ValueIterable = iterator.Iterable(kv.ValueIterator, "URLSearchParamsValueI
const EntryIterable = iterator.Iterable(kv.EntryIterator, "URLSearchParamsEntryIterator");
const testing = @import("../../testing.zig");
test "Browser.URL" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "var url = new URL('https://foo.bar/path?query#fragment')", "undefined" },
.{ "url.origin", "https://foo.bar" },
.{ "url.href", "https://foo.bar/path?query#fragment" },
.{ "url.protocol", "https:" },
.{ "url.username", "" },
.{ "url.password", "" },
.{ "url.host", "foo.bar" },
.{ "url.hostname", "foo.bar" },
.{ "url.port", "" },
.{ "url.pathname", "/path" },
.{ "url.search", "?query" },
.{ "url.hash", "#fragment" },
.{ "url.searchParams.get('query')", "" },
.{ "url.search = 'hello=world'", null },
.{ "url.searchParams.size", "1" },
.{ "url.searchParams.get('hello')", "world" },
.{ "url.search = '?over=9000'", null },
.{ "url.searchParams.size", "1" },
.{ "url.searchParams.get('over')", "9000" },
.{ "url.search = ''", null },
.{ "url.searchParams.size", "0" },
.{ " const url2 = new URL(url);", null },
.{ "url2.href", "https://foo.bar/path#fragment" },
.{ " try { new URL(document.createElement('a')); } catch (e) { e }", "TypeError: invalid argument" },
.{ " let a = document.createElement('a');", null },
.{ " a.href = 'https://www.lightpanda.io/over?9000=!!';", null },
.{ " const url3 = new URL(a);", null },
.{ "url3.href", "https://www.lightpanda.io/over?9000=%21%21" },
}, .{});
try runner.testCases(&.{
.{ "var url = new URL('https://foo.bar/path?a=~&b=%7E#fragment')", "undefined" },
.{ "url.searchParams.get('a')", "~" },
.{ "url.searchParams.get('b')", "~" },
.{ "url.searchParams.append('c', 'foo')", "undefined" },
.{ "url.searchParams.get('c')", "foo" },
.{ "url.searchParams.getAll('c').length", "1" },
.{ "url.searchParams.getAll('c')[0]", "foo" },
.{ "url.searchParams.size", "3" },
// search is dynamic
.{ "url.search", "?a=~&b=~&c=foo" },
// href is dynamic
.{ "url.href", "https://foo.bar/path?a=~&b=~&c=foo#fragment" },
.{ "url.searchParams.delete('c', 'foo')", "undefined" },
.{ "url.searchParams.get('c')", "null" },
.{ "url.searchParams.delete('a')", "undefined" },
.{ "url.searchParams.get('a')", "null" },
}, .{});
try runner.testCases(&.{
.{ "var url = new URL('over?9000', 'https://lightpanda.io')", null },
.{ "url.href", "https://lightpanda.io/over?9000" },
}, .{});
test "Browser: URL" {
try testing.htmlRunner("url/url.html");
}
test "Browser.URLSearchParams" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let usp = new URLSearchParams()", null },
.{ "usp.get('a')", "null" },
.{ "usp.has('a')", "false" },
.{ "usp.getAll('a')", "" },
.{ "usp.delete('a')", "undefined" },
.{ "usp.set('a', 1)", "undefined" },
.{ "usp.has('a')", "true" },
.{ "usp.get('a')", "1" },
.{ "usp.getAll('a')", "1" },
.{ "usp.append('a', 2)", "undefined" },
.{ "usp.has('a')", "true" },
.{ "usp.get('a')", "1" },
.{ "usp.getAll('a')", "1,2" },
.{ "usp.append('b', '3')", "undefined" },
.{ "usp.has('a')", "true" },
.{ "usp.get('a')", "1" },
.{ "usp.getAll('a')", "1,2" },
.{ "usp.has('b')", "true" },
.{ "usp.get('b')", "3" },
.{ "usp.getAll('b')", "3" },
.{ "let acc = [];", null },
.{ "for (const key of usp.keys()) { acc.push(key) }; acc;", "a,a,b" },
.{ "acc = [];", null },
.{ "for (const value of usp.values()) { acc.push(value) }; acc;", "1,2,3" },
.{ "acc = [];", null },
.{ "for (const entry of usp.entries()) { acc.push(entry) }; acc;", "a,1,a,2,b,3" },
.{ "acc = [];", null },
.{ "for (const entry of usp) { acc.push(entry) }; acc;", "a,1,a,2,b,3" },
.{ "usp.delete('a')", "undefined" },
.{ "usp.has('a')", "false" },
.{ "usp.has('b')", "true" },
.{ "acc = [];", null },
.{ "for (const key of usp.keys()) { acc.push(key) }; acc;", "b" },
.{ "acc = [];", null },
.{ "for (const value of usp.values()) { acc.push(value) }; acc;", "3" },
.{ "acc = [];", null },
.{ "for (const entry of usp.entries()) { acc.push(entry) }; acc;", "b,3" },
.{ "acc = [];", null },
.{ "for (const entry of usp) { acc.push(entry) }; acc;", "b,3" },
}, .{});
try runner.testCases(&.{
.{ "usp = new URLSearchParams('?hello')", null },
.{ "usp.get('hello')", "" },
.{ "usp = new URLSearchParams('?abc=')", null },
.{ "usp.get('abc')", "" },
.{ "usp = new URLSearchParams('?abc=123&')", null },
.{ "usp.get('abc')", "123" },
.{ "usp.size", "1" },
.{ "var fd = new FormData()", null },
.{ "fd.append('a', '1')", null },
.{ "fd.append('a', '2')", null },
.{ "fd.append('b', '3')", null },
.{ "ups = new URLSearchParams(fd)", null },
.{ "ups.size", "3" },
.{ "ups.getAll('a')", "1,2" },
.{ "ups.getAll('b')", "3" },
.{ "fd.delete('a')", null }, // the two aren't linked, it created a copy
.{ "ups.size", "3" },
.{ "ups = new URLSearchParams({over: 9000, spice: 'flow'})", null },
.{ "ups.size", "2" },
.{ "ups.getAll('over')", "9000" },
.{ "ups.getAll('spice')", "flow" },
}, .{});
test "Browser: URLSearchParams" {
try testing.htmlRunner("url/url_search_params.html");
}

Some files were not shown because too many files have changed in this diff Show More