371 Commits

Author SHA1 Message Date
Halil Durak
1015fc09ee Merge pull request #1170 from lightpanda-io/nikneym/ada-in-web-apis
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 ada-url for URL operations in web APIs
2025-10-23 12:13:45 +03:00
Pierre Tachoire
1c37b1c70e Merge pull request #1173 from lightpanda-io/renderer-size
renderer: set a default box size of 5 pixels
2025-10-23 10:17:10 +02:00
Pierre Tachoire
2422c8718c renderer: set a default box size of 5 pixels 2025-10-22 15:54:43 +02:00
Halil Durak
8d4cf400ce bring back invalidUrl test with error expectation 2025-10-22 15:03:06 +03:00
Halil Durak
c6a0368c61 add a searchParamsSetHref test according to href setter change 2025-10-22 15:02:48 +03:00
Halil Durak
033eb82ae5 reinitialize search_params too when href set 2025-10-22 15:01:27 +03:00
Halil Durak
2d14452dda remove stale todo comments 2025-10-22 13:40:44 +03:00
Halil Durak
d4d35670a0 prefer call_arena in web APIs 2025-10-22 11:42:16 +03:00
Halil Durak
7d39bc979f remove invalidUrl test in url.html 2025-10-21 16:50:16 +03:00
Halil Durak
d60d3ebaac update link.html test 2025-10-21 16:49:48 +03:00
Halil Durak
ba66b7c5db refactor HTMLAnchorElement regarding to URL changes
This still doesn't use `state` since `state` doesn't allow us to iterate the nodes when releasing the memory and we need to call `URL.destructor` when freeing. In the future, we might omit getter allocations by making such change.
2025-10-21 16:49:12 +03:00
Halil Durak
8342f0c394 omit try keyword when not necessary 2025-10-21 16:46:06 +03:00
Halil Durak
69884b9d8d Location changes regarding to changes in URL 2025-10-21 16:44:29 +03:00
Halil Durak
c568a75599 refactor URL web API 2025-10-21 16:43:09 +03:00
Halil Durak
9deb5249a9 introduce ada-url to build system
Also add ada-url bindings.
2025-10-21 16:42:01 +03:00
Pierre Tachoire
fb6fbffe3f Merge pull request #1169 from lightpanda-io/cdp-security-ignore-cert-err
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
cdp: implement Security.setIgnoreCertificateErrors
2025-10-21 15:15:51 +02:00
Pierre Tachoire
510c61cc20 cdp: add test for setIgnoreCertificateErrors 2025-10-21 14:08:26 +02:00
Pierre Tachoire
6915738e02 cdp: ensure no inflight conns is running before set TLS verify 2025-10-21 14:07:59 +02:00
Pierre Tachoire
4f62cc833b http: fix VERIFY_HOST value 2025-10-21 13:47:09 +02:00
Karl Seguin
46ffb801db Merge pull request #1168 from lightpanda-io/dom_range_fixes
Reverses 2 incorrect comparions
2025-10-21 19:45:32 +08:00
Pierre Tachoire
d2065f713f cdp: implement Security.setIgnoreCertificateErrors 2025-10-21 13:44:29 +02:00
Karl Seguin
6f8c3abb55 Merge pull request #1167 from lightpanda-io/typos
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
correct typos (all in comments)
2025-10-21 18:09:27 +08:00
Karl Seguin
163a0e8b70 Reverses 2 incorrect comparions
A bit obvious when you see the "expected -1 but got 1".

Goal is to bring us over 40K passing :)
2025-10-21 18:08:05 +08:00
Karl Seguin
ca3efb3ad9 correct typos (all in comments) 2025-10-21 16:17:38 +08:00
Karl Seguin
4468932346 Merge pull request #1166 from gootik/patch-1
Fix typo in 'input' selector check
2025-10-21 16:04:03 +08:00
Sasan Hezarkhani
9a03ba61c5 Fix typo in 'input' selector check
Fix a small typo in selector check
2025-10-20 21:30:35 -07:00
Karl Seguin
fe3777041d Merge pull request #1164 from lightpanda-io/nix_0.15.2
Update `flake.lock` for Zig 0.15.2
2025-10-21 08:18:46 +08:00
Muki Kiboigo
1c579a98b4 update flake.lock 2025-10-20 07:20:46 -07:00
Karl Seguin
3e10cf0a64 Merge pull request #1163 from lightpanda-io/zig_0_15_2
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 to Zig 0.15.2 - no code changes
2025-10-20 17:32:59 +08:00
Karl Seguin
ef9784a7d4 Upgrade to Zig 0.15.2 - no code changes 2025-10-20 16:44:45 +08:00
Karl Seguin
6f1c3c8fd2 Merge pull request #1162 from lightpanda-io/fix_node_iterator_regression
Fixes the regression to node iterator
2025-10-20 16:44:11 +08:00
Karl Seguin
e12c650ea5 Fixes the regression to node iterator
Caused by: https://github.com/lightpanda-io/browser/pull/1149/

WPT go from 727/766 (the pre-regression value) to 744/766.
2025-10-20 16:28:07 +08:00
Karl Seguin
9373cbb440 Merge pull request #1159 from lightpanda-io/make_test_filter_compiler
Filter out the huge compile command when using `make test`
2025-10-20 15:36:16 +08:00
Pierre Tachoire
fd6d038956 Merge pull request #1152 from lightpanda-io/cdp-inserttext
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
cdp: add input.insertText
2025-10-19 18:16:00 +02:00
Karl Seguin
9845392b71 Simplify filter and try to make it work with progressive build info 2025-10-18 11:18:50 +08:00
Karl Seguin
0795b7a583 Filter out the huge compile command when using make test
I couldn't figure out how (or if it's possible) to do this with build.zig
2025-10-18 08:14:07 +08:00
Karl Seguin
29f0e71f10 Merge pull request #1158 from lightpanda-io/concurrent-waitformodule
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
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
handle multiple waiters for the same module
2025-10-18 07:46:30 +08:00
Karl Seguin
1a47f7b5a8 Merge pull request #1157 from lightpanda-io/rootNode_composed
support the composed option of getRootNode()
2025-10-18 07:44:38 +08:00
Karl Seguin
6a30ab7a57 Merge pull request #1156 from lightpanda-io/report_error
add window.reportError
2025-10-18 07:44:24 +08:00
Karl Seguin
758f7deb93 Merge pull request #1155 from lightpanda-io/composition_event
add CompositionEvent
2025-10-18 07:44:13 +08:00
Pierre Tachoire
9f4e3bf792 add a shared boolean to GetResult to avoid deinit 2025-10-17 18:02:21 +02:00
Pierre Tachoire
a5dfe8ab28 handle multiple waiters for the same module 2025-10-17 17:49:56 +02:00
Karl Seguin
c52dce1c48 Merge pull request #1154 from lightpanda-io/module_evalute_error_handling
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
Handle (log) module evaluation errors directly
2025-10-16 19:26:14 +08:00
Karl Seguin
288379aa7d support the composed option of getRootNode() 2025-10-16 19:08:33 +08:00
Karl Seguin
a9739bf361 add window.reportError 2025-10-16 18:33:18 +08:00
Karl Seguin
c69adcb163 add CompositionEvent 2025-10-16 15:57:37 +08:00
Karl Seguin
0b4a1b4a1b Handle (log) module evaluation errors directly
Some module evluation errors aren't handled by the normal TryCatch mechanism.
Instead, the exception needs to be retrieved directly from the module.
2025-10-16 15:10:30 +08:00
Karl Seguin
cc0c1bcf3a Merge pull request #1153 from lightpanda-io/normalized_specifier_lifetime
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 a potential segfault on log message for failing to load module
2025-10-16 15:01:50 +08:00
Karl Seguin
55746f1a1d log the normalized specifier now that we've extended its lifetime to the page.arena 2025-10-16 14:34:07 +08:00
Karl Seguin
7bb8581a95 Fix referrer in log (was printing using the src instead :/) 2025-10-16 14:31:09 +08:00
Karl Seguin
521c0f8460 Fix a potential segfault on log message for failing to load module
Using the `call_arena` here is unsafe in the case of a failure. It's possible
for the call_arena to be reset during module processing, making the log crash.

The issue is that the lifetime of a URL is often conditional. If the stitched
URL has already been seen (i.e. is in the module_cache), then it can be short-
lived. EXCEPT, URL.stitch might require an allocation..and then you start to
think, well, if URL.stitch is going to allocate anyways...If we stitch with
the `page.arena`, and end up not needing a long lifetime, we've wasted memory.
If we stitch with `page.call_arena` and end up needing a long lifetime, we need
to dupe.

It's a bit messy, and I'd like to take a stab at improving it after:
https://github.com/lightpanda-io/browser/pull/1127.

I'm thinking that we need a URL intern pool. HashMap with a composite key of
base + path -> resolved. Then all URLs are resolved using the page.arena, but
we don't have any duplicates, so it isn't wasteful.
2025-10-16 14:15:38 +08:00
Pierre Tachoire
14a23123c0 add Document.hasFocus placeholder 2025-10-15 15:34:06 +02:00
Pierre Tachoire
09be5e23f1 add input.select placeholder 2025-10-15 15:32:27 +02:00
Pierre Tachoire
0aaed08c1e cdp: add input.insertText 2025-10-15 13:52:21 +02:00
Karl Seguin
4bfe3b6fe1 Merge pull request #1151 from lightpanda-io/unicode_nbsp_encoding
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
Encode UTF8 non breaking space (194, 160) as   - same as chrome
2025-10-15 18:28:45 +08:00
Karl Seguin
b610aa1c0c Encode UTF8 non breakingspace (194, 160) as   - same as chrome 2025-10-15 17:34:23 +08:00
Karl Seguin
73da04bea2 Merge pull request #1150 from lightpanda-io/isdone-async
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
isDone must be run after script's deinit
2025-10-15 15:58:42 +08:00
Karl Seguin
18c851e53f Merge pull request #1149 from lightpanda-io/iterators_and_walker_fix
Improve correctness of NodeIterator and Treewalker
2025-10-15 15:58:12 +08:00
Pierre Tachoire
41f4533bc0 isDone must be run after script's deinit 2025-10-15 09:50:17 +02:00
Karl Seguin
4db8a967b6 update netsurf deps 2025-10-15 14:35:58 +08:00
Karl Seguin
ff70f4e79f Merge pull request #1147 from lightpanda-io/svg_tag_name_test
Add tests for svg tag names
2025-10-15 09:47:07 +08:00
Karl Seguin
c9517aff7d Add tests for svg tag names
Depends on: https://github.com/lightpanda-io/libdom/pull/46
2025-10-15 09:37:56 +08:00
Karl Seguin
3657a49a2c Improve correctness of NodeIterator and Treewalker
In their current implementation, both the NodeIterator and TreeWalker would
skip over ignored nodes. However, while the node itself should have been ignored
its children should still be iterated.

For example, when going over:

```
<div id="container">
  <!-- comment1 -->
  <span>
    <!-- comment2 -->
  </span>
</div>
```

With `SHOW_COMMENT`, the previous version would completely skip over `container`
and its children. Now the code still won't emit the `container` div itself,
it will still iterate through its children (and thus emit the two comments).

This change relates to ongoing react compatibility.
2025-10-15 09:23:54 +08:00
Karl Seguin
71e7aa5262 Merge pull request #1146 from lightpanda-io/test_normalized_text_nodes
add a test for the changes to parsing adjascent text ndoes
2025-10-15 08:13:52 +08:00
Karl Seguin
2e435f5d4e Merge pull request #1145 from lightpanda-io/page_events
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
Fire page lifecycle events when all scripts are either inline or async
2025-10-14 19:48:59 +08:00
Karl Seguin
859b03c4a6 update libdom and libhubbub 2025-10-14 19:46:21 +08:00
Karl Seguin
ee8786444f add another test 2025-10-14 13:48:23 +08:00
Karl Seguin
d87d782fd5 Merge pull request #1137 from lightpanda-io/profiler
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
Expose v8 CpuProfiler + add fast properties for some window properties
2025-10-14 05:45:31 +08:00
Karl Seguin
afac4fc37f add a test for the changes to parsing adjascent text ndoes 2025-10-14 00:23:35 +08:00
Karl Seguin
de83521e08 Fire page lifecycle events when all scripts are either inline or async
This is how, for example, scripts on lightpanda.io are. Though fixing this
doesn't really change anything, it's good to make sure these events are firing
correctly.
2025-10-13 21:53:58 +08:00
Karl Seguin
99f8fe1592 Merge pull request #1139 from lightpanda-io/inspector-deinit
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
cdp: drain microtasks before inspector deinit
2025-10-11 08:14:37 +08:00
Pierre Tachoire
02c092a122 Merge pull request #1140 from lightpanda-io/invalid-errdefer
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 invalid errdefer
2025-10-10 18:54:16 +02:00
Pierre Tachoire
70ca74747f remove invalid errdefer 2025-10-10 18:09:57 +02:00
Pierre Tachoire
594d754022 cdp: drain microtasks before inspector deinit 2025-10-10 17:43:08 +02:00
Karl Seguin
c381e4153d Expose v8 CpuProfiler + add fast properties for some window properties
First, this exposes the v8 Profiler. Right now it's just a commented-out block
in `fetch` and meant for internal debugging.
Depends on: https://github.com/lightpanda-io/zig-v8-fork/pull/105

Use postAttach on Window to attach "static" properties. This comes from
profiling (lightpanda.io) and seeing window.get_self called tens of thousands
of times.
2025-10-10 19:51:29 +08:00
Halil Durak
e761c7e8f4 Merge pull request #1115 from lightpanda-io/nikneym/url-changes
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
Small URL & Location changes
2025-10-10 10:54:47 +03:00
Halil Durak
b8d4e3ac50 change after rebase 2025-10-10 10:43:04 +03:00
Halil Durak
4c2b95d00b always prefer navigateFromWebAPI when navigating from a web API 2025-10-10 10:40:11 +03:00
nikneym
cea4f052ba location: add href setter
* `page.navigateFromWebAPI` seem to be not working while testing; `page.navigate` is preferred instead.
2025-10-10 10:40:11 +03:00
nikneym
9b4ea7a040 add an invalid url test
* this test should not pass; it's related to implementation of `std.Uri` though.
2025-10-10 10:40:10 +03:00
nikneym
26c2b258b4 get_protocol: don't allocate for protocol string 2025-10-10 10:40:04 +03:00
Halil Durak
27c9e18535 Merge pull request #1134 from lightpanda-io/nikneym/default-location
Location: prefer `about:blank` when not navigated yet
2025-10-10 10:33:36 +03:00
Pierre Tachoire
b53c2bfa0c Merge pull request #1135 from lightpanda-io/importmap
Importmap support
2025-10-10 09:33:23 +02:00
Pierre Tachoire
80605633c4 update wpt 2025-10-10 08:46:06 +02:00
Pierre Tachoire
acf06fdd8f Resolve importmap against page's url
Co-authored-by: Karl Seguin <karlseguin@users.noreply.github.com>
2025-10-10 08:34:56 +02:00
Pierre Tachoire
58cc5b4684 typo fix 2025-10-10 08:02:45 +02:00
Karl Seguin
c502bd901e Merge pull request #1136 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
Update libdom
2025-10-10 13:15:11 +08:00
Karl Seguin
55027747fd Update libdom
Apply case-preservation on all SVG elements.
2025-10-10 12:46:27 +08:00
Karl Seguin
f6d77afe2e Merge pull request #1130 from lightpanda-io/intersection_observer
Rework IntersectionObserver
2025-10-10 11:10:08 +08:00
Pierre Tachoire
cd9466dafa free importmap on reset and don't retain capacity 2025-10-09 16:30:29 +02:00
Pierre Tachoire
4bf79e4bc9 add importmap support 2025-10-09 16:09:25 +02:00
Pierre Tachoire
7afecf0f85 move mod specifier resolution js/context => script manager 2025-10-09 16:09:24 +02:00
Halil Durak
0b38b7d473 location: prefer about:blank when not navigated yet 2025-10-09 16:55:05 +03:00
Karl Seguin
1b462da4aa Merge pull request #1133 from lightpanda-io/nikneym/cookie-validation
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 a fast path for validating cookie strings
2025-10-09 20:25:52 +08:00
Halil Durak
07948304b2 fix misleading comment
Co-authored-by: Karl Seguin <karlseguin@users.noreply.github.com>
2025-10-09 14:00:39 +03:00
Halil Durak
0634acdac4 add a fast path for validating cookie strings
This prefers `suggestVectorLength` in order to pick a vector size; for cookie strings shorter than, say 64, this might cause it to fallback to slow path on architectures that support larger vector sizes (like AVX-512). We may also add checks for smaller vector sizes if desired in the future.
2025-10-09 12:03:14 +03:00
Karl Seguin
75e0637d2d Ensure page background tasks are re-registered on reset 2025-10-09 16:29:09 +08:00
Karl Seguin
852c30b2e5 Rework IntersectionObserver
1 - Always fire the callback on the next tick. This is probably the most
important change, as frameworks like React don't react well if the callback is
fired immediately (they expect to continue processing the page in its current
state, not in the mutated state from the callback)

2 - Always fire the callback for observed elements with a parent, whether or
not those intersect or are connected. From MDN, the callback is fired
"The first time the observer is initially asked to watch a target element."

3 - Add a mutation observer so that if a node is added to the root (or removed)
the callback is fired. This, I think, is the best we can currently do for
"intersection".
2025-10-09 14:17:03 +08:00
Karl Seguin
dc85c6552a Merge pull request #1132 from lightpanda-io/reduce_http_tick_blocking
Remove potential processing blocking with CDP
2025-10-09 14:14:05 +08:00
Karl Seguin
76e8506022 Remove potential processing blocking with CDP
When using CDP, we poll the HTTP clients along with the CDP socket. Because this
polling can be long, we first process any pending message. This can end up
processing _all_ messages, in which case the poll will block for a long time.

This change makes it so that when the initial processing processes 1+ message,
we do not poll, but rather return. This allows the page lifecycle to be
processed normally (and not just blocking on poll, waiting for the CDP client
to send data).
2025-10-09 13:18:47 +08:00
Karl Seguin
2d6e2551f6 Merge pull request #1131 from lightpanda-io/microtask-queue-drain
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
drain micro task queue before reset ExecutionWorld and page
2025-10-09 08:36:26 +08:00
Pierre Tachoire
080b1d9a7c drain micro task queue before reset ExecutionWorld and page 2025-10-08 13:55:17 +02:00
Karl Seguin
fe008b0966 Merge pull request #1128 from lightpanda-io/console_trace_svg_test
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
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
Add console.trace and svg attribute test
2025-10-08 00:25:20 +08:00
Karl Seguin
4ad10d057b Add console.trace and svg attribute test
update to latest libdom
2025-10-07 22:26:38 +08:00
Karl Seguin
a65aa9f312 Merge pull request #1126 from lightpanda-io/add_debug_context
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
Attempt to add more context to debug logs.
2025-10-06 17:48:41 +08:00
Karl Seguin
5b43c16f35 Merge pull request #1125 from lightpanda-io/call_arena
Move the call_arena to the page.
2025-10-06 17:22:41 +08:00
Karl Seguin
9cb37dc011 Attempt to add more context to debug logs.
1 - On `unkown global property`, include the stack trace (this might be WAY too
verbose)

2 - On script get, include stack trace (when available)

3 - On script get, include referrer

4 - Stack traces will now include the script name/src when available
2025-10-06 16:56:54 +08:00
Karl Seguin
2ba6737c41 Merge pull request #1119 from lightpanda-io/cdp_log_entry
Emit Log.addEntry
2025-10-06 16:45:48 +08:00
Karl Seguin
33d737f957 Merge pull request #1123 from lightpanda-io/blocking_scripts
Remove the single-blocking-import restrictions
2025-10-06 15:57:29 +08:00
Karl Seguin
381a18a40e Move the call_arena to the page.
The call_arena was previously owned by the js.Context, but it has to exist on
the page, and the page is created before the context, so it's set to undefined
on the page. While this has never caused an issue, there's no reason for the
page not to own this, and the context to simply reference it.

Also, renamed the js.Context.context_arena to simply `arena`, which is more
consistent with other arena names (e.g. page.arena).
2025-10-06 15:52:56 +08:00
Karl Seguin
207f0655dd Merge pull request #1117 from lightpanda-io/cleanup_js
This is the last of the big changes to the js code
2025-10-06 15:33:21 +08:00
Karl Seguin
88d64da257 Merge pull request #1124 from lightpanda-io/brotli
Supports brotli compression
2025-10-06 14:33:25 +08:00
Karl Seguin
cf378dfd6d add brotli include path 2025-10-06 12:39:30 +08:00
Karl Seguin
a3939d9a66 Supports brotli compression
Adds bortli as a submodules, and compiles the decoder, making it available to
libcurl.

Some websites appear to sent brotli encoded responses even though we don't
advertise support for it (e.g. https://movie.douban.com).
2025-10-06 12:30:06 +08:00
Karl Seguin
ef363209a4 Remove the single-blocking-import restrictions
Remove the is_blocking variable (and checks) from the ScriptManager. This is a
holdover to when blocking imports had a dedicated connections, and thus couldn't
be loaded concurrently.

This changes two obvious things (and probably a few subtle ones). The first is
that plain async scripts are now free to be executed as soon as they are
completed. As far as I can tell, this is a safe behavior, is simpler and should
be a bit faster.

More importantly, it allows for chains of imports. normal import (A) -> dynamic
import (B) -> normal import (C). This would previously fail an assertion. The
superficial issue was that dynamic import handling didn't respect the
`is_blocking` flag. But if we did respect it, than module B would never be able
to load module C, which would block module A from ever completing. By removing
the flag, module C will now be properly evaluated, which unblocks module B which
allows module A to unblock.

(1) I believe this is the issue seen here:
    https://github.com/lightpanda-io/browser/issues/1121
2025-10-06 09:48:57 +08:00
Karl Seguin
fe9a10c617 Emit Log.addEntry
Currently, this hooks a single log.Interceptor into the logging framework, but
changing it to take a list shouldn't be too hard. Biggest issue is who will own
it, as we'd need an allocator to maintain a list / lookup (which log doesn't
currently have).

Uses logFmt format, and, for now, always filters out debug messages and a few
particularly verbose scopes.
2025-10-03 17:29:01 +08:00
Karl Seguin
2e734fae57 This is the last of the big changes to the js code
This Pr largely tightens up a lot of the code. 'v8' is no longer imported
outside of js. A number of helper functions have been moved to the js.Context.
For example, js.Function.getName used to call:

```zig
return js.valueToString(allocator, name, self.context.isolate, self.context.v8_context);
```

It now calls:

```zig
return self.context.valueToString(name, .{ .allocator = allocator });
```

Page.main_context has been renamed to `Page.js`. This, in combination with new
promise helpers, turns:

```zig
const resolver = page.main_context.createPromiseResolver();
try resolver.resolve({});
return resolver.promise();
```

into:

```zig
return page.js.resolvePromise({});
```
2025-10-03 15:06:16 +08:00
Karl Seguin
432e3c3a5e Merge pull request #1118 from lightpanda-io/inspector_linking
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
Make sure inspector implementation is always exported
2025-10-03 14:10:47 +08:00
Karl Seguin
a4b13a80ce fix sloppiness 2025-10-03 13:50:53 +08:00
Karl Seguin
a6997a7e85 Make sure inspector implementation is always exported 2025-10-03 13:32:03 +08:00
Karl Seguin
a60d06af6b Merge pull request #1114 from lightpanda-io/extract_js_structs_to_files
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
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Start extract JS structs into their own files
2025-10-03 09:54:21 +08:00
Karl Seguin
dab8012b6a Start extract JS structs into their own files
Renames JsContext -> js.Context, JsObject -> js.Object and JsThis -> js.This
which is more consistent with the other types. The JsObject -> js.Object is
the reason so many files were touched.

This is still a [messy] transition, with more refactoring planned to clean it
up.
2025-10-02 12:48:50 +08:00
Karl Seguin
66f82fd9cc Merge pull request #1109 from lightpanda-io/remove_generic_js
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
Remove the generic nature of Env and most of the JS classes
2025-10-02 10:58:34 +08:00
Karl Seguin
0bff8ba632 Merge pull request #1113 from lightpanda-io/url-stitch-fix
Fix URL `stitch` Issue with parent traversal
2025-10-02 10:21:23 +08:00
Karl Seguin
32226297ab Remove the generic nature of Env and most of the JS classes
Back in the zig-js-runtime days, globals were used for the state and webapi
declarations. This caused problems largely because it was done across
compilation units (using @import("root")...).

The generic Env(S, WebApi) was used to solve these problems, while still making
it work for different States and WebApis.

This change removes the generics and hard-codes the *Page as the state and
only supports our WebApis for the class declarations.

To accommodate this change, the runtime/*tests* have been removed. I don't
consider this a huge loss - whatever behavior these were testing, already
exists in the browser/**/*.zig web api.

As we write more complex/complete WebApis, we're seeing more and more cases
that need to rely on js objects directly (JsObject, Function, Promises, etc...).
The goal is to make these easier to use. Rather than using Env.JsObject, you
now import "js.zig" and use js.JsObject (TODO: rename JsObject to Object).
Everything is just a plain Zig struct, rather than being nested in a generic.

After this change, I plan on:

1 - Renaming the js objects, JsObject -> Object. These should be referenced in
    the webapi as js.Object, js.This, ...

2 - Splitting the code across multiple files (Env.zig, Context.zig,
    Caller.zig, ...)
2025-10-02 10:16:58 +08:00
Karl Seguin
ab18c90b36 Merge pull request #1112 from lightpanda-io/window_scroll
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 window scroll
2025-10-02 09:22:39 +08:00
Karl Seguin
27b6fd561a Merge pull request #1104 from lightpanda-io/fetch_wait
Add Session.fetchWait so that 'fetch' mode will follow navigation
2025-10-02 09:22:29 +08:00
Karl Seguin
15b64d5a25 Improve window scroll
scroll alias for scrollTo

add get_scrollX and get_scrollY, along with their aliases: pageXOffset and
pageYOffset. These always return 0, unless scroll or scrollTo are called.
2025-10-01 18:41:56 +08:00
Karl Seguin
08a50a8ada Merge pull request #1110 from lightpanda-io/telemetry_leak
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
Fixes a 'leak' with telemetry
2025-10-01 17:29:59 +08:00
Karl Seguin
9d172bb29d Fixes a 'leak' with telemetry
This is just something that isn't cleaned up on exit, so it isn't a "leak",
but better to be explicit with the free.
2025-10-01 16:41:20 +08:00
Karl Seguin
c891322129 Merge pull request #1108 from lightpanda-io/wpt_panic_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
Add custom panic handler to printt which file caused a panic
2025-10-01 15:04:24 +08:00
Muki Kiboigo
77434850f7 url traverse down to the root 2025-09-30 22:13:25 -07:00
Karl Seguin
69b65dbd41 Add custom panic handler to printt which file caused a panic 2025-10-01 11:24:41 +08:00
Karl Seguin
c335a545a3 Merge pull request #1107 from lightpanda-io/mutation_observer_improvement
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 correct 'this' on MutationObserver callback
2025-10-01 08:44:07 +08:00
Karl Seguin
5bcccec610 Merge pull request #1103 from lightpanda-io/text_decode_view
Text decode view
2025-10-01 08:42:54 +08:00
Karl Seguin
20ae9c3a53 fix dep link 2025-09-30 21:41:08 +08:00
Karl Seguin
92ca7c5a4b update zig-v8-form 2025-09-30 19:47:41 +08:00
Karl Seguin
37fa41b4a2 fix buffer ranges 2025-09-30 19:47:41 +08:00
Karl Seguin
298f959e13 Add broken TextDecoder test that should pass 2025-09-30 19:47:26 +08:00
Karl Seguin
1cb431f204 Better support for Uint8Array in ReadableStream
There's always going to be ambiguity between a string and a Uint8Array. We
already had TypedArray(u8) as a discriminator when _returning_ values. But now
the type is also used by mapping JS values to Zig. To support this efficiently
when probing the union, the typed array mapping logic was extracted into its
own function (so that it can be used by the probe).
2025-09-30 19:47:22 +08:00
Karl Seguin
74dc7b278b Merge pull request #1105 from lightpanda-io/fix_bad_window_test
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 typo and wrong API in window test
2025-09-30 19:44:10 +08:00
Karl Seguin
b47d8a794c Use correct 'this' on MutationObserver callback
Add support for MutationObserver.disconnect
2025-09-30 19:36:06 +08:00
Halil Durak
eaf845959c Merge pull request #1106 from lightpanda-io/nikneym/window-onload-fix
Don't allow object to be set on `window.onload`
2025-09-30 14:12:01 +03:00
Karl Seguin
651521d346 Merge pull request #1102 from lightpanda-io/readable_stream_uint8array
Better support for Uint8Array in ReadableStream
2025-09-30 19:03:46 +08:00
nikneym
fb37b29671 don't allow object to be set on window.onload 2025-09-30 12:38:08 +03:00
Karl Seguin
2ecf9016ba Better support for Uint8Array in ReadableStream
There's always going to be ambiguity between a string and a Uint8Array. We
already had TypedArray(u8) as a discriminator when _returning_ values. But now
the type is also used by mapping JS values to Zig. To support this efficiently
when probing the union, the typed array mapping logic was extracted into its
own function (so that it can be used by the probe).
2025-09-30 16:32:55 +08:00
Karl Seguin
444b08be32 fix typo and wrong API in window test 2025-09-30 16:28:47 +08:00
Karl Seguin
2b84712eee Add Session.fetchWait so that 'fetch' mode will follow navigation 2025-09-30 13:36:05 +08:00
Karl Seguin
20cb6cdd8b Merge pull request #1091 from lightpanda-io/concurrent_blocking_imports
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
Concurrent blocking imports
2025-09-30 12:30:42 +08:00
Karl Seguin
477a5e5338 Merge pull request #1088 from lightpanda-io/nonblocking_dynamic_imports
nonblocking dynamic imports
2025-09-30 12:30:31 +08:00
Karl Seguin
2a151229cb Merge pull request #1101 from lightpanda-io/nikneym/window-onload
Add `window.onload` getter and setter
2025-09-30 09:15:40 +08:00
nikneym
1d50e091c7 add window.onload test 2025-09-29 14:45:47 +03:00
nikneym
c587e380a0 add window.onload getter and setter 2025-09-29 14:45:35 +03:00
Karl Seguin
54f9bfba84 Merge pull request #1099 from lightpanda-io/nikneym/qol-changes
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
Small changes
2025-09-29 17:39:32 +08:00
Karl Seguin
489ba131c5 Merge pull request #1097 from lightpanda-io/check_visibility_opts
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 missing checkVisibility options
2025-09-29 15:18:10 +08:00
Karl Seguin
5eac1a146f Merge pull request #1098 from lightpanda-io/html_collection_indexed_accessor
Replace HTMLCollection postAttach's with indexed/named getter
2025-09-29 15:17:57 +08:00
Karl Seguin
d7ce6bdeff Replace HTMLCollection postAttach's with indexed/named getter
This solves two issues. First, it's more correct, the indexers should be live.
Second, it makes sure that anything with an HTMLCollection prototype, like
HTMLOptionsCollection, also gets access to the index getters.

We could solve the 2nd issue by making `postAttach` work up the prototype
chain, but since postAttach is wrong (not live), I prefer this solution.
2025-09-29 14:03:59 +08:00
Karl Seguin
e88473d090 add missing checkVisibility options 2025-09-29 12:04:11 +08:00
nikneym
b9024ab032 set_innerHTML: simpler iteration 2025-09-26 15:38:23 +03:00
nikneym
98906be0f6 parseData: remove iterator variant 2025-09-26 15:38:22 +03:00
Pierre Tachoire
220775715d Merge pull request #1094 from lightpanda-io/wpt-debug
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
ci: use debug mode for WPT tests
2025-09-26 13:54:17 +02:00
Pierre Tachoire
ecbf52157b ci: use debug mode for WPT tests 2025-09-26 13:33:13 +02:00
Pierre Tachoire
a579977f66 Merge pull request #1086 from lightpanda-io/history
Implement `History` WebAPI.
2025-09-26 12:15:07 +02:00
Karl Seguin
418dc6fdc2 Start downloading all synchronous imports ASAP
This changes how non-async module loading works. In general, module loading
is triggered by a v8 callback. We ask it to process a module (a <script type=
module>) and then for every module that it depends on, we get a callback. This
callback expects the nested v8.Module instance, so we need to load it then and
there (as opposed to dynamic imports, where we only have to return a promise).

Previously, we solved this by issuing a blocking HTTP get in each callback. The
HTTP loop was able to continuing downloading already-queued resources, but if
a module depended on 20 nested modules, we'd issue 20 blocking gets one after
the other.

Once a module is compiled, we can ask v8 for a list of its dependent module. We
can them immediately start to download all of those modules. We then evaluate
the original module, which will trigger our callback. At this point, we still
need to block and wait for the response, but we've already started the download
and it's much faster. Sure, for the first module, we might need to wait the same
amount of time, but for the other 19, chances are by the time the callback
executes, we already have it downloaded and ready.
2025-09-26 15:38:50 +08:00
Karl Seguin
2aa4b03673 try to cleanup persisted references 2025-09-26 15:34:32 +08:00
Karl Seguin
f236a65a79 Merge pull request #1092 from lightpanda-io/nikneym/insert-adjacent-html
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 `Element#insertAdjacentHTML`
2025-09-26 14:51:08 +08:00
nikneym
f7b08a1160 prefer orelse return instead of orelse unreachable 2025-09-26 09:43:30 +03:00
Karl Seguin
eed10dd1bb Apply suggestions from code review
fix typos

Co-authored-by: Pierre Tachoire <pierre@lightpanda.io>
2025-09-26 10:37:31 +08:00
Muki Kiboigo
9992bd0999 clean up history api 2025-09-25 12:33:30 -07:00
nikneym
6912175e7e prefer $ instead of document.querySelector 2025-09-25 19:30:10 +03:00
nikneym
a59c32757e assert that nodes exist 2025-09-25 19:29:44 +03:00
nikneym
2438a0e60b fix comment 2025-09-25 19:17:08 +03:00
nikneym
a850a902ce make sure parent is not Document in beforebegin and afterend 2025-09-25 15:04:26 +03:00
nikneym
b7ba993ba6 improve insertAdjacentHTML test 2025-09-25 14:42:58 +03:00
nikneym
3eb0d57d5b correct element insertation in insertAdjacentHTML
* also DRY since the loop is repeated multiple times.
2025-09-25 14:41:50 +03:00
Karl Seguin
6bf2ff9168 Protect against context changing during module resolution. 2025-09-25 13:39:02 +08:00
Karl Seguin
92226a8d06 Merge pull request #1090 from lightpanda-io/script_data_url_test
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 script dataurl test
2025-09-25 10:15:47 +08:00
Karl Seguin
134424dfdc add script dataurl test 2025-09-25 08:18:59 +08:00
Karl Seguin
58ceb66452 Merge pull request #1089 from lightpanda-io/fix-datauri
fix data uri scripts
2025-09-25 08:15:36 +08:00
nikneym
902b8fc789 add insertAdjacentHTML test 2025-09-24 20:26:05 +03:00
nikneym
923491a510 make ref_node of nodeInsertBefore nullable 2025-09-24 20:21:48 +03:00
nikneym
255b45d07b initial insertAdjacentHTML attempt 2025-09-24 20:21:08 +03:00
Pierre Tachoire
8f68b5b289 fix data uri scripts 2025-09-24 17:29:23 +02:00
Karl Seguin
252fd78473 remove duplicate put, add more assertions 2025-09-24 22:44:46 +08:00
Karl Seguin
b692c5db60 nonblocking dynamic imports
Allows dynamic imports to be loading asynchronously. I know reddit isnt the
best example, since it doesn't fully load, but this reduced the load time from
~7.2s to ~4.8s.
2025-09-24 22:28:22 +08:00
Pierre Tachoire
eff7d58f4b Merge pull request #1087 from lightpanda-io/fix-beyboardevent
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 pointer parameter into KeyboardEvent contructor
2025-09-24 09:56:47 +02:00
Pierre Tachoire
17e9bdf8e8 fix pointer parameter into MouseEvent contructor 2025-09-24 09:40:24 +02:00
Pierre Tachoire
22d2694b71 fix pointer parameter into KeyboardEvent contructor 2025-09-24 09:29:37 +02:00
Muki Kiboigo
e74d7fa454 add popstate event for History 2025-09-24 00:22:20 -07:00
Muki Kiboigo
464f42a121 add history tests 2025-09-24 00:21:16 -07:00
Muki Kiboigo
05e7079178 functional history WebAPI 2025-09-24 00:21:16 -07:00
Muki Kiboigo
f03fcc9a31 support for returning Env.Value 2025-09-24 00:21:16 -07:00
Muki Kiboigo
c3ad054bb3 add toJson object and fromJson value 2025-09-24 00:21:16 -07:00
Karl Seguin
202e137d77 Merge pull request #1084 from lightpanda-io/slotchange
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
Dispatch slotchange event
2025-09-24 09:23:28 +08:00
Karl Seguin
6b35664e37 Merge pull request #1079 from lightpanda-io/dynamic_import_caching
Dynamic import caching
2025-09-24 09:23:16 +08:00
Karl Seguin
1a7dbd56ac Dispatch slotchange event
The first time a `slotchange` event is registered, we setup a SlotChangeMonitor
on the page. This uses a global (ugh) MutationEvent to detect slot changes.

We could improve the perfomance of this by installing a MutationEvent per
custom element, but a global is obviously a lot easier.

Our MutationEvent currently fired _during_ the changes. This is problematic
(in general, but specifically for slotchange). You can image something like:

```
slot.addEventListener('slotchange', () => {
   // do something with slot.assignedNodes()
});
```

But, if we dispatch the `slotchange` during the MutationEvent, assignedNodes
will return old nodes. So, our SlotChangeMonitor uses the page scheduler to
schedule dispatches on the next tick.
2025-09-23 17:41:05 +08:00
Karl Seguin
1a40853aae Merge pull request #1082 from lightpanda-io/response_type
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
Set Response.type to basic on same-origin requests
2025-09-23 14:23:16 +08:00
Karl Seguin
6bad2b16e4 Set Response.type to basic on same-origin requests 2025-09-23 11:35:51 +08:00
Karl Seguin
db166b4633 Merge pull request #1081 from lightpanda-io/nikneym/link-rel
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 `rel` property to `HTMLLinkElement`
2025-09-22 22:35:34 +08:00
nikneym
71bc624a74 add a link element test 2025-09-22 16:35:06 +03:00
nikneym
907a941795 add rel setter to HTMLLinkElement 2025-09-22 16:34:37 +03:00
Pierre Tachoire
559783eed7 Merge pull request #1080 from lightpanda-io/bump-netsurf
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
update libdom version
2025-09-22 14:26:24 +02:00
nikneym
68585c8837 add rel getter to HTMLLinkElement 2025-09-22 15:08:07 +03:00
Pierre Tachoire
eccbc9d9b3 update libdom version 2025-09-22 11:19:28 +02:00
Karl Seguin
e7d1d55170 update zig-v8-fork 2025-09-22 15:19:28 +08:00
Karl Seguin
f04754c254 Correct dynamic module loading/caching
Refactors some of the module loading logic. Both normal modules import and
dynamic module import now share more of the same code - they both go through
the slightly modified `module` function.

Dynamic modules now check the cache first, before loading, and when cached,
resolve the correct promise. This can now happen regardless of the module
loading state.

Also tried to replace some page arenas with call arenas and added some basic
tests for both normal and dynamic module loading.
2025-09-22 15:15:00 +08:00
Karl Seguin
a8e5a48b87 Merge pull request #1074 from lightpanda-io/cdp-nodeid
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
cdp: start nodeId from 1 instead of 0
2025-09-20 07:21:20 +08:00
Pierre Tachoire
283a9af406 cdp: start nodeId from 1 instead of 0
chromedp expects the nodeId starts to 1.
A start to 0 make it enter in infinite loop b/c it expects the Go's
default int, ie 0, to be nil from a map to stop the loop.
If the 0 index is set, it will loop...
2025-09-19 17:58:37 +02:00
Karl Seguin
e3896455db Merge pull request #1073 from lightpanda-io/increase_mimalloc_get_rss_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
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
Seems 4K isn't always enough
2025-09-19 19:38:28 +08:00
Karl Seguin
5e6d2700a2 Merge pull request #1070 from lightpanda-io/dump_strip_mode
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
Replace --noscript with more advanced --strip_mode
2025-09-19 19:25:06 +08:00
Karl Seguin
dfd0dfe0f6 Seems 4K isn't always enough 2025-09-19 19:22:02 +08:00
Pierre Tachoire
e6b9be5020 Merge pull request #1072 from lightpanda-io/assert_corretly_set_exit_when_done
Ensure extra_socket can't happen when exit_when_done == true
2025-09-19 12:20:33 +02:00
Pierre Tachoire
6f7c87516f Merge pull request #1067 from lightpanda-io/more_testing_metrics
Add libdom RSS and v8 total_physical_size to testing --json output
2025-09-19 12:16:47 +02:00
Pierre Tachoire
516a78326d Merge pull request #1066 from lightpanda-io/nikneym/relaxed-post-message
Relaxed `MessagePort.postMessage`
2025-09-19 11:14:35 +02:00
Karl Seguin
853b7f84ef Ensure extra_socket can't happen when exit_when_done == true
exit_when_done is pretty much a sneaky way to get CDP knowledge into the page.
exit_when_done == true means "this isn't a CDP session".

extra_socket is another sneaky weay to get CDP knowledge into the page. When
we get an `extra_socket` message it means "Return control to the CDP server".

Therefore it should be impossible to get an `extra_socket` message (return to
CDP) when `exit_when_done == true` (this isn't a CDP session).
2025-09-19 16:59:36 +08:00
Karl Seguin
b248a2515e Merge pull request #1071 from lightpanda-io/nikneym/element-dir
Add `element.dir` getter & setter
2025-09-19 16:51:32 +08:00
nikneym
6826c42c65 check for correct dir in HTML elements 2025-09-19 11:30:15 +03:00
nikneym
4f041e48a3 make sure dir attribute is parsed if provided 2025-09-19 11:26:53 +03:00
nikneym
ec6800500b add a test for element.dir 2025-09-19 11:11:58 +03:00
nikneym
856d65a8e9 add element.dir getter & setter 2025-09-19 10:48:37 +03:00
Karl Seguin
8a2efde365 Merge pull request #1069 from lightpanda-io/response-gettype
Adds `Response.type`
2025-09-19 15:12:10 +08:00
Karl Seguin
2ddcc6d9e6 Replace --noscript with more advanced --strip_mode
--noscript is deprecated (warning) and automatically maps to --strip_mode js

--strip_mode takes a comma separated list of values. From the help:

- "js" script and link[as=script, rel=preload]
- "ui" includes img, picture, video, css and svg
- "css" includes style and link[rel=stylesheet]
- "full" includes js, ui and css

Maybe this is overkill, but i sometimes find myself looking --dump outputs over
and over again, and removing noise (like HUGE svgs) seems like a small
improvement.
2025-09-19 14:27:53 +08:00
Muki Kiboigo
25962326d2 add support for Response.type 2025-09-18 22:27:51 -07:00
Karl Seguin
bbc2fbf984 Merge pull request #1068 from lightpanda-io/fix_wpt_runner_user_agent
git wpt runner a (not required) user_agent
2025-09-19 13:07:14 +08:00
Karl Seguin
edc53d6de3 git wpt runner a (not required) user_agent 2025-09-19 12:38:40 +08:00
Karl Seguin
47710210bd Add libdom RSS and v8 total_physical_size to testing --json output
https://github.com/lightpanda-io/browser/issues/1057
2025-09-19 10:21:39 +08:00
Pierre Tachoire
823b7f0670 Merge pull request #1064 from lightpanda-io/testing_metrics
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
Re-enable test metrics
2025-09-18 18:03:57 +02:00
Pierre Tachoire
f5130ce48f Merge pull request #1061 from lightpanda-io/remove_inline
Remove all inlines
2025-09-18 17:59:35 +02:00
Halil Durak
347524a5b3 Add setImmediate, clearImmediate (#1065) 2025-09-18 17:56:09 +02:00
nikneym
51830f5907 relaxed MessagePort.postMessage 2025-09-18 17:07:12 +03:00
Karl Seguin
346f538c3b Re-enable test metrics
Both the durations and allocations will be _much_ higher with the new htmlRunner
which, for example, does 2 HTTP requests per test (html, testing.js).

https://github.com/lightpanda-io/browser/issues/1057
2025-09-18 19:55:37 +08:00
Karl Seguin
9d2948ff50 Remove all inlines
Following Zig recommendation not to inline except in specific cases, none of
which I think applies to use.

Also, mimalloc.create can't fail (it used to be possible, but that changed a
while ago), so removed its error return.
2025-09-18 19:10:22 +08:00
Karl Seguin
36ce227bf6 Merge pull request #1055 from lightpanda-io/env_string
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
Introduces an Env.String for persistent strings
2025-09-18 19:06:46 +08:00
Karl Seguin
024f7ad9ef Merge pull request #1056 from lightpanda-io/DOM_NO_ERR
Convert more DOM_NO_ERR cases to assertions
2025-09-18 19:06:32 +08:00
Karl Seguin
f8425fe614 Merge pull request #1063 from lightpanda-io/remove_jsrunner
Remove JSRunner
2025-09-18 18:46:59 +08:00
Karl Seguin
7802a1b5a4 Merge pull request #1062 from lightpanda-io/fetch_newHeaders
use client.newHeaders
2025-09-18 15:56:35 +08:00
Karl Seguin
17549d8a43 Remove JSRunner
It only had a few fetch tests still using it. But now everything is migrated
to the htmlRunner
2025-09-18 15:50:19 +08:00
Karl Seguin
f6ed706855 use client.newHeaders 2025-09-18 15:46:23 +08:00
Pierre Tachoire
89ef25501b Merge pull request #1060 from lightpanda-io/fetch-ua
fetch: init headers w page's client UA
2025-09-18 09:44:00 +02:00
Pierre Tachoire
4870125e64 fetch: init headers w page's client UA 2025-09-18 09:34:55 +02:00
Pierre Tachoire
2d24e3c7f7 Merge pull request #972 from lightpanda-io/fetch
Fetch + ReadableStream
2025-09-18 09:29:05 +02:00
Karl Seguin
cdb3f46506 Merge pull request #1059 from lightpanda-io/user_agent_suffix
Add --user_agent_suffix argument
2025-09-18 15:06:21 +08:00
Karl Seguin
e225ed9f19 fix for telemetry and one-off requests 2025-09-18 11:40:25 +08:00
Karl Seguin
17bebf4f3a Merge pull request #1058 from lightpanda-io/test_doctype
Give tests <!DOCTYPE html> so they work correct in browser
2025-09-18 11:29:31 +08:00
Karl Seguin
26550129ea Add --user_agent_suffix argument
Allows appending a value (separated by a space) to the existing Lightpanda/X.Y
user agent.
2025-09-18 11:28:27 +08:00
Karl Seguin
66362c2762 Give tests <!DOCTYPE html> so they work correct in browser 2025-09-18 10:53:29 +08:00
Muki Kiboigo
f6f0e141a1 PeristentPromiseResolver with page lifetime 2025-09-17 12:12:10 -07:00
Muki Kiboigo
f22ee54bd8 use fetch logging scope, clean some comments 2025-09-17 08:46:35 -07:00
Muki Kiboigo
2a969f911e stop using destructor callback for fetch 2025-09-17 08:46:29 -07:00
Muki Kiboigo
2a0964f66b htmlRunner for ReadableStream tests, fix ReadableStream enqueue 2025-09-17 08:46:25 -07:00
Muki Kiboigo
c553a2cd38 use Env.PersistentPromiseResolver 2025-09-17 08:46:20 -07:00
Karl Seguin
24330a7491 remove meaningless text from test 2025-09-17 08:46:16 -07:00
Karl Seguin
cd763a7a35 fix arena, add fetch test 2025-09-17 08:46:03 -07:00
Muki Kiboigo
ed11eab0a7 use content length to reserve body size 2025-09-17 08:45:53 -07:00
Muki Kiboigo
a875ce4d68 copy our Request headers into the HTTP client 2025-09-17 08:45:46 -07:00
Muki Kiboigo
969bfb4e53 migrate fetch tests to htmlRunner 2025-09-17 08:45:42 -07:00
Muki Kiboigo
76dae43103 properly handle closed for ReadableStream 2025-09-17 08:45:37 -07:00
Muki Kiboigo
af75ce79ac deinit persistent promise resolver 2025-09-17 08:45:30 -07:00
Muki Kiboigo
fe89c2ff9b simplify cloning of Req/Resp 2025-09-17 08:45:25 -07:00
Muki Kiboigo
bb2595eca5 use call arena for json in Req/Resp 2025-09-17 08:45:20 -07:00
Muki Kiboigo
618fff0191 simplify Headers 2025-09-17 08:45:14 -07:00
Muki Kiboigo
9bbd06ce76 headers iterators should not allocate 2025-09-17 08:45:05 -07:00
Muki Kiboigo
20463a662b use destructor callback for FetchContext 2025-09-17 08:45:00 -07:00
Muki Kiboigo
9251180501 support object as HeadersInit 2025-09-17 08:44:54 -07:00
Muki Kiboigo
2659043afd add logging on fetch error callback 2025-09-17 08:44:47 -07:00
sjorsdonkers
7766892ad2 retain value, avoid str alloc 2025-09-17 08:44:36 -07:00
sjorsdonkers
a7848f43cd avoid explicit memcpy 2025-09-17 08:44:31 -07:00
sjorsdonkers
cf8f76b454 remove length check of fixed size 2025-09-17 08:44:26 -07:00
sjorsdonkers
f68f184c68 jsValueToZig for fixed sized arrays 2025-09-17 08:44:12 -07:00
Muki Kiboigo
463440bce4 implement remaining ReadableStream functionality 2025-09-17 08:43:42 -07:00
Muki Kiboigo
51ee313910 working Header iterators 2025-09-17 08:43:36 -07:00
Muki Kiboigo
744b0bfff7 TypeError when Stream is locked 2025-09-17 08:43:31 -07:00
Muki Kiboigo
949479aa81 cleaning up various Headers routines 2025-09-17 08:43:22 -07:00
Muki Kiboigo
8743841145 use proper Headers in fetch() 2025-09-17 08:43:16 -07:00
Muki Kiboigo
6225cb38ae expand Request/Response interfaces 2025-09-17 08:43:05 -07:00
Muki Kiboigo
8dcba37672 expand Headers interface 2025-09-17 08:42:59 -07:00
Muki Kiboigo
38b922df75 remove debug logging in ReadableStream 2025-09-17 08:42:50 -07:00
Muki Kiboigo
6d884382a1 move fetch() into fetch.zig 2025-09-17 08:42:41 -07:00
Muki Kiboigo
752e75e94b add bodyUsed checks on Request and Response 2025-09-17 08:42:36 -07:00
Muki Kiboigo
5ca41b5e13 more Headers compatibility 2025-09-17 08:42:30 -07:00
Muki Kiboigo
1b3707ad33 add fetch to cdp domain 2025-09-17 08:42:20 -07:00
Muki Kiboigo
c6e82d5af6 add json response method 2025-09-17 08:42:12 -07:00
Muki Kiboigo
814e41122a basic readable stream working 2025-09-17 08:42:07 -07:00
Muki Kiboigo
a133a71eb9 proper fetch method and body setting 2025-09-17 08:41:22 -07:00
Muki Kiboigo
dc2addb0ed fetch callback logging 2025-09-17 08:41:16 -07:00
Muki Kiboigo
f9014bb90c request url as null terminated 2025-09-17 08:41:11 -07:00
Muki Kiboigo
df0b6d5b07 initial fetch in zig 2025-09-17 08:40:32 -07:00
Muki Kiboigo
56c6e8be06 remove polyfill and add req/resp 2025-09-17 08:40:10 -07:00
Pierre Tachoire
b47b8297d6 Merge pull request #1021 from lightpanda-io/patchright
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
Patchright compatibility
2025-09-17 16:14:00 +02:00
Pierre Tachoire
5d1e17c598 cdp: use for...else instead of found bool 2025-09-17 14:42:08 +02:00
Pierre Tachoire
94fe34bd10 cdp: multiple isolated worlds 2025-09-17 14:42:08 +02:00
Pierre Tachoire
e68ff62723 cdp: use depth param on DOM.describeNode 2025-09-17 14:42:08 +02:00
Pierre Tachoire
04487b6b91 cdp: allow double isolated world with same world name
In this case we reuse the existing isolated world and isolated context
and we log a warning
2025-09-17 14:42:07 +02:00
Pierre Tachoire
49a27a67bc cdp: send a warning for pierce parameter 2025-09-17 14:42:07 +02:00
Pierre Tachoire
745de2ede2 cdp: add Runtime.getProperties 2025-09-17 14:42:07 +02:00
Pierre Tachoire
82e5698f1d cdp: accept neg depth in describeNode 2025-09-17 14:42:06 +02:00
Pierre Tachoire
c4090851c5 css: accept digit as name start 2025-09-17 14:42:06 +02:00
Pierre Tachoire
9cb4431e89 cdp: add initiator on request will be send 2025-09-17 14:42:06 +02:00
Pierre Tachoire
2221d0cb6f cdp: send the chrome's error on missing node 2025-09-17 14:42:05 +02:00
Pierre Tachoire
5ea97c4910 cdp: add send error options with session id by default 2025-09-17 14:42:05 +02:00
Pierre Tachoire
a40590b4bf cdp: add DOM.getFrameOwner 2025-09-17 14:42:00 +02:00
Karl Seguin
58acb2b821 Convert more DOM_NO_ERR cases to assertions
There is some risk to this change. The first is that I made a mistake. The
other is that one of the APIs that doesn't currently return an error changes
in the future.
2025-09-17 13:37:48 +08:00
Karl Seguin
6b9dc90639 Introduces an Env.String for persistent strings
If a webapi has a []const u8 parameter, then the page.call_arena is used. This
is our favorite arena to use, but if the string value has a lifetime beyond the
call, it then needs to be duped again (using page.arena).

When a webapi has a Env.String parameter, the page.arena will be used directly
to get the value from V8, removing the need for an intermediary dupe in the
call_arena.

This allows HTMLCollections to be streamlined. They no longer need to dupe the
filter (tag name, class name, ...), which means they can no longer fail. It also
means that we no longer need to needlessly dupe the string literals.
2025-09-17 12:12:42 +08:00
Karl Seguin
b7d26cf0d5 Merge pull request #1053 from lightpanda-io/nikneym/create-event-html-events
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
allow `HTMLEvents` in `createEvent`
2025-09-17 00:31:36 +08:00
Pierre Tachoire
59b4033ab2 Merge pull request #1052 from lightpanda-io/fix-auth-interception-overflow
Fix auth interception integer overflow
2025-09-16 16:31:07 +02:00
nikneym
13a7219dbd allow HTMLEvents in createEvent 2025-09-16 17:24:50 +03:00
Pierre Tachoire
eae8a90a89 ci: add request interception through proxy test 2025-09-16 16:24:19 +02:00
Karl Seguin
a87f4abd5f Merge pull request #1050 from lightpanda-io/event_window_bubble
Event window bubble
2025-09-16 18:44:22 +08:00
Karl Seguin
1b73691c69 update libdom dep 2025-09-16 18:21:16 +08:00
Pierre Tachoire
e00066466b http: decrement intercepted on auth abortion 2025-09-16 12:18:49 +02:00
Pierre Tachoire
b87a8ba97d http: increment intercepted counter on auth interception 2025-09-16 12:18:49 +02:00
Karl Seguin
57aa270032 Merge pull request #1048 from lightpanda-io/nikneym/mime-changes
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
Mime: charset identification changes
2025-09-16 16:13:46 +08:00
nikneym
90a96fd8a7 set a zero char right after attrib value instead of memset 2025-09-16 10:41:49 +03:00
nikneym
c05470515f double quotes must be first and last argument of slice if provided 2025-09-16 10:40:38 +03:00
Pierre Tachoire
81ed4f3699 Merge pull request #1051 from lightpanda-io/explicit_microtask
Set Isolate Microtask to Explicit
2025-09-16 09:38:33 +02:00
Karl Seguin
c9ac1eab11 Set Isolate Microtask to Explicit
This defaults to Auto, which means it runs when the call stack reaches 0.
It appears that both Node and Deno set this to explicit.

I don't really understand why Auto doesn't work. It says the call stack is the
C++/C callstack, and I don't see what would block the current code from reaching
a depth of 0. Still, we already have explicit calls to performMicrotasksCheckpoint
which ties it holistically with our scheduler, so having it be explicit like
this should...well make it more explicit

This broke a test, but since the tests are being redone in the [fetch PR](https://github.com/lightpanda-io/browser/pull/972) I simply removed the offending one.
2025-09-16 14:52:31 +08:00
Karl Seguin
1ba542fb3b use redispatch, check for stopped 2025-09-16 10:31:37 +08:00
Karl Seguin
4f127c9de3 Bubble events to the Window 2025-09-15 22:24:35 +08:00
Karl Seguin
16656f6c13 Merge pull request #1049 from lightpanda-io/netsurf_event_errors
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 unnecessary error handling from non-erroring netsurf event fun…
2025-09-15 21:55:18 +08:00
Karl Seguin
0f13e062fe Remove unnecessary error handling from non-erroring netsurf event functions 2025-09-15 21:37:53 +08:00
nikneym
2e68407fbe update Mime tests 2025-09-15 15:15:29 +03:00
nikneym
974f350f27 store charset value directly in Mime 2025-09-15 15:15:08 +03:00
nikneym
27ffea9052 add vectorized parseCharset impl 2025-09-15 11:15:09 +03:00
Pierre Tachoire
9b2b35e8a2 Merge pull request #1047 from lightpanda-io/ci-cache-libiconv
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
ci: cache libiconv
2025-09-15 09:19:46 +02:00
Pierre Tachoire
3b51ca3947 make: download libiconv from GH fork 2025-09-15 08:59:30 +02:00
Pierre Tachoire
62a2d08b53 ci: cache libiconv 2025-09-15 08:43:48 +02:00
Pierre Tachoire
e790bde717 Merge pull request #1046 from lightpanda-io/fork-netsurf
submodule: use GH fork of netsurf buildsystem
2025-09-15 08:23:39 +02:00
Pierre Tachoire
0ab6b15292 submodule: use GH fork of netsurf buildsystem 2025-09-15 08:18:22 +02:00
Karl Seguin
2aeeb14c21 Merge pull request #1043 from lightpanda-io/html_slot_assigned_elements
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 assignedElements to HTMLSlotElement
2025-09-13 10:12:20 +08:00
Karl Seguin
e5e57ab3bd Merge pull request #1044 from lightpanda-io/script_nonce_and_df_host
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 Script get/set nonce
2025-09-12 20:45:50 +08:00
Karl Seguin
f3ce5dcfbd Add Script get/set nonce 2025-09-12 19:19:36 +08:00
Halil Durak
bc341e98fc Merge pull request #1041 from lightpanda-io/nikneym/keyboard-event
KeyboardEvent support
2025-09-12 13:57:39 +03:00
nikneym
80851f4861 don't inline keyboardEventKeyIsSet 2025-09-12 13:39:15 +03:00
nikneym
22b4456bce correct indentation in tests 2025-09-12 13:39:05 +03:00
nikneym
8d67502997 don't expose DOMErr function 2025-09-12 13:38:58 +03:00
nikneym
8f31fd778b add KeyboardEvent tests 2025-09-12 13:38:47 +03:00
nikneym
f79f25bcf4 implement KeyboardEvent properties and methods 2025-09-12 13:38:41 +03:00
nikneym
68e237eec5 add license 2025-09-12 13:38:31 +03:00
nikneym
8895c70c7f make toInterface be aware of KeyboardEvent 2025-09-12 13:38:04 +03:00
nikneym
3964f8649d initial keyboard event 2025-09-12 13:33:30 +03:00
Karl Seguin
b7fb0ef1d3 add assignedElements to HTMLSlotElement 2025-09-12 17:40:29 +08:00
Karl Seguin
66e403c5b4 Merge pull request #1042 from lightpanda-io/textdecoder_decode
Improve TextDecoder.decode
2025-09-12 14:34:14 +08:00
Karl Seguin
0913abe806 Improve TextDecoder.decode
1 - Optional input (why? I don't know, but it's part of the spec and happens)
2 - Optional stream parameter
3 - More test cases
2025-09-12 12:31:28 +08:00
Karl Seguin
6d3065c4c6 Merge pull request #1037 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
Upgrade v8
2025-09-12 08:29:20 +08:00
Karl Seguin
9092d1f8eb update v8 deps 2025-09-12 07:55:27 +08:00
Karl Seguin
1bd1f123a3 Upgrade v8
Depends on https://github.com/lightpanda-io/zig-v8-fork/pull/93
2025-09-12 07:53:22 +08:00
Karl Seguin
44c072dcbb Merge pull request #1040 from lightpanda-io/event_timestamp_resolution
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
Increase event timeStamp resolution
2025-09-11 21:08:09 +08:00
Karl Seguin
45c59e2990 update libdom 2025-09-11 20:43:32 +08:00
Karl Seguin
75f0cd6e62 fix test 2025-09-11 16:10:06 +08:00
Karl Seguin
80f758018c Increase event timeStamp resolution
Depends on https://github.com/lightpanda-io/libdom/pull/36

The spec says this should be a High Definition timestamp. But browsers avoid
that in order to avoid fingerprinting. By default, FireFox rounds to 2ms (which
is what this PR does).

Previously, the timestamp was seconds, so you'd think: isn't that better? Well,
it's pretty far off the spec and what browsers do, but more importantly, it
crashes our WPT test. If you look at `Event-timestamp-safe-resolution.html`
you'll see that it's trying to find the delta between two timestamps, in an
endless loop (without a loop of many iterations). With second-resolution, it
just takes too long (and crashes..memory).
2025-09-11 15:53:34 +08:00
Karl Seguin
b5e2c62fdd Merge pull request #1039 from lightpanda-io/migrate_some_tests_11
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 more tests to htmlRunner
2025-09-11 14:12:39 +08:00
Karl Seguin
ede35718ae migrate more tests to htmlRunner 2025-09-11 12:07:17 +08:00
Karl Seguin
31fe2807aa Merge pull request #1038 from lightpanda-io/migrate_some_tests_10
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-11 00:24:17 +08:00
Karl Seguin
f77693d768 migrate more tests to htmlRunner 2025-09-10 20:32:15 +08:00
Pierre Tachoire
96e3c16cca Merge pull request #1036 from lightpanda-io/css-contains
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
CSS: move tests + implement :containsOwn
2025-09-10 13:59:27 +02:00
Karl Seguin
edd41b37f0 Merge pull request #1033 from lightpanda-io/nikneym/custom-event
Support for CustomEvent in document.createEvent
2025-09-10 15:23:34 +08:00
Karl Seguin
139d0038f2 Merge pull request #1035 from lightpanda-io/migrate_some_tests_9
migrate more tests to htmlRunner
2025-09-10 15:21:27 +08:00
Pierre Tachoire
d25fc64d7a css: implement :containsOwn pseudo-selector
:containsOwn is implemented with case sensitive comparison.
2025-09-10 08:55:44 +02:00
Pierre Tachoire
42092ac16a css: move match_test into selector 2025-09-10 08:41:15 +02:00
Pierre Tachoire
e4860d5bae css: move libdom_test into libdom
To be added by the test_runner as part of used files.
2025-09-10 08:41:14 +02:00
Karl Seguin
a5d9b658fb migrate more tests to htmlRunner 2025-09-10 11:54:03 +08:00
Karl Seguin
f464e89415 Merge pull request #1034 from lightpanda-io/persistent-promise
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
Clean Up Persistent Promises in Dynamic Import
2025-09-10 08:39:50 +08:00
Karl Seguin
cdc439c4ef Merge pull request #1026 from lightpanda-io/network_idle_500ms_delay
Send NetworkIdle and NetworkAlmostIdle notifications after 500ms delay
2025-09-10 08:22:34 +08:00
Karl Seguin
746168f9ed Merge pull request #1031 from lightpanda-io/migrate_some_tests_8
migrate more tests to htmlRunner
2025-09-10 08:22:21 +08:00
Karl Seguin
5ad4885102 Merge pull request #1028 from lightpanda-io/wpt_runner_tweak
Try to address WPT running OOM
2025-09-10 08:22:08 +08:00
Muki Kiboigo
7eb53ca2bc deinit persistent in dynamic import 2025-09-09 15:02:26 -07:00
Karl Seguin
56b08bddd8 migrate more tests to htmlRunner 2025-09-09 20:40:19 +08:00
Karl Seguin
0aa1e0200f Merge branch 'wpt' into wpt_runner_tweak 2025-09-09 19:24:36 +08:00
Karl Seguin
575f827958 disable telemetry when running WPT action 2025-09-09 19:23:31 +08:00
Karl Seguin
67935b11c9 Try to address WPT running OOM
- Continue to reuse the Browser/Env/Isolate, but no start a new session per test.
- Test http server now properly closes the sendFile fd
- Run WPT in ReleaseMode
- Add --quiet option to WPT and some commented out debug code for dumping v8
  memory stats
2025-09-09 19:15:35 +08:00
Karl Seguin
9971de2ccd Send NetworkIdle and NetworkAlmostIdle notifications after 500ms delay
Like Chrome, the NetworkIdle and NetworkAlmostIdle will only be sent if the
condition (no network requests / <= 2 network requests) holds for at least 500ms

Also merged runHighPriority and runLowPriority as they are now always run
together (but we still only block/wait for high priority tasks).
2025-09-09 14:06:03 +08:00
239 changed files with 14522 additions and 11012 deletions

View File

@@ -5,7 +5,7 @@ inputs:
zig:
description: 'Zig version to install'
required: false
default: '0.15.1'
default: '0.15.2'
arch:
description: 'CPU arch used to select the v8 lib'
required: false
@@ -17,11 +17,11 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.1.28'
default: 'v0.1.33'
v8:
description: 'v8 version to install'
required: false
default: '13.6.233.8'
default: '14.0.365.4'
cache-dir:
description: 'cache dir to use'
required: false
@@ -67,9 +67,23 @@ runs:
mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a
- name: libiconv
- name: Cache libiconv
id: cache-libiconv
uses: actions/cache@v4
env:
cache-name: cache-libiconv
with:
path: ${{ inputs.cache-dir }}/libiconv
key: vendor/libiconv/libiconv-1.17
- name: download libiconv
if: ${{ steps.cache-libiconv.outputs.cache-hit != 'true' }}
shell: bash
run: make install-libiconv
run: make download-libiconv
- name: build libiconv
shell: bash
run: make build-libiconv
- name: build mimalloc
shell: bash

View File

@@ -108,6 +108,15 @@ jobs:
go run runner/main.go
kill `cat LPD.pid` `cat PROXY.id`
- name: run request interception through proxy
run: |
export PROXY_USERNAME=username PROXY_PASSWORD=password
./proxy/proxy & echo $! > PROXY.id
./lightpanda serve & echo $! > LPD.pid
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
kill `cat LPD.pid` `cat PROXY.id`
cdp-and-hyperfine-bench:
name: cdp-and-hyperfine-bench
needs: zig-build-release

View File

@@ -5,6 +5,7 @@ env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
LIGHTPANDA_DISABLE_TELEMETRY: true
on:
schedule:

View File

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

5
.gitmodules vendored
View File

@@ -9,7 +9,7 @@
url = https://github.com/lightpanda-io/libdom.git/
[submodule "vendor/netsurf/share/netsurf-buildsystem"]
path = vendor/netsurf/share/netsurf-buildsystem
url = https://source.netsurf-browser.org/buildsystem.git
url = https://github.com/lightpanda-io/netsurf-buildsystem.git
[submodule "vendor/netsurf/libhubbub"]
path = vendor/netsurf/libhubbub
url = https://github.com/lightpanda-io/libhubbub.git/
@@ -31,3 +31,6 @@
[submodule "vendor/curl"]
path = vendor/curl
url = https://github.com/curl/curl.git
[submodule "vendor/brotli"]
path = vendor/brotli
url = https://github.com/google/brotli

View File

@@ -1,10 +1,10 @@
FROM debian:stable
ARG MINISIG=0.12
ARG ZIG=0.15.1
ARG ZIG=0.15.2
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=13.6.233.8
ARG ZIG_V8=v0.1.28
ARG V8=14.0.365.4
ARG ZIG_V8=v0.1.33
ARG TARGETPLATFORM
RUN apt-get update -yq && \

View File

@@ -96,9 +96,16 @@ wpt-summary:
@printf "\e[36mBuilding wpt...\e[0m\n"
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
## Test
## Test - `grep` is used to filter out the huge compile command on build
ifeq ($(OS), macos)
test:
@TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
@script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \
| grep --line-buffered -v "^/.*zig test -freference-trace"
else
test:
@script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' /dev/null 2>&1 \
| grep --line-buffered -v "^/.*zig test -freference-trace"
endif
## Run demo/runner end to end tests
end2end:
@@ -199,14 +206,16 @@ download-libiconv:
ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","")
@mkdir -p vendor/libiconv
@cd vendor/libiconv && \
curl https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.17.tar.gz | tar -xvzf -
curl -L https://github.com/lightpanda-io/libiconv/releases/download/1.17/libiconv-1.17.tar.gz | tar -xvzf -
endif
install-libiconv: download-libiconv clean-libiconv
build-libiconv: clean-libiconv
@cd vendor/libiconv/libiconv-1.17 && \
./configure --prefix=$(ICONV) --enable-static && \
make && make install
install-libiconv: download-libiconv build-libiconv
clean-libiconv:
ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
@cd vendor/libiconv/libiconv-1.17 && \

View File

@@ -164,7 +164,7 @@ You can also follow the progress of our Javascript support in our dedicated [zig
### Prerequisites
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to
install it with the right version in order to build the project.
Lightpanda also depends on

View File

@@ -23,7 +23,7 @@ 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.15.1";
const recommended_zig_version = "0.15.2";
pub fn build(b: *Build) !void {
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
@@ -245,6 +245,7 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
mod.addCMacro("HAVE_ASSERT_H", "1");
mod.addCMacro("HAVE_BASENAME", "1");
mod.addCMacro("HAVE_BOOL_T", "1");
mod.addCMacro("HAVE_BROTLI", "1");
mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1");
mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
mod.addCMacro("HAVE_DLFCN_H", "1");
@@ -379,9 +380,11 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
}
try buildZlib(b, mod);
try buildBrotli(b, mod);
try buildMbedtls(b, mod);
try buildNghttp2(b, mod);
try buildCurl(b, mod);
try buildAda(b, mod);
switch (target.result.os.tag) {
.macos => {
@@ -484,6 +487,30 @@ fn buildZlib(b: *Build, m: *Build.Module) !void {
} });
}
fn buildBrotli(b: *Build, m: *Build.Module) !void {
const brotli = b.addLibrary(.{
.name = "brotli",
.root_module = m,
});
const root = "vendor/brotli/c/";
brotli.addIncludePath(b.path(root ++ "include"));
brotli.addCSourceFiles(.{ .flags = &.{}, .files = &.{
root ++ "common/constants.c",
root ++ "common/context.c",
root ++ "common/dictionary.c",
root ++ "common/platform.c",
root ++ "common/shared_dictionary.c",
root ++ "common/transform.c",
root ++ "dec/bit_reader.c",
root ++ "dec/decode.c",
root ++ "dec/huffman.c",
root ++ "dec/prefix.c",
root ++ "dec/state.c",
root ++ "dec/static_init.c",
} });
}
fn buildMbedtls(b: *Build, m: *Build.Module) !void {
const mbedtls = b.addLibrary(.{
.name = "mbedtls",
@@ -823,3 +850,34 @@ fn buildCurl(b: *Build, m: *Build.Module) !void {
},
});
}
pub fn buildAda(b: *Build, m: *Build.Module) !void {
const ada_dep = b.dependency("ada-singleheader", .{});
const ada_mod = b.createModule(.{
.root_source_file = b.path("vendor/ada/root.zig"),
});
const ada_lib = b.addLibrary(.{
.name = "ada",
.root_module = b.createModule(.{
.link_libcpp = true,
.target = m.resolved_target,
.optimize = m.optimize,
}),
.linkage = .static,
});
ada_lib.addCSourceFile(.{
.file = ada_dep.path("ada.cpp"),
.flags = &.{ "-std=c++20", "-O3" },
.language = .cpp,
});
ada_lib.installHeader(ada_dep.path("ada_c.h"), "ada_c.h");
// Link the library to ada module.
ada_mod.linkLibrary(ada_lib);
// Expose ada module to main module.
m.addImport("ada", ada_mod);
}

View File

@@ -5,9 +5,13 @@
.fingerprint = 0xda130f3af836cea0,
.dependencies = .{
.v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d3040a953e5a37290dae20e7ddf138b7aeb5e67d.tar.gz",
.hash = "v8-0.0.0-xddH6w_EAwA8vK0NAxfxfI7IcbnpkUAcXKNujn7qwnmY",
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/305bb3706716d32d59b2ffa674731556caa1002b.tar.gz",
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
},
//.v8 = .{ .path = "../zig-v8-fork" }
.@"ada-singleheader" = .{
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
},
},
}

12
flake.lock generated
View File

@@ -75,11 +75,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1756822655,
"narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
"lastModified": 1760968520,
"narHash": "sha256-EjGslHDzCBKOVr+dnDB1CAD7wiQSHfUt3suOpFj9O1Q=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
"rev": "e755547441a0413942a37692f7bf7fc6315bb7f6",
"type": "github"
},
"original": {
@@ -136,11 +136,11 @@
]
},
"locked": {
"lastModified": 1756555914,
"narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=",
"lastModified": 1760747435,
"narHash": "sha256-wNB/W3x+or4mdNxFPNOH5/WFckNpKgFRZk7OnOsLtm0=",
"owner": "mitchellh",
"repo": "zig-overlay",
"rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6",
"rev": "d0f239b887b1ac736c0f3dde91bf5bf2ecf3a420",
"type": "github"
},
"original": {

View File

@@ -49,7 +49,7 @@
targetPkgs =
pkgs: with pkgs; [
# Build Tools
zigpkgs."0.15.1"
zigpkgs."0.15.2"
zls
python3
pkg-config

View File

@@ -74,6 +74,7 @@ pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
else => return err,
};
defer file.close();
const stat = try file.stat();
var send_buffer: [4096]u8 = undefined;

View File

@@ -4,7 +4,7 @@ const Allocator = std.mem.Allocator;
const log = @import("log.zig");
const Http = @import("http/Http.zig");
const Platform = @import("runtime/js.zig").Platform;
const Platform = @import("browser/js/Platform.zig");
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
const Notification = @import("notification.zig").Notification;
@@ -36,6 +36,7 @@ pub const App = struct {
http_connect_timeout_ms: ?u31 = null,
http_max_host_open: ?u8 = null,
http_max_concurrent: ?u8 = null,
user_agent: [:0]const u8,
};
pub fn init(allocator: Allocator, config: Config) !*App {
@@ -53,6 +54,7 @@ pub const App = struct {
.http_proxy = config.http_proxy,
.tls_verify_host = config.tls_verify_host,
.proxy_bearer_token = config.proxy_bearer_token,
.user_agent = config.user_agent,
});
errdefer http.deinit();

View File

@@ -17,7 +17,6 @@
// 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();
@@ -38,8 +37,10 @@ pub fn init(allocator: Allocator) Scheduler {
}
pub fn reset(self: *Scheduler) void {
self.high_priority.clearRetainingCapacity();
self.low_priority.clearRetainingCapacity();
// Our allocator is the page arena, it's been reset. We cannot use
// clearAndRetainCapacity, since that space is no longer ours
self.high_priority.clearAndFree();
self.low_priority.clearAndFree();
}
const AddOpts = struct {
@@ -65,14 +66,11 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: Ad
});
}
pub fn runHighPriority(self: *Scheduler) !?i32 {
pub fn run(self: *Scheduler) !?i32 {
_ = try self.runQueue(&self.low_priority);
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) {
@@ -127,33 +125,24 @@ test "Scheduler" {
var task = TestTask{ .allocator = testing.arena_allocator };
var s = Scheduler.init(testing.arena_allocator);
try testing.expectEqual(null, s.runHighPriority());
try testing.expectEqual(null, s.run());
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.expectDelta(3, try s.run(), 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.expectEqual(null, s.run());
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.expectDelta(null, try s.run(), 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 {

View File

@@ -18,10 +18,10 @@
const std = @import("std");
const js = @import("js/js.zig");
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");
@@ -38,9 +38,6 @@ 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,
@@ -48,12 +45,6 @@ static_scripts_done: bool,
// 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,
@@ -67,6 +58,22 @@ client: *Http.Client,
allocator: Allocator,
buffer_pool: BufferPool,
script_pool: std.heap.MemoryPool(PendingScript),
sync_module_pool: std.heap.MemoryPool(SyncModule),
async_module_pool: std.heap.MemoryPool(AsyncModule),
// We can download multiple sync modules in parallel, but we want to process
// then in order. We can't use an OrderList, like the other script types,
// because the order we load them might not be the order we want to process
// them in (I'm not sure this is true, but as far as I can tell, v8 doesn't
// make any guarantees about the list of sub-module dependencies it gives us
// So this is more like a cache. When a SyncModule is complete, it's put here
// and can be requested as needed.
sync_modules: std.StringHashMapUnmanaged(*SyncModule),
// Mapping between module specifier and resolution.
// see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap
// importmap contains resolved urls.
importmap: std.StringHashMapUnmanaged([:0]const u8),
const OrderList = std.DoublyLinkedList;
@@ -78,27 +85,51 @@ pub fn init(browser: *Browser, page: *Page) ScriptManager {
.asyncs = .{},
.scripts = .{},
.deferreds = .{},
.asyncs_ready = .{},
.importmap = .empty,
.sync_modules = .empty,
.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),
.sync_module_pool = std.heap.MemoryPool(SyncModule).init(allocator),
.async_module_pool = std.heap.MemoryPool(AsyncModule).init(allocator),
};
}
pub fn deinit(self: *ScriptManager) void {
self.reset();
var it = self.sync_modules.valueIterator();
while (it.next()) |value_ptr| {
value_ptr.*.buffer.deinit(self.allocator);
self.sync_module_pool.destroy(value_ptr.*);
}
self.buffer_pool.deinit();
self.script_pool.deinit();
self.sync_module_pool.deinit();
self.async_module_pool.deinit();
self.sync_modules.deinit(self.allocator);
// we don't deinit self.importmap b/c we use the page's arena for its
// allocations.
}
pub fn reset(self: *ScriptManager) void {
var it = self.sync_modules.valueIterator();
while (it.next()) |value_ptr| {
value_ptr.*.buffer.deinit(self.allocator);
self.sync_module_pool.destroy(value_ptr.*);
}
self.sync_modules.clearRetainingCapacity();
// Our allocator is the page arena, it's been reset. We cannot use
// clearAndRetainCapacity, since that space is no longer ours
self.importmap = .empty;
self.clearList(&self.asyncs);
self.clearList(&self.scripts);
self.clearList(&self.deferreds);
self.clearList(&self.asyncs_ready);
self.static_scripts_done = false;
}
@@ -111,7 +142,7 @@ fn clearList(_: *const ScriptManager, list: *OrderList) void {
std.debug.assert(list.first == null);
}
pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime ctx: []const u8) !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.
@@ -144,6 +175,9 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
if (std.ascii.eqlIgnoreCase(script_type, "module")) {
break :blk .module;
}
if (std.ascii.eqlIgnoreCase(script_type, "importmap")) {
break :blk .importmap;
}
// "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
@@ -157,11 +191,12 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
if (try parser.elementGetAttribute(element, "src")) |src| {
if (try DataURI.parse(page.arena, src)) |data_uri| {
source = .{ .@"inline" = data_uri };
} else {
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true });
source = .{ .remote = .{} };
}
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;
const inline_source = parser.nodeTextContent(@ptrCast(element)) orelse return;
source = .{ .@"inline" = inline_source };
}
@@ -177,7 +212,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
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)
// as it needs to be executed in order)
return script.eval(page);
}
@@ -198,14 +233,18 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
self.scripts.append(&pending_script.node);
return;
} else {
log.debug(.http, "script queue", .{ .url = remote_url.? });
log.debug(.http, "script queue", .{
.ctx = ctx,
.url = remote_url.?,
.stack = page.js.stackTrace() catch "???",
});
}
pending_script.getList().append(&pending_script.node);
errdefer pending_script.deinit();
var headers = try Http.Headers.init();
var headers = try self.client.newHeaders();
try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers);
try self.client.request(.{
@@ -223,88 +262,144 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
});
}
// @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();
// Resolve a module specifier to an valid URL.
pub fn resolveSpecifier(self: *ScriptManager, arena: Allocator, specifier: []const u8, base: []const u8) ![:0]const u8 {
// If the specifier is mapped in the importmap, return the pre-resolved value.
if (self.importmap.get(specifier)) |s| {
return s;
}
var blocking = Blocking{
.allocator = self.allocator,
.buffer_pool = &self.buffer_pool,
};
return URL.stitch(
arena,
specifier,
base,
.{ .alloc = .if_needed, .null_terminated = true },
);
}
var headers = try Http.Headers.init();
pub fn getModule(self: *ScriptManager, url: [:0]const u8, referrer: []const u8) !void {
const gop = try self.sync_modules.getOrPut(self.allocator, url);
if (gop.found_existing) {
// already requested
return;
}
errdefer _ = self.sync_modules.remove(url);
const sync = try self.sync_module_pool.create();
errdefer self.sync_module_pool.destroy(sync);
sync.* = .{ .manager = self };
gop.value_ptr.* = sync;
var headers = try self.client.newHeaders();
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
log.debug(.http, "script queue", .{
.url = url,
.ctx = "module",
.referrer = referrer,
.stack = self.page.js.stackTrace() catch "???",
});
try self.client.request(.{
.url = url,
.ctx = sync,
.method = .GET,
.headers = headers,
.cookie_jar = self.page.cookie_jar,
.resource_type = .script,
.start_callback = if (log.enabled(.http, .debug)) SyncModule.startCallback else null,
.header_callback = SyncModule.headerCallback,
.data_callback = SyncModule.dataCallback,
.done_callback = SyncModule.doneCallback,
.error_callback = SyncModule.errorCallback,
});
}
pub fn waitForModule(self: *ScriptManager, url: [:0]const u8) !GetResult {
// Normally it's dangerous to hold on to map pointers. But here, the map
// can't change. It's possible that by calling `tick`, other entries within
// the map will have their value change, but the map itself is immutable
// during this tick.
const entry = self.sync_modules.getEntry(url) orelse {
return error.UnknownModule;
};
const sync = entry.value_ptr.*;
// We can have multiple scripts waiting for the same module in concurrency.
// We use the waiters to ensures only the last waiter deinit the resources.
sync.waiters += 1;
defer sync.waiters -= 1;
var client = self.client;
try client.blockingRequest(.{
while (true) {
switch (sync.state) {
.loading => {},
.done => {
if (sync.waiters == 1) {
// Our caller has its own higher level cache (caching the
// actual compiled module). There's no reason for us to keep
// this if we are the last waiter.
defer self.sync_module_pool.destroy(sync);
defer self.sync_modules.removeByPtr(entry.key_ptr);
return .{
.shared = false,
.buffer = sync.buffer,
.buffer_pool = &self.buffer_pool,
};
}
return .{
.shared = true,
.buffer = sync.buffer,
.buffer_pool = &self.buffer_pool,
};
},
.err => |err| return err,
}
// rely on http's timeout settings to avoid an endless/long loop.
_ = try client.tick(200);
}
}
pub fn getAsyncModule(self: *ScriptManager, url: [:0]const u8, cb: AsyncModule.Callback, cb_data: *anyopaque, referrer: []const u8) !void {
const async = try self.async_module_pool.create();
errdefer self.async_module_pool.destroy(async);
async.* = .{
.cb = cb,
.manager = self,
.cb_data = cb_data,
};
var headers = try self.client.newHeaders();
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
log.debug(.http, "script queue", .{
.url = url,
.ctx = "dynamic module",
.referrer = referrer,
.stack = self.page.js.stackTrace() catch "???",
});
try self.client.request(.{
.url = url,
.method = .GET,
.headers = headers,
.cookie_jar = self.page.cookie_jar,
.ctx = &blocking,
.ctx = async,
.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,
.start_callback = if (log.enabled(.http, .debug)) AsyncModule.startCallback else null,
.header_callback = AsyncModule.headerCallback,
.data_callback = AsyncModule.dataCallback,
.done_callback = AsyncModule.doneCallback,
.error_callback = AsyncModule.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;
self.evaluate();
}
// try to evaluate completed scripts (in order). This is called whenever a script
@@ -318,24 +413,10 @@ fn evaluate(self: *ScriptManager) void {
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) {
@@ -385,6 +466,12 @@ pub fn isDone(self: *const ScriptManager) bool {
self.deferreds.first == null; // and there are no more <script defer src=> to wait for
}
fn asyncScriptIsDone(self: *ScriptManager) void {
if (self.isDone()) {
self.page.documentIsComplete();
}
}
fn startCallback(transfer: *Http.Transfer) !void {
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
script.startCallback(transfer) catch |err| {
@@ -416,6 +503,38 @@ fn errorCallback(ctx: *anyopaque, err: anyerror) void {
script.errorCallback(err);
}
fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
const content = script.source.content();
const Imports = struct {
imports: std.json.ArrayHashMap([]const u8),
};
const imports = try std.json.parseFromSliceLeaky(
Imports,
self.page.arena,
content,
.{ .allocate = .alloc_always },
);
var iter = imports.imports.map.iterator();
while (iter.next()) |entry| {
// > Relative URLs are resolved to absolute URL addresses using the
// > base URL of the document containing the import map.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_modules_using_import_maps
const resolved_url = try URL.stitch(
self.page.arena,
entry.value_ptr.*,
self.page.url.raw,
.{ .alloc = .if_needed, .null_terminated = true },
);
try self.importmap.put(self.page.arena, entry.key_ptr.*, resolved_url);
}
return;
}
// A script which is pending execution.
// It could be pending because:
// (a) we're still downloading its content or
@@ -493,11 +612,15 @@ pub const PendingScript = struct {
const manager = self.manager;
self.complete = true;
if (self.script.is_async) {
manager.asyncs.remove(&self.node);
manager.asyncs_ready.append(&self.node);
if (!self.script.is_async) {
manager.evaluate();
return;
}
manager.evaluate();
// async script can be evaluated immediately
self.script.eval(manager.page);
self.deinit();
// asyncScriptIsDone must be run after the pending script is deinit.
manager.asyncScriptIsDone();
}
fn errorCallback(self: *PendingScript, err: anyerror) void {
@@ -521,7 +644,7 @@ pub const PendingScript = struct {
const script = &self.script;
if (script.is_async) {
return if (self.complete) &self.manager.asyncs_ready else &self.manager.asyncs;
return &self.manager.asyncs;
}
if (script.is_defer) {
@@ -543,11 +666,12 @@ const Script = struct {
const Kind = enum {
module,
javascript,
importmap,
};
const Callback = union(enum) {
string: []const u8,
function: Env.Function,
function: js.Function,
};
const Source = union(enum) {
@@ -583,8 +707,25 @@ const Script = struct {
.cacheable = cacheable,
});
const js_context = page.main_context;
var try_catch: Env.TryCatch = undefined;
// Handle importmap special case here: the content is a JSON containing
// imports.
if (self.kind == .importmap) {
page.script_manager.parseImportmap(self) catch |err| {
log.err(.browser, "parse importmap script", .{
.err = err,
.src = url,
.kind = self.kind,
.cacheable = cacheable,
});
self.executeCallback("onerror", page);
return;
};
self.executeCallback("onload", page);
return;
}
const js_context = page.js;
var try_catch: js.TryCatch = undefined;
try_catch.init(js_context);
defer try_catch.deinit();
@@ -594,8 +735,9 @@ const Script = struct {
.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;
js_context.module(false, content, url, cacheable) catch break :blk false;
},
.importmap => unreachable, // handled before the try/catch.
}
break :blk true;
};
@@ -626,11 +768,11 @@ const Script = struct {
switch (callback) {
.string => |str| {
var try_catch: Env.TryCatch = undefined;
try_catch.init(page.main_context);
var try_catch: js.TryCatch = undefined;
try_catch.init(page.js);
defer try_catch.deinit();
_ = page.main_context.exec(str, typ) catch |err| {
_ = page.js.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,
@@ -648,11 +790,8 @@ const Script = struct {
};
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;
};
var result: js.Function.Result = undefined;
const iface = Event.toInterface(loadevt);
f.tryCall(void, .{iface}, &result) catch {
log.warn(.user_script, "script callback", .{
.url = self.url,
@@ -754,16 +893,17 @@ const BufferPool = struct {
}
};
const Blocking = struct {
allocator: Allocator,
buffer_pool: *BufferPool,
state: State = .{ .running = {} },
const SyncModule = struct {
manager: *ScriptManager,
buffer: std.ArrayListUnmanaged(u8) = .{},
state: State = .loading,
// number of waiters for the module.
waiters: u8 = 0,
const State = union(enum) {
running: void,
done,
loading,
err: anyerror,
done: BlockingResult,
};
fn startCallback(transfer: *Http.Transfer) !void {
@@ -779,12 +919,13 @@ const Blocking = struct {
.content_type = header.contentType(),
});
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx));
if (header.status != 200) {
self.finished(.{ .err = error.InvalidStatusCode });
return error.InvalidStatusCode;
}
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
self.buffer = self.buffer_pool.get();
self.buffer = self.manager.buffer_pool.get();
}
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
@@ -794,8 +935,8 @@ const Blocking = struct {
// .blocking = true,
// });
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
self.buffer.appendSlice(self.allocator, data) catch |err| {
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx));
self.buffer.appendSlice(self.manager.allocator, data) catch |err| {
log.err(.http, "SM.dataCallback", .{
.err = err,
.len = data.len,
@@ -807,29 +948,107 @@ const Blocking = struct {
}
fn doneCallback(ctx: *anyopaque) !void {
var self: *Blocking = @ptrCast(@alignCast(ctx));
self.state = .{ .done = .{
.buffer = self.buffer,
.buffer_pool = self.buffer_pool,
} };
var self: *SyncModule = @ptrCast(@alignCast(ctx));
self.finished(.done);
}
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
var self: *Blocking = @ptrCast(@alignCast(ctx));
self.state = .{ .err = err };
self.buffer_pool.release(self.buffer);
var self: *SyncModule = @ptrCast(@alignCast(ctx));
self.finished(.{ .err = err });
}
fn finished(self: *SyncModule, state: State) void {
self.state = state;
}
};
pub const BlockingResult = struct {
pub const AsyncModule = struct {
cb: Callback,
cb_data: *anyopaque,
manager: *ScriptManager,
buffer: std.ArrayListUnmanaged(u8) = .{},
pub const Callback = *const fn (ptr: *anyopaque, result: anyerror!GetResult) void;
fn startCallback(transfer: *Http.Transfer) !void {
log.debug(.http, "script fetch start", .{ .req = transfer, .async = true });
}
fn headerCallback(transfer: *Http.Transfer) !void {
const header = &transfer.response_header.?;
log.debug(.http, "script header", .{
.req = transfer,
.async = true,
.status = header.status,
.content_type = header.contentType(),
});
if (header.status != 200) {
return error.InvalidStatusCode;
}
var self: *AsyncModule = @ptrCast(@alignCast(transfer.ctx));
self.buffer = self.manager.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: *AsyncModule = @ptrCast(@alignCast(transfer.ctx));
self.buffer.appendSlice(self.manager.allocator, data) catch |err| {
log.err(.http, "SM.dataCallback", .{
.err = err,
.len = data.len,
.ascyn = true,
.transfer = transfer,
});
return err;
};
}
fn doneCallback(ctx: *anyopaque) !void {
var self: *AsyncModule = @ptrCast(@alignCast(ctx));
defer self.manager.async_module_pool.destroy(self);
self.cb(self.cb_data, .{
.shared = false,
.buffer = self.buffer,
.buffer_pool = &self.manager.buffer_pool,
});
}
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
var self: *AsyncModule = @ptrCast(@alignCast(ctx));
if (err != error.Abort) {
self.cb(self.cb_data, err);
}
if (self.buffer.items.len > 0) {
self.manager.buffer_pool.release(self.buffer);
}
self.manager.async_module_pool.destroy(self);
}
};
pub const GetResult = struct {
buffer: std.ArrayListUnmanaged(u8),
buffer_pool: *BufferPool,
shared: bool,
pub fn deinit(self: *BlockingResult) void {
pub fn deinit(self: *GetResult) void {
// if the result is shared, don't deinit.
if (self.shared) {
return;
}
self.buffer_pool.release(self.buffer);
}
pub fn src(self: *const BlockingResult) []const u8 {
pub fn src(self: *const GetResult) []const u8 {
return self.buffer.items;
}
};

View File

@@ -0,0 +1,189 @@
const std = @import("std");
const log = @import("../log.zig");
const parser = @import("netsurf.zig");
const collection = @import("dom/html_collection.zig");
const Page = @import("page.zig").Page;
const SlotChangeMonitor = @This();
page: *Page,
event_node: parser.EventNode,
slots_changed: std.ArrayList(*parser.Slot),
// Monitors the document in order to trigger slotchange events.
pub fn init(page: *Page) !*SlotChangeMonitor {
// on the heap, we need a stable address for event_node
const self = try page.arena.create(SlotChangeMonitor);
self.* = .{
.page = page,
.slots_changed = .empty,
.event_node = .{ .func = mutationCallback },
};
const root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document));
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, root),
"DOMNodeInserted",
&self.event_node,
false,
);
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, root),
"DOMNodeRemoved",
&self.event_node,
false,
);
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, root),
"DOMAttrModified",
&self.event_node,
false,
);
return self;
}
// Given a element, finds its slot, if any.
pub fn findSlot(element: *parser.Element, page: *const Page) !?*parser.Slot {
const target_name = (try parser.elementGetAttribute(element, "slot")) orelse return null;
return findNamedSlot(element, target_name, page);
}
// Given an element and a name, find the slo, if any. This is only useful for
// MutationEvents where findSlot is unreliable because parser.elementGetAttribute(element, "slot")
// could return the new or old value.
fn findNamedSlot(element: *parser.Element, target_name: []const u8, page: *const Page) !?*parser.Slot {
// I believe elements need to be added as direct descendents of the host,
// so we don't need to go find the host, we just grab the parent.
const host = parser.nodeParentNode(@ptrCast(element)) orelse return null;
const state = page.getNodeState(host) orelse return null;
const shadow_root = state.shadow_root orelse return null;
// if we're here, we found a host, now find the slot
var nodes = collection.HTMLCollectionByTagName(
@ptrCast(@alignCast(shadow_root.proto)),
"slot",
.{ .include_root = false },
);
for (0..1000) |i| {
const n = (try nodes.item(@intCast(i))) orelse return null;
const slot_name = (try parser.elementGetAttribute(@ptrCast(n), "name")) orelse "";
if (std.mem.eql(u8, target_name, slot_name)) {
return @ptrCast(n);
}
}
return null;
}
// Event callback from the mutation event, signaling either the addition of
// a node, removal of a node, or a change in attribute
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
const mutation_event = parser.eventToMutationEvent(event);
const self: *SlotChangeMonitor = @fieldParentPtr("event_node", en);
self._mutationCallback(mutation_event) catch |err| {
log.err(.web_api, "slot change callback", .{ .err = err });
};
}
fn _mutationCallback(self: *SlotChangeMonitor, event: *parser.MutationEvent) !void {
const event_type = parser.eventType(@ptrCast(event));
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
return self.nodeAddedOrRemoved(@ptrCast(event_target));
}
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
return self.nodeAddedOrRemoved(@ptrCast(event_target));
}
if (std.mem.eql(u8, event_type, "DOMAttrModified")) {
const attribute_name = try parser.mutationEventAttributeName(event);
if (std.mem.eql(u8, attribute_name, "slot") == false) {
return;
}
const new_value = parser.mutationEventNewValue(event);
const prev_value = parser.mutationEventPrevValue(event);
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
return self.nodeAttributeChanged(@ptrCast(event_target), new_value, prev_value);
}
}
// A node was removed or added. If it's an element, and if it has a slot attribute
// then we'll dispatch a slotchange event.
fn nodeAddedOrRemoved(self: *SlotChangeMonitor, node: *parser.Node) !void {
if (parser.nodeType(node) != .element) {
return;
}
const el: *parser.Element = @ptrCast(node);
if (try findSlot(el, self.page)) |slot| {
return self.scheduleSlotChange(slot);
}
}
// An attribute was modified. If the attribute is "slot", then we'll trigger 1
// slotchange for the old slot (if there was one) and 1 slotchange for the new
// one (if there is one)
fn nodeAttributeChanged(self: *SlotChangeMonitor, node: *parser.Node, new_value: ?[]const u8, prev_value: ?[]const u8) !void {
if (parser.nodeType(node) != .element) {
return;
}
const el: *parser.Element = @ptrCast(node);
if (try findNamedSlot(el, prev_value orelse "", self.page)) |slot| {
try self.scheduleSlotChange(slot);
}
if (try findNamedSlot(el, new_value orelse "", self.page)) |slot| {
try self.scheduleSlotChange(slot);
}
}
// OK. Our MutationEvent is not a MutationObserver - it's an older, deprecated
// API. It gets dispatched in the middle of the change. While I'm sure it has
// some rules, from our point of view, it fires too early. DOMAttrModified fires
// before the attribute is actually updated and DOMNodeRemoved before the node
// is actually removed. This is a problem if the callback will call
// `slot.assignedNodes`, since that won't return the new state.
// So, we use the page schedule to schedule the dispatching of the slotchange
// event.
fn scheduleSlotChange(self: *SlotChangeMonitor, slot: *parser.Slot) !void {
for (self.slots_changed.items) |changed| {
if (slot == changed) {
return;
}
}
try self.slots_changed.append(self.page.arena, slot);
if (self.slots_changed.items.len == 1) {
// first item added, schedule the callback
try self.page.scheduler.add(self, scheduleCallback, 0, .{ .name = "slot change" });
}
}
// Callback from the schedule. Time to dispatch the slotchange event
fn scheduleCallback(ctx: *anyopaque) ?u32 {
var self: *SlotChangeMonitor = @ptrCast(@alignCast(ctx));
self._scheduleCallback() catch |err| {
log.err(.app, "slot change schedule", .{ .err = err });
};
return null;
}
fn _scheduleCallback(self: *SlotChangeMonitor) !void {
for (self.slots_changed.items) |slot| {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, "slotchange", .{});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(parser.Element, @ptrCast(@alignCast(slot))),
event,
);
}
self.slots_changed.clearRetainingCapacity();
}

View File

@@ -26,17 +26,16 @@
// this quickly proved necessary, since different fields are needed on the same
// data at different levels of the prototype chain. This isn't memory efficient.
const Env = @import("env.zig").Env;
const js = @import("js/js.zig");
const parser = @import("netsurf.zig");
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,
onerror: ?Env.Function = null,
onload: ?js.Function = null,
onerror: ?js.Function = null,
// for HTMLElement
style: CSSStyleDeclaration = .empty,
@@ -54,7 +53,7 @@ style_sheet: ?*StyleSheet = null,
// for dom/document
active_element: ?*parser.Element = null,
adopted_style_sheets: ?Env.JsObject = null,
adopted_style_sheets: ?js.Object = null,
// for HTMLSelectElement
// By default, if no option is explicitly selected, the first option should

View File

@@ -21,8 +21,8 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const js = @import("js/js.zig");
const State = @import("State.zig");
const Env = @import("env.zig").Env;
const App = @import("../app.zig").App;
const Session = @import("session.zig").Session;
const Notification = @import("../notification.zig").Notification;
@@ -34,11 +34,12 @@ const HttpClient = @import("../http/Client.zig");
// You can create multiple browser instances.
// A browser contains only one session.
pub const Browser = struct {
env: *Env,
env: *js.Env,
app: *App,
session: ?Session,
allocator: Allocator,
http_client: *HttpClient,
call_arena: ArenaAllocator,
page_arena: ArenaAllocator,
session_arena: ArenaAllocator,
transfer_arena: ArenaAllocator,
@@ -48,7 +49,7 @@ pub const Browser = struct {
pub fn init(app: *App) !Browser {
const allocator = app.allocator;
const env = try Env.init(allocator, &app.platform, .{});
const env = try js.Env.init(allocator, &app.platform, .{});
errdefer env.deinit();
const notification = try Notification.init(allocator, app.notification);
@@ -63,6 +64,7 @@ pub const Browser = struct {
.allocator = allocator,
.notification = notification,
.http_client = app.http.client,
.call_arena = ArenaAllocator.init(allocator),
.page_arena = ArenaAllocator.init(allocator),
.session_arena = ArenaAllocator.init(allocator),
.transfer_arena = ArenaAllocator.init(allocator),
@@ -73,6 +75,7 @@ pub const Browser = struct {
pub fn deinit(self: *Browser) void {
self.closeSession();
self.env.deinit();
self.call_arena.deinit();
self.page_arena.deinit();
self.session_arena.deinit();
self.transfer_arena.deinit();

View File

@@ -20,48 +20,47 @@ const std = @import("std");
const builtin = @import("builtin");
const log = @import("../../log.zig");
const Allocator = std.mem.Allocator;
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const JsObject = @import("../env.zig").Env.JsObject;
pub const Console = struct {
// TODO: configurable writer
timers: std.StringHashMapUnmanaged(u32) = .{},
counts: std.StringHashMapUnmanaged(u32) = .{},
pub fn _lp(values: []JsObject, page: *Page) !void {
pub fn _lp(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
}
pub fn _log(values: []JsObject, page: *Page) !void {
pub fn _log(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
}
pub fn _info(values: []JsObject, page: *Page) !void {
pub fn _info(values: []js.Object, page: *Page) !void {
return _log(values, page);
}
pub fn _debug(values: []JsObject, page: *Page) !void {
pub fn _debug(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
}
pub fn _warn(values: []JsObject, page: *Page) !void {
pub fn _warn(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
}
pub fn _error(values: []JsObject, page: *Page) !void {
pub fn _error(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
@@ -72,6 +71,16 @@ pub const Console = struct {
});
}
pub fn _trace(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
log.debug(.console, "debug", .{
.stack = page.js.stackTrace() catch "???",
.args = try serializeValues(values, page),
});
}
pub fn _clear() void {}
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
@@ -133,7 +142,7 @@ pub const Console = struct {
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
}
pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
pub fn _assert(assertion: js.Object, values: []js.Object, page: *Page) !void {
if (assertion.isTruthy()) {
return;
}
@@ -144,7 +153,7 @@ pub const Console = struct {
log.info(.console, "assertion failed", .{ .values = serialized_values });
}
fn serializeValues(values: []JsObject, page: *Page) ![]const u8 {
fn serializeValues(values: []js.Object, page: *Page) ![]const u8 {
if (values.len == 0) {
return "";
}
@@ -166,74 +175,3 @@ pub const Console = struct {
fn timestamp() u32 {
return @import("../../datetime.zig").timestamp();
}
// const testing = @import("../../testing.zig");
// test "Browser.Console" {
// defer testing.reset();
// var runner = try testing.jsRunner(testing.tracking_allocator, .{});
// defer runner.deinit();
// {
// try runner.testCases(&.{
// .{ "console.log('a')", "undefined" },
// .{ "console.warn('hello world', 23, true, new Object())", "undefined" },
// }, .{});
// const captured = test_capture.captured.items;
// try testing.expectEqual("[info] args= 1: a", captured[0]);
// try testing.expectEqual("[warn] args= 1: hello world 2: 23 3: true 4: #<Object>", captured[1]);
// }
// {
// test_capture.reset();
// try runner.testCases(&.{
// .{ "console.countReset()", "undefined" },
// .{ "console.count()", "undefined" },
// .{ "console.count('teg')", "undefined" },
// .{ "console.count('teg')", "undefined" },
// .{ "console.count('teg')", "undefined" },
// .{ "console.count()", "undefined" },
// .{ "console.countReset('teg')", "undefined" },
// .{ "console.countReset()", "undefined" },
// .{ "console.count()", "undefined" },
// }, .{});
// const captured = test_capture.captured.items;
// try testing.expectEqual("[invalid counter] label=default", captured[0]);
// try testing.expectEqual("[count] label=default count=1", captured[1]);
// try testing.expectEqual("[count] label=teg count=1", captured[2]);
// try testing.expectEqual("[count] label=teg count=2", captured[3]);
// try testing.expectEqual("[count] label=teg count=3", captured[4]);
// try testing.expectEqual("[count] label=default count=2", captured[5]);
// try testing.expectEqual("[count reset] label=teg count=3", captured[6]);
// try testing.expectEqual("[count reset] label=default count=2", captured[7]);
// try testing.expectEqual("[count] label=default count=1", captured[8]);
// }
// {
// test_capture.reset();
// try runner.testCases(&.{
// .{ "console.assert(true)", "undefined" },
// .{ "console.assert('a', 2, 3, 4)", "undefined" },
// .{ "console.assert('')", "undefined" },
// .{ "console.assert('', 'x', true)", "undefined" },
// .{ "console.assert(false, 'x')", "undefined" },
// }, .{});
// const captured = test_capture.captured.items;
// try testing.expectEqual("[assertion failed] values=", captured[0]);
// try testing.expectEqual("[assertion failed] values= 1: x 2: true", captured[1]);
// try testing.expectEqual("[assertion failed] values= 1: x", captured[2]);
// }
// {
// test_capture.reset();
// try runner.testCases(&.{
// .{ "[1].forEach(console.log)", null },
// }, .{});
// const captured = test_capture.captured.items;
// try testing.expectEqual("[info] args= 1: 1 2: 0 3: [1]", captured[0]);
// }
// }

View File

@@ -17,14 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Env = @import("../env.zig").Env;
const js = @import("../js/js.zig");
const uuidv4 = @import("../../id.zig").uuidv4;
// https://w3c.github.io/webcrypto/#crypto-interface
pub const Crypto = struct {
_not_empty: bool = true,
pub fn _getRandomValues(_: *const Crypto, js_obj: Env.JsObject) !Env.JsObject {
pub fn _getRandomValues(_: *const Crypto, js_obj: js.Object) !js.Object {
var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues);
const buf = into.asBuffer();
if (buf.len > 65_536) {

View File

@@ -46,17 +46,15 @@ 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: *const Selector, node: anytype, m: anytype) !bool {
var c = try node.firstChild();
while (true) {
if (c == null) break;
if (try s.match(c.?)) {
try m.match(c.?);
var child = node.firstChild();
while (child) |c| {
if (try s.match(c)) {
try m.match(c);
return true;
}
if (try matchFirst(s, c.?, m)) return true;
c = try c.?.nextSibling();
if (try matchFirst(s, c, m)) return true;
child = c.nextSibling();
}
return false;
}
@@ -64,13 +62,11 @@ pub fn matchFirst(s: *const 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: *const Selector, node: anytype, m: anytype) !void {
var c = try node.firstChild();
while (true) {
if (c == null) break;
if (try s.match(c.?)) try m.match(c.?);
try matchAll(s, c.?, m);
c = try c.?.nextSibling();
var child = node.firstChild();
while (child) |c| {
if (try s.match(c)) try m.match(c);
try matchAll(s, c, m);
child = c.nextSibling();
}
}

View File

@@ -19,68 +19,74 @@
const std = @import("std");
const parser = @import("../netsurf.zig");
const css = @import("css.zig");
const Allocator = std.mem.Allocator;
// Node implementation with Netsurf Libdom C lib.
pub const Node = struct {
node: *parser.Node,
pub fn firstChild(n: Node) !?Node {
const c = try parser.nodeFirstChild(n.node);
pub fn firstChild(n: Node) ?Node {
const c = parser.nodeFirstChild(n.node);
if (c) |cc| return .{ .node = cc };
return null;
}
pub fn lastChild(n: Node) !?Node {
const c = try parser.nodeLastChild(n.node);
pub fn lastChild(n: Node) ?Node {
const c = parser.nodeLastChild(n.node);
if (c) |cc| return .{ .node = cc };
return null;
}
pub fn nextSibling(n: Node) !?Node {
const c = try parser.nodeNextSibling(n.node);
pub fn nextSibling(n: Node) ?Node {
const c = parser.nodeNextSibling(n.node);
if (c) |cc| return .{ .node = cc };
return null;
}
pub fn prevSibling(n: Node) !?Node {
const c = try parser.nodePreviousSibling(n.node);
pub fn prevSibling(n: Node) ?Node {
const c = parser.nodePreviousSibling(n.node);
if (c) |cc| return .{ .node = cc };
return null;
}
pub fn parent(n: Node) !?Node {
const c = try parser.nodeParentNode(n.node);
pub fn parent(n: Node) ?Node {
const c = parser.nodeParentNode(n.node);
if (c) |cc| return .{ .node = cc };
return null;
}
pub fn isElement(n: Node) bool {
const t = parser.nodeType(n.node) catch return false;
return t == .element;
return parser.nodeType(n.node) == .element;
}
pub fn isDocument(n: Node) bool {
const t = parser.nodeType(n.node) catch return false;
return t == .document;
return parser.nodeType(n.node) == .document;
}
pub fn isComment(n: Node) bool {
const t = parser.nodeType(n.node) catch return false;
return t == .comment;
return parser.nodeType(n.node) == .comment;
}
pub fn isText(n: Node) bool {
const t = parser.nodeType(n.node) catch return false;
return t == .text;
return parser.nodeType(n.node) == .text;
}
pub fn isEmptyText(n: Node) !bool {
const data = try parser.nodeTextContent(n.node);
pub fn text(n: Node) ?[]const u8 {
const data = parser.nodeTextContent(n.node);
if (data == null) return null;
if (data.?.len == 0) return null;
return std.mem.trim(u8, data.?, &std.ascii.whitespace);
}
pub fn isEmptyText(n: Node) bool {
const data = parser.nodeTextContent(n.node);
if (data == null) return true;
if (data.?.len == 0) return true;
@@ -88,7 +94,7 @@ pub const Node = struct {
}
pub fn tag(n: Node) ![]const u8 {
return try parser.nodeName(n.node);
return parser.nodeName(n.node);
}
pub fn attr(n: Node, key: []const u8) !?[]const u8 {
@@ -100,3 +106,318 @@ pub const Node = struct {
return a.node == b.node;
}
};
const MatcherTest = struct {
const Nodes = std.ArrayListUnmanaged(Node);
nodes: Nodes,
allocator: Allocator,
fn init(allocator: Allocator) MatcherTest {
return .{
.nodes = .empty,
.allocator = allocator,
};
}
fn deinit(m: *MatcherTest) void {
m.nodes.deinit(m.allocator);
}
fn reset(m: *MatcherTest) void {
m.nodes.clearRetainingCapacity();
}
pub fn match(m: *MatcherTest, n: Node) !void {
try m.nodes.append(m.allocator, n);
}
};
test "Browser.CSS.Libdom: matchFirst" {
const alloc = std.testing.allocator;
parser.init();
defer parser.deinit();
var matcher = MatcherTest.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
html: []const u8,
exp: usize,
}{
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
.{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
.{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
.{ .q = ":containsOwn(\"Inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
.{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
.{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
.{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
.{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
};
for (testcases) |tc| {
matcher.reset();
const doc = try parser.documentHTMLParseFromStr(tc.html);
defer parser.documentHTMLClose(doc) catch {};
const s = css.parse(alloc, tc.q, .{}) catch |e| {
std.debug.print("parse, query: {s}\n", .{tc.q});
return e;
};
defer s.deinit(alloc);
const node = Node{ .node = parser.documentHTMLToNode(doc) };
_ = css.matchFirst(&s, node, &matcher) catch |e| {
std.debug.print("match, query: {s}\n", .{tc.q});
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("expectation, query: {s}\n", .{tc.q});
return e;
};
}
}
test "Browser.CSS.Libdom: matchAll" {
const alloc = std.testing.allocator;
parser.init();
defer parser.deinit();
var matcher = MatcherTest.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
html: []const u8,
exp: usize,
}{
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
.{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
.{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
.{ .q = ":containsOwn(\"Inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
.{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
.{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
.{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
.{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 2 },
};
for (testcases) |tc| {
matcher.reset();
const doc = try parser.documentHTMLParseFromStr(tc.html);
defer parser.documentHTMLClose(doc) catch {};
const s = css.parse(alloc, tc.q, .{}) catch |e| {
std.debug.print("parse, query: {s}\n", .{tc.q});
return e;
};
defer s.deinit(alloc);
const node = Node{ .node = parser.documentHTMLToNode(doc) };
_ = css.matchAll(&s, node, &matcher) catch |e| {
std.debug.print("match, query: {s}\n", .{tc.q});
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("expectation, query: {s}\n", .{tc.q});
return e;
};
}
}

View File

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

View File

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

View File

@@ -557,8 +557,6 @@ pub const Parser = struct {
const val = try buf.toOwnedSlice(allocator);
errdefer allocator.free(val);
lowerstr(val);
return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } };
},
.matches, .matchesown => {
@@ -823,7 +821,8 @@ pub const Parser = struct {
// nameStart returns whether c can be the first character of an identifier
// (not counting an initial hyphen, or an escape sequence).
fn nameStart(c: u8) bool {
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127;
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127 or
'0' <= c and c <= '9';
}
// nameChar returns whether c can be a character within an identifier
@@ -892,7 +891,7 @@ test "parser.parseIdentifier" {
err: bool = false,
}{
.{ .s = "x", .exp = "x" },
.{ .s = "96", .exp = "", .err = true },
.{ .s = "96", .exp = "96", .err = false },
.{ .s = "-x", .exp = "-x" },
.{ .s = "r\\e9 sumé", .exp = "résumé" },
.{ .s = "r\\0000e9 sumé", .exp = "résumé" },
@@ -977,6 +976,7 @@ test "parser.parse" {
.{ .s = ":root", .exp = .{ .pseudo_class = .root } },
.{ .s = ".\\:bar", .exp = .{ .class = ":bar" } },
.{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } },
.{ .s = "[class=75c0fa18a94b9e3a6b8e14d6cbe688a27f5da10a]", .exp = .{ .attribute = .{ .key = "class", .val = "75c0fa18a94b9e3a6b8e14d6cbe688a27f5da10a", .op = .eql } } },
};
for (testcases) |tc| {

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const css = @import("css.zig");
pub const AttributeOP = enum {
eql, // =
@@ -332,41 +334,39 @@ pub const Selector = union(enum) {
if (!try v.second.match(n)) return false;
// The first must match a ascendent.
var p = try n.parent();
while (p != null) {
if (try v.first.match(p.?)) {
var parent = n.parent();
while (parent) |p| {
if (try v.first.match(p)) {
return true;
}
p = try p.?.parent();
parent = p.parent();
}
return false;
},
.child => {
const p = try n.parent();
if (p == null) return false;
return try v.second.match(n) and try v.first.match(p.?);
const p = n.parent() orelse return false;
return try v.second.match(n) and try v.first.match(p);
},
.next_sibling => {
if (!try v.second.match(n)) return false;
var c = try n.prevSibling();
while (c != null) {
if (c.?.isText() or c.?.isComment()) {
c = try c.?.prevSibling();
var child = n.prevSibling();
while (child) |c| {
if (c.isText() or c.isComment()) {
child = c.prevSibling();
continue;
}
return try v.first.match(c.?);
return try v.first.match(c);
}
return false;
},
.subsequent_sibling => {
if (!try v.second.match(n)) return false;
var c = try n.prevSibling();
while (c != null) {
if (try v.first.match(c.?)) return true;
c = try c.?.prevSibling();
var child = n.prevSibling();
while (child) |c| {
if (try v.first.match(c)) return true;
child = c.prevSibling();
}
return false;
},
@@ -432,7 +432,25 @@ pub const Selector = union(enum) {
else => Error.UnsupportedRelativePseudoClass,
};
},
.pseudo_class_contains => return Error.UnsupportedContainsPseudoClass, // TODO, need mem allocation.
.pseudo_class_contains => |v| {
// Only containsOwn is implemented.
if (v.own == false) return Error.UnsupportedContainsPseudoClass;
var child = n.firstChild();
while (child) |c| {
if (c.isText()) {
const text = c.text();
if (text) |_text| {
if (contains(_text, v.val, false)) { // we are case sensitive. Is this correct behavior?
return true;
}
}
}
child = c.nextSibling();
}
return false;
},
.pseudo_class_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation.
.pseudo_class_nth => |v| {
if (v.a == 0) {
@@ -457,16 +475,16 @@ pub const Selector = union(enum) {
.empty => {
if (!n.isElement()) return false;
var c = try n.firstChild();
while (c != null) {
if (c.?.isElement()) return false;
var child = n.firstChild();
while (child) |c| {
if (c.isElement()) return false;
if (c.?.isText()) {
if (try c.?.isEmptyText()) continue;
if (c.isText()) {
if (c.isEmptyText()) continue;
return false;
}
c = try c.?.nextSibling();
child = c.nextSibling();
}
return true;
@@ -474,7 +492,7 @@ pub const Selector = union(enum) {
.root => {
if (!n.isElement()) return false;
const p = try n.parent();
const p = n.parent();
return (p != null and p.?.isDocument());
},
.link => {
@@ -544,7 +562,7 @@ pub const Selector = union(enum) {
const ntag = try n.tag();
if (std.ascii.eqlIgnoreCase("intput", ntag)) {
if (std.ascii.eqlIgnoreCase("input", ntag)) {
const ntype = try n.attr("type");
if (ntype == null) return false;
@@ -589,24 +607,23 @@ pub const Selector = union(enum) {
}
fn hasLegendInPreviousSiblings(n: anytype) anyerror!bool {
var c = try n.prevSibling();
while (c != null) {
const ctag = try c.?.tag();
var child = n.prevSibling();
while (child) |c| {
const ctag = try c.tag();
if (std.ascii.eqlIgnoreCase("legend", ctag)) return true;
c = try c.?.prevSibling();
child = c.prevSibling();
}
return false;
}
fn inDisabledFieldset(n: anytype) anyerror!bool {
const p = try n.parent();
if (p == null) return false;
const p = n.parent() orelse return false;
const ntag = try n.tag();
const ptag = try p.?.tag();
const ptag = try p.tag();
if (std.ascii.eqlIgnoreCase("fieldset", ptag) and
try p.?.attr("disabled") != null and
try p.attr("disabled") != null and
(!std.ascii.eqlIgnoreCase("legend", ntag) or try hasLegendInPreviousSiblings(n)))
{
return true;
@@ -622,7 +639,7 @@ pub const Selector = union(enum) {
// ```
// https://github.com/andybalholm/cascadia/blob/master/pseudo_classes.go#L434
return try inDisabledFieldset(p.?);
return try inDisabledFieldset(p);
}
fn langMatch(lang: []const u8, n: anytype) anyerror!bool {
@@ -636,10 +653,8 @@ pub const Selector = union(enum) {
}
// if the tag doesn't match, try the parent.
const p = try n.parent();
if (p == null) return false;
return langMatch(lang, p.?);
const p = n.parent() orelse return false;
return langMatch(lang, p);
}
// onlyChildMatch implements :only-child
@@ -647,25 +662,24 @@ pub const Selector = union(enum) {
fn onlyChildMatch(of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false;
const p = try n.parent();
if (p == null) return false;
const p = n.parent() orelse return false;
const ntag = try n.tag();
var count: usize = 0;
var c = try p.?.firstChild();
var child = p.firstChild();
// loop hover all n siblings.
while (c != null) {
while (child) |c| {
// ignore non elements or others tags if of-type is true.
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
c = try c.?.nextSibling();
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) {
child = c.nextSibling();
continue;
}
count += 1;
if (count > 1) return false;
c = try c.?.nextSibling();
child = c.nextSibling();
}
return count == 1;
@@ -676,27 +690,25 @@ pub const Selector = union(enum) {
fn simpleNthLastChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false;
const p = try n.parent();
if (p == null) return false;
const p = n.parent() orelse return false;
const ntag = try n.tag();
var count: isize = 0;
var c = try p.?.lastChild();
var child = p.lastChild();
// loop hover all n siblings.
while (c != null) {
while (child) |c| {
// ignore non elements or others tags if of-type is true.
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
c = try c.?.prevSibling();
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) {
child = c.prevSibling();
continue;
}
count += 1;
if (n.eql(c.?)) return count == b;
if (n.eql(c)) return count == b;
if (count >= b) return false;
c = try c.?.prevSibling();
child = c.prevSibling();
}
return false;
@@ -707,27 +719,25 @@ pub const Selector = union(enum) {
fn simpleNthChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false;
const p = try n.parent();
if (p == null) return false;
const p = n.parent() orelse return false;
const ntag = try n.tag();
var count: isize = 0;
var c = try p.?.firstChild();
var child = p.firstChild();
// loop hover all n siblings.
while (c != null) {
while (child) |c| {
// ignore non elements or others tags if of-type is true.
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
c = try c.?.nextSibling();
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) {
child = c.nextSibling();
continue;
}
count += 1;
if (n.eql(c.?)) return count == b;
if (n.eql(c)) return count == b;
if (count >= b) return false;
c = try c.?.nextSibling();
child = c.nextSibling();
}
return false;
@@ -739,29 +749,27 @@ pub const Selector = union(enum) {
fn nthChildMatch(a: isize, b: isize, last: bool, of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false;
const p = try n.parent();
if (p == null) return false;
const p = n.parent() orelse return false;
const ntag = try n.tag();
var i: isize = -1;
var count: isize = 0;
var c = try p.?.firstChild();
var child = p.firstChild();
// loop hover all n siblings.
while (c != null) {
while (child) |c| {
// ignore non elements or others tags if of-type is true.
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
c = try c.?.nextSibling();
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) {
child = c.nextSibling();
continue;
}
count += 1;
if (n.eql(c.?)) {
if (n.eql(c)) {
i = count;
if (!last) break;
}
c = try c.?.nextSibling();
child = c.nextSibling();
}
if (i == -1) return false;
@@ -774,21 +782,21 @@ pub const Selector = union(enum) {
}
fn hasDescendantMatch(s: *const Selector, n: anytype) anyerror!bool {
var c = try n.firstChild();
while (c != null) {
if (try s.match(c.?)) return true;
if (c.?.isElement() and try hasDescendantMatch(s, c.?)) return true;
c = try c.?.nextSibling();
var child = n.firstChild();
while (child) |c| {
if (try s.match(c)) return true;
if (c.isElement() and try hasDescendantMatch(s, c)) return true;
child = c.nextSibling();
}
return false;
}
fn hasChildMatch(s: *const Selector, n: anytype) anyerror!bool {
var c = try n.firstChild();
while (c != null) {
if (try s.match(c.?)) return true;
c = try c.?.nextSibling();
var child = n.firstChild();
while (child) |c| {
if (try s.match(c)) return true;
child = c.nextSibling();
}
return false;
@@ -827,3 +835,583 @@ pub const Selector = union(enum) {
}
}
};
// NodeTest mock implementation for test only.
pub const NodeTest = struct {
child: ?*const NodeTest = null,
last: ?*const NodeTest = null,
sibling: ?*const NodeTest = null,
prev: ?*const NodeTest = null,
par: ?*const NodeTest = null,
name: []const u8 = "",
att: ?[]const u8 = null,
pub fn firstChild(n: *const NodeTest) ?*const NodeTest {
return n.child;
}
pub fn lastChild(n: *const NodeTest) ?*const NodeTest {
return n.last;
}
pub fn nextSibling(n: *const NodeTest) ?*const NodeTest {
return n.sibling;
}
pub fn prevSibling(n: *const NodeTest) ?*const NodeTest {
return n.prev;
}
pub fn parent(n: *const NodeTest) ?*const NodeTest {
return n.par;
}
pub fn isElement(_: *const NodeTest) bool {
return true;
}
pub fn isDocument(_: *const NodeTest) bool {
return false;
}
pub fn isComment(_: *const NodeTest) bool {
return false;
}
pub fn text(_: *const NodeTest) ?[]const u8 {
return null;
}
pub fn isText(_: *const NodeTest) bool {
return false;
}
pub fn isEmptyText(_: *const NodeTest) bool {
return false;
}
pub fn tag(n: *const NodeTest) ![]const u8 {
return n.name;
}
pub fn attr(n: *const NodeTest, _: []const u8) !?[]const u8 {
return n.att;
}
pub fn eql(a: *const NodeTest, b: *const NodeTest) bool {
return a == b;
}
};
const MatcherTest = struct {
const NodeTests = std.ArrayListUnmanaged(*const NodeTest);
nodes: NodeTests,
allocator: Allocator,
fn init(allocator: Allocator) MatcherTest {
return .{
.nodes = .empty,
.allocator = allocator,
};
}
fn deinit(m: *MatcherTest) void {
m.nodes.deinit(m.allocator);
}
fn reset(m: *MatcherTest) void {
m.nodes.clearRetainingCapacity();
}
pub fn match(m: *MatcherTest, n: *const NodeTest) !void {
try m.nodes.append(m.allocator, n);
}
};
test "Browser.CSS.Selector: matchFirst" {
const alloc = std.testing.allocator;
var matcher = MatcherTest.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
n: NodeTest,
exp: usize,
}{
.{
.q = "address",
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
.exp = 1,
},
.{
.q = "#foo",
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
.exp = 1,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
.exp = 0,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 0,
},
.{
.q = "[foo=1baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 0,
},
.{
.q = "[foo!=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo!=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
.exp = 1,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 0,
},
.{
.q = "[foo^=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo$=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo*=rb]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
.exp = 0,
},
.{
.q = "strong, a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
.name = "a",
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
} } } },
.exp = 1,
},
.{
.q = ":not(p)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:has(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:has(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:haschild(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:haschild(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:lang(en)",
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "a:lang(en)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
.exp = 1,
},
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
_ = css.matchFirst(&s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}
test "Browser.CSS.Selector: matchAll" {
const alloc = std.testing.allocator;
var matcher = MatcherTest.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
n: NodeTest,
exp: usize,
}{
.{
.q = "address",
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
.exp = 1,
},
.{
.q = "#foo",
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
.exp = 1,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
.exp = 0,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 0,
},
.{
.q = "[foo!=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo!=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 2,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
.exp = 1,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 0,
},
.{
.q = "[foo^=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo$=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo*=rb]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
.exp = 0,
},
.{
.q = "strong, a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 2,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
.name = "a",
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
} } } },
.exp = 1,
},
.{
.q = ":not(p)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 2,
},
.{
.q = "p:has(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:has(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:haschild(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:haschild(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:lang(en)",
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "a:lang(en)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
.exp = 1,
},
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
css.matchAll(&s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}
test "Browser.CSS.Selector: pseudo class" {
const alloc = std.testing.allocator;
var matcher = MatcherTest.init(alloc);
defer matcher.deinit();
var p1: NodeTest = .{ .name = "p" };
var p2: NodeTest = .{ .name = "p" };
var a1: NodeTest = .{ .name = "a" };
p1.sibling = &p2;
p2.prev = &p1;
p2.sibling = &a1;
a1.prev = &p2;
var root: NodeTest = .{ .child = &p1, .last = &a1 };
p1.par = &root;
p2.par = &root;
a1.par = &root;
const testcases = [_]struct {
q: []const u8,
n: NodeTest,
exp: ?*const NodeTest,
}{
.{ .q = "p:only-child", .n = root, .exp = null },
.{ .q = "a:only-of-type", .n = root, .exp = &a1 },
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
css.matchAll(&s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
if (tc.exp) |exp_n| {
const exp: usize = 1;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
continue;
}
const exp: usize = 0;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}
test "Browser.CSS.Selector: nth pseudo class" {
const alloc = std.testing.allocator;
var matcher = MatcherTest.init(alloc);
defer matcher.deinit();
var p1: NodeTest = .{ .name = "p" };
var p2: NodeTest = .{ .name = "p" };
p1.sibling = &p2;
p2.prev = &p1;
var root: NodeTest = .{ .child = &p1, .last = &p2 };
p1.par = &root;
p2.par = &root;
const testcases = [_]struct {
q: []const u8,
n: NodeTest,
exp: ?*const NodeTest,
}{
.{ .q = "a:nth-of-type(1)", .n = root, .exp = null },
.{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 },
.{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 },
.{ .q = "p:nth-of-type(0)", .n = root, .exp = null },
.{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 },
.{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 },
.{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 },
.{ .q = "p:nth-child(1)", .n = root, .exp = &p1 },
.{ .q = "p:nth-child(2)", .n = root, .exp = &p2 },
.{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 },
.{ .q = "p:nth-child(even)", .n = root, .exp = &p2 },
.{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 },
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
css.matchAll(&s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
if (tc.exp) |exp_n| {
const exp: usize = 1;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
continue;
}
const exp: usize = 0;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}

View File

@@ -19,7 +19,6 @@
const std = @import("std");
const CSSRule = @import("CSSRule.zig");
const StyleSheet = @import("StyleSheet.zig").StyleSheet;
const CSSImportRule = CSSRule.CSSImportRule;

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const Env = @import("../env.zig").Env;
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const StyleSheet = @import("StyleSheet.zig");
const CSSRuleList = @import("CSSRuleList.zig");
@@ -73,15 +73,13 @@ pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
_ = self.css_rules.list.orderedRemove(index);
}
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !Env.Promise {
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.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();
return page.js.resolvePromise({});
}
pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {

View File

@@ -18,19 +18,17 @@
const std = @import("std");
const js = @import("../js/js.zig");
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,
effect: ?js.Object,
timeline: ?js.Object,
ready_resolver: ?js.PromiseResolver,
finished_resolver: ?js.PromiseResolver,
pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !Animation {
pub fn constructor(effect: ?js.Object, timeline: ?js.Object) !Animation {
return .{
.effect = if (effect) |eo| try eo.persist() else null,
.timeline = if (timeline) |to| try to.persist() else null,
@@ -49,37 +47,37 @@ pub fn get_pending(self: *const Animation) bool {
return false;
}
pub fn get_finished(self: *Animation, page: *Page) !Promise {
pub fn get_finished(self: *Animation, page: *Page) !js.Promise {
if (self.finished_resolver == null) {
const resolver = page.main_context.createPromiseResolver();
const resolver = page.js.createPromiseResolver(.none);
try resolver.resolve(self);
self.finished_resolver = resolver;
}
return self.finished_resolver.?.promise();
}
pub fn get_ready(self: *Animation, page: *Page) !Promise {
pub fn get_ready(self: *Animation, page: *Page) !js.Promise {
// never resolved, because we're always "finished"
if (self.ready_resolver == null) {
const resolver = page.main_context.createPromiseResolver();
const resolver = page.js.createPromiseResolver(.none);
self.ready_resolver = resolver;
}
return self.ready_resolver.?.promise();
}
pub fn get_effect(self: *const Animation) ?JsObject {
pub fn get_effect(self: *const Animation) ?js.Object {
return self.effect;
}
pub fn set_effect(self: *Animation, effect: JsObject) !void {
pub fn set_effect(self: *Animation, effect: js.Object) !void {
self.effect = try effect.persist();
}
pub fn get_timeline(self: *const Animation) ?JsObject {
pub fn get_timeline(self: *const Animation) ?js.Object {
return self.timeline;
}
pub fn set_timeline(self: *Animation, timeline: JsObject) !void {
pub fn set_timeline(self: *Animation, timeline: js.Object) !void {
self.timeline = try timeline.persist();
}

View File

@@ -0,0 +1,329 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const Node = @import("node.zig").Node;
const Element = @import("element.zig").Element;
pub const Interfaces = .{
IntersectionObserver,
Entry,
};
// This implementation attempts to be as less wrong as possible. Since we don't
// render, or know how things are positioned, our best guess isn't very good.
const IntersectionObserver = @This();
page: *Page,
root: *parser.Node,
callback: js.Function,
event_node: parser.EventNode,
observed_entries: std.ArrayList(Entry),
pending_elements: std.ArrayList(*parser.Element),
ready_elements: std.ArrayList(*parser.Element),
pub fn constructor(callback: js.Function, opts_: ?IntersectionObserverOptions, page: *Page) !*IntersectionObserver {
const opts = opts_ orelse IntersectionObserverOptions{};
const self = try page.arena.create(IntersectionObserver);
self.* = .{
.page = page,
.callback = callback,
.ready_elements = .{},
.observed_entries = .{},
.pending_elements = .{},
.event_node = .{ .func = mutationCallback },
.root = opts.root orelse parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
};
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, self.root),
"DOMNodeInserted",
&self.event_node,
false,
);
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, self.root),
"DOMNodeRemoved",
&self.event_node,
false,
);
return self;
}
pub fn _disconnect(self: *IntersectionObserver) !void {
// We don't free as it is on an arena
self.ready_elements = .{};
self.observed_entries = .{};
self.pending_elements = .{};
}
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element, page: *Page) !void {
for (self.observed_entries.items) |*observer| {
if (observer.target == target_element) {
return; // Already observed
}
}
if (self.isPending(target_element)) {
return; // Already pending
}
for (self.ready_elements.items) |element| {
if (element == target_element) {
return; // Already primed
}
}
// We can never fire callbacks synchronously. Code like React expects any
// callback to fire in the future (e.g. via microtasks).
try self.ready_elements.append(self.page.arena, target_element);
if (self.ready_elements.items.len == 1) {
// this is our first ready entry, schedule a callback
try page.scheduler.add(self, processReady, 0, .{
.name = "intersection ready",
});
}
}
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
if (self.removeObserved(target)) {
return;
}
for (self.ready_elements.items, 0..) |el, index| {
if (el == target) {
_ = self.ready_elements.swapRemove(index);
return;
}
}
for (self.pending_elements.items, 0..) |el, index| {
if (el == target) {
_ = self.pending_elements.swapRemove(index);
return;
}
}
}
pub fn _takeRecords(self: *IntersectionObserver) []Entry {
return self.observed_entries.items;
}
fn processReady(ctx: *anyopaque) ?u32 {
const self: *IntersectionObserver = @ptrCast(@alignCast(ctx));
self._processReady() catch |err| {
log.err(.web_api, "intersection ready", .{ .err = err });
};
return null;
}
fn _processReady(self: *IntersectionObserver) !void {
defer self.ready_elements.clearRetainingCapacity();
for (self.ready_elements.items) |element| {
// IntersectionObserver probably doesn't work like what your intuition
// thinks. As long as a node has a parent, even if that parent isn't
// connected and even if the two nodes don't intersect, it'll fire the
// callback once.
if (try Node.get_parentNode(@ptrCast(element)) == null) {
if (!self.isPending(element)) {
try self.pending_elements.append(self.page.arena, element);
}
continue;
}
try self.forceObserve(element);
}
}
fn isPending(self: *IntersectionObserver, element: *parser.Element) bool {
for (self.pending_elements.items) |el| {
if (el == element) {
return true;
}
}
return false;
}
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
const mutation_event = parser.eventToMutationEvent(event);
const self: *IntersectionObserver = @fieldParentPtr("event_node", en);
self._mutationCallback(mutation_event) catch |err| {
log.err(.web_api, "mutation callback", .{ .err = err, .source = "intersection observer" });
};
}
fn _mutationCallback(self: *IntersectionObserver, event: *parser.MutationEvent) !void {
const event_type = parser.eventType(@ptrCast(event));
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
if (parser.nodeType(node) != .element) {
return;
}
const el: *parser.Element = @ptrCast(node);
if (self.removePending(el)) {
// It was pending (because it wasn't in the root), but now it is
// we should observe it.
try self.forceObserve(el);
}
return;
}
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
if (parser.nodeType(node) != .element) {
return;
}
const el: *parser.Element = @ptrCast(node);
if (self.removeObserved(el)) {
// It _was_ observed, it no longer is in our root, but if it was
// to get re-added, it should be observed again (I think), so
// we add it to our pending list
try self.pending_elements.append(self.page.arena, el);
}
return;
}
// impossible event type
unreachable;
}
// Exists to skip the checks made _observe when called from a DOMNodeInserted
// event. In such events, the event handler has alread done the necessary
// checks.
fn forceObserve(self: *IntersectionObserver, target: *parser.Element) !void {
try self.observed_entries.append(self.page.arena, .{
.page = self.page,
.root = self.root,
.target = target,
});
var result: js.Function.Result = undefined;
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
log.debug(.user_script, "callback error", .{
.err = result.exception,
.stack = result.stack,
.source = "intersection observer",
});
};
}
fn removeObserved(self: *IntersectionObserver, target: *parser.Element) bool {
for (self.observed_entries.items, 0..) |*observer, index| {
if (observer.target == target) {
_ = self.observed_entries.swapRemove(index);
return true;
}
}
return false;
}
fn removePending(self: *IntersectionObserver, target: *parser.Element) bool {
for (self.pending_elements.items, 0..) |el, index| {
if (el == target) {
_ = self.pending_elements.swapRemove(index);
return true;
}
}
return false;
}
const IntersectionObserverOptions = struct {
root: ?*parser.Node = null, // Element or Document
rootMargin: ?[]const u8 = "0px 0px 0px 0px",
threshold: ?Threshold = .{ .single = 0.0 },
const Threshold = union(enum) {
single: f32,
list: []const f32,
};
};
// https://developer.mozilla.org/en-US/docs/Web/API/Entry
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
pub const Entry = struct {
page: *Page,
root: *parser.Node,
target: *parser.Element,
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
pub fn get_boundingClientRect(self: *const Entry) !Element.DOMRect {
return Element._getBoundingClientRect(self.target, self.page);
}
// Returns the ratio of the intersectionRect to the boundingClientRect.
pub fn get_intersectionRatio(_: *const Entry) f32 {
return 1.0;
}
// Returns a DOMRectReadOnly representing the target's visible area.
pub fn get_intersectionRect(self: *const Entry) !Element.DOMRect {
return Element._getBoundingClientRect(self.target, self.page);
}
// A Boolean value which is true if the target element intersects with the
// intersection observer's root. If this is true, then, the
// Entry describes a transition into a state of
// intersection; if it's false, then you know the transition is from
// intersecting to not-intersecting.
pub fn get_isIntersecting(_: *const Entry) bool {
return true;
}
// Returns a DOMRectReadOnly for the intersection observer's root.
pub fn get_rootBounds(self: *const Entry) !Element.DOMRect {
const root = self.root;
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
return self.page.renderer.boundingRect();
}
const root_type = parser.nodeType(root);
var element: *parser.Element = undefined;
switch (root_type) {
.element => element = parser.nodeToElement(root),
.document => {
const doc = parser.nodeToDocument(root);
element = (try parser.documentGetDocumentElement(doc)).?;
},
else => return error.InvalidState,
}
return Element._getBoundingClientRect(element, self.page);
}
// The Element whose intersection with the root changed.
pub fn get_target(self: *const Entry) *parser.Element {
return self.target;
}
// TODO: pub fn get_time(self: *const Entry)
};
const testing = @import("../../testing.zig");
test "Browser: DOM.IntersectionObserver" {
try testing.htmlRunner("dom/intersection_observer.html");
}

View File

@@ -20,13 +20,11 @@ const std = @import("std");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const js = @import("../js/js.zig");
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;
@@ -72,29 +70,28 @@ pub const MessagePort = struct {
pair: *MessagePort,
closed: bool = false,
started: bool = false,
onmessage_cbk: ?Function = null,
onmessageerror_cbk: ?Function = null,
onmessage_cbk: ?js.Function = null,
onmessageerror_cbk: ?js.Function = null,
// This is the queue of messages to dispatch to THIS MessagePort when the
// MessagePort is started.
queue: std.ArrayListUnmanaged(JsObject) = .empty,
queue: std.ArrayListUnmanaged(js.Object) = .empty,
pub const PostMessageOption = union(enum) {
transfer: JsObject,
transfer: js.Object,
options: Opts,
pub const Opts = struct {
transfer: JsObject,
transfer: js.Object,
};
};
pub fn _postMessage(self: *MessagePort, obj: JsObject, opts_: ?PostMessageOption, page: *Page) !void {
pub fn _postMessage(self: *MessagePort, obj: js.Object, 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);
@@ -125,10 +122,10 @@ pub const MessagePort = struct {
self.pair.closed = true;
}
pub fn get_onmessage(self: *MessagePort) ?Function {
pub fn get_onmessage(self: *MessagePort) ?js.Function {
return self.onmessage_cbk;
}
pub fn get_onmessageerror(self: *MessagePort) ?Function {
pub fn get_onmessageerror(self: *MessagePort) ?js.Function {
return self.onmessageerror_cbk;
}
@@ -153,7 +150,7 @@ pub const MessagePort = struct {
// 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 {
fn dispatchOrQueue(self: *MessagePort, obj: js.Object, arena: Allocator) !void {
// our pair should have checked this already
std.debug.assert(self.closed == false);
@@ -168,7 +165,7 @@ pub const MessagePort = struct {
return self.queue.append(arena, try obj.persist());
}
fn dispatch(self: *MessagePort, obj: JsObject) !void {
fn dispatch(self: *MessagePort, obj: js.Object) !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 });
@@ -183,7 +180,7 @@ pub const MessagePort = struct {
alloc: Allocator,
typ: []const u8,
listener: EventHandler.Listener,
) !?Function {
) !?js.Function {
const target = @as(*parser.EventTarget, @ptrCast(self));
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
return eh.callback;
@@ -208,12 +205,12 @@ pub const MessageEvent = struct {
pub const union_make_copy = true;
proto: parser.Event,
data: ?JsObject,
data: ?js.Object,
// 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,
source: ?js.Object,
origin: []const u8,
@@ -227,8 +224,8 @@ pub const MessageEvent = struct {
ports: []*MessagePort,
const Options = struct {
data: ?JsObject = null,
source: ?JsObject = null,
data: ?js.Object = null,
source: ?js.Object = null,
origin: []const u8 = "",
lastEventId: []const u8 = "",
ports: []*MessagePort = &.{},
@@ -244,7 +241,7 @@ pub const MessageEvent = struct {
});
}
// This is like "constructor", but it assumes JsObjects have already been
// This is like "constructor", but it assumes js.Objects 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);
@@ -252,7 +249,7 @@ pub const MessageEvent = struct {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, "message", .{});
try parser.eventSetInternalType(event, .message_event);
parser.eventSetInternalType(event, .message_event);
return .{
.proto = event.*,
@@ -264,7 +261,7 @@ pub const MessageEvent = struct {
};
}
pub fn get_data(self: *const MessageEvent) !?JsObject {
pub fn get_data(self: *const MessageEvent) !?js.Object {
return self.data;
}
@@ -272,7 +269,7 @@ pub const MessageEvent = struct {
return self.origin;
}
pub fn get_source(self: *const MessageEvent) ?JsObject {
pub fn get_source(self: *const MessageEvent) ?js.Object {
return self.source;
}

View File

@@ -25,24 +25,24 @@ pub const Attr = struct {
pub const prototype = *Node;
pub const subtype = .node;
pub fn get_namespaceURI(self: *parser.Attribute) !?[]const u8 {
return try parser.nodeGetNamespace(parser.attributeToNode(self));
pub fn get_namespaceURI(self: *parser.Attribute) ?[]const u8 {
return parser.nodeGetNamespace(parser.attributeToNode(self));
}
pub fn get_prefix(self: *parser.Attribute) !?[]const u8 {
return try parser.nodeGetPrefix(parser.attributeToNode(self));
pub fn get_prefix(self: *parser.Attribute) ?[]const u8 {
return parser.nodeGetPrefix(parser.attributeToNode(self));
}
pub fn get_localName(self: *parser.Attribute) ![]const u8 {
return try parser.nodeLocalName(parser.attributeToNode(self));
return parser.nodeLocalName(parser.attributeToNode(self));
}
pub fn get_name(self: *parser.Attribute) ![]const u8 {
return try parser.attributeGetName(self);
return parser.attributeGetName(self);
}
pub fn get_value(self: *parser.Attribute) !?[]const u8 {
return try parser.attributeGetValue(self);
return parser.attributeGetValue(self);
}
pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 {

View File

@@ -51,7 +51,7 @@ pub const CharacterData = struct {
}
pub fn get_nextElementSibling(self: *parser.CharacterData) !?ElementUnion {
const res = try parser.nodeNextElementSibling(parser.characterDataToNode(self));
const res = parser.nodeNextElementSibling(parser.characterDataToNode(self));
if (res == null) {
return null;
}
@@ -59,7 +59,7 @@ pub const CharacterData = struct {
}
pub fn get_previousElementSibling(self: *parser.CharacterData) !?ElementUnion {
const res = try parser.nodePreviousElementSibling(parser.characterDataToNode(self));
const res = parser.nodePreviousElementSibling(parser.characterDataToNode(self));
if (res == null) {
return null;
}
@@ -68,8 +68,8 @@ pub const CharacterData = struct {
// Read/Write attributes
pub fn get_data(self: *parser.CharacterData) ![]const u8 {
return try parser.characterDataData(self);
pub fn get_data(self: *parser.CharacterData) []const u8 {
return parser.characterDataData(self);
}
pub fn set_data(self: *parser.CharacterData, data: []const u8) !void {
@@ -96,18 +96,18 @@ pub const CharacterData = struct {
}
pub fn _substringData(self: *parser.CharacterData, offset: u32, count: u32) ![]const u8 {
return try parser.characterDataSubstringData(self, offset, count);
return parser.characterDataSubstringData(self, offset, count);
}
// netsurf's CharacterData (text, comment) doesn't implement the
// dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode.
pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) !bool {
if (try parser.nodeType(@ptrCast(@alignCast(self))) != try parser.nodeType(other_node)) {
pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) bool {
if (parser.nodeType(@ptrCast(@alignCast(self))) != parser.nodeType(other_node)) {
return false;
}
const other: *parser.CharacterData = @ptrCast(other_node);
if (std.mem.eql(u8, try get_data(self), try get_data(other)) == false) {
if (std.mem.eql(u8, get_data(self), get_data(other)) == false) {
return false;
}

View File

@@ -17,8 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
@@ -38,8 +39,6 @@ const Range = @import("range.zig").Range;
const CustomEvent = @import("../events/custom_event.zig").CustomEvent;
const Env = @import("../env.zig").Env;
const DOMImplementation = @import("implementation.zig").DOMImplementation;
// WEB IDL https://dom.spec.whatwg.org/#document
@@ -116,7 +115,9 @@ pub const Document = struct {
base: *parser.Event,
custom: CustomEvent,
} {
if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) {
const eqlIgnoreCase = std.ascii.eqlIgnoreCase;
if (eqlIgnoreCase(eventCstr, "Event") or eqlIgnoreCase(eventCstr, "Events") or eqlIgnoreCase(eventCstr, "HTMLEvents")) {
return .{ .base = try parser.eventCreate() };
}
@@ -154,22 +155,14 @@ pub const Document = struct {
// the spec changed to return an HTMLCollection instead.
// That's why we reimplemented getElementsByTagName by using an
// HTMLCollection in zig here.
pub fn _getElementsByTagName(
self: *parser.Document,
tag_name: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, .{
pub fn _getElementsByTagName(self: *parser.Document, tag_name: js.String) !collection.HTMLCollection {
return collection.HTMLCollectionByTagName(parser.documentToNode(self), tag_name.string, .{
.include_root = true,
});
}
pub fn _getElementsByClassName(
self: *parser.Document,
classNames: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, .{
pub fn _getElementsByClassName(self: *parser.Document, class_names: js.String) !collection.HTMLCollection {
return collection.HTMLCollectionByClassName(parser.documentToNode(self), class_names.string, .{
.include_root = true,
});
}
@@ -306,21 +299,26 @@ pub const Document = struct {
return &.{};
}
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !Env.JsObject {
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !js.Object {
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();
const obj = try page.js.createArray(0).persist();
state.adopted_style_sheets = obj;
return obj;
}
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: Env.JsObject, page: *Page) !void {
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: js.Object, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.adopted_style_sheets = try sheets.persist();
}
pub fn _hasFocus(_: *parser.Document) bool {
log.debug(.web_api, "not implemented", .{ .feature = "Document hasFocus" });
return true;
}
};
const testing = @import("../../testing.zig");

View File

@@ -38,8 +38,8 @@ pub const DocumentFragment = struct {
);
}
pub fn _isEqualNode(self: *parser.DocumentFragment, other_node: *parser.Node) !bool {
const other_type = try parser.nodeType(other_node);
pub fn _isEqualNode(self: *parser.DocumentFragment, other_node: *parser.Node) bool {
const other_type = parser.nodeType(other_node);
if (other_type != .document_fragment) {
return false;
}

View File

@@ -29,21 +29,21 @@ pub const DocumentType = struct {
pub const subtype = .node;
pub fn get_name(self: *parser.DocumentType) ![]const u8 {
return try parser.documentTypeGetName(self);
return parser.documentTypeGetName(self);
}
pub fn get_publicId(self: *parser.DocumentType) ![]const u8 {
return try parser.documentTypeGetPublicId(self);
pub fn get_publicId(self: *parser.DocumentType) []const u8 {
return parser.documentTypeGetPublicId(self);
}
pub fn get_systemId(self: *parser.DocumentType) ![]const u8 {
return try parser.documentTypeGetSystemId(self);
pub fn get_systemId(self: *parser.DocumentType) []const u8 {
return parser.documentTypeGetSystemId(self);
}
// netsurf's DocumentType doesn't implement the dom_node_get_attributes
// and thus will crash if we try to call nodeIsEqualNode.
pub fn _isEqualNode(self: *parser.DocumentType, other_node: *parser.Node) !bool {
if (try parser.nodeType(other_node) != .document_type) {
if (parser.nodeType(other_node) != .document_type) {
return false;
}
@@ -51,10 +51,10 @@ pub const DocumentType = struct {
if (std.mem.eql(u8, try get_name(self), try get_name(other)) == false) {
return false;
}
if (std.mem.eql(u8, try get_publicId(self), try get_publicId(other)) == false) {
if (std.mem.eql(u8, get_publicId(self), get_publicId(other)) == false) {
return false;
}
if (std.mem.eql(u8, try get_systemId(self), try get_systemId(other)) == false) {
if (std.mem.eql(u8, get_systemId(self), get_systemId(other)) == false) {
return false;
}
return true;

View File

@@ -25,7 +25,6 @@ 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;
@@ -44,7 +43,6 @@ pub const Interfaces = .{
Node.Interfaces,
ResizeObserver.Interfaces,
MutationObserver.Interfaces,
IntersectionObserver.Interfaces,
DOMParser,
TreeWalker,
NodeIterator,
@@ -54,4 +52,5 @@ pub const Interfaces = .{
@import("range.zig").Interfaces,
@import("Animation.zig"),
@import("MessageChannel.zig").Interfaces,
@import("IntersectionObserver.zig").Interfaces,
};

View File

@@ -18,6 +18,7 @@
const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
@@ -33,7 +34,6 @@ 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;
@@ -61,7 +61,7 @@ pub const Element = struct {
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));
const doc = parser.nodeOwnerDocument(parser.elementToNode(e));
if (doc != null and !doc.?.is_html) {
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
}
@@ -73,7 +73,7 @@ pub const Element = struct {
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));
const doc = parser.nodeOwnerDocument(parser.elementToNode(e));
if (doc != null and doc.?.is_html) {
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
}
@@ -87,12 +87,12 @@ pub const Element = struct {
// JS funcs
// --------
pub fn get_namespaceURI(self: *parser.Element) !?[]const u8 {
return try parser.nodeGetNamespace(parser.elementToNode(self));
pub fn get_namespaceURI(self: *parser.Element) ?[]const u8 {
return parser.nodeGetNamespace(parser.elementToNode(self));
}
pub fn get_prefix(self: *parser.Element) !?[]const u8 {
return try parser.nodeGetPrefix(parser.elementToNode(self));
pub fn get_prefix(self: *parser.Element) ?[]const u8 {
return parser.nodeGetPrefix(parser.elementToNode(self));
}
pub fn get_localName(self: *parser.Element) ![]const u8 {
@@ -103,6 +103,14 @@ pub const Element = struct {
return try parser.nodeName(parser.elementToNode(self));
}
pub fn get_dir(self: *parser.Element) ![]const u8 {
return try parser.elementGetAttribute(self, "dir") orelse "";
}
pub fn set_dir(self: *parser.Element, dir: []const u8) !void {
return parser.elementSetAttribute(self, "dir", dir);
}
pub fn get_id(self: *parser.Element) ![]const u8 {
return try parser.elementGetAttribute(self, "id") orelse "";
}
@@ -127,6 +135,10 @@ pub const Element = struct {
return try parser.elementSetAttribute(self, "slot", slot);
}
pub fn get_assignedSlot(self: *parser.Element, page: *const Page) !?*parser.Slot {
return @import("../SlotChangeMonitor.zig").findSlot(self, page);
}
pub fn get_classList(self: *parser.Element) !*parser.TokenList {
return try parser.tokenListCreate(self, "class");
}
@@ -150,7 +162,7 @@ pub const Element = struct {
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;
const doc = parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
// parse the fragment
const fragment = try parser.documentParseFragmentFromStr(doc, str);
@@ -168,9 +180,9 @@ pub const Element = struct {
// 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;
const html = parser.nodeFirstChild(fragment_node) orelse return;
const head = parser.nodeFirstChild(html) orelse return;
const body = parser.nodeNextSibling(head) orelse return;
if (try parser.elementTag(self) == .template) {
// HTMLElementTemplate is special. We don't append these as children
@@ -179,11 +191,9 @@ pub const Element = struct {
// 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;
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
while (parser.nodeListItem(children, 0)) |child| {
_ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child);
}
@@ -197,27 +207,102 @@ pub const Element = struct {
{
// 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;
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
while (parser.nodeListItem(children, 0)) |child| {
_ = 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;
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
while (parser.nodeListItem(children, 0)) |child| {
_ = try parser.nodeAppendChild(node, child);
}
}
}
/// Parses the given `input` string and inserts its children to an element at given `position`.
/// https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
///
/// TODO: Support for XML parsing and `TrustedHTML` instances.
pub fn _insertAdjacentHTML(self: *parser.Element, position: []const u8, input: []const u8) !void {
const self_node = parser.elementToNode(self);
const doc = parser.nodeOwnerDocument(self_node) orelse {
return parser.DOMError.WrongDocument;
};
// Parse the fragment.
// Should return error.Syntax on fail?
const fragment = try parser.documentParseFragmentFromStr(doc, input);
const fragment_node = parser.documentFragmentToNode(fragment);
// We always get it wrapped like so:
// <html><head></head><body>{ ... }</body></html>
// None of the following can be null.
const maybe_html = parser.nodeFirstChild(fragment_node);
std.debug.assert(maybe_html != null);
const html = maybe_html orelse return;
const maybe_body = parser.nodeLastChild(html);
std.debug.assert(maybe_body != null);
const body = maybe_body orelse return;
const children = try parser.nodeGetChildNodes(body);
// * `target_node` is `*Node` (where we actually insert),
// * `prev_node` is `?*Node`.
const target_node, const prev_node = blk: {
// Prefer case-sensitive match.
// "beforeend" was the most common case in my tests; we might adjust the order
// depending on which ones websites prefer most.
if (std.mem.eql(u8, position, "beforeend")) {
break :blk .{ self_node, null };
}
if (std.mem.eql(u8, position, "afterbegin")) {
// Get the first child; null indicates there are no children.
const first_child = parser.nodeFirstChild(self_node);
break :blk .{ self_node, first_child };
}
if (std.mem.eql(u8, position, "beforebegin")) {
// The node must have a parent node in order to use this variant.
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
// Parent cannot be Document.
// Should have checks for document_fragment and document_type?
if (parser.nodeType(parent) == .document) {
return error.NoModificationAllowed;
}
break :blk .{ parent, self_node };
}
if (std.mem.eql(u8, position, "afterend")) {
// The node must have a parent node in order to use this variant.
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
// Parent cannot be Document.
if (parser.nodeType(parent) == .document) {
return error.NoModificationAllowed;
}
// Get the next sibling or null; null indicates our node is the only one.
const sibling = parser.nodeNextSibling(self_node);
break :blk .{ parent, sibling };
}
// Thrown if:
// * position is not one of the four listed values.
// * The input is XML that is not well-formed.
return error.Syntax;
};
while (parser.nodeListItem(children, 0)) |child| {
_ = try parser.nodeInsertBefore(target_node, child, prev_node);
}
}
// The closest() method of the Element interface traverses the element and its parents (heading toward the document root) until it finds a node that matches the specified CSS selector.
// Returns the closest ancestor Element or itself, which matches the selectors. If there are no such element, null.
pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element {
@@ -234,7 +319,7 @@ pub const Element = struct {
}
return parser.nodeToElement(current.node);
}
current = try current.parent() orelse return null;
current = current.parent() orelse return null;
}
}
@@ -350,28 +435,18 @@ pub const Element = struct {
return try parser.elementRemoveAttributeNode(self, attr);
}
pub fn _getElementsByTagName(
self: *parser.Element,
tag_name: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(
page.arena,
pub fn _getElementsByTagName(self: *parser.Element, tag_name: js.String) !collection.HTMLCollection {
return collection.HTMLCollectionByTagName(
parser.elementToNode(self),
tag_name,
tag_name.string,
.{ .include_root = false },
);
}
pub fn _getElementsByClassName(
self: *parser.Element,
classNames: []const u8,
page: *Page,
) !collection.HTMLCollection {
pub fn _getElementsByClassName(self: *parser.Element, class_names: js.String) !collection.HTMLCollection {
return try collection.HTMLCollectionByClassName(
page.arena,
parser.elementToNode(self),
classNames,
class_names.string,
.{ .include_root = false },
);
}
@@ -407,13 +482,13 @@ pub const Element = struct {
// NonDocumentTypeChildNode
// https://dom.spec.whatwg.org/#interface-nondocumenttypechildnode
pub fn get_previousElementSibling(self: *parser.Element) !?Union {
const res = try parser.nodePreviousElementSibling(parser.elementToNode(self));
const res = parser.nodePreviousElementSibling(parser.elementToNode(self));
if (res == null) return null;
return try toInterface(res.?);
}
pub fn get_nextElementSibling(self: *parser.Element) !?Union {
const res = try parser.nodeNextElementSibling(parser.elementToNode(self));
const res = parser.nodeNextElementSibling(parser.elementToNode(self));
if (res == null) return null;
return try toInterface(res.?);
}
@@ -426,7 +501,7 @@ pub const Element = struct {
while (true) {
next = try walker.get_next(root, next) orelse return null;
// ignore non-element nodes.
if (try parser.nodeType(next.?) != .element) {
if (parser.nodeType(next.?) != .element) {
continue;
}
const e = parser.nodeToElement(next.?);
@@ -474,7 +549,7 @@ pub const Element = struct {
// Returns a 0 DOMRect object if the element is eventually detached from the main window
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect {
// Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes.
if (!try page.isNodeAttached(parser.elementToNode(self))) {
if (!page.isNodeAttached(parser.elementToNode(self))) {
return DOMRect{
.x = 0,
.y = 0,
@@ -493,7 +568,7 @@ pub const Element = struct {
// We do not render so it only always return the element's bounding rect.
// Returns an empty array if the element is eventually detached from the main window
pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect {
if (!try page.isNodeAttached(parser.elementToNode(self))) {
if (!page.isNodeAttached(parser.elementToNode(self))) {
return &.{};
}
const heap_ptr = try page.call_arena.create(DOMRect);
@@ -524,6 +599,8 @@ pub const Element = struct {
contentVisibilityAuto: bool,
opacityProperty: bool,
visibilityProperty: bool,
checkVisibilityCSS: bool,
checkOpacity: bool,
};
pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool {
@@ -549,7 +626,7 @@ pub const Element = struct {
}
// Not sure what to do if there is no owner document
const doc = try parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument;
const doc = parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument;
const fragment = try parser.documentCreateDocumentFragment(doc);
const sr = try page.arena.create(ShadowRoot);
sr.* = .{
@@ -583,7 +660,7 @@ pub const Element = struct {
return sr;
}
pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation {
pub fn _animate(self: *parser.Element, effect: js.Object, opts: js.Object) !Animation {
_ = self;
_ = opts;
return Animation.constructor(effect, null);
@@ -595,7 +672,7 @@ pub const Element = struct {
// 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;
const parent = parser.nodeParentNode(as_node) orelse return;
_ = try Node._removeChild(parent, as_node);
}
};

View File

@@ -17,7 +17,6 @@
// 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;
@@ -48,7 +47,7 @@ pub const EventTarget = struct {
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
// libdom assumes that all event targets are libdom nodes. They are not.
switch (try parser.eventTargetInternalType(et)) {
switch (parser.eventTargetInternalType(et)) {
.libdom_node => {
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
},
@@ -101,6 +100,9 @@ pub const EventTarget = struct {
page: *Page,
) !void {
_ = try EventHandler.register(page.arena, self, typ, listener, opts);
if (std.mem.eql(u8, typ, "slotchange")) {
try page.registerSlotChangeMonitor();
}
}
const RemoveEventListenerOpts = union(enum) {
@@ -148,8 +150,15 @@ pub const EventTarget = struct {
);
}
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool {
return try parser.eventTargetDispatchEvent(self, event);
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event, page: *Page) !bool {
const res = try parser.eventTargetDispatchEvent(self, event);
if (!parser.eventBubbles(event) or parser.eventIsStopped(event)) {
return res;
}
try page.window.dispatchForDocumentTarget(event);
return true;
}
};

View File

@@ -23,7 +23,6 @@ const parser = @import("../netsurf.zig");
const Element = @import("element.zig").Element;
const Union = @import("element.zig").Union;
const JsThis = @import("../env.zig").JsThis;
const Walker = @import("walker.zig").Walker;
const Matcher = union(enum) {
@@ -52,13 +51,13 @@ pub const MatchByTagName = struct {
tag: []const u8,
is_wildcard: bool,
fn init(arena: Allocator, tag_name: []const u8) !MatchByTagName {
fn init(tag_name: []const u8) MatchByTagName {
if (std.mem.eql(u8, tag_name, "*")) {
return .{ .tag = "*", .is_wildcard = true };
}
return .{
.tag = try arena.dupe(u8, tag_name),
.tag = tag_name,
.is_wildcard = false,
};
}
@@ -69,15 +68,14 @@ pub const MatchByTagName = struct {
};
pub fn HTMLCollectionByTagName(
arena: Allocator,
root: ?*parser.Node,
tag_name: []const u8,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
) HTMLCollection {
return .{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) },
.matcher = .{ .matchByTagName = MatchByTagName.init(tag_name) },
.mutable = opts.mutable,
.include_root = opts.include_root,
};
@@ -86,9 +84,9 @@ pub fn HTMLCollectionByTagName(
pub const MatchByClassName = struct {
class_names: []const u8,
fn init(arena: Allocator, class_names: []const u8) !MatchByClassName {
fn init(class_names: []const u8) !MatchByClassName {
return .{
.class_names = try arena.dupe(u8, class_names),
.class_names = class_names,
};
}
@@ -107,15 +105,14 @@ pub const MatchByClassName = struct {
};
pub fn HTMLCollectionByClassName(
arena: Allocator,
root: ?*parser.Node,
classNames: []const u8,
class_names: []const u8,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) },
.matcher = .{ .matchByClassName = try MatchByClassName.init(class_names) },
.mutable = opts.mutable,
.include_root = opts.include_root,
};
@@ -124,10 +121,8 @@ pub fn HTMLCollectionByClassName(
pub const MatchByName = struct {
name: []const u8,
fn init(arena: Allocator, name: []const u8) !MatchByName {
return .{
.name = try arena.dupe(u8, name),
};
fn init(name: []const u8) !MatchByName {
return .{ .name = name };
}
pub fn match(self: MatchByName, node: *parser.Node) !bool {
@@ -138,7 +133,6 @@ pub const MatchByName = struct {
};
pub fn HTMLCollectionByName(
arena: Allocator,
root: ?*parser.Node,
name: []const u8,
opts: Opts,
@@ -146,7 +140,7 @@ pub fn HTMLCollectionByName(
return HTMLCollection{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByName = try MatchByName.init(arena, name) },
.matcher = .{ .matchByName = try MatchByName.init(name) },
.mutable = opts.mutable,
.include_root = opts.include_root,
};
@@ -203,8 +197,8 @@ pub fn HTMLCollectionChildren(
};
}
pub fn HTMLCollectionEmpty() !HTMLCollection {
return HTMLCollection{
pub fn HTMLCollectionEmpty() HTMLCollection {
return .{
.root = null,
.walker = .{ .walkerNone = .{} },
.matcher = .{ .matchFalse = .{} },
@@ -226,14 +220,11 @@ pub const MatchByLinks = struct {
}
};
pub fn HTMLCollectionByLinks(
root: ?*parser.Node,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
pub fn HTMLCollectionByLinks(root: ?*parser.Node, opts: Opts) HTMLCollection {
return .{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByLinks = MatchByLinks{} },
.matcher = .{ .matchByLinks = .{} },
.mutable = opts.mutable,
.include_root = opts.include_root,
};
@@ -252,14 +243,11 @@ pub const MatchByAnchors = struct {
}
};
pub fn HTMLCollectionByAnchors(
root: ?*parser.Node,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
pub fn HTMLCollectionByAnchors(root: ?*parser.Node, opts: Opts) HTMLCollection {
return .{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByAnchors = MatchByAnchors{} },
.matcher = .{ .matchByAnchors = .{} },
.mutable = opts.mutable,
.include_root = opts.include_root,
};
@@ -298,7 +286,7 @@ const Opts = struct {
// 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.
// dom_html_collection expects a comparison function callback as argument.
// But we wanted a dynamically comparison here, according to the match tagname.
pub const HTMLCollection = struct {
matcher: Matcher,
@@ -344,7 +332,7 @@ pub const HTMLCollection = struct {
var node = try self.start() orelse return 0;
while (true) {
if (try parser.nodeType(node) == .element) {
if (parser.nodeType(node) == .element) {
if (try self.matcher.match(node)) {
len += 1;
}
@@ -371,7 +359,7 @@ pub const HTMLCollection = struct {
}
while (true) {
if (try parser.nodeType(node) == .element) {
if (parser.nodeType(node) == .element) {
if (try self.matcher.match(node)) {
// check if we found the searched element.
if (i == index) {
@@ -405,7 +393,7 @@ pub const HTMLCollection = struct {
var node = try self.start() orelse return null;
while (true) {
if (try parser.nodeType(node) == .element) {
if (parser.nodeType(node) == .element) {
if (try self.matcher.match(node)) {
const elem = @as(*parser.Element, @ptrCast(node));
@@ -440,24 +428,23 @@ pub const HTMLCollection = struct {
return null;
}
pub fn postAttach(self: *HTMLCollection, js_this: JsThis) !void {
const len = try self.get_length();
for (0..len) |i| {
const node = try self.item(@intCast(i)) orelse unreachable;
const e = @as(*parser.Element, @ptrCast(node));
const as_interface = try Element.toInterface(e);
try js_this.setIndex(@intCast(i), as_interface, .{});
pub fn indexed_get(self: *HTMLCollection, index: u32, has_value: *bool) !?Union {
return (try _item(self, index)) orelse {
has_value.* = false;
return undefined;
};
}
if (try item_name(e)) |name| {
// Even though an entry might have an empty id, the spec says
// that namedItem("") should always return null
if (name.len > 0) {
// Named fields should not be enumerable (it is defined with
// the LegacyUnenumerableNamedProperties flag.)
try js_this.set(name, as_interface, .{ .DONT_ENUM = true });
}
}
pub fn named_get(self: *const HTMLCollection, name: []const u8, has_value: *bool) !?Union {
// Even though an entry might have an empty id, the spec says
// that namedItem("") should always return null
if (name.len == 0) {
return null;
}
return (try _namedItem(self, name)) orelse {
has_value.* = false;
return undefined;
};
}
};

View File

@@ -1,186 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const Env = @import("../env.zig").Env;
const Element = @import("element.zig").Element;
pub const Interfaces = .{
IntersectionObserver,
IntersectionObserverEntry,
};
// This is supposed to listen to change between the root and observation targets.
// However, our rendered stores everything as 1 pixel sized boxes in a long row that never changes.
// As such, there are no changes to intersections between the root and any target.
// Instead we keep a list of all entries that are being observed.
// The callback is called with all entries everytime a new entry is added(observed).
// Potentially we should also call the callback at a regular interval.
// The returned Entries are phony, they always indicate full intersection.
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
pub const IntersectionObserver = struct {
page: *Page,
callback: Env.Function,
options: IntersectionObserverOptions,
observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry),
// new IntersectionObserver(callback)
// new IntersectionObserver(callback, options) [not supported yet]
pub fn constructor(callback: Env.Function, options_: ?IntersectionObserverOptions, page: *Page) !IntersectionObserver {
var options = IntersectionObserverOptions{
.root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
.rootMargin = "0px 0px 0px 0px",
.threshold = .{ .single = 0.0 },
};
if (options_) |*o| {
if (o.root) |root| {
options.root = root;
} // Other properties are not used due to the way we render
}
return .{
.page = page,
.callback = callback,
.options = options,
.observed_entries = .{},
};
}
pub fn _disconnect(self: *IntersectionObserver) !void {
self.observed_entries = .{}; // We don't free as it is on an arena
}
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element) !void {
for (self.observed_entries.items) |*observer| {
if (observer.target == target_element) {
return; // Already observed
}
}
try self.observed_entries.append(self.page.arena, .{
.page = self.page,
.target = target_element,
.options = &self.options,
});
var result: Env.Function.Result = undefined;
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
log.debug(.user_script, "callback error", .{
.err = result.exception,
.stack = result.stack,
.source = "intersection observer",
});
};
}
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
for (self.observed_entries.items, 0..) |*observer, index| {
if (observer.target == target) {
_ = self.observed_entries.swapRemove(index);
break;
}
}
}
pub fn _takeRecords(self: *IntersectionObserver) []IntersectionObserverEntry {
return self.observed_entries.items;
}
};
const IntersectionObserverOptions = struct {
root: ?*parser.Node, // Element or Document
rootMargin: ?[]const u8,
threshold: ?Threshold,
const Threshold = union(enum) {
single: f32,
list: []const f32,
};
};
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
pub const IntersectionObserverEntry = struct {
page: *Page,
target: *parser.Element,
options: *IntersectionObserverOptions,
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
return Element._getBoundingClientRect(self.target, self.page);
}
// Returns the ratio of the intersectionRect to the boundingClientRect.
pub fn get_intersectionRatio(_: *const IntersectionObserverEntry) f32 {
return 1.0;
}
// Returns a DOMRectReadOnly representing the target's visible area.
pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
return Element._getBoundingClientRect(self.target, self.page);
}
// A Boolean value which is true if the target element intersects with the
// intersection observer's root. If this is true, then, the
// IntersectionObserverEntry describes a transition into a state of
// intersection; if it's false, then you know the transition is from
// intersecting to not-intersecting.
pub fn get_isIntersecting(_: *const IntersectionObserverEntry) bool {
return true;
}
// Returns a DOMRectReadOnly for the intersection observer's root.
pub fn get_rootBounds(self: *const IntersectionObserverEntry) !Element.DOMRect {
const root = self.options.root.?;
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
return self.page.renderer.boundingRect();
}
const root_type = try parser.nodeType(root);
var element: *parser.Element = undefined;
switch (root_type) {
.element => element = parser.nodeToElement(root),
.document => {
const doc = parser.nodeToDocument(root);
element = (try parser.documentGetDocumentElement(doc)).?;
},
else => return error.InvalidState,
}
return Element._getBoundingClientRect(element, self.page);
}
// The Element whose intersection with the root changed.
pub fn get_target(self: *const IntersectionObserverEntry) *parser.Element {
return self.target;
}
// TODO: pub fn get_time(self: *const IntersectionObserverEntry)
};
const testing = @import("../../testing.zig");
test "Browser: DOM.IntersectionObserver" {
try testing.htmlRunner("dom/intersection_observer.html");
}

View File

@@ -17,13 +17,12 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const Env = @import("../env.zig").Env;
const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = .{
@@ -36,21 +35,21 @@ 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,
connected: bool,
cbk: js.Function,
scheduled: bool,
observers: std.ArrayListUnmanaged(*Observer),
// List of records which were observed. When the call scope ends, we need to
// execute our callback with it.
observed: std.ArrayListUnmanaged(MutationRecord),
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
pub fn constructor(cbk: js.Function, page: *Page) !MutationObserver {
return .{
.cbk = cbk,
.page = page,
.observed = .{},
.connected = true,
.scheduled = false,
.observers = .empty,
};
}
@@ -69,15 +68,17 @@ pub const MutationObserver = struct {
.event_node = .{ .id = self.cbk.id, .func = Observer.handle },
};
try self.observers.append(arena, observer);
// register node's events
if (options.childList or options.subtree) {
_ = try parser.eventTargetAddEventListener(
observer.dom_node_inserted_listener = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
"DOMNodeInserted",
&observer.event_node,
false,
);
_ = try parser.eventTargetAddEventListener(
observer.dom_node_removed_listener = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
"DOMNodeRemoved",
&observer.event_node,
@@ -85,7 +86,7 @@ pub const MutationObserver = struct {
);
}
if (options.attr()) {
_ = try parser.eventTargetAddEventListener(
observer.dom_node_attribute_modified_listener = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
"DOMAttrModified",
&observer.event_node,
@@ -93,7 +94,7 @@ pub const MutationObserver = struct {
);
}
if (options.cdata()) {
_ = try parser.eventTargetAddEventListener(
observer.dom_cdata_modified_listener = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
"DOMCharacterDataModified",
&observer.event_node,
@@ -101,7 +102,7 @@ pub const MutationObserver = struct {
);
}
if (options.subtree) {
_ = try parser.eventTargetAddEventListener(
observer.dom_subtree_modified_listener = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
"DOMSubtreeModified",
&observer.event_node,
@@ -112,10 +113,6 @@ pub const MutationObserver = struct {
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;
@@ -125,8 +122,8 @@ pub const MutationObserver = struct {
defer self.observed.clearRetainingCapacity();
var result: Env.Function.Result = undefined;
self.cbk.tryCall(void, .{records}, &result) catch {
var result: js.Function.Result = undefined;
self.cbk.tryCallWithThis(void, self, .{records}, &result) catch {
log.debug(.user_script, "callback error", .{
.err = result.exception,
.stack = result.stack,
@@ -136,9 +133,55 @@ pub const MutationObserver = struct {
return null;
}
// TODO
pub fn _disconnect(self: *MutationObserver) !void {
self.connected = false;
for (self.observers.items) |observer| {
const event_target = parser.toEventTarget(parser.Node, observer.node);
if (observer.dom_node_inserted_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMNodeInserted",
listener,
false,
);
}
if (observer.dom_node_removed_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMNodeRemoved",
listener,
false,
);
}
if (observer.dom_node_attribute_modified_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMAttrModified",
listener,
false,
);
}
if (observer.dom_cdata_modified_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMCharacterDataModified",
listener,
false,
);
}
if (observer.dom_subtree_modified_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMSubtreeModified",
listener,
false,
);
}
}
self.observers.clearRetainingCapacity();
}
// TODO
@@ -223,6 +266,12 @@ const Observer = struct {
event_node: parser.EventNode,
dom_node_inserted_listener: ?*parser.EventListener = null,
dom_node_removed_listener: ?*parser.EventListener = null,
dom_node_attribute_modified_listener: ?*parser.EventListener = null,
dom_cdata_modified_listener: ?*parser.EventListener = null,
dom_subtree_modified_listener: ?*parser.EventListener = null,
fn appliesTo(
self: *const Observer,
target: *parser.Node,
@@ -278,13 +327,13 @@ const Observer = struct {
var mutation_observer = self.mutation_observer;
const node = blk: {
const event_target = try parser.eventTarget(event) orelse return;
const event_target = parser.eventTarget(event) orelse return;
break :blk parser.eventTargetToNode(event_target);
};
const mutation_event = parser.eventToMutationEvent(event);
const event_type = blk: {
const t = try parser.eventType(event);
const t = parser.eventType(event);
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
};
@@ -302,12 +351,12 @@ const Observer = struct {
.DOMAttrModified => {
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
if (self.options.attributeOldValue) {
record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
record.old_value = parser.mutationEventPrevValue(mutation_event);
}
},
.DOMCharacterDataModified => {
if (self.options.characterDataOldValue) {
record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
record.old_value = parser.mutationEventPrevValue(mutation_event);
}
},
.DOMNodeInserted => {

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const generate = @import("../../runtime/generate.zig");
const generate = @import("../js/generate.zig");
const Page = @import("../page.zig").Page;
const EventTarget = @import("event_target.zig").EventTarget;
@@ -67,7 +67,7 @@ pub const Node = struct {
pub const subtype = .node;
pub fn toInterface(node: *parser.Node) !Union {
return switch (try parser.nodeType(node)) {
return switch (parser.nodeType(node)) {
.element => try Element.toInterfaceT(
Union,
@as(*parser.Element, @ptrCast(node)),
@@ -124,7 +124,7 @@ pub const Node = struct {
}
pub fn get_firstChild(self: *parser.Node) !?Union {
const res = try parser.nodeFirstChild(self);
const res = parser.nodeFirstChild(self);
if (res == null) {
return null;
}
@@ -132,7 +132,7 @@ pub const Node = struct {
}
pub fn get_lastChild(self: *parser.Node) !?Union {
const res = try parser.nodeLastChild(self);
const res = parser.nodeLastChild(self);
if (res == null) {
return null;
}
@@ -140,7 +140,7 @@ pub const Node = struct {
}
pub fn get_nextSibling(self: *parser.Node) !?Union {
const res = try parser.nodeNextSibling(self);
const res = parser.nodeNextSibling(self);
if (res == null) {
return null;
}
@@ -148,7 +148,7 @@ pub const Node = struct {
}
pub fn get_previousSibling(self: *parser.Node) !?Union {
const res = try parser.nodePreviousSibling(self);
const res = parser.nodePreviousSibling(self);
if (res == null) {
return null;
}
@@ -156,7 +156,7 @@ pub const Node = struct {
}
pub fn get_parentNode(self: *parser.Node) !?Union {
const res = try parser.nodeParentNode(self);
const res = parser.nodeParentNode(self);
if (res == null) {
return null;
}
@@ -164,7 +164,7 @@ pub const Node = struct {
}
pub fn get_parentElement(self: *parser.Node) !?ElementUnion {
const res = try parser.nodeParentElement(self);
const res = parser.nodeParentElement(self);
if (res == null) {
return null;
}
@@ -176,11 +176,11 @@ pub const Node = struct {
}
pub fn get_nodeType(self: *parser.Node) !u8 {
return @intFromEnum(try parser.nodeType(self));
return @intFromEnum(parser.nodeType(self));
}
pub fn get_ownerDocument(self: *parser.Node) !?*parser.DocumentHTML {
const res = try parser.nodeOwnerDocument(self);
const res = parser.nodeOwnerDocument(self);
if (res == null) {
return null;
}
@@ -190,12 +190,12 @@ pub const Node = struct {
pub fn get_isConnected(self: *parser.Node) !bool {
var node = self;
while (true) {
const node_type = try parser.nodeType(node);
const node_type = parser.nodeType(node);
if (node_type == .document) {
return true;
}
if (try parser.nodeParentNode(node)) |parent| {
if (parser.nodeParentNode(node)) |parent| {
// didn't find a document, but node has a parent, let's see
// if it's connected;
node = parent;
@@ -222,15 +222,15 @@ pub const Node = struct {
// Read/Write attributes
pub fn get_nodeValue(self: *parser.Node) !?[]const u8 {
return try parser.nodeValue(self);
return parser.nodeValue(self);
}
pub fn set_nodeValue(self: *parser.Node, data: []u8) !void {
try parser.nodeSetValue(self, data);
}
pub fn get_textContent(self: *parser.Node) !?[]const u8 {
return try parser.nodeTextContent(self);
pub fn get_textContent(self: *parser.Node) ?[]const u8 {
return parser.nodeTextContent(self);
}
pub fn set_textContent(self: *parser.Node, data: []u8) !void {
@@ -240,8 +240,8 @@ 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);
const self_owner = parser.nodeOwnerDocument(self);
const child_owner = 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
@@ -272,14 +272,14 @@ pub const Node = struct {
return 0;
}
const docself = try parser.nodeOwnerDocument(self) orelse blk: {
if (try parser.nodeType(self) == .document) {
const docself = parser.nodeOwnerDocument(self) orelse blk: {
if (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) {
const docother = parser.nodeOwnerDocument(other) orelse blk: {
if (parser.nodeType(other) == .document) {
break :blk @as(*parser.Document, @ptrCast(other));
}
break :blk null;
@@ -299,8 +299,8 @@ pub const Node = struct {
@intFromEnum(parser.DocumentPosition.contained_by);
}
const rootself = try parser.nodeGetRootNode(self);
const rootother = try parser.nodeGetRootNode(other);
const rootself = parser.nodeGetRootNode(self);
const rootother = parser.nodeGetRootNode(other);
if (rootself != rootother) {
return @intFromEnum(parser.DocumentPosition.disconnected) +
@intFromEnum(parser.DocumentPosition.implementation_specific) +
@@ -347,8 +347,8 @@ pub const Node = struct {
return 0;
}
pub fn _contains(self: *parser.Node, other: *parser.Node) !bool {
return try parser.nodeContains(self, other);
pub fn _contains(self: *parser.Node, other: *parser.Node) bool {
return parser.nodeContains(self, other);
}
// Returns itself or ancestor object inheriting from Node.
@@ -360,32 +360,44 @@ pub const Node = struct {
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" });
};
const composed = if (options) |opts| opts.composed else false;
const root = try parser.nodeGetRootNode(self);
if (page.getNodeState(root)) |state| {
if (state.shadow_root) |sr| {
return .{ .shadow_root = sr };
var current_root = parser.nodeGetRootNode(self);
while (true) {
const node_type = parser.nodeType(current_root);
if (node_type == .document_fragment) {
if (parser.documentFragmentGetHost(@ptrCast(current_root))) |host| {
if (page.getNodeState(host)) |state| {
if (state.shadow_root) |sr| {
if (!composed) {
return .{ .shadow_root = sr };
}
current_root = parser.nodeGetRootNode(@ptrCast(sr.host));
continue;
}
}
}
}
break;
}
return .{ .node = try Node.toInterface(root) };
return .{ .node = try Node.toInterface(current_root) };
}
pub fn _hasChildNodes(self: *parser.Node) !bool {
return try parser.nodeHasChildNodes(self);
pub fn _hasChildNodes(self: *parser.Node) bool {
return parser.nodeHasChildNodes(self);
}
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
const allocator = page.arena;
var list: NodeList = .{};
var n = try parser.nodeFirstChild(self) orelse return list;
var n = parser.nodeFirstChild(self) orelse return list;
while (true) {
try list.append(allocator, n);
n = try parser.nodeNextSibling(n) orelse return list;
n = parser.nodeNextSibling(n) orelse return list;
}
}
@@ -394,8 +406,8 @@ pub const Node = struct {
return _appendChild(self, new_node);
}
const self_owner = try parser.nodeOwnerDocument(self);
const new_node_owner = try parser.nodeOwnerDocument(new_node);
const self_owner = parser.nodeOwnerDocument(self);
const new_node_owner = 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
@@ -415,7 +427,7 @@ pub const Node = struct {
}
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
return try parser.nodeIsDefaultNamespace(self, namespace);
return parser.nodeIsDefaultNamespace(self, namespace);
}
pub fn _isEqualNode(self: *parser.Node, other: *parser.Node) !bool {
@@ -423,10 +435,10 @@ pub const Node = struct {
return try parser.nodeIsEqualNode(self, other);
}
pub fn _isSameNode(self: *parser.Node, other: *parser.Node) !bool {
pub fn _isSameNode(self: *parser.Node, other: *parser.Node) bool {
// TODO: other is not an optional parameter, but can be null.
// NOTE: there is no need to use isSameNode(); instead use the === strict equality operator
return try parser.nodeIsSameNode(self, other);
return parser.nodeIsSameNode(self, other);
}
pub fn _lookupPrefix(self: *parser.Node, namespace: ?[]const u8) !?[]const u8 {
@@ -461,7 +473,7 @@ pub const Node = struct {
// Check if the hierarchy node tree constraints are respected.
// For now, it checks only if new nodes are not self.
// TODO implements the others contraints.
// TODO implements the others constraints.
// see https://dom.spec.whatwg.org/#concept-node-tree
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
for (nodes) |n| {
@@ -482,9 +494,9 @@ pub const Node = struct {
return parser.DOMError.HierarchyRequest;
}
const doc = (try parser.nodeOwnerDocument(self)) orelse return;
const doc = (parser.nodeOwnerDocument(self)) orelse return;
if (try parser.nodeFirstChild(self)) |first| {
if (parser.nodeFirstChild(self)) |first| {
for (nodes) |node| {
_ = try parser.nodeInsertBefore(self, try node.toNode(doc), first);
}
@@ -506,7 +518,7 @@ pub const Node = struct {
return parser.DOMError.HierarchyRequest;
}
const doc = (try parser.nodeOwnerDocument(self)) orelse return;
const doc = (parser.nodeOwnerDocument(self)) orelse return;
for (nodes) |node| {
_ = try parser.nodeAppendChild(self, try node.toNode(doc));
}
@@ -525,7 +537,7 @@ pub const Node = struct {
// remove existing children
try removeChildren(self);
const doc = (try parser.nodeOwnerDocument(self)) orelse return;
const doc = (parser.nodeOwnerDocument(self)) orelse return;
// add new children
for (nodes) |node| {
_ = try parser.nodeAppendChild(self, try node.toNode(doc));
@@ -533,30 +545,30 @@ pub const Node = struct {
}
pub fn removeChildren(self: *parser.Node) !void {
if (!try parser.nodeHasChildNodes(self)) return;
if (!parser.nodeHasChildNodes(self)) return;
const children = try parser.nodeGetChildNodes(self);
const ln = try parser.nodeListLength(children);
const ln = parser.nodeListLength(children);
var i: u32 = 0;
while (i < ln) {
defer i += 1;
// we always retrieve the 0 index child on purpose: libdom nodelist
// are dynamic. So the next child to remove is always as pos 0.
const child = try parser.nodeListItem(children, 0) orelse continue;
const child = parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeRemoveChild(self, child);
}
}
pub fn before(self: *parser.Node, nodes: []const NodeOrText) !void {
const parent = try parser.nodeParentNode(self) orelse return;
const doc = (try parser.nodeOwnerDocument(parent)) orelse return;
const parent = parser.nodeParentNode(self) orelse return;
const doc = (parser.nodeOwnerDocument(parent)) orelse return;
var sibling: ?*parser.Node = self;
// have to find the first sibling that isn't in nodes
CHECK: while (sibling) |s| {
for (nodes) |n| {
if (n.is(s)) {
sibling = try parser.nodePreviousSibling(s);
sibling = parser.nodePreviousSibling(s);
continue :CHECK;
}
}
@@ -564,7 +576,7 @@ pub const Node = struct {
}
if (sibling == null) {
sibling = try parser.nodeFirstChild(parent);
sibling = parser.nodeFirstChild(parent);
}
if (sibling) |ref_node| {
@@ -578,15 +590,15 @@ pub const Node = struct {
}
pub fn after(self: *parser.Node, nodes: []const NodeOrText) !void {
const parent = try parser.nodeParentNode(self) orelse return;
const doc = (try parser.nodeOwnerDocument(parent)) orelse return;
const parent = parser.nodeParentNode(self) orelse return;
const doc = (parser.nodeOwnerDocument(parent)) orelse return;
// have to find the first sibling that isn't in nodes
var sibling = try parser.nodeNextSibling(self);
var sibling = parser.nodeNextSibling(self);
CHECK: while (sibling) |s| {
for (nodes) |n| {
if (n.is(s)) {
sibling = try parser.nodeNextSibling(s);
sibling = parser.nodeNextSibling(s);
continue :CHECK;
}
}
@@ -631,262 +643,7 @@ pub const Node = struct {
};
const testing = @import("../../testing.zig");
test "Browser.DOM.node" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
{
var err_out: ?[]const u8 = null;
try runner.exec(
\\ function trimAndReplace(str) {
\\ str = str.replace(/(\r\n|\n|\r)/gm,'');
\\ str = str.replace(/\s+/g, ' ');
\\ str = str.trim();
\\ return str;
\\ }
, "trimAndReplace", &err_out);
}
try runner.testCases(&.{
.{ "document.body.compareDocumentPosition(document.firstChild); ", "10" },
.{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", "10" },
.{ "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "20" },
.{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", "0" },
.{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", "2" },
.{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "4" },
}, .{});
try runner.testCases(&.{
.{ "document.getElementById('content').getRootNode().__proto__.constructor.name", "HTMLDocument" },
}, .{});
try runner.testCases(&.{
// for next test cases
.{ "let content = document.getElementById('content')", "undefined" },
.{ "let link = document.getElementById('link')", "undefined" },
.{ "let first_child = content.firstChild.nextSibling", "undefined" }, // nextSibling because of line return \n
.{ "let body_first_child = document.body.firstChild", "undefined" },
.{ "body_first_child.localName", "div" },
.{ "body_first_child.__proto__.constructor.name", "HTMLDivElement" },
.{ "document.getElementById('para-empty').firstChild.firstChild", "null" },
}, .{});
try runner.testCases(&.{
.{ "let last_child = content.lastChild.previousSibling", "undefined" }, // previousSibling because of line return \n
.{ "last_child.__proto__.constructor.name", "Comment" },
}, .{});
try runner.testCases(&.{
.{ "let next_sibling = link.nextSibling.nextSibling", "undefined" },
.{ "next_sibling.localName", "p" },
.{ "next_sibling.__proto__.constructor.name", "HTMLParagraphElement" },
.{ "content.nextSibling.nextSibling", "null" },
}, .{});
try runner.testCases(&.{
.{ "let prev_sibling = document.getElementById('para-empty').previousSibling.previousSibling", "undefined" },
.{ "prev_sibling.localName", "a" },
.{ "prev_sibling.__proto__.constructor.name", "HTMLAnchorElement" },
.{ "content.previousSibling", "null" },
}, .{});
try runner.testCases(&.{
.{ "let parent = document.getElementById('para').parentElement", "undefined" },
.{ "parent.localName", "div" },
.{ "parent.__proto__.constructor.name", "HTMLDivElement" },
.{ "let h = content.parentElement.parentElement", "undefined" },
.{ "h.parentElement", "null" },
.{ "h.parentNode.__proto__.constructor.name", "HTMLDocument" },
}, .{});
try runner.testCases(&.{
.{ "first_child.nodeName === 'A'", "true" },
.{ "link.firstChild.nodeName === '#text'", "true" },
.{ "last_child.nodeName === '#comment'", "true" },
.{ "document.nodeName === '#document'", "true" },
}, .{});
try runner.testCases(&.{
.{ "first_child.nodeType === 1", "true" },
.{ "link.firstChild.nodeType === 3", "true" },
.{ "last_child.nodeType === 8", "true" },
.{ "document.nodeType === 9", "true" },
}, .{});
try runner.testCases(&.{
.{ "let owner = content.ownerDocument", "undefined" },
.{ "owner.__proto__.constructor.name", "HTMLDocument" },
.{ "document.ownerDocument", "null" },
.{ "let owner2 = document.createElement('div').ownerDocument", "undefined" },
.{ "owner2.__proto__.constructor.name", "HTMLDocument" },
}, .{});
try runner.testCases(&.{
.{ "content.isConnected", "true" },
.{ "document.isConnected", "true" },
.{ "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(&.{
.{ "last_child.nodeValue === 'comment'", "true" },
.{ "link.nodeValue === null", "true" },
.{ "let text = link.firstChild", "undefined" },
.{ "text.nodeValue === 'OK'", "true" },
.{ "text.nodeValue = 'OK modified'", "OK modified" },
.{ "text.nodeValue === 'OK modified'", "true" },
.{ "link.nodeValue = 'nothing'", "nothing" },
}, .{});
try runner.testCases(&.{
.{ "text.textContent === 'OK modified'", "true" },
.{ "trimAndReplace(content.textContent) === 'OK modified And'", "true" },
.{ "text.textContent = 'OK'", "OK" },
.{ "text.textContent", "OK" },
.{ "trimAndReplace(document.getElementById('para-empty').textContent)", "" },
.{ "document.getElementById('para-empty').textContent = 'OK'", "OK" },
.{ "document.getElementById('para-empty').firstChild.nodeName === '#text'", "true" },
}, .{});
try runner.testCases(&.{
.{ "let append = document.createElement('h1')", "undefined" },
.{ "content.appendChild(append).toString()", "[object HTMLHeadingElement]" },
.{ "content.lastChild.__proto__.constructor.name", "HTMLHeadingElement" },
.{ "content.appendChild(link).toString()", "[object HTMLAnchorElement]" },
}, .{});
try runner.testCases(&.{
.{ "let clone = link.cloneNode()", "undefined" },
.{ "clone.toString()", "[object HTMLAnchorElement]" },
.{ "clone.parentNode === null", "true" },
.{ "clone.firstChild === null", "true" },
.{ "let clone_deep = link.cloneNode(true)", "undefined" },
.{ "clone_deep.firstChild.nodeName === '#text'", "true" },
}, .{});
try runner.testCases(&.{
.{ "link.contains(text)", "true" },
.{ "text.contains(link)", "false" },
}, .{});
try runner.testCases(&.{
.{ "link.hasChildNodes()", "true" },
.{ "text.hasChildNodes()", "false" },
}, .{});
try runner.testCases(&.{
.{ "link.childNodes.length", "1" },
.{ "text.childNodes.length", "0" },
}, .{});
try runner.testCases(&.{
.{ "let insertBefore = document.createElement('a')", "undefined" },
.{ "link.insertBefore(insertBefore, text) !== undefined", "true" },
.{ "link.firstChild.localName === 'a'", "true" },
.{ "let insertBefore2 = document.createElement('b')", null },
.{ "link.insertBefore(insertBefore2, null).localName", "b" },
.{ "link.childNodes[link.childNodes.length - 1].localName", "b" },
}, .{});
try runner.testCases(&.{
// TODO: does not seems to work
// .{ "link.isDefaultNamespace('')", "true" },
.{ "link.isDefaultNamespace('false')", "false" },
}, .{});
try runner.testCases(&.{
.{ "let equal1 = document.createElement('a')", "undefined" },
.{ "let equal2 = document.createElement('a')", "undefined" },
.{ "equal1.textContent = 'is equal'", "is equal" },
.{ "equal2.textContent = 'is equal'", "is equal" },
// TODO: does not seems to work
// .{ "equal1.isEqualNode(equal2)", "true" },
}, .{});
try runner.testCases(&.{
.{ "document.body.isSameNode(document.body)", "true" },
}, .{});
try runner.testCases(&.{
// TODO: no test
.{ "link.normalize()", "undefined" },
}, .{});
try runner.testCases(&.{
.{ "link.baseURI", "https://lightpanda.io/opensource-browser/" },
}, .{});
try runner.testCases(&.{
.{ "content.removeChild(append) !== undefined", "true" },
.{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" },
}, .{});
try runner.testCases(&.{
.{ "let replace = document.createElement('div')", "undefined" },
.{ "link.replaceChild(replace, insertBefore) !== undefined", "true" },
}, .{});
try runner.testCases(&.{
.{ "Node.ELEMENT_NODE", "1" },
.{ "Node.ATTRIBUTE_NODE", "2" },
.{ "Node.TEXT_NODE", "3" },
.{ "Node.CDATA_SECTION_NODE", "4" },
.{ "Node.PROCESSING_INSTRUCTION_NODE", "7" },
.{ "Node.COMMENT_NODE", "8" },
.{ "Node.DOCUMENT_NODE", "9" },
.{ "Node.DOCUMENT_TYPE_NODE", "10" },
.{ "Node.DOCUMENT_FRAGMENT_NODE", "11" },
.{ "Node.ENTITY_REFERENCE_NODE", "5" },
.{ "Node.ENTITY_NODE", "6" },
.{ "Node.NOTATION_NODE", "12" },
}, .{});
}
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" },
}, .{});
test "Browser: DOM.Node" {
try testing.htmlRunner("dom/node.html");
try testing.htmlRunner("dom/node_owner.html");
}

View File

@@ -17,8 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Node = @import("node.zig").Node;
pub const NodeFilter = struct {
@@ -43,10 +43,13 @@ pub const NodeFilter = struct {
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);
pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !VerifyResult {
const node_type = parser.nodeType(node);
// Verify that we can show this node type.
// Per the DOM spec, what_to_show filters which nodes to return, but should
// still traverse children. So we return .skip (not .reject) when the node
// type doesn't match.
if (!switch (node_type) {
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
@@ -60,7 +63,7 @@ pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !Ver
.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;
}) return .skip;
// Verify that we aren't filtering it out.
if (filter) |f| {
@@ -75,15 +78,6 @@ pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !Ver
}
const testing = @import("../../testing.zig");
test "Browser.DOM.NodeFilter" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "NodeFilter.FILTER_ACCEPT", "1" },
.{ "NodeFilter.FILTER_REJECT", "2" },
.{ "NodeFilter.FILTER_SKIP", "3" },
.{ "NodeFilter.SHOW_ALL", "4294967295" },
.{ "NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT", "129" },
}, .{});
test "Browser: DOM.NodeFilter" {
try testing.htmlRunner("dom/node_filter.html");
}

View File

@@ -18,8 +18,8 @@
const std = @import("std");
const js = @import("../js/js.zig");
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;
@@ -37,7 +37,7 @@ pub const NodeIterator = struct {
reference_node: *parser.Node,
what_to_show: u32,
filter: ?NodeIteratorOpts,
filter_func: ?Env.Function,
filter_func: ?js.Function,
pointer_before_current: bool = true,
// used to track / block recursive filters
is_in_callback: bool = false,
@@ -45,15 +45,15 @@ pub const NodeIterator = struct {
// 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 WhatToShow = js.Object;
pub const NodeIteratorOpts = union(enum) {
function: Env.Function,
object: struct { acceptNode: Env.Function },
function: js.Function,
object: struct { acceptNode: js.Function },
};
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator {
var filter_func: ?Env.Function = null;
var filter_func: ?js.Function = null;
if (filter) |f| {
filter_func = switch (f) {
.function => |func| func,
@@ -74,10 +74,10 @@ pub const NodeIterator = struct {
return .{
.root = node,
.reference_node = node,
.what_to_show = what_to_show,
.filter = filter,
.reference_node = node,
.filter_func = filter_func,
.what_to_show = what_to_show,
};
}
@@ -115,17 +115,30 @@ pub const NodeIterator = struct {
if (try self.firstChild(self.reference_node)) |child| {
self.reference_node = child;
self.pointer_before_current = false;
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);
// Try to get next sibling (including .skip/.reject nodes we need to descend into)
if (try self.nextSiblingOrSkipReject(current)) |result| {
if (result.should_descend) {
// This is a .skip/.reject node - try to find acceptable children within it
if (try self.firstChild(result.node)) |child| {
self.reference_node = child;
return try Node.toInterface(child);
}
// No acceptable children, continue looking at this node's siblings
current = result.node;
continue;
}
// This is an .accept node - return it
self.reference_node = result.node;
return try Node.toInterface(result.node);
}
current = (try parser.nodeParentNode(current)) orelse break;
current = (parser.nodeParentNode(current)) orelse break;
}
return null;
@@ -147,7 +160,7 @@ pub const NodeIterator = struct {
}
var current = self.reference_node;
while (try parser.nodePreviousSibling(current)) |previous| {
while (parser.nodePreviousSibling(current)) |previous| {
current = previous;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
@@ -189,11 +202,11 @@ pub const NodeIterator = struct {
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node);
const child_count = try parser.nodeListLength(children);
const child_count = parser.nodeListLength(children);
for (0..child_count) |i| {
const index: u32 = @intCast(i);
const child = (try parser.nodeListItem(children, index)) orelse return null;
const child = (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
@@ -206,12 +219,12 @@ pub const NodeIterator = struct {
fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node);
const child_count = try parser.nodeListLength(children);
const child_count = parser.nodeListLength(children);
var index: u32 = child_count;
while (index > 0) {
index -= 1;
const child = (try parser.nodeListItem(children, index)) orelse return null;
const child = (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
@@ -229,7 +242,7 @@ pub const NodeIterator = struct {
var current = node;
while (true) {
if (current == self.root) return null;
current = (try parser.nodeParentNode(current)) orelse return null;
current = (parser.nodeParentNode(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current,
@@ -243,7 +256,7 @@ pub const NodeIterator = struct {
var current = node;
while (true) {
current = (try parser.nodeNextSibling(current)) orelse return null;
current = (parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current,
@@ -254,6 +267,22 @@ pub const NodeIterator = struct {
return null;
}
// Get the next sibling that is either acceptable or should be descended into (skip/reject)
fn nextSiblingOrSkipReject(self: *const NodeIterator, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
var current = node;
while (true) {
current = (parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return .{ .node = current, .should_descend = false },
.skip, .reject => return .{ .node = current, .should_descend = true },
}
}
return null;
}
fn callbackStart(self: *NodeIterator) !void {
if (self.is_in_callback) {
// this is the correct DOMExeption
@@ -268,71 +297,6 @@ pub const NodeIterator = struct {
};
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",
},
}, .{});
test "Browser: DOM.NodeIterator" {
try testing.htmlRunner("dom/node_iterator.html");
}

View File

@@ -17,13 +17,12 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const JsThis = @import("../env.zig").JsThis;
const Function = @import("../env.zig").Function;
const NodeUnion = @import("node.zig").Union;
const Node = @import("node.zig").Node;
@@ -101,13 +100,20 @@ pub const NodeList = struct {
nodes: NodesArrayList = .{},
pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void {
// TODO unref all nodes
self.nodes.deinit(alloc);
pub fn deinit(self: *NodeList, allocator: Allocator) void {
self.nodes.deinit(allocator);
}
pub fn append(self: *NodeList, alloc: std.mem.Allocator, node: *parser.Node) !void {
try self.nodes.append(alloc, node);
pub fn ensureTotalCapacity(self: *NodeList, allocator: Allocator, n: usize) !void {
return self.nodes.ensureTotalCapacity(allocator, n);
}
pub fn append(self: *NodeList, allocator: Allocator, node: *parser.Node) !void {
try self.nodes.append(allocator, node);
}
pub fn appendAssumeCapacity(self: *NodeList, node: *parser.Node) void {
self.nodes.appendAssumeCapacity(node);
}
pub fn get_length(self: *const NodeList) u32 {
@@ -140,10 +146,10 @@ pub const NodeList = struct {
// };
// }
pub fn _forEach(self: *NodeList, cbk: Function) !void { // TODO handle thisArg
pub fn _forEach(self: *NodeList, cbk: js.Function) !void { // TODO handle thisArg
for (self.nodes.items, 0..) |n, i| {
const ii: u32 = @intCast(i);
var result: Function.Result = undefined;
var result: js.Function.Result = undefined;
cbk.tryCall(void, .{ n, ii, self }, &result) catch {
log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack });
};
@@ -167,7 +173,7 @@ pub const NodeList = struct {
}
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
pub fn postAttach(self: *NodeList, js_this: JsThis) !void {
pub fn postAttach(self: *NodeList, js_this: js.This) !void {
const len = self.get_length();
for (0..len) |i| {
const node = try self._item(@intCast(i)) orelse unreachable;
@@ -177,22 +183,6 @@ pub const NodeList = struct {
};
const testing = @import("../../testing.zig");
test "Browser.DOM.NodeList" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let list = document.getElementById('content').childNodes", "undefined" },
.{ "list.length", "9" },
.{ "list[0].__proto__.constructor.name", "Text" },
.{
\\ let i = 0;
\\ list.forEach(function (n, idx) {
\\ i += idx;
\\ });
\\ i;
,
"36",
},
}, .{});
test "Browser: DOM.NodeList" {
try testing.htmlRunner("dom/node_list.html");
}

View File

@@ -18,9 +18,9 @@
const std = @import("std");
const js = @import("../js/js.zig");
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;
@@ -61,7 +61,7 @@ pub const Performance = struct {
return milliTimestamp() - self.time_origin;
}
pub fn _mark(_: *Performance, name: []const u8, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
pub fn _mark(_: *Performance, name: js.String, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
const mark: PerformanceMark = try .constructor(name, _options, page);
// TODO: Should store this in an entries list
return mark;
@@ -148,14 +148,14 @@ pub const PerformanceMark = struct {
pub const prototype = *PerformanceEntry;
proto: PerformanceEntry,
detail: ?Env.JsObject,
detail: ?js.Object,
const Options = struct {
detail: ?Env.JsObject = null,
detail: ?js.Object = null,
startTime: ?f64 = null,
};
pub fn constructor(name: []const u8, _options: ?Options, page: *Page) !PerformanceMark {
pub fn constructor(name: js.String, _options: ?Options, page: *Page) !PerformanceMark {
const perf = &page.window.performance;
const options = _options orelse Options{};
@@ -166,14 +166,12 @@ pub const PerformanceMark = struct {
}
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 };
const proto = PerformanceEntry{ .name = name.string, .entry_type = .mark, .start_time = start_time };
return .{ .proto = proto, .detail = detail };
}
pub fn get_detail(self: *const PerformanceMark) ?Env.JsObject {
pub fn get_detail(self: *const PerformanceMark) ?js.Object {
return self.detail;
}
};
@@ -197,28 +195,12 @@ test "Performance: now" {
}
var after = perf._now();
while (after <= now) { // Loop untill after > now
while (after <= now) { // Loop until after > now
try testing.expectEqual(after, now);
after = perf._now();
}
}
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" },
}, .{});
test "Browser: Performance.Mark" {
try testing.htmlRunner("dom/performance.html");
}

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Env = @import("../env.zig").Env;
const js = @import("../js/js.zig");
const PerformanceEntry = @import("performance.zig").PerformanceEntry;
@@ -25,7 +25,7 @@ const PerformanceEntry = @import("performance.zig").PerformanceEntry;
pub const PerformanceObserver = struct {
pub const _supportedEntryTypes = [0][]const u8{};
pub fn constructor(cbk: Env.Function) PerformanceObserver {
pub fn constructor(cbk: js.Function) PerformanceObserver {
_ = cbk;
return .{};
}
@@ -53,11 +53,6 @@ const Options = struct {
};
const testing = @import("../../testing.zig");
test "Browser.DOM.PerformanceObserver" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "PerformanceObserver.supportedEntryTypes.length", "0" },
}, .{});
test "Browser: DOM.PerformanceObserver" {
try testing.htmlRunner("dom/performance_observer.html");
}

View File

@@ -48,7 +48,7 @@ pub const ProcessingInstruction = struct {
}
pub fn get_data(self: *parser.ProcessingInstruction) !?[]const u8 {
return try parser.nodeValue(parser.processingInstructionToNode(self));
return parser.nodeValue(parser.processingInstructionToNode(self));
}
pub fn set_data(self: *parser.ProcessingInstruction, data: []u8) !void {
@@ -58,7 +58,7 @@ pub const ProcessingInstruction = struct {
// netsurf's ProcessInstruction doesn't implement the dom_node_get_attributes
// and thus will crash if we try to call nodeIsEqualNode.
pub fn _isEqualNode(self: *parser.ProcessingInstruction, other_node: *parser.Node) !bool {
if (try parser.nodeType(other_node) != .processing_instruction) {
if (parser.nodeType(other_node) != .processing_instruction) {
return false;
}
@@ -87,30 +87,6 @@ pub const ProcessingInstruction = struct {
};
const testing = @import("../../testing.zig");
test "Browser.DOM.ProcessingInstruction" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" },
.{ "pi.target", "foo" },
.{ "pi.data", "bar" },
.{ "pi.data = 'foo'", "foo" },
.{ "pi.data", "foo" },
.{ "let pi2 = pi.cloneNode()", "undefined" },
.{ "pi2.nodeType", "7" },
}, .{});
try runner.testCases(&.{
.{ "let pi11 = document.createProcessingInstruction('target1', 'data1');", "undefined" },
.{ "let pi12 = document.createProcessingInstruction('target2', 'data2');", "undefined" },
.{ "let pi13 = document.createProcessingInstruction('target1', 'data1');", "undefined" },
.{ "pi11.isEqualNode(pi11)", "true" },
.{ "pi11.isEqualNode(pi13)", "true" },
.{ "pi11.isEqualNode(pi12)", "false" },
.{ "pi12.isEqualNode(pi13)", "false" },
.{ "pi11.isEqualNode(document)", "false" },
.{ "document.isEqualNode(pi11)", "false" },
}, .{});
test "Browser: DOM.ProcessingInstruction" {
try testing.htmlRunner("dom/processing_instruction.html");
}

View File

@@ -92,7 +92,7 @@ pub const Range = struct {
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) {
const position = compare(node, offset, self.proto.end_node, self.proto.end_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
@@ -103,7 +103,7 @@ pub const Range = struct {
};
if (position == 1) {
// if we're setting the node after the current start, the end must
// if we're setting the node after the current end, the end must
// be set too.
self.proto.end_offset = offset;
self.proto.end_node = node;
@@ -176,10 +176,10 @@ pub const Range = struct {
self.proto.end_node = node;
// Set end_offset
switch (try parser.nodeType(node)) {
switch (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| {
if (parser.nodeValue(node)) |text_data| {
self.proto.end_offset = @intCast(text_data.len);
} else {
self.proto.end_offset = 0;
@@ -188,7 +188,7 @@ pub const Range = struct {
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);
const child_count = parser.nodeListLength(child_nodes);
self.proto.end_offset = @intCast(child_count);
},
}
@@ -211,7 +211,7 @@ pub const Range = struct {
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)) {
if (parser.nodeGetRootNode(start) != 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
@@ -219,7 +219,7 @@ pub const Range = struct {
return error.WrongDocument;
}
if (try parser.nodeType(node) == .document_type) {
if (parser.nodeType(node) == .document_type) {
return error.InvalidNodeType;
}
@@ -245,8 +245,8 @@ pub const Range = struct {
}
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);
const start_root = parser.nodeGetRootNode(self.proto.start_node);
const node_root = parser.nodeGetRootNode(node);
if (start_root != node_root) {
return false;
}
@@ -299,29 +299,29 @@ fn ensureValidOffset(node: *parser.Node, offset: i32) !void {
fn nodeLength(node: *parser.Node) !usize {
switch (try isTextual(node)) {
true => return ((try parser.nodeTextContent(node)) orelse "").len,
true => return ((parser.nodeTextContent(node)) orelse "").len,
false => {
const children = try parser.nodeGetChildNodes(node);
return @intCast(try parser.nodeListLength(children));
return @intCast(parser.nodeListLength(children));
},
}
}
fn isTextual(node: *parser.Node) !bool {
return switch (try parser.nodeType(node)) {
return switch (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 parent = (parser.nodeParentNode(child)) orelse return error.InvalidNodeType;
const children = try parser.nodeGetChildNodes(parent);
const ln = try parser.nodeListLength(children);
const ln = parser.nodeListLength(children);
var i: u32 = 0;
while (i < ln) {
defer i += 1;
const c = try parser.nodeListItem(children, i) orelse continue;
const c = parser.nodeListItem(children, i) orelse continue;
if (c == child) {
return .{ parent, i };
}
@@ -363,7 +363,7 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
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| {
while (parser.nodeParentNode(child)) |parent| {
if (parent == node_a) {
// child.parentNode == node_a
break;
@@ -378,51 +378,13 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
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 if (offset_a <= child_index) -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
}, .{});
test "Browser: Range" {
try testing.htmlRunner("dom/range.html");
}

View File

@@ -16,7 +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 Env = @import("../env.zig").Env;
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig");
pub const Interfaces = .{
@@ -25,7 +25,7 @@ pub const Interfaces = .{
// WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface
pub const ResizeObserver = struct {
pub fn constructor(cbk: Env.Function) ResizeObserver {
pub fn constructor(cbk: js.Function) ResizeObserver {
_ = cbk;
return .{};
}

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const dump = @import("../dump.zig");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const js = @import(".././js/js.zig");
const Page = @import("../page.zig").Page;
const Node = @import("node.zig").Node;
const Element = @import("element.zig").Element;
@@ -34,7 +34,7 @@ pub const ShadowRoot = struct {
mode: Mode,
host: *parser.Element,
proto: *parser.DocumentFragment,
adopted_style_sheets: ?Env.JsObject = null,
adopted_style_sheets: ?js.Object = null,
pub const Mode = enum {
open,
@@ -45,17 +45,17 @@ pub const ShadowRoot = struct {
return Element.toInterface(self.host);
}
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject {
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object {
if (self.adopted_style_sheets) |obj| {
return obj;
}
const obj = try page.main_context.newArray(0).persist();
const obj = try page.js.createArray(0).persist();
self.adopted_style_sheets = obj;
return obj;
}
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: js.Object) !void {
self.adopted_style_sheets = try sheets.persist();
}
@@ -67,7 +67,7 @@ pub const ShadowRoot = struct {
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;
const doc = parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument;
try Node.removeChildren(sr_doc);
const str = str_ orelse return;
@@ -80,76 +80,22 @@ pub const ShadowRoot = struct {
// 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 html = parser.nodeFirstChild(fragment_node) orelse return;
const head = parser.nodeFirstChild(html) orelse return;
const body = parser.nodeNextSibling(head) orelse return;
const children = try parser.nodeGetChildNodes(body);
const ln = try parser.nodeListLength(children);
const ln = 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;
const child = 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" },
}, .{});
test "Browser: DOM.ShadowRoot" {
try testing.htmlRunner("dom/shadow_root.html");
}

View File

@@ -56,31 +56,7 @@ pub const Text = struct {
}
};
// Tests
// -----
const testing = @import("../../testing.zig");
test "Browser.DOM.Text" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let t = new Text('foo')", "undefined" },
.{ "t.data", "foo" },
.{ "let emptyt = new Text()", "undefined" },
.{ "emptyt.data", "" },
}, .{});
try runner.testCases(&.{
.{ "let text = document.getElementById('link').firstChild", "undefined" },
.{ "text.wholeText === 'OK'", "true" },
}, .{});
try runner.testCases(&.{
.{ "text.data = 'OK modified'", "OK modified" },
.{ "let split = text.splitText('OK'.length)", "undefined" },
.{ "split.data === ' modified'", "true" },
.{ "text.data === 'OK'", "true" },
}, .{});
test "Browser: DOM.Text" {
try testing.htmlRunner("dom/text.html");
}

View File

@@ -18,12 +18,11 @@
const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const iterator = @import("../iterator/iterator.zig");
const Function = @import("../env.zig").Function;
const JsObject = @import("../env.zig").JsObject;
const DOMException = @import("exceptions.zig").DOMException;
pub const Interfaces = .{
@@ -137,10 +136,10 @@ pub const DOMTokenList = struct {
}
// TODO handle thisArg
pub fn _forEach(self: *parser.TokenList, cbk: Function, this_arg: JsObject) !void {
pub fn _forEach(self: *parser.TokenList, cbk: js.Function, this_arg: js.Object) !void {
var entries = _entries(self);
while (try entries._next()) |entry| {
var result: Function.Result = undefined;
var result: js.Function.Result = undefined;
cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
log.debug(.user_script, "callback error", .{
.err = result.exception,
@@ -169,77 +168,7 @@ pub const Iterator = struct {
}
};
// Tests
// -----
const testing = @import("../../testing.zig");
test "Browser.DOM.TokenList" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let gs = document.getElementById('para-empty')", "undefined" },
.{ "let cl = gs.classList", "undefined" },
.{ "gs.className", "ok empty" },
.{ "cl.value", "ok empty" },
.{ "cl.length", "2" },
.{ "gs.className = 'foo bar baz'", "foo bar baz" },
.{ "gs.className", "foo bar baz" },
.{ "cl.length", "3" },
.{ "gs.className = 'ok empty'", "ok empty" },
.{ "cl.length", "2" },
}, .{});
try runner.testCases(&.{
.{ "let cl2 = gs.classList", "undefined" },
.{ "cl2.length", "2" },
.{ "cl2.item(0)", "ok" },
.{ "cl2.item(1)", "empty" },
.{ "cl2.contains('ok')", "true" },
.{ "cl2.contains('nok')", "false" },
.{ "cl2.add('foo', 'bar', 'baz')", "undefined" },
.{ "cl2.length", "5" },
.{ "cl2.remove('foo', 'bar', 'baz')", "undefined" },
.{ "cl2.length", "2" },
}, .{});
try runner.testCases(&.{
.{ "let cl3 = gs.classList", "undefined" },
.{ "cl3.toggle('ok')", "false" },
.{ "cl3.toggle('ok')", "true" },
.{ "cl3.length", "2" },
}, .{});
try runner.testCases(&.{
.{ "let cl4 = gs.classList", "undefined" },
.{ "cl4.replace('ok', 'nok')", "true" },
.{ "cl4.value", "empty nok" },
.{ "cl4.replace('nok', 'ok')", "true" },
.{ "cl4.value", "empty ok" },
}, .{});
try runner.testCases(&.{
.{ "let cl5 = gs.classList", "undefined" },
.{ "let keys = [...cl5.keys()]", "undefined" },
.{ "keys.length", "2" },
.{ "keys[0]", "0" },
.{ "keys[1]", "1" },
.{ "let values = [...cl5.values()]", "undefined" },
.{ "values.length", "2" },
.{ "values[0]", "empty" },
.{ "values[1]", "ok" },
.{ "let entries = [...cl5.entries()]", "undefined" },
.{ "entries.length", "2" },
.{ "entries[0]", "0,empty" },
.{ "entries[1]", "1,ok" },
}, .{});
try runner.testCases(&.{
.{ "let cl6 = gs.classList", "undefined" },
.{ "cl6.value = 'a b ccc'", "a b ccc" },
.{ "cl6.value", "a b ccc" },
.{ "cl6.toString()", "a b ccc" },
}, .{});
test "Browser: DOM.TokenList" {
try testing.htmlRunner("dom/token_list.html");
}

View File

@@ -17,11 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig");
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;
@@ -31,20 +30,20 @@ pub const TreeWalker = struct {
current_node: *parser.Node,
what_to_show: u32,
filter: ?TreeWalkerOpts,
filter_func: ?Env.Function,
filter_func: ?js.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 WhatToShow = js.Object;
pub const TreeWalkerOpts = union(enum) {
function: Env.Function,
object: struct { acceptNode: Env.Function },
function: js.Function,
object: struct { acceptNode: js.Function },
};
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker {
var filter_func: ?Env.Function = null;
var filter_func: ?js.Function = null;
if (filter) |f| {
filter_func = switch (f) {
@@ -95,11 +94,11 @@ pub const TreeWalker = struct {
fn firstChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node);
const child_count = try parser.nodeListLength(children);
const child_count = parser.nodeListLength(children);
for (0..child_count) |i| {
const index: u32 = @intCast(i);
const child = (try parser.nodeListItem(children, index)) orelse return null;
const child = (parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
.accept => return child,
@@ -113,12 +112,12 @@ pub const TreeWalker = struct {
fn lastChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node);
const child_count = try parser.nodeListLength(children);
const child_count = parser.nodeListLength(children);
var index: u32 = child_count;
while (index > 0) {
index -= 1;
const child = (try parser.nodeListItem(children, index)) orelse return null;
const child = (parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
.accept => return child,
@@ -134,7 +133,7 @@ pub const TreeWalker = struct {
var current = node;
while (true) {
current = (try parser.nodeNextSibling(current)) orelse return null;
current = (parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current,
@@ -145,11 +144,28 @@ pub const TreeWalker = struct {
return null;
}
// Get the next sibling that is either acceptable or should be descended into (skip)
fn nextSiblingOrSkip(self: *const TreeWalker, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
var current = node;
while (true) {
current = (parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return .{ .node = current, .should_descend = false },
.skip => return .{ .node = current, .should_descend = true },
.reject => continue,
}
}
return null;
}
fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
var current = node;
while (true) {
current = (try parser.nodePreviousSibling(current)) orelse return null;
current = (parser.nodePreviousSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current,
@@ -166,7 +182,7 @@ pub const TreeWalker = struct {
var current = node;
while (true) {
if (current == self.root) return null;
current = (try parser.nodeParentNode(current)) orelse return null;
current = (parser.nodeParentNode(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current,
@@ -194,19 +210,36 @@ pub const TreeWalker = struct {
}
pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
if (try self.firstChild(self.current_node)) |child| {
var current = self.current_node;
// First, try to go to first child of current node
if (try self.firstChild(current)) |child| {
self.current_node = child;
return try Node.toInterface(child);
}
var current = self.current_node;
// No acceptable children, move to next node in tree
while (current != self.root) {
if (try self.nextSibling(current)) |sibling| {
self.current_node = sibling;
return try Node.toInterface(sibling);
const result = try self.nextSiblingOrSkip(current) orelse {
// No next sibling, go up to parent and continue
// or, if there is no parent, we're done
current = (parser.nodeParentNode(current)) orelse break;
continue;
};
if (!result.should_descend) {
// This is an .accept node - return it
self.current_node = result.node;
return try Node.toInterface(result.node);
}
current = (try parser.nodeParentNode(current)) orelse break;
// This is a .skip node - try to find acceptable children within it
if (try self.firstChild(result.node)) |child| {
self.current_node = child;
return try Node.toInterface(child);
}
// No acceptable children, continue looking at this node's siblings
current = result.node;
}
return null;
@@ -234,7 +267,7 @@ pub const TreeWalker = struct {
if (self.current_node == self.root) return null;
var current = self.current_node;
while (try parser.nodePreviousSibling(current)) |previous| {
while (parser.nodePreviousSibling(current)) |previous| {
current = previous;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {

View File

@@ -44,39 +44,39 @@ pub const WalkerDepthFirst = struct {
var n = cur orelse root;
// TODO deinit next
if (try parser.nodeFirstChild(n)) |next| {
if (parser.nodeFirstChild(n)) |next| {
return next;
}
// TODO deinit next
if (try parser.nodeNextSibling(n)) |next| {
if (parser.nodeNextSibling(n)) |next| {
return next;
}
// TODO deinit parent
// Back to the parent of cur.
// If cur has no parent, then the iteration is over.
var parent = try parser.nodeParentNode(n) orelse return null;
var parent = parser.nodeParentNode(n) orelse return null;
// TODO deinit lastchild
var lastchild = try parser.nodeLastChild(parent);
var lastchild = parser.nodeLastChild(parent);
while (n != root and n == lastchild) {
n = parent;
// TODO deinit parent
// Back to the prev's parent.
// If prev has no parent, then the loop must stop.
parent = try parser.nodeParentNode(n) orelse break;
parent = parser.nodeParentNode(n) orelse break;
// TODO deinit lastchild
lastchild = try parser.nodeLastChild(parent);
lastchild = parser.nodeLastChild(parent);
}
if (n == root) {
return null;
}
return try parser.nodeNextSibling(n);
return parser.nodeNextSibling(n);
}
};
@@ -84,14 +84,14 @@ pub const WalkerDepthFirst = struct {
pub const WalkerChildren = struct {
pub fn get_next(_: WalkerChildren, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
// On walk start, we return the first root's child.
if (cur == null) return try parser.nodeFirstChild(root);
if (cur == null) return parser.nodeFirstChild(root);
// If cur is root, then return null.
// This is a special case, if the root is included in the walk, we
// don't want to go further to find children.
if (root == cur.?) return null;
return try parser.nodeNextSibling(cur.?);
return parser.nodeNextSibling(cur.?);
}
};

View File

@@ -26,7 +26,13 @@ pub const Opts = struct {
// set to include element shadowroots in the dump
page: ?*const Page = null,
exclude_scripts: bool = false,
strip_mode: StripMode = .{},
pub const StripMode = struct {
js: bool = false,
ui: bool = false,
css: bool = false,
};
};
// writer must be a std.io.Writer
@@ -41,8 +47,8 @@ pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !voi
try writer.writeAll("<!DOCTYPE ");
try writer.writeAll(try parser.documentTypeGetName(doc_type));
const public_id = try parser.documentTypeGetPublicId(doc_type);
const system_id = try parser.documentTypeGetSystemId(doc_type);
const public_id = parser.documentTypeGetPublicId(doc_type);
const system_id = parser.documentTypeGetSystemId(doc_type);
if (public_id.len != 0 and system_id.len != 0) {
try writer.writeAll(" PUBLIC \"");
try writeEscapedAttributeValue(writer, public_id);
@@ -63,11 +69,11 @@ pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !voi
}
pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerror!void {
switch (try parser.nodeType(node)) {
switch (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)) {
if (try isStripped(tag_type, node, opts.strip_mode)) {
return;
}
@@ -104,7 +110,7 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerro
if (try isVoid(parser.nodeToElement(node))) return;
if (tag_type == .script) {
try writer.writeAll(try parser.nodeTextContent(node) orelse "");
try writer.writeAll(parser.nodeTextContent(node) orelse "");
} else {
// write the children
// TODO avoid recursion
@@ -117,17 +123,17 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerro
try writer.writeAll(">");
},
.text => {
const v = try parser.nodeValue(node) orelse return;
const v = parser.nodeValue(node) orelse return;
try writeEscapedTextNode(writer, v);
},
.cdata_section => {
const v = try parser.nodeValue(node) orelse return;
const v = parser.nodeValue(node) orelse return;
try writer.writeAll("<![CDATA[");
try writer.writeAll(v);
try writer.writeAll("]]>");
},
.comment => {
const v = try parser.nodeValue(node) orelse return;
const v = parser.nodeValue(node) orelse return;
try writer.writeAll("<!--");
try writer.writeAll(v);
try writer.writeAll("-->");
@@ -159,9 +165,22 @@ pub fn writeChildren(root: *parser.Node, opts: Opts, writer: *std.Io.Writer) !vo
}
}
// 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 {
fn isStripped(tag_type: parser.Tag, node: *parser.Node, strip_mode: Opts.StripMode) !bool {
if (strip_mode.js and try isJsRelated(tag_type, node)) {
return true;
}
if (strip_mode.css and try isCssRelated(tag_type, node)) {
return true;
}
if (strip_mode.ui and try isUIRelated(tag_type, node)) {
return true;
}
return false;
}
fn isJsRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
if (tag_type == .script) {
return true;
}
@@ -178,6 +197,34 @@ fn isScriptOrRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
return false;
}
fn isCssRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
if (tag_type == .style) {
return true;
}
if (tag_type == .link) {
const el = parser.nodeToElement(node);
const rel = try parser.elementGetAttribute(el, "rel") orelse return false;
return std.ascii.eqlIgnoreCase(rel, "stylesheet");
}
return false;
}
fn isUIRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
if (try isCssRelated(tag_type, node)) {
return true;
}
if (tag_type == .img or tag_type == .picture or tag_type == .video) {
return true;
}
if (tag_type == .undef) {
const name = try parser.nodeLocalName(node);
if (std.mem.eql(u8, name, "svg")) {
return true;
}
}
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 {
@@ -189,10 +236,10 @@ fn isVoid(elem: *parser.Element) !bool {
};
}
fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void {
var v = value;
while (v.len > 0) {
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', 194 }) orelse {
return writer.writeAll(v);
};
try writer.writeAll(v[0..index]);
@@ -200,13 +247,22 @@ fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
'&' => try writer.writeAll("&amp;"),
'<' => try writer.writeAll("&lt;"),
'>' => try writer.writeAll("&gt;"),
194 => {
// non breaking space
if (v.len > index + 1 and v[index + 1] == 160) {
try writer.writeAll("&nbsp;");
v = v[index + 2 ..];
continue;
}
try writer.writeByte(194);
},
else => unreachable,
}
v = v[index + 1 ..];
}
}
fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
fn writeEscapedAttributeValue(writer: *std.Io.Writer, value: []const u8) !void {
var v = value;
while (v.len > 0) {
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
@@ -226,7 +282,7 @@ fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
const testing = std.testing;
test "dump.writeHTML" {
try parser.init();
parser.init();
defer parser.deinit();
try testWriteHTML(

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const log = @import("../../log.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
// https://encoding.spec.whatwg.org/#interface-textdecoder
const TextDecoder = @This();
@@ -37,6 +37,7 @@ const Options = struct {
fatal: bool,
ignore_bom: bool,
stream: std.ArrayList(u8),
pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
if (label_) |l| {
@@ -47,6 +48,7 @@ pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
}
const opts = opts_ orelse Options{};
return .{
.stream = .empty,
.fatal = opts.fatal,
.ignore_bom = opts.ignoreBOM,
};
@@ -64,18 +66,34 @@ 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)) {
const DecodeOptions = struct {
stream: bool = false,
};
pub fn _decode(self: *TextDecoder, str_: ?[]const u8, opts_: ?DecodeOptions, page: *Page) ![]const u8 {
var str = str_ orelse return "";
const opts: DecodeOptions = opts_ orelse .{};
if (self.stream.items.len > 0) {
try self.stream.appendSlice(page.arena, str);
str = self.stream.items;
}
if (self.fatal and !std.unicode.utf8ValidateSlice(str)) {
if (opts.stream) {
if (self.stream.items.len == 0) {
try self.stream.appendSlice(page.arena, str);
}
return "";
}
return error.InvalidUtf8;
}
if (self.ignore_bom == false and std.mem.startsWith(u8, v, &.{ 0xEF, 0xBB, 0xBF })) {
return v[3..];
self.stream.clearRetainingCapacity();
if (self.ignore_bom == false and std.mem.startsWith(u8, str, &.{ 0xEF, 0xBB, 0xBF })) {
return str[3..];
}
return v;
return str;
}
const testing = @import("../../testing.zig");

View File

@@ -18,7 +18,7 @@
const std = @import("std");
const Env = @import("../env.zig").Env;
const js = @import("../js/js.zig");
// https://encoding.spec.whatwg.org/#interface-textencoder
const TextEncoder = @This();
@@ -31,7 +31,7 @@ pub fn get_encoding(_: *const TextEncoder) []const u8 {
return "utf-8";
}
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
pub fn _encode(_: *const TextEncoder, v: []const u8) !js.TypedArray(u8) {
// Ensure the input is a valid utf-8
// It seems chrome accepts invalid utf-8 sequence.
//

View File

@@ -1,49 +0,0 @@
const std = @import("std");
const Page = @import("page.zig").Page;
const js = @import("../runtime/js.zig");
const generate = @import("../runtime/generate.zig");
const WebApis = struct {
// Wrapped like this for debug ergonomics.
// When we create our Env, a few lines down, we define it as:
// pub const Env = js.Env(*Page, WebApis);
//
// If there's a compile time error witht he Env, it's type will be readable,
// i.e.: runtime.js.Env(*browser.env.Page, browser.env.WebApis)
//
// But if we didn't wrap it in the struct, like we once didn't, and defined
// env as:
// pub const Env = js.Env(*Page, Interfaces);
//
// Because Interfaces is an anynoumous type, it doesn't have a friendly name
// and errors would be something like:
// runtime.js.Env(*browser.Page, .{...A HUNDRED TYPES...})
pub const Interfaces = generate.Tuple(.{
@import("crypto/crypto.zig").Crypto,
@import("console/console.zig").Console,
@import("css/css.zig").Interfaces,
@import("cssom/cssom.zig").Interfaces,
@import("dom/dom.zig").Interfaces,
@import("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,
@import("storage/storage.zig").Interfaces,
@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,
});
};
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

@@ -0,0 +1,57 @@
// 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");
// https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
pub const CompositionEvent = struct {
data: []const u8,
proto: parser.Event,
pub const union_make_copy = true;
pub const prototype = *parser.Event;
pub const ConstructorOptions = struct {
data: []const u8 = "",
};
pub fn constructor(event_type: []const u8, options_: ?ConstructorOptions) !CompositionEvent {
const options: ConstructorOptions = options_ orelse .{};
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .composition_event);
return .{
.proto = event.*,
.data = options.data,
};
}
pub fn get_data(self: *const CompositionEvent) []const u8 {
return self.data;
}
};
const testing = @import("../../testing.zig");
test "Browser: Events.Composition" {
try testing.htmlRunner("events/composition.html");
}

View File

@@ -16,9 +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/>.
const js = @import("../js/js.zig");
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
@@ -27,13 +28,13 @@ pub const CustomEvent = struct {
pub const union_make_copy = true;
proto: parser.Event,
detail: ?JsObject,
detail: ?js.Object,
const CustomEventInit = struct {
bubbles: bool = false,
cancelable: bool = false,
composed: bool = false,
detail: ?JsObject = null,
detail: ?js.Object = null,
};
pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent {
@@ -53,7 +54,7 @@ pub const CustomEvent = struct {
};
}
pub fn get_detail(self: *CustomEvent) ?JsObject {
pub fn get_detail(self: *CustomEvent) ?js.Object {
return self.detail;
}
@@ -64,7 +65,7 @@ pub const CustomEvent = struct {
event_type: []const u8,
can_bubble: bool,
cancelable: bool,
maybe_detail: ?JsObject,
maybe_detail: ?js.Object,
) !void {
// This function can only be called after the constructor has called.
// So we assume proto is initialized already by constructor.

View File

@@ -21,7 +21,7 @@ const Allocator = std.mem.Allocator;
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const generate = @import("../../runtime/generate.zig");
const generate = @import("../js/generate.zig");
const Page = @import("../page.zig").Page;
const Node = @import("../dom/node.zig").Node;
@@ -33,11 +33,24 @@ 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 KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
// Event interfaces
pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent };
pub const Interfaces = .{
Event,
CustomEvent,
ProgressEvent,
MouseEvent,
KeyboardEvent,
ErrorEvent,
MessageEvent,
PopStateEvent,
CompositionEvent,
};
pub const Union = generate.Union(Interfaces);
@@ -55,14 +68,17 @@ pub const Event = struct {
pub const _AT_TARGET = 2;
pub const _BUBBLING_PHASE = 3;
pub fn toInterface(evt: *parser.Event) !Union {
return switch (try parser.eventGetInternalType(evt)) {
pub fn toInterface(evt: *parser.Event) Union {
return switch (parser.eventGetInternalType(evt)) {
.event, .abort_signal, .xhr_event => .{ .Event = evt },
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
.error_event => .{ .ErrorEvent = (@as(*ErrorEvent, @fieldParentPtr("proto", evt))).* },
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
.composition_event => .{ .CompositionEvent = (@as(*CompositionEvent, @fieldParentPtr("proto", evt))).* },
};
}
@@ -74,46 +90,46 @@ pub const Event = struct {
// Getters
pub fn get_type(self: *parser.Event) ![]const u8 {
return try parser.eventType(self);
pub fn get_type(self: *parser.Event) []const u8 {
return parser.eventType(self);
}
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
const et = try parser.eventTarget(self);
const et = parser.eventTarget(self);
if (et == null) return null;
return try EventTarget.toInterface(et.?, page);
}
pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion {
const et = try parser.eventCurrentTarget(self);
const et = parser.eventCurrentTarget(self);
if (et == null) return null;
return try EventTarget.toInterface(et.?, page);
}
pub fn get_eventPhase(self: *parser.Event) !u8 {
return try parser.eventPhase(self);
pub fn get_eventPhase(self: *parser.Event) u8 {
return parser.eventPhase(self);
}
pub fn get_bubbles(self: *parser.Event) !bool {
return try parser.eventBubbles(self);
pub fn get_bubbles(self: *parser.Event) bool {
return parser.eventBubbles(self);
}
pub fn get_cancelable(self: *parser.Event) !bool {
return try parser.eventCancelable(self);
pub fn get_cancelable(self: *parser.Event) bool {
return parser.eventCancelable(self);
}
pub fn get_defaultPrevented(self: *parser.Event) !bool {
return try parser.eventDefaultPrevented(self);
pub fn get_defaultPrevented(self: *parser.Event) bool {
return parser.eventDefaultPrevented(self);
}
pub fn get_isTrusted(self: *parser.Event) !bool {
return try parser.eventIsTrusted(self);
pub fn get_isTrusted(self: *parser.Event) bool {
return parser.eventIsTrusted(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 {
pub fn get_timeStamp(self: *parser.Event) u64 {
return parser.eventTimestamp(self);
}
@@ -133,22 +149,22 @@ pub const Event = struct {
}
pub fn _stopPropagation(self: *parser.Event) !void {
return try parser.eventStopPropagation(self);
return parser.eventStopPropagation(self);
}
pub fn _stopImmediatePropagation(self: *parser.Event) !void {
return try parser.eventStopImmediatePropagation(self);
return parser.eventStopImmediatePropagation(self);
}
pub fn _preventDefault(self: *parser.Event) !void {
return try parser.eventPreventDefault(self);
return parser.eventPreventDefault(self);
}
pub fn _composedPath(self: *parser.Event, page: *Page) ![]const EventTargetUnion {
const et_ = try parser.eventTarget(self);
const et_ = parser.eventTarget(self);
const et = et_ orelse return &.{};
var node: ?*parser.Node = switch (try parser.eventTargetInternalType(et)) {
var node: ?*parser.Node = switch (parser.eventTargetInternalType(et)) {
.libdom_node => @as(*parser.Node, @ptrCast(et)),
.plain => parser.eventTargetToNode(et),
else => {
@@ -164,8 +180,8 @@ pub const Event = struct {
.node = try Node.toInterface(n),
});
node = try parser.nodeParentNode(n);
if (node == null and try parser.nodeType(n) == .document_fragment) {
node = parser.nodeParentNode(n);
if (node == null and 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
@@ -206,18 +222,17 @@ pub const Event = struct {
pub const EventHandler = struct {
once: bool,
capture: bool,
callback: Function,
callback: js.Function,
node: parser.EventNode,
listener: *parser.EventListener,
const Env = @import("../env.zig").Env;
const Function = Env.Function;
const js = @import("../js/js.zig");
pub const Listener = union(enum) {
function: Function,
object: Env.JsObject,
function: js.Function,
object: js.Object,
pub fn callback(self: Listener, target: *parser.EventTarget) !?Function {
pub fn callback(self: Listener, target: *parser.EventTarget) !?js.Function {
return switch (self) {
.function => |func| try func.withThis(target),
.object => |obj| blk: {
@@ -316,13 +331,9 @@ pub const EventHandler = struct {
}
fn handle(node: *parser.EventNode, event: *parser.Event) void {
const ievent = Event.toInterface(event) catch |err| {
log.err(.app, "toInterface error", .{ .err = err });
return;
};
const ievent = Event.toInterface(event);
const self: *EventHandler = @fieldParentPtr("node", node);
var result: Function.Result = undefined;
var result: js.Function.Result = undefined;
self.callback.tryCall(void, .{ievent}, &result) catch {
log.debug(.user_script, "callback error", .{
.err = result.exception,
@@ -332,8 +343,8 @@ pub const EventHandler = struct {
};
if (self.once) {
const target = (parser.eventTarget(event) catch return).?;
const typ = parser.eventType(event) catch return;
const target = parser.eventTarget(event).?;
const typ = parser.eventType(event);
parser.eventTargetRemoveEventListener(
target,
typ,

View File

@@ -0,0 +1,159 @@
// 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 builtin = @import("builtin");
const parser = @import("../netsurf.zig");
const Event = @import("event.zig").Event;
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
const UIEvent = Event;
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
pub const KeyboardEvent = struct {
pub const Self = parser.KeyboardEvent;
pub const prototype = *UIEvent;
pub const ConstructorOptions = struct {
key: []const u8 = "",
code: []const u8 = "",
location: parser.KeyboardEventOpts.LocationCode = .standard,
repeat: bool = false,
isComposing: bool = false,
// Currently not supported but we take as argument.
charCode: u32 = 0,
// Currently not supported but we take as argument.
keyCode: u32 = 0,
// Currently not supported but we take as argument.
which: u32 = 0,
ctrlKey: bool = false,
shiftKey: bool = false,
altKey: bool = false,
metaKey: bool = false,
};
pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*parser.KeyboardEvent {
const options: ConstructorOptions = maybe_options orelse .{};
const event = try parser.keyboardEventCreate();
parser.eventSetInternalType(@ptrCast(event), .keyboard_event);
try parser.keyboardEventInit(
event,
event_type,
.{
.key = options.key,
.code = options.code,
.location = options.location,
.repeat = options.repeat,
.is_composing = options.isComposing,
.ctrl_key = options.ctrlKey,
.shift_key = options.shiftKey,
.alt_key = options.altKey,
.meta_key = options.metaKey,
},
);
return event;
}
// Returns the modifier state for given modifier key.
pub fn _getModifierState(self: *Self, key: []const u8) bool {
// Chrome and Firefox do case-sensitive match, here we prefer the same.
if (std.mem.eql(u8, key, "Alt")) {
return get_altKey(self);
}
if (std.mem.eql(u8, key, "AltGraph")) {
return (get_altKey(self) and get_ctrlKey(self));
}
if (std.mem.eql(u8, key, "Control")) {
return get_ctrlKey(self);
}
if (std.mem.eql(u8, key, "Shift")) {
return get_shiftKey(self);
}
if (std.mem.eql(u8, key, "Meta") or std.mem.eql(u8, key, "OS")) {
return get_metaKey(self);
}
// Special case for IE.
if (comptime builtin.os.tag == .windows) {
if (std.mem.eql(u8, key, "Win")) {
return get_metaKey(self);
}
}
// getModifierState() also accepts a deprecated virtual modifier named "Accel".
// event.getModifierState("Accel") returns true when at least one of
// KeyboardEvent.ctrlKey or KeyboardEvent.metaKey is true.
//
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState#accel_virtual_modifier
if (std.mem.eql(u8, key, "Accel")) {
return (get_ctrlKey(self) or get_metaKey(self));
}
// TODO: Add support for "CapsLock", "ScrollLock".
return false;
}
// Getters.
pub fn get_altKey(self: *Self) bool {
return parser.keyboardEventKeyIsSet(self, .alt);
}
pub fn get_ctrlKey(self: *Self) bool {
return parser.keyboardEventKeyIsSet(self, .ctrl);
}
pub fn get_metaKey(self: *Self) bool {
return parser.keyboardEventKeyIsSet(self, .meta);
}
pub fn get_shiftKey(self: *Self) bool {
return parser.keyboardEventKeyIsSet(self, .shift);
}
pub fn get_isComposing(self: *Self) bool {
return self.is_composing;
}
pub fn get_location(self: *Self) u32 {
return self.location;
}
pub fn get_key(self: *Self) ![]const u8 {
return parser.keyboardEventGetKey(self);
}
pub fn get_repeat(self: *Self) bool {
return self.repeat;
}
};
const testing = @import("../../testing.zig");
test "Browser: Events.Keyboard" {
try testing.htmlRunner("events/keyboard.html");
}

View File

@@ -21,7 +21,6 @@ const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Event = @import("event.zig").Event;
const JsObject = @import("../env.zig").JsObject;
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
@@ -55,8 +54,8 @@ pub const MouseEvent = struct {
pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent {
const opts = opts_ orelse MouseEventInit{};
var mouse_event = try parser.mouseEventCreate();
try parser.eventSetInternalType(@ptrCast(&mouse_event), .mouse_event);
const mouse_event = try parser.mouseEventCreate();
parser.eventSetInternalType(@ptrCast(mouse_event), .mouse_event);
try parser.mouseEventInit(mouse_event, event_type, .{
.x = opts.clientX,
@@ -69,7 +68,7 @@ pub const MouseEvent = struct {
});
if (!std.mem.eql(u8, event_type, "click")) {
log.warn(.mouse_event, "unsupported mouse event", .{ .event = event_type });
log.warn(.browser, "unsupported mouse event", .{ .event = event_type });
}
return mouse_event;

View File

@@ -0,0 +1,225 @@
// 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 js = @import("../js/js.zig");
const log = @import("../../log.zig");
const URL = @import("../../url.zig").URL;
const Page = @import("../page.zig").Page;
const iterator = @import("../iterator/iterator.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
const Headers = @This();
// Case-Insensitive String HashMap.
// This allows us to avoid having to allocate lowercase keys all the time.
const HeaderHashMap = std.HashMapUnmanaged([]const u8, []const u8, struct {
pub fn hash(_: @This(), s: []const u8) u64 {
var buf: [64]u8 = undefined;
var hasher = std.hash.Wyhash.init(s.len);
var key = s;
while (key.len >= 64) {
const lower = std.ascii.lowerString(buf[0..], key[0..64]);
hasher.update(lower);
key = key[64..];
}
if (key.len > 0) {
const lower = std.ascii.lowerString(buf[0..key.len], key);
hasher.update(lower);
}
return hasher.final();
}
pub fn eql(_: @This(), a: []const u8, b: []const u8) bool {
return std.ascii.eqlIgnoreCase(a, b);
}
}, 80);
headers: HeaderHashMap = .empty,
// They can either be:
//
// 1. An array of string pairs.
// 2. An object with string keys to string values.
// 3. Another Headers object.
pub const HeadersInit = union(enum) {
// List of Pairs of []const u8
strings: []const [2][]const u8,
// Headers
headers: *Headers,
// Mappings
object: js.Object,
};
pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
const arena = page.arena;
var headers: HeaderHashMap = .empty;
if (_init) |init| {
switch (init) {
.strings => |kvs| {
for (kvs) |pair| {
const key = try arena.dupe(u8, pair[0]);
const value = try arena.dupe(u8, pair[1]);
try headers.put(arena, key, value);
}
},
.headers => |hdrs| {
var iter = hdrs.headers.iterator();
while (iter.next()) |entry| {
try headers.put(arena, entry.key_ptr.*, entry.value_ptr.*);
}
},
.object => |obj| {
var iter = obj.nameIterator();
while (try iter.next()) |name_value| {
const name = try name_value.toString(arena);
const value = try obj.get(name);
const value_string = try value.toString(arena);
try headers.put(arena, name, value_string);
}
},
}
}
return .{
.headers = headers,
};
}
pub fn append(self: *Headers, name: []const u8, value: []const u8, allocator: std.mem.Allocator) !void {
const key = try allocator.dupe(u8, name);
const gop = try self.headers.getOrPut(allocator, key);
if (gop.found_existing) {
// If we found it, append the value.
const new_value = try std.fmt.allocPrint(allocator, "{s}, {s}", .{ gop.value_ptr.*, value });
gop.value_ptr.* = new_value;
} else {
// Otherwise, we should just put it in.
gop.value_ptr.* = try allocator.dupe(u8, value);
}
}
pub fn _append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
const arena = page.arena;
try self.append(name, value, arena);
}
pub fn _delete(self: *Headers, name: []const u8) void {
_ = self.headers.remove(name);
}
pub const HeadersEntryIterator = struct {
slot: [2][]const u8,
iter: HeaderHashMap.Iterator,
// TODO: these SHOULD be in lexigraphical order but I'm not sure how actually
// important that is.
pub fn _next(self: *HeadersEntryIterator) ?[2][]const u8 {
if (self.iter.next()) |entry| {
self.slot[0] = entry.key_ptr.*;
self.slot[1] = entry.value_ptr.*;
return self.slot;
} else {
return null;
}
}
};
pub fn _entries(self: *const Headers) HeadersEntryIterable {
return .{
.inner = .{
.slot = undefined,
.iter = self.headers.iterator(),
},
};
}
pub fn _forEach(self: *Headers, callback_fn: js.Function, this_arg: ?js.Object) !void {
var iter = self.headers.iterator();
const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn;
while (iter.next()) |entry| {
try cb.call(void, .{ entry.key_ptr.*, entry.value_ptr.*, self });
}
}
pub fn _get(self: *const Headers, name: []const u8) ?[]const u8 {
return self.headers.get(name);
}
pub fn _has(self: *const Headers, name: []const u8) bool {
return self.headers.contains(name);
}
pub const HeadersKeyIterator = struct {
iter: HeaderHashMap.KeyIterator,
pub fn _next(self: *HeadersKeyIterator) ?[]const u8 {
if (self.iter.next()) |key| {
return key.*;
} else {
return null;
}
}
};
pub fn _keys(self: *const Headers) HeadersKeyIterable {
return .{ .inner = .{ .iter = self.headers.keyIterator() } };
}
pub fn _set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
const arena = page.arena;
const key = try arena.dupe(u8, name);
const gop = try self.headers.getOrPut(arena, key);
gop.value_ptr.* = try arena.dupe(u8, value);
}
pub const HeadersValueIterator = struct {
iter: HeaderHashMap.ValueIterator,
pub fn _next(self: *HeadersValueIterator) ?[]const u8 {
if (self.iter.next()) |value| {
return value.*;
} else {
return null;
}
}
};
pub fn _values(self: *const Headers) HeadersValueIterable {
return .{ .inner = .{ .iter = self.headers.valueIterator() } };
}
pub const HeadersKeyIterable = iterator.Iterable(HeadersKeyIterator, "HeadersKeyIterator");
pub const HeadersValueIterable = iterator.Iterable(HeadersValueIterator, "HeadersValueIterator");
pub const HeadersEntryIterable = iterator.Iterable(HeadersEntryIterator, "HeadersEntryIterator");
const testing = @import("../../testing.zig");
test "fetch: Headers" {
try testing.htmlRunner("fetch/headers.html");
}

View File

@@ -0,0 +1,283 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const URL = @import("../../url.zig").URL;
const Page = @import("../page.zig").Page;
const Response = @import("./Response.zig");
const Http = @import("../../http/Http.zig");
const ReadableStream = @import("../streams/ReadableStream.zig");
const Headers = @import("Headers.zig");
const HeadersInit = @import("Headers.zig").HeadersInit;
pub const RequestInput = union(enum) {
string: []const u8,
request: *Request,
};
pub const RequestCache = enum {
default,
@"no-store",
reload,
@"no-cache",
@"force-cache",
@"only-if-cached",
pub fn fromString(str: []const u8) ?RequestCache {
for (std.enums.values(RequestCache)) |cache| {
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
return cache;
}
} else {
return null;
}
}
pub fn toString(self: RequestCache) []const u8 {
return @tagName(self);
}
};
pub const RequestCredentials = enum {
omit,
@"same-origin",
include,
pub fn fromString(str: []const u8) ?RequestCredentials {
for (std.enums.values(RequestCredentials)) |cache| {
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
return cache;
}
} else {
return null;
}
}
pub fn toString(self: RequestCredentials) []const u8 {
return @tagName(self);
}
};
pub const RequestMode = enum {
cors,
@"no-cors",
@"same-origin",
navigate,
pub fn fromString(str: []const u8) ?RequestMode {
for (std.enums.values(RequestMode)) |cache| {
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
return cache;
}
} else {
return null;
}
}
pub fn toString(self: RequestMode) []const u8 {
return @tagName(self);
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
pub const RequestInit = struct {
body: ?[]const u8 = null,
cache: ?[]const u8 = null,
credentials: ?[]const u8 = null,
headers: ?HeadersInit = null,
integrity: ?[]const u8 = null,
method: ?[]const u8 = null,
mode: ?[]const u8 = null,
};
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
const Request = @This();
method: Http.Method,
url: [:0]const u8,
cache: RequestCache,
credentials: RequestCredentials,
// no-cors is default is not built with constructor.
mode: RequestMode = .@"no-cors",
headers: Headers,
body: ?[]const u8,
body_used: bool = false,
integrity: []const u8,
pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Request {
const arena = page.arena;
const options: RequestInit = _options orelse .{};
const url: [:0]const u8 = blk: switch (input) {
.string => |str| {
break :blk try URL.stitch(arena, str, page.url.raw, .{ .null_terminated = true });
},
.request => |req| {
break :blk try arena.dupeZ(u8, req.url);
},
};
const cache = (if (options.cache) |cache| RequestCache.fromString(cache) else null) orelse RequestCache.default;
const credentials = (if (options.credentials) |creds| RequestCredentials.fromString(creds) else null) orelse RequestCredentials.@"same-origin";
const integrity = if (options.integrity) |integ| try arena.dupe(u8, integ) else "";
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
const mode = (if (options.mode) |mode| RequestMode.fromString(mode) else null) orelse RequestMode.cors;
const method: Http.Method = blk: {
if (options.method) |given_method| {
for (std.enums.values(Http.Method)) |method| {
if (std.ascii.eqlIgnoreCase(given_method, @tagName(method))) {
break :blk method;
}
} else {
return error.TypeError;
}
} else {
break :blk Http.Method.GET;
}
};
// Can't have a body on .GET or .HEAD.
const body: ?[]const u8 = blk: {
if (method == .GET or method == .HEAD) {
break :blk null;
} else break :blk if (options.body) |body| try arena.dupe(u8, body) else null;
};
return .{
.method = method,
.url = url,
.cache = cache,
.credentials = credentials,
.mode = mode,
.headers = headers,
.body = body,
.integrity = integrity,
};
}
pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream {
if (self.body) |body| {
const stream = try ReadableStream.constructor(null, null, page);
try stream.queue.append(page.arena, .{ .string = body });
return stream;
} else return null;
}
pub fn get_bodyUsed(self: *const Request) bool {
return self.body_used;
}
pub fn get_cache(self: *const Request) RequestCache {
return self.cache;
}
pub fn get_credentials(self: *const Request) RequestCredentials {
return self.credentials;
}
pub fn get_headers(self: *Request) *Headers {
return &self.headers;
}
pub fn get_integrity(self: *const Request) []const u8 {
return self.integrity;
}
// TODO: If we ever support the Navigation API, we need isHistoryNavigation
// https://developer.mozilla.org/en-US/docs/Web/API/Request/isHistoryNavigation
pub fn get_method(self: *const Request) []const u8 {
return @tagName(self.method);
}
pub fn get_mode(self: *const Request) RequestMode {
return self.mode;
}
pub fn get_url(self: *const Request) []const u8 {
return self.url;
}
pub fn _clone(self: *Request) !Request {
// Not allowed to clone if the body was used.
if (self.body_used) {
return error.TypeError;
}
// OK to just return the same fields BECAUSE
// all of these fields are read-only and can't be modified.
return Request{
.body = self.body,
.body_used = self.body_used,
.cache = self.cache,
.credentials = self.credentials,
.headers = self.headers,
.method = self.method,
.integrity = self.integrity,
.url = self.url,
};
}
pub fn _bytes(self: *Response, page: *Page) !js.Promise {
if (self.body_used) {
return error.TypeError;
}
self.body_used = true;
return page.js.resolvePromise(self.body);
}
pub fn _json(self: *Response, page: *Page) !js.Promise {
if (self.body_used) {
return error.TypeError;
}
self.body_used = true;
if (self.body) |body| {
const p = std.json.parseFromSliceLeaky(
std.json.Value,
page.call_arena,
body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
return error.SyntaxError;
};
return page.js.resolvePromise(p);
}
return page.js.resolvePromise(null);
}
pub fn _text(self: *Response, page: *Page) !js.Promise {
if (self.body_used) {
return error.TypeError;
}
self.body_used = true;
return page.js.resolvePromise(self.body);
}
const testing = @import("../../testing.zig");
test "fetch: Request" {
try testing.htmlRunner("fetch/request.html");
}

View File

@@ -0,0 +1,209 @@
// 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 js = @import("../js/js.zig");
const log = @import("../../log.zig");
const HttpClient = @import("../../http/Client.zig");
const Http = @import("../../http/Http.zig");
const URL = @import("../../url.zig").URL;
const ReadableStream = @import("../streams/ReadableStream.zig");
const Headers = @import("Headers.zig");
const HeadersInit = @import("Headers.zig").HeadersInit;
const Mime = @import("../mime.zig").Mime;
const Page = @import("../page.zig").Page;
// https://developer.mozilla.org/en-US/docs/Web/API/Response
const Response = @This();
status: u16 = 200,
status_text: []const u8 = "",
headers: Headers,
mime: ?Mime = null,
url: []const u8 = "",
body: ?[]const u8 = null,
body_used: bool = false,
redirected: bool = false,
type: ResponseType = .basic,
const ResponseBody = union(enum) {
string: []const u8,
};
const ResponseOptions = struct {
status: u16 = 200,
statusText: ?[]const u8 = null,
headers: ?HeadersInit = null,
};
pub const ResponseType = enum {
basic,
cors,
@"error",
@"opaque",
opaqueredirect,
pub fn fromString(str: []const u8) ?ResponseType {
for (std.enums.values(ResponseType)) |cache| {
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
return cache;
}
} else {
return null;
}
}
pub fn toString(self: ResponseType) []const u8 {
return @tagName(self);
}
};
pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Page) !Response {
const arena = page.arena;
const options: ResponseOptions = _options orelse .{};
const body = blk: {
if (_input) |input| {
switch (input) {
.string => |str| {
break :blk try arena.dupe(u8, str);
},
}
} else {
break :blk null;
}
};
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
const status_text = if (options.statusText) |st| try arena.dupe(u8, st) else "";
return .{
.body = body,
.headers = headers,
.status = options.status,
.status_text = status_text,
};
}
pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
const stream = try ReadableStream.constructor(null, null, page);
if (self.body) |body| {
try stream.queue.append(page.arena, .{ .string = body });
}
return stream;
}
pub fn get_bodyUsed(self: *const Response) bool {
return self.body_used;
}
pub fn get_headers(self: *Response) *Headers {
return &self.headers;
}
pub fn get_ok(self: *const Response) bool {
return self.status >= 200 and self.status <= 299;
}
pub fn get_redirected(self: *const Response) bool {
return self.redirected;
}
pub fn get_status(self: *const Response) u16 {
return self.status;
}
pub fn get_statusText(self: *const Response) []const u8 {
return self.status_text;
}
pub fn get_type(self: *const Response) ResponseType {
return self.type;
}
pub fn get_url(self: *const Response) []const u8 {
return self.url;
}
pub fn _clone(self: *const Response) !Response {
if (self.body_used) {
return error.TypeError;
}
// OK to just return the same fields BECAUSE
// all of these fields are read-only and can't be modified.
return Response{
.body = self.body,
.body_used = self.body_used,
.mime = self.mime,
.headers = self.headers,
.redirected = self.redirected,
.status = self.status,
.url = self.url,
.type = self.type,
};
}
pub fn _bytes(self: *Response, page: *Page) !js.Promise {
if (self.body_used) {
return error.TypeError;
}
self.body_used = true;
return page.js.resolvePromise(self.body);
}
pub fn _json(self: *Response, page: *Page) !js.Promise {
if (self.body_used) {
return error.TypeError;
}
if (self.body) |body| {
self.body_used = true;
const p = std.json.parseFromSliceLeaky(
std.json.Value,
page.call_arena,
body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
return error.SyntaxError;
};
return page.js.resolvePromise(p);
}
return page.js.resolvePromise(null);
}
pub fn _text(self: *Response, page: *Page) !js.Promise {
if (self.body_used) {
return error.TypeError;
}
self.body_used = true;
return page.js.resolvePromise(self.body);
}
const testing = @import("../../testing.zig");
test "fetch: Response" {
try testing.htmlRunner("fetch/response.html");
}

243
src/browser/fetch/fetch.zig Normal file
View File

@@ -0,0 +1,243 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const Http = @import("../../http/Http.zig");
const HttpClient = @import("../../http/Client.zig");
const Mime = @import("../mime.zig").Mime;
const Headers = @import("Headers.zig");
const RequestInput = @import("Request.zig").RequestInput;
const RequestInit = @import("Request.zig").RequestInit;
const Request = @import("Request.zig");
const Response = @import("Response.zig");
pub const Interfaces = .{
@import("Headers.zig"),
@import("Headers.zig").HeadersEntryIterable,
@import("Headers.zig").HeadersKeyIterable,
@import("Headers.zig").HeadersValueIterable,
@import("Request.zig"),
@import("Response.zig"),
};
pub const FetchContext = struct {
page: *Page,
arena: std.mem.Allocator,
promise_resolver: js.PersistentPromiseResolver,
method: Http.Method,
url: []const u8,
body: std.ArrayListUnmanaged(u8) = .empty,
headers: std.ArrayListUnmanaged([]const u8) = .empty,
status: u16 = 0,
mime: ?Mime = null,
mode: Request.RequestMode,
transfer: ?*HttpClient.Transfer = null,
/// This effectively takes ownership of the FetchContext.
///
/// We just return the underlying slices used for `headers`
/// and for `body` here to avoid an allocation.
pub fn toResponse(self: *const FetchContext) !Response {
var headers: Headers = .{};
// seems to be the highest priority
const same_origin = try self.page.isSameOrigin(self.url);
// If the mode is "no-cors", we need to return this opaque/stripped Response.
// https://developer.mozilla.org/en-US/docs/Web/API/Response/type
if (!same_origin and self.mode == .@"no-cors") {
return Response{
.status = 0,
.headers = headers,
.mime = self.mime,
.body = null,
.url = self.url,
.type = .@"opaque",
};
}
// convert into Headers
for (self.headers.items) |hdr| {
var iter = std.mem.splitScalar(u8, hdr, ':');
const name = iter.next() orelse "";
const value = iter.next() orelse "";
try headers.append(name, value, self.arena);
}
const resp_type: Response.ResponseType = blk: {
if (same_origin or std.mem.startsWith(u8, self.url, "data:")) {
break :blk .basic;
}
break :blk switch (self.mode) {
.cors => .cors,
.@"same-origin", .navigate => .basic,
.@"no-cors" => unreachable,
};
};
return Response{
.status = self.status,
.headers = headers,
.mime = self.mime,
.body = self.body.items,
.url = self.url,
.type = resp_type,
};
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !js.Promise {
const arena = page.arena;
const req = try Request.constructor(input, options, page);
var headers = try page.http_client.newHeaders();
// Copy our headers into the HTTP headers.
var header_iter = req.headers.headers.iterator();
while (header_iter.next()) |entry| {
const combined = try std.fmt.allocPrintSentinel(
page.arena,
"{s}: {s}",
.{ entry.key_ptr.*, entry.value_ptr.* },
0,
);
try headers.add(combined.ptr);
}
try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers);
const resolver = try page.js.createPromiseResolver(.page);
const fetch_ctx = try arena.create(FetchContext);
fetch_ctx.* = .{
.page = page,
.arena = arena,
.promise_resolver = resolver,
.method = req.method,
.url = req.url,
.mode = req.mode,
};
try page.http_client.request(.{
.ctx = @ptrCast(fetch_ctx),
.url = req.url,
.method = req.method,
.headers = headers,
.body = req.body,
.cookie_jar = page.cookie_jar,
.resource_type = .fetch,
.start_callback = struct {
fn startCallback(transfer: *HttpClient.Transfer) !void {
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
log.debug(.fetch, "request start", .{ .method = self.method, .url = self.url, .source = "fetch" });
self.transfer = transfer;
}
}.startCallback,
.header_callback = struct {
fn headerCallback(transfer: *HttpClient.Transfer) !void {
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
const header = &transfer.response_header.?;
log.debug(.fetch, "request header", .{
.source = "fetch",
.method = self.method,
.url = self.url,
.status = header.status,
});
if (header.contentType()) |ct| {
self.mime = Mime.parse(ct) catch {
return error.MimeParsing;
};
}
if (transfer.getContentLength()) |cl| {
try self.body.ensureTotalCapacity(self.arena, cl);
}
var it = transfer.responseHeaderIterator();
while (it.next()) |hdr| {
const joined = try std.fmt.allocPrint(self.arena, "{s}: {s}", .{ hdr.name, hdr.value });
try self.headers.append(self.arena, joined);
}
self.status = header.status;
}
}.headerCallback,
.data_callback = struct {
fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
try self.body.appendSlice(self.arena, data);
}
}.dataCallback,
.done_callback = struct {
fn doneCallback(ctx: *anyopaque) !void {
const self: *FetchContext = @ptrCast(@alignCast(ctx));
self.transfer = null;
log.info(.fetch, "request complete", .{
.source = "fetch",
.method = self.method,
.url = self.url,
.status = self.status,
});
const response = try self.toResponse();
try self.promise_resolver.resolve(response);
}
}.doneCallback,
.error_callback = struct {
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *FetchContext = @ptrCast(@alignCast(ctx));
self.transfer = null;
log.err(.fetch, "error", .{
.url = self.url,
.err = err,
.source = "fetch error",
});
// We throw an Abort error when the page is getting closed so,
// in this case, we don't need to reject the promise.
if (err != error.Abort) {
self.promise_resolver.reject(@errorName(err)) catch unreachable;
}
}
}.errorCallback,
});
return resolver.promise();
}
const testing = @import("../../testing.zig");
test "fetch: fetch" {
try testing.htmlRunner("fetch/fetch.html");
}

View File

@@ -17,9 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
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;
@@ -91,7 +91,7 @@ pub const AbortSignal = struct {
self.reason = reason_ orelse DEFAULT_REASON;
const abort_event = try parser.eventCreate();
try parser.eventSetInternalType(abort_event, .abort_signal);
parser.eventSetInternalType(abort_event, .abort_signal);
defer parser.eventDestroy(abort_event);
try parser.eventInit(abort_event, "abort", .{});
@@ -113,12 +113,12 @@ pub const AbortSignal = struct {
}
const ThrowIfAborted = union(enum) {
exception: Env.Exception,
exception: js.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);
const ex = page.js.throw(self.reason orelse DEFAULT_REASON);
return .{ .exception = ex };
}
return .{ .undefined = {} };
@@ -138,44 +138,6 @@ const TimeoutCallback = struct {
};
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" },
}, .{});
test "Browser: HTML.AbortController" {
try testing.htmlRunner("html/abort_controller.html");
}

View File

@@ -17,7 +17,8 @@
// 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 js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const Allocator = std.mem.Allocator;
@@ -26,7 +27,7 @@ const DataSet = @This();
element: *parser.Element,
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !Env.UndefinedOr([]const u8) {
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !js.UndefinedOr([]const u8) {
const normalized_name = try normalize(page.call_arena, name);
if (try parser.elementGetAttribute(self.element, normalized_name)) |value| {
return .{ .value = value };
@@ -76,22 +77,6 @@ fn normalize(allocator: Allocator, name: []const u8) ![]const u8 {
}
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" },
}, .{});
test "Browser: HTML.DataSet" {
try testing.htmlRunner("html/dataset.html");
}

View File

@@ -0,0 +1,215 @@
// 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 js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
const History = @This();
const HistoryEntry = struct {
url: []const u8,
// This is serialized as JSON because
// History must survive a JsContext.
state: ?[]u8,
};
const ScrollRestorationMode = enum {
auto,
manual,
pub fn fromString(str: []const u8) ?ScrollRestorationMode {
for (std.enums.values(ScrollRestorationMode)) |mode| {
if (std.ascii.eqlIgnoreCase(str, @tagName(mode))) {
return mode;
}
} else {
return null;
}
}
pub fn toString(self: ScrollRestorationMode) []const u8 {
return @tagName(self);
}
};
scroll_restoration: ScrollRestorationMode = .auto,
stack: std.ArrayListUnmanaged(HistoryEntry) = .empty,
current: ?usize = null,
pub fn get_length(self: *History) u32 {
return @intCast(self.stack.items.len);
}
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
return self.scroll_restoration;
}
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
self.scroll_restoration = ScrollRestorationMode.fromString(mode) orelse self.scroll_restoration;
}
pub fn get_state(self: *History, page: *Page) !?js.Value {
if (self.current) |curr| {
const entry = self.stack.items[curr];
if (entry.state) |state| {
const value = try js.Value.fromJson(page.js, state);
return value;
} else {
return null;
}
} else {
return null;
}
}
pub fn pushNavigation(self: *History, _url: []const u8, page: *Page) !void {
const arena = page.session.arena;
const url = try arena.dupe(u8, _url);
const entry = HistoryEntry{ .state = null, .url = url };
try self.stack.append(arena, entry);
self.current = self.stack.items.len - 1;
}
pub fn dispatchPopStateEvent(state: ?[]const u8, page: *Page) void {
log.debug(.script_event, "dispatch popstate event", .{
.type = "popstate",
.source = "history",
});
History._dispatchPopStateEvent(state, page) catch |err| {
log.err(.app, "dispatch popstate event error", .{
.err = err,
.type = "popstate",
.source = "history",
});
};
}
fn _dispatchPopStateEvent(state: ?[]const u8, page: *Page) !void {
var evt = try PopStateEvent.constructor("popstate", .{ .state = state });
_ = try parser.eventTargetDispatchEvent(
@as(*parser.EventTarget, @ptrCast(&page.window)),
&evt.proto,
);
}
pub fn _pushState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
const json = try state.toJson(arena);
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
const entry = HistoryEntry{ .state = json, .url = url };
try self.stack.append(arena, entry);
self.current = self.stack.items.len - 1;
}
pub fn _replaceState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
if (self.current) |curr| {
const entry = &self.stack.items[curr];
const json = try state.toJson(arena);
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
entry.* = HistoryEntry{ .state = json, .url = url };
} else {
try self._pushState(state, "", _url, page);
}
}
pub fn go(self: *History, delta: i32, page: *Page) !void {
// 0 behaves the same as no argument, both reloading the page.
// If this is getting called, there SHOULD be an entry, atleast from pushNavigation.
const current = self.current.?;
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
if (index_s < 0 or index_s > self.stack.items.len - 1) {
return;
}
const index = @as(usize, @intCast(index_s));
const entry = self.stack.items[index];
self.current = index;
if (try page.isSameOrigin(entry.url)) {
History.dispatchPopStateEvent(entry.state, page);
}
try page.navigateFromWebAPI(entry.url, .{ .reason = .history });
}
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
try self.go(_delta orelse 0, page);
}
pub fn _back(self: *History, page: *Page) !void {
try self.go(-1, page);
}
pub fn _forward(self: *History, page: *Page) !void {
try self.go(1, page);
}
const parser = @import("../netsurf.zig");
const Event = @import("../events/event.zig").Event;
pub const PopStateEvent = struct {
pub const prototype = *Event;
pub const union_make_copy = true;
pub const EventInit = struct {
state: ?[]const u8 = null,
};
proto: parser.Event,
state: ?[]const u8,
pub fn constructor(event_type: []const u8, opts: ?EventInit) !PopStateEvent {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .pop_state);
const o = opts orelse EventInit{};
return .{
.proto = event.*,
.state = o.state,
};
}
// `hasUAVisualTransition` is not implemented. It isn't baseline so this is okay.
pub fn get_state(self: *const PopStateEvent, page: *Page) !?js.Value {
if (self.state) |state| {
const value = try js.Value.fromJson(page.js, state);
return value;
} else {
return null;
}
}
};
const testing = @import("../../testing.zig");
test "Browser: HTML.History" {
try testing.htmlRunner("html/history.html");
}

View File

@@ -42,12 +42,12 @@ pub const HTMLDocument = struct {
// JS funcs
// --------
pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
pub fn get_domain(self: *parser.DocumentHTML) ![]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);
return location.get_host();
}
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
@@ -115,67 +115,69 @@ pub const HTMLDocument = struct {
}
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList {
const arena = page.arena;
var list: NodeList = .{};
if (name.len == 0) return list;
if (name.len == 0) {
return list;
}
const root = parser.documentHTMLToNode(self);
var c = try collection.HTMLCollectionByName(arena, root, name, .{
var c = try collection.HTMLCollectionByName(root, name, .{
.include_root = false,
});
const ln = try c.get_length();
try list.ensureTotalCapacity(page.arena, ln);
var i: u32 = 0;
while (i < ln) {
while (i < ln) : (i += 1) {
const n = try c.item(i) orelse break;
try list.append(arena, n);
i += 1;
list.appendAssumeCapacity(n);
}
return list;
}
pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", .{
pub fn get_images(self: *parser.DocumentHTML) collection.HTMLCollection {
return collection.HTMLCollectionByTagName(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", .{
pub fn get_embeds(self: *parser.DocumentHTML) collection.HTMLCollection {
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "embed", .{
.include_root = false,
});
}
pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return get_embeds(self, page);
pub fn get_plugins(self: *parser.DocumentHTML) collection.HTMLCollection {
return get_embeds(self);
}
pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", .{
pub fn get_forms(self: *parser.DocumentHTML) collection.HTMLCollection {
return collection.HTMLCollectionByTagName(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", .{
pub fn get_scripts(self: *parser.DocumentHTML) collection.HTMLCollection {
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "script", .{
.include_root = false,
});
}
pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection {
return try collection.HTMLCollectionEmpty();
pub fn get_applets(_: *parser.DocumentHTML) collection.HTMLCollection {
return collection.HTMLCollectionEmpty();
}
pub fn get_links(self: *parser.DocumentHTML) !collection.HTMLCollection {
return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{
pub fn get_links(self: *parser.DocumentHTML) collection.HTMLCollection {
return collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{
.include_root = false,
});
}
pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection {
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{
pub fn get_anchors(self: *parser.DocumentHTML) collection.HTMLCollection {
return collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{
.include_root = false,
});
}
@@ -314,116 +316,7 @@ pub const HTMLDocument = struct {
}
};
// Tests
// -----
const testing = @import("../../testing.zig");
test "Browser.HTML.Document" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "document.__proto__.constructor.name", "HTMLDocument" },
.{ "document.__proto__.__proto__.constructor.name", "Document" },
.{ "document.body.localName == 'body'", "true" },
}, .{});
try runner.testCases(&.{
.{ "document.domain", "lightpanda.io" },
.{ "document.referrer", "" },
.{ "document.title", "" },
.{ "document.body.localName", "body" },
.{ "document.head.localName", "head" },
.{ "document.images.length", "0" },
.{ "document.embeds.length", "0" },
.{ "document.plugins.length", "0" },
.{ "document.scripts.length", "0" },
.{ "document.forms.length", "0" },
.{ "document.links.length", "1" },
.{ "document.applets.length", "0" },
.{ "document.anchors.length", "0" },
.{ "document.all.length", "8" },
.{ "document.currentScript", "null" },
}, .{});
try runner.testCases(&.{
.{ "document.title = 'foo'", "foo" },
.{ "document.title", "foo" },
.{ "document.title = ''", "" },
}, .{});
try runner.testCases(&.{
.{ "document.getElementById('link').setAttribute('name', 'foo')", "undefined" },
.{ "let list = document.getElementsByName('foo')", "undefined" },
.{ "list.length", "1" },
}, .{});
try runner.testCases(&.{
.{ "document.cookie", "" },
.{ "document.cookie = 'name=Oeschger; SameSite=None; Secure'", "name=Oeschger; SameSite=None; Secure" },
.{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" },
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
.{ "document.cookie = 'IgnoreMy=Ghost; HttpOnly'", null }, // "" should be returned, but the framework overrules it atm
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
}, .{});
try runner.testCases(&.{
.{ "document.elementFromPoint(0.5, 0.5)", "null" }, // Return null since we only return element s when they have previously been localized
.{ "document.elementsFromPoint(0.5, 0.5)", "" },
.{
\\ let div1 = document.createElement('div');
\\ document.body.appendChild(div1);
\\ div1.getClientRects();
,
null,
},
.{ "document.elementFromPoint(0.5, 0.5)", "[object HTMLDivElement]" },
.{ "let elems = document.elementsFromPoint(0.5, 0.5)", null },
.{ "elems.length", "3" },
.{ "elems[0]", "[object HTMLDivElement]" },
.{ "elems[1]", "[object HTMLBodyElement]" },
.{ "elems[2]", "[object HTMLHtmlElement]" },
}, .{});
try runner.testCases(&.{
.{
\\ let a = document.createElement('a');
\\ a.href = "https://lightpanda.io";
\\ document.body.appendChild(a);
\\ a.getClientRects();
, // Note this will be placed after the div of previous test
null,
},
.{ "let a_again = document.elementFromPoint(1.5, 0.5)", null },
.{ "a_again", "[object HTMLAnchorElement]" },
.{ "a_again.href", "https://lightpanda.io" },
.{ "let a_agains = document.elementsFromPoint(1.5, 0.5)", null },
.{ "a_agains[0].href", "https://lightpanda.io" },
}, .{});
try runner.testCases(&.{
.{ "!document.all", "true" },
.{ "!!document.all", "false" },
.{ "document.all(5)", "[object HTMLParagraphElement]" },
.{ "document.all('content')", "[object HTMLDivElement]" },
}, .{});
try runner.testCases(&.{
.{ "document.defaultView.document == document", "true" },
}, .{});
try runner.testCases(&.{
.{ "document.readyState", "loading" },
}, .{});
try HTMLDocument.documentIsLoaded(runner.page.window.document, runner.page);
try runner.testCases(&.{
.{ "document.readyState", "interactive" },
}, .{});
try HTMLDocument.documentIsComplete(runner.page.window.document, runner.page);
try runner.testCases(&.{
.{ "document.readyState", "complete" },
}, .{});
test "Browser: HTML.Document" {
try testing.htmlRunner("html/document.html");
}

View File

@@ -18,9 +18,9 @@
const std = @import("std");
const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig");
const generate = @import("../../runtime/generate.zig");
const Env = @import("../env.zig").Env;
const generate = @import("../js/generate.zig");
const Page = @import("../page.zig").Page;
const urlStitch = @import("../../url.zig").URL.stitch;
@@ -133,14 +133,14 @@ pub const HTMLElement = struct {
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
const n = @as(*parser.Node, @ptrCast(e));
return try parser.nodeTextContent(n) orelse "";
return parser.nodeTextContent(n) orelse "";
}
pub fn set_innerText(e: *parser.ElementHTML, s: []const u8) !void {
const n = @as(*parser.Node, @ptrCast(e));
// create text node.
const doc = try parser.nodeOwnerDocument(n) orelse return error.NoDocument;
const doc = parser.nodeOwnerDocument(n) orelse return error.NoDocument;
const t = try parser.documentCreateTextNode(doc, s);
// remove existing children.
@@ -167,12 +167,12 @@ pub const HTMLElement = struct {
focusVisible: bool,
};
pub fn _focus(e: *parser.ElementHTML, _: ?FocusOpts, page: *Page) !void {
if (!try page.isNodeAttached(@ptrCast(e))) {
if (!page.isNodeAttached(@ptrCast(e))) {
return;
}
const Document = @import("../dom/document.zig").Document;
const root_node = try parser.nodeGetRootNode(@ptrCast(e));
const root_node = parser.nodeGetRootNode(@ptrCast(e));
try Document.setFocus(@ptrCast(root_node), e, page);
}
};
@@ -218,40 +218,40 @@ pub const HTMLAnchorElement = struct {
}
pub fn get_href(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetHref(self);
return parser.anchorGetHref(self);
}
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void {
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
return try parser.anchorSetHref(self, full);
return parser.anchorSetHref(self, full);
}
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetHrefLang(self);
return parser.anchorGetHrefLang(self);
}
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
return try parser.anchorSetHrefLang(self, href);
return parser.anchorSetHrefLang(self, href);
}
pub fn get_type(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetType(self);
return parser.anchorGetType(self);
}
pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
return try parser.anchorSetType(self, t);
return parser.anchorSetType(self, t);
}
pub fn get_rel(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetRel(self);
return parser.anchorGetRel(self);
}
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
return try parser.anchorSetRel(self, t);
return parser.anchorSetRel(self, t);
}
pub fn get_text(self: *parser.Anchor) !?[]const u8 {
return try parser.nodeTextContent(parser.anchorToNode(self));
return parser.nodeTextContent(parser.anchorToNode(self));
}
pub fn set_text(self: *parser.Anchor, v: []const u8) !void {
@@ -269,182 +269,175 @@ pub const HTMLAnchorElement = struct {
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
}
return .empty;
return error.NotProvided;
}
// TODO return a disposable string
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return try u.get_origin(page);
defer u.destructor();
return u.get_origin(page);
}
// TODO return a disposable string
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return u.get_protocol(page);
defer u.destructor();
return page.call_arena.dupe(u8, u.get_protocol());
}
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
pub fn set_protocol(self: *parser.Anchor, protocol: []const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
try u.set_protocol(protocol);
u.uri.scheme = v;
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return try u.get_host(page);
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_host());
}
pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void {
// search : separator
var p: ?u16 = null;
var h: []const u8 = undefined;
for (v, 0..) |c, i| {
if (c == ':') {
h = v[0..i];
p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
break;
}
}
const arena = page.arena;
pub fn set_host(self: *parser.Anchor, host: []const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
try u.set_host(host);
if (p) |pp| {
u.uri.host = .{ .raw = h };
u.uri.port = pp;
} else {
u.uri.host = .{ .raw = v };
u.uri.port = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return u.get_hostname();
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_hostname());
}
pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
var u = try url(self, page);
u.uri.host = .{ .raw = v };
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
defer u.destructor();
try u.set_hostname(hostname);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return try u.get_port(page);
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_port());
}
pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
if (v != null and v.?.len > 0) {
u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
if (maybe_port) |port| {
try u.set_port(port);
} else {
u.uri.port = null;
u.clearPort();
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return u.get_username();
}
var u = url(self, page) catch return "";
defer u.destructor();
pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
if (v) |vv| {
u.uri.user = .{ .raw = vv };
} else {
u.uri.user = null;
const username = u.get_username();
if (username.len == 0) {
return "";
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
return page.call_arena.dupe(u8, username);
}
pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
const username = if (maybe_username) |username| username else "";
try u.set_username(username);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return try page.arena.dupe(u8, u.get_password());
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_password());
}
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
if (v) |vv| {
u.uri.password = .{ .raw = vv };
} else {
u.uri.password = null;
}
const href = try u.toString(arena);
const password = if (maybe_password) |password| password else "";
try u.set_password(password);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return u.get_pathname();
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_pathname());
}
pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void {
var u = try url(self, page);
u.uri.path = .{ .raw = v };
const href = try u.toString(arena);
defer u.destructor();
try parser.anchorSetHref(self, href);
try u.set_pathname(pathname);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return try u.get_search(page);
var u = url(self, page) catch return "";
defer u.destructor();
// This allocates in page arena so no need to dupe.
return u.get_search(page);
}
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
try u.set_search(v, page);
const href = try u.toString(page.call_arena);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return try u.get_hash(page);
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_hash());
}
pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
pub fn set_hash(self: *parser.Anchor, maybe_hash: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
if (v) |vv| {
u.uri.fragment = .{ .raw = vv };
if (maybe_hash) |hash| {
try u.set_hash(hash);
} else {
u.uri.fragment = null;
u.clearHash();
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
};
@@ -732,6 +725,9 @@ pub const HTMLInputElement = struct {
pub fn set_value(self: *parser.Input, value: []const u8) !void {
try parser.inputSetValue(self, value);
}
pub fn _select(_: *parser.Input) void {
log.debug(.web_api, "not implemented", .{ .feature = "HTMLInputElement select" });
}
};
pub const HTMLLIElement = struct {
@@ -757,13 +753,21 @@ pub const HTMLLinkElement = struct {
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn get_rel(self: *parser.Link) ![]const u8 {
return parser.linkGetRel(self);
}
pub fn set_rel(self: *parser.Link, rel: []const u8) !void {
return parser.linkSetRel(self, rel);
}
pub fn get_href(self: *parser.Link) ![]const u8 {
return try parser.linkGetHref(self);
return parser.linkGetHref(self);
}
pub fn set_href(self: *parser.Link, href: []const u8, page: *const Page) !void {
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
return try parser.linkSetHref(self, full);
return parser.linkSetHref(self, full);
}
};
@@ -879,7 +883,7 @@ pub const HTMLScriptElement = struct {
// s.src = '...';
// This should load the script.
// addFromElement protects against double execution.
try page.script_manager.addFromElement(@ptrCast(@alignCast(self)));
try page.script_manager.addFromElement(@ptrCast(@alignCast(self)), "dynamic");
}
}
@@ -977,22 +981,37 @@ pub const HTMLScriptElement = struct {
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule");
}
pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function {
pub fn get_nonce(self: *parser.Script) !?[]const u8 {
return try parser.elementGetAttribute(
parser.scriptToElt(self),
"nonce",
) orelse "";
}
pub fn set_nonce(self: *parser.Script, v: []const u8) !void {
try parser.elementSetAttribute(
parser.scriptToElt(self),
"nonce",
v,
);
}
pub fn get_onload(self: *parser.Script, page: *Page) !?js.Function {
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
return state.onload;
}
pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
pub fn set_onload(self: *parser.Script, function: ?js.Function, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.onload = function;
}
pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function {
pub fn get_onerror(self: *parser.Script, page: *Page) !?js.Function {
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
return state.onerror;
}
pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
pub fn set_onerror(self: *parser.Script, function: ?js.Function, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.onerror = function;
}
@@ -1027,68 +1046,84 @@ pub const HTMLSlotElement = struct {
flatten: bool = false,
};
pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
const opts = opts_ orelse AssignedNodesOpts{ .flatten = false };
if (try findAssignedSlotNodes(self, opts, page)) |nodes| {
return nodes;
}
if (!opts.flatten) {
return &.{};
}
const node: *parser.Node = @ptrCast(@alignCast(self));
const nl = try parser.nodeGetChildNodes(node);
const len = try parser.nodeListLength(nl);
if (len == 0) {
return &.{};
}
var assigned = try page.call_arena.alloc(NodeUnion, len);
var i: usize = 0;
while (true) : (i += 1) {
const child = try parser.nodeListItem(nl, @intCast(i)) orelse break;
assigned[i] = try Node.toInterface(child);
}
return assigned[0..i];
return findAssignedSlotNodes(self, opts_, false, page);
}
fn findAssignedSlotNodes(self: *parser.Slot, opts: AssignedNodesOpts, page: *Page) !?[]NodeUnion {
// This should return Union, instead of NodeUnion, but we want to re-use
// findAssignedSlotNodes. Returning NodeUnion is fine, as long as every element
// within is an Element. This could be more efficient
pub fn _assignedElements(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
return findAssignedSlotNodes(self, opts_, true, page);
}
fn findAssignedSlotNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, element_only: bool, page: *Page) ![]NodeUnion {
const opts = opts_ orelse AssignedNodesOpts{ .flatten = false };
if (opts.flatten) {
log.warn(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
log.debug(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
}
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
const node: *parser.Node = @ptrCast(@alignCast(self));
var root = try parser.nodeGetRootNode(node);
if (page.getNodeState(root)) |state| {
if (state.shadow_root) |sr| {
root = @ptrCast(@alignCast(sr.host));
}
}
var arr: std.ArrayList(NodeUnion) = .empty;
const w = @import("../dom/walker.zig").WalkerChildren{};
var next: ?*parser.Node = null;
while (true) {
next = try w.get_next(root, next) orelse break;
if (try parser.nodeType(next.?) != .element) {
if (slot_name == null) {
// default slot (with no name), takes everything
try arr.append(page.call_arena, try Node.toInterface(next.?));
// First we look for any explicitly assigned nodes (via the slot attribute)
{
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
var root = parser.nodeGetRootNode(node);
if (page.getNodeState(root)) |state| {
if (state.shadow_root) |sr| {
root = @ptrCast(@alignCast(sr.host));
}
continue;
}
const el: *parser.Element = @ptrCast(@alignCast(next.?));
const element_slot = try parser.elementGetAttribute(el, "slot");
if (nullableStringsAreEqual(slot_name, element_slot)) {
// either they're the same string or they are both null
try arr.append(page.call_arena, try Node.toInterface(next.?));
continue;
var arr: std.ArrayList(NodeUnion) = .empty;
const w = @import("../dom/walker.zig").WalkerChildren{};
var next: ?*parser.Node = null;
while (true) {
next = try w.get_next(root, next) orelse break;
if (parser.nodeType(next.?) != .element) {
if (slot_name == null and !element_only) {
// default slot (with no name), takes everything
try arr.append(page.call_arena, try Node.toInterface(next.?));
}
continue;
}
const el: *parser.Element = @ptrCast(@alignCast(next.?));
const element_slot = try parser.elementGetAttribute(el, "slot");
if (nullableStringsAreEqual(slot_name, element_slot)) {
// either they're the same string or they are both null
try arr.append(page.call_arena, try Node.toInterface(next.?));
continue;
}
}
if (arr.items.len > 0) {
return arr.items;
}
if (!opts.flatten) {
return &.{};
}
}
return if (arr.items.len == 0) null else arr.items;
// Since, we have no explicitly assigned nodes and flatten == false,
// we'll collect the children of the slot - the defaults.
{
const nl = try parser.nodeGetChildNodes(node);
const len = parser.nodeListLength(nl);
if (len == 0) {
return &.{};
}
var assigned = try page.call_arena.alloc(NodeUnion, len);
var i: usize = 0;
while (true) : (i += 1) {
const child = parser.nodeListItem(nl, @intCast(i)) orelse break;
if (!element_only or parser.nodeType(child) == .element) {
assigned[i] = try Node.toInterface(child);
}
}
return assigned[0..i];
}
}
fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool {
@@ -1285,336 +1320,38 @@ pub fn toInterfaceFromTag(comptime T: type, e: *parser.Element, tag: parser.Tag)
}
const testing = @import("../../testing.zig");
test "Browser.HTML.Element" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let link = document.getElementById('link')", "undefined" },
.{ "link.target", "" },
.{ "link.target = '_blank'", "_blank" },
.{ "link.target", "_blank" },
.{ "link.target = ''", "" },
.{ "link.href", "foo" },
.{ "link.href = 'https://lightpanda.io/'", "https://lightpanda.io/" },
.{ "link.href", "https://lightpanda.io/" },
.{ "link.origin", "https://lightpanda.io" },
.{ "link.host = 'lightpanda.io:443'", "lightpanda.io:443" },
.{ "link.host", "lightpanda.io:443" },
.{ "link.port", "443" },
.{ "link.hostname", "lightpanda.io" },
.{ "link.host = 'lightpanda.io'", "lightpanda.io" },
.{ "link.host", "lightpanda.io" },
.{ "link.port", "" },
.{ "link.hostname", "lightpanda.io" },
.{ "link.host", "lightpanda.io" },
.{ "link.hostname", "lightpanda.io" },
.{ "link.hostname = 'foo.bar'", "foo.bar" },
.{ "link.href", "https://foo.bar/" },
.{ "link.search", "" },
.{ "link.search = 'q=bar'", "q=bar" },
.{ "link.search", "?q=bar" },
.{ "link.href", "https://foo.bar/?q=bar" },
.{ "link.hash", "" },
.{ "link.hash = 'frag'", "frag" },
.{ "link.hash", "#frag" },
.{ "link.href", "https://foo.bar/?q=bar#frag" },
.{ "link.port", "" },
.{ "link.port = '443'", "443" },
.{ "link.host", "foo.bar:443" },
.{ "link.hostname", "foo.bar" },
.{ "link.href", "https://foo.bar:443/?q=bar#frag" },
.{ "link.port = null", "null" },
.{ "link.href", "https://foo.bar/?q=bar#frag" },
.{ "link.href = 'foo'", "foo" },
.{ "link.type", "" },
.{ "link.type = 'text/html'", "text/html" },
.{ "link.type", "text/html" },
.{ "link.type = ''", "" },
.{ "link.text", "OK" },
.{ "link.text = 'foo'", "foo" },
.{ "link.text", "foo" },
.{ "link.text = 'OK'", "OK" },
}, .{});
try runner.testCases(&.{
.{ "let script = document.createElement('script')", "undefined" },
.{ "script.src = 'foo.bar'", "foo.bar" },
.{ "script.async = true", "true" },
.{ "script.async", "true" },
.{ "script.async = false", "false" },
.{ "script.async", "false" },
}, .{});
try runner.testCases(&.{
.{ "const backup = document.getElementById('content')", "undefined" },
.{ "document.getElementById('content').innerText = 'foo';", "foo" },
.{ "document.getElementById('content').innerText", "foo" },
.{ "document.getElementById('content').innerHTML = backup; true;", "true" },
}, .{});
try runner.testCases(&.{
.{ "let click_count = 0;", "undefined" },
.{ "let clickCbk = function() { click_count++ }", "undefined" },
.{ "document.getElementById('content').addEventListener('click', clickCbk);", "undefined" },
.{ "document.getElementById('content').click()", "undefined" },
.{ "click_count", "1" },
}, .{});
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" },
.{ "style.setProperty('background-color', 'blue')", "undefined" },
.{ "style.getPropertyValue('background-color')", "blue" },
.{ "style.length", "4" },
}, .{});
// Image
try runner.testCases(&.{
// Testing constructors
.{ "(new Image).width", "0" },
.{ "(new Image).height", "0" },
.{ "(new Image(4)).width", "4" },
.{ "(new Image(4, 6)).height", "6" },
// Testing ulong property
.{ "let fruit = new Image", null },
.{ "fruit.width", "0" },
.{ "fruit.width = 5", "5" },
.{ "fruit.width", "5" },
.{ "fruit.width = '15'", "15" },
.{ "fruit.width", "15" },
.{ "fruit.width = 'apple'", "apple" },
.{ "fruit.width;", "0" },
// Testing string property
.{ "let lyric = new Image", null },
.{ "lyric.src", "" },
.{ "lyric.src = 'okay'", "okay" },
.{ "lyric.src", "okay" },
.{ "lyric.src = 15", "15" },
.{ "lyric.src", "15" },
}, .{});
try runner.testCases(&.{
.{ "let a = document.createElement('a');", null },
.{ "a.href", "" },
.{ "a.host", "" },
.{ "a.href = 'about'", null },
.{ "a.href", "https://lightpanda.io/opensource-browser/about" },
}, .{});
// detached node cannot be focused
try runner.testCases(&.{
.{ "const focused = document.activeElement", null },
.{ "document.createElement('a').focus()", null },
.{ "document.activeElement === focused", "true" },
}, .{});
try runner.testCases(&.{
.{ "let l2 = document.createElement('link');", null },
.{ "l2.href", "" },
.{ "l2.href = 'https://lightpanda.io/opensource-browser/15'", null },
.{ "l2.href", "https://lightpanda.io/opensource-browser/15" },
.{ "l2.href = '/over/9000'", null },
.{ "l2.href", "https://lightpanda.io/over/9000" },
}, .{});
test "Browser: HTML.Element" {
try testing.htmlRunner("html/element.html");
}
test "Browser.HTML.Element.DataSet" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=x data-power='over 9000' data-empty data-some-long-key=ok></div>" });
defer runner.deinit();
try runner.testCases(&.{ .{ "let div = document.getElementById('x')", null }, .{ "div.dataset.nope", "undefined" }, .{ "div.dataset.power", "over 9000" }, .{ "div.dataset.empty", "" }, .{ "div.dataset.someLongKey", "ok" }, .{ "delete div.dataset.power", "true" }, .{ "div.dataset.power", "undefined" } }, .{});
test "Browser: HTML.HtmlLinkElement" {
try testing.htmlRunner("html/link.html");
}
test "Browser.HTML.HtmlInputElement.properties" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io/noslashattheend" });
defer runner.deinit();
var alloc = std.heap.ArenaAllocator.init(runner.allocator);
defer alloc.deinit();
const arena = alloc.allocator();
try runner.testCases(&.{.{ "let elem_input = document.createElement('input')", null }}, .{});
try runner.testCases(&.{.{ "elem_input.form", "null" }}, .{}); // Initial value
// Valid input.form is tested separately :Browser.HTML.HtmlInputElement.propeties.form
try testProperty(arena, &runner, "elem_input.form", "null", &.{.{ .input = "'foo'" }}); // Invalid
try runner.testCases(&.{.{ "elem_input.accept", "" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.accept", null, &str_valids); // Valid
try runner.testCases(&.{.{ "elem_input.alt", "" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.alt", null, &str_valids); // Valid
try runner.testCases(&.{.{ "elem_input.disabled", "false" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.disabled", null, &bool_valids); // Valid
try runner.testCases(&.{.{ "elem_input.maxLength", "-1" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.maxLength", null, &.{.{ .input = "5" }}); // Valid
try testProperty(arena, &runner, "elem_input.maxLength", "0", &.{.{ .input = "'banana'" }}); // Invalid
try runner.testCases(&.{.{ "try { elem_input.maxLength = -45 } catch(e) {e}", "Error: NegativeValueNotAllowed" }}, .{}); // Error
try runner.testCases(&.{.{ "elem_input.name", "" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.name", null, &str_valids); // Valid
try runner.testCases(&.{.{ "elem_input.readOnly", "false" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.readOnly", null, &bool_valids); // Valid
try runner.testCases(&.{.{ "elem_input.size", "20" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.size", null, &.{.{ .input = "5" }}); // Valid
try testProperty(arena, &runner, "elem_input.size", "20", &.{.{ .input = "-26" }}); // Invalid
try runner.testCases(&.{.{ "try { elem_input.size = 0 } catch(e) {e}", "Error: ZeroNotAllowed" }}, .{}); // Error
try runner.testCases(&.{.{ "try { elem_input.size = 'banana' } catch(e) {e}", "Error: ZeroNotAllowed" }}, .{}); // Error
try runner.testCases(&.{.{ "elem_input.src", "" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.src", null, &.{
.{ .input = "'foo'", .expected = "https://lightpanda.io/foo" }, // TODO stitch should work with spaces -> %20
.{ .input = "-3", .expected = "https://lightpanda.io/-3" },
.{ .input = "''", .expected = "https://lightpanda.io/noslashattheend" },
});
try runner.testCases(&.{.{ "elem_input.type", "text" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.type", null, &.{.{ .input = "'checkbox'", .expected = "checkbox" }}); // Valid
try testProperty(arena, &runner, "elem_input.type", "text", &.{.{ .input = "'5'" }}); // Invalid
// Properties that are related
try runner.testCases(&.{
.{ "let input_checked = document.createElement('input')", null },
.{ "input_checked.defaultChecked", "false" },
.{ "input_checked.checked", "false" },
.{ "input_checked.defaultChecked = true", "true" },
.{ "input_checked.defaultChecked", "true" },
.{ "input_checked.checked", "true" }, // Also perceived as true
.{ "input_checked.checked = false", "false" },
.{ "input_checked.defaultChecked", "true" },
.{ "input_checked.checked", "false" },
.{ "input_checked.defaultChecked = true", "true" },
.{ "input_checked.checked", "false" }, // Still false
}, .{});
try runner.testCases(&.{
.{ "let input_value = document.createElement('input')", null },
.{ "input_value.defaultValue", "" },
.{ "input_value.value", "" },
.{ "input_value.defaultValue = 3.1", "3.1" },
.{ "input_value.defaultValue", "3.1" },
.{ "input_value.value", "3.1" }, // Also perceived as 3.1
.{ "input_value.value = 'mango'", "mango" },
.{ "input_value.defaultValue", "3.1" },
.{ "input_value.value", "mango" },
.{ "input_value.defaultValue = true", "true" },
.{ "input_value.value", "mango" }, // Still mango
}, .{});
test "Browser: HTML.HtmlImageElement" {
try testing.htmlRunner("html/image.html");
}
test "Browser.HTML.HtmlInputElement.properties.form" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
\\ <form action="test.php" target="_blank">
\\ <p>
\\ <label>First name: <input type="text" name="first-name" /></label>
\\ </p>
\\ </form>
});
defer runner.deinit();
try runner.testCases(&.{
.{ "let elem_input = document.querySelector('input')", null },
.{ "elem_input.form", "[object HTMLFormElement]" }, // Initial value
.{ "elem_input.form = 'foo'", null },
.{ "elem_input.form", "[object HTMLFormElement]" }, // Invalid
}, .{});
test "Browser: HTML.HtmlInputElement" {
try testing.htmlRunner("html/input.html");
}
test "Browser.HTML.HTMLTemplateElement" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=c></div>" });
defer runner.deinit();
try runner.testCases(&.{
.{ "let t = document.createElement('template')", null },
.{ "let d = document.createElement('div')", null },
.{ "d.id = 'abc'", null },
.{ "t.content.append(d)", null },
.{ "document.getElementById('abc')", "null" },
.{ "document.getElementById('c').appendChild(t.content.cloneNode(true))", null },
.{ "document.getElementById('abc').id", "abc" },
.{ "t.innerHTML = '<span>over</span><p>9000!</p>';", null },
.{ "t.content.childNodes.length", "2" },
.{ "t.content.childNodes[0].tagName", "SPAN" },
.{ "t.content.childNodes[0].innerHTML", "over" },
.{ "t.content.childNodes[1].tagName", "P" },
.{ "t.content.childNodes[1].innerHTML", "9000!" },
}, .{});
test "Browser: HTML.HtmlTemplateElement" {
try testing.htmlRunner("html/template.html");
}
test "Browser.HTML.HTMLStyleElement" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
defer runner.deinit();
try runner.testCases(&.{
.{ "let s = document.createElement('style')", null },
.{ "s.sheet.type", "text/css" },
.{ "s.sheet == s.sheet", "true" },
.{ "document.createElement('style').sheet == s.sheet", "false" },
}, .{});
test "Browser: HTML.HtmlStyleElement" {
try testing.htmlRunner("html/style.html");
}
test "Browser: HTML.HTMLScriptElement" {
test "Browser: HTML.HtmlScriptElement" {
try testing.htmlRunner("html/script/script.html");
try testing.htmlRunner("html/script/inline_defer.html");
try testing.htmlRunner("html/script/import.html");
try testing.htmlRunner("html/script/dynamic_import.html");
try testing.htmlRunner("html/script/importmap.html");
}
test "Browser: HTML.HTMLSlotElement" {
try testing.htmlRunner("html/html_slot_element.html");
}
const Check = struct {
input: []const u8,
expected: ?[]const u8 = null, // Needed when input != expected
};
const bool_valids = [_]Check{
.{ .input = "true" },
.{ .input = "''", .expected = "false" },
.{ .input = "13.5", .expected = "true" },
};
const str_valids = [_]Check{
.{ .input = "'foo'", .expected = "foo" },
.{ .input = "5", .expected = "5" },
.{ .input = "''", .expected = "" },
.{ .input = "document", .expected = "[object HTMLDocument]" },
};
// .{ "elem.type = '5'", "5" },
// .{ "elem.type", "text" },
fn testProperty(
arena: std.mem.Allocator,
runner: *testing.JsRunner,
elem_dot_prop: []const u8,
always: ?[]const u8, // Ignores checks' expected if set
checks: []const Check,
) !void {
for (checks) |check| {
try runner.testCases(&.{
.{ try std.mem.concat(arena, u8, &.{ elem_dot_prop, " = ", check.input }), null },
.{ elem_dot_prop, always orelse check.expected orelse check.input },
}, .{});
}
test "Browser: HTML.HtmlSlotElement" {
try testing.htmlRunner("html/slot.html");
}

View File

@@ -15,7 +15,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 Env = @import("../env.zig").Env;
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
@@ -28,21 +28,21 @@ pub const ErrorEvent = struct {
filename: []const u8,
lineno: i32,
colno: i32,
@"error": ?Env.JsObject,
@"error": ?js.Object,
const ErrorEventInit = struct {
message: []const u8 = "",
filename: []const u8 = "",
lineno: i32 = 0,
colno: i32 = 0,
@"error": ?Env.JsObject = null,
@"error": ?js.Object = 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);
parser.eventSetInternalType(event, .error_event);
const o = opts orelse ErrorEventInit{};
@@ -72,7 +72,7 @@ pub const ErrorEvent = struct {
return self.colno;
}
pub fn get_error(self: *const ErrorEvent) Env.UndefinedOr(Env.JsObject) {
pub fn get_error(self: *const ErrorEvent) js.UndefinedOr(js.Object) {
if (self.@"error") |e| {
return .{ .value = e };
}

View File

@@ -21,7 +21,6 @@ const Allocator = std.mem.Allocator;
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const HTMLElement = @import("elements.zig").HTMLElement;
const FormData = @import("../xhr/form_data.zig").FormData;
pub const HTMLFormElement = struct {
pub const Self = parser.Form;

View File

@@ -1,120 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
pub const History = struct {
const ScrollRestorationMode = enum {
auto,
manual,
};
scrollRestoration: ScrollRestorationMode = .auto,
state: std.json.Value = .null,
// count tracks the history length until we implement correctly pushstate.
count: u32 = 0,
pub fn get_length(self: *History) u32 {
// TODO return the real history length value.
return self.count;
}
pub fn get_scrollRestoration(self: *History) []const u8 {
return switch (self.scrollRestoration) {
.auto => "auto",
.manual => "manual",
};
}
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
if (std.mem.eql(u8, "manual", mode)) self.scrollRestoration = .manual;
if (std.mem.eql(u8, "auto", mode)) self.scrollRestoration = .auto;
}
pub fn get_state(self: *History) std.json.Value {
return self.state;
}
// TODO implement the function
// data must handle any argument. We could expect a std.json.Value but
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
pub fn _pushState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
self.count += 1;
_ = url;
_ = data;
}
// TODO implement the function
// data must handle any argument. We could expect a std.json.Value but
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
pub fn _replaceState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
_ = self;
_ = url;
_ = data;
}
// TODO implement the function
pub fn _go(self: *History, delta: ?i32) void {
_ = self;
_ = delta;
}
// TODO implement the function
pub fn _back(self: *History) void {
_ = self;
}
// TODO implement the function
pub fn _forward(self: *History) void {
_ = self;
}
};
// Tests
// -----
const testing = @import("../../testing.zig");
test "Browser.HTML.History" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "history.scrollRestoration", "auto" },
.{ "history.scrollRestoration = 'manual'", "manual" },
.{ "history.scrollRestoration = 'foo'", "foo" },
.{ "history.scrollRestoration", "manual" },
.{ "history.scrollRestoration = 'auto'", "auto" },
.{ "history.scrollRestoration", "auto" },
.{ "history.state", "null" },
.{ "history.pushState({}, null, '')", "undefined" },
.{ "history.replaceState({}, null, '')", "undefined" },
.{ "history.go()", "undefined" },
.{ "history.go(1)", "undefined" },
.{ "history.go(-1)", "undefined" },
.{ "history.forward()", "undefined" },
.{ "history.back()", "undefined" },
}, .{});
}

View File

@@ -21,7 +21,7 @@ const HTMLElem = @import("elements.zig");
const SVGElem = @import("svg_elements.zig");
const Window = @import("window.zig").Window;
const Navigator = @import("navigator.zig").Navigator;
const History = @import("history.zig").History;
const History = @import("History.zig");
const Location = @import("location.zig").Location;
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;

View File

@@ -16,10 +16,8 @@
// 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

View File

@@ -16,57 +16,62 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const Page = @import("../page.zig").Page;
const Uri = @import("std").Uri;
const Page = @import("../page.zig").Page;
const URL = @import("../url/url.zig").URL;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface
pub const Location = struct {
url: ?URL = null,
url: URL,
/// Initializes the `Location` to be used in `Window`.
/// Browsers give such initial values when user not navigated yet:
/// Chrome -> chrome://new-tab-page/
/// Firefox -> about:newtab
/// Safari -> favorites://
pub fn init(url: []const u8) !Location {
return .{ .url = try .initForLocation(url) };
}
pub fn get_href(self: *Location, page: *Page) ![]const u8 {
if (self.url) |*u| return u.get_href(page);
return "";
return self.url.get_href(page);
}
pub fn get_protocol(self: *Location, page: *Page) ![]const u8 {
if (self.url) |*u| return u.get_protocol(page);
return "";
pub fn set_href(_: *const Location, href: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(href, .{ .reason = .script });
}
pub fn get_host(self: *Location, page: *Page) ![]const u8 {
if (self.url) |*u| return u.get_host(page);
return "";
pub fn get_protocol(self: *Location) []const u8 {
return self.url.get_protocol();
}
pub fn get_host(self: *Location) []const u8 {
return self.url.get_host();
}
pub fn get_hostname(self: *Location) []const u8 {
if (self.url) |*u| return u.get_hostname();
return "";
return self.url.get_hostname();
}
pub fn get_port(self: *Location, page: *Page) ![]const u8 {
if (self.url) |*u| return u.get_port(page);
return "";
pub fn get_port(self: *Location) []const u8 {
return self.url.get_port();
}
pub fn get_pathname(self: *Location) []const u8 {
if (self.url) |*u| return u.get_pathname();
return "";
return self.url.get_pathname();
}
pub fn get_search(self: *Location, page: *Page) ![]const u8 {
if (self.url) |*u| return u.get_search(page);
return "";
return self.url.get_search(page);
}
pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
if (self.url) |*u| return u.get_hash(page);
return "";
pub fn get_hash(self: *Location) []const u8 {
return self.url.get_hash();
}
pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
if (self.url) |*u| return u.get_origin(page);
return "";
return self.url.get_origin(page);
}
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
@@ -82,25 +87,11 @@ pub const Location = struct {
}
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
return try self.get_href(page);
return self.get_href(page);
}
};
const testing = @import("../../testing.zig");
test "Browser.HTML.Location" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "location.href", "https://lightpanda.io/opensource-browser/" },
.{ "document.location.href", "https://lightpanda.io/opensource-browser/" },
.{ "location.host", "lightpanda.io" },
.{ "location.hostname", "lightpanda.io" },
.{ "location.origin", "https://lightpanda.io" },
.{ "location.pathname", "/opensource-browser/" },
.{ "location.hash", "" },
.{ "location.port", "" },
.{ "location.search", "" },
}, .{});
test "Browser: HTML.Location" {
try testing.htmlRunner("html/location.html");
}

View File

@@ -16,8 +16,8 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig");
const Function = @import("../env.zig").Function;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
// https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface
@@ -39,7 +39,7 @@ pub const MediaQueryList = struct {
return self.media;
}
pub fn _addListener(_: *const MediaQueryList, _: Function) void {}
pub fn _addListener(_: *const MediaQueryList, _: js.Function) void {}
pub fn _removeListener(_: *const MediaQueryList, _: Function) void {}
pub fn _removeListener(_: *const MediaQueryList, _: js.Function) void {}
};

View File

@@ -80,17 +80,7 @@ pub const Navigator = struct {
}
};
// Tests
// -----
const testing = @import("../../testing.zig");
test "Browser.HTML.Navigator" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "navigator.userAgent", "Lightpanda/1.0" },
.{ "navigator.appVersion", "1.0" },
.{ "navigator.language", "en-US" },
}, .{});
test "Browser: HTML.Navigator" {
try testing.htmlRunner("html/navigator.html");
}

View File

@@ -199,80 +199,6 @@ pub const HTMLOptionsCollection = struct {
};
const testing = @import("../../testing.zig");
test "Browser.HTML.Select" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
\\ <form id=f1>
\\ <select id=s1 name=s1><option>o1<option>o2</select>
\\ </form>
\\ <select id=s2></select>
});
defer runner.deinit();
try runner.testCases(&.{
.{ "const s = document.getElementById('s1');", null },
.{ "s.form", "[object HTMLFormElement]" },
.{ "document.getElementById('s2').form", "null" },
.{ "s.disabled", "false" },
.{ "s.disabled = true", null },
.{ "s.disabled", "true" },
.{ "s.disabled = false", null },
.{ "s.disabled", "false" },
.{ "s.multiple", "false" },
.{ "s.multiple = true", null },
.{ "s.multiple", "true" },
.{ "s.multiple = false", null },
.{ "s.multiple", "false" },
.{ "s.name;", "s1" },
.{ "s.name = 'sel1';", null },
.{ "s.name", "sel1" },
.{ "s.length;", "2" },
.{ "s.selectedIndex", "0" },
.{ "s.selectedIndex = 2", null }, // out of range
.{ "s.selectedIndex", "-1" },
.{ "s.selectedIndex = -1", null },
.{ "s.selectedIndex", "-1" },
.{ "s.selectedIndex = 0", null },
.{ "s.selectedIndex", "0" },
.{ "s.selectedIndex = 1", null },
.{ "s.selectedIndex", "1" },
.{ "s.selectedIndex = -323", null },
.{ "s.selectedIndex", "-1" },
.{ "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" },
}, .{});
test "Browser: HTML.Select" {
try testing.htmlRunner("html/select.html");
}

View File

@@ -25,17 +25,12 @@ pub const SVGElement = struct {
// Currently the prototype chain is not implemented (will not be returned by toInterface())
// For that we need parser.SvgElement and the derived types with tags in the v-table.
pub const prototype = *Element;
// While this is a Node, could consider not exposing the subtype untill we have
// While this is a Node, could consider not exposing the subtype until we have
// a Self type to cast to.
pub const subtype = .node;
};
const testing = @import("../../testing.zig");
test "Browser.HTML.SVGElement" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "'AString' instanceof SVGElement", "false" },
}, .{});
test "Browser: HTML.SVGElement" {
try testing.htmlRunner("html/svg.html");
}

View File

@@ -18,13 +18,13 @@
const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const Navigator = @import("navigator.zig").Navigator;
const History = @import("history.zig").History;
const History = @import("History.zig");
const Location = @import("location.zig").Location;
const Crypto = @import("../crypto/crypto.zig").Crypto;
const Console = @import("../console/console.zig").Console;
@@ -35,11 +35,13 @@ 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 EventHandler = @import("../events/event.zig").EventHandler;
const Function = Env.Function;
const JsObject = Env.JsObject;
const Request = @import("../fetch/Request.zig");
const fetchFn = @import("../fetch/fetch.zig").fetch;
const storage = @import("../storage/storage.zig");
const ErrorEvent = @import("error_event.zig").ErrorEvent;
// https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
@@ -51,8 +53,7 @@ pub const Window = struct {
document: *parser.DocumentHTML,
target: []const u8 = "",
history: History = .{},
location: Location = .{},
location: Location,
storage_shelf: ?*storage.Shelf = null,
// counter for having unique timer ids
@@ -65,6 +66,9 @@ pub const Window = struct {
performance: Performance,
screen: Screen = .{},
css: Css = .{},
scroll_x: u32 = 0,
scroll_y: u32 = 0,
onload_callback: ?js.Function = null,
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
var fbs = std.io.fixedBufferStream("");
@@ -75,6 +79,7 @@ pub const Window = struct {
return .{
.document = html_doc,
.target = target orelse "",
.location = try .init("about:blank"),
.navigator = navigator orelse .{},
.performance = Performance.init(),
};
@@ -85,6 +90,10 @@ pub const Window = struct {
try parser.documentHTMLSetLocation(Location, self.document, &self.location);
}
pub fn changeLocation(self: *Window, new_url: []const u8, page: *Page) !void {
return self.location.url.reinit(new_url, page);
}
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
self.document = doc;
@@ -95,12 +104,42 @@ pub const Window = struct {
self.storage_shelf = shelf;
}
pub fn get_window(self: *Window) *Window {
return self;
pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !js.Promise {
return fetchFn(input, options, page);
}
pub fn get_navigator(self: *Window) *Navigator {
return &self.navigator;
/// Returns `onload_callback`.
pub fn get_onload(self: *const Window) ?js.Function {
return self.onload_callback;
}
/// Sets `onload_callback`.
pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
const event_target = parser.toEventTarget(Window, self);
const event_type = "load";
// Check if we have a listener set.
if (self.onload_callback) |callback| {
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
std.debug.assert(listener != null);
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
}
if (maybe_listener) |listener| {
switch (listener) {
// If an object is given as listener, do nothing.
.object => {},
.function => |callback| {
_ = try EventHandler.register(page.arena, event_target, event_type, listener, null) orelse unreachable;
self.onload_callback = callback;
return;
},
}
}
// Just unset the listener.
self.onload_callback = null;
}
pub fn get_location(self: *Window) *Location {
@@ -111,22 +150,6 @@ pub const Window = struct {
return page.navigateFromWebAPI(url, .{ .reason = .script });
}
pub fn get_console(self: *Window) *Console {
return &self.console;
}
pub fn get_crypto(self: *Window) *Crypto {
return &self.crypto;
}
pub fn get_self(self: *Window) *Window {
return self;
}
pub fn get_parent(self: *Window) *Window {
return self;
}
// frames return the window itself, but accessing it via a pseudo
// array returns the Window object corresponding to the given frame or
// iframe.
@@ -164,16 +187,12 @@ pub const Window = struct {
return frames.get_length();
}
pub fn get_top(self: *Window) *Window {
return self;
}
pub fn get_document(self: *Window) ?*parser.DocumentHTML {
return self.document;
}
pub fn get_history(self: *Window) *History {
return &self.history;
pub fn get_history(_: *Window, page: *Page) *History {
return &page.session.history;
}
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
@@ -202,19 +221,11 @@ pub const Window = struct {
return &self.storage_shelf.?.bucket.session;
}
pub fn get_performance(self: *Window) *Performance {
return &self.performance;
}
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 {
pub fn _requestAnimationFrame(self: *Window, cbk: js.Function, page: *Page) !u32 {
return self.createTimeout(cbk, 5, page, .{
.animation_frame = true,
.name = "animationFrame",
@@ -226,11 +237,11 @@ pub const Window = struct {
_ = self.timers.remove(id);
}
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
pub fn _setTimeout(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
}
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
pub fn _setInterval(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
}
@@ -242,14 +253,22 @@ pub const Window = struct {
_ = self.timers.remove(id);
}
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !u32 {
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
}
pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
pub fn _setImmediate(self: *Window, cbk: js.Function, page: *Page) !u32 {
return self.createTimeout(cbk, 0, page, .{ .name = "setImmediate" });
}
pub fn _clearImmediate(self: *Window, id: u32) void {
_ = self.timers.remove(id);
}
pub fn _matchMedia(_: *const Window, media: js.String) !MediaQueryList {
return .{
.matches = false, // TODO?
.media = try page.arena.dupe(u8, media),
.media = media.string,
};
}
@@ -268,14 +287,33 @@ pub const Window = struct {
return out;
}
pub fn _reportError(self: *Window, err: js.Object, page: *Page) !void {
var error_event = try ErrorEvent.constructor("error", .{
.@"error" = err,
});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, self),
@as(*parser.Event, &error_event.proto),
);
if (parser.eventDefaultPrevented(&error_event.proto) == false) {
const err_string = err.toString() catch "Unknown error";
log.info(.user_script, "error", .{
.err = err_string,
.stack = page.stackTrace() catch "???",
.source = "window.reportError",
});
}
}
const CreateTimeoutOpts = struct {
name: []const u8,
args: []Env.JsObject = &.{},
args: []js.Object = &.{},
repeat: bool = false,
animation_frame: bool = false,
low_priority: bool = false,
};
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
fn createTimeout(self: *Window, cbk: js.Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
const delay = delay_ orelse 0;
if (self.timers.count() > 512) {
return error.TooManyTimeout;
@@ -295,9 +333,9 @@ pub const Window = struct {
errdefer _ = self.timers.remove(timer_id);
const args = opts.args;
var persisted_args: []Env.JsObject = &.{};
var persisted_args: []js.Object = &.{};
if (args.len > 0) {
persisted_args = try page.arena.alloc(Env.JsObject, args.len);
persisted_args = try page.arena.alloc(js.Object, args.len);
for (args, persisted_args) |a, *ca| {
ca.* = try a.persist();
}
@@ -338,12 +376,20 @@ pub const Window = struct {
const Opts = struct {
top: i32,
left: i32,
behavior: []const u8,
behavior: []const u8 = "",
};
};
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?u32) !void {
_ = opts;
_ = y;
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32) !void {
switch (opts) {
.x => |x| {
self.scroll_x = @intCast(@max(x, 0));
self.scroll_y = @intCast(@max(0, y orelse 0));
},
.opts => |o| {
self.scroll_y = @intCast(@max(0, o.top));
self.scroll_x = @intCast(@max(0, o.left));
},
}
{
const scroll_event = try parser.eventCreate();
@@ -367,6 +413,28 @@ pub const Window = struct {
);
}
}
pub fn _scroll(self: *Window, opts: ScrollToOpts, y: ?i32) !void {
// just an alias for scrollTo
return self._scrollTo(opts, y);
}
pub fn get_scrollX(self: *const Window) u32 {
return self.scroll_x;
}
pub fn get_scrollY(self: *const Window) u32 {
return self.scroll_y;
}
pub fn get_pageXOffset(self: *const Window) u32 {
// just an alias for scrollX
return self.get_scrollX();
}
pub fn get_pageYOffset(self: *const Window) u32 {
// just an alias for scrollY
return self.get_scrollY();
}
// libdom's document doesn't have a parent, which is correct, but
// breaks the event bubbling that happens for many events from
@@ -384,6 +452,18 @@ pub const Window = struct {
// and thus the target has already been set to the document.
return self.base.redispatchEvent(evt);
}
pub fn postAttach(self: *Window, js_this: js.This) !void {
try js_this.set("top", self, .{});
try js_this.set("self", self, .{});
try js_this.set("parent", self, .{});
try js_this.set("window", self, .{});
try js_this.set("crypto", &self.crypto, .{});
try js_this.set("screen", &self.screen, .{});
try js_this.set("console", &self.console, .{});
try js_this.set("navigator", &self.navigator, .{});
try js_this.set("performance", &self.performance, .{});
}
};
const TimerCallback = struct {
@@ -394,13 +474,13 @@ const TimerCallback = struct {
repeat: ?u32,
// The JavaScript callback to execute
cbk: Function,
cbk: js.Function,
animation_frame: bool = false,
window: *Window,
args: []Env.JsObject = &.{},
args: []js.Object = &.{},
fn run(ctx: *anyopaque) ?u32 {
const self: *TimerCallback = @ptrCast(@alignCast(ctx));
@@ -414,7 +494,7 @@ const TimerCallback = struct {
return null;
}
var result: Function.Result = undefined;
var result: js.Function.Result = undefined;
var call: anyerror!void = undefined;
if (self.animation_frame) {

561
src/browser/js/Caller.zig Normal file
View File

@@ -0,0 +1,561 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const Page = @import("../page.zig").Page;
const types = @import("types.zig");
const Context = @import("Context.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const CALL_ARENA_RETAIN = 1024 * 16;
// Responsible for calling Zig functions from JS invocations. This could
// probably just contained in ExecutionWorld, but having this specific logic, which
// is somewhat repetitive between constructors, functions, getters, etc contained
// here does feel like it makes it cleaner.
const Caller = @This();
context: *Context,
v8_context: v8.Context,
isolate: v8.Isolate,
call_arena: Allocator,
// info is a v8.PropertyCallbackInfo or a v8.FunctionCallback
// All we really want from it is the isolate.
// executor = Isolate -> getCurrentContext -> getEmbedderData()
pub fn init(info: anytype) Caller {
const isolate = info.getIsolate();
const v8_context = isolate.getCurrentContext();
const context: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
context.call_depth += 1;
return .{
.context = context,
.isolate = isolate,
.v8_context = v8_context,
.call_arena = context.call_arena,
};
}
pub fn deinit(self: *Caller) void {
const context = self.context;
const call_depth = context.call_depth - 1;
// Because of callbacks, calls can be nested. Because of this, we
// can't clear the call_arena after _every_ call. Imagine we have
// arr.forEach((i) => { console.log(i); }
//
// First we call forEach. Inside of our forEach call,
// we call console.log. If we reset the call_arena after this call,
// it'll reset it for the `forEach` call after, which might still
// need the data.
//
// Therefore, we keep a call_depth, and only reset the call_arena
// when a top-level (call_depth == 0) function ends.
if (call_depth == 0) {
const arena: *ArenaAllocator = @ptrCast(@alignCast(context.call_arena.ptr));
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
// Set this _after_ we've executed the above code, so that if the
// above code executes any callbacks, they aren't being executed
// at scope 0, which would be wrong.
context.call_depth = call_depth;
}
pub fn constructor(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
const args = try self.getArgs(Struct, named_function, 0, info);
const res = @call(.auto, Struct.constructor, args);
const ReturnType = @typeInfo(@TypeOf(Struct.constructor)).@"fn".return_type orelse {
@compileError(@typeName(Struct) ++ " has a constructor without a return type");
};
const this = info.getThis();
if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err;
_ = try self.context.mapZigInstanceToJs(this, non_error_res);
} else {
_ = try self.context.mapZigInstanceToJs(this, res);
}
info.getReturnValue().set(this);
}
pub fn method(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
if (comptime isSelfReceiver(Struct, named_function) == false) {
return self.function(Struct, named_function, info);
}
const context = self.context;
const func = @field(Struct, named_function.name);
var args = try self.getArgs(Struct, named_function, 1, info);
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
// inject 'self' as the first parameter
@field(args, "0") = zig_instance;
const res = @call(.auto, func, args);
info.getReturnValue().set(try context.zigValueToJs(res));
}
pub fn function(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
const context = self.context;
const func = @field(Struct, named_function.name);
const args = try self.getArgs(Struct, named_function, 0, info);
const res = @call(.auto, func, args);
info.getReturnValue().set(try context.zigValueToJs(res));
}
pub fn getIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, idx: u32, info: v8.PropertyCallbackInfo) !u8 {
const context = self.context;
const func = @field(Struct, named_function.name);
const IndexedGet = @TypeOf(func);
if (@typeInfo(IndexedGet).@"fn".return_type == null) {
@compileError(named_function.full_name ++ " must have a return type");
}
var has_value = true;
var args: ParamterTypes(IndexedGet) = undefined;
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
switch (arg_fields.len) {
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
3, 4 => {
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
comptime assertSelfReceiver(Struct, named_function);
@field(args, "0") = zig_instance;
@field(args, "1") = idx;
@field(args, "2") = &has_value;
if (comptime arg_fields.len == 4) {
comptime assertIsPageArg(Struct, named_function, 3);
@field(args, "3") = context.page;
}
},
else => @compileError(named_function.full_name ++ " has too many parmaters"),
}
const res = @call(.auto, func, args);
if (has_value == false) {
return v8.Intercepted.No;
}
info.getReturnValue().set(try context.zigValueToJs(res));
return v8.Intercepted.Yes;
}
pub fn getNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
const context = self.context;
const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function);
var has_value = true;
var args = try self.getArgs(Struct, named_function, 3, info);
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
@field(args, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = &has_value;
const res = @call(.auto, func, args);
if (has_value == false) {
return v8.Intercepted.No;
}
info.getReturnValue().set(try self.context.zigValueToJs(res));
return v8.Intercepted.Yes;
}
pub fn setNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo) !u8 {
const context = self.context;
const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function);
var has_value = true;
var args = try self.getArgs(Struct, named_function, 4, info);
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
@field(args, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = try context.jsValueToZig(named_function, @TypeOf(@field(args, "2")), js_value);
@field(args, "3") = &has_value;
const res = @call(.auto, func, args);
return namedSetOrDeleteCall(res, has_value);
}
pub fn deleteNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
const context = self.context;
const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function);
var has_value = true;
var args = try self.getArgs(Struct, named_function, 3, info);
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
@field(args, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = &has_value;
const res = @call(.auto, func, args);
return namedSetOrDeleteCall(res, has_value);
}
fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 {
if (@typeInfo(@TypeOf(res)) == .error_union) {
_ = try res;
}
if (has_value == false) {
return v8.Intercepted.No;
}
return v8.Intercepted.Yes;
}
fn nameToString(self: *Caller, name: v8.Name) ![]const u8 {
return self.context.valueToString(.{ .handle = name.handle }, .{});
}
fn isSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) bool {
return checkSelfReceiver(Struct, named_function, false);
}
fn assertSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) void {
_ = checkSelfReceiver(Struct, named_function, true);
}
fn checkSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction, comptime fail: bool) bool {
const func = @field(Struct, named_function.name);
const params = @typeInfo(@TypeOf(func)).@"fn".params;
if (params.len == 0) {
if (fail) {
@compileError(named_function.full_name ++ " must have a self parameter");
}
return false;
}
const R = types.Receiver(Struct);
const first_param = params[0].type.?;
if (first_param != *R and first_param != *const R) {
if (fail) {
@compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{
named_function.full_name,
@typeName(R),
@typeName(R),
@typeName(first_param),
}));
}
return false;
}
return true;
}
fn assertIsPageArg(comptime Struct: type, comptime named_function: NamedFunction, index: comptime_int) void {
const F = @TypeOf(@field(Struct, named_function.name));
const param = @typeInfo(F).@"fn".params[index].type.?;
if (isPage(param)) {
return;
}
@compileError(std.fmt.comptimePrint("The {d} parameter to {s} must be a *Page or *const Page. Got: {s}", .{ index, named_function.full_name, @typeName(param) }));
}
pub fn handleError(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, err: anyerror, info: anytype) void {
const isolate = self.isolate;
if (comptime @import("builtin").mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
if (log.enabled(.js, .warn)) {
self.logFunctionCallError(err, named_function.full_name, info);
}
}
var js_err: ?v8.Value = switch (err) {
error.InvalidArgument => createTypeException(isolate, "invalid argument"),
error.OutOfMemory => js._createException(isolate, "out of memory"),
error.IllegalConstructor => js._createException(isolate, "Illegal Contructor"),
else => blk: {
const func = @field(Struct, named_function.name);
const return_type = @typeInfo(@TypeOf(func)).@"fn".return_type orelse {
// void return type;
break :blk null;
};
if (@typeInfo(return_type) != .error_union) {
// type defines a custom exception, but this function should
// not fail. We failed somewhere inside of js.zig and
// should return the error as-is, since it isn't related
// to our Struct
break :blk null;
}
const function_error_set = @typeInfo(return_type).error_union.error_set;
const E = comptime getCustomException(Struct) orelse break :blk null;
if (function_error_set == E or isErrorSetException(E, err)) {
const custom_exception = E.init(self.call_arena, err, named_function.js_name) catch |init_err| {
switch (init_err) {
// if a custom exceptions' init wants to return a
// different error, we need to think about how to
// handle that failure.
error.OutOfMemory => break :blk js._createException(isolate, "out of memory"),
}
};
// ughh..how to handle an error here?
break :blk self.context.zigValueToJs(custom_exception) catch js._createException(isolate, "internal error");
}
// this error isn't part of a custom exception
break :blk null;
},
};
if (js_err == null) {
js_err = js._createException(isolate, @errorName(err));
}
const js_exception = isolate.throwException(js_err.?);
info.getReturnValue().setValueHandle(js_exception.handle);
}
// walk the prototype chain to see if a type declares a custom Exception
fn getCustomException(comptime Struct: type) ?type {
var S = Struct;
while (true) {
if (@hasDecl(S, "Exception")) {
return S.Exception;
}
if (@hasDecl(S, "prototype") == false) {
return null;
}
// long ago, we validated that every prototype declaration
// is a pointer.
S = @typeInfo(S.prototype).pointer.child;
}
}
// Does the error we want to return belong to the custom exeception's ErrorSet
fn isErrorSetException(comptime E: type, err: anytype) bool {
const Entry = std.meta.Tuple(&.{ []const u8, void });
const error_set = @typeInfo(E.ErrorSet).error_set.?;
const entries = comptime blk: {
var kv: [error_set.len]Entry = undefined;
for (error_set, 0..) |e, i| {
kv[i] = .{ e.name, {} };
}
break :blk kv;
};
const lookup = std.StaticStringMap(void).initComptime(entries);
return lookup.has(@errorName(err));
}
// If we call a method in javascript: cat.lives('nine');
//
// Then we'd expect a Zig function with 2 parameters: a self and the string.
// In this case, offset == 1. Offset is always 1 for setters or methods.
//
// Offset is always 0 for constructors.
//
// For constructors, setters and methods, we can further increase offset + 1
// if the first parameter is an instance of Page.
//
// Finally, if the JS function is called with _more_ parameters and
// the last parameter in Zig is an array, we'll try to slurp the additional
// parameters into the array.
fn getArgs(self: *const Caller, comptime Struct: type, comptime named_function: NamedFunction, comptime offset: usize, info: anytype) !ParamterTypes(@TypeOf(@field(Struct, named_function.name))) {
const context = self.context;
const F = @TypeOf(@field(Struct, named_function.name));
var args: ParamterTypes(F) = undefined;
const params = @typeInfo(F).@"fn".params[offset..];
// Except for the constructor, the first parameter is always `self`
// This isn't something we'll bind from JS, so skip it.
const params_to_map = blk: {
if (params.len == 0) {
return args;
}
// If the last parameter is the Page, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime isPage(params[params.len - 1].type.?)) {
@field(args, tupleFieldName(params.len - 1 + offset)) = self.context.page;
break :blk params[0 .. params.len - 1];
}
// If the last parameter is a special JsThis, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime params[params.len - 1].type.? == js.This) {
@field(args, tupleFieldName(params.len - 1 + offset)) = .{ .obj = .{
.context = context,
.js_obj = info.getThis(),
} };
// AND the 2nd last parameter is state
if (params.len > 1 and comptime isPage(params[params.len - 2].type.?)) {
@field(args, tupleFieldName(params.len - 2 + offset)) = self.context.page;
break :blk params[0 .. params.len - 2];
}
break :blk params[0 .. params.len - 1];
}
// we have neither a Page nor a JsObject. All params must be
// bound to a JavaScript value.
break :blk params;
};
if (params_to_map.len == 0) {
return args;
}
const js_parameter_count = info.length();
const last_js_parameter = params_to_map.len - 1;
var is_variadic = false;
{
// This is going to get complicated. If the last Zig parameter
// is a slice AND the corresponding javascript parameter is
// NOT an an array, then we'll treat it as a variadic.
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
const last_parameter_type_info = @typeInfo(last_parameter_type);
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
const slice_type = last_parameter_type_info.pointer.child;
const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter)));
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
is_variadic = true;
if (js_parameter_count == 0) {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
} else if (js_parameter_count >= params_to_map.len) {
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
for (arr, last_js_parameter..) |*a, i| {
const js_value = info.getArg(@as(u32, @intCast(i)));
a.* = try context.jsValueToZig(named_function, slice_type, js_value);
}
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
} else {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
}
}
}
}
inline for (params_to_map, 0..) |param, i| {
const field_index = comptime i + offset;
if (comptime i == params_to_map.len - 1) {
if (is_variadic) {
break;
}
}
if (comptime isPage(param.type.?)) {
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ named_function.full_name);
} else if (comptime param.type.? == js.This) {
@compileError("JsThis must be the last parameter: " ++ named_function.full_name);
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument;
}
@field(args, tupleFieldName(field_index)) = null;
} else {
const js_value = info.getArg(@as(u32, @intCast(i)));
@field(args, tupleFieldName(field_index)) = context.jsValueToZig(named_function, param.type.?, js_value) catch {
return error.InvalidArgument;
};
}
}
return args;
}
// This is extracted to speed up compilation. When left inlined in handleError,
// this can add as much as 10 seconds of compilation time.
fn logFunctionCallError(self: *Caller, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void {
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
log.info(.js, "function call error", .{
.name = function_name,
.err = err,
.args = args_dump,
.stack = self.context.stackTrace() catch |err1| @errorName(err1),
});
}
fn serializeFunctionArgs(self: *Caller, info: v8.FunctionCallbackInfo) ![]const u8 {
const separator = log.separator();
const js_parameter_count = info.length();
const context = self.context;
var arr: std.ArrayListUnmanaged(u8) = .{};
for (0..js_parameter_count) |i| {
const js_value = info.getArg(@intCast(i));
const value_string = try context.valueToDetailString(js_value);
const value_type = try context.jsStringToZig(try js_value.typeOf(self.isolate), .{});
try std.fmt.format(arr.writer(context.call_arena), "{s}{d}: {s} ({s})", .{
separator,
i + 1,
value_string,
value_type,
});
}
return arr.items;
}
// We want the function name, or more precisely, the "Struct.function" for
// displaying helpful @compileError.
// However, there's no way to get the name from a std.Builtin.Fn, so we create
// a NamedFunction as part of our binding, and pass it around incase we need
// to display an error
pub const NamedFunction = struct {
name: []const u8,
js_name: []const u8,
full_name: []const u8,
pub fn init(comptime Struct: type, comptime name: []const u8) NamedFunction {
return .{
.name = name,
.js_name = if (name[0] == '_') name[1..] else name,
.full_name = @typeName(Struct) ++ "." ++ name,
};
}
};
// Takes a function, and returns a tuple for its argument. Used when we
// @call a function
fn ParamterTypes(comptime F: type) type {
const params = @typeInfo(F).@"fn".params;
var fields: [params.len]std.builtin.Type.StructField = undefined;
inline for (params, 0..) |param, i| {
fields[i] = .{
.name = tupleFieldName(i),
.type = param.type.?,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(param.type.?),
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.fields = &fields,
.is_tuple = true,
} });
}
fn tupleFieldName(comptime i: usize) [:0]const u8 {
return switch (i) {
0 => "0",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "6",
7 => "7",
8 => "8",
9 => "9",
else => std.fmt.comptimePrint("{d}", .{i}),
};
}
fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page;
}
fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value {
return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg));
}

1894
src/browser/js/Context.zig Normal file

File diff suppressed because it is too large Load Diff

539
src/browser/js/Env.zig Normal file
View File

@@ -0,0 +1,539 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const types = @import("types.zig");
const Types = types.Types;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const Platform = @import("Platform.zig");
const Inspector = @import("Inspector.zig");
const ExecutionWorld = @import("ExecutionWorld.zig");
const NamedFunction = Caller.NamedFunction;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
// The Env maps to a V8 isolate, which represents a isolated sandbox for
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
// and it's where we'll start ExecutionWorlds, which actually execute JavaScript.
// The `S` parameter is arbitrary state. When we start an ExecutionWorld, an instance
// of S must be given. This instance is available to any Zig binding.
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
const Env = @This();
allocator: Allocator,
platform: *const Platform,
// the global isolate
isolate: v8.Isolate,
// just kept around because we need to free it on deinit
isolate_params: *v8.CreateParams,
// Given a type, we can lookup its index in TYPE_LOOKUP and then have
// access to its TunctionTemplate (the thing we need to create an instance
// of it)
// I.e.:
// const index = @field(TYPE_LOOKUP, @typeName(type_name))
// const template = templates[index];
templates: [Types.len]v8.FunctionTemplate,
// Given a type index (retrieved via the TYPE_LOOKUP), we can retrieve
// the index of its prototype. Types without a prototype have their own
// index.
prototype_lookup: [Types.len]u16,
meta_lookup: [Types.len]types.Meta,
context_id: usize,
const Opts = struct {};
pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
// var params = v8.initCreateParams();
var params = try allocator.create(v8.CreateParams);
errdefer allocator.destroy(params);
v8.c.v8__Isolate__CreateParams__CONSTRUCT(params);
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
var isolate = v8.Isolate.init(params);
errdefer isolate.deinit();
// This is the callback that runs whenever a module is dynamically imported.
isolate.setHostImportModuleDynamicallyCallback(Context.dynamicModuleCallback);
isolate.setPromiseRejectCallback(promiseRejectCallback);
isolate.setMicrotasksPolicy(v8.c.kExplicit);
isolate.enter();
errdefer isolate.exit();
isolate.setHostInitializeImportMetaObjectCallback(Context.metaObjectCallback);
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
const env = try allocator.create(Env);
errdefer allocator.destroy(env);
env.* = .{
.context_id = 0,
.platform = platform,
.isolate = isolate,
.templates = undefined,
.allocator = allocator,
.isolate_params = params,
.meta_lookup = undefined,
.prototype_lookup = undefined,
};
// Populate our templates lookup. generateClass creates the
// v8.FunctionTemplate, which we store in our env.templates.
// The ordering doesn't matter. What matters is that, given a type
// we can get its index via: @field(types.LOOKUP, type_name)
const templates = &env.templates;
inline for (Types, 0..) |s, i| {
@setEvalBranchQuota(10_000);
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, generateClass(s.defaultValue().?, isolate)).castToFunctionTemplate();
}
// Above, we've created all our our FunctionTemplates. Now that we
// have them all, we can hook up the prototypes.
const meta_lookup = &env.meta_lookup;
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) {
const TI = @typeInfo(Struct.prototype);
const proto_name = @typeName(types.Receiver(TI.pointer.child));
if (@hasField(types.Lookup, proto_name) == false) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ proto_name, @typeName(Struct) }));
}
// Hey, look! This is our first real usage of the types.LOOKUP.
// Just like we said above, given a type, we can get its
// template index.
const proto_index = @field(types.LOOKUP, proto_name);
templates[i].inherit(templates[proto_index]);
}
// while we're here, let's populate our meta lookup
const subtype: ?types.Sub = if (@hasDecl(Struct, "subtype")) Struct.subtype else null;
const proto_offset = comptime blk: {
if (!@hasField(Struct, "proto")) {
break :blk 0;
}
const proto_info = std.meta.fieldInfo(Struct, .proto);
if (@typeInfo(proto_info.type) == .pointer) {
// we store the offset as a negative, to so that,
// when we reverse this, we know that it's
// behind a pointer that we need to resolve.
break :blk -@offsetOf(Struct, "proto");
}
break :blk @offsetOf(Struct, "proto");
};
meta_lookup[i] = .{
.index = i,
.subtype = subtype,
.proto_offset = proto_offset,
};
}
return env;
}
pub fn deinit(self: *Env) void {
self.isolate.exit();
self.isolate.deinit();
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
self.allocator.destroy(self.isolate_params);
self.allocator.destroy(self);
}
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector {
return Inspector.init(arena, self.isolate, ctx);
}
pub fn runMicrotasks(self: *const Env) void {
self.isolate.performMicrotasksCheckpoint();
}
pub fn pumpMessageLoop(self: *const Env) bool {
return self.platform.inner.pumpMessageLoop(self.isolate, false);
}
pub fn runIdleTasks(self: *const Env) void {
return self.platform.inner.runIdleTasks(self.isolate, 1);
}
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
return .{
.env = self,
.context = null,
.context_arena = ArenaAllocator.init(self.allocator),
};
}
// V8 doesn't immediately free memory associated with
// a Context, it's managed by the garbage collector. We use the
// `lowMemoryNotification` call on the isolate to encourage v8 to free
// any contexts which have been freed.
pub fn lowMemoryNotification(self: *Env) void {
var handle_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&handle_scope, self.isolate);
defer handle_scope.deinit();
self.isolate.lowMemoryNotification();
}
pub fn dumpMemoryStats(self: *Env) void {
const stats = self.isolate.getHeapStatistics();
std.debug.print(
\\ Total Heap Size: {d}
\\ Total Heap Size Executable: {d}
\\ Total Physical Size: {d}
\\ Total Available Size: {d}
\\ Used Heap Size: {d}
\\ Heap Size Limit: {d}
\\ Malloced Memory: {d}
\\ External Memory: {d}
\\ Peak Malloced Memory: {d}
\\ Number Of Native Contexts: {d}
\\ Number Of Detached Contexts: {d}
\\ Total Global Handles Size: {d}
\\ Used Global Handles Size: {d}
\\ Zap Garbage: {any}
\\
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
}
fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
const msg = v8.PromiseRejectMessage.initFromC(v8_msg);
const isolate = msg.getPromise().toObject().getIsolate();
const context = Context.fromIsolate(isolate);
const value =
if (msg.getValue()) |v8_value| context.valueToString(v8_value, .{}) catch |err| @errorName(err) else "no value";
log.debug(.js, "unhandled rejection", .{ .value = value });
}
// Give it a Zig struct, get back a v8.FunctionTemplate.
// The FunctionTemplate is a bit like a struct container - it's where
// we'll attach functions/getters/setters and where we'll "inherit" a
// prototype type (if there is any)
fn generateClass(comptime Struct: type, isolate: v8.Isolate) v8.FunctionTemplate {
const template = generateConstructor(Struct, isolate);
attachClass(Struct, isolate, template);
return template;
}
// Normally this is called from generateClass. Where generateClass creates
// the constructor (hence, the FunctionTemplate), attachClass adds all
// of its functions, getters, setters, ...
// But it's extracted from generateClass because we also have 1 global
// object (i.e. the Window), which gets attached not only to the Window
// constructor/FunctionTemplate as normal, but also through the default
// FunctionTemplate of the isolate (in createContext)
pub fn attachClass(comptime Struct: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
const template_proto = template.getPrototypeTemplate();
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
const name = declaration.name;
if (comptime name[0] == '_') {
switch (@typeInfo(@TypeOf(@field(Struct, name)))) {
.@"fn" => generateMethod(Struct, name, isolate, template_proto),
else => |ti| if (!comptime js.isComplexAttributeType(ti)) {
generateAttribute(Struct, name, isolate, template, template_proto);
},
}
} else if (comptime std.mem.startsWith(u8, name, "get_")) {
generateProperty(Struct, name[4..], isolate, template_proto);
} else if (comptime std.mem.startsWith(u8, name, "static_")) {
generateFunction(Struct, name[7..], isolate, template);
}
}
if (@hasDecl(Struct, "get_symbol_toStringTag") == false) {
// If this WAS defined, then we would have created it in generateProperty.
// But if it isn't, we create a default one
const string_tag_callback = v8.FunctionTemplate.initCallback(isolate, struct {
fn stringTag(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
const class_name = v8.String.initUtf8(info.getIsolate(), comptime js.classNameForStruct(Struct));
info.getReturnValue().set(class_name);
}
}.stringTag);
const key = v8.Symbol.getToStringTag(isolate).toName();
template_proto.setAccessorGetter(key, string_tag_callback);
}
generateIndexer(Struct, template_proto);
generateNamedIndexer(Struct, template.getInstanceTemplate());
generateUndetectable(Struct, template.getInstanceTemplate());
}
// Even if a struct doesn't have a `constructor` function, we still
// `generateConstructor`, because this is how we create our
// FunctionTemplate. Such classes exist, but they can't be instantiated
// via `new ClassName()` - but they could, for example, be created in
// Zig and returned from a function call, which is why we need the
// FunctionTemplate.
fn generateConstructor(comptime Struct: type, isolate: v8.Isolate) v8.FunctionTemplate {
const template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
// See comment above. We generateConstructor on all types
// in order to create the FunctionTemplate, but there might
// not be an actual "constructor" function. So if someone
// does `new ClassName()` where ClassName doesn't have
// a constructor function, we'll return an error.
if (@hasDecl(Struct, "constructor") == false) {
const iso = caller.isolate;
log.warn(.js, "Illegal constructor call", .{ .name = @typeName(Struct) });
const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
info.getReturnValue().set(js_exception);
return;
}
// Safe to call now, because if Struct.constructor didn't
// exist, the above if block would have returned.
const named_function = comptime NamedFunction.init(Struct, "constructor");
caller.constructor(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
if (comptime types.isEmpty(types.Receiver(Struct)) == false) {
// If the struct is empty, we won't store a Zig reference inside
// the JS object, so we don't need to set the internal field count
template.getInstanceTemplate().setInternalFieldCount(1);
}
const class_name = v8.String.initUtf8(isolate, comptime js.classNameForStruct(Struct));
template.setClassName(class_name);
return template;
}
fn generateMethod(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void {
var js_name: v8.Name = undefined;
if (comptime std.mem.eql(u8, name, "_symbol_iterator")) {
js_name = v8.Symbol.getIterator(isolate).toName();
} else {
js_name = v8.String.initUtf8(isolate, name[1..]).toName();
}
const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, name);
caller.method(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
}
fn generateFunction(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
const js_name = v8.String.initUtf8(isolate, name).toName();
const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "static_" ++ name);
caller.function(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
template.set(js_name, function_template, v8.PropertyAttribute.None);
}
fn generateAttribute(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void {
const zig_value = @field(Struct, name);
const js_value = js.simpleZigValueToJs(isolate, zig_value, true);
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
// apply it both to the type itself
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
// and to instances of the type
template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
}
fn generateProperty(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void {
var js_name: v8.Name = undefined;
if (comptime std.mem.eql(u8, name, "symbol_toStringTag")) {
js_name = v8.Symbol.getToStringTag(isolate).toName();
} else {
js_name = v8.String.initUtf8(isolate, name).toName();
}
const getter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "get_" ++ name);
caller.method(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
const setter_name = "set_" ++ name;
if (@hasDecl(Struct, setter_name) == false) {
template_proto.setAccessorGetter(js_name, getter_callback);
return;
}
const setter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
std.debug.assert(info.length() == 1);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "set_" ++ name);
caller.method(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
template_proto.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
}
fn generateIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
if (@hasDecl(Struct, "indexed_get") == false) {
return;
}
const configuration = v8.IndexedPropertyHandlerConfiguration{
.getter = struct {
fn callback(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "indexed_get");
return caller.getIndex(Struct, named_function, idx, info) catch |err| blk: {
caller.handleError(Struct, named_function, err, info);
break :blk v8.Intercepted.No;
};
}
}.callback,
};
// If you're trying to implement setter, read:
// https://groups.google.com/g/v8-users/c/8tahYBsHpgY/m/IteS7Wn2AAAJ
// The issue I had was
// (a) where to attache it: does it go on the instance_template
// instead of the prototype?
// (b) defining the getter or query to respond with the
// PropertyAttribute to indicate if the property can be set
template_proto.setIndexedProperty(configuration, null);
}
fn generateNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
if (@hasDecl(Struct, "named_get") == false) {
return;
}
var configuration = v8.NamedPropertyHandlerConfiguration{
.getter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "named_get");
return caller.getNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
caller.handleError(Struct, named_function, err, info);
break :blk v8.Intercepted.No;
};
}
}.callback,
// This is really cool. Without this, we'd intercept _all_ properties
// even those explicitly set. So, node.length for example would get routed
// to our `named_get`, rather than a `get_length`. This might be
// useful if we run into a type that we can't model properly in Zig.
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
};
if (@hasDecl(Struct, "named_set")) {
configuration.setter = struct {
fn callback(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "named_set");
return caller.setNamedIndex(Struct, named_function, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info) catch |err| blk: {
caller.handleError(Struct, named_function, err, info);
break :blk v8.Intercepted.No;
};
}
}.callback;
}
if (@hasDecl(Struct, "named_delete")) {
configuration.deleter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "named_delete");
return caller.deleteNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
caller.handleError(Struct, named_function, err, info);
break :blk v8.Intercepted.No;
};
}
}.callback;
}
template_proto.setNamedProperty(configuration, null);
}
fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void {
const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction");
if (has_js_call_as_function) {
template.setCallAsFunctionHandler(struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "jsCallAsFunction");
caller.method(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
}
if (@hasDecl(Struct, "mark_as_undetectable") and Struct.mark_as_undetectable) {
if (!has_js_call_as_function) {
@compileError(@typeName(Struct) ++ ": mark_as_undetectable required jsCallAsFunction to be defined. This is a hard-coded requirement in V8, because mark_as_undetectable only exists for HTMLAllCollection which is also callable.");
}
template.markAsUndetectable();
}
}

View File

@@ -0,0 +1,251 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const Page = @import("../page.zig").Page;
const ScriptManager = @import("../ScriptManager.zig");
const types = @import("types.zig");
const Types = types.Types;
const Env = @import("Env.zig");
const Context = @import("Context.zig");
const ArenaAllocator = std.heap.ArenaAllocator;
const CONTEXT_ARENA_RETAIN = 1024 * 64;
// ExecutionWorld closely models a JS World.
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#World
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
const ExecutionWorld = @This();
env: *Env,
// Arena whose lifetime is for a single page load. Where
// the call_arena lives for a single function call, the context_arena
// lives for the lifetime of the entire page. The allocator will be
// owned by the Context, but the arena itself is owned by the ExecutionWorld
// so that we can re-use it from context to context.
context_arena: ArenaAllocator,
// Currently a context maps to a Browser's Page. Here though, it's only a
// mechanism to organization page-specific memory. The ExecutionWorld
// does all the work, but having all page-specific data structures
// grouped together helps keep things clean.
context: ?Context = null,
// no init, must be initialized via env.newExecutionWorld()
pub fn deinit(self: *ExecutionWorld) void {
if (self.context != null) {
self.removeContext();
}
self.context_arena.deinit();
}
// Only the top Context in the Main ExecutionWorld should hold a handle_scope.
// A v8.HandleScope is like an arena. Once created, any "Local" that
// v8 creates will be released (or at least, releasable by the v8 GC)
// when the handle_scope is freed.
// We also maintain our own "context_arena" which allows us to have
// all page related memory easily managed.
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_callback: ?js.GlobalMissingCallback) !*Context {
std.debug.assert(self.context == null);
const env = self.env;
const isolate = env.isolate;
const Global = @TypeOf(page.window);
const templates = &self.env.templates;
var v8_context: v8.Context = blk: {
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
const js_global = v8.FunctionTemplate.initDefault(isolate);
Env.attachClass(Global, isolate, js_global);
const global_template = js_global.getInstanceTemplate();
global_template.setInternalFieldCount(1);
// Configure the missing property callback on the global
// object.
if (global_callback != null) {
const configuration = v8.NamedPropertyHandlerConfiguration{
.getter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
const context = Context.fromIsolate(info.getIsolate());
const property = context.valueToString(.{ .handle = c_name.? }, .{}) catch "???";
if (context.global_callback.?.missing(property, context)) {
return v8.Intercepted.Yes;
}
return v8.Intercepted.No;
}
}.callback,
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
};
global_template.setNamedProperty(configuration, null);
}
// All the FunctionTemplates that we created and setup in Env.init
// are now going to get associated with our global instance.
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
const class_name = v8.String.initUtf8(isolate, comptime js.classNameForStruct(Struct));
global_template.set(class_name.toName(), templates[i], v8.PropertyAttribute.None);
}
// The global object (Window) has already been hooked into the v8
// engine when the Env was initialized - like every other type.
// But the V8 global is its own FunctionTemplate instance so even
// though it's also a Window, we need to set the prototype for this
// specific instance of the the Window.
if (@hasDecl(Global, "prototype")) {
const proto_type = types.Receiver(@typeInfo(Global.prototype).pointer.child);
const proto_name = @typeName(proto_type);
const proto_index = @field(types.LOOKUP, proto_name);
js_global.inherit(templates[proto_index]);
}
const context_local = v8.Context.init(isolate, global_template, null);
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
v8_context.enter();
errdefer if (enter) v8_context.exit();
defer if (!enter) v8_context.exit();
// This shouldn't be necessary, but it is:
// https://groups.google.com/g/v8-users/c/qAQQBmbi--8
// TODO: see if newer V8 engines have a way around this.
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) {
const proto_type = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
const proto_name = @typeName(proto_type);
if (@hasField(types.Lookup, proto_name) == false) {
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name);
}
const proto_index = @field(types.LOOKUP, proto_name);
const proto_obj = templates[proto_index].getFunction(v8_context).toObject();
const self_obj = templates[i].getFunction(v8_context).toObject();
_ = self_obj.setPrototype(v8_context, proto_obj);
}
}
break :blk v8_context;
};
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
var handle_scope: ?v8.HandleScope = null;
if (enter) {
handle_scope = @as(v8.HandleScope, undefined);
v8.HandleScope.init(&handle_scope.?, isolate);
}
errdefer if (enter) handle_scope.?.deinit();
{
// If we want to overwrite the built-in console, we have to
// delete the built-in one.
const js_obj = v8_context.getGlobal();
const console_key = v8.String.initUtf8(isolate, "console");
if (js_obj.deleteValue(v8_context, console_key) == false) {
return error.ConsoleDeleteError;
}
}
const context_id = env.context_id;
env.context_id = context_id + 1;
self.context = Context{
.page = page,
.id = context_id,
.isolate = isolate,
.v8_context = v8_context,
.templates = &env.templates,
.meta_lookup = &env.meta_lookup,
.handle_scope = handle_scope,
.script_manager = &page.script_manager,
.call_arena = page.call_arena,
.arena = self.context_arena.allocator(),
.global_callback = global_callback,
};
var context = &self.context.?;
{
// Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out
const data = isolate.initBigIntU64(@intCast(@intFromPtr(context)));
v8_context.setEmbedderData(1, data);
}
// Custom exception
// NOTE: there is no way in v8 to subclass the Error built-in type
// TODO: this is an horrible hack
inline for (Types) |s| {
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "ErrorSet")) {
const script = comptime js.classNameForStruct(Struct) ++ ".prototype.__proto__ = Error.prototype";
_ = try context.exec(script, "errorSubclass");
}
}
// Primitive attributes are set directly on the FunctionTemplate
// when we setup the environment. But we cannot set more complex
// types (v8 will crash).
//
// Plus, just to create more complex types, we always need a
// context, i.e. an Array has to have a Context to exist.
//
// As far as I can tell, getting the FunctionTemplate's object
// and setting values directly on it, for each context, is the
// way to do this.
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
const name = declaration.name;
if (comptime name[0] == '_') {
const value = @field(Struct, name);
if (comptime js.isComplexAttributeType(@typeInfo(@TypeOf(value)))) {
const js_obj = templates[i].getFunction(v8_context).toObject();
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
const js_val = try context.zigValueToJs(value);
if (!js_obj.setValue(v8_context, js_name, js_val)) {
log.fatal(.app, "set class attribute", .{
.@"struct" = @typeName(Struct),
.name = name,
});
}
}
}
}
}
try context.setupGlobal();
return context;
}
pub fn removeContext(self: *ExecutionWorld) void {
// Force running the micro task to drain the queue before reseting the
// context arena.
// Tasks in the queue are relying to the arena memory could be present in
// the queue. Running them later could lead to invalid memory accesses.
self.env.runMicrotasks();
self.context.?.deinit();
self.context = null;
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
}
pub fn terminateExecution(self: *const ExecutionWorld) void {
self.env.isolate.terminateExecution();
}
pub fn resumeExecution(self: *const ExecutionWorld) void {
self.env.isolate.cancelTerminateExecution();
}

144
src/browser/js/Function.zig Normal file
View File

@@ -0,0 +1,144 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const PersistentFunction = v8.Persistent(v8.Function);
const Allocator = std.mem.Allocator;
const Function = @This();
id: usize,
context: *js.Context,
this: ?v8.Object = null,
func: PersistentFunction,
pub const Result = struct {
stack: ?[]const u8,
exception: []const u8,
};
pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 {
const name = self.func.castToFunction().getName();
return self.context.valueToString(name, .{ .allocator = allocator });
}
pub fn setName(self: *const Function, name: []const u8) void {
const v8_name = v8.String.initUtf8(self.context.isolate, name);
self.func.castToFunction().setName(v8_name);
}
pub fn withThis(self: *const Function, value: anytype) !Function {
const this_obj = if (@TypeOf(value) == js.Object)
value.js_obj
else
(try self.context.zigValueToJs(value)).castTo(v8.Object);
return .{
.id = self.id,
.this = this_obj,
.func = self.func,
.context = self.context,
};
}
pub fn newInstance(self: *const Function, result: *Result) !js.Object {
const context = self.context;
var try_catch: js.TryCatch = undefined;
try_catch.init(context);
defer try_catch.deinit();
// This creates a new instance using this Function as a constructor.
// This returns a generic Object
const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse {
if (try_catch.hasCaught()) {
const allocator = context.call_arena;
result.stack = try_catch.stack(allocator) catch null;
result.exception = (try_catch.exception(allocator) catch "???") orelse "???";
} else {
result.stack = null;
result.exception = "???";
}
return error.JsConstructorFailed;
};
return .{
.context = context,
.js_obj = js_obj,
};
}
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
return self.callWithThis(T, self.getThis(), args);
}
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T {
return self.tryCallWithThis(T, self.getThis(), args, result);
}
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !T {
var try_catch: js.TryCatch = undefined;
try_catch.init(self.context);
defer try_catch.deinit();
return self.callWithThis(T, this, args) catch |err| {
if (try_catch.hasCaught()) {
const allocator = self.context.call_arena;
result.stack = try_catch.stack(allocator) catch null;
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
} else {
result.stack = null;
result.exception = @errorName(err);
}
return err;
};
}
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
const context = self.context;
const js_this = try context.valueToExistingObject(this);
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
const js_args: []const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
.@"struct" => |s| blk: {
const fields = s.fields;
var js_args: [fields.len]v8.Value = undefined;
inline for (fields, 0..) |f, i| {
js_args[i] = try context.zigValueToJs(@field(aargs, f.name));
}
const cargs: [fields.len]v8.Value = js_args;
break :blk &cargs;
},
.pointer => blk: {
var values = try context.call_arena.alloc(v8.Value, args.len);
for (args, 0..) |a, i| {
values[i] = try context.zigValueToJs(a);
}
break :blk values;
},
else => @compileError("JS Function called with invalid paremter type"),
};
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
if (result == null) {
return error.JSExecCallback;
}
if (@typeInfo(T) == .void) return {};
const named_function = comptime Caller.NamedFunction.init(T, "callResult");
return context.jsValueToZig(named_function, T, result.?);
}
fn getThis(self: *const Function) v8.Object {
return self.this orelse self.context.v8_context.getGlobal();
}
pub fn src(self: *const Function) ![]const u8 {
const value = self.func.castToFunction().toValue();
return self.context.valueToString(value, .{});
}

View File

@@ -0,0 +1,125 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Context = @import("Context.zig");
const Allocator = std.mem.Allocator;
const Inspector = @This();
pub const RemoteObject = v8.RemoteObject;
isolate: v8.Isolate,
inner: *v8.Inspector,
session: v8.InspectorSession,
// We expect allocator to be an arena
pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector {
const ContextT = @TypeOf(ctx);
const InspectorContainer = switch (@typeInfo(ContextT)) {
.@"struct" => ContextT,
.pointer => |ptr| ptr.child,
.void => NoopInspector,
else => @compileError("invalid context type"),
};
// If necessary, turn a void context into something we can safely ptrCast
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
const channel = v8.InspectorChannel.init(safe_context, InspectorContainer.onInspectorResponse, InspectorContainer.onInspectorEvent, isolate);
const client = v8.InspectorClient.init();
const inner = try allocator.create(v8.Inspector);
v8.Inspector.init(inner, client, channel, isolate);
return .{ .inner = inner, .isolate = isolate, .session = inner.connect() };
}
pub fn deinit(self: *const Inspector) void {
self.session.deinit();
self.inner.deinit();
}
pub fn send(self: *const Inspector, msg: []const u8) void {
// Can't assume the main Context exists (with its HandleScope)
// available when doing this. Pages (and thus the HandleScope)
// comes and goes, but CDP can keep sending messages.
const isolate = self.isolate;
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
self.session.dispatchProtocolMessage(isolate, msg);
}
// From CDP docs
// https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-ExecutionContextDescription
// ----
// - name: Human readable name describing given context.
// - origin: Execution context origin (ie. URL who initialised the request)
// - auxData: Embedder-specific auxiliary data likely matching
// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
// - is_default_context: Whether the execution context is default, should match the auxData
pub fn contextCreated(
self: *const Inspector,
context: *const Context,
name: []const u8,
origin: []const u8,
aux_data: ?[]const u8,
is_default_context: bool,
) void {
self.inner.contextCreated(context.v8_context, name, origin, aux_data, is_default_context);
}
// Retrieves the RemoteObject for a given value.
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
// just like a method return value. Therefore, if we've mapped this
// value before, we'll get the existing JS PersistedObject and if not
// we'll create it and track it for cleanup when the context ends.
pub fn getRemoteObject(
self: *const Inspector,
context: *Context,
group: []const u8,
value: anytype,
) !RemoteObject {
const js_value = try context.zigValueToJs(value);
// We do not want to expose this as a parameter for now
const generate_preview = false;
return self.session.wrapObject(
context.isolate,
context.v8_context,
js_value,
group,
generate_preview,
);
}
// Gets a value by object ID regardless of which context it is in.
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !?*anyopaque {
const unwrapped = try self.session.unwrapObject(allocator, object_id);
// The values context and groupId are not used here
const toa = getTaggedAnyOpaque(unwrapped.value) orelse return null;
if (toa.subtype == null or toa.subtype != .node) return error.ObjectIdIsNotANode;
return toa.ptr;
}
const NoopInspector = struct {
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
};
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
if (value.isObject() == false) {
return null;
}
const obj = value.castTo(v8.Object);
if (obj.internalFieldCount() == 0) {
return null;
}
const external_data = obj.getInternalField(0).castTo(v8.External).get().?;
return @ptrCast(@alignCast(external_data));
}

149
src/browser/js/Object.zig Normal file
View File

@@ -0,0 +1,149 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const PersistentObject = v8.Persistent(v8.Object);
const Allocator = std.mem.Allocator;
const Object = @This();
js_obj: v8.Object,
context: *js.Context,
pub const SetOpts = packed struct(u32) {
READ_ONLY: bool = false,
DONT_ENUM: bool = false,
DONT_DELETE: bool = false,
_: u29 = 0,
};
pub fn setIndex(self: Object, index: u32, value: anytype, opts: SetOpts) !void {
@setEvalBranchQuota(10000);
const key = switch (index) {
inline 0...20 => |i| std.fmt.comptimePrint("{d}", .{i}),
else => try std.fmt.allocPrint(self.context.arena, "{d}", .{index}),
};
return self.set(key, value, opts);
}
pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{ FailedToSet, OutOfMemory }!void {
const context = self.context;
const js_key = v8.String.initUtf8(context.isolate, key);
const js_value = try context.zigValueToJs(value);
const res = self.js_obj.defineOwnProperty(context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false;
if (!res) {
return error.FailedToSet;
}
}
pub fn get(self: Object, key: []const u8) !js.Value {
const context = self.context;
const js_key = v8.String.initUtf8(context.isolate, key);
const js_val = try self.js_obj.getValue(context.v8_context, js_key);
return context.createValue(js_val);
}
pub fn isTruthy(self: Object) bool {
const js_value = self.js_obj.toValue();
return js_value.toBool(self.context.isolate);
}
pub fn toString(self: Object) ![]const u8 {
const js_value = self.js_obj.toValue();
return self.context.valueToString(js_value, .{});
}
pub fn toDetailString(self: Object) ![]const u8 {
const js_value = self.js_obj.toValue();
return self.context.valueToDetailString(js_value);
}
pub fn format(self: Object, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
return writer.writeAll(try self.toString());
}
pub fn toJson(self: Object, allocator: Allocator) ![]u8 {
const json_string = try v8.Json.stringify(self.context.v8_context, self.js_obj.toValue(), null);
const str = try self.context.jsStringToZig(json_string, .{ .allocator = allocator });
return str;
}
pub fn persist(self: Object) !Object {
var context = self.context;
const js_obj = self.js_obj;
const persisted = PersistentObject.init(context.isolate, js_obj);
try context.js_object_list.append(context.arena, persisted);
return .{
.context = context,
.js_obj = persisted.castToObject(),
};
}
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
if (self.isNullOrUndefined()) {
return null;
}
const context = self.context;
const js_name = v8.String.initUtf8(context.isolate, name);
const js_value = try self.js_obj.getValue(context.v8_context, js_name.toName());
if (!js_value.isFunction()) {
return null;
}
return try context.createFunction(js_value);
}
pub fn isNull(self: Object) bool {
return self.js_obj.toValue().isNull();
}
pub fn isUndefined(self: Object) bool {
return self.js_obj.toValue().isUndefined();
}
pub fn triState(self: Object, comptime Struct: type, comptime name: []const u8, comptime T: type) !TriState(T) {
if (self.isNull()) {
return .{ .null = {} };
}
if (self.isUndefined()) {
return .{ .undefined = {} };
}
return .{ .value = try self.toZig(Struct, name, T) };
}
pub fn isNullOrUndefined(self: Object) bool {
return self.js_obj.toValue().isNullOrUndefined();
}
pub fn nameIterator(self: Object) js.ValueIterator {
const context = self.context;
const js_obj = self.js_obj;
const array = js_obj.getPropertyNames(context.v8_context);
const count = array.length();
return .{
.count = count,
.context = context,
.js_obj = array.castTo(v8.Object),
};
}
pub fn toZig(self: Object, comptime Struct: type, comptime name: []const u8, comptime T: type) !T {
const named_function = comptime Caller.NamedFunction.init(Struct, name);
return self.context.jsValueToZig(named_function, T, self.js_obj.toValue());
}
pub fn TriState(comptime T: type) type {
return union(enum) {
null: void,
undefined: void,
value: T,
};
}

View File

@@ -0,0 +1,21 @@
const js = @import("js.zig");
const v8 = js.v8;
const Platform = @This();
inner: v8.Platform,
pub fn init() !Platform {
if (v8.initV8ICU() == false) {
return error.FailedToInitializeICU;
}
const platform = v8.Platform.initDefault(0, true);
v8.initV8Platform(platform);
v8.initV8();
return .{ .inner = platform };
}
pub fn deinit(self: Platform) void {
_ = v8.deinitV8();
v8.deinitV8Platform();
self.inner.deinit();
}

25
src/browser/js/This.zig Normal file
View File

@@ -0,0 +1,25 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
// This only exists so that we know whether a function wants the opaque
// JS argument (js.Object), or if it wants the receiver as an opaque
// value.
// js.Object is normally used when a method wants an opaque JS object
// that it'll pass into a callback.
// This is used when the function wants to do advanced manipulation
// of the v8.Object bound to the instance. For example, postAttach is an
// example of using This.
const This = @This();
obj: js.Object,
pub fn setIndex(self: This, index: u32, value: anytype, opts: js.Object.SetOpts) !void {
return self.obj.setIndex(index, value, opts);
}
pub fn set(self: This, key: []const u8, value: anytype, opts: js.Object.SetOpts) !void {
return self.obj.set(key, value, opts);
}

View File

@@ -0,0 +1,64 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
const TryCatch = @This();
inner: v8.TryCatch,
context: *const js.Context,
pub fn init(self: *TryCatch, context: *const js.Context) void {
self.context = context;
self.inner.init(context.isolate);
}
pub fn hasCaught(self: TryCatch) bool {
return self.inner.hasCaught();
}
// the caller needs to deinit the string returned
pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 {
const msg = self.inner.getException() orelse return null;
return try self.context.valueToString(msg, .{ .allocator = allocator });
}
// the caller needs to deinit the string returned
pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
const context = self.context;
const s = self.inner.getStackTrace(context.v8_context) orelse return null;
return try context.valueToString(s, .{ .allocator = allocator });
}
// the caller needs to deinit the string returned
pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 {
const context = self.context;
const msg = self.inner.getMessage() orelse return null;
const sl = msg.getSourceLine(context.v8_context) orelse return null;
return try context.jsStringToZig(sl, .{ .allocator = allocator });
}
pub fn sourceLineNumber(self: TryCatch) ?u32 {
const context = self.context;
const msg = self.inner.getMessage() orelse return null;
return msg.getLineNumber(context.v8_context);
}
// a shorthand method to return either the entire stack message
// or just the exception message
// - in Debug mode return the stack if available
// - otherwise return the exception if available
// the caller needs to deinit the string returned
pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 {
if (comptime @import("builtin").mode == .Debug) {
if (try self.stack(allocator)) |msg| {
return msg;
}
}
return try self.exception(allocator);
}
pub fn deinit(self: *TryCatch) void {
self.inner.deinit();
}

View File

@@ -190,7 +190,7 @@ test "generate: Union" {
const value = Union(.{ Astruct, Bstruct, .{Cstruct} });
const ti = @typeInfo(value).@"union";
try std.testing.expectEqual(3, ti.fields.len);
try std.testing.expectEqualStrings("*runtime.generate.test.generate: Union.Astruct.Other", @typeName(ti.fields[0].type));
try std.testing.expectEqualStrings("*browser.js.generate.test.generate: Union.Astruct.Other", @typeName(ti.fields[0].type));
try std.testing.expectEqualStrings(ti.fields[0].name, "Astruct");
try std.testing.expectEqual(*Bstruct, ti.fields[1].type);
try std.testing.expectEqualStrings(ti.fields[1].name, "Bstruct");

499
src/browser/js/js.zig Normal file
View File

@@ -0,0 +1,499 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
pub const v8 = @import("v8");
const types = @import("types.zig");
const log = @import("../../log.zig");
const Page = @import("../page.zig").Page;
const Allocator = std.mem.Allocator;
pub const Env = @import("Env.zig");
pub const ExecutionWorld = @import("ExecutionWorld.zig");
pub const Context = @import("Context.zig");
pub const Inspector = @import("Inspector.zig");
// TODO: Is "This" really necessary?
pub const This = @import("This.zig");
pub const Object = @import("Object.zig");
pub const TryCatch = @import("TryCatch.zig");
pub const Function = @import("Function.zig");
const Caller = @import("Caller.zig");
const NamedFunction = Context.NamedFunction;
// If a function returns a []i32, should that map to a plain-old
// JavaScript array, or a Int32Array? It's ambiguous. By default, we'll
// map arrays/slices to the JavaScript arrays. If you want a TypedArray
// wrap it in this.
// Also, this type has nothing to do with the Env. But we place it here
// for consistency. Want a callback? Env.Callback. Want a JsObject?
// Env.JsObject. Want a TypedArray? Env.TypedArray.
pub fn TypedArray(comptime T: type) type {
return struct {
pub const _TYPED_ARRAY_ID_KLUDGE = true;
values: []const T,
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
return .{ .values = try allocator.dupe(T, self.values) };
}
};
}
pub const PromiseResolver = struct {
context: *Context,
resolver: v8.PromiseResolver,
pub fn promise(self: PromiseResolver) Promise {
return self.resolver.getPromise();
}
pub fn resolve(self: PromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
// resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.resolve(context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToResolvePromise;
}
}
pub fn reject(self: PromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
// resolver.reject will return null if the promise isn't pending
const ok = self.resolver.reject(context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToRejectPromise;
}
}
};
pub const PersistentPromiseResolver = struct {
context: *Context,
resolver: v8.Persistent(v8.PromiseResolver),
pub fn deinit(self: *PersistentPromiseResolver) void {
self.resolver.deinit();
}
pub fn promise(self: PersistentPromiseResolver) Promise {
return self.resolver.castToPromiseResolver().getPromise();
}
pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
// resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToResolvePromise;
}
}
pub fn reject(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
// resolver.reject will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToRejectPromise;
}
}
};
pub const Promise = v8.Promise;
// When doing jsValueToZig, string ([]const u8) are managed by the
// call_arena. That means that if the API wants to persist the string
// (which is relatively common), it needs to dupe it again.
// If the parameter is an Env.String rather than a []const u8, then
// the page's arena will be used (rather than the call arena).
pub const String = struct {
string: []const u8,
};
pub const Exception = struct {
inner: v8.Value,
context: *const Context,
// the caller needs to deinit the string returned
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.inner, .{ .allocator = allocator });
}
};
pub const Value = struct {
value: v8.Value,
context: *const Context,
// the caller needs to deinit the string returned
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.value, .{ .allocator = allocator });
}
pub fn fromJson(ctx: *Context, json: []const u8) !Value {
const json_string = v8.String.initUtf8(ctx.isolate, json);
const value = try v8.Json.parse(ctx.v8_context, json_string);
return Value{ .context = ctx, .value = value };
}
};
pub const ValueIterator = struct {
count: u32,
idx: u32 = 0,
js_obj: v8.Object,
context: *const Context,
pub fn next(self: *ValueIterator) !?Value {
const idx = self.idx;
if (idx == self.count) {
return null;
}
self.idx += 1;
const context = self.context;
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx);
return context.createValue(js_val);
}
};
pub fn UndefinedOr(comptime T: type) type {
return union(enum) {
undefined: void,
value: T,
};
}
// An interface for types that want to have their jsScopeEnd function be
// called when the call context ends
const CallScopeEndCallback = struct {
ptr: *anyopaque,
callScopeEndFn: *const fn (ptr: *anyopaque) void,
fn init(ptr: anytype) CallScopeEndCallback {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn callScopeEnd(pointer: *anyopaque) void {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.pointer.child.jsCallScopeEnd(self);
}
};
return .{
.ptr = ptr,
.callScopeEndFn = gen.callScopeEnd,
};
}
pub fn callScopeEnd(self: CallScopeEndCallback) void {
self.callScopeEndFn(self.ptr);
}
};
// Callback called on global's property missing.
// Return true to intercept the execution or false to let the call
// continue the chain.
pub const GlobalMissingCallback = struct {
ptr: *anyopaque,
missingFn: *const fn (ptr: *anyopaque, name: []const u8, ctx: *Context) bool,
pub fn init(ptr: anytype) GlobalMissingCallback {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn missing(pointer: *anyopaque, name: []const u8, ctx: *Context) bool {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.pointer.child.missing(self, name, ctx);
}
};
return .{
.ptr = ptr,
.missingFn = gen.missing,
};
}
pub fn missing(self: GlobalMissingCallback, name: []const u8, ctx: *Context) bool {
return self.missingFn(self.ptr, name, ctx);
}
};
// Attributes that return a primitive type are setup directly on the
// FunctionTemplate when the Env is setup. More complex types need a v8.Context
// and cannot be set directly on the FunctionTemplate.
// We default to saying types are primitives because that's mostly what
// we have. If we add a new complex type that isn't explictly handled here,
// we'll get a compiler error in simpleZigValueToJs, and can then explicitly
// add the type here.
pub fn isComplexAttributeType(ti: std.builtin.Type) bool {
return switch (ti) {
.array => true,
else => false,
};
}
// These are simple types that we can convert to JS with only an isolate. This
// is separated from the Caller's zigValueToJs to make it available when we
// don't have a caller (i.e., when setting static attributes on types)
pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool) if (fail) v8.Value else ?v8.Value {
switch (@typeInfo(@TypeOf(value))) {
.void => return v8.initUndefined(isolate).toValue(),
.null => return v8.initNull(isolate).toValue(),
.bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)),
.int => |n| switch (n.signedness) {
.signed => {
if (value >= -2_147_483_648 and value <= 2_147_483_647) {
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
}
if (comptime n.bits <= 64) {
return v8.getValue(v8.BigInt.initI64(isolate, @intCast(value)));
}
@compileError(@typeName(value) ++ " is not supported");
},
.unsigned => {
if (value <= 4_294_967_295) {
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
}
if (comptime n.bits <= 64) {
return v8.getValue(v8.BigInt.initU64(isolate, @intCast(value)));
}
@compileError(@typeName(value) ++ " is not supported");
},
},
.comptime_int => {
if (value >= 0) {
if (value <= 4_294_967_295) {
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
}
return v8.BigInt.initU64(isolate, @intCast(value)).toValue();
}
if (value >= -2_147_483_648) {
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
}
return v8.BigInt.initI64(isolate, @intCast(value)).toValue();
},
.comptime_float => return v8.Number.init(isolate, value).toValue(),
.float => |f| switch (f.bits) {
64 => return v8.Number.init(isolate, value).toValue(),
32 => return v8.Number.init(isolate, @floatCast(value)).toValue(),
else => @compileError(@typeName(value) ++ " is not supported"),
},
.pointer => |ptr| {
if (ptr.size == .slice and ptr.child == u8) {
return v8.String.initUtf8(isolate, value).toValue();
}
if (ptr.size == .one) {
const one_info = @typeInfo(ptr.child);
if (one_info == .array and one_info.array.child == u8) {
return v8.String.initUtf8(isolate, value).toValue();
}
}
},
.array => return simpleZigValueToJs(isolate, &value, fail),
.optional => {
if (value) |v| {
return simpleZigValueToJs(isolate, v, fail);
}
return v8.initNull(isolate).toValue();
},
.@"struct" => {
const T = @TypeOf(value);
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
const values = value.values;
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
const len = values.len;
const bits = switch (@typeInfo(value_type)) {
.int => |n| n.bits,
.float => |f| f.bits,
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
};
var array_buffer: v8.ArrayBuffer = undefined;
if (len == 0) {
array_buffer = v8.ArrayBuffer.init(isolate, 0);
} else {
const buffer_len = len * bits / 8;
const backing_store = v8.BackingStore.init(isolate, buffer_len);
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
}
switch (@typeInfo(value_type)) {
.int => |n| switch (n.signedness) {
.unsigned => switch (n.bits) {
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
.signed => switch (n.bits) {
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
},
.float => |f| switch (f.bits) {
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
else => {},
}
// We normally don't fail in this function unless fail == true
// but this can never be valid.
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
}
},
.@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail),
.@"enum" => {
const T = @TypeOf(value);
if (@hasDecl(T, "toString")) {
return simpleZigValueToJs(isolate, value.toString(), fail);
}
},
else => {},
}
if (fail) {
@compileError("Unsupported Zig type " ++ @typeName(@TypeOf(value)));
}
return null;
}
pub fn _createException(isolate: v8.Isolate, msg: []const u8) v8.Value {
return v8.Exception.initError(v8.String.initUtf8(isolate, msg));
}
pub fn classNameForStruct(comptime Struct: type) []const u8 {
if (@hasDecl(Struct, "js_name")) {
return Struct.js_name;
}
@setEvalBranchQuota(10_000);
const full_name = @typeName(Struct);
const last = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse return full_name;
return full_name[last + 1 ..];
}
// When we return a Zig object to V8, we put it on the heap and pass it into
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
// function parameter, we know what type it _should_ be. Above, in Caller.method
// (for example), we know all the parameter types. So if a Zig function takes
// a single parameter (its receiver), we know what that type is.
//
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
// to the parameter type:
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
//
// But there are 2 reasons we can't do that.
//
// == Reason 1 ==
// The JS code might pass the wrong type:
//
// var cat = new Cat();
// cat.setOwner(new Cat());
//
// The zig _setOwner method expects the 2nd parameter to be an *Owner, but
// the JS code passed a *Cat.
//
// To solve this issue, we tag every returned value so that we can check what
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
//
// == Reason 2 ==
// Because of prototype inheritance, even "correct" code can be a challenge. For
// example, say the above JavaScript is fixed:
//
// var cat = new Cat();
// cat.setOwner(new Owner("Leto"));
//
// The issue is that setOwner might not expect an *Owner, but rather a
// *Person, which is the prototype for Owner. Now our Zig code is expecting
// a *Person, but it was (correctly) given an *Owner.
// For this reason, we also store the prototype's type index.
//
// One of the prototype mechanisms that we support is via composition. Owner
// can have a "proto: *Person" field. For this reason, we also store the offset
// of the proto field, so that, given an intFromPtr(*Owner) we can access its
// proto field.
//
// The other prototype mechanism that we support is for netsurf, where we just
// cast one type to another. In this case, we'll store an offset of -1 (as a
// sentinel to indicate that we should just cast directly).
pub const TaggedAnyOpaque = struct {
// The type of object this is. The type is captured as an index, which
// corresponds to both a field in TYPE_LOOKUP and the index of
// PROTOTYPE_TABLE
index: u16,
// Ptr to the Zig instance. Between the context where it's called (i.e.
// we have the comptime parameter info for all functions), and the index field
// we can figure out what type this is.
ptr: *anyopaque,
// When we're asked to describe an object via the Inspector, we _must_ include
// the proper subtype (and description) fields in the returned JSON.
// V8 will give us a Value and ask us for the subtype. From the v8.Value we
// can get a v8.Object, and from the v8.Object, we can get out TaggedAnyOpaque
// which is where we store the subtype.
subtype: ?types.Sub,
};
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
// included (e.g. in the wpt build).
// This is called from V8. Whenever the v8 inspector has to describe a value
// it'll call this function to gets its [optional] subtype - which, from V8's
// point of view, is an arbitrary string.
pub export fn v8_inspector__Client__IMPL__valueSubtype(
_: *v8.c.InspectorClientImpl,
c_value: *const v8.C_Value,
) callconv(.c) [*c]const u8 {
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
return if (external_entry.subtype) |st| @tagName(st) else null;
}
// Same as valueSubType above, but for the optional description field.
// From what I can tell, some drivers _need_ the description field to be
// present, even if it's empty. So if we have a subType for the value, we'll
// put an empty description.
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
_: *v8.c.InspectorClientImpl,
v8_context: *const v8.C_Context,
c_value: *const v8.C_Value,
) callconv(.c) [*c]const u8 {
_ = v8_context;
// We _must_ include a non-null description in order for the subtype value
// to be included. Besides that, I don't know if the value has any meaning
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
return if (external_entry.subtype == null) null else "";
}

183
src/browser/js/types.zig Normal file
View File

@@ -0,0 +1,183 @@
const std = @import("std");
const generate = @import("generate.zig");
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("../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,
@import("../storage/storage.zig").Interfaces,
@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("../fetch/fetch.zig").Interfaces,
@import("../streams/streams.zig").Interfaces,
});
pub const Types = @typeInfo(Interfaces).@"struct".fields;
// Imagine we have a type Cat which has a getter:
//
// fn get_owner(self: *Cat) *Owner {
// return self.owner;
// }
//
// When we execute caller.getter, we'll end up doing something like:
// const res = @call(.auto, Cat.get_owner, .{cat_instance});
//
// How do we turn `res`, which is an *Owner, into something we can return
// to v8? We need the ObjectTemplate associated with Owner. How do we
// get that? Well, we store all the ObjectTemplates in an array that's
// tied to env. So we do something like:
//
// env.templates[index_of_owner].initInstance(...);
//
// But how do we get that `index_of_owner`? `Lookup` is a struct
// that looks like:
//
// const Lookup = struct {
// comptime cat: usize = 0,
// comptime owner: usize = 1,
// ...
// }
//
// So to get the template index of `owner`, we can do:
//
// const index_id = @field(type_lookup, @typeName(@TypeOf(res));
//
pub const Lookup = blk: {
var fields: [Types.len]std.builtin.Type.StructField = undefined;
for (Types, 0..) |s, i| {
// This prototype type check has nothing to do with building our
// Lookup. But we put it here, early, so that the rest of the
// code doesn't have to worry about checking if Struct.prototype is
// a pointer.
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype") and @typeInfo(Struct.prototype) != .pointer) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s} must be a pointer", .{ @typeName(Struct.prototype), @typeName(Struct) }));
}
fields[i] = .{
.name = @typeName(Receiver(Struct)),
.type = usize,
.is_comptime = true,
.alignment = @alignOf(usize),
.default_value_ptr = &i,
};
}
break :blk @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.is_tuple = false,
.fields = &fields,
} });
};
pub const LOOKUP = Lookup{};
// Creates a list where the index of a type contains its prototype index
// const Animal = struct{};
// const Cat = struct{
// pub const prototype = *Animal;
// };
//
// Would create an array: [0, 0]
// Animal, at index, 0, has no prototype, so we set it to itself
// Cat, at index 1, has an Animal prototype, so we set it to 0.
//
// When we're trying to pass an argument to a Zig function, we'll know the
// target type (the function parameter type), and we'll have a
// TaggedAnyOpaque which will have the index of the type of that parameter.
// We'll use the PROTOTYPE_TABLE to see if the TaggedAnyType should be
// cast to a prototype.
pub const PROTOTYPE_TABLE = blk: {
var table: [Types.len]u16 = undefined;
for (Types, 0..) |s, i| {
var prototype_index = i;
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) {
const TI = @typeInfo(Struct.prototype);
const proto_name = @typeName(Receiver(TI.pointer.child));
prototype_index = @field(LOOKUP, proto_name);
}
table[i] = prototype_index;
}
break :blk table;
};
// This is essentially meta data for each type. Each is stored in env.meta_lookup
// The index for a type can be retrieved via:
// const index = @field(TYPE_LOOKUP, @typeName(Receiver(Struct)));
// const meta = env.meta_lookup[index];
pub const Meta = struct {
// Every type is given a unique index. That index is used to lookup various
// things, i.e. the prototype chain.
index: u16,
// We store the type's subtype here, so that when we create an instance of
// the type, and bind it to JavaScript, we can store the subtype along with
// the created TaggedAnyOpaque.s
subtype: ?Sub,
// If this type has composition-based prototype, represents the byte-offset
// from ptr where the `proto` field is located. A negative offsets is used
// to indicate that the prototype field is behind a pointer.
proto_offset: i32,
};
pub const Sub = enum {
@"error",
array,
arraybuffer,
dataview,
date,
generator,
iterator,
map,
node,
promise,
proxy,
regexp,
set,
typedarray,
wasmvalue,
weakmap,
weakset,
webassemblymemory,
};
// When we map a Zig instance into a JsObject, we'll normally store the a
// TaggedAnyOpaque (TAO) inside of the JsObject's internal field. This requires
// ensuring that the instance template has an InternalFieldCount of 1. However,
// for empty objects, we don't need to store the TAO, because we can't just cast
// one empty object to another, so for those, as an optimization, we do not set
// the InternalFieldCount.
pub fn isEmpty(comptime T: type) bool {
return @typeInfo(T) != .@"opaque" and @sizeOf(T) == 0 and @hasDecl(T, "js_legacy_factory") == false;
}
// If we have a struct:
// const Cat = struct {
// pub fn meow(self: *Cat) void { ... }
// }
// Then obviously, the receiver of its methods are going to be a *Cat (or *const Cat)
//
// However, we can also do:
// const Cat = struct {
// pub const Self = OtherImpl;
// pub fn meow(self: *OtherImpl) void { ... }
// }
// In which case, as we see above, the receiver is derived from the Self declaration
pub fn Receiver(comptime Struct: type) type {
return if (@hasDecl(Struct, "Self")) Struct.Self else Struct;
}

View File

@@ -26,14 +26,9 @@ const c = @cImport({
@cInclude("mimalloc.h");
});
const Error = error{
HeapNotNull,
HeapNull,
};
var heap: ?*c.mi_heap_t = null;
pub fn create() Error!void {
pub fn create() void {
std.debug.assert(heap == null);
heap = c.mi_heap_new();
std.debug.assert(heap != null);
@@ -45,6 +40,45 @@ pub fn destroy() void {
heap = null;
}
pub fn getRSS() i64 {
if (@import("builtin").mode != .Debug) {
// just don't trust my implementation, plus a caller might not know
// that this requires parsing some unstructured data
@compileError("Only available in debug builds");
}
var buf: [1024 * 8]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var writer = std.Io.Writer.Allocating.init(fba.allocator());
c.mi_stats_print_out(struct {
fn print(msg: [*c]const u8, data: ?*anyopaque) callconv(.c) void {
const w: *std.Io.Writer = @ptrCast(@alignCast(data.?));
w.writeAll(std.mem.span(msg)) catch |err| {
std.debug.print("Failed to write mimalloc data: {}\n", .{err});
};
}
}.print, &writer.writer);
const data = writer.written();
const index = std.mem.indexOf(u8, data, "rss: ") orelse return -1;
const sep = std.mem.indexOfScalarPos(u8, data, index + 5, ' ') orelse return -2;
const value = std.fmt.parseFloat(f64, data[index + 5 .. sep]) catch return -3;
const unit = data[sep + 1 ..];
if (std.mem.startsWith(u8, unit, "KiB,")) {
return @as(i64, @intFromFloat(value)) * 1024;
}
if (std.mem.startsWith(u8, unit, "MiB,")) {
return @as(i64, @intFromFloat(value)) * 1024 * 1024;
}
if (std.mem.startsWith(u8, unit, "GiB,")) {
return @as(i64, @intFromFloat(value)) * 1024 * 1024 * 1024;
}
return -4;
}
pub export fn m_alloc(size: usize) callconv(.c) ?*anyopaque {
std.debug.assert(heap != null);
return c.mi_heap_malloc(heap.?, size);

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