61 Commits

Author SHA1 Message Date
Pierre Tachoire
cc83d85542 Merge pull request #1188 from lightpanda-io/script-load-order
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
add a test for script load order
2025-10-28 14:12:31 +01:00
Pierre Tachoire
706a87a458 keep consistent queue for inline 2025-10-28 13:12:18 +01:00
Pierre Tachoire
3ec15ad1f7 add a test for script load order 2025-10-28 13:12:18 +01:00
Karl Seguin
07e603ecda Merge pull request #1186 from lightpanda-io/defer-module
module scripts are deferred by default
2025-10-28 18:36:54 +08:00
Pierre Tachoire
52fc2c365f use getList() to pick the right queue w/ inline scripts 2025-10-28 11:23:29 +01:00
Pierre Tachoire
8f3620adf0 modules are deferred by default 2025-10-28 09:17:57 +01:00
Karl Seguin
f7abf0956f Merge pull request #1184 from lightpanda-io/usage-fix
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
add --log_filter_scopes usage
2025-10-28 10:07:34 +08:00
Karl Seguin
73217f7832 Merge pull request #1185 from lightpanda-io/fix-script-print-wait-analysis
fix printWaitAnalysis with queue name changes
2025-10-28 10:07:13 +08:00
Pierre Tachoire
52fb2010fc fix printWaitAnalysis with queue name changes 2025-10-27 17:50:30 +01:00
Pierre Tachoire
03ffcdb604 add --log_filter_scopes usage 2025-10-27 17:37:05 +01:00
Karl Seguin
20314fccec Merge pull request #1182 from lightpanda-io/navigation-file-fix
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Fix build issues related to Navigation
2025-10-27 23:08:01 +08:00
Muki Kiboigo
018e95bea7 rename navigation.zig to navigation/root.zig 2025-10-27 07:48:22 -07:00
Pierre Tachoire
c9dc4ef57a Merge pull request #1144 from lightpanda-io/readme-mac
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
update mac instructions to build from source
2025-10-24 09:59:27 +02:00
Pierre Tachoire
6c9d013e20 update mac instructions to build from source 2025-10-24 09:58:38 +02:00
Pierre Tachoire
d2d10d5db4 Merge pull request #1175 from lightpanda-io/readme-cdp
README: add CDP link
2025-10-24 09:57:15 +02:00
Pierre Tachoire
37a8a24528 README: add CDP link 2025-10-24 09:53:29 +02:00
Pierre Tachoire
d0b83c674c Merge pull request #1138 from lightpanda-io/navigation
add `Navigation` WebAPI
2025-10-24 09:30:09 +02:00
Pierre Tachoire
b58ff2c869 Merge pull request #1171 from lightpanda-io/cdp-lifecycle
support url on createTarget and send lifecycle events
2025-10-24 08:33:11 +02:00
Pierre Tachoire
b2e41837d9 Merge pull request #1174 from lightpanda-io/nikneym/url-can-parse
Add `URL.canParse`
2025-10-24 08:32:25 +02:00
Halil Durak
2e6ec1e23b add URL.canParse test 2025-10-23 13:31:01 +03:00
Halil Durak
7808d12de2 add URL.canParse static method 2025-10-23 13:30:39 +03:00
Halil Durak
1015fc09ee Merge pull request #1170 from lightpanda-io/nikneym/ada-in-web-apis
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Use ada-url for URL operations in web APIs
2025-10-23 12:13:45 +03:00
Pierre Tachoire
1c37b1c70e Merge pull request #1173 from lightpanda-io/renderer-size
renderer: set a default box size of 5 pixels
2025-10-23 10:17:10 +02:00
Muki Kiboigo
28ec8d4b94 use page arena in get_sameDocument 2025-10-22 08:42:53 -07:00
Muki Kiboigo
6e42df2e71 set oncurrententrychange callback to null 2025-10-22 08:42:26 -07:00
Muki Kiboigo
6b924e8a4c use toEventTarget in NavigationEventTarget 2025-10-22 07:54:17 -07:00
Muki Kiboigo
80ae3c9fc6 not implemented on Navigation traverseTo 2025-10-22 07:54:10 -07:00
Pierre Tachoire
2422c8718c renderer: set a default box size of 5 pixels 2025-10-22 15:54:43 +02:00
Pierre Tachoire
b5ef8418a6 cdp: fix double createTarget response 2025-10-22 14:18:53 +02:00
Halil Durak
8d4cf400ce bring back invalidUrl test with error expectation 2025-10-22 15:03:06 +03:00
Halil Durak
c6a0368c61 add a searchParamsSetHref test according to href setter change 2025-10-22 15:02:48 +03:00
Halil Durak
033eb82ae5 reinitialize search_params too when href set 2025-10-22 15:01:27 +03:00
Halil Durak
2d14452dda remove stale todo comments 2025-10-22 13:40:44 +03:00
Pierre Tachoire
a69164b482 page: fix page mode when loading about:blank 2025-10-22 12:08:27 +02:00
Halil Durak
d4d35670a0 prefer call_arena in web APIs 2025-10-22 11:42:16 +03:00
Muki Kiboigo
b40e7ece91 no nullable url on Navigation pushEntry 2025-10-21 19:25:24 -07:00
Muki Kiboigo
9c4367b26e check query on eqlDocument 2025-10-21 19:23:19 -07:00
Muki Kiboigo
0eb639ac76 fix navigation shortcut URL stitching 2025-10-21 18:31:41 -07:00
Muki Kiboigo
9778eed1ed clean up Navigation test names 2025-10-21 18:31:41 -07:00
Muki Kiboigo
8b4ffeb911 fix NavigationCurrentEntryChange Constructor 2025-10-21 18:31:41 -07:00
Muki Kiboigo
b55b9bba0a functional NavigationCurrentEntryChangeEvent 2025-10-21 18:31:39 -07:00
Muki Kiboigo
82a45253de add direct event handlers 2025-10-21 18:31:09 -07:00
Muki Kiboigo
4c957041e2 add tests for eqlDocument 2025-10-21 18:31:08 -07:00
Muki Kiboigo
b8f9598de3 add NavigationCurrentEntryChangeEvent 2025-10-21 18:31:07 -07:00
Muki Kiboigo
907bd33d87 split NavigationType and NavigationKind 2025-10-21 18:29:28 -07:00
Muki Kiboigo
e9b08f19cf fix navigation and related tests 2025-10-21 18:29:28 -07:00
Muki Kiboigo
f97697535f History as compat layer over Navigation 2025-10-21 18:29:28 -07:00
Muki Kiboigo
e80c8d5bff add functional Navigation 2025-10-21 18:29:28 -07:00
Muki Kiboigo
70a009a52b add eqlDocument comparison 2025-10-21 18:29:28 -07:00
Muki Kiboigo
8ab9364f19 add ENUM_JS_USE_TAG for enums 2025-10-21 18:29:27 -07:00
Muki Kiboigo
186655e614 initial Navigation scaffolding 2025-10-21 18:29:27 -07:00
Pierre Tachoire
43958b81f8 http: remove inflight conn check
chromiumoxide sends the command while connections are in progress and it
doesn't cause issue w/ curl.
2025-10-21 17:50:11 +02:00
Pierre Tachoire
2d8a95946a cdp: dispatch lifecycle events when enable 2025-10-21 17:48:51 +02:00
Pierre Tachoire
a7c3bad9ad cdp: implement url parameter on createTarget 2025-10-21 17:45:19 +02:00
Halil Durak
7d39bc979f remove invalidUrl test in url.html 2025-10-21 16:50:16 +03:00
Halil Durak
d60d3ebaac update link.html test 2025-10-21 16:49:48 +03:00
Halil Durak
ba66b7c5db refactor HTMLAnchorElement regarding to URL changes
This still doesn't use `state` since `state` doesn't allow us to iterate the nodes when releasing the memory and we need to call `URL.destructor` when freeing. In the future, we might omit getter allocations by making such change.
2025-10-21 16:49:12 +03:00
Halil Durak
8342f0c394 omit try keyword when not necessary 2025-10-21 16:46:06 +03:00
Halil Durak
69884b9d8d Location changes regarding to changes in URL 2025-10-21 16:44:29 +03:00
Halil Durak
c568a75599 refactor URL web API 2025-10-21 16:43:09 +03:00
Halil Durak
9deb5249a9 introduce ada-url to build system
Also add ada-url bindings.
2025-10-21 16:42:01 +03:00
48 changed files with 1816 additions and 569 deletions

View File

@@ -47,7 +47,7 @@ help:
# $(ZIG) commands
# ------------
.PHONY: build build-dev run run-release shell test bench download-zig wpt data
.PHONY: build build-dev run run-release shell test bench download-zig wpt data get-v8 build-v8 build-v8-dev
.PHONY: end2end
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
@@ -112,6 +112,19 @@ end2end:
@test -d ../demo
cd ../demo && go run runner/main.go
## v8
get-v8:
@printf "\e[36mGetting v8 source...\e[0m\n"
@$(ZIG) build get-v8
build-v8-dev:
@printf "\e[36mBuilding v8 (dev)...\e[0m\n"
@$(ZIG) build build-v8
build-v8:
@printf "\e[36mBuilding v8...\e[0m\n"
@$(ZIG) build -Doptimize=ReleaseSafe build-v8
# Install and build required dependencies commands
# ------------
.PHONY: install-submodule

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

@@ -46,8 +46,6 @@ pub fn build(b: *Build) !void {
b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
);
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
@@ -61,7 +59,7 @@ pub fn build(b: *Build) !void {
.link_libc = true,
.link_libcpp = true,
});
try addDependencies(b, lightpanda_module, opts, prebuilt_v8_path);
try addDependencies(b, lightpanda_module, opts);
{
// browser
@@ -115,7 +113,7 @@ pub fn build(b: *Build) !void {
.target = target,
.optimize = optimize,
});
try addDependencies(b, wpt_module, opts, prebuilt_v8_path);
try addDependencies(b, wpt_module, opts);
// compile and install
const wpt = b.addExecutable(.{
@@ -133,9 +131,27 @@ pub fn build(b: *Build) !void {
const wpt_step = b.step("wpt", "WPT tests");
wpt_step.dependOn(&wpt_cmd.step);
}
{
// get v8
// -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
const get_v8 = b.addRunArtifact(v8.artifact("get-v8"));
const get_step = b.step("get-v8", "Get v8");
get_step.dependOn(&get_v8.step);
}
{
// build v8
// -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
const build_v8 = b.addRunArtifact(v8.artifact("build-v8"));
const build_step = b.step("build-v8", "Build v8");
build_step.dependOn(&build_v8.step);
}
}
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, prebuilt_v8_path: ?[]const u8) !void {
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !void {
try moduleNetSurf(b, mod);
mod.addImport("build_config", opts.createModule());
@@ -143,7 +159,6 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, pre
const dep_opts = .{
.target = target,
.optimize = mod.optimize.?,
.prebuilt_v8_path = prebuilt_v8_path,
};
mod.addIncludePath(b.path("vendor/lightpanda"));
@@ -156,6 +171,36 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, pre
const v8_mod = b.dependency("v8", dep_opts).module("v8");
v8_mod.addOptions("default_exports", v8_opts);
mod.addImport("v8", v8_mod);
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
const os = switch (target.result.os.tag) {
.linux => "linux",
.macos => "macos",
else => return error.UnsupportedPlatform,
};
var lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/out/{s}/{s}/obj/zig/libc_v8.a",
.{ os, release_dir },
);
std.fs.cwd().access(lib_path, .{}) catch {
// legacy path
lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/out/{s}/obj/zig/libc_v8.a",
.{release_dir},
);
};
mod.addObjectFile(mod.owner.path(lib_path));
switch (target.result.os.tag) {
.macos => {
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
mod.linkFramework("CoreFoundation", .{});
},
else => {},
}
}
{
@@ -339,6 +384,7 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, pre
try buildMbedtls(b, mod);
try buildNghttp2(b, mod);
try buildCurl(b, mod);
try buildAda(b, mod);
switch (target.result.os.tag) {
.macos => {
@@ -804,3 +850,34 @@ fn buildCurl(b: *Build, m: *Build.Module) !void {
},
});
}
pub fn buildAda(b: *Build, m: *Build.Module) !void {
const ada_dep = b.dependency("ada-singleheader", .{});
const ada_mod = b.createModule(.{
.root_source_file = b.path("vendor/ada/root.zig"),
});
const ada_lib = b.addLibrary(.{
.name = "ada",
.root_module = b.createModule(.{
.link_libcpp = true,
.target = m.resolved_target,
.optimize = m.optimize,
}),
.linkage = .static,
});
ada_lib.addCSourceFile(.{
.file = ada_dep.path("ada.cpp"),
.flags = &.{ "-std=c++20", "-O3" },
.language = .cpp,
});
ada_lib.installHeader(ada_dep.path("ada_c.h"), "ada_c.h");
// Link the library to ada module.
ada_mod.linkLibrary(ada_lib);
// Expose ada module to main module.
m.addImport("ada", ada_mod);
}

View File

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

View File

@@ -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", .{
@@ -643,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;
}

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

@@ -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");
@@ -38,6 +39,7 @@ 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 = .{
@@ -50,6 +52,7 @@ pub const Interfaces = .{
MessageEvent,
PopStateEvent,
CompositionEvent,
NavigationCurrentEntryChangeEvent,
};
pub const Union = generate.Union(Interfaces);
@@ -79,6 +82,9 @@ pub const Event = struct {
.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)).*,
},
};
}
@@ -226,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,
@@ -399,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);
}
};
@@ -1357,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

@@ -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,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;
@@ -43,6 +44,8 @@ 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
pub const Window = struct {
@@ -53,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
@@ -69,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("");
@@ -79,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(),
};
@@ -89,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;
@@ -110,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 {
@@ -142,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
@@ -190,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

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

@@ -560,6 +560,7 @@ pub const EventType = enum(u8) {
keyboard_event = 8,
pop_state = 9,
composition_event = 10,
navigation_current_entry_change_event = 11,
};
pub const MutationEvent = c.dom_mutation_event;
@@ -831,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);
@@ -1043,8 +1063,25 @@ pub const Page = struct {
// 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:
@@ -1067,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.
@@ -1120,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 {
@@ -1178,6 +1217,7 @@ pub const NavigateReason = enum {
form,
script,
history,
navigation,
};
pub const NavigateOpts = struct {
@@ -1186,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

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

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

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

@@ -338,8 +338,6 @@ pub fn restoreOriginalProxy(self: *Client) !void {
// Enable TLS verification on all connections.
pub fn enableTlsVerify(self: *const Client) !void {
try self.ensureNoActiveConnection();
for (self.handles.handles) |*h| {
const easy = h.conn.easy;
@@ -355,8 +353,6 @@ pub fn enableTlsVerify(self: *const Client) !void {
// Disable TLS verification on all connections.
pub fn disableTlsVerify(self: *const Client) !void {
try self.ensureNoActiveConnection();
for (self.handles.handles) |*h| {
const easy = h.conn.easy;

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

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

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

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);
}