137 Commits

Author SHA1 Message Date
Muki Kiboigo
cb141c35b7 create proper navigate event 2025-11-11 21:01:27 -08:00
Muki Kiboigo
01c2f2c6ea wip navigate event 2025-11-11 20:20:39 -08:00
muki
43b210dcf5 Merge pull request #1200 from lightpanda-io/location-set-hash
add `set_hash` to Location
2025-11-11 20:13:40 -08:00
Muki Kiboigo
16e7c0841d handle empty hashes in Location 2025-11-10 06:52:14 -08:00
Pierre Tachoire
2f2870c066 Merge pull request #1201 from lightpanda-io/devtools
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
update zig-v8
2025-11-10 14:51:21 +01:00
Pierre Tachoire
9c277ae26e add debugger placeholders 2025-11-10 09:47:58 +01:00
Muki Kiboigo
19b9ba8601 add hash support to URL stitch 2025-11-05 10:11:12 -08:00
Muki Kiboigo
92ddb5640d new NavigationEventTarget on new page 2025-11-05 10:08:37 -08:00
Muki Kiboigo
38c6a9bd9d changeLocation on nav 2025-11-05 10:08:37 -08:00
Muki Kiboigo
3cc53b579b add location set hash tests 2025-11-05 10:08:37 -08:00
Muki Kiboigo
c009669ec8 properly handle replace navigation case 2025-11-05 10:08:37 -08:00
Muki Kiboigo
0e3f18367a add set_hash to Location 2025-11-05 10:08:37 -08:00
Halil Durak
47ceabc43f Merge pull request #1195 from lightpanda-io/nikneym/blob-simd
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
2025-11-04 19:42:20 +03:00
Karl Seguin
dc4927d49e Merge pull request #1191 from lightpanda-io/refactor_script_manager
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
Refactor the ScriptManager
2025-11-04 22:35:43 +08:00
Pierre Tachoire
bc29fce41a Merge pull request #1198 from lightpanda-io/nikneym/url-trim
Trim CR and LF characters from both ends
2025-11-04 15:32:45 +01:00
Halil Durak
97c92d7873 replace trimmed_path with path 2025-11-04 17:24:45 +03:00
Karl Seguin
68fbe742eb Update src/browser/ScriptManager.zig
Co-authored-by: Pierre Tachoire <pierre@lightpanda.io>
2025-11-04 22:22:13 +08:00
Karl Seguin
5b08188b93 Merge pull request #1196 from lightpanda-io/nikneym/tiny-changes
Tiny changes
2025-11-04 21:46:14 +08:00
Halil Durak
aa884803e3 trim CR and LF characters from both ends 2025-11-04 16:42:37 +03:00
Karl Seguin
f9087d3840 ignore errorCallback on shutdown 2025-11-04 20:11:26 +08:00
Halil Durak
0fab9be5c2 queueMicrotask should not return a timer ID 2025-11-04 14:39:05 +03:00
Pierre Tachoire
53c73c5851 Merge pull request #1189 from lightpanda-io/cdp-browser-permissions
cdp: add browser permissions noop
2025-11-04 12:12:40 +01:00
Halil Durak
996837ab0c return an empty origin and protocol string if url not provided 2025-11-04 13:30:55 +03:00
Halil Durak
74a5438587 update Blob test 2025-11-04 13:07:23 +03:00
Halil Durak
1fd28cef40 add vectorized line endings scanner 2025-11-04 13:07:02 +03:00
Karl Seguin
7c825cbe82 fix segfault on http error callback 2025-11-04 10:40:53 +08:00
muki
40522d8720 Merge pull request #1192 from lightpanda-io/wpt-navigation
Some checks failed
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Update wpt submodule for Navigation
2025-11-03 09:17:21 -08:00
Pierre Tachoire
2446580db9 update zig-v8 2025-11-03 17:47:35 +01:00
Karl Seguin
70e02dcfc7 Refactor the ScriptManager
This PR introduces two major changes to the ScriptManager.

1 - Simplification.
Rather than having a `Script`, `PendingScript`, `AsyncModule` and `SyncModule`,
there is only a `Script`, with an added `mode` union. All of the previous
objects had the same behavior (collect the response in a buffer), up to the
point of execution, which is where the mode comes in.

2 - Correctness
Whether or not the previous version was "incorrect", it was difficult to use
correctly. Specifically, the previous version would execute async scripts and
async modules as soon as they're done. That seems allowed, but it caused issues
with module loading in Context.js. Specifically, between compiling and
instantiating a module, or between instantiation and evaluation, an async script
or module could be evaluated. It isn't clear whether v8 allows that, but if it
does, it introduces a lot of new potential states (specifically, unexpected
changes to the v8.Module's status) that we have to handle.

This version only evaluate scripts in the `evaluate`, which doesn't allow
recursive calls (so a waitForImport, which continues to pump the HTTP loop, can
never result in `evaluate` being called again).

This undoes the change made in https://github.com/lightpanda-io/browser/pull/1158
because I do not think it's possible to have multiple waiters waiting for the
same (or even different) modules. The linked issue points to a crash in
https://www.nytimes.com which doesn't crash with this version.
2025-11-03 20:21:46 +08:00
Muki Kiboigo
235337d1c9 update wpt submodule for Navigation 2025-11-02 20:00:46 -08:00
Karl Seguin
8a867bc9c2 Merge pull request #1190 from lightpanda-io/nikneym/blob
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
`Blob` support
2025-11-01 19:58:18 +08:00
Halil Durak
7aafab9c0a prefer js.resolvePromise helper for promise returns 2025-11-01 13:29:24 +03:00
Halil Durak
10c2d7dc87 remove unnecessary import and declaration 2025-11-01 13:28:27 +03:00
Halil Durak
9b990da7fa update Blob test 2025-10-31 17:18:33 +03:00
Halil Durak
93542c9756 support Blob.slice 2025-10-31 17:18:22 +03:00
Halil Durak
4be7fa178c support Blob.arrayBuffer
Also adds support for `ArrayBuffer` to js.zig.
2025-10-31 17:18:03 +03:00
Halil Durak
5785c147da support Blob type 2025-10-31 17:17:02 +03:00
Halil Durak
b68675bb94 update Blob test 2025-10-30 22:36:07 +03:00
Halil Durak
3307a664c4 prefer writeVec instead of writeAll + writeByte 2025-10-30 22:35:58 +03:00
Halil Durak
dd43be4818 remove method added for testing
This was only required while testing things, not an actual API.
2025-10-30 22:35:37 +03:00
Halil Durak
c491648941 implement various Blob methods
Support for `stream`, `text` and `bytes`.
2025-10-30 22:34:58 +03:00
Halil Durak
1085950b88 initial Blob support 2025-10-30 16:08:03 +03:00
Pierre Tachoire
1d91d24b12 cdp: add browser permissions noop 2025-10-28 15:02:24 +01:00
Pierre Tachoire
cc83d85542 Merge pull request #1188 from lightpanda-io/script-load-order
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
add a test for script load order
2025-10-28 14:12:31 +01:00
Pierre Tachoire
706a87a458 keep consistent queue for inline 2025-10-28 13:12:18 +01:00
Pierre Tachoire
3ec15ad1f7 add a test for script load order 2025-10-28 13:12:18 +01:00
Karl Seguin
07e603ecda Merge pull request #1186 from lightpanda-io/defer-module
module scripts are deferred by default
2025-10-28 18:36:54 +08:00
Pierre Tachoire
52fc2c365f use getList() to pick the right queue w/ inline scripts 2025-10-28 11:23:29 +01:00
Pierre Tachoire
8f3620adf0 modules are deferred by default 2025-10-28 09:17:57 +01:00
Karl Seguin
f7abf0956f Merge pull request #1184 from lightpanda-io/usage-fix
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 --log_filter_scopes usage
2025-10-28 10:07:34 +08:00
Karl Seguin
73217f7832 Merge pull request #1185 from lightpanda-io/fix-script-print-wait-analysis
fix printWaitAnalysis with queue name changes
2025-10-28 10:07:13 +08:00
Pierre Tachoire
52fb2010fc fix printWaitAnalysis with queue name changes 2025-10-27 17:50:30 +01:00
Pierre Tachoire
03ffcdb604 add --log_filter_scopes usage 2025-10-27 17:37:05 +01:00
Karl Seguin
20314fccec Merge pull request #1182 from lightpanda-io/navigation-file-fix
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 build issues related to Navigation
2025-10-27 23:08:01 +08:00
Muki Kiboigo
018e95bea7 rename navigation.zig to navigation/root.zig 2025-10-27 07:48:22 -07:00
Pierre Tachoire
c9dc4ef57a Merge pull request #1144 from lightpanda-io/readme-mac
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
update mac instructions to build from source
2025-10-24 09:59:27 +02:00
Pierre Tachoire
6c9d013e20 update mac instructions to build from source 2025-10-24 09:58:38 +02:00
Pierre Tachoire
d2d10d5db4 Merge pull request #1175 from lightpanda-io/readme-cdp
README: add CDP link
2025-10-24 09:57:15 +02:00
Pierre Tachoire
37a8a24528 README: add CDP link 2025-10-24 09:53:29 +02:00
Pierre Tachoire
d0b83c674c Merge pull request #1138 from lightpanda-io/navigation
add `Navigation` WebAPI
2025-10-24 09:30:09 +02:00
Pierre Tachoire
b58ff2c869 Merge pull request #1171 from lightpanda-io/cdp-lifecycle
support url on createTarget and send lifecycle events
2025-10-24 08:33:11 +02:00
Pierre Tachoire
b2e41837d9 Merge pull request #1174 from lightpanda-io/nikneym/url-can-parse
Add `URL.canParse`
2025-10-24 08:32:25 +02:00
Halil Durak
2e6ec1e23b add URL.canParse test 2025-10-23 13:31:01 +03:00
Halil Durak
7808d12de2 add URL.canParse static method 2025-10-23 13:30:39 +03:00
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
Muki Kiboigo
28ec8d4b94 use page arena in get_sameDocument 2025-10-22 08:42:53 -07:00
Muki Kiboigo
6e42df2e71 set oncurrententrychange callback to null 2025-10-22 08:42:26 -07:00
Muki Kiboigo
6b924e8a4c use toEventTarget in NavigationEventTarget 2025-10-22 07:54:17 -07:00
Muki Kiboigo
80ae3c9fc6 not implemented on Navigation traverseTo 2025-10-22 07:54:10 -07:00
Pierre Tachoire
2422c8718c renderer: set a default box size of 5 pixels 2025-10-22 15:54:43 +02:00
Pierre Tachoire
b5ef8418a6 cdp: fix double createTarget response 2025-10-22 14:18:53 +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
Pierre Tachoire
a69164b482 page: fix page mode when loading about:blank 2025-10-22 12:08:27 +02:00
Halil Durak
d4d35670a0 prefer call_arena in web APIs 2025-10-22 11:42:16 +03:00
Muki Kiboigo
b40e7ece91 no nullable url on Navigation pushEntry 2025-10-21 19:25:24 -07:00
Muki Kiboigo
9c4367b26e check query on eqlDocument 2025-10-21 19:23:19 -07:00
Muki Kiboigo
0eb639ac76 fix navigation shortcut URL stitching 2025-10-21 18:31:41 -07:00
Muki Kiboigo
9778eed1ed clean up Navigation test names 2025-10-21 18:31:41 -07:00
Muki Kiboigo
8b4ffeb911 fix NavigationCurrentEntryChange Constructor 2025-10-21 18:31:41 -07:00
Muki Kiboigo
b55b9bba0a functional NavigationCurrentEntryChangeEvent 2025-10-21 18:31:39 -07:00
Muki Kiboigo
82a45253de add direct event handlers 2025-10-21 18:31:09 -07:00
Muki Kiboigo
4c957041e2 add tests for eqlDocument 2025-10-21 18:31:08 -07:00
Muki Kiboigo
b8f9598de3 add NavigationCurrentEntryChangeEvent 2025-10-21 18:31:07 -07:00
Muki Kiboigo
907bd33d87 split NavigationType and NavigationKind 2025-10-21 18:29:28 -07:00
Muki Kiboigo
e9b08f19cf fix navigation and related tests 2025-10-21 18:29:28 -07:00
Muki Kiboigo
f97697535f History as compat layer over Navigation 2025-10-21 18:29:28 -07:00
Muki Kiboigo
e80c8d5bff add functional Navigation 2025-10-21 18:29:28 -07:00
Muki Kiboigo
70a009a52b add eqlDocument comparison 2025-10-21 18:29:28 -07:00
Muki Kiboigo
8ab9364f19 add ENUM_JS_USE_TAG for enums 2025-10-21 18:29:27 -07:00
Muki Kiboigo
186655e614 initial Navigation scaffolding 2025-10-21 18:29:27 -07:00
Pierre Tachoire
43958b81f8 http: remove inflight conn check
chromiumoxide sends the command while connections are in progress and it
doesn't cause issue w/ curl.
2025-10-21 17:50:11 +02:00
Pierre Tachoire
2d8a95946a cdp: dispatch lifecycle events when enable 2025-10-21 17:48:51 +02:00
Pierre Tachoire
a7c3bad9ad cdp: implement url parameter on createTarget 2025-10-21 17:45:19 +02: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
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
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
79 changed files with 3171 additions and 1107 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

View File

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

View File

@@ -1,7 +1,7 @@
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=14.0.365.4
ARG ZIG_V8=v0.1.33

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:

View File

@@ -18,7 +18,7 @@ Lightpanda is the open-source browser made for headless usage:
- Javascript execution
- Support of Web APIs (partial, WIP)
- Compatible with Playwright[^1], Puppeteer, chromedp through CDP
- Compatible with Playwright[^1], Puppeteer, chromedp through [CDP](https://chromedevtools.github.io/devtools-protocol/)
Fast web automation for AI agents, LLM training, scraping and testing:
@@ -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
@@ -190,10 +190,10 @@ For systems with [Nix](https://nixos.org/download/), you can use the devShell:
nix develop
```
For MacOS, you only need cmake:
For MacOS, you need [Xcode](https://developer.apple.com/xcode/) and the following pacakges from homebrew:
```
brew install cmake
brew install cmake pkgconf
```
### Install and build dependencies

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)) {
@@ -384,6 +384,7 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
try buildMbedtls(b, mod);
try buildNghttp2(b, mod);
try buildCurl(b, mod);
try buildAda(b, mod);
switch (target.result.os.tag) {
.macos => {
@@ -849,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/305bb3706716d32d59b2ffa674731556caa1002b.tar.gz",
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/84cdca7cd9065f67c7933388f2091810fc485bc6.tar.gz",
.hash = "v8-0.0.0-xddH67vcAwCuN2gBsAO8TBzEw523KMroIKGrdZwc-Q-y",
},
//.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

File diff suppressed because it is too large Load Diff

View File

@@ -562,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;

View File

@@ -17,6 +17,7 @@
// 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");
@@ -313,6 +314,11 @@ pub const Document = struct {
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

@@ -34,6 +34,7 @@ pub const Union = union(enum) {
screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
performance: *@import("performance.zig").Performance,
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
navigation: *@import("../navigation/Navigation.zig"),
};
// EventTarget implementation
@@ -82,6 +83,11 @@ pub const EventTarget = struct {
.media_query_list => {
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
},
.navigation => {
const NavigationEventTarget = @import("../navigation/NavigationEventTarget.zig");
const base: *NavigationEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
return .{ .navigation = @fieldParentPtr("proto", base) };
},
}
}

View File

@@ -286,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,

View File

@@ -360,18 +360,30 @@ 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 = parser.nodeGetRootNode(self);
if (page.getNodeState(root)) |state| {
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 {
@@ -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| {

View File

@@ -106,7 +106,6 @@ pub const NodeIterator = struct {
defer self.callbackEnd();
if (self.pointer_before_current) {
self.pointer_before_current = false;
// Unlike TreeWalker, NodeIterator starts at the first node
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
self.pointer_before_current = false;
@@ -116,6 +115,7 @@ 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);
}

View File

@@ -195,7 +195,7 @@ 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();
}

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;
@@ -378,7 +378,7 @@ 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;

View File

@@ -227,7 +227,6 @@ pub const TreeWalker = struct {
continue;
};
if (!result.should_descend) {
// This is an .accept node - return it
self.current_node = result.node;

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

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
const Allocator = std.mem.Allocator;
const log = @import("../../log.zig");
@@ -37,6 +38,9 @@ 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;
const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent;
const NavigateEvent = @import("../navigation/root.zig").NavigateEvent;
// Event interfaces
pub const Interfaces = .{
@@ -48,6 +52,9 @@ pub const Interfaces = .{
ErrorEvent,
MessageEvent,
PopStateEvent,
CompositionEvent,
NavigationCurrentEntryChangeEvent,
NavigateEvent,
};
pub const Union = generate.Union(Interfaces);
@@ -72,10 +79,15 @@ pub const Event = struct {
.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))).* },
.navigation_current_entry_change_event => .{
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
},
.navigate_event => .{ .NavigateEvent = @as(*NavigateEvent, @ptrCast(evt)).* },
};
}
@@ -223,8 +235,6 @@ pub const EventHandler = struct {
node: parser.EventNode,
listener: *parser.EventListener,
const js = @import("../js/js.zig");
pub const Listener = union(enum) {
function: js.Function,
object: js.Object,
@@ -396,6 +406,40 @@ const SignalCallback = struct {
}
};
pub fn DirectEventHandler(
comptime TargetT: type,
target: *TargetT,
event_type: []const u8,
maybe_listener: ?EventHandler.Listener,
cb: *?js.Function,
page_arena: std.mem.Allocator,
) !void {
const event_target = parser.toEventTarget(TargetT, target);
// Check if we have a listener set.
if (cb.*) |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;
cb.* = callback;
return;
},
}
}
// Just unset the listener.
cb.* = null;
}
const testing = @import("../../testing.zig");
test "Browser: Event" {
try testing.htmlRunner("events/event.html");

284
src/browser/file/Blob.zig Normal file
View File

@@ -0,0 +1,284 @@
// 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 Writer = std.Io.Writer;
const Page = @import("../page.zig").Page;
const js = @import("../js/js.zig");
const ReadableStream = @import("../streams/ReadableStream.zig");
/// https://w3c.github.io/FileAPI/#blob-section
/// https://developer.mozilla.org/en-US/docs/Web/API/Blob
const Blob = @This();
/// Immutable slice of blob.
/// Note that another blob may hold a pointer/slice to this,
/// so its better to leave the deallocation of it to arena allocator.
slice: []const u8,
/// MIME attached to blob. Can be an empty string.
mime: []const u8,
const ConstructorOptions = struct {
/// MIME type.
type: []const u8 = "",
/// How to handle line endings (CR and LF).
/// `transparent` means do nothing, `native` expects CRLF (\r\n) on Windows.
endings: []const u8 = "transparent",
};
/// Creates a new Blob.
pub fn constructor(
maybe_blob_parts: ?[]const []const u8,
maybe_options: ?ConstructorOptions,
page: *Page,
) !Blob {
const options: ConstructorOptions = maybe_options orelse .{};
// Setup MIME; This can be any string according to my observations.
const mime: []const u8 = blk: {
const t = options.type;
if (t.len == 0) {
break :blk "";
}
break :blk try page.arena.dupe(u8, t);
};
if (maybe_blob_parts) |blob_parts| {
var w: Writer.Allocating = .init(page.arena);
const use_native_endings = std.mem.eql(u8, options.endings, "native");
try writeBlobParts(&w.writer, blob_parts, use_native_endings);
return .{ .slice = w.written(), .mime = mime };
}
// We don't have `blob_parts`, why would you want a Blob anyway then?
return .{ .slice = "", .mime = mime };
}
const largest_vector = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
/// Array of possible vector sizes for the current arch in decrementing order.
/// We may move this to some file for SIMD helpers in the future.
const vector_sizes = blk: {
// Required for length calculation.
var n: usize = largest_vector;
var total: usize = 0;
while (n != 2) : (n /= 2) total += 1;
// Populate an array with vector sizes.
n = largest_vector;
var i: usize = 0;
var items: [total]usize = undefined;
while (n != 2) : (n /= 2) {
defer i += 1;
items[i] = n;
}
break :blk items;
};
/// Writes blob parts to given `Writer` with desired endings.
fn writeBlobParts(
writer: *Writer,
blob_parts: []const []const u8,
use_native_endings: bool,
) !void {
// Transparent.
if (!use_native_endings) {
for (blob_parts) |part| {
try writer.writeAll(part);
}
return;
}
// TODO: Windows support.
// Linux & Unix.
// Both Firefox and Chrome implement it as such:
// CRLF => LF
// CR => LF
// So even though CR is not followed by LF, it gets replaced.
//
// I believe this is because such scenario is possible:
// ```
// let parts = [ "the quick\r", "\nbrown fox" ];
// ```
// In the example, one should have to check the part before in order to
// understand that CRLF is being presented in the final buffer.
// So they took a simpler approach, here's what given blob parts produce:
// ```
// "the quick\n\nbrown fox"
// ```
scan_parts: for (blob_parts) |part| {
var end: usize = 0;
inline for (vector_sizes) |vector_len| {
const Vec = @Vector(vector_len, u8);
while (end + vector_len <= part.len) : (end += vector_len) {
const cr: Vec = @splat('\r');
// Load chunk as vectors.
const slice = part[end..][0..vector_len];
const chunk: Vec = slice.*;
// Look for CR.
const match = chunk == cr;
// Create a bitset out of match vector.
const bitset = std.bit_set.IntegerBitSet(vector_len){
.mask = @bitCast(@intFromBool(match)),
};
var iter = bitset.iterator(.{});
var relative_start: usize = 0;
while (iter.next()) |index| {
_ = try writer.writeVec(&.{ slice[relative_start..index], "\n" });
if (index + 1 != slice.len and slice[index + 1] == '\n') {
relative_start = index + 2;
} else {
relative_start = index + 1;
}
}
_ = try writer.writeVec(&.{slice[relative_start..]});
}
}
// Scalar scan fallback.
var relative_start: usize = end;
while (end < part.len) {
if (part[end] == '\r') {
_ = try writer.writeVec(&.{ part[relative_start..end], "\n" });
// Part ends with CR. We can continue to next part.
if (end + 1 == part.len) {
continue :scan_parts;
}
// If next char is LF, skip it too.
if (part[end + 1] == '\n') {
relative_start = end + 2;
} else {
relative_start = end + 1;
}
}
end += 1;
}
// Write the remaining. We get this in such situations:
// `the quick brown\rfox`
// `the quick brown\r\nfox`
try writer.writeAll(part[relative_start..end]);
}
}
/// Returns a Promise that resolves with the contents of the blob
/// as binary data contained in an ArrayBuffer.
pub fn _arrayBuffer(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(js.ArrayBuffer{ .values = self.slice });
}
/// Returns a ReadableStream which upon reading returns the data
/// contained within the Blob.
pub fn _stream(self: *const Blob, page: *Page) !*ReadableStream {
const stream = try ReadableStream.constructor(null, null, page);
try stream.queue.append(page.arena, .{
.uint8array = .{ .values = self.slice },
});
return stream;
}
/// Returns a Promise that resolves with a string containing
/// the contents of the blob, interpreted as UTF-8.
pub fn _text(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(self.slice);
}
/// Extension to Blob; works on Firefox and Safari.
/// https://developer.mozilla.org/en-US/docs/Web/API/Blob/bytes
/// Returns a Promise that resolves with a Uint8Array containing
/// the contents of the blob as an array of bytes.
pub fn _bytes(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(js.TypedArray(u8){ .values = self.slice });
}
/// Returns a new Blob object which contains data
/// from a subset of the blob on which it's called.
pub fn _slice(
self: *const Blob,
maybe_start: ?i32,
maybe_end: ?i32,
maybe_content_type: ?[]const u8,
page: *Page,
) !Blob {
const mime: []const u8 = blk: {
if (maybe_content_type) |content_type| {
if (content_type.len == 0) {
break :blk "";
}
break :blk try page.arena.dupe(u8, content_type);
}
break :blk "";
};
const slice = self.slice;
if (maybe_start) |_start| {
const start = blk: {
if (_start < 0) {
break :blk slice.len -| @abs(_start);
}
break :blk @min(slice.len, @as(u31, @intCast(_start)));
};
const end: usize = blk: {
if (maybe_end) |_end| {
if (_end < 0) {
break :blk @max(start, slice.len -| @abs(_end));
}
break :blk @min(slice.len, @max(start, @as(u31, @intCast(_end))));
}
break :blk slice.len;
};
return .{ .slice = slice[start..end], .mime = mime };
}
return .{ .slice = slice, .mime = mime };
}
/// Returns the size of the Blob in bytes.
pub fn get_size(self: *const Blob) usize {
return self.slice.len;
}
/// Returns the type of Blob; likely a MIME type, yet anything can be given.
pub fn get_type(self: *const Blob) []const u8 {
return self.mime;
}
const testing = @import("../../testing.zig");
test "Browser: File.Blob" {
try testing.htmlRunner("file/blob.html");
}

View File

@@ -21,14 +21,12 @@ const std = @import("std");
// https://w3c.github.io/FileAPI/#file-section
const File = @This();
// Very incomplete. The prototype for this is Blob, which we don't have.
// This minimum "implementation" is added because some JavaScript code just
// checks: if (x instanceof File) throw Error(...)
/// TODO: Implement File API.
pub fn constructor() File {
return .{};
}
const testing = @import("../../testing.zig");
test "Browser: File" {
try testing.htmlRunner("xhr/file.html");
test "Browser: File.File" {
try testing.htmlRunner("file/file.html");
}

View File

@@ -0,0 +1,7 @@
//! File API.
//! https://developer.mozilla.org/en-US/docs/Web/API/File_API
pub const Interfaces = .{
@import("./Blob.zig"),
@import("./File.zig"),
};

View File

@@ -21,140 +21,79 @@ const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const Window = @import("window.zig").Window;
// 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 {
pub const ENUM_JS_USE_TAG = true;
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_length(_: *History, page: *Page) u32 {
return @intCast(page.session.navigation.entries.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 set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
self.scroll_restoration = mode;
}
pub fn get_state(self: *History, page: *Page) !?js.Value {
if (self.current) |curr| {
const entry = self.stack.items[curr];
if (entry.state) |state| {
pub fn get_state(_: *History, page: *Page) !?js.Value {
if (page.session.navigation.currentEntry().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 {
pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
const url = try arena.dupe(u8, _url);
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
const entry = HistoryEntry{ .state = null, .url = url };
try self.stack.append(arena, entry);
self.current = self.stack.items.len - 1;
const json = state.toJson(arena) catch return error.DataClone;
_ = try page.session.navigation.pushEntry(url, json, page, true);
}
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 {
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
const entry = page.session.navigation.currentEntry();
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;
entry.state = json;
entry.url = url;
}
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 {
pub fn go(_: *const 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 current = page.session.navigation.index;
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) {
if (index_s < 0 or index_s > page.session.navigation.entries.items.len - 1) {
return;
}
const index = @as(usize, @intCast(index_s));
const entry = self.stack.items[index];
self.current = index;
const entry = page.session.navigation.entries.items[index];
if (try page.isSameOrigin(entry.url)) {
History.dispatchPopStateEvent(entry.state, page);
if (entry.url) |url| {
if (try page.isSameOrigin(url)) {
PopStateEvent.dispatch(entry.state, page);
}
}
try page.navigateFromWebAPI(entry.url, .{ .reason = .history });
_ = try page.session.navigation.navigate(entry.url, .{ .traverse = index }, page);
}
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
@@ -207,9 +146,38 @@ pub const PopStateEvent = struct {
return null;
}
}
pub fn dispatch(state: ?[]const u8, page: *Page) void {
log.debug(.script_event, "dispatch popstate event", .{
.type = "popstate",
.source = "history",
});
var evt = PopStateEvent.constructor("popstate", .{ .state = state }) catch |err| {
log.err(.app, "event constructor error", .{
.err = err,
.type = "popstate",
.source = "history",
});
return;
};
_ = parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, &page.window),
&evt.proto,
) catch |err| {
log.err(.app, "dispatch popstate event error", .{
.err = err,
.type = "popstate",
.source = "history",
});
};
}
};
const testing = @import("../../testing.zig");
test "Browser: HTML.History" {
try testing.htmlRunner("html/history.html");
try testing.htmlRunner("html/history/history.html");
try testing.htmlRunner("html/history/history2.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 {
@@ -195,7 +195,7 @@ pub const HTMLDocument = struct {
}
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script });
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
}
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {

View File

@@ -218,36 +218,36 @@ 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 {
@@ -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);
var u = url(self, page) catch return "";
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();
var u = url(self, page) catch return "";
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 = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_host());
}
pub fn set_host(self: *parser.Anchor, host: []const u8, page: *Page) !void {
var u = try url(self, page);
return try u.get_host(page);
}
defer u.destructor();
try u.set_host(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;
var u = try url(self, page);
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 = url(self, page) catch return "";
defer u.destructor();
const username = u.get_username();
if (username.len == 0) {
return "";
}
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);
return u.get_username();
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);
}
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 href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
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, maybe_password: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
return try page.arena.dupe(u8, u.get_password());
defer u.destructor();
const password = if (maybe_password) |password| password else "";
try u.set_password(password);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
if (v) |vv| {
u.uri.password = .{ .raw = vv };
} else {
u.uri.password = null;
}
const href = try u.toString(arena);
try 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 {
@@ -1354,6 +1350,7 @@ test "Browser: HTML.HtmlScriptElement" {
try testing.htmlRunner("html/script/import.html");
try testing.htmlRunner("html/script/dynamic_import.html");
try testing.htmlRunner("html/script/importmap.html");
try testing.htmlRunner("html/script/order.html");
}
test "Browser: HTML.HtmlSlotElement" {

View File

@@ -42,7 +42,7 @@ pub const ErrorEvent = struct {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .event);
parser.eventSetInternalType(event, .error_event);
const o = opts orelse ErrorEventInit{};

View File

@@ -16,8 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const Uri = @import("std").Uri;
const std = @import("std");
const Page = @import("../page.zig").Page;
const URL = @import("../url/url.zig").URL;
@@ -25,36 +24,55 @@ const URL = @import("../url/url.zig").URL;
pub const Location = struct {
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 const default = Location{
.url = .initWithoutSearchParams(Uri.parse("about:blank") catch unreachable),
};
pub fn init(url: []const u8) !Location {
return .{ .url = try .initForLocation(url) };
}
pub fn get_href(self: *Location, page: *Page) ![]const u8 {
return self.url.get_href(page);
}
pub fn set_href(_: *const Location, href: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(href, .{ .reason = .script });
return page.navigateFromWebAPI(href, .{ .reason = .script }, .{ .push = null });
}
pub fn set_hash(_: *const Location, hash: []const u8, page: *Page) !void {
const normalized_hash = blk: {
if (hash.len == 0) {
const old_url = page.url.raw;
break :blk if (std.mem.indexOfScalar(u8, old_url, '#')) |index|
old_url[0..index]
else
old_url;
} else if (hash[0] == '#')
break :blk hash
else
break :blk try std.fmt.allocPrint(page.arena, "#{s}", .{hash});
};
return page.navigateFromWebAPI(normalized_hash, .{ .reason = .script }, .replace);
}
pub fn get_protocol(self: *Location) []const u8 {
return self.url.get_protocol();
}
pub fn get_host(self: *Location, page: *Page) ![]const u8 {
return self.url.get_host(page);
pub fn get_host(self: *Location) []const u8 {
return self.url.get_host();
}
pub fn get_hostname(self: *Location) []const u8 {
return self.url.get_hostname();
}
pub fn get_port(self: *Location, page: *Page) ![]const u8 {
return self.url.get_port(page);
pub fn get_port(self: *Location) []const u8 {
return self.url.get_port();
}
pub fn get_pathname(self: *Location) []const u8 {
@@ -65,8 +83,8 @@ pub const Location = struct {
return self.url.get_search(page);
}
pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
return self.url.get_hash(page);
pub fn get_hash(self: *Location) []const u8 {
return self.url.get_hash();
}
pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
@@ -74,19 +92,19 @@ pub const Location = struct {
}
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script });
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
}
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script });
return page.navigateFromWebAPI(url, .{ .reason = .script }, .replace);
}
pub fn _reload(_: *const Location, page: *Page) !void {
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script });
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script }, .reload);
}
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
return try self.get_href(page);
return self.get_href(page);
}
};

View File

@@ -25,7 +25,7 @@ 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;
};

View File

@@ -25,6 +25,7 @@ const Page = @import("../page.zig").Page;
const Navigator = @import("navigator.zig").Navigator;
const History = @import("History.zig");
const Navigation = @import("../navigation/Navigation.zig");
const Location = @import("location.zig").Location;
const Crypto = @import("../crypto/crypto.zig").Crypto;
const Console = @import("../console/console.zig").Console;
@@ -41,6 +42,9 @@ 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;
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
// https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
@@ -52,7 +56,7 @@ pub const Window = struct {
document: *parser.DocumentHTML,
target: []const u8 = "",
location: Location = .default,
location: Location,
storage_shelf: ?*storage.Shelf = null,
// counter for having unique timer ids
@@ -68,6 +72,7 @@ pub const Window = struct {
scroll_x: u32 = 0,
scroll_y: u32 = 0,
onload_callback: ?js.Function = null,
onpopstate_callback: ?js.Function = null,
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
var fbs = std.io.fixedBufferStream("");
@@ -78,6 +83,7 @@ pub const Window = struct {
return .{
.document = html_doc,
.target = target orelse "",
.location = try .init("about:blank"),
.navigator = navigator orelse .{},
.performance = Performance.init(),
};
@@ -88,6 +94,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;
@@ -109,31 +119,17 @@ pub const Window = struct {
/// 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);
try DirectEventHandler(Window, self, "load", maybe_listener, &self.onload_callback, page.arena);
}
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;
},
}
/// Returns `onpopstate_callback`.
pub fn get_onpopstate(self: *const Window) ?js.Function {
return self.onpopstate_callback;
}
// Just unset the listener.
self.onload_callback = null;
/// Sets `onpopstate_callback`.
pub fn set_onpopstate(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
try DirectEventHandler(Window, self, "popstate", maybe_listener, &self.onpopstate_callback, page.arena);
}
pub fn get_location(self: *Window) *Location {
@@ -141,7 +137,7 @@ pub const Window = struct {
}
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script });
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
}
// frames return the window itself, but accessing it via a pseudo
@@ -189,6 +185,10 @@ pub const Window = struct {
return &page.session.history;
}
pub fn get_navigation(_: *Window, page: *Page) *Navigation {
return &page.session.navigation;
}
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
pub fn get_innerHeight(_: *Window, page: *Page) u32 {
// We do not have scrollbars or padding so this is the same as Element.clientHeight
@@ -247,8 +247,8 @@ pub const Window = struct {
_ = self.timers.remove(id);
}
pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !u32 {
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !void {
_ = try self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
}
pub fn _setImmediate(self: *Window, cbk: js.Function, page: *Page) !u32 {
@@ -281,6 +281,25 @@ 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: []js.Object = &.{},

View File

@@ -262,7 +262,7 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
gop.key_ptr.* = owned_specifier;
gop.value_ptr.* = .{};
try self.script_manager.?.getModule(owned_specifier, url);
try self.script_manager.?.preloadImport(owned_specifier, url);
}
}
}
@@ -750,9 +750,16 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
unreachable;
},
.@"enum" => |e| {
if (@hasDecl(T, "ENUM_JS_USE_TAG")) {
const str = try self.jsValueToZig(named_function, []const u8, js_value);
return std.meta.stringToEnum(T, str) orelse return error.InvalidEnumValue;
} else {
switch (@typeInfo(e.tag_type)) {
.int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_value, self.v8_context)),
else => @compileError(named_function.full_name ++ " has an unsupported enum parameter type: " ++ @typeName(T)),
else => {
@compileError(named_function.full_name ++ " has an unsupported enum parameter type: " ++ @typeName(T));
},
}
}
},
else => {},
@@ -1206,17 +1213,17 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
// harm in handling this case.
@branchHint(.unlikely);
gop.value_ptr.* = .{};
try self.script_manager.?.getModule(normalized_specifier, referrer_path);
try self.script_manager.?.preloadImport(normalized_specifier, referrer_path);
}
var fetch_result = try self.script_manager.?.waitForModule(normalized_specifier);
defer fetch_result.deinit();
var source = try self.script_manager.?.waitForImport(normalized_specifier);
defer source.deinit();
var try_catch: js.TryCatch = undefined;
try_catch.init(self);
defer try_catch.deinit();
const entry = self.module(true, fetch_result.src(), normalized_specifier, true) catch |err| {
const entry = self.module(true, source.src(), normalized_specifier, true) catch |err| {
switch (err) {
error.EvaluationError => {
// This is a sentinel value telling us that the error was already
@@ -1290,7 +1297,7 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
};
// Next, we need to actually load it.
self.script_manager.?.getAsyncModule(specifier, dynamicModuleSourceCallback, state, referrer) catch |err| {
self.script_manager.?.getAsyncImport(specifier, dynamicModuleSourceCallback, state, referrer) catch |err| {
const error_msg = v8.String.initUtf8(isolate, @errorName(err));
_ = resolver.reject(self.v8_context, error_msg.toValue());
};
@@ -1323,24 +1330,24 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
return promise;
}
fn dynamicModuleSourceCallback(ctx: *anyopaque, fetch_result_: anyerror!ScriptManager.GetResult) void {
fn dynamicModuleSourceCallback(ctx: *anyopaque, module_source_: anyerror!ScriptManager.ModuleSource) void {
const state: *DynamicModuleResolveState = @ptrCast(@alignCast(ctx));
var self = state.context;
var fetch_result = fetch_result_ catch |err| {
var ms = module_source_ catch |err| {
const error_msg = v8.String.initUtf8(self.isolate, @errorName(err));
_ = state.resolver.castToPromiseResolver().reject(self.v8_context, error_msg.toValue());
return;
};
const module_entry = blk: {
defer fetch_result.deinit();
defer ms.deinit();
var try_catch: js.TryCatch = undefined;
try_catch.init(self);
defer try_catch.deinit();
break :blk self.module(true, fetch_result.src(), state.specifier, true) catch {
break :blk self.module(true, ms.src(), state.specifier, true) catch {
const ex = try_catch.exception(self.call_arena) catch |err| @errorName(err) orelse "unknown error";
log.err(.js, "module compilation failed", .{
.specifier = state.specifier,

View File

@@ -28,7 +28,14 @@ pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector
// 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 channel = v8.InspectorChannel.init(
safe_context,
InspectorContainer.onInspectorResponse,
InspectorContainer.onInspectorEvent,
InspectorContainer.onRunMessageLoopOnPause,
InspectorContainer.onQuitMessageLoopOnPause,
isolate,
);
const client = v8.InspectorClient.init();
@@ -109,6 +116,8 @@ pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []con
const NoopInspector = struct {
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {}
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
};
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {

View File

@@ -58,6 +58,10 @@ pub fn TypedArray(comptime T: type) type {
};
}
pub const ArrayBuffer = struct {
values: []const u8,
};
pub const PromiseResolver = struct {
context: *Context,
resolver: v8.PromiseResolver,
@@ -324,6 +328,19 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
},
.@"struct" => {
const T = @TypeOf(value);
if (T == ArrayBuffer) {
const values = value.values;
const len = values.len;
var array_buffer: v8.ArrayBuffer = undefined;
const backing_store = v8.BackingStore.init(isolate, len);
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
return .{ .handle = array_buffer.handle };
}
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
const values = value.values;
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
@@ -378,8 +395,13 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
.@"enum" => {
const T = @TypeOf(value);
if (@hasDecl(T, "toString")) {
// This should be deprecated in favor of the ENUM_JS_USE_TAG.
return simpleZigValueToJs(isolate, value.toString(), fail);
}
if (@hasDecl(T, "ENUM_JS_USE_TAG")) {
return simpleZigValueToJs(isolate, @tagName(value), fail);
}
},
else => {},
}

View File

@@ -16,8 +16,9 @@ const Interfaces = generate.Tuple(.{
@import("../storage/storage.zig").Interfaces,
@import("../url/url.zig").Interfaces,
@import("../xhr/xhr.zig").Interfaces,
@import("../navigation/root.zig").Interfaces,
@import("../file/root.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,

View File

@@ -0,0 +1,328 @@
// 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 URL = @import("../../url.zig").URL;
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler;
const parser = @import("../netsurf.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/Navigation
const Navigation = @This();
const NavigationKind = @import("root.zig").NavigationKind;
const NavigationHistoryEntry = @import("root.zig").NavigationHistoryEntry;
const NavigationTransition = @import("root.zig").NavigationTransition;
const NavigationCurrentEntryChangeEvent = @import("root.zig").NavigationCurrentEntryChangeEvent;
const NavigationEventTarget = @import("NavigationEventTarget.zig");
pub const prototype = *NavigationEventTarget;
proto: NavigationEventTarget = NavigationEventTarget{},
index: usize = 0,
// Need to be stable pointers, because Events can reference entries.
entries: std.ArrayListUnmanaged(*NavigationHistoryEntry) = .empty,
next_entry_id: usize = 0,
pub fn resetForNewPage(self: *Navigation) void {
// libdom will automatically clean this up when a new page is made.
// We must create a new target whenever we create a new page.
self.proto = NavigationEventTarget{};
}
pub fn get_canGoBack(self: *const Navigation) bool {
return self.index > 0;
}
pub fn get_canGoForward(self: *const Navigation) bool {
return self.entries.items.len > self.index + 1;
}
pub fn currentEntry(self: *Navigation) *NavigationHistoryEntry {
return self.entries.items[self.index];
}
pub fn get_currentEntry(self: *Navigation) *NavigationHistoryEntry {
return self.currentEntry();
}
pub fn get_transition(_: *const Navigation) ?NavigationTransition {
// For now, all transitions are just considered complete.
return null;
}
const NavigationReturn = struct {
committed: js.Promise,
finished: js.Promise,
};
pub fn _back(self: *Navigation, page: *Page) !NavigationReturn {
if (!self.get_canGoBack()) {
return error.InvalidStateError;
}
const new_index = self.index - 1;
const next_entry = self.entries.items[new_index];
self.index = new_index;
return self.navigate(next_entry.url, .{ .traverse = new_index }, page);
}
pub fn _entries(self: *const Navigation) []*NavigationHistoryEntry {
return self.entries.items;
}
pub fn _forward(self: *Navigation, page: *Page) !NavigationReturn {
if (!self.get_canGoForward()) {
return error.InvalidStateError;
}
const new_index = self.index + 1;
const next_entry = self.entries.items[new_index];
self.index = new_index;
return self.navigate(next_entry.url, .{ .traverse = new_index }, page);
}
pub fn updateEntries(self: *Navigation, url: []const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void {
switch (kind) {
.replace => {
_ = try self.replaceEntry(url, null, page, dispatch);
},
.push => |state| {
_ = try self.pushEntry(url, state, page, dispatch);
},
.traverse => |index| {
self.index = index;
},
.reload => {},
}
}
// This is for after true navigation processing, where we need to ensure that our entries are up to date.
// This is only really safe to run in the `pageDoneCallback` where we can guarantee that the URL and NavigationKind are correct.
pub fn processNavigation(self: *Navigation, page: *Page) !void {
const url = page.url.raw;
const kind: NavigationKind = page.session.navigation_kind orelse .{ .push = null };
try self.updateEntries(url, kind, page, false);
}
/// Pushes an entry into the Navigation stack WITHOUT actually navigating to it.
/// For that, use `navigate`.
pub fn pushEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry {
const arena = page.session.arena;
const url = try arena.dupe(u8, _url);
// truncates our history here.
if (self.entries.items.len > self.index + 1) {
self.entries.shrinkRetainingCapacity(self.index + 1);
}
const index = self.entries.items.len;
const id = self.next_entry_id;
self.next_entry_id += 1;
const id_str = try std.fmt.allocPrint(arena, "{d}", .{id});
const entry = try arena.create(NavigationHistoryEntry);
entry.* = NavigationHistoryEntry{
.id = id_str,
.key = id_str,
.url = url,
.state = state,
};
// we don't always have a current entry...
const previous = if (self.entries.items.len > 0) self.currentEntry() else null;
try self.entries.append(arena, entry);
if (previous) |prev| {
if (dispatch) {
NavigationCurrentEntryChangeEvent.dispatch(self, prev, .push);
}
}
self.index = index;
return entry;
}
pub fn replaceEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry {
const arena = page.session.arena;
const url = try arena.dupe(u8, _url);
const previous = self.currentEntry();
const id = self.next_entry_id;
self.next_entry_id += 1;
const id_str = try std.fmt.allocPrint(arena, "{d}", .{id});
const entry = try arena.create(NavigationHistoryEntry);
entry.* = NavigationHistoryEntry{
.id = id_str,
.key = id_str,
.url = url,
.state = state,
};
self.entries.items[self.index] = entry;
if (dispatch) {
NavigationCurrentEntryChangeEvent.dispatch(self, previous, .replace);
}
return entry;
}
const NavigateOptions = struct {
const NavigateOptionsHistory = enum {
pub const ENUM_JS_USE_TAG = true;
auto,
push,
replace,
};
state: ?js.Object = null,
info: ?js.Object = null,
history: NavigateOptionsHistory = .auto,
};
pub fn navigate(
self: *Navigation,
_url: ?[]const u8,
kind: NavigationKind,
page: *Page,
) !NavigationReturn {
const arena = page.session.arena;
const url = _url orelse return error.MissingURL;
// https://github.com/WICG/navigation-api/issues/95
//
// These will only settle on same-origin navigation (mostly intended for SPAs).
// It is fine (and expected) for these to not settle on cross-origin requests :)
const committed = try page.js.createPromiseResolver(.page);
const finished = try page.js.createPromiseResolver(.page);
const new_url_string = try URL.stitch(arena, url, page.url.raw, .{});
const new_url = try URL.parse(new_url_string, null);
const is_same_document = try page.url.eqlDocument(&new_url, arena);
switch (kind) {
.push => |state| {
if (is_same_document) {
page.url = new_url;
try committed.resolve({});
// todo: Fire navigate event
try finished.resolve({});
_ = try self.pushEntry(url, state, page, true);
} else {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
}
},
.traverse => |index| {
self.index = index;
if (is_same_document) {
page.url = new_url;
try committed.resolve({});
// todo: Fire navigate event
try finished.resolve({});
} else {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
}
},
.reload => {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
},
else => unreachable,
}
return .{
.committed = committed.promise(),
.finished = finished.promise(),
};
}
pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn {
const opts = _opts orelse NavigateOptions{};
const json = if (opts.state) |state| state.toJson(page.session.arena) catch return error.DataClone else null;
return try self.navigate(_url, .{ .push = json }, page);
}
pub const ReloadOptions = struct {
state: ?js.Object = null,
info: ?js.Object = null,
};
pub fn _reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !NavigationReturn {
const arena = page.session.arena;
const opts = _opts orelse ReloadOptions{};
const entry = self.currentEntry();
if (opts.state) |state| {
const previous = entry;
entry.state = state.toJson(arena) catch return error.DataClone;
NavigationCurrentEntryChangeEvent.dispatch(self, previous, .reload);
}
return self.navigate(entry.url, .reload, page);
}
pub const TraverseToOptions = struct {
info: ?js.Object = null,
};
pub fn _traverseTo(self: *Navigation, key: []const u8, _opts: ?TraverseToOptions, page: *Page) !NavigationReturn {
if (_opts != null) {
log.debug(.browser, "not implemented", .{ .options = _opts });
}
for (self.entries.items, 0..) |entry, i| {
if (std.mem.eql(u8, key, entry.key)) {
return try self.navigate(entry.url, .{ .traverse = i }, page);
}
}
return error.InvalidStateError;
}
pub const UpdateCurrentEntryOptions = struct {
state: js.Object,
};
pub fn _updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions, page: *Page) !void {
const arena = page.session.arena;
const previous = self.currentEntry();
self.currentEntry().state = options.state.toJson(arena) catch return error.DataClone;
NavigationCurrentEntryChangeEvent.dispatch(self, previous, null);
}

View File

@@ -0,0 +1,58 @@
const std = @import("std");
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 parser = @import("../netsurf.zig");
pub const NavigationEventTarget = @This();
pub const prototype = *EventTarget;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .navigation },
oncurrententrychange_cbk: ?js.Function = null,
fn register(
self: *NavigationEventTarget,
alloc: std.mem.Allocator,
typ: []const u8,
listener: EventHandler.Listener,
) !?js.Function {
const target = parser.toEventTarget(NavigationEventTarget, self);
// The only time this can return null if the listener is already
// registered. But before calling `register`, all of our functions
// remove any existing listener, so it should be impossible to get null
// from this function call.
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
return eh.callback;
}
fn unregister(self: *NavigationEventTarget, typ: []const u8, cbk_id: usize) !void {
const et = parser.toEventTarget(NavigationEventTarget, self);
// check if event target has already this listener
const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id);
if (lst == null) {
return;
}
// remove listener
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
}
pub fn get_oncurrententrychange(self: *NavigationEventTarget) ?js.Function {
return self.oncurrententrychange_cbk;
}
pub fn set_oncurrententrychange(self: *NavigationEventTarget, listener: ?EventHandler.Listener, page: *Page) !void {
if (self.oncurrententrychange_cbk) |cbk| try self.unregister("currententrychange", cbk.id);
if (listener) |listen| {
self.oncurrententrychange_cbk = try self.register(page.arena, "currententrychange", listen);
} else {
self.oncurrententrychange_cbk = null;
}
}

View File

@@ -0,0 +1,347 @@
// 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 URL = @import("../../url.zig").URL;
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler;
const parser = @import("../netsurf.zig");
const Navigation = @import("Navigation.zig");
const NavigationEventTarget = @import("NavigationEventTarget.zig");
pub const Interfaces = .{
Navigation,
NavigationEventTarget,
NavigationActivation,
NavigationTransition,
NavigationHistoryEntry,
};
pub const NavigationType = enum {
pub const ENUM_JS_USE_TAG = true;
push,
replace,
traverse,
reload,
};
pub const NavigationKind = union(NavigationType) {
push: ?[]const u8,
replace,
traverse: usize,
reload,
};
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry
pub const NavigationHistoryEntry = struct {
pub const prototype = *EventTarget;
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain },
id: []const u8,
key: []const u8,
url: ?[]const u8,
state: ?[]const u8,
pub fn get_id(self: *const NavigationHistoryEntry) []const u8 {
return self.id;
}
pub fn get_index(self: *const NavigationHistoryEntry, page: *Page) i32 {
const navigation = page.session.navigation;
for (navigation.entries.items, 0..) |entry, i| {
if (std.mem.eql(u8, entry.id, self.id)) {
return @intCast(i);
}
}
return -1;
}
pub fn get_key(self: *const NavigationHistoryEntry) []const u8 {
return self.key;
}
pub fn get_sameDocument(self: *const NavigationHistoryEntry, page: *Page) !bool {
const _url = self.url orelse return false;
const url = try URL.parse(_url, null);
return page.url.eqlDocument(&url, page.call_arena);
}
pub fn get_url(self: *const NavigationHistoryEntry) ?[]const u8 {
return self.url;
}
pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !?js.Value {
if (self.state) |state| {
return try js.Value.fromJson(page.js, state);
} else {
return null;
}
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation
pub const NavigationActivation = struct {
const NavigationActivationType = enum {
pub const ENUM_JS_USE_TAG = true;
push,
reload,
replace,
traverse,
};
entry: NavigationHistoryEntry,
from: ?NavigationHistoryEntry = null,
type: NavigationActivationType,
pub fn get_entry(self: *const NavigationActivation) NavigationHistoryEntry {
return self.entry;
}
pub fn get_from(self: *const NavigationActivation) ?NavigationHistoryEntry {
return self.from;
}
pub fn get_navigationType(self: *const NavigationActivation) NavigationActivationType {
return self.type;
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationTransition
pub const NavigationTransition = struct {
finished: js.Promise,
from: NavigationHistoryEntry,
navigation_type: NavigationActivation.NavigationActivationType,
};
const Event = @import("../events/event.zig").Event;
pub const NavigationCurrentEntryChangeEvent = struct {
pub const prototype = *Event;
pub const union_make_copy = true;
pub const EventInit = struct {
from: *NavigationHistoryEntry,
navigationType: ?NavigationType = null,
};
proto: parser.Event,
from: *NavigationHistoryEntry,
navigation_type: ?NavigationType,
pub fn constructor(event_type: []const u8, opts: EventInit) !NavigationCurrentEntryChangeEvent {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .navigation_current_entry_change_event);
return .{
.proto = event.*,
.from = opts.from,
.navigation_type = opts.navigationType,
};
}
pub fn get_from(self: *NavigationCurrentEntryChangeEvent) *NavigationHistoryEntry {
return self.from;
}
pub fn get_navigationType(self: *const NavigationCurrentEntryChangeEvent) ?NavigationType {
return self.navigation_type;
}
pub fn dispatch(navigation: *Navigation, from: *NavigationHistoryEntry, typ: ?NavigationType) void {
log.debug(.script_event, "dispatch event", .{
.type = "currententrychange",
.source = "navigation",
});
var evt = NavigationCurrentEntryChangeEvent.constructor(
"currententrychange",
.{ .from = from, .navigationType = typ },
) catch |err| {
log.err(.app, "event constructor error", .{
.err = err,
.type = "currententrychange",
.source = "navigation",
});
return;
};
_ = parser.eventTargetDispatchEvent(
@as(*parser.EventTarget, @ptrCast(navigation)),
&evt.proto,
) catch |err| {
log.err(.app, "dispatch event error", .{
.err = err,
.type = "currententrychange",
.source = "navigation",
});
};
}
};
pub const NavigateEvent = struct {
pub const prototype = *Event;
pub const union_make_copy = true;
pub const EventInit = struct {
canIntercept: ?bool = false,
// todo: destination
downloadRequest: ?[]const u8 = null,
// todo: formData
hashChange: ?bool = false,
hasUAVisualTransition: ?bool = false,
// info: ?js.Value,
navigationType: ?NavigationType = .push,
// todo: signal
// todo: sourceElement
userInitiated: ?bool = false,
};
proto: parser.Event,
can_intercept: bool,
download_request: ?[]const u8,
// todo: desintation
hash_change: bool,
has_ua_visual_transition: bool,
info: []const u8,
navigation_type: NavigationType,
// todo: signal
// todo: sourceElement
user_initiated: bool,
pub fn constructor(event_type: []const u8, opts: EventInit) !NavigateEvent {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .navigate_event);
return .{
.proto = event.*,
.can_intercept = opts.canIntercept orelse false,
.download_request = opts.downloadRequest orelse null,
.hash_change = opts.hashChange orelse false,
.has_ua_visual_transition = opts.hasUAVisualTransition orelse false,
.info = undefined,
.navigation_type = opts.navigationType orelse .push,
// .info = if (opts.info) |info| try info.toString(page.arena) else null,
.user_initiated = opts.userInitiated orelse false,
};
}
pub const InterceptOptions = struct {
// runs after currentEntry is updated
handler: ?js.Function,
// runs before currentEntry is updated
precommitHandler: ?js.Function,
focusReset: ?enum { @"after-transition", manual },
scroll: ?enum { @"after-transition", manual },
};
// https://developer.mozilla.org/en-US/docs/Web/API/NavigateEvent/intercept
pub fn intercept(
self: *NavigateEvent,
opts: ?InterceptOptions,
_: *Page,
) !void {
if (!self.can_intercept) {
return error.Security;
}
if (self.proto.in_dispatch) {
return error.InvalidState;
}
// try parser.eventPreventDefault(&self.proto);
if (opts) |options| {
if (options.precommitHandler) |handler| {
_ = try handler.call(void, .{});
}
}
// update current entry here.
if (opts) |options| {
if (options.handler) |handler| {
const result = try handler.call(js.Value, .{});
if (result.value.isPromise()) {
// must be stored and resolved based on nav outcome.
}
}
// todo: handle focusReset and scroll
}
}
pub fn scroll(self: *NavigateEvent) !void {
_ = self;
}
pub fn dispatch(navigation: *Navigation, from: *NavigationHistoryEntry, typ: ?NavigationType) void {
log.debug(.script_event, "dispatch event", .{
.type = "navigate",
.source = "navigation",
});
var evt = NavigateEvent.constructor(
"navigate",
.{ .from = from, .navigationType = typ },
) catch |err| {
log.err(.app, "event constructor error", .{
.err = err,
.type = "navigate",
.source = "navigation",
});
return;
};
_ = parser.eventTargetDispatchEvent(
@as(*parser.EventTarget, @ptrCast(navigation)),
&evt.proto,
) catch |err| {
log.err(.app, "dispatch event error", .{
.err = err,
.type = "navigate",
.source = "navigation",
});
};
}
};
const testing = @import("../../testing.zig");
test "Browser: Navigation" {
try testing.htmlRunner("html/navigation/navigation.html");
try testing.htmlRunner("html/navigation/navigation_currententrychange.html");
}

View File

@@ -559,6 +559,9 @@ pub const EventType = enum(u8) {
message_event = 7,
keyboard_event = 8,
pop_state = 9,
composition_event = 10,
navigation_current_entry_change_event = 11,
navigate_event = 12,
};
pub const MutationEvent = c.dom_mutation_event;
@@ -830,6 +833,7 @@ pub const EventTargetTBase = extern struct {
message_port = 7,
screen = 8,
screen_orientation = 9,
navigation = 10,
};
vtable: ?*const c.struct_dom_event_target_vtable = &c.struct_dom_event_target_vtable{

View File

@@ -35,6 +35,9 @@ const ScriptManager = @import("ScriptManager.zig");
const SlotChangeMonitor = @import("SlotChangeMonitor.zig");
const HTMLDocument = @import("html/document.zig").HTMLDocument;
const NavigationKind = @import("navigation/root.zig").NavigationKind;
const NavigationCurrentEntryChangeEvent = @import("navigation/root.zig").NavigationCurrentEntryChangeEvent;
const js = @import("js/js.zig");
const URL = @import("../url.zig").URL;
@@ -485,16 +488,16 @@ pub const Page = struct {
}
{
std.debug.print("\nprimary schedule: {d}\n", .{self.scheduler.primary.count()});
var it = self.scheduler.primary.iterator();
std.debug.print("\nhigh_priority schedule: {d}\n", .{self.scheduler.high_priority.count()});
var it = self.scheduler.high_priority.iterator();
while (it.next()) |task| {
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now });
}
}
{
std.debug.print("\nsecondary schedule: {d}\n", .{self.scheduler.secondary.count()});
var it = self.scheduler.secondary.iterator();
std.debug.print("\nlow_priority schedule: {d}\n", .{self.scheduler.low_priority.count()});
var it = self.scheduler.low_priority.iterator();
while (it.next()) |task| {
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now });
}
@@ -549,14 +552,31 @@ pub const Page = struct {
.body = opts.body != null,
});
// if the url is about:blank, nothing to do.
// if the url is about:blank, we load an empty HTML document in the
// page and dispatch the events.
if (std.mem.eql(u8, "about:blank", request_url)) {
const html_doc = try parser.documentHTMLParseFromStr("");
try self.setDocument(html_doc);
// Assume we parsed the document.
// It's important to force a reset during the following navigation.
self.mode = .parsed;
// We do not processHTMLDoc here as we know we don't have any scripts
// This assumption may be false when CDP Page.addScriptToEvaluateOnNewDocument is implemented
try HTMLDocument.documentIsComplete(self.window.document, self);
self.documentIsComplete();
self.session.browser.notification.dispatch(.page_navigate, &.{
.opts = opts,
.url = request_url,
.timestamp = timestamp(),
});
self.session.browser.notification.dispatch(.page_navigated, &.{
.url = request_url,
.timestamp = timestamp(),
});
return;
}
@@ -815,8 +835,8 @@ pub const Page = struct {
},
}
// Push the navigation after a successful load.
try self.session.history.pushNavigation(self.url.raw, self);
// We need to handle different navigation types differently.
try self.session.navigation.processNavigation(self);
}
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
@@ -859,7 +879,7 @@ pub const Page = struct {
self.window.setStorageShelf(
try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
);
try self.window.replaceLocation(.{ .url = try self.url.toWebApi(self.arena) });
try self.window.changeLocation(self.url.raw, self);
}
pub const MouseEvent = struct {
@@ -906,7 +926,7 @@ pub const Page = struct {
.a => {
const element: *parser.Element = @ptrCast(node);
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
try self.navigateFromWebAPI(href, .{});
try self.navigateFromWebAPI(href, .{}, .{ .push = null });
},
.input => {
const element: *parser.Element = @ptrCast(node);
@@ -1013,13 +1033,62 @@ pub const Page = struct {
}
}
// insertText is a shortcut to insert text into the active element.
pub fn insertText(self: *Page, v: []const u8) !void {
const Document = @import("dom/document.zig").Document;
const element = (try Document.getActiveElement(@ptrCast(self.window.document), self)) orelse return;
const node = parser.elementToNode(element);
const tag = (try parser.nodeHTMLGetTagType(node)) orelse return;
switch (tag) {
.input => {
const input_type = try parser.inputGetType(@ptrCast(element));
if (std.mem.eql(u8, input_type, "text")) {
const value = try parser.inputGetValue(@ptrCast(element));
const new_value = try std.mem.concat(self.arena, u8, &.{ value, v });
try parser.inputSetValue(@ptrCast(element), new_value);
}
},
.textarea => {
const value = try parser.textareaGetValue(@ptrCast(node));
const new_value = try std.mem.concat(self.arena, u8, &.{ value, v });
try parser.textareaSetValue(@ptrCast(node), new_value);
},
else => {},
}
}
// We cannot navigate immediately as navigating will delete the DOM tree,
// which holds this event's node.
// As such we schedule the function to be called as soon as possible.
// The page.arena is safe to use here, but the transfer_arena exists
// specifically for this type of lifetime.
pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts) !void {
pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts, kind: NavigationKind) !void {
const session = self.session;
const stitched_url = try URL.stitch(
session.transfer_arena,
url,
self.url.raw,
.{
.alloc = .always,
.null_terminated = true,
},
);
// Force will force a page load.
// Otherwise, we need to check if this is a true navigation.
if (!opts.force) {
// If we are navigating within the same document, just change URL.
const new_url = try URL.parse(stitched_url, null);
if (try self.url.eqlDocument(&new_url, session.transfer_arena)) {
self.url = new_url;
try self.window.changeLocation(self.url.raw, self);
try session.navigation.updateEntries(stitched_url, kind, self, true);
return;
}
}
if (session.queued_navigation != null) {
// It might seem like this should never happen. And it might not,
// BUT..consider the case where we have script like:
@@ -1042,9 +1111,11 @@ pub const Page = struct {
session.queued_navigation = .{
.opts = opts,
.url = try URL.stitch(session.transfer_arena, url, self.url.raw, .{ .alloc = .always }),
.url = stitched_url,
};
session.navigation_kind = kind;
self.http_client.abort();
// In v8, this throws an exception which JS code cannot catch.
@@ -1095,7 +1166,7 @@ pub const Page = struct {
} else {
action = try URL.concatQueryString(transfer_arena, action, buf.items);
}
try self.navigateFromWebAPI(action, opts);
try self.navigateFromWebAPI(action, opts, .{ .push = null });
}
pub fn isNodeAttached(self: *const Page, node: *parser.Node) bool {
@@ -1153,6 +1224,7 @@ pub const NavigateReason = enum {
form,
script,
history,
navigation,
};
pub const NavigateOpts = struct {
@@ -1161,6 +1233,7 @@ pub const NavigateOpts = struct {
method: Http.Method = .GET,
body: ?[]const u8 = null,
header: ?[:0]const u8 = null,
force: bool = false,
};
const IdleNotification = union(enum) {

View File

@@ -41,6 +41,10 @@ const FlatRenderer = struct {
const Element = @import("dom/element.zig").Element;
// Define the size of each element in the grid.
const default_w = 5;
const default_h = 5;
// we expect allocator to be an arena
pub fn init(allocator: Allocator) FlatRenderer {
return .{
@@ -62,10 +66,10 @@ const FlatRenderer = struct {
gop.value_ptr.* = x;
}
const _x: f64 = @floatFromInt(x);
const _x: f64 = @floatFromInt(x * default_w);
const y: f64 = 0.0;
const w: f64 = 1.0;
const h: f64 = 1.0;
const w: f64 = default_w;
const h: f64 = default_h;
return .{
.x = _x,
@@ -98,18 +102,20 @@ const FlatRenderer = struct {
}
pub fn width(self: *const FlatRenderer) u32 {
return @max(@as(u32, @intCast(self.elements.items.len)), 1); // At least 1 pixel even if empty
return @max(@as(u32, @intCast(self.elements.items.len * default_w)), default_w); // At least default width pixels even if empty
}
pub fn height(_: *const FlatRenderer) u32 {
return 1;
return 5;
}
pub fn getElementAtPosition(self: *const FlatRenderer, x: i32, y: i32) ?*parser.Element {
if (y != 0 or x < 0) {
pub fn getElementAtPosition(self: *const FlatRenderer, _x: i32, y: i32) ?*parser.Element {
if (y < 0 or y > default_h or _x < 0) {
return null;
}
const x = @divFloor(_x, default_w);
const elements = self.elements.items;
return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null;
}

View File

@@ -22,9 +22,11 @@ const Allocator = std.mem.Allocator;
const js = @import("js/js.zig");
const Page = @import("page.zig").Page;
const NavigationKind = @import("navigation/root.zig").NavigationKind;
const Browser = @import("browser.zig").Browser;
const NavigateOpts = @import("page.zig").NavigateOpts;
const History = @import("html/History.zig");
const Navigation = @import("navigation/Navigation.zig");
const log = @import("../log.zig");
const parser = @import("netsurf.zig");
@@ -57,6 +59,8 @@ pub const Session = struct {
// History is persistent across the "tab".
// https://developer.mozilla.org/en-US/docs/Web/API/History
history: History = .{},
navigation: Navigation = .{},
navigation_kind: ?NavigationKind = null,
page: ?Page = null,
@@ -100,6 +104,9 @@ pub const Session = struct {
// We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded
parser.init();
// creates a new event target for Navigation
self.navigation.resetForNewPage();
const page_arena = &self.browser.page_arena;
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
_ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 });

View File

@@ -18,6 +18,8 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const Writer = std.Io.Writer;
const ada = @import("ada");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig");
@@ -35,182 +37,235 @@ pub const Interfaces = .{
EntryIterable,
};
// https://url.spec.whatwg.org/#url
//
// TODO we could avoid many of these getter string allocatoration in two differents
// way:
//
// 1. We can eventually get the slice of scheme *with* the following char in
// the underlying string. But I don't know if it's possible and how to do that.
// I mean, if the rawuri contains `https://foo.bar`, uri.scheme is a slice
// containing only `https`. I want `https:` so, in theory, I don't need to
// allocatorate data, I should be able to retrieve the scheme + the following `:`
// from rawuri.
//
// 2. The other way would be to copy the `std.Uri` code to have a dedicated
// parser including the characters we want for the web API.
/// https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
pub const URL = struct {
uri: std.Uri,
internal: ada.URL,
/// We prefer in-house search params solution here;
/// ada's search params impl use more memory.
/// It also offers it's own iterator implementation
/// where we'd like to use ours.
search_params: URLSearchParams,
pub const empty = URL{
.uri = .{ .scheme = "" },
.internal = null,
.search_params = .{},
};
const URLArg = union(enum) {
url: *URL,
element: *parser.ElementHTML,
// You can use an existing URL object for either argument, and it will be
// stringified from the object's href property.
const ConstructorArg = union(enum) {
string: []const u8,
url: *const URL,
element: *parser.Element,
fn toString(self: URLArg, arena: Allocator) !?[]const u8 {
switch (self) {
.string => |s| return s,
.url => |url| return try url.toString(arena),
.element => |e| return try parser.elementGetAttribute(@ptrCast(e), "href"),
}
fn toString(self: ConstructorArg, page: *Page) ![]const u8 {
return switch (self) {
.string => |s| s,
.url => |url| url._toString(page),
.element => |e| {
const attrib = try parser.elementGetAttribute(@ptrCast(e), "href") orelse {
return error.InvalidArgument;
};
return attrib;
},
};
}
};
pub fn constructor(url: URLArg, base: ?URLArg, page: *Page) !URL {
const arena = page.arena;
const url_str = try url.toString(arena) orelse return error.InvalidArgument;
pub fn constructor(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !URL {
const url_str = try url.toString(page);
var raw: ?[]const u8 = null;
if (base) |b| {
if (try b.toString(arena)) |bb| {
raw = try @import("../../url.zig").URL.stitch(arena, url_str, bb, .{});
}
const internal = try blk: {
if (maybe_base) |base| {
break :blk ada.parseWithBase(url_str, try base.toString(page));
}
if (raw == null) {
// if it was a URL, then it's already be owned by the arena
raw = if (url == .url) url_str else try arena.dupe(u8, url_str);
}
const uri = std.Uri.parse(raw.?) catch blk: {
if (!std.mem.endsWith(u8, raw.?, "://")) {
return error.TypeError;
}
// schema only is valid!
break :blk std.Uri{
.scheme = raw.?[0 .. raw.?.len - 3],
.host = .{ .percent_encoded = "" },
};
break :blk ada.parse(url_str);
};
return init(arena, uri);
}
pub fn init(arena: Allocator, uri: std.Uri) !URL {
return .{
.uri = uri,
.search_params = try URLSearchParams.init(
arena,
uriComponentNullStr(uri.query),
),
.internal = internal,
.search_params = try prepareSearchParams(page.arena, internal),
};
}
pub fn initWithoutSearchParams(uri: std.Uri) URL {
return .{ .uri = uri, .search_params = .{} };
pub fn destructor(self: *const URL) void {
// Not tracked by arena.
return ada.free(self.internal);
}
pub fn get_origin(self: *URL, page: *Page) ![]const u8 {
var aw = std.Io.Writer.Allocating.init(page.arena);
try self.uri.writeToStream(&aw.writer, .{
.scheme = true,
.authentication = false,
.authority = true,
.path = false,
.query = false,
.fragment = false,
});
return aw.written();
/// Only to be used by `Location` API. `url` MUST NOT provide search params.
pub fn initForLocation(url: []const u8) !URL {
return .{ .internal = try ada.parse(url), .search_params = .{} };
}
// get_href returns the URL by writing all its components.
pub fn get_href(self: *URL, page: *Page) ![]const u8 {
return self.toString(page.arena);
/// Reinitializes the URL by parsing given `url`. Search params can be provided.
pub fn reinit(self: *URL, url: []const u8, page: *Page) !void {
_ = ada.setHref(self.internal, url);
if (!ada.isValid(self.internal)) return error.Internal;
self.search_params = try prepareSearchParams(page.arena, self.internal);
}
pub fn _toString(self: *URL, page: *Page) ![]const u8 {
return self.toString(page.arena);
/// Prepares a `URLSearchParams` from given `internal`.
/// Resets `search` of `internal`.
fn prepareSearchParams(arena: Allocator, internal: ada.URL) !URLSearchParams {
const maybe_search = ada.getSearchNullable(internal);
// Empty.
if (maybe_search.data == null) return .{};
const search = maybe_search.data[0..maybe_search.length];
const search_params = URLSearchParams.initFromString(arena, search);
// After a call to this function, search params are tracked by
// `search_params`. So we reset the internal's search.
ada.clearSearch(internal);
return search_params;
}
// format the url with all its components.
pub fn toString(self: *const URL, arena: Allocator) ![]const u8 {
var aw = std.Io.Writer.Allocating.init(arena);
try self.uri.writeToStream(&aw.writer, .{
.scheme = true,
.authentication = true,
.authority = true,
.path = uriComponentNullStr(self.uri.path).len > 0,
});
pub fn clearPort(self: *const URL) void {
return ada.clearPort(self.internal);
}
pub fn clearHash(self: *const URL) void {
return ada.clearHash(self.internal);
}
/// Returns a boolean indicating whether or not an absolute URL,
/// or a relative URL combined with a base URL, are parsable and valid.
pub fn static_canParse(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !bool {
const url_str = try url.toString(page);
if (maybe_base) |base| {
return ada.canParseWithBase(url_str, try base.toString(page));
}
return ada.canParse(url_str);
}
/// Alias to get_href.
pub fn _toString(self: *const URL, page: *Page) ![]const u8 {
return self.get_href(page);
}
// Getters.
pub fn get_searchParams(self: *URL) *URLSearchParams {
return &self.search_params;
}
pub fn get_origin(self: *const URL, page: *Page) ![]const u8 {
// `ada.getOriginNullable` allocates memory in order to find the `origin`.
// We'd like to use our arena allocator for such case;
// so here we allocate the `origin` in page arena and free the original.
const maybe_origin = ada.getOriginNullable(self.internal);
if (maybe_origin.data == null) {
return "";
}
defer ada.freeOwnedString(maybe_origin);
const origin = maybe_origin.data[0..maybe_origin.length];
return page.call_arena.dupe(u8, origin);
}
pub fn get_href(self: *const URL, page: *Page) ![]const u8 {
var w: Writer.Allocating = .init(page.arena);
// If URL is not valid, return immediately.
if (!ada.isValid(self.internal)) {
return "";
}
// Since the earlier check passed, this can't be null.
const str = ada.getHrefNullable(self.internal);
const href = str.data[0..str.length];
// This can't be null either.
const comps = ada.getComponents(self.internal);
// If hash provided, we write it after we fit-in the search params.
const has_hash = comps.hash_start != ada.URLOmitted;
const href_part = if (has_hash) href[0..comps.hash_start] else href;
try w.writer.writeAll(href_part);
// Write search params if provided.
if (self.search_params.get_size() > 0) {
try aw.writer.writeByte('?');
try self.search_params.write(&aw.writer);
try w.writer.writeByte('?');
try self.search_params.write(&w.writer);
}
{
const fragment = uriComponentNullStr(self.uri.fragment);
if (fragment.len > 0) {
try aw.writer.writeByte('#');
try aw.writer.writeAll(fragment);
}
// Write hash if provided before.
const hash = self.get_hash();
try w.writer.writeAll(hash);
return w.written();
}
return aw.written();
pub fn get_username(self: *const URL) []const u8 {
const username = ada.getUsernameNullable(self.internal);
if (username.data == null) {
return "";
}
pub fn get_protocol(self: *const URL) []const u8 {
// std.Uri keeps a pointer to "https", "http" (scheme part) so we know
// its followed by ':'.
const scheme = self.uri.scheme;
return scheme.ptr[0 .. scheme.len + 1];
return username.data[0..username.length];
}
pub fn get_username(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.user);
pub fn get_password(self: *const URL) []const u8 {
const password = ada.getPasswordNullable(self.internal);
if (password.data == null) {
return "";
}
pub fn get_password(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.password);
return password.data[0..password.length];
}
pub fn get_host(self: *URL, page: *Page) ![]const u8 {
var aw = std.Io.Writer.Allocating.init(page.arena);
try self.uri.writeToStream(&aw.writer, .{
.scheme = false,
.authentication = false,
.authority = true,
.path = false,
.query = false,
.fragment = false,
});
return aw.written();
pub fn get_port(self: *const URL) []const u8 {
const port = ada.getPortNullable(self.internal);
if (port.data == null) {
return "";
}
pub fn get_hostname(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.host);
return port.data[0..port.length];
}
pub fn get_port(self: *URL, page: *Page) ![]const u8 {
const arena = page.arena;
if (self.uri.port == null) return try arena.dupe(u8, "");
var aw = std.Io.Writer.Allocating.init(arena);
try aw.writer.printInt(self.uri.port.?, 10, .lower, .{});
return aw.written();
pub fn get_hash(self: *const URL) []const u8 {
const hash = ada.getHashNullable(self.internal);
if (hash.data == null) {
return "";
}
pub fn get_pathname(self: *URL) []const u8 {
if (uriComponentStr(self.uri.path).len == 0) return "/";
return uriComponentStr(self.uri.path);
return hash.data[0..hash.length];
}
pub fn get_search(self: *URL, page: *Page) ![]const u8 {
pub fn get_host(self: *const URL) []const u8 {
const host = ada.getHostNullable(self.internal);
if (host.data == null) {
return "";
}
return host.data[0..host.length];
}
pub fn get_hostname(self: *const URL) []const u8 {
const hostname = ada.getHostnameNullable(self.internal);
if (hostname.data == null) {
return "";
}
return hostname.data[0..hostname.length];
}
pub fn get_pathname(self: *const URL) []const u8 {
const path = ada.getPathnameNullable(self.internal);
// Return a slash if path is null.
if (path.data == null) {
return "/";
}
return path.data[0..path.length];
}
/// get_search depends on the current state of `search_params`.
pub fn get_search(self: *const URL, page: *Page) ![]const u8 {
const arena = page.arena;
if (self.search_params.get_size() == 0) {
@@ -223,72 +278,104 @@ pub const URL = struct {
return buf.items;
}
pub fn set_search(self: *URL, qs_: ?[]const u8, page: *Page) !void {
pub fn get_protocol(self: *const URL) []const u8 {
const protocol = ada.getProtocolNullable(self.internal);
if (protocol.data == null) {
return "";
}
return protocol.data[0..protocol.length];
}
// Setters.
/// Ada-url don't define any errors, so we just prefer one unified
/// `Internal` error for failing cases.
const SetterError = error{Internal};
pub fn set_href(self: *URL, input: []const u8, page: *Page) !void {
_ = ada.setHref(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
// Can't call `get_search` here since it uses `search_params`.
self.search_params = try prepareSearchParams(page.arena, self.internal);
}
pub fn set_host(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setHost(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_hostname(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setHostname(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_protocol(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setProtocol(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_username(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setUsername(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_password(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setPassword(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_port(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setPort(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_pathname(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setPathname(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_search(self: *URL, maybe_input: ?[]const u8, page: *Page) !void {
self.search_params = .{};
if (qs_) |qs| {
self.search_params = try URLSearchParams.init(page.arena, qs);
if (maybe_input) |input| {
self.search_params = try .initFromString(page.arena, input);
}
}
pub fn get_hash(self: *URL, page: *Page) ![]const u8 {
const arena = page.arena;
if (self.uri.fragment == null) return try arena.dupe(u8, "");
return try std.mem.concat(arena, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) });
}
pub fn get_searchParams(self: *URL) *URLSearchParams {
return &self.search_params;
}
pub fn _toJSON(self: *URL, page: *Page) ![]const u8 {
return self.get_href(page);
pub fn set_hash(self: *const URL, input: []const u8) !void {
ada.setHash(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
};
// uriComponentNullStr converts an optional std.Uri.Component to string value.
// The string value can be undecoded.
fn uriComponentNullStr(c: ?std.Uri.Component) []const u8 {
if (c == null) return "";
return uriComponentStr(c.?);
}
fn uriComponentStr(c: std.Uri.Component) []const u8 {
return switch (c) {
.raw => |v| v,
.percent_encoded => |v| v,
};
}
// https://url.spec.whatwg.org/#interface-urlsearchparams
pub const URLSearchParams = struct {
entries: kv.List = .{},
const URLSearchParamsOpts = union(enum) {
qs: []const u8,
pub const ConstructorOptions = union(enum) {
query_string: []const u8,
form_data: *const FormData,
js_obj: js.Object,
object: js.Object,
};
pub fn constructor(opts_: ?URLSearchParamsOpts, page: *Page) !URLSearchParams {
const opts = opts_ orelse return .{ .entries = .{} };
return switch (opts) {
.qs => |qs| init(page.arena, qs),
.form_data => |fd| .{ .entries = try fd.entries.clone(page.arena) },
.js_obj => |js_obj| {
const arena = page.arena;
var it = js_obj.nameIterator();
var entries: kv.List = .{};
pub fn constructor(maybe_options: ?ConstructorOptions, page: *Page) !URLSearchParams {
const options = maybe_options orelse return .{};
const arena = page.arena;
return switch (options) {
.query_string => |string| .{ .entries = try parseQuery(arena, string) },
.form_data => |form_data| .{ .entries = try form_data.entries.clone(arena) },
.object => |object| {
var it = object.nameIterator();
var entries = kv.List{};
try entries.ensureTotalCapacity(arena, it.count);
while (try it.next()) |js_name| {
const name = try js_name.toString(arena);
const js_val = try js_obj.get(name);
entries.appendOwnedAssumeCapacity(
name,
try js_val.toString(arena),
);
const js_value = try object.get(name);
const value = try js_value.toString(arena);
entries.appendOwnedAssumeCapacity(name, value);
}
return .{ .entries = entries };
@@ -296,10 +383,9 @@ pub const URLSearchParams = struct {
};
}
pub fn init(arena: Allocator, qs_: ?[]const u8) !URLSearchParams {
return .{
.entries = if (qs_) |qs| try parseQuery(arena, qs) else .{},
};
/// Initializes URLSearchParams from a query string.
pub fn initFromString(arena: Allocator, query_string: []const u8) !URLSearchParams {
return .{ .entries = try parseQuery(arena, query_string) };
}
pub fn get_size(self: *const URLSearchParams) u32 {

View File

@@ -632,6 +632,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
};
}
// debugger events
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {
// onRunMessageLoopOnPause is called when a breakpoint is hit.
// Until quit pause, we must continue to run a nested message loop
// to interact with the the debugger ony (ie. Chrome DevTools).
}
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {
// Quit breakpoint pause.
}
// This is hacky x 2. First, we create the JSON payload by gluing our
// session_id onto it. Second, we're much more client/websocket aware than
// we should be.
@@ -702,7 +713,7 @@ const IsolatedWorld = struct {
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
// (assuming grantUniveralAccess will be set to True!).
// We just created the world and the page. The page's state lives in the session, but is update on navigation.
// This also means this pointer becomes invalid after removePage untill a new page is created.
// This also means this pointer becomes invalid after removePage until a new page is created.
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
// if (self.executor.context != null) return error.Only1IsolatedContextSupported;

View File

@@ -38,16 +38,22 @@ const DEV_TOOLS_WINDOW_ID = 1923710101;
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
getVersion,
setDownloadBehavior,
getWindowForTarget,
setPermission,
setWindowBounds,
resetPermissions,
grantPermissions,
getWindowForTarget,
setDownloadBehavior,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
.getVersion => return getVersion(cmd),
.setDownloadBehavior => return setDownloadBehavior(cmd),
.getWindowForTarget => return getWindowForTarget(cmd),
.setPermission => return setPermission(cmd),
.setWindowBounds => return setWindowBounds(cmd),
.resetPermissions => return resetPermissions(cmd),
.grantPermissions => return grantPermissions(cmd),
.getWindowForTarget => return getWindowForTarget(cmd),
.setDownloadBehavior => return setDownloadBehavior(cmd),
}
}
@@ -89,6 +95,21 @@ fn setWindowBounds(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn grantPermissions(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn setPermission(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn resetPermissions(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
const testing = @import("../testing.zig");
test "cdp.browser: getVersion" {
var ctx = testing.context();

View File

@@ -663,11 +663,11 @@ test "cdp.dom: getBoxModel" {
.params = .{ .nodeId = 6 },
});
try ctx.expectSentResult(.{ .model = BoxModel{
.content = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
.padding = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
.border = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
.margin = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
.width = 1,
.height = 1,
.content = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
.padding = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
.border = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
.margin = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
.width = 5,
.height = 5,
} }, .{ .id = 5 });
}

View File

@@ -23,11 +23,13 @@ pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
dispatchKeyEvent,
dispatchMouseEvent,
insertText,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
.dispatchKeyEvent => return dispatchKeyEvent(cmd),
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
.insertText => return insertText(cmd),
}
}
@@ -115,6 +117,20 @@ fn dispatchMouseEvent(cmd: anytype) !void {
// result already sent
}
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText
fn insertText(cmd: anytype) !void {
const params = (try cmd.params(struct {
text: []const u8, // The text to insert
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return;
const page = bc.session.currentPage() orelse return;
try page.insertText(params.text);
try cmd.sendResult(null, .{});
}
fn clickNavigate(cmd: anytype, uri: std.Uri) !void {
const bc = cmd.browser_context.?;

View File

@@ -18,6 +18,7 @@
const std = @import("std");
const Page = @import("../../browser/page.zig").Page;
const timestampF = @import("../../datetime.zig").timestamp;
const Notification = @import("../../notification.zig").Notification;
const Allocator = std.mem.Allocator;
@@ -82,11 +83,33 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void {
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
if (params.enabled) {
try bc.lifecycleEventsEnable();
} else {
if (params.enabled == false) {
bc.lifecycleEventsDisable();
return cmd.sendResult(null, .{});
}
// Enable lifecycle events.
try bc.lifecycleEventsEnable();
// When we enable lifecycle events, we must dispatch events for all
// attached targets.
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
if (page.load_state == .complete) {
try sendPageLifecycle(bc, "DOMContentLoaded", timestampF());
try sendPageLifecycle(bc, "load", timestampF());
const http_active = page.http_client.active;
const total_network_activity = http_active + page.http_client.intercepted;
if (page.notified_network_almost_idle.check(total_network_activity <= 2)) {
try sendPageLifecycle(bc, "networkAlmostIdle", timestampF());
}
if (page.notified_network_idle.check(total_network_activity == 0)) {
try sendPageLifecycle(bc, "networkIdle", timestampF());
}
}
return cmd.sendResult(null, .{});
}
@@ -174,7 +197,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
var cdp = bc.cdp;
const reason_: ?[]const u8 = switch (event.opts.reason) {
.anchor => "anchorClick",
.script, .history => "scriptInitiated",
.script, .history, .navigation => "scriptInitiated",
.form => switch (event.opts.method) {
.GET => "formSubmissionGet",
.POST => "formSubmissionPost",

View File

@@ -21,9 +21,48 @@ const std = @import("std");
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
setIgnoreCertificateErrors,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
.enable => return cmd.sendResult(null, .{}),
.setIgnoreCertificateErrors => return setIgnoreCertificateErrors(cmd),
}
}
fn setIgnoreCertificateErrors(cmd: anytype) !void {
const params = (try cmd.params(struct {
ignore: bool,
})) orelse return error.InvalidParams;
if (params.ignore) {
try cmd.cdp.browser.http_client.disableTlsVerify();
} else {
try cmd.cdp.browser.http_client.enableTlsVerify();
}
return cmd.sendResult(null, .{});
}
const testing = @import("../testing.zig");
test "cdp.Security: setIgnoreCertificateErrors" {
var ctx = testing.context();
defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-9" });
try ctx.processMessage(.{
.id = 8,
.method = "Security.setIgnoreCertificateErrors",
.params = .{ .ignore = true },
});
try ctx.expectSentResult(null, .{ .id = 8 });
try ctx.processMessage(.{
.id = 9,
.method = "Security.setIgnoreCertificateErrors",
.params = .{ .ignore = false },
});
try ctx.expectSentResult(null, .{ .id = 9 });
}

View File

@@ -109,7 +109,7 @@ fn disposeBrowserContext(cmd: anytype) !void {
fn createTarget(cmd: anytype) !void {
const params = (try cmd.params(struct {
// url: []const u8,
url: []const u8 = "about:blank",
// width: ?u64 = null,
// height: ?u64 = null,
browserContextId: ?[]const u8 = null,
@@ -167,7 +167,7 @@ fn createTarget(cmd: anytype) !void {
.targetInfo = TargetInfo{
.attached = false,
.targetId = target_id,
.title = "about:blank",
.title = params.url,
.browserContextId = bc.id,
.url = "about:blank",
},
@@ -178,6 +178,10 @@ fn createTarget(cmd: anytype) !void {
try doAttachtoTarget(cmd, target_id);
}
try page.navigate(params.url, .{
.reason = .address_bar,
});
try cmd.sendResult(.{
.targetId = target_id,
}, .{});
@@ -517,7 +521,7 @@ test "cdp.target: createTarget" {
{
var ctx = testing.context();
defer ctx.deinit();
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about/blank" } });
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about:blank" } });
// should create a browser context
const bc = ctx.cdp().browser_context.?;
@@ -529,7 +533,7 @@ test "cdp.target: createTarget" {
defer ctx.deinit();
// active auto attach to get the Target.attachedToTarget event.
try ctx.processMessage(.{ .id = 9, .method = "Target.setAutoAttach", .params = .{ .autoAttach = true, .waitForDebuggerOnStart = false } });
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about/blank" } });
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about:blank" } });
// should create a browser context
const bc = ctx.cdp().browser_context.?;

View File

@@ -93,6 +93,11 @@ notification: ?*Notification = null,
// restoring, this originally-configured value is what it goes to.
http_proxy: ?[:0]const u8 = null,
// track if the client use a proxy for connections.
// We can't use http_proxy because we want also to track proxy configured via
// CDP.
use_proxy: bool,
// The complete user-agent header line
user_agent: [:0]const u8,
@@ -126,6 +131,7 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie
.handles = handles,
.allocator = allocator,
.http_proxy = opts.http_proxy,
.use_proxy = opts.http_proxy != null,
.user_agent = opts.user_agent,
.transfer_pool = transfer_pool,
};
@@ -315,6 +321,7 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void {
for (self.handles.handles) |*h| {
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
}
self.use_proxy = true;
}
// Same restriction as changeProxy. Should be ok since this is only called on
@@ -326,6 +333,37 @@ pub fn restoreOriginalProxy(self: *Client) !void {
for (self.handles.handles) |*h| {
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy));
}
self.use_proxy = proxy != null;
}
// Enable TLS verification on all connections.
pub fn enableTlsVerify(self: *const Client) !void {
for (self.handles.handles) |*h| {
const easy = h.conn.easy;
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 2)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 1)));
if (self.use_proxy) {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 2)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 1)));
}
}
}
// Disable TLS verification on all connections.
pub fn disableTlsVerify(self: *const Client) !void {
for (self.handles.handles) |*h| {
const easy = h.conn.easy;
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0)));
if (self.use_proxy) {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 0)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 0)));
}
}
}
fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
@@ -808,7 +846,7 @@ pub const Transfer = struct {
self.deinit();
}
// abortAuthChallenge is called when an auth chanllenge interception is
// abortAuthChallenge is called when an auth challenge interception is
// abort. We don't call self.client.endTransfer here b/c it has been done
// before interception process.
pub fn abortAuthChallenge(self: *Transfer) void {

View File

@@ -373,7 +373,11 @@ const Command = struct {
\\ Defaults to
++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++
\\
\\ --user_agent_suffix
\\--log_filter_scopes
\\ Filter out too verbose logs per scope:
\\ http, unknown_prop, script_event, ...
\\
\\--user_agent_suffix
\\ Suffix to append to the Lightpanda/X.Y User-Agent
\\
;

View File

@@ -487,7 +487,7 @@ pub const Client = struct {
}
// called by CDP
// Websocket frames have a variable lenght header. For server-client,
// Websocket frames have a variable length header. For server-client,
// it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have
// writev, so we need to get creative. We'll JSON serialize to a
// buffer, where the first 10 bytes are reserved. We can then backfill

View File

@@ -402,19 +402,13 @@ pub fn htmlRunner(file: []const u8) !void {
const url = try std.fmt.allocPrint(arena_allocator, "http://localhost:9582/src/tests/{s}", .{file});
try page.navigate(url, .{});
_ = page.wait(2000);
test_session.fetchWait(2000);
// page exits more aggressively in tests. We want to make sure this is called
// at lease once.
page.session.browser.runMicrotasks();
page.session.browser.runMessageLoop();
const needs_second_wait = try js_context.exec("testing._onPageWait.length > 0", "check_onPageWait");
if (needs_second_wait.value.toBool(page.js.isolate)) {
// sets the isSecondWait flag in testing.
_ = js_context.exec("testing._isSecondWait = true", "set_second_wait_flag") catch {};
_ = page.wait(2000);
}
@import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start);
const value = js_context.exec("testing.getStatus()", "testing.getStatus()") catch |err| {

View File

@@ -168,35 +168,35 @@
<script id=dimensions>
const para = document.getElementById('para');
testing.expectEqual(1, para.clientWidth);
testing.expectEqual(1, para.clientHeight);
testing.expectEqual(5, para.clientWidth);
testing.expectEqual(5, para.clientHeight);
// let r1 = document.getElementById('para').getBoundingClientRect();
// testing.expectEqual(0, r1.x);
// testing.expectEqual(0, r1.y);
// testing.expectEqual(1, r1.width);
// testing.expectEqual(2, r1.height);
let r1 = document.getElementById('para').getBoundingClientRect();
testing.expectEqual(0, r1.x);
testing.expectEqual(0, r1.y);
testing.expectEqual(5, r1.width);
testing.expectEqual(5, r1.height);
// let r2 = document.getElementById('content').getBoundingClientRect();
// testing.expectEqual(1, r2.x);
// testing.expectEqual(0, r2.y);
// testing.expectEqual(1, r2.width);
// testing.expectEqual(1, r2.height);
let r2 = document.getElementById('content').getBoundingClientRect();
testing.expectEqual(5, r2.x);
testing.expectEqual(0, r2.y);
testing.expectEqual(5, r2.width);
testing.expectEqual(5, r2.height);
// let r3 = document.getElementById('para').getBoundingClientRect();
// testing.expectEqual(0, r3.x);
// testing.expectEqual(0, r3.y);
// testing.expectEqual(1, r3.width);
// testing.expectEqual(1, r3.height);
let r3 = document.getElementById('para').getBoundingClientRect();
testing.expectEqual(0, r3.x);
testing.expectEqual(0, r3.y);
testing.expectEqual(5, r3.width);
testing.expectEqual(5, r3.height);
// testing.expectEqual(1, para.clientWidth);
// testing.expectEqual(1, para.clientHeight);
testing.expectEqual(10, para.clientWidth);
testing.expectEqual(5, para.clientHeight);
// let r4 = document.createElement('div').getBoundingClientRect();
// testing.expectEqual(0, r4.x);
// testing.expectEqual(0, r4.y);
// testing.expectEqual(0, r4.width);
// testing.expectEqual(0, r4.height);
let r4 = document.createElement('div').getBoundingClientRect();
testing.expectEqual(0, r4.x);
testing.expectEqual(0, r4.y);
testing.expectEqual(0, r4.width);
testing.expectEqual(0, r4.height);
</script>
<script id=matches>

View File

@@ -122,13 +122,13 @@
testing.expectEqual(1, entry.intersectionRatio);
testing.expectEqual(0, entry.intersectionRect.x);
testing.expectEqual(0, entry.intersectionRect.y);
testing.expectEqual(1, entry.intersectionRect.width);
testing.expectEqual(1, entry.intersectionRect.height);
testing.expectEqual(5, entry.intersectionRect.width);
testing.expectEqual(5, entry.intersectionRect.height);
testing.expectEqual(true, entry.isIntersecting);
testing.expectEqual(0, entry.rootBounds.x);
testing.expectEqual(0, entry.rootBounds.y);
testing.expectEqual(1, entry.rootBounds.width);
testing.expectEqual(1, entry.rootBounds.height);
testing.expectEqual(5, entry.rootBounds.width);
testing.expectEqual(5, entry.rootBounds.height);
testing.expectEqual('[object HTMLDivElement]', entry.target.toString());
});
}

View File

@@ -7,6 +7,7 @@
<p id="para"> And</p>
<!--comment-->
</div>
<div id="rootNodeComposed"></div>
</body>
<script src="../testing.js"></script>
@@ -36,6 +37,26 @@ let first_child = content.firstChild.nextSibling; // nextSibling because of line
testing.expectEqual('HTMLDocument', content.getRootNode().__proto__.constructor.name);
</script>
<script id=getRootNodeComposed>
const testContainer = $('#rootNodeComposed');
const shadowHost = document.createElement('div');
testContainer.appendChild(shadowHost);
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
const shadowChild = document.createElement('span');
shadowRoot.appendChild(shadowChild);
testing.expectEqual('ShadowRoot', shadowChild.getRootNode().__proto__.constructor.name);
testing.expectEqual('ShadowRoot', shadowChild.getRootNode({ composed: false }).__proto__.constructor.name);
testing.expectEqual('HTMLDocument', shadowChild.getRootNode({ composed: true }).__proto__.constructor.name);
testing.expectEqual('HTMLDocument', shadowHost.getRootNode().__proto__.constructor.name);
const disconnected = document.createElement('div');
const disconnectedChild = document.createElement('span');
disconnected.appendChild(disconnectedChild);
testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode().__proto__.constructor.name);
testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode({ composed: true }).__proto__.constructor.name);
</script>
<script id=firstChild>
let body_first_child = document.body.firstChild;
testing.expectEqual('div', body_first_child.localName);

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=noNata>
{
let event = new CompositionEvent("test", {});
testing.expectEqual(true, event instanceof CompositionEvent);
testing.expectEqual(true, event instanceof Event);
testing.expectEqual("test", event.type);
testing.expectEqual("", event.data);
}
</script>
<script id=withData>
{
let event = new CompositionEvent("test2", {data: "over 9000!"});
testing.expectEqual("test2", event.type);
testing.expectEqual("over 9000!", event.data);
}
</script>
<script id=dispatch>
{
let called = 0;
document.addEventListener('CE', (e) => {
testing.expectEqual('test-data', e.data);
testing.expectEqual(true, e instanceof CompositionEvent);
called += 1
});
document.dispatchEvent(new CompositionEvent('CE', {data: 'test-data'}));
testing.expectEqual(1, called);
}
</script>

125
src/tests/file/blob.html Normal file
View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=Blob/Blob.text>
{
const parts = ["\r\nthe quick brown\rfo\rx\r", "\njumps over\r\nthe\nlazy\r", "\ndog"];
// "transparent" ending should not modify the final buffer.
const blob = new Blob(parts, { type: "text/html" });
const expected = parts.join("");
testing.expectEqual(expected.length, blob.size);
testing.expectEqual("text/html", blob.type);
testing.async(blob.text(), result => testing.expectEqual(expected, result));
}
{
const parts = ["\rhello\r", "\nwor\r\nld"];
// "native" ending should modify the final buffer.
const blob = new Blob(parts, { endings: "native" });
const expected = "\nhello\n\nwor\nld";
testing.expectEqual(expected.length, blob.size);
testing.async(blob.text(), result => testing.expectEqual(expected, result));
testing.async(blob.arrayBuffer(), result => testing.expectEqual(true, result instanceof ArrayBuffer));
}
</script>
<script id=Blob.stream>
{
const parts = ["may", "thy", "knife", "chip", "and", "shatter"];
const blob = new Blob(parts);
const reader = blob.stream().getReader();
testing.async(reader.read(), ({ done, value }) => {
const expected = new Uint8Array([109, 97, 121, 116, 104, 121, 107, 110,
105, 102, 101, 99, 104, 105, 112, 97,
110, 100, 115, 104, 97, 116, 116, 101,
114]);
testing.expectEqual(false, done);
testing.expectEqual(true, value instanceof Uint8Array);
testing.expectEqual(expected, value);
});
}
</script>
<script id=Blob.arrayBuffer/Blob.slice>
{
const parts = ["la", "symphonie", "des", "éclairs"];
const blob = new Blob(parts);
testing.async(blob.arrayBuffer(), result => testing.expectEqual(true, result instanceof ArrayBuffer));
let temp = blob.slice(0);
testing.expectEqual(blob.size, temp.size);
testing.async(temp.text(), result => {
testing.expectEqual("lasymphoniedeséclairs", result);
});
temp = blob.slice(-4, -2, "custom");
testing.expectEqual(2, temp.size);
testing.expectEqual("custom", temp.type);
testing.async(temp.text(), result => testing.expectEqual("ai", result));
temp = blob.slice(14);
testing.expectEqual(8, temp.size);
testing.async(temp.text(), result => testing.expectEqual("éclairs", result));
temp = blob.slice(6, -10, "text/eclair");
testing.expectEqual(6, temp.size);
testing.expectEqual("text/eclair", temp.type);
testing.async(temp.text(), result => testing.expectEqual("honied", result));
}
</script>
<!-- Firefox and Safari only -->
<script id=Blob.bytes>
{
const parts = ["light ", "panda ", "rocks ", "!"];
const blob = new Blob(parts);
testing.async(blob.bytes(), result => {
const expected = new Uint8Array([108, 105, 103, 104, 116, 32, 112, 97,
110, 100, 97, 32, 114, 111, 99, 107, 115,
32, 33]);
testing.expectEqual(true, result instanceof Uint8Array);
testing.expectEqual(expected, result);
});
}
// Test for SIMD.
{
const parts = [
"\rThe opened package\r\nof potato\nchi\rps",
"held the\r\nanswer to the\r mystery. Both det\rectives looke\r\rd\r",
"\rat it but failed to realize\nit was\r\nthe\rkey\r\n",
"\r\nto solve the \rcrime.\r"
];
const blob = new Blob(parts, { type: "text/html", endings: "native" });
testing.expectEqual(161, blob.size);
testing.expectEqual("text/html", blob.type);
testing.async(blob.bytes(), result => {
const expected = new Uint8Array([10, 84, 104, 101, 32, 111, 112, 101, 110,
101, 100, 32, 112, 97, 99, 107, 97, 103,
101, 10, 111, 102, 32, 112, 111, 116, 97,
116, 111, 10, 99, 104, 105, 10, 112, 115,
104, 101, 108, 100, 32, 116, 104, 101, 10,
97, 110, 115, 119, 101, 114, 32, 116, 111,
32, 116, 104, 101, 10, 32, 109, 121, 115,
116, 101, 114, 121, 46, 32, 66, 111, 116,
104, 32, 100, 101, 116, 10, 101, 99, 116,
105, 118, 101, 115, 32, 108, 111, 111, 107,
101, 10, 10, 100, 10, 10, 97, 116, 32, 105,
116, 32, 98, 117, 116, 32, 102, 97, 105, 108,
101, 100, 32, 116, 111, 32, 114, 101, 97,
108, 105, 122, 101, 10, 105, 116, 32, 119, 97,
115, 10, 116, 104, 101, 10, 107, 101, 121,
10, 10, 116, 111, 32, 115, 111, 108, 118, 101,
32, 116, 104, 101, 32, 10, 99, 114, 105, 109,
101, 46, 10]);
testing.expectEqual(true, result instanceof Uint8Array);
testing.expectEqual(expected, result);
});
}
</script>

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=file>
let f = new File()
let f = new File();
testing.expectEqual(true, f instanceof File);
</script>

View File

@@ -46,15 +46,15 @@
testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);
// Return null since we only return elements when they have previously been localized
testing.expectEqual(null, document.elementFromPoint(0.5, 0.5));
testing.expectEqual([], document.elementsFromPoint(0.5, 0.5));
testing.expectEqual(null, document.elementFromPoint(2.5, 2.5));
testing.expectEqual([], document.elementsFromPoint(2.5, 2.5));
let div1 = document.createElement('div');
document.body.appendChild(div1);
div1.getClientRects(); // clal this to position it
testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(0.5, 0.5).toString());
testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(2.5, 2.5).toString());
let elems = document.elementsFromPoint(0.5, 0.5);
let elems = document.elementsFromPoint(2.5, 2.5);
testing.expectEqual(3, elems.length);
testing.expectEqual('[object HTMLDivElement]', elems[0].toString());
testing.expectEqual('[object HTMLBodyElement]', elems[1].toString());
@@ -66,11 +66,11 @@
// Note this will be placed after the div of previous test
a.getClientRects();
let a_again = document.elementFromPoint(1.5, 0.5);
let a_again = document.elementFromPoint(7.5, 0.5);
testing.expectEqual('[object HTMLAnchorElement]', a_again.toString());
testing.expectEqual('https://lightpanda.io', a_again.href);
let a_agains = document.elementsFromPoint(1.5, 0.5);
let a_agains = document.elementsFromPoint(7.5, 0.5);
testing.expectEqual('https://lightpanda.io', a_agains[0].href);

View File

@@ -1,21 +1,22 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script src="../../testing.js"></script>
<script id=history>
testing.expectEqual('auto', history.scrollRestoration);
history.scrollRestoration = 'manual';
history.scrollRestoration = 'foo';
testing.expectEqual('manual', history.scrollRestoration);
history.scrollRestoration = 'auto';
testing.expectEqual('auto', history.scrollRestoration);
testing.expectEqual(null, history.state)
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/xhr/json');
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/src/tests/html/history/history_after_nav.html');
testing.expectEqual({ testInProgress: true }, history.state);
history.pushState({ testInProgress: false }, null, 'http://127.0.0.1:9582/xhr/json');
history.replaceState({ "new": "field", testComplete: true }, null);
let state = { "new": "field", testComplete: true };
testing.expectEqual(state, history.state);
@@ -32,10 +33,5 @@
testing.expectEqual(state, popstateEventState);
})
testing.onPageWait(() => {
testing.expectEqual(true, history.state && history.state.testComplete);
testing.expectEqual(state, history.state);
});
testing.expectEqual(undefined, history.go());
history.back();
</script>

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id=history2>
history.pushState(
{"new": "field", testComplete: true },
null,
'http://127.0.0.1:9582/src/tests/html/history/history_after_nav.html'
);
let popstateEventFired = false;
let popstateEventState = null;
// uses the window event listener.
window.onpopstate = (event) => {
popstateEventFired = true;
popstateEventState = event.state;
};
testing.eventually(() => {
testing.expectEqual(true, popstateEventFired);
testing.expectEqual(true, popstateEventState.testComplete);
})
history.back();
</script>

View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id=history2>
testing.expectEqual(true, history.state && history.state.testInProgress);
</script>

View File

@@ -16,8 +16,8 @@
testing.expectEqual('https://lightpanda.io', link.origin);
link.host = 'lightpanda.io:443';
testing.expectEqual('lightpanda.io:443', link.host);
testing.expectEqual('443', link.port);
testing.expectEqual('lightpanda.io', link.host);
testing.expectEqual('', link.port);
testing.expectEqual('lightpanda.io', link.hostname);
link.host = 'lightpanda.io';
@@ -42,9 +42,9 @@
testing.expectEqual('', link.port);
link.port = '443';
testing.expectEqual('foo.bar:443', link.host);
testing.expectEqual('foo.bar', link.host);
testing.expectEqual('foo.bar', link.hostname);
testing.expectEqual('https://foo.bar:443/?q=bar#frag', link.href);
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
link.port = null;
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);

View File

@@ -13,3 +13,21 @@
testing.expectEqual("9582", location.port);
testing.expectEqual("", location.search);
</script>
<script id=location_hash>
location.hash = "";
testing.expectEqual("", location.hash);
testing.expectEqual('http://localhost:9582/src/tests/html/location.html', location.href);
location.hash = "#abcdef";
testing.expectEqual("#abcdef", location.hash);
testing.expectEqual('http://localhost:9582/src/tests/html/location.html#abcdef', location.href);
location.hash = "xyzxyz";
testing.expectEqual("#xyzxyz", location.hash);
testing.expectEqual('http://localhost:9582/src/tests/html/location.html#xyzxyz', location.href);
location.hash = "";
testing.expectEqual("", location.hash);
testing.expectEqual('http://localhost:9582/src/tests/html/location.html', location.href);
</script>

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id=navigation>
testing.expectEqual('object', typeof navigation);
testing.expectEqual('object', typeof navigation.currentEntry);
testing.expectEqual('string', typeof navigation.currentEntry.id);
testing.expectEqual('string', typeof navigation.currentEntry.key);
testing.expectEqual('string', typeof navigation.currentEntry.url);
const currentIndex = navigation.currentEntry.index;
navigation.navigate(
'http://localhost:9582/src/tests/html/navigation/navigation2.html',
{ state: { currentIndex: currentIndex, navTestInProgress: true } }
);
</script>

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id=navigation2>
const state = navigation.currentEntry.getState();
testing.expectEqual(true, state.navTestInProgress);
testing.expectEqual(state.currentIndex + 1, navigation.currentEntry.index);
</script>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id=navigation_currententrychange>
let currentEntryChanged = false;
navigation.addEventListener("currententrychange", () => {
currentEntryChanged = true;
});
// Doesn't fully navigate if same document.
location.href = location.href + "#1";
testing.expectEqual(true, currentEntryChanged);
</script>

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script defer id="remote_defer" src="order_defer.js"></script>
<script defer id="remote_async" src="order_async.js"></script>
<script type=module id="inline_module">
// inline module is always deferred.
list += 'g';
testing.expectEqual('abcdefg', list);
</script>
<script>
var list = '';
</script>
<script id="remote" src="order.js"></script>
<script async id="inline_async">
// inline script ignore async
list += 'b';
testing.expectEqual('ab', list);
</script>
<script defer id="inline_defer">
// inline script ignore defer
list += 'c';
testing.expectEqual('abc', list);
</script>
<script id="default">
// simple inline script
list += 'd';
testing.expectEqual('abcd', list);
</script>

View File

@@ -0,0 +1,2 @@
list += 'a';
testing.expectEqual('a', list);

View File

@@ -0,0 +1,3 @@
list += 'f';
testing.expectEqual('abcdef', list);

View File

@@ -0,0 +1,2 @@
list += 'e';
testing.expectEqual('abcde', list);

View File

@@ -51,14 +51,6 @@
// if we're already in a fail state, return fail, nothing can recover this
if (testing._status === 'fail') return 'fail';
if (testing._isSecondWait) {
for (const pw of (testing._onPageWait)) {
testing._captured = pw[1];
pw[0]();
testing._captured = null;
}
}
// run any eventually's that we've captured
for (const ev of testing._eventually) {
testing._captured = ev[1];
@@ -101,18 +93,6 @@
_registerErrorCallback();
}
// Set expectations to happen on the next time that `page.wait` is executed.
//
// History specifically uses this as it queues navigation that needs to be checked
// when the next page is loaded.
function onPageWait(fn) {
// Store callbacks to run when page.wait() happens
testing._onPageWait.push([fn, {
script_id: document.currentScript.id,
stack: new Error().stack,
}]);
}
async function async(promise, cb) {
const script_id = document.currentScript ? document.currentScript.id : '<script id is unavailable in browsers>';
const stack = new Error().stack;
@@ -192,15 +172,12 @@
window.testing = {
_status: 'empty',
_eventually: [],
_onPageWait: [],
_executed_scripts: new Set(),
_captured: null,
_isSecondWait: false,
skip: skip,
async: async,
getStatus: getStatus,
eventually: eventually,
onPageWait: onPageWait,
expectEqual: expectEqual,
expectError: expectError,
withError: withError,

View File

@@ -64,6 +64,23 @@
testing.expectEqual(null, url.searchParams.get('a'));
</script>
<script id=searchParamsSetHref>
url = new URL("https://foo.bar");
const searchParams = url.searchParams;
// SearchParams should be empty.
testing.expectEqual(0, searchParams.size);
url.href = "https://lightpanda.io?over=9000&light=panda";
// It won't hurt to check href and host too.
testing.expectEqual("https://lightpanda.io/?over=9000&light=panda", url.href);
testing.expectEqual("lightpanda.io", url.host);
// SearchParams should be updated too when URL is set.
testing.expectEqual(2, searchParams.size);
testing.expectEqual("9000", searchParams.get("over"));
testing.expectEqual("panda", searchParams.get("light"));
</script>
<script id=base>
url = new URL('over?9000', 'https://lightpanda.io');
testing.expectEqual("https://lightpanda.io/over?9000", url.href);
@@ -78,6 +95,15 @@
</script>
<script id=invalidUrl>
let u = new URL("://foo.bar/path?query#fragment");
testing.expectEqual(":", u.protocol);
testing.expectError("Error: Invalid", () => {
_ = new URL("://foo.bar/path?query#fragment");
});
</script>
<script id=URL.canParse>
testing.expectEqual(true, URL.canParse("https://lightpanda.io"));
testing.expectEqual(false, URL.canParse("://lightpanda.io"));
testing.expectEqual(true, URL.canParse("/home", "https://lightpanda.io"));
testing.expectEqual(false, URL.canParse("lightpanda.io", "https"));
</script>

View File

@@ -25,9 +25,9 @@
</script>
<script id=dimensions>
testing.expectEqual(1, innerHeight);
// Width is 1 even if there are no elements
testing.expectEqual(1, innerWidth);
testing.expectEqual(5, innerHeight);
// Width is 5 even if there are no elements
testing.expectEqual(5, innerWidth);
let div1 = document.createElement('div');
document.body.appendChild(div1);
@@ -37,8 +37,8 @@
document.body.appendChild(div2);
div2.getClientRects();
testing.expectEqual(1, innerHeight);
testing.expectEqual(2, innerWidth);
testing.expectEqual(5, innerHeight);
testing.expectEqual(10, innerWidth);
</script>
<script id=setTimeout>
@@ -149,3 +149,19 @@
testing.eventually(() => testing.expectEqual(true, isWindowTarget));
</script>
<script id=reportError>
let errorEventFired = false;
let capturedError = null;
window.addEventListener('error', (e) => {
errorEventFired = true;
capturedError = e.error;
});
const testError = new Error('Test error message');
window.reportError(testError);
testing.expectEqual(true, errorEventFired);
testing.expectEqual(testError, capturedError);
</script>

View File

@@ -75,26 +75,42 @@ pub const URL = struct {
return writer.writeAll(self.raw);
}
pub fn toWebApi(self: *const URL, allocator: Allocator) !WebApiURL {
return WebApiURL.init(allocator, self.uri);
}
/// Properly stitches two URL fragments together.
///
/// For URLs with a path, it will replace the last entry with the src.
/// For URLs without a path, it will add src as the path.
pub fn stitch(
allocator: Allocator,
path: []const u8,
base: []const u8,
raw_path: []const u8,
raw_base: []const u8,
comptime opts: StitchOpts,
) !StitchReturn(opts) {
if (base.len == 0 or isCompleteHTTPUrl(path)) {
return simpleStitch(allocator, path, opts);
const trimmed_path = std.mem.trim(u8, raw_path, &.{ '\n', '\r' });
if (raw_base.len == 0 or isCompleteHTTPUrl(trimmed_path)) {
return simpleStitch(allocator, trimmed_path, opts);
}
if (trimmed_path.len == 0) {
return simpleStitch(allocator, raw_base, opts);
}
// base should get stripped of its hash whenever we are stitching.
const base = if (std.mem.indexOfScalar(u8, raw_base, '#')) |hash_pos|
raw_base[0..hash_pos]
else
raw_base;
const path_hash_start = std.mem.indexOfScalar(u8, trimmed_path, '#');
const path = if (path_hash_start) |pos| trimmed_path[0..pos] else trimmed_path;
const hash = if (path_hash_start) |pos| trimmed_path[pos..] else "";
// if path is just hash, we just append it to base.
if (path.len == 0) {
return simpleStitch(allocator, base, opts);
if (comptime opts.null_terminated) {
return std.fmt.allocPrintSentinel(allocator, "{s}{s}", .{ base, hash }, 0);
}
return std.fmt.allocPrint(allocator, "{s}{s}", .{ base, hash });
}
if (std.mem.startsWith(u8, path, "//")) {
@@ -105,9 +121,9 @@ pub const URL = struct {
const protocol = base[0..index];
if (comptime opts.null_terminated) {
return std.fmt.allocPrintSentinel(allocator, "{s}:{s}", .{ protocol, path }, 0);
return std.fmt.allocPrintSentinel(allocator, "{s}:{s}{s}", .{ protocol, path, hash }, 0);
}
return std.fmt.allocPrint(allocator, "{s}:{s}", .{ protocol, path });
return std.fmt.allocPrint(allocator, "{s}:{s}{s}", .{ protocol, path, hash });
}
// Quick hack because domains have to be at least 3 characters.
@@ -128,25 +144,28 @@ pub const URL = struct {
return std.fmt.allocPrint(allocator, "{s}{s}", .{ root, path });
}
var old_path = std.mem.trimStart(u8, base[root.len..], "/");
if (std.mem.lastIndexOfScalar(u8, old_path, '/')) |pos| {
old_path = old_path[0..pos];
var oldraw_path = std.mem.trimStart(u8, base[root.len..], "/");
if (std.mem.lastIndexOfScalar(u8, oldraw_path, '/')) |pos| {
oldraw_path = oldraw_path[0..pos];
} else {
old_path = "";
oldraw_path = "";
}
// We preallocate all of the space possibly needed.
// This is the root, old_path, new path, 3 slashes and perhaps a null terminated slot.
var out = try allocator.alloc(u8, root.len + old_path.len + path.len + 3 + if (comptime opts.null_terminated) 1 else 0);
// This is the root, oldraw_path, new path, 3 slashes and perhaps a null terminated slot.
var out = try allocator.alloc(
u8,
root.len + oldraw_path.len + path.len + hash.len + 3 + if (comptime opts.null_terminated) 1 else 0,
);
var end: usize = 0;
@memmove(out[0..root.len], root);
end += root.len;
out[root.len] = '/';
end += 1;
// If we don't have an old path, do nothing here.
if (old_path.len > 0) {
@memmove(out[end .. end + old_path.len], old_path);
end += old_path.len;
if (oldraw_path.len > 0) {
@memmove(out[end .. end + oldraw_path.len], oldraw_path);
end += oldraw_path.len;
out[end] = '/';
end += 1;
}
@@ -184,6 +203,11 @@ pub const URL = struct {
read += 1;
}
if (hash.len > 0) {
@memmove(out[write .. write + hash.len], hash);
write += hash.len;
}
if (comptime opts.null_terminated) {
// we always have an extra space
out[write] = 0;
@@ -217,6 +241,26 @@ pub const URL = struct {
buf.appendSliceAssumeCapacity(query_string);
return buf.items;
}
// Compares two URLs, returning true if it is the same document.
pub fn eqlDocument(self: *const URL, other: *const URL, arena: Allocator) !bool {
if (!std.mem.eql(u8, self.scheme(), other.scheme())) return false;
if (!std.mem.eql(u8, self.host(), other.host())) return false;
if (self.port() != other.port()) return false;
const path1 = try self.uri.path.toRawMaybeAlloc(arena);
const path2 = try other.uri.path.toRawMaybeAlloc(arena);
if ((self.uri.query == null) != (other.uri.query == null)) return false;
if (self.uri.query) |self_query| {
const other_query = other.uri.query.?;
const query1 = try self_query.toRawMaybeAlloc(arena);
const query2 = try other_query.toRawMaybeAlloc(arena);
if (!std.mem.eql(u8, query1, query2)) return false;
}
return std.mem.eql(u8, path1, path2);
}
};
const StitchOpts = struct {
@@ -553,3 +597,92 @@ test "URL: concatQueryString" {
try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url);
}
}
test "URL: eqlDocument" {
defer testing.reset();
const arena = testing.arena_allocator;
{
const url1 = try URL.parse("https://lightpanda.io/about", null);
const url2 = try URL.parse("https://lightpanda.io/about", null);
try testing.expectEqual(true, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://lightpanda.io/about", null);
const url2 = try URL.parse("http://lightpanda.io/about", null);
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://lightpanda.io/about", null);
const url2 = try URL.parse("https://example.com/about", null);
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://lightpanda.io:8080/about", null);
const url2 = try URL.parse("https://lightpanda.io:9090/about", null);
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://lightpanda.io/about", null);
const url2 = try URL.parse("https://lightpanda.io/contact", null);
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://lightpanda.io/about?foo=bar", null);
const url2 = try URL.parse("https://lightpanda.io/about?baz=qux", null);
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://lightpanda.io/about#section1", null);
const url2 = try URL.parse("https://lightpanda.io/about#section2", null);
try testing.expectEqual(true, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://lightpanda.io/about", null);
const url2 = try URL.parse("https://lightpanda.io/about/", null);
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://lightpanda.io/", null);
const url2 = try URL.parse("https://lightpanda.io", null);
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://lightpanda.io/about?foo=bar", null);
const url2 = try URL.parse("https://lightpanda.io/about", null);
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://lightpanda.io/about", null);
const url2 = try URL.parse("https://lightpanda.io/about?foo=bar", null);
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://lightpanda.io/about?foo=bar", null);
const url2 = try URL.parse("https://lightpanda.io/about?foo=bar", null);
try testing.expectEqual(true, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://lightpanda.io/about?", null);
const url2 = try URL.parse("https://lightpanda.io/about", null);
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
}
{
const url1 = try URL.parse("https://duckduckgo.com/", null);
const url2 = try URL.parse("https://duckduckgo.com/?q=lightpanda", null);
try testing.expectEqual(false, try url1.eqlDocument(&url2, arena));
}
}

176
vendor/ada/root.zig vendored Normal file
View File

@@ -0,0 +1,176 @@
//! Wrappers for ada URL parser.
//! https://github.com/ada-url/ada
const c = @cImport({
@cInclude("ada_c.h");
});
pub const URLComponents = c.ada_url_components;
pub const URLOmitted = c.ada_url_omitted;
pub const String = c.ada_string;
pub const OwnedString = c.ada_owned_string;
/// Pointer types.
pub const URL = c.ada_url;
pub const URLSearchParams = c.ada_url_search_params;
pub const ParseError = error{Invalid};
pub fn parse(input: []const u8) ParseError!URL {
const url = c.ada_parse(input.ptr, input.len);
if (!c.ada_is_valid(url)) {
return error.Invalid;
}
return url;
}
pub fn parseWithBase(input: []const u8, base: []const u8) ParseError!URL {
const url = c.ada_parse_with_base(input.ptr, input.len, base.ptr, base.len);
if (!c.ada_is_valid(url)) {
return error.Invalid;
}
return url;
}
pub inline fn canParse(input: []const u8) bool {
return c.ada_can_parse(input.ptr, input.len);
}
pub inline fn canParseWithBase(input: []const u8, base: []const u8) bool {
return c.ada_can_parse_with_base(input.ptr, input.len, base.ptr, base.len);
}
pub inline fn getComponents(url: URL) *const URLComponents {
return c.ada_get_components(url);
}
pub inline fn free(url: URL) void {
return c.ada_free(url);
}
pub inline fn freeOwnedString(owned: OwnedString) void {
return c.ada_free_owned_string(owned);
}
/// Returns true if given URL is valid.
pub inline fn isValid(url: URL) bool {
return c.ada_is_valid(url);
}
/// Creates a new `URL` from given `URL`.
pub inline fn copy(url: URL) URL {
return c.ada_copy(url);
}
/// Contrary to other getters, this heap allocates.
pub inline fn getOriginNullable(url: URL) OwnedString {
return c.ada_get_origin(url);
}
pub inline fn getHrefNullable(url: URL) String {
return c.ada_get_href(url);
}
pub inline fn getUsernameNullable(url: URL) String {
return c.ada_get_username(url);
}
pub inline fn getPasswordNullable(url: URL) String {
return c.ada_get_password(url);
}
pub inline fn getSearchNullable(url: URL) String {
return c.ada_get_search(url);
}
pub inline fn getPortNullable(url: URL) String {
return c.ada_get_port(url);
}
pub inline fn getHashNullable(url: URL) String {
return c.ada_get_hash(url);
}
pub inline fn getHostNullable(url: URL) String {
return c.ada_get_host(url);
}
pub inline fn getHostnameNullable(url: URL) String {
return c.ada_get_hostname(url);
}
pub inline fn getPathnameNullable(url: URL) String {
return c.ada_get_pathname(url);
}
pub inline fn getProtocolNullable(url: URL) String {
return c.ada_get_protocol(url);
}
pub inline fn setHref(url: URL, input: []const u8) bool {
return c.ada_set_href(url, input.ptr, input.len);
}
pub inline fn setHost(url: URL, input: []const u8) bool {
return c.ada_set_host(url, input.ptr, input.len);
}
pub inline fn setHostname(url: URL, input: []const u8) bool {
return c.ada_set_hostname(url, input.ptr, input.len);
}
pub inline fn setProtocol(url: URL, input: []const u8) bool {
return c.ada_set_protocol(url, input.ptr, input.len);
}
pub inline fn setUsername(url: URL, input: []const u8) bool {
return c.ada_set_username(url, input.ptr, input.len);
}
pub inline fn setPassword(url: URL, input: []const u8) bool {
return c.ada_set_password(url, input.ptr, input.len);
}
pub inline fn setPort(url: URL, input: []const u8) bool {
return c.ada_set_port(url, input.ptr, input.len);
}
pub inline fn setPathname(url: URL, input: []const u8) bool {
return c.ada_set_pathname(url, input.ptr, input.len);
}
pub inline fn setSearch(url: URL, input: []const u8) void {
return c.ada_set_search(url, input.ptr, input.len);
}
pub inline fn setHash(url: URL, input: []const u8) void {
return c.ada_set_hash(url, input.ptr, input.len);
}
pub inline fn clearHash(url: URL) void {
return c.ada_clear_hash(url);
}
pub inline fn clearSearch(url: URL) void {
return c.ada_clear_search(url);
}
pub inline fn clearPort(url: URL) void {
return c.ada_clear_port(url);
}
pub const Scheme = struct {
pub const http: u8 = 0;
pub const not_special: u8 = 1;
pub const https: u8 = 2;
pub const ws: u8 = 3;
pub const ftp: u8 = 4;
pub const wss: u8 = 5;
pub const file: u8 = 6;
};
/// Returns one of the constants defined in `Scheme`.
pub inline fn getSchemeType(url: URL) u8 {
return c.ada_get_scheme_type(url);
}