94 Commits

Author SHA1 Message Date
Pierre Tachoire
cc83d85542 Merge pull request #1188 from lightpanda-io/script-load-order
Some checks are pending
e2e-test / zig build release (push) Waiting to run
e2e-test / demo-scripts (push) Blocked by required conditions
e2e-test / cdp-and-hyperfine-bench (push) Blocked by required conditions
e2e-test / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / browser fetch (push) Blocked by required conditions
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
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
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
70 changed files with 2112 additions and 597 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

@@ -9,5 +9,9 @@
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
},
//.v8 = .{ .path = "../zig-v8-fork" }
.@"ada-singleheader" = .{
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
},
},
}

12
flake.lock generated
View File

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

View File

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

View File

@@ -212,7 +212,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
if (source == .@"inline" and self.scripts.first == null) {
// inline script with no pending scripts, execute it immediately.
// (if there is a pending script, then we cannot execute this immediately
// as it needs to best executed in order)
// as it needs to be executed in order)
return script.eval(page);
}
@@ -228,9 +228,9 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
if (source == .@"inline") {
// if we're here, it means that we have pending scripts (i.e. self.scripts
// is not empty). Because the script is inline, it's complete/ready, but
// we need to process them in order
// we need to process them in order.
pending_script.complete = true;
self.scripts.append(&pending_script.node);
pending_script.getList().append(&pending_script.node);
return;
} else {
log.debug(.http, "script queue", .{
@@ -326,16 +326,31 @@ pub fn waitForModule(self: *ScriptManager, url: [:0]const u8) !GetResult {
};
const sync = entry.value_ptr.*;
// We can have multiple scripts waiting for the same module in concurrency.
// We use the waiters to ensures only the last waiter deinit the resources.
sync.waiters += 1;
defer sync.waiters -= 1;
var client = self.client;
while (true) {
switch (sync.state) {
.loading => {},
.done => {
// Our caller has its own higher level cache (caching the
// actual compiled module). There's no reason for us to keep this
defer self.sync_module_pool.destroy(sync);
defer self.sync_modules.removeByPtr(entry.key_ptr);
if (sync.waiters == 1) {
// Our caller has its own higher level cache (caching the
// actual compiled module). There's no reason for us to keep
// this if we are the last waiter.
defer self.sync_module_pool.destroy(sync);
defer self.sync_modules.removeByPtr(entry.key_ptr);
return .{
.shared = false,
.buffer = sync.buffer,
.buffer_pool = &self.buffer_pool,
};
}
return .{
.shared = true,
.buffer = sync.buffer,
.buffer_pool = &self.buffer_pool,
};
@@ -628,6 +643,18 @@ pub const PendingScript = struct {
// if async isn't known, it'll fallback to defer.
const script = &self.script;
// Module scripts are deferred by default.
// https://v8.dev/features/modules#defer
if (script.kind == .module) {
return &self.manager.deferreds;
}
// Script is not a module but inline, we ignore async/defer properties.
if (script.source == .@"inline") {
return &self.manager.scripts;
}
if (script.is_async) {
return &self.manager.asyncs;
}
@@ -882,6 +909,8 @@ const SyncModule = struct {
manager: *ScriptManager,
buffer: std.ArrayListUnmanaged(u8) = .{},
state: State = .loading,
// number of waiters for the module.
waiters: u8 = 0,
const State = union(enum) {
done,
@@ -997,6 +1026,7 @@ pub const AsyncModule = struct {
var self: *AsyncModule = @ptrCast(@alignCast(ctx));
defer self.manager.async_module_pool.destroy(self);
self.cb(self.cb_data, .{
.shared = false,
.buffer = self.buffer,
.buffer_pool = &self.manager.buffer_pool,
});
@@ -1020,8 +1050,13 @@ pub const AsyncModule = struct {
pub const GetResult = struct {
buffer: std.ArrayListUnmanaged(u8),
buffer_pool: *BufferPool,
shared: bool,
pub fn deinit(self: *GetResult) void {
// if the result is shared, don't deinit.
if (self.shared) {
return;
}
self.buffer_pool.release(self.buffer);
}

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| {
if (state.shadow_root) |sr| {
return .{ .shadow_root = sr };
var current_root = parser.nodeGetRootNode(self);
while (true) {
const node_type = parser.nodeType(current_root);
if (node_type == .document_fragment) {
if (parser.documentFragmentGetHost(@ptrCast(current_root))) |host| {
if (page.getNodeState(host)) |state| {
if (state.shadow_root) |sr| {
if (!composed) {
return .{ .shadow_root = sr };
}
current_root = parser.nodeGetRootNode(@ptrCast(sr.host));
continue;
}
}
}
}
break;
}
return .{ .node = try Node.toInterface(root) };
return .{ .node = try Node.toInterface(current_root) };
}
pub fn _hasChildNodes(self: *parser.Node) bool {
@@ -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,8 @@ 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;
// Event interfaces
pub const Interfaces = .{
@@ -48,6 +51,8 @@ pub const Interfaces = .{
ErrorEvent,
MessageEvent,
PopStateEvent,
CompositionEvent,
NavigationCurrentEntryChangeEvent,
};
pub const Union = generate.Union(Interfaces);
@@ -72,10 +77,14 @@ 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)).*,
},
};
}
@@ -223,8 +232,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 +403,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");

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| {
const value = try js.Value.fromJson(page.js, state);
return value;
} else {
return null;
}
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;
}
}
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);
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();
defer u.destructor();
return page.call_arena.dupe(u8, u.get_protocol());
}
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
pub fn set_protocol(self: *parser.Anchor, protocol: []const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
try u.set_protocol(protocol);
u.uri.scheme = v;
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return try u.get_host(page);
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_host());
}
pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void {
// search : separator
var p: ?u16 = null;
var h: []const u8 = undefined;
for (v, 0..) |c, i| {
if (c == ':') {
h = v[0..i];
p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
break;
}
}
const arena = page.arena;
pub fn set_host(self: *parser.Anchor, host: []const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
try u.set_host(host);
if (p) |pp| {
u.uri.host = .{ .raw = h };
u.uri.port = pp;
} else {
u.uri.host = .{ .raw = v };
u.uri.port = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return u.get_hostname();
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_hostname());
}
pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
var u = try url(self, page);
u.uri.host = .{ .raw = v };
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
defer u.destructor();
try u.set_hostname(hostname);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return try u.get_port(page);
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_port());
}
pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
if (v != null and v.?.len > 0) {
u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
if (maybe_port) |port| {
try u.set_port(port);
} else {
u.uri.port = null;
u.clearPort();
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return u.get_username();
}
var u = url(self, page) catch return "";
defer u.destructor();
pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
if (v) |vv| {
u.uri.user = .{ .raw = vv };
} else {
u.uri.user = null;
const username = u.get_username();
if (username.len == 0) {
return "";
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
return page.call_arena.dupe(u8, username);
}
pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
const username = if (maybe_username) |username| username else "";
try u.set_username(username);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return try page.arena.dupe(u8, u.get_password());
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_password());
}
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
if (v) |vv| {
u.uri.password = .{ .raw = vv };
} else {
u.uri.password = null;
}
const href = try u.toString(arena);
const password = if (maybe_password) |password| password else "";
try u.set_password(password);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return u.get_pathname();
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_pathname());
}
pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void {
var u = try url(self, page);
u.uri.path = .{ .raw = v };
const href = try u.toString(arena);
defer u.destructor();
try parser.anchorSetHref(self, href);
try u.set_pathname(pathname);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return try u.get_search(page);
var u = url(self, page) catch return "";
defer u.destructor();
// This allocates in page arena so no need to dupe.
return u.get_search(page);
}
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
try u.set_search(v, page);
const href = try u.toString(page.call_arena);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page);
return try u.get_hash(page);
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_hash());
}
pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
pub fn set_hash(self: *parser.Anchor, maybe_hash: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
if (v) |vv| {
u.uri.fragment = .{ .raw = vv };
if (maybe_hash) |hash| {
try u.set_hash(hash);
} else {
u.uri.fragment = null;
u.clearHash();
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
}
};
@@ -732,6 +725,9 @@ pub const HTMLInputElement = struct {
pub fn set_value(self: *parser.Input, value: []const u8) !void {
try parser.inputSetValue(self, value);
}
pub fn _select(_: *parser.Input) void {
log.debug(.web_api, "not implemented", .{ .feature = "HTMLInputElement select" });
}
};
pub const HTMLLIElement = struct {
@@ -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

@@ -25,36 +25,37 @@ 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 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 +66,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 +75,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";
try DirectEventHandler(Window, self, "load", maybe_listener, &self.onload_callback, page.arena);
}
// 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);
}
/// Returns `onpopstate_callback`.
pub fn get_onpopstate(self: *const Window) ?js.Function {
return self.onpopstate_callback;
}
if (maybe_listener) |listener| {
switch (listener) {
// If an object is given as listener, do nothing.
.object => {},
.function => |callback| {
_ = try EventHandler.register(page.arena, event_target, event_type, listener, null) orelse unreachable;
self.onload_callback = callback;
return;
},
}
}
// Just unset the listener.
self.onload_callback = null;
/// 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
@@ -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

@@ -750,9 +750,16 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
unreachable;
},
.@"enum" => |e| {
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)),
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 => {},

View File

@@ -378,8 +378,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,6 +16,7 @@ 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("../xhr/form_data.zig").Interfaces,
@import("../xhr/File.zig"),
@import("../xmlserializer/xmlserializer.zig").Interfaces,

View File

@@ -0,0 +1,294 @@
// 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 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);
}
// 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 = page.session.navigation_kind;
if (kind) |k| {
switch (k) {
.replace => {
// When replacing, we just update the URL but the state is nullified.
const entry = self.currentEntry();
entry.url = url;
entry.state = null;
},
.push => |state| {
_ = try self.pushEntry(url, state, page, false);
},
.traverse, .reload => {},
}
} else {
_ = try self.pushEntry(url, null, 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;
}
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 = try URL.parse(url, 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,215 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const 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",
});
};
}
};
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,8 @@ pub const EventType = enum(u8) {
message_event = 7,
keyboard_event = 8,
pop_state = 9,
composition_event = 10,
navigation_current_entry_change_event = 11,
};
pub const MutationEvent = c.dom_mutation_event;
@@ -830,6 +832,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,55 @@ 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 });
// 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;
const prev = session.navigation.currentEntry();
NavigationCurrentEntryChangeEvent.dispatch(&self.session.navigation, prev, kind);
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 +1104,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 +1159,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 +1217,7 @@ pub const NavigateReason = enum {
form,
script,
history,
navigation,
};
pub const NavigateOpts = struct {
@@ -1161,6 +1226,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,

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();
}
pub fn get_username(self: *const URL) []const u8 {
const username = ada.getUsernameNullable(self.internal);
if (username.data == null) {
return "";
}
return aw.written();
return username.data[0..username.length];
}
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];
pub fn get_password(self: *const URL) []const u8 {
const password = ada.getPasswordNullable(self.internal);
if (password.data == null) {
return "";
}
return password.data[0..password.length];
}
pub fn get_username(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.user);
pub fn get_port(self: *const URL) []const u8 {
const port = ada.getPortNullable(self.internal);
if (port.data == null) {
return "";
}
return port.data[0..port.length];
}
pub fn get_password(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.password);
pub fn get_hash(self: *const URL) []const u8 {
const hash = ada.getHashNullable(self.internal);
if (hash.data == null) {
return "";
}
return hash.data[0..hash.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_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: *URL) []const u8 {
return uriComponentNullStr(self.uri.host);
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_port(self: *URL, page: *Page) ![]const u8 {
const arena = page.arena;
if (self.uri.port == null) return try arena.dupe(u8, "");
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 "/";
}
var aw = std.Io.Writer.Allocating.init(arena);
try aw.writer.printInt(self.uri.port.?, 10, .lower, .{});
return aw.written();
return path.data[0..path.length];
}
pub fn get_pathname(self: *URL) []const u8 {
if (uriComponentStr(self.uri.path).len == 0) return "/";
return uriComponentStr(self.uri.path);
}
pub fn get_search(self: *URL, page: *Page) ![]const u8 {
/// 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

@@ -702,7 +702,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

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

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

@@ -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,10 +75,6 @@ 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.
@@ -217,6 +213,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 +569,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);
}