mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 23:23:28 +00:00
Compare commits
25 Commits
cdp-browse
...
nikneym/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa00a5da52 | ||
|
|
344420f708 | ||
|
|
b87a59fa49 | ||
|
|
535a21c9f2 | ||
|
|
51a328d357 | ||
|
|
146b56c8c0 | ||
|
|
8e7d8225ba | ||
|
|
a46218cbae | ||
|
|
e9755bd38b | ||
|
|
6820a00cd0 | ||
|
|
4c4feef9fc | ||
|
|
c930d942fe | ||
|
|
c371538d27 | ||
|
|
7629bf274a | ||
|
|
9e7e9b67ff | ||
|
|
9b3be14650 | ||
|
|
6af8add7ff | ||
|
|
cf9ecbd9fd | ||
|
|
cecdd47bbc | ||
|
|
900c8d2473 | ||
|
|
c1160543ad | ||
|
|
1c7971b096 | ||
|
|
45cd494298 | ||
|
|
8f99e36cde | ||
|
|
25a1d588a9 |
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -5,7 +5,7 @@ inputs:
|
||||
zig:
|
||||
description: 'Zig version to install'
|
||||
required: false
|
||||
default: '0.15.2'
|
||||
default: '0.15.1'
|
||||
arch:
|
||||
description: 'CPU arch used to select the v8 lib'
|
||||
required: false
|
||||
|
||||
2
.github/workflows/zig-fmt.yml
vendored
2
.github/workflows/zig-fmt.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: zig-fmt
|
||||
|
||||
env:
|
||||
ZIG_VERSION: 0.15.2
|
||||
ZIG_VERSION: 0.15.1
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM debian:stable
|
||||
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG=0.15.2
|
||||
ARG ZIG=0.15.1
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG V8=14.0.365.4
|
||||
ARG ZIG_V8=v0.1.33
|
||||
|
||||
11
Makefile
11
Makefile
@@ -96,16 +96,9 @@ wpt-summary:
|
||||
@printf "\e[36mBuilding wpt...\e[0m\n"
|
||||
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
|
||||
## Test - `grep` is used to filter out the huge compile command on build
|
||||
ifeq ($(OS), macos)
|
||||
## Test
|
||||
test:
|
||||
@script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \
|
||||
| grep --line-buffered -v "^/.*zig test -freference-trace"
|
||||
else
|
||||
test:
|
||||
@script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' /dev/null 2>&1 \
|
||||
| grep --line-buffered -v "^/.*zig test -freference-trace"
|
||||
endif
|
||||
@TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
|
||||
|
||||
## Run demo/runner end to end tests
|
||||
end2end:
|
||||
|
||||
@@ -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](https://chromedevtools.github.io/devtools-protocol/)
|
||||
- Compatible with Playwright[^1], Puppeteer, chromedp through CDP
|
||||
|
||||
Fast web automation for AI agents, LLM training, scraping and testing:
|
||||
|
||||
@@ -164,7 +164,7 @@ You can also follow the progress of our Javascript support in our dedicated [zig
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to
|
||||
install it with the right version in order to build the project.
|
||||
|
||||
Lightpanda also depends on
|
||||
@@ -190,10 +190,10 @@ For systems with [Nix](https://nixos.org/download/), you can use the devShell:
|
||||
nix develop
|
||||
```
|
||||
|
||||
For MacOS, you need [Xcode](https://developer.apple.com/xcode/) and the following pacakges from homebrew:
|
||||
For MacOS, you only need cmake:
|
||||
|
||||
```
|
||||
brew install cmake pkgconf
|
||||
brew install cmake
|
||||
```
|
||||
|
||||
### Install and build dependencies
|
||||
|
||||
@@ -23,7 +23,7 @@ const Build = std.Build;
|
||||
|
||||
/// Do not rename this constant. It is scanned by some scripts to determine
|
||||
/// which zig version to install.
|
||||
const recommended_zig_version = "0.15.2";
|
||||
const recommended_zig_version = "0.15.1";
|
||||
|
||||
pub fn build(b: *Build) !void {
|
||||
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
||||
|
||||
12
flake.lock
generated
12
flake.lock
generated
@@ -75,11 +75,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1760968520,
|
||||
"narHash": "sha256-EjGslHDzCBKOVr+dnDB1CAD7wiQSHfUt3suOpFj9O1Q=",
|
||||
"lastModified": 1756822655,
|
||||
"narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e755547441a0413942a37692f7bf7fc6315bb7f6",
|
||||
"rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -136,11 +136,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1760747435,
|
||||
"narHash": "sha256-wNB/W3x+or4mdNxFPNOH5/WFckNpKgFRZk7OnOsLtm0=",
|
||||
"lastModified": 1756555914,
|
||||
"narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=",
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "d0f239b887b1ac736c0f3dde91bf5bf2ecf3a420",
|
||||
"rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
targetPkgs =
|
||||
pkgs: with pkgs; [
|
||||
# Build Tools
|
||||
zigpkgs."0.15.2"
|
||||
zigpkgs."0.15.1"
|
||||
zls
|
||||
python3
|
||||
pkg-config
|
||||
|
||||
@@ -192,7 +192,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
|
||||
if (try DataURI.parse(page.arena, src)) |data_uri| {
|
||||
source = .{ .@"inline" = data_uri };
|
||||
} else {
|
||||
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true });
|
||||
remote_url = try URL.stitch(page.arena, src, page.url.getHref(), .{ .null_terminated = true });
|
||||
source = .{ .remote = .{} };
|
||||
}
|
||||
} else {
|
||||
@@ -204,7 +204,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
|
||||
.kind = kind,
|
||||
.element = element,
|
||||
.source = source,
|
||||
.url = remote_url orelse page.url.raw,
|
||||
.url = remote_url orelse page.url.getHref(),
|
||||
.is_defer = if (remote_url == null) false else try parser.elementGetAttribute(element, "defer") != null,
|
||||
.is_async = if (remote_url == null) false else try parser.elementGetAttribute(element, "async") != null,
|
||||
};
|
||||
@@ -212,7 +212,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
|
||||
if (source == .@"inline" and self.scripts.first == null) {
|
||||
// inline script with no pending scripts, execute it immediately.
|
||||
// (if there is a pending script, then we cannot execute this immediately
|
||||
// as it needs to be executed in order)
|
||||
// as it needs to best executed in order)
|
||||
return script.eval(page);
|
||||
}
|
||||
|
||||
@@ -228,9 +228,9 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
|
||||
if (source == .@"inline") {
|
||||
// if we're here, it means that we have pending scripts (i.e. self.scripts
|
||||
// is not empty). Because the script is inline, it's complete/ready, but
|
||||
// we need to process them in order.
|
||||
// we need to process them in order
|
||||
pending_script.complete = true;
|
||||
pending_script.getList().append(&pending_script.node);
|
||||
self.scripts.append(&pending_script.node);
|
||||
return;
|
||||
} else {
|
||||
log.debug(.http, "script queue", .{
|
||||
@@ -326,31 +326,16 @@ pub fn waitForModule(self: *ScriptManager, url: [:0]const u8) !GetResult {
|
||||
};
|
||||
const sync = entry.value_ptr.*;
|
||||
|
||||
// We can have multiple scripts waiting for the same module in concurrency.
|
||||
// We use the waiters to ensures only the last waiter deinit the resources.
|
||||
sync.waiters += 1;
|
||||
defer sync.waiters -= 1;
|
||||
|
||||
var client = self.client;
|
||||
while (true) {
|
||||
switch (sync.state) {
|
||||
.loading => {},
|
||||
.done => {
|
||||
if (sync.waiters == 1) {
|
||||
// Our caller has its own higher level cache (caching the
|
||||
// actual compiled module). There's no reason for us to keep
|
||||
// this if we are the last waiter.
|
||||
defer self.sync_module_pool.destroy(sync);
|
||||
defer self.sync_modules.removeByPtr(entry.key_ptr);
|
||||
return .{
|
||||
.shared = false,
|
||||
.buffer = sync.buffer,
|
||||
.buffer_pool = &self.buffer_pool,
|
||||
};
|
||||
}
|
||||
|
||||
// Our caller has its own higher level cache (caching the
|
||||
// actual compiled module). There's no reason for us to keep this
|
||||
defer self.sync_module_pool.destroy(sync);
|
||||
defer self.sync_modules.removeByPtr(entry.key_ptr);
|
||||
return .{
|
||||
.shared = true,
|
||||
.buffer = sync.buffer,
|
||||
.buffer_pool = &self.buffer_pool,
|
||||
};
|
||||
@@ -399,7 +384,6 @@ pub fn getAsyncModule(self: *ScriptManager, url: [:0]const u8, cb: AsyncModule.C
|
||||
pub fn staticScriptsDone(self: *ScriptManager) void {
|
||||
std.debug.assert(self.static_scripts_done == false);
|
||||
self.static_scripts_done = true;
|
||||
self.evaluate();
|
||||
}
|
||||
|
||||
// try to evaluate completed scripts (in order). This is called whenever a script
|
||||
@@ -466,12 +450,6 @@ pub fn isDone(self: *const ScriptManager) bool {
|
||||
self.deferreds.first == null; // and there are no more <script defer src=> to wait for
|
||||
}
|
||||
|
||||
fn asyncScriptIsDone(self: *ScriptManager) void {
|
||||
if (self.isDone()) {
|
||||
self.page.documentIsComplete();
|
||||
}
|
||||
}
|
||||
|
||||
fn startCallback(transfer: *Http.Transfer) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||
script.startCallback(transfer) catch |err| {
|
||||
@@ -525,7 +503,7 @@ fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
||||
const resolved_url = try URL.stitch(
|
||||
self.page.arena,
|
||||
entry.value_ptr.*,
|
||||
self.page.url.raw,
|
||||
self.page.url.getHref(),
|
||||
.{ .alloc = .if_needed, .null_terminated = true },
|
||||
);
|
||||
|
||||
@@ -617,10 +595,8 @@ pub const PendingScript = struct {
|
||||
return;
|
||||
}
|
||||
// async script can be evaluated immediately
|
||||
defer self.deinit();
|
||||
self.script.eval(manager.page);
|
||||
self.deinit();
|
||||
// asyncScriptIsDone must be run after the pending script is deinit.
|
||||
manager.asyncScriptIsDone();
|
||||
}
|
||||
|
||||
fn errorCallback(self: *PendingScript, err: anyerror) void {
|
||||
@@ -651,12 +627,6 @@ pub const PendingScript = struct {
|
||||
return &self.manager.deferreds;
|
||||
}
|
||||
|
||||
// Module scripts are deferred by default.
|
||||
// https://v8.dev/features/modules#defer
|
||||
if (script.kind == .module) {
|
||||
return &self.manager.deferreds;
|
||||
}
|
||||
|
||||
return &self.manager.scripts;
|
||||
}
|
||||
};
|
||||
@@ -903,8 +873,6 @@ const SyncModule = struct {
|
||||
manager: *ScriptManager,
|
||||
buffer: std.ArrayListUnmanaged(u8) = .{},
|
||||
state: State = .loading,
|
||||
// number of waiters for the module.
|
||||
waiters: u8 = 0,
|
||||
|
||||
const State = union(enum) {
|
||||
done,
|
||||
@@ -1020,7 +988,6 @@ pub const AsyncModule = struct {
|
||||
var self: *AsyncModule = @ptrCast(@alignCast(ctx));
|
||||
defer self.manager.async_module_pool.destroy(self);
|
||||
self.cb(self.cb_data, .{
|
||||
.shared = false,
|
||||
.buffer = self.buffer,
|
||||
.buffer_pool = &self.manager.buffer_pool,
|
||||
});
|
||||
@@ -1044,13 +1011,8 @@ pub const AsyncModule = struct {
|
||||
pub const GetResult = struct {
|
||||
buffer: std.ArrayListUnmanaged(u8),
|
||||
buffer_pool: *BufferPool,
|
||||
shared: bool,
|
||||
|
||||
pub fn deinit(self: *GetResult) void {
|
||||
// if the result is shared, don't deinit.
|
||||
if (self.shared) {
|
||||
return;
|
||||
}
|
||||
self.buffer_pool.release(self.buffer);
|
||||
}
|
||||
|
||||
|
||||
@@ -562,7 +562,7 @@ pub const Selector = union(enum) {
|
||||
|
||||
const ntag = try n.tag();
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("input", ntag)) {
|
||||
if (std.ascii.eqlIgnoreCase("intput", ntag)) {
|
||||
const ntype = try n.attr("type");
|
||||
if (ntype == null) return false;
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
@@ -314,11 +313,6 @@ pub const Document = struct {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.adopted_style_sheets = try sheets.persist();
|
||||
}
|
||||
|
||||
pub fn _hasFocus(_: *parser.Document) bool {
|
||||
log.debug(.web_api, "not implemented", .{ .feature = "Document hasFocus" });
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -34,7 +34,6 @@ 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
|
||||
@@ -83,11 +82,6 @@ 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) };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -286,7 +286,7 @@ const Opts = struct {
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection
|
||||
// HTMLCollection is re implemented in zig here because libdom
|
||||
// dom_html_collection expects a comparison function callback as argument.
|
||||
// dom_html_collection expects a comparison function callback as arguement.
|
||||
// But we wanted a dynamically comparison here, according to the match tagname.
|
||||
pub const HTMLCollection = struct {
|
||||
matcher: Matcher,
|
||||
|
||||
@@ -119,8 +119,8 @@ pub const Node = struct {
|
||||
// --------
|
||||
|
||||
// Read-only attributes
|
||||
pub fn get_baseURI(_: *parser.Node, page: *Page) ![]const u8 {
|
||||
return page.url.raw;
|
||||
pub fn get_baseURI(_: *parser.Node, page: *Page) []const u8 {
|
||||
return page.url.getHref();
|
||||
}
|
||||
|
||||
pub fn get_firstChild(self: *parser.Node) !?Union {
|
||||
@@ -360,30 +360,18 @@ pub const Node = struct {
|
||||
node: Union,
|
||||
};
|
||||
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
|
||||
const composed = if (options) |opts| opts.composed else false;
|
||||
if (options) |options_| if (options_.composed) {
|
||||
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
|
||||
};
|
||||
|
||||
var current_root = parser.nodeGetRootNode(self);
|
||||
|
||||
while (true) {
|
||||
const node_type = parser.nodeType(current_root);
|
||||
|
||||
if (node_type == .document_fragment) {
|
||||
if (parser.documentFragmentGetHost(@ptrCast(current_root))) |host| {
|
||||
if (page.getNodeState(host)) |state| {
|
||||
if (state.shadow_root) |sr| {
|
||||
if (!composed) {
|
||||
return .{ .shadow_root = sr };
|
||||
}
|
||||
current_root = parser.nodeGetRootNode(@ptrCast(sr.host));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
const root = parser.nodeGetRootNode(self);
|
||||
if (page.getNodeState(root)) |state| {
|
||||
if (state.shadow_root) |sr| {
|
||||
return .{ .shadow_root = sr };
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return .{ .node = try Node.toInterface(current_root) };
|
||||
return .{ .node = try Node.toInterface(root) };
|
||||
}
|
||||
|
||||
pub fn _hasChildNodes(self: *parser.Node) bool {
|
||||
@@ -473,7 +461,7 @@ pub const Node = struct {
|
||||
|
||||
// Check if the hierarchy node tree constraints are respected.
|
||||
// For now, it checks only if new nodes are not self.
|
||||
// TODO implements the others constraints.
|
||||
// TODO implements the others contraints.
|
||||
// see https://dom.spec.whatwg.org/#concept-node-tree
|
||||
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
|
||||
for (nodes) |n| {
|
||||
|
||||
@@ -47,9 +47,6 @@ pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !Veri
|
||||
const node_type = parser.nodeType(node);
|
||||
|
||||
// Verify that we can show this node type.
|
||||
// Per the DOM spec, what_to_show filters which nodes to return, but should
|
||||
// still traverse children. So we return .skip (not .reject) when the node
|
||||
// type doesn't match.
|
||||
if (!switch (node_type) {
|
||||
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
|
||||
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
|
||||
@@ -63,7 +60,7 @@ pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !Veri
|
||||
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
|
||||
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
|
||||
.text => what_to_show & NodeFilter._SHOW_TEXT != 0,
|
||||
}) return .skip;
|
||||
}) return .reject;
|
||||
|
||||
// Verify that we aren't filtering it out.
|
||||
if (filter) |f| {
|
||||
|
||||
@@ -74,10 +74,10 @@ pub const NodeIterator = struct {
|
||||
|
||||
return .{
|
||||
.root = node,
|
||||
.filter = filter,
|
||||
.reference_node = node,
|
||||
.filter_func = filter_func,
|
||||
.what_to_show = what_to_show,
|
||||
.filter = filter,
|
||||
.filter_func = filter_func,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,27 +115,14 @@ pub const NodeIterator = struct {
|
||||
|
||||
if (try self.firstChild(self.reference_node)) |child| {
|
||||
self.reference_node = child;
|
||||
self.pointer_before_current = false;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
var current = self.reference_node;
|
||||
while (current != self.root) {
|
||||
// Try to get next sibling (including .skip/.reject nodes we need to descend into)
|
||||
if (try self.nextSiblingOrSkipReject(current)) |result| {
|
||||
if (result.should_descend) {
|
||||
// This is a .skip/.reject node - try to find acceptable children within it
|
||||
if (try self.firstChild(result.node)) |child| {
|
||||
self.reference_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
// No acceptable children, continue looking at this node's siblings
|
||||
current = result.node;
|
||||
continue;
|
||||
}
|
||||
// This is an .accept node - return it
|
||||
self.reference_node = result.node;
|
||||
return try Node.toInterface(result.node);
|
||||
if (try self.nextSibling(current)) |sibling| {
|
||||
self.reference_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
}
|
||||
|
||||
current = (parser.nodeParentNode(current)) orelse break;
|
||||
@@ -267,22 +254,6 @@ pub const NodeIterator = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the next sibling that is either acceptable or should be descended into (skip/reject)
|
||||
fn nextSiblingOrSkipReject(self: *const NodeIterator, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
|
||||
var current = node;
|
||||
|
||||
while (true) {
|
||||
current = (parser.nodeNextSibling(current)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return .{ .node = current, .should_descend = false },
|
||||
.skip, .reject => return .{ .node = current, .should_descend = true },
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn callbackStart(self: *NodeIterator) !void {
|
||||
if (self.is_in_callback) {
|
||||
// this is the correct DOMExeption
|
||||
|
||||
@@ -195,7 +195,7 @@ test "Performance: now" {
|
||||
}
|
||||
|
||||
var after = perf._now();
|
||||
while (after <= now) { // Loop until after > now
|
||||
while (after <= now) { // Loop untill after > now
|
||||
try testing.expectEqual(after, now);
|
||||
after = perf._now();
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ pub const Range = struct {
|
||||
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
|
||||
try ensureValidOffset(node, offset_);
|
||||
const offset: u32 = @intCast(offset_);
|
||||
const position = compare(node, offset, self.proto.end_node, self.proto.end_offset) catch |err| switch (err) {
|
||||
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
|
||||
error.WrongDocument => blk: {
|
||||
// allow a node with a different root than the current, or
|
||||
// a disconnected one. Treat it as if it's "after", so that
|
||||
@@ -103,7 +103,7 @@ pub const Range = struct {
|
||||
};
|
||||
|
||||
if (position == 1) {
|
||||
// if we're setting the node after the current end, the end must
|
||||
// if we're setting the node after the current start, the end must
|
||||
// be set too.
|
||||
self.proto.end_offset = offset;
|
||||
self.proto.end_node = node;
|
||||
@@ -378,7 +378,7 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
|
||||
|
||||
const child_parent, const child_index = try getParentAndIndex(child);
|
||||
std.debug.assert(node_a == child_parent);
|
||||
return if (offset_a <= child_index) -1 else 1;
|
||||
return if (child_index < offset_a) -1 else 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
|
||||
@@ -144,23 +144,6 @@ pub const TreeWalker = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the next sibling that is either acceptable or should be descended into (skip)
|
||||
fn nextSiblingOrSkip(self: *const TreeWalker, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
|
||||
var current = node;
|
||||
|
||||
while (true) {
|
||||
current = (parser.nodeNextSibling(current)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return .{ .node = current, .should_descend = false },
|
||||
.skip => return .{ .node = current, .should_descend = true },
|
||||
.reject => continue,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
||||
var current = node;
|
||||
|
||||
@@ -210,36 +193,19 @@ pub const TreeWalker = struct {
|
||||
}
|
||||
|
||||
pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
|
||||
var current = self.current_node;
|
||||
|
||||
// First, try to go to first child of current node
|
||||
if (try self.firstChild(current)) |child| {
|
||||
if (try self.firstChild(self.current_node)) |child| {
|
||||
self.current_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
// No acceptable children, move to next node in tree
|
||||
var current = self.current_node;
|
||||
while (current != self.root) {
|
||||
const result = try self.nextSiblingOrSkip(current) orelse {
|
||||
// No next sibling, go up to parent and continue
|
||||
// or, if there is no parent, we're done
|
||||
current = (parser.nodeParentNode(current)) orelse break;
|
||||
continue;
|
||||
};
|
||||
|
||||
if (!result.should_descend) {
|
||||
// This is an .accept node - return it
|
||||
self.current_node = result.node;
|
||||
return try Node.toInterface(result.node);
|
||||
if (try self.nextSibling(current)) |sibling| {
|
||||
self.current_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
}
|
||||
|
||||
// This is a .skip node - try to find acceptable children within it
|
||||
if (try self.firstChild(result.node)) |child| {
|
||||
self.current_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
// No acceptable children, continue looking at this node's siblings
|
||||
current = result.node;
|
||||
current = (parser.nodeParentNode(current)) orelse break;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -236,10 +236,10 @@ fn isVoid(elem: *parser.Element) !bool {
|
||||
};
|
||||
}
|
||||
|
||||
fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void {
|
||||
fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
|
||||
var v = value;
|
||||
while (v.len > 0) {
|
||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', 194 }) orelse {
|
||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
|
||||
return writer.writeAll(v);
|
||||
};
|
||||
try writer.writeAll(v[0..index]);
|
||||
@@ -247,22 +247,13 @@ fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void {
|
||||
'&' => try writer.writeAll("&"),
|
||||
'<' => try writer.writeAll("<"),
|
||||
'>' => try writer.writeAll(">"),
|
||||
194 => {
|
||||
// non breaking space
|
||||
if (v.len > index + 1 and v[index + 1] == 160) {
|
||||
try writer.writeAll(" ");
|
||||
v = v[index + 2 ..];
|
||||
continue;
|
||||
}
|
||||
try writer.writeByte(194);
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
v = v[index + 1 ..];
|
||||
}
|
||||
}
|
||||
|
||||
fn writeEscapedAttributeValue(writer: *std.Io.Writer, value: []const u8) !void {
|
||||
fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
|
||||
var v = value;
|
||||
while (v.len > 0) {
|
||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
|
||||
pub const CompositionEvent = struct {
|
||||
data: []const u8,
|
||||
proto: parser.Event,
|
||||
|
||||
pub const union_make_copy = true;
|
||||
pub const prototype = *parser.Event;
|
||||
|
||||
pub const ConstructorOptions = struct {
|
||||
data: []const u8 = "",
|
||||
};
|
||||
|
||||
pub fn constructor(event_type: []const u8, options_: ?ConstructorOptions) !CompositionEvent {
|
||||
const options: ConstructorOptions = options_ orelse .{};
|
||||
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, event_type, .{});
|
||||
parser.eventSetInternalType(event, .composition_event);
|
||||
|
||||
return .{
|
||||
.proto = event.*,
|
||||
.data = options.data,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_data(self: *const CompositionEvent) []const u8 {
|
||||
return self.data;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Events.Composition" {
|
||||
try testing.htmlRunner("events/composition.html");
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
// 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,8 +37,6 @@ const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
|
||||
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
||||
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
||||
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
|
||||
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
|
||||
const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent;
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = .{
|
||||
@@ -51,8 +48,6 @@ pub const Interfaces = .{
|
||||
ErrorEvent,
|
||||
MessageEvent,
|
||||
PopStateEvent,
|
||||
CompositionEvent,
|
||||
NavigationCurrentEntryChangeEvent,
|
||||
};
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
@@ -77,14 +72,10 @@ pub const Event = struct {
|
||||
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
|
||||
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
|
||||
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
|
||||
.error_event => .{ .ErrorEvent = (@as(*ErrorEvent, @fieldParentPtr("proto", evt))).* },
|
||||
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
|
||||
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
|
||||
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
|
||||
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
|
||||
.composition_event => .{ .CompositionEvent = (@as(*CompositionEvent, @fieldParentPtr("proto", evt))).* },
|
||||
.navigation_current_entry_change_event => .{
|
||||
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -232,6 +223,8 @@ 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,
|
||||
@@ -403,40 +396,6 @@ 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");
|
||||
|
||||
@@ -130,7 +130,7 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re
|
||||
|
||||
const url: [:0]const u8 = blk: switch (input) {
|
||||
.string => |str| {
|
||||
break :blk try URL.stitch(arena, str, page.url.raw, .{ .null_terminated = true });
|
||||
break :blk try URL.stitch(arena, str, page.url.getHref(), .{ .null_terminated = true });
|
||||
},
|
||||
.request => |req| {
|
||||
break :blk try arena.dupeZ(u8, req.url);
|
||||
|
||||
@@ -21,79 +21,140 @@ 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 ScrollRestorationMode = enum {
|
||||
pub const ENUM_JS_USE_TAG = true;
|
||||
const HistoryEntry = struct {
|
||||
url: []const u8,
|
||||
// This is serialized as JSON because
|
||||
// History must survive a JsContext.
|
||||
state: ?[]u8,
|
||||
};
|
||||
|
||||
const ScrollRestorationMode = enum {
|
||||
auto,
|
||||
manual,
|
||||
|
||||
pub fn fromString(str: []const u8) ?ScrollRestorationMode {
|
||||
for (std.enums.values(ScrollRestorationMode)) |mode| {
|
||||
if (std.ascii.eqlIgnoreCase(str, @tagName(mode))) {
|
||||
return mode;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toString(self: ScrollRestorationMode) []const u8 {
|
||||
return @tagName(self);
|
||||
}
|
||||
};
|
||||
|
||||
scroll_restoration: ScrollRestorationMode = .auto,
|
||||
stack: std.ArrayListUnmanaged(HistoryEntry) = .empty,
|
||||
current: ?usize = null,
|
||||
|
||||
pub fn get_length(_: *History, page: *Page) u32 {
|
||||
return @intCast(page.session.navigation.entries.items.len);
|
||||
pub fn get_length(self: *History) u32 {
|
||||
return @intCast(self.stack.items.len);
|
||||
}
|
||||
|
||||
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
|
||||
return self.scroll_restoration;
|
||||
}
|
||||
|
||||
pub fn set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
|
||||
self.scroll_restoration = mode;
|
||||
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
|
||||
self.scroll_restoration = ScrollRestorationMode.fromString(mode) orelse self.scroll_restoration;
|
||||
}
|
||||
|
||||
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;
|
||||
pub fn get_state(self: *History, page: *Page) !?js.Value {
|
||||
if (self.current) |curr| {
|
||||
const entry = self.stack.items[curr];
|
||||
if (entry.state) |state| {
|
||||
const value = try js.Value.fromJson(page.js, state);
|
||||
return value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
||||
pub fn pushNavigation(self: *History, _url: []const u8, page: *Page) !void {
|
||||
const arena = page.session.arena;
|
||||
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
||||
const url = try arena.dupe(u8, _url);
|
||||
|
||||
const json = state.toJson(arena) catch return error.DataClone;
|
||||
_ = try page.session.navigation.pushEntry(url, json, page, true);
|
||||
const entry = HistoryEntry{ .state = null, .url = url };
|
||||
try self.stack.append(arena, entry);
|
||||
self.current = self.stack.items.len - 1;
|
||||
}
|
||||
|
||||
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
||||
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, maybe_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);
|
||||
|
||||
entry.state = json;
|
||||
entry.url = url;
|
||||
const url = if (maybe_url) |u| try arena.dupe(u8, u) else page.url.getHref();
|
||||
const entry = HistoryEntry{ .state = json, .url = url };
|
||||
try self.stack.append(arena, entry);
|
||||
self.current = self.stack.items.len - 1;
|
||||
}
|
||||
|
||||
pub fn go(_: *const History, delta: i32, page: *Page) !void {
|
||||
// 0 behaves the same as no argument, both reloading the page.
|
||||
pub fn _replaceState(self: *History, state: js.Object, _: ?[]const u8, maybe_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 (maybe_url) |u| try arena.dupe(u8, u) else page.url.getHref();
|
||||
entry.* = HistoryEntry{ .state = json, .url = url };
|
||||
} else {
|
||||
try self._pushState(state, "", maybe_url, page);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go(self: *History, delta: i32, page: *Page) !void {
|
||||
// 0 behaves the same as no argument, both reloading the page.
|
||||
// If this is getting called, there SHOULD be an entry, atleast from pushNavigation.
|
||||
const current = self.current.?;
|
||||
|
||||
const 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 > page.session.navigation.entries.items.len - 1) {
|
||||
if (index_s < 0 or index_s > self.stack.items.len - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = @as(usize, @intCast(index_s));
|
||||
const entry = page.session.navigation.entries.items[index];
|
||||
const entry = self.stack.items[index];
|
||||
self.current = index;
|
||||
|
||||
if (entry.url) |url| {
|
||||
if (try page.isSameOrigin(url)) {
|
||||
PopStateEvent.dispatch(entry.state, page);
|
||||
}
|
||||
if (try page.isSameOrigin(entry.url)) {
|
||||
History.dispatchPopStateEvent(entry.state, page);
|
||||
}
|
||||
|
||||
_ = try page.session.navigation.navigate(entry.url, .{ .traverse = index }, page);
|
||||
try page.navigateFromWebAPI(entry.url, .{ .reason = .history });
|
||||
}
|
||||
|
||||
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
|
||||
@@ -146,38 +207,9 @@ 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/history.html");
|
||||
try testing.htmlRunner("html/history/history2.html");
|
||||
try testing.htmlRunner("html/history.html");
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ pub const HTMLDocument = struct {
|
||||
|
||||
pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{
|
||||
try page.cookie_jar.forRequest(page.url, buf.writer(page.arena), .{
|
||||
.is_http = false,
|
||||
.is_navigation = true,
|
||||
});
|
||||
@@ -95,7 +95,7 @@ pub const HTMLDocument = struct {
|
||||
pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, page: *Page) ![]const u8 {
|
||||
// we use the cookie jar's allocator to parse the cookie because it
|
||||
// outlives the page's arena.
|
||||
const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str);
|
||||
const c = try Cookie.parse(page.cookie_jar.allocator, page.url, cookie_str);
|
||||
errdefer c.deinit();
|
||||
if (c.http_only) {
|
||||
c.deinit();
|
||||
@@ -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 }, .{ .push = null });
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
|
||||
|
||||
@@ -221,33 +221,53 @@ pub const HTMLAnchorElement = struct {
|
||||
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 parser.anchorSetHref(self, full);
|
||||
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *Page) !void {
|
||||
const full = try urlStitch(page.call_arena, href, page.url.getHref(), .{});
|
||||
|
||||
// Get the stored internal URL if we had one.
|
||||
if (page.getObjectData(self)) |internal_url| {
|
||||
const u = NativeURL.fromInternal(internal_url);
|
||||
// Reparse with the new href.
|
||||
_ = try u.reparse(full);
|
||||
errdefer u.deinit();
|
||||
|
||||
// TODO: Remove the entry from the map on an error situation.
|
||||
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
// We don't have internal URL stored in object_data yet.
|
||||
// Create one for this anchor element.
|
||||
const u = try NativeURL.parse(full, null);
|
||||
errdefer u.deinit();
|
||||
// Save to map.
|
||||
try page.putObjectData(self, u.internal.?);
|
||||
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
|
||||
return parser.anchorGetHrefLang(self);
|
||||
return try parser.anchorGetHrefLang(self);
|
||||
}
|
||||
|
||||
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
|
||||
return parser.anchorSetHrefLang(self, href);
|
||||
return try parser.anchorSetHrefLang(self, href);
|
||||
}
|
||||
|
||||
pub fn get_type(self: *parser.Anchor) ![]const u8 {
|
||||
return parser.anchorGetType(self);
|
||||
return try parser.anchorGetType(self);
|
||||
}
|
||||
|
||||
pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
|
||||
return parser.anchorSetType(self, t);
|
||||
return try parser.anchorSetType(self, t);
|
||||
}
|
||||
|
||||
pub fn get_rel(self: *parser.Anchor) ![]const u8 {
|
||||
return parser.anchorGetRel(self);
|
||||
return try parser.anchorGetRel(self);
|
||||
}
|
||||
|
||||
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
|
||||
return parser.anchorSetRel(self, t);
|
||||
return try parser.anchorSetRel(self, t);
|
||||
}
|
||||
|
||||
pub fn get_text(self: *parser.Anchor) !?[]const u8 {
|
||||
@@ -258,186 +278,187 @@ pub const HTMLAnchorElement = struct {
|
||||
return try parser.nodeSetTextContent(parser.anchorToNode(self), v);
|
||||
}
|
||||
|
||||
fn url(self: *parser.Anchor, page: *Page) !URL {
|
||||
// Although the URL.constructor union accepts an .{.element = X}, we
|
||||
// can't use this here because the behavior is different.
|
||||
// URL.constructor(document.createElement('a')
|
||||
// should fail (a.href isn't a valid URL)
|
||||
// But
|
||||
// document.createElement('a').host
|
||||
// should not fail, it should return an empty string
|
||||
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
|
||||
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
|
||||
fn getHref(self: *parser.Anchor) !?[]const u8 {
|
||||
return parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href");
|
||||
}
|
||||
|
||||
/// Returns the URL associated with given anchor element.
|
||||
/// Creates a new URL object if not created before.
|
||||
fn getURL(self: *parser.Anchor, page: *Page) !NativeURL {
|
||||
if (page.getObjectData(self)) |internal_url| {
|
||||
return NativeURL.fromInternal(internal_url);
|
||||
}
|
||||
return error.NotProvided;
|
||||
|
||||
// Try to get href string.
|
||||
const maybe_anchor_href = try getHref(self);
|
||||
if (maybe_anchor_href) |anchor_href| {
|
||||
// Allocate a URL for this anchor element.
|
||||
const u = try NativeURL.parse(anchor_href, null);
|
||||
// Save in map.
|
||||
try page.putObjectData(self, u.internal.?);
|
||||
|
||||
return u;
|
||||
}
|
||||
|
||||
// No anchor href string found; let's just return an error.
|
||||
return error.HrefAttributeNotGiven;
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
return u.get_origin(page);
|
||||
const u = getURL(self, page) catch return "";
|
||||
// Though we store the URL in object data map, we still have to allocate
|
||||
// for origin string sadly.
|
||||
return u.getOrigin(page.arena);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
|
||||
return page.call_arena.dupe(u8, u.get_protocol());
|
||||
const u = getURL(self, page) catch return "";
|
||||
return u.getProtocol();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
||||
const u = try getURL(self, page);
|
||||
try u.setProtocol(v);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
const NativeURL = @import("../../url.zig").URL;
|
||||
|
||||
// TODO: Return a disposable string.
|
||||
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
|
||||
return page.call_arena.dupe(u8, u.get_host());
|
||||
const u = getURL(self, page) catch return "";
|
||||
return u.host();
|
||||
}
|
||||
|
||||
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);
|
||||
pub fn set_host(self: *parser.Anchor, host_str: []const u8, page: *Page) !void {
|
||||
const u = blk: {
|
||||
if (page.getObjectData(self)) |internal_url| {
|
||||
break :blk NativeURL.fromInternal(internal_url);
|
||||
}
|
||||
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
const maybe_anchor_href = try getHref(self);
|
||||
if (maybe_anchor_href) |anchor_href| {
|
||||
const new_u = try NativeURL.parse(anchor_href, null);
|
||||
try page.putObjectData(self, new_u.internal.?);
|
||||
break :blk new_u;
|
||||
}
|
||||
|
||||
// Last resort; try to create URL object out of host_str.
|
||||
const new_u = try NativeURL.parse(host_str, null);
|
||||
// We can just return here since host is updated.
|
||||
return page.putObjectData(self, new_u.internal.?);
|
||||
};
|
||||
|
||||
try u.setHost(host_str);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
return page.call_arena.dupe(u8, u.get_hostname());
|
||||
pub fn get_hostname(self: *parser.Anchor, page: *Page) []const u8 {
|
||||
const u = getURL(self, page) catch return "";
|
||||
return u.getHostname();
|
||||
}
|
||||
|
||||
pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
try u.set_hostname(hostname);
|
||||
const u = blk: {
|
||||
if (page.getObjectData(self)) |internal_url| {
|
||||
break :blk NativeURL.fromInternal(internal_url);
|
||||
}
|
||||
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
const maybe_anchor_href = try getHref(self);
|
||||
if (maybe_anchor_href) |anchor_href| {
|
||||
const new_u = try NativeURL.parse(anchor_href, null);
|
||||
try page.putObjectData(self, new_u.internal.?);
|
||||
break :blk new_u;
|
||||
}
|
||||
|
||||
// Last resort; try to create URL object out of hostname.
|
||||
const new_u = try NativeURL.parse(hostname, null);
|
||||
// We can just return here since hostname is updated.
|
||||
return page.putObjectData(self, new_u.internal.?);
|
||||
};
|
||||
|
||||
try u.setHostname(hostname);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
return page.call_arena.dupe(u8, u.get_port());
|
||||
const u = getURL(self, page) catch return "";
|
||||
return u.getPort();
|
||||
}
|
||||
|
||||
pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
|
||||
// TODO: Check for valid port (u16 integer).
|
||||
if (maybe_port) |port| {
|
||||
try u.set_port(port);
|
||||
} else {
|
||||
u.clearPort();
|
||||
}
|
||||
const u = try getURL(self, page);
|
||||
try u.setPort(port);
|
||||
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
|
||||
const username = u.get_username();
|
||||
if (username.len == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return page.call_arena.dupe(u8, username);
|
||||
const u = try getURL(self, page);
|
||||
return u.getUsername() orelse "";
|
||||
}
|
||||
|
||||
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);
|
||||
if (maybe_username) |username| {
|
||||
const u = try getURL(self, page);
|
||||
try u.setUsername(username);
|
||||
try parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
|
||||
return page.call_arena.dupe(u8, u.get_password());
|
||||
const u = try getURL(self, page);
|
||||
return u.getPassword() orelse "";
|
||||
}
|
||||
|
||||
pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
|
||||
const password = if (maybe_password) |password| password else "";
|
||||
try u.set_password(password);
|
||||
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
if (maybe_password) |password| {
|
||||
const u = try getURL(self, page);
|
||||
try u.setPassword(password);
|
||||
try parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
|
||||
return page.call_arena.dupe(u8, u.get_pathname());
|
||||
const u = try getURL(self, page);
|
||||
return u.getPath();
|
||||
}
|
||||
|
||||
pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
|
||||
try u.set_pathname(pathname);
|
||||
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
const u = try getURL(self, page);
|
||||
try u.setPath(pathname);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
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);
|
||||
const u = try getURL(self, page);
|
||||
return u.getSearch() orelse "";
|
||||
}
|
||||
|
||||
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);
|
||||
return parser.anchorSetHref(self, href);
|
||||
pub fn set_search(self: *parser.Anchor, search: []const u8, page: *Page) !void {
|
||||
const u = try getURL(self, page);
|
||||
u.setSearch(search);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
|
||||
return page.call_arena.dupe(u8, u.get_hash());
|
||||
const u = try getURL(self, page);
|
||||
return u.getHash() orelse "";
|
||||
}
|
||||
|
||||
pub fn set_hash(self: *parser.Anchor, maybe_hash: ?[]const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
|
||||
if (maybe_hash) |hash| {
|
||||
try u.set_hash(hash);
|
||||
} else {
|
||||
u.clearHash();
|
||||
}
|
||||
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
pub fn set_hash(self: *parser.Anchor, hash: []const u8, page: *Page) !void {
|
||||
const u = try getURL(self, page);
|
||||
u.setHash(hash);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -710,7 +731,7 @@ pub const HTMLInputElement = struct {
|
||||
return try parser.inputGetSrc(self);
|
||||
}
|
||||
pub fn set_src(self: *parser.Input, src: []const u8, page: *Page) !void {
|
||||
const new_src = try urlStitch(page.call_arena, src, page.url.raw, .{ .alloc = .if_needed });
|
||||
const new_src = try urlStitch(page.call_arena, src, page.url.getHref(), .{ .alloc = .if_needed });
|
||||
try parser.inputSetSrc(self, new_src);
|
||||
}
|
||||
pub fn get_type(self: *parser.Input) ![]const u8 {
|
||||
@@ -725,9 +746,6 @@ pub const HTMLInputElement = struct {
|
||||
pub fn set_value(self: *parser.Input, value: []const u8) !void {
|
||||
try parser.inputSetValue(self, value);
|
||||
}
|
||||
pub fn _select(_: *parser.Input) void {
|
||||
log.debug(.web_api, "not implemented", .{ .feature = "HTMLInputElement select" });
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLLIElement = struct {
|
||||
@@ -766,7 +784,7 @@ pub const HTMLLinkElement = struct {
|
||||
}
|
||||
|
||||
pub fn set_href(self: *parser.Link, href: []const u8, page: *const Page) !void {
|
||||
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
|
||||
const full = try urlStitch(page.call_arena, href, page.url.getHref(), .{});
|
||||
return parser.linkSetHref(self, full);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ pub const ErrorEvent = struct {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, event_type, .{});
|
||||
parser.eventSetInternalType(event, .error_event);
|
||||
parser.eventSetInternalType(event, .event);
|
||||
|
||||
const o = opts orelse ErrorEventInit{};
|
||||
|
||||
|
||||
@@ -25,21 +25,16 @@ 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 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 }, .{ .push = null });
|
||||
return page.navigateFromWebAPI(href, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn get_protocol(self: *Location) []const u8 {
|
||||
@@ -75,15 +70,15 @@ pub const Location = struct {
|
||||
}
|
||||
|
||||
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script }, .replace);
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn _reload(_: *const Location, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script }, .reload);
|
||||
return page.navigateFromWebAPI(page.url.getHref(), .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
|
||||
|
||||
@@ -25,7 +25,7 @@ pub const SVGElement = struct {
|
||||
// Currently the prototype chain is not implemented (will not be returned by toInterface())
|
||||
// For that we need parser.SvgElement and the derived types with tags in the v-table.
|
||||
pub const prototype = *Element;
|
||||
// While this is a Node, could consider not exposing the subtype until we have
|
||||
// While this is a Node, could consider not exposing the subtype untill we have
|
||||
// a Self type to cast to.
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,6 @@ 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;
|
||||
@@ -37,14 +36,13 @@ const Screen = @import("screen.zig").Screen;
|
||||
const domcss = @import("../dom/css.zig");
|
||||
const Css = @import("../css/css.zig").Css;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
const URL = @import("../../url.zig").URL;
|
||||
const WebApiURL = @import("../url/url.zig").URL;
|
||||
|
||||
const Request = @import("../fetch/Request.zig");
|
||||
const fetchFn = @import("../fetch/fetch.zig").fetch;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
const ErrorEvent = @import("error_event.zig").ErrorEvent;
|
||||
|
||||
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-window-extensions
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
|
||||
@@ -72,7 +70,6 @@ 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("");
|
||||
@@ -80,22 +77,30 @@ pub const Window = struct {
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
try parser.documentSetDocumentURI(doc, "about:blank");
|
||||
|
||||
const native_url = URL.parse("about:blank", null) catch unreachable;
|
||||
|
||||
// Here we manually initialize; this is a special case and
|
||||
// one should prefer constructor functions instead.
|
||||
const url = WebApiURL{
|
||||
.internal = native_url.internal,
|
||||
.search_params = .{},
|
||||
};
|
||||
|
||||
return .{
|
||||
.document = html_doc,
|
||||
.location = .{ .url = url },
|
||||
.target = target orelse "",
|
||||
.location = try .init("about:blank"),
|
||||
.navigator = navigator orelse .{},
|
||||
.performance = Performance.init(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn replaceLocation(self: *Window, loc: Location) !void {
|
||||
self.location = loc;
|
||||
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 replaceLocation(self: *Window, location: Location) !void {
|
||||
// Remove current.
|
||||
self.location.url.destructor();
|
||||
// Put the new one.
|
||||
self.location = location;
|
||||
return parser.documentHTMLSetLocation(Location, self.document, &self.location);
|
||||
}
|
||||
|
||||
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
|
||||
@@ -119,17 +124,31 @@ pub const Window = struct {
|
||||
|
||||
/// Sets `onload_callback`.
|
||||
pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
|
||||
try DirectEventHandler(Window, self, "load", maybe_listener, &self.onload_callback, page.arena);
|
||||
}
|
||||
const event_target = parser.toEventTarget(Window, self);
|
||||
const event_type = "load";
|
||||
|
||||
/// Returns `onpopstate_callback`.
|
||||
pub fn get_onpopstate(self: *const Window) ?js.Function {
|
||||
return self.onpopstate_callback;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
if (maybe_listener) |listener| {
|
||||
switch (listener) {
|
||||
// If an object is given as listener, do nothing.
|
||||
.object => {},
|
||||
.function => |callback| {
|
||||
_ = try EventHandler.register(page.arena, event_target, event_type, listener, null) orelse unreachable;
|
||||
self.onload_callback = callback;
|
||||
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Just unset the listener.
|
||||
self.onload_callback = null;
|
||||
}
|
||||
|
||||
pub fn get_location(self: *Window) *Location {
|
||||
@@ -137,7 +156,7 @@ pub const Window = struct {
|
||||
}
|
||||
|
||||
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
}
|
||||
|
||||
// frames return the window itself, but accessing it via a pseudo
|
||||
@@ -185,10 +204,6 @@ pub const Window = struct {
|
||||
return &page.session.history;
|
||||
}
|
||||
|
||||
pub fn get_navigation(_: *Window, page: *Page) *Navigation {
|
||||
return &page.session.navigation;
|
||||
}
|
||||
|
||||
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
|
||||
pub fn get_innerHeight(_: *Window, page: *Page) u32 {
|
||||
// We do not have scrollbars or padding so this is the same as Element.clientHeight
|
||||
@@ -281,25 +296,6 @@ pub const Window = struct {
|
||||
return out;
|
||||
}
|
||||
|
||||
pub fn _reportError(self: *Window, err: js.Object, page: *Page) !void {
|
||||
var error_event = try ErrorEvent.constructor("error", .{
|
||||
.@"error" = err,
|
||||
});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(Window, self),
|
||||
@as(*parser.Event, &error_event.proto),
|
||||
);
|
||||
|
||||
if (parser.eventDefaultPrevented(&error_event.proto) == false) {
|
||||
const err_string = err.toString() catch "Unknown error";
|
||||
log.info(.user_script, "error", .{
|
||||
.err = err_string,
|
||||
.stack = page.stackTrace() catch "???",
|
||||
.source = "window.reportError",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const CreateTimeoutOpts = struct {
|
||||
name: []const u8,
|
||||
args: []js.Object = &.{},
|
||||
|
||||
@@ -262,7 +262,7 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
|
||||
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
|
||||
gop.key_ptr.* = owned_specifier;
|
||||
gop.value_ptr.* = .{};
|
||||
try self.script_manager.?.getModule(owned_specifier, url);
|
||||
try self.script_manager.?.getModule(owned_specifier, src);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,18 +271,7 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
|
||||
return error.ModuleInstantiationError;
|
||||
}
|
||||
|
||||
const evaluated = m.evaluate(v8_context) catch {
|
||||
std.debug.assert(m.getStatus() == .kErrored);
|
||||
|
||||
// Some module-loading errors aren't handled by TryCatch. We need to
|
||||
// get the error from the module itself.
|
||||
log.warn(.js, "evaluate module", .{
|
||||
.specifier = owned_url,
|
||||
.message = self.valueToString(m.getException(), .{}) catch "???",
|
||||
});
|
||||
return error.EvaluationError;
|
||||
};
|
||||
|
||||
const evaluated = try m.evaluate(v8_context);
|
||||
// https://v8.github.io/api/head/classv8_1_1Module.html#a1f1758265a4082595757c3251bb40e0f
|
||||
// Must be a promise that gets returned here.
|
||||
std.debug.assert(evaluated.isPromise());
|
||||
@@ -750,16 +739,9 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
|
||||
unreachable;
|
||||
},
|
||||
.@"enum" => |e| {
|
||||
if (@hasDecl(T, "ENUM_JS_USE_TAG")) {
|
||||
const str = try self.jsValueToZig(named_function, []const u8, js_value);
|
||||
return std.meta.stringToEnum(T, str) orelse return error.InvalidEnumValue;
|
||||
} else {
|
||||
switch (@typeInfo(e.tag_type)) {
|
||||
.int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_value, self.v8_context)),
|
||||
else => {
|
||||
@compileError(named_function.full_name ++ " has an unsupported enum parameter type: " ++ @typeName(T));
|
||||
},
|
||||
}
|
||||
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 => {},
|
||||
@@ -1189,7 +1171,7 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
|
||||
};
|
||||
|
||||
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
|
||||
self.arena, // might need to survive until the module is loaded
|
||||
self.call_arena,
|
||||
specifier,
|
||||
referrer_path,
|
||||
);
|
||||
@@ -1224,21 +1206,13 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
|
||||
defer try_catch.deinit();
|
||||
|
||||
const entry = self.module(true, fetch_result.src(), normalized_specifier, true) catch |err| {
|
||||
switch (err) {
|
||||
error.EvaluationError => {
|
||||
// This is a sentinel value telling us that the error was already
|
||||
// logged. Some module-loading errors aren't captured by Try/Catch.
|
||||
// We need to handle those errors differently, where the module
|
||||
// exists.
|
||||
},
|
||||
else => log.warn(.js, "compile resolved module", .{
|
||||
.specifier = normalized_specifier,
|
||||
.stack = try_catch.stack(self.call_arena) catch null,
|
||||
.src = try_catch.sourceLine(self.call_arena) catch "err",
|
||||
.line = try_catch.sourceLineNumber() orelse 0,
|
||||
.exception = (try_catch.exception(self.call_arena) catch @errorName(err)) orelse @errorName(err),
|
||||
}),
|
||||
}
|
||||
log.warn(.js, "compile resolved module", .{
|
||||
.specifier = specifier,
|
||||
.stack = try_catch.stack(self.call_arena) catch null,
|
||||
.src = try_catch.sourceLine(self.call_arena) catch "err",
|
||||
.line = try_catch.sourceLineNumber() orelse 0,
|
||||
.exception = (try_catch.exception(self.call_arena) catch @errorName(err)) orelse @errorName(err),
|
||||
});
|
||||
return null;
|
||||
};
|
||||
// entry.module is always set when returning from self.module()
|
||||
|
||||
@@ -378,13 +378,8 @@ 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 => {},
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ 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,
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const 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);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const 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");
|
||||
}
|
||||
@@ -559,8 +559,6 @@ pub const EventType = enum(u8) {
|
||||
message_event = 7,
|
||||
keyboard_event = 8,
|
||||
pop_state = 9,
|
||||
composition_event = 10,
|
||||
navigation_current_entry_change_event = 11,
|
||||
};
|
||||
|
||||
pub const MutationEvent = c.dom_mutation_event;
|
||||
@@ -832,7 +830,6 @@ 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{
|
||||
|
||||
@@ -35,9 +35,6 @@ 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;
|
||||
|
||||
@@ -87,6 +84,11 @@ pub const Page = struct {
|
||||
|
||||
polyfill_loader: polyfill.Loader = .{},
|
||||
|
||||
/// KV map for various object data; use pointers as unsigned integer keys
|
||||
/// and store any `*anyopaque` as values. If a key or value will be
|
||||
/// deinitialized (freed), it should be removed from the map too.
|
||||
object_data: ObjectDataMap = .{},
|
||||
|
||||
scheduler: Scheduler,
|
||||
http_client: *Http.Client,
|
||||
script_manager: ScriptManager,
|
||||
@@ -125,12 +127,30 @@ pub const Page = struct {
|
||||
complete,
|
||||
};
|
||||
|
||||
const ObjectDataMap = std.HashMapUnmanaged(
|
||||
usize,
|
||||
*anyopaque,
|
||||
struct {
|
||||
pub fn hash(_: @This(), key: usize) usize {
|
||||
return key;
|
||||
}
|
||||
|
||||
pub fn eql(_: @This(), a: usize, b: usize) bool {
|
||||
return a == b;
|
||||
}
|
||||
},
|
||||
std.hash_map.default_max_load_percentage,
|
||||
);
|
||||
|
||||
pub fn init(self: *Page, arena: Allocator, call_arena: Allocator, session: *Session) !void {
|
||||
const browser = session.browser;
|
||||
const script_manager = ScriptManager.init(browser, self);
|
||||
|
||||
const url = try URL.parse("about:blank", null);
|
||||
errdefer url.deinit();
|
||||
|
||||
self.* = .{
|
||||
.url = URL.empty,
|
||||
.url = url,
|
||||
.mode = .{ .pre = {} },
|
||||
.window = try Window.create(null, null),
|
||||
.arena = arena,
|
||||
@@ -153,11 +173,14 @@ pub const Page = struct {
|
||||
try self.registerBackgroundTasks();
|
||||
}
|
||||
|
||||
// FIXME: Deinit self.url.
|
||||
pub fn deinit(self: *Page) void {
|
||||
self.script_manager.shutdown = true;
|
||||
|
||||
self.http_client.abort();
|
||||
self.script_manager.deinit();
|
||||
self.url.deinit();
|
||||
self.object_data.deinit(self.arena);
|
||||
}
|
||||
|
||||
fn reset(self: *Page) !void {
|
||||
@@ -168,6 +191,12 @@ pub const Page = struct {
|
||||
self.http_client.abort();
|
||||
self.script_manager.reset();
|
||||
|
||||
_ = try self.url.reparse("about:blank");
|
||||
errdefer self.url.deinit();
|
||||
|
||||
self.object_data.deinit(self.arena);
|
||||
self.object_data = .{};
|
||||
|
||||
self.load_state = .parsing;
|
||||
self.mode = .{ .pre = {} };
|
||||
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
@@ -198,6 +227,21 @@ pub const Page = struct {
|
||||
}.runMessageLoop, 5, .{ .name = "page.messageLoop" });
|
||||
}
|
||||
|
||||
/// Returns the object data by given key.
|
||||
/// `key` must be a pointer type.
|
||||
/// Type of value is unknown to map; so the caller must do the type casting.
|
||||
pub fn getObjectData(self: *Page, key: anytype) ?*anyopaque {
|
||||
std.debug.assert(@typeInfo(@TypeOf(key)) == .pointer);
|
||||
return self.object_data.get(@intFromPtr(key));
|
||||
}
|
||||
|
||||
/// Puts the object data by given key.
|
||||
/// `key` must be a pointer type.
|
||||
pub fn putObjectData(self: *Page, key: anytype, value: *anyopaque) Allocator.Error!void {
|
||||
std.debug.assert(@typeInfo(@TypeOf(key)) == .pointer);
|
||||
return self.object_data.put(self.arena, @intFromPtr(key), value);
|
||||
}
|
||||
|
||||
pub const DumpOpts = struct {
|
||||
// set to include element shadowroots in the dump
|
||||
page: ?*const Page = null,
|
||||
@@ -242,7 +286,7 @@ pub const Page = struct {
|
||||
|
||||
const doc = parser.documentHTMLToDocument(self.window.document);
|
||||
|
||||
// if the base si requested, add the base's node in the document's headers.
|
||||
// if the base is requested, add the base's node in the document's headers.
|
||||
if (opts.with_base) {
|
||||
try self.addDOMTreeBase();
|
||||
}
|
||||
@@ -265,7 +309,7 @@ pub const Page = struct {
|
||||
const head = parser.nodeListItem(list, 0) orelse return;
|
||||
|
||||
const base = try parser.documentCreateElement(doc, "base");
|
||||
try parser.elementSetAttribute(base, "href", self.url.raw);
|
||||
try parser.elementSetAttribute(base, "href", self.url.getHref());
|
||||
|
||||
const Node = @import("dom/node.zig").Node;
|
||||
try Node.prepend(head, &[_]Node.NodeOrText{.{ .node = parser.elementToNode(base) }});
|
||||
@@ -488,16 +532,16 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
{
|
||||
std.debug.print("\nhigh_priority schedule: {d}\n", .{self.scheduler.high_priority.count()});
|
||||
var it = self.scheduler.high_priority.iterator();
|
||||
std.debug.print("\nprimary schedule: {d}\n", .{self.scheduler.primary.count()});
|
||||
var it = self.scheduler.primary.iterator();
|
||||
while (it.next()) |task| {
|
||||
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now });
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std.debug.print("\nlow_priority schedule: {d}\n", .{self.scheduler.low_priority.count()});
|
||||
var it = self.scheduler.low_priority.iterator();
|
||||
std.debug.print("\nsecondary schedule: {d}\n", .{self.scheduler.secondary.count()});
|
||||
var it = self.scheduler.secondary.iterator();
|
||||
while (it.next()) |task| {
|
||||
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now });
|
||||
}
|
||||
@@ -519,19 +563,18 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
pub fn origin(self: *const Page, arena: Allocator) ![]const u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(arena);
|
||||
try self.url.origin(&aw.writer);
|
||||
return aw.written();
|
||||
return self.url.getOrigin(arena);
|
||||
}
|
||||
|
||||
const RequestCookieOpts = struct {
|
||||
is_http: bool = true,
|
||||
is_navigation: bool = false,
|
||||
};
|
||||
|
||||
pub fn requestCookie(self: *const Page, opts: RequestCookieOpts) Http.Client.RequestCookie {
|
||||
return .{
|
||||
.jar = self.cookie_jar,
|
||||
.origin = &self.url.uri,
|
||||
.cookie_jar = self.cookie_jar,
|
||||
.origin_url = self.url,
|
||||
.is_http = opts.is_http,
|
||||
.is_navigation = opts.is_navigation,
|
||||
};
|
||||
@@ -552,31 +595,14 @@ pub const Page = struct {
|
||||
.body = opts.body != null,
|
||||
});
|
||||
|
||||
// if the url is about:blank, we load an empty HTML document in the
|
||||
// page and dispatch the events.
|
||||
// if the url is about:blank, nothing to do.
|
||||
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
|
||||
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(),
|
||||
});
|
||||
|
||||
try HTMLDocument.documentIsComplete(self.window.document, self);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -656,7 +682,7 @@ pub const Page = struct {
|
||||
};
|
||||
|
||||
self.session.browser.notification.dispatch(.page_navigated, &.{
|
||||
.url = self.url.raw,
|
||||
.url = self.url.getHref(),
|
||||
.timestamp = timestamp(),
|
||||
});
|
||||
}
|
||||
@@ -835,8 +861,8 @@ pub const Page = struct {
|
||||
},
|
||||
}
|
||||
|
||||
// We need to handle different navigation types differently.
|
||||
try self.session.navigation.processNavigation(self);
|
||||
// Push the navigation after a successful load.
|
||||
try self.session.history.pushNavigation(self.url.getHref(), self);
|
||||
}
|
||||
|
||||
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
@@ -872,14 +898,14 @@ pub const Page = struct {
|
||||
// extracted because this is called from tests to set things up.
|
||||
pub fn setDocument(self: *Page, html_doc: *parser.DocumentHTML) !void {
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
try parser.documentSetDocumentURI(doc, self.url.raw);
|
||||
try parser.documentSetDocumentURI(doc, self.url.getHref());
|
||||
|
||||
// TODO set the referrer to the document.
|
||||
try self.window.replaceDocument(html_doc);
|
||||
self.window.setStorageShelf(
|
||||
try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
|
||||
);
|
||||
try self.window.changeLocation(self.url.raw, self);
|
||||
try self.window.replaceLocation(.{ .url = try self.url.toWebApi(self.arena) });
|
||||
}
|
||||
|
||||
pub const MouseEvent = struct {
|
||||
@@ -926,7 +952,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, .{}, .{ .push = null });
|
||||
try self.navigateFromWebAPI(href, .{});
|
||||
},
|
||||
.input => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
@@ -1033,55 +1059,13 @@ pub const Page = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// insertText is a shortcut to insert text into the active element.
|
||||
pub fn insertText(self: *Page, v: []const u8) !void {
|
||||
const Document = @import("dom/document.zig").Document;
|
||||
const element = (try Document.getActiveElement(@ptrCast(self.window.document), self)) orelse return;
|
||||
const node = parser.elementToNode(element);
|
||||
|
||||
const tag = (try parser.nodeHTMLGetTagType(node)) orelse return;
|
||||
switch (tag) {
|
||||
.input => {
|
||||
const input_type = try parser.inputGetType(@ptrCast(element));
|
||||
if (std.mem.eql(u8, input_type, "text")) {
|
||||
const value = try parser.inputGetValue(@ptrCast(element));
|
||||
const new_value = try std.mem.concat(self.arena, u8, &.{ value, v });
|
||||
try parser.inputSetValue(@ptrCast(element), new_value);
|
||||
}
|
||||
},
|
||||
.textarea => {
|
||||
const value = try parser.textareaGetValue(@ptrCast(node));
|
||||
const new_value = try std.mem.concat(self.arena, u8, &.{ value, v });
|
||||
try parser.textareaSetValue(@ptrCast(node), new_value);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// We cannot navigate immediately as navigating will delete the DOM tree,
|
||||
// which holds this event's node.
|
||||
// As such we schedule the function to be called as soon as possible.
|
||||
// The page.arena is safe to use here, but the transfer_arena exists
|
||||
// specifically for this type of lifetime.
|
||||
pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts, kind: NavigationKind) !void {
|
||||
pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts) !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:
|
||||
@@ -1104,11 +1088,9 @@ pub const Page = struct {
|
||||
|
||||
session.queued_navigation = .{
|
||||
.opts = opts,
|
||||
.url = stitched_url,
|
||||
.url = try URL.stitch(session.transfer_arena, url, self.url.getHref(), .{ .alloc = .always }),
|
||||
};
|
||||
|
||||
session.navigation_kind = kind;
|
||||
|
||||
self.http_client.abort();
|
||||
|
||||
// In v8, this throws an exception which JS code cannot catch.
|
||||
@@ -1146,7 +1128,7 @@ pub const Page = struct {
|
||||
try form_data.write(encoding, buf.writer(transfer_arena));
|
||||
|
||||
const method = try parser.elementGetAttribute(@ptrCast(@alignCast(form)), "method") orelse "";
|
||||
var action = try parser.elementGetAttribute(@ptrCast(@alignCast(form)), "action") orelse self.url.raw;
|
||||
var action = try parser.elementGetAttribute(@ptrCast(@alignCast(form)), "action") orelse self.url.getHref();
|
||||
|
||||
var opts = NavigateOpts{
|
||||
.reason = .form,
|
||||
@@ -1159,7 +1141,7 @@ pub const Page = struct {
|
||||
} else {
|
||||
action = try URL.concatQueryString(transfer_arena, action, buf.items);
|
||||
}
|
||||
try self.navigateFromWebAPI(action, opts, .{ .push = null });
|
||||
try self.navigateFromWebAPI(action, opts);
|
||||
}
|
||||
|
||||
pub fn isNodeAttached(self: *const Page, node: *parser.Node) bool {
|
||||
@@ -1217,7 +1199,6 @@ pub const NavigateReason = enum {
|
||||
form,
|
||||
script,
|
||||
history,
|
||||
navigation,
|
||||
};
|
||||
|
||||
pub const NavigateOpts = struct {
|
||||
@@ -1226,7 +1207,6 @@ pub const NavigateOpts = struct {
|
||||
method: Http.Method = .GET,
|
||||
body: ?[]const u8 = null,
|
||||
header: ?[:0]const u8 = null,
|
||||
force: bool = false,
|
||||
};
|
||||
|
||||
const IdleNotification = union(enum) {
|
||||
|
||||
@@ -41,10 +41,6 @@ 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 .{
|
||||
@@ -66,10 +62,10 @@ const FlatRenderer = struct {
|
||||
gop.value_ptr.* = x;
|
||||
}
|
||||
|
||||
const _x: f64 = @floatFromInt(x * default_w);
|
||||
const _x: f64 = @floatFromInt(x);
|
||||
const y: f64 = 0.0;
|
||||
const w: f64 = default_w;
|
||||
const h: f64 = default_h;
|
||||
const w: f64 = 1.0;
|
||||
const h: f64 = 1.0;
|
||||
|
||||
return .{
|
||||
.x = _x,
|
||||
@@ -102,20 +98,18 @@ const FlatRenderer = struct {
|
||||
}
|
||||
|
||||
pub fn width(self: *const FlatRenderer) u32 {
|
||||
return @max(@as(u32, @intCast(self.elements.items.len * default_w)), default_w); // At least default width pixels even if empty
|
||||
return @max(@as(u32, @intCast(self.elements.items.len)), 1); // At least 1 pixel even if empty
|
||||
}
|
||||
|
||||
pub fn height(_: *const FlatRenderer) u32 {
|
||||
return 5;
|
||||
return 1;
|
||||
}
|
||||
|
||||
pub fn getElementAtPosition(self: *const FlatRenderer, _x: i32, y: i32) ?*parser.Element {
|
||||
if (y < 0 or y > default_h or _x < 0) {
|
||||
pub fn getElementAtPosition(self: *const FlatRenderer, x: i32, y: i32) ?*parser.Element {
|
||||
if (y != 0 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;
|
||||
}
|
||||
|
||||
@@ -22,11 +22,9 @@ 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");
|
||||
@@ -59,8 +57,6 @@ 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,
|
||||
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
const std = @import("std");
|
||||
const Uri = std.Uri;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const DateTime = @import("../../datetime.zig").DateTime;
|
||||
const public_suffix_list = @import("../../data/public_suffix_list.zig").lookup;
|
||||
|
||||
pub const LookupOpts = struct {
|
||||
request_time: ?i64 = null,
|
||||
origin_uri: ?*const Uri = null,
|
||||
is_http: bool,
|
||||
is_navigation: bool = true,
|
||||
prefix: ?[]const u8 = null,
|
||||
};
|
||||
const URL = @import("../../url.zig").URL;
|
||||
|
||||
pub const Jar = struct {
|
||||
allocator: Allocator,
|
||||
@@ -80,13 +72,22 @@ pub const Jar = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forRequest(self: *Jar, target_uri: *const Uri, writer: anytype, opts: LookupOpts) !void {
|
||||
pub const LookupOpts = struct {
|
||||
request_time: ?i64 = null,
|
||||
origin_url: ?URL = null,
|
||||
is_http: bool,
|
||||
is_navigation: bool = true,
|
||||
prefix: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
// FIXME: Invalid behavior.
|
||||
pub fn forRequest(self: *Jar, target_url: URL, writer: anytype, opts: LookupOpts) !void {
|
||||
const target = PreparedUri{
|
||||
.host = (target_uri.host orelse return error.InvalidURI).percent_encoded,
|
||||
.path = target_uri.path.percent_encoded,
|
||||
.secure = std.mem.eql(u8, target_uri.scheme, "https"),
|
||||
.host = target_url.getHostname(),
|
||||
.path = target_url.getPath(),
|
||||
.secure = target_url.isSecure(),
|
||||
};
|
||||
const same_site = try areSameSite(opts.origin_uri, target.host);
|
||||
const same_site = try areSameSite(opts.origin_url, target.host);
|
||||
|
||||
removeExpired(self, opts.request_time);
|
||||
|
||||
@@ -109,8 +110,8 @@ pub const Jar = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn populateFromResponse(self: *Jar, uri: *const Uri, set_cookie: []const u8) !void {
|
||||
const c = Cookie.parse(self.allocator, uri, set_cookie) catch |err| {
|
||||
pub fn populateFromResponse(self: *Jar, url: URL, set_cookie: []const u8) !void {
|
||||
const c = Cookie.parse(self.allocator, url, set_cookie) catch |err| {
|
||||
log.warn(.web_api, "cookie parse failed", .{ .raw = set_cookie, .err = err });
|
||||
return;
|
||||
};
|
||||
@@ -148,9 +149,9 @@ fn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
fn areSameSite(origin_uri_: ?*const std.Uri, target_host: []const u8) !bool {
|
||||
const origin_uri = origin_uri_ orelse return true;
|
||||
const origin_host = (origin_uri.host orelse return error.InvalidURI).percent_encoded;
|
||||
fn areSameSite(maybe_origin_url: ?URL, target_host: []const u8) !bool {
|
||||
const origin_url = maybe_origin_url orelse return true;
|
||||
const origin_host = origin_url.host();
|
||||
|
||||
// common case
|
||||
if (std.mem.eql(u8, target_host, origin_host)) {
|
||||
@@ -161,6 +162,7 @@ fn areSameSite(origin_uri_: ?*const std.Uri, target_host: []const u8) !bool {
|
||||
}
|
||||
|
||||
fn findSecondLevelDomain(host: []const u8) []const u8 {
|
||||
// TODO: maybe reverseIterator?
|
||||
var i = std.mem.lastIndexOfScalar(u8, host, '.') orelse return host;
|
||||
while (true) {
|
||||
i = std.mem.lastIndexOfScalar(u8, host[0..i], '.') orelse return host;
|
||||
@@ -206,7 +208,7 @@ pub const Cookie = struct {
|
||||
// Invalid attribute values? Ignore.
|
||||
// Duplicate attributes - use the last valid
|
||||
// Value-less attributes with a value? Ignore the value
|
||||
pub fn parse(allocator: Allocator, uri: *const std.Uri, str: []const u8) !Cookie {
|
||||
pub fn parse(allocator: Allocator, url: URL, str: []const u8) !Cookie {
|
||||
try validateCookieString(str);
|
||||
|
||||
const cookie_name, const cookie_value, const rest = parseNameValue(str) catch {
|
||||
@@ -269,8 +271,8 @@ pub const Cookie = struct {
|
||||
const aa = arena.allocator();
|
||||
const owned_name = try aa.dupe(u8, cookie_name);
|
||||
const owned_value = try aa.dupe(u8, cookie_value);
|
||||
const owned_path = try parsePath(aa, uri, path);
|
||||
const owned_domain = try parseDomain(aa, uri, domain);
|
||||
const owned_path = try parsePath(aa, url, path);
|
||||
const owned_domain = try parseDomain(aa, url, domain);
|
||||
|
||||
var normalized_expires: ?f64 = null;
|
||||
if (max_age) |ma| {
|
||||
@@ -362,37 +364,39 @@ pub const Cookie = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parsePath(arena: Allocator, uri: ?*const std.Uri, explicit_path: ?[]const u8) ![]const u8 {
|
||||
pub fn parsePath(arena: Allocator, maybe_url: ?URL, maybe_explicit_path: ?[]const u8) ![]const u8 {
|
||||
// path attribute value either begins with a '/' or we
|
||||
// ignore it and use the "default-path" algorithm
|
||||
if (explicit_path) |path| {
|
||||
if (maybe_explicit_path) |path| {
|
||||
if (path.len > 0 and path[0] == '/') {
|
||||
return try arena.dupe(u8, path);
|
||||
return arena.dupe(u8, path);
|
||||
}
|
||||
}
|
||||
|
||||
// default-path
|
||||
const url_path = (uri orelse return "/").path;
|
||||
const url_path = blk: {
|
||||
if (maybe_url) |url| {
|
||||
break :blk url.getPath();
|
||||
}
|
||||
|
||||
const either = url_path.percent_encoded;
|
||||
if (either.len == 0 or (either.len == 1 and either[0] == '/')) {
|
||||
return "/";
|
||||
};
|
||||
|
||||
if (url_path.len == 0 or (url_path.len == 1 and url_path[0] == '/')) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
var owned_path: []const u8 = try percentEncode(arena, url_path, isPathChar);
|
||||
const last = std.mem.lastIndexOfScalar(u8, owned_path[1..], '/') orelse {
|
||||
const last = std.mem.lastIndexOfScalar(u8, url_path[1..], '/') orelse {
|
||||
return "/";
|
||||
};
|
||||
return try arena.dupe(u8, owned_path[0 .. last + 1]);
|
||||
|
||||
return arena.dupe(u8, url_path[0 .. last + 1]);
|
||||
}
|
||||
|
||||
pub fn parseDomain(arena: Allocator, uri: ?*const std.Uri, explicit_domain: ?[]const u8) ![]const u8 {
|
||||
pub fn parseDomain(arena: Allocator, maybe_url: ?URL, explicit_domain: ?[]const u8) ![]const u8 {
|
||||
var encoded_host: ?[]const u8 = null;
|
||||
if (uri) |uri_| {
|
||||
const uri_host = uri_.host orelse return error.InvalidURI;
|
||||
const host = try percentEncode(arena, uri_host, isHostChar);
|
||||
_ = toLower(host);
|
||||
encoded_host = host;
|
||||
if (maybe_url) |url| {
|
||||
const url_host = url.getHostname();
|
||||
encoded_host = url_host;
|
||||
}
|
||||
|
||||
if (explicit_domain) |domain| {
|
||||
@@ -421,19 +425,6 @@ pub const Cookie = struct {
|
||||
return encoded_host orelse return error.InvalidDomain; // default-domain
|
||||
}
|
||||
|
||||
pub fn percentEncode(arena: Allocator, component: std.Uri.Component, comptime isValidChar: fn (u8) bool) ![]u8 {
|
||||
switch (component) {
|
||||
.raw => |str| {
|
||||
var aw = try std.Io.Writer.Allocating.initCapacity(arena, str.len);
|
||||
try std.Uri.Component.percentEncode(&aw.writer, str, isValidChar);
|
||||
return aw.written(); // @memory retains memory used before growing
|
||||
},
|
||||
.percent_encoded => |str| {
|
||||
return try arena.dupe(u8, str);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isHostChar(c: u8) bool {
|
||||
return switch (c) {
|
||||
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
|
||||
@@ -601,37 +592,40 @@ test "Jar: add" {
|
||||
defer jar.deinit();
|
||||
try expectCookies(&.{}, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9000;Max-Age=0"), now);
|
||||
const test_url = try URL.parse("http://lightpanda.io/", null);
|
||||
defer test_url.deinit();
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9000;Max-Age=0"), now);
|
||||
try expectCookies(&.{}, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9000"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9000"), now);
|
||||
try expectCookies(&.{.{ "over", "9000" }}, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9000!!"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9000!!"), now);
|
||||
try expectCookies(&.{.{ "over", "9000!!" }}, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "spice=flow"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "spice=flow"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flow" } }, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "spice=flows;Path=/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "spice=flows;Path=/"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" } }, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9001;Path=/other"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9001;Path=/other"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9001" } }, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9002;Path=/;Domain=lightpanda.io"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9002;Path=/;Domain=lightpanda.io"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9001" }, .{ "over", "9002" } }, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=x;Path=/other;Max-Age=-200"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=x;Path=/other;Max-Age=-200"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9002" } }, jar);
|
||||
}
|
||||
|
||||
test "Jar: forRequest" {
|
||||
const expectCookies = struct {
|
||||
fn expect(expected: []const u8, jar: *Jar, target_uri: Uri, opts: LookupOpts) !void {
|
||||
fn expect(expected: []const u8, jar: *Jar, target_url: URL, opts: Jar.LookupOpts) !void {
|
||||
var arr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer arr.deinit(testing.allocator);
|
||||
try jar.forRequest(&target_uri, arr.writer(testing.allocator), opts);
|
||||
try jar.forRequest(target_url, arr.writer(testing.allocator), opts);
|
||||
try testing.expectEqual(expected, arr.items);
|
||||
}
|
||||
}.expect;
|
||||
@@ -641,131 +635,142 @@ test "Jar: forRequest" {
|
||||
var jar = Jar.init(testing.allocator);
|
||||
defer jar.deinit();
|
||||
|
||||
const test_uri_2 = Uri.parse("http://test.lightpanda.io/") catch unreachable;
|
||||
const test_url = try URL.parse("http://lightpanda.io/", null);
|
||||
defer test_url.deinit();
|
||||
|
||||
const test_url_2 = try URL.parse("http://test.lightpanda.io/", null);
|
||||
defer test_url_2.deinit();
|
||||
|
||||
{
|
||||
// test with no cookies
|
||||
try expectCookies("", &jar, test_uri, .{ .is_http = true });
|
||||
try expectCookies("", &jar, test_url, .{ .is_http = true });
|
||||
}
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global1=1"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global2=2;Max-Age=30;domain=lightpanda.io"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "path1=3;Path=/about"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "path2=4;Path=/docs/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "secure=5;Secure"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "sitenone=6;SameSite=None;Path=/x/;Secure"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "sitelax=7;SameSite=Lax;Path=/x/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "sitestrict=8;SameSite=Strict;Path=/x/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri_2, "domain1=9;domain=test.lightpanda.io"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "global1=1"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "global2=2;Max-Age=30;domain=lightpanda.io"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "path1=3;Path=/about"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "path2=4;Path=/docs/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "secure=5;Secure"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "sitenone=6;SameSite=None;Path=/x/;Secure"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "sitelax=7;SameSite=Lax;Path=/x/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "sitestrict=8;SameSite=Strict;Path=/x/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url_2, "domain1=9;domain=test.lightpanda.io"), now);
|
||||
|
||||
// nothing fancy here
|
||||
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .is_http = true });
|
||||
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .is_navigation = false, .is_http = true });
|
||||
try expectCookies("global1=1; global2=2", &jar, test_url, .{ .is_http = true });
|
||||
try expectCookies("global1=1; global2=2", &jar, test_url, .{ .origin_url = test_url, .is_navigation = false, .is_http = true });
|
||||
|
||||
// We reuse this URL to reparse.
|
||||
const reuse_url = try URL.parse("http://anothersitelightpanda.io/", null);
|
||||
defer reuse_url.deinit();
|
||||
|
||||
// We have a cookie where Domain=lightpanda.io
|
||||
// This should _not_ match xyxlightpanda.io
|
||||
try expectCookies("", &jar, try std.Uri.parse("http://anothersitelightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("", &jar, reuse_url, .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// matching path without trailing /
|
||||
try expectCookies("global1=1; global2=2; path1=3", &jar, try std.Uri.parse("http://lightpanda.io/about"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2; path1=3", &jar, try reuse_url.reparse("http://lightpanda.io/about"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// incomplete prefix path
|
||||
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/abou"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2", &jar, try reuse_url.reparse("http://lightpanda.io/abou"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// path doesn't match
|
||||
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/aboutus"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2", &jar, try reuse_url.reparse("http://lightpanda.io/aboutus"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// path doesn't match cookie directory
|
||||
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/docs"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2", &jar, try reuse_url.reparse("http://lightpanda.io/docs"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// exact directory match
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, try reuse_url.reparse("http://lightpanda.io/docs/"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// sub directory match
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/more"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, try reuse_url.reparse("http://lightpanda.io/docs/more"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// secure
|
||||
try expectCookies("global1=1; global2=2; secure=5", &jar, try std.Uri.parse("https://lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2; secure=5", &jar, try reuse_url.reparse("https://lightpanda.io/"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// navigational cross domain, secure
|
||||
try expectCookies("global1=1; global2=2; secure=5; sitenone=6; sitelax=7", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
const example_com_url = try URL.parse("https://example.com/", null);
|
||||
defer example_com_url.deinit();
|
||||
|
||||
try expectCookies("global1=1; global2=2; secure=5; sitenone=6; sitelax=7", &jar, try reuse_url.reparse("https://lightpanda.io/x/"), .{
|
||||
.origin_url = example_com_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// navigational cross domain, insecure
|
||||
try expectCookies("global1=1; global2=2; sitelax=7", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
try expectCookies("global1=1; global2=2; sitelax=7", &jar, try reuse_url.reparse("http://lightpanda.io/x/"), .{
|
||||
.origin_url = example_com_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// non-navigational cross domain, insecure
|
||||
try expectCookies("", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
try expectCookies("", &jar, try reuse_url.reparse("http://lightpanda.io/x/"), .{
|
||||
.origin_url = example_com_url,
|
||||
.is_http = true,
|
||||
.is_navigation = false,
|
||||
});
|
||||
|
||||
// non-navigational cross domain, secure
|
||||
try expectCookies("sitenone=6", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
try expectCookies("sitenone=6", &jar, try reuse_url.reparse("https://lightpanda.io/x/"), .{
|
||||
.origin_url = example_com_url,
|
||||
.is_http = true,
|
||||
.is_navigation = false,
|
||||
});
|
||||
|
||||
// non-navigational same origin
|
||||
try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://lightpanda.io/")),
|
||||
try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try reuse_url.reparse("http://lightpanda.io/x/"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
.is_navigation = false,
|
||||
});
|
||||
|
||||
// exact domain match + suffix
|
||||
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://test.lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global2=2; domain1=9", &jar, try reuse_url.reparse("http://test.lightpanda.io/"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// domain suffix match + suffix
|
||||
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://1.test.lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global2=2; domain1=9", &jar, try reuse_url.reparse("http://1.test.lightpanda.io/"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// non-matching domain
|
||||
try expectCookies("global2=2", &jar, try std.Uri.parse("http://other.lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global2=2", &jar, try reuse_url.reparse("http://other.lightpanda.io/"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
const l = jar.cookies.items.len;
|
||||
try expectCookies("global1=1", &jar, test_uri, .{
|
||||
try expectCookies("global1=1", &jar, test_url, .{
|
||||
.request_time = now + 100,
|
||||
.origin_uri = &test_uri,
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
try testing.expectEqual(l - 1, jar.cookies.items.len);
|
||||
@@ -961,9 +966,11 @@ const ExpectedCookie = struct {
|
||||
same_site: Cookie.SameSite = .lax,
|
||||
};
|
||||
|
||||
fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u8) !void {
|
||||
const uri = try Uri.parse(url);
|
||||
var cookie = try Cookie.parse(testing.allocator, &uri, set_cookie);
|
||||
fn expectCookie(expected: ExpectedCookie, url_str: []const u8, set_cookie: []const u8) !void {
|
||||
const url = try URL.parse(url_str, null);
|
||||
defer url.deinit();
|
||||
|
||||
var cookie = try Cookie.parse(testing.allocator, url, set_cookie);
|
||||
defer cookie.deinit();
|
||||
|
||||
try testing.expectEqual(expected.name, cookie.name);
|
||||
@@ -977,9 +984,11 @@ fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u
|
||||
try testing.expectDelta(expected.expires, cookie.expires, 2.0);
|
||||
}
|
||||
|
||||
fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) !void {
|
||||
const uri = if (url) |u| try Uri.parse(u) else test_uri;
|
||||
var cookie = try Cookie.parse(testing.allocator, &uri, set_cookie);
|
||||
fn expectAttribute(expected: anytype, maybe_url_str: ?[]const u8, set_cookie: []const u8) !void {
|
||||
const url = try URL.parse(if (maybe_url_str) |url_str| url_str else "https://lightpanda.io/", null);
|
||||
defer url.deinit();
|
||||
|
||||
var cookie = try Cookie.parse(testing.allocator, url, set_cookie);
|
||||
defer cookie.deinit();
|
||||
|
||||
inline for (@typeInfo(@TypeOf(expected)).@"struct".fields) |f| {
|
||||
@@ -994,9 +1003,7 @@ fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8)
|
||||
}
|
||||
}
|
||||
|
||||
fn expectError(expected: anyerror, url: ?[]const u8, set_cookie: []const u8) !void {
|
||||
const uri = if (url) |u| try Uri.parse(u) else test_uri;
|
||||
try testing.expectError(expected, Cookie.parse(testing.allocator, &uri, set_cookie));
|
||||
fn expectError(expected: anyerror, maybe_url_str: ?[]const u8, set_cookie: []const u8) !void {
|
||||
const url = try URL.parse(if (maybe_url_str) |url_str| url_str else "https://lightpanda.io/", null);
|
||||
try testing.expectError(expected, Cookie.parse(testing.allocator, url, set_cookie));
|
||||
}
|
||||
|
||||
const test_uri = Uri.parse("http://lightpanda.io/") catch unreachable;
|
||||
|
||||
@@ -95,28 +95,27 @@ pub const URL = struct {
|
||||
return ada.free(self.internal);
|
||||
}
|
||||
|
||||
/// 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 = .{} };
|
||||
}
|
||||
/// Initializes a `URL` from given `internal`.
|
||||
/// Note that this copies the given `internal`; meaning 2 instances
|
||||
/// of it has to be tracked separately.
|
||||
pub fn constructFromInternal(arena: Allocator, internal: ada.URL) !URL {
|
||||
const copy = ada.copy(internal);
|
||||
|
||||
/// 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);
|
||||
return .{
|
||||
.internal = copy,
|
||||
.search_params = try prepareSearchParams(arena, copy),
|
||||
};
|
||||
}
|
||||
|
||||
/// Prepares a `URLSearchParams` from given `internal`.
|
||||
/// Resets `search` of `internal`.
|
||||
fn prepareSearchParams(arena: Allocator, internal: ada.URL) !URLSearchParams {
|
||||
const maybe_search = ada.getSearchNullable(internal);
|
||||
const search = ada.getSearch(internal);
|
||||
// Empty.
|
||||
if (maybe_search.data == null) return .{};
|
||||
if (search.data == null) return .{};
|
||||
|
||||
const search = maybe_search.data[0..maybe_search.length];
|
||||
const search_params = URLSearchParams.initFromString(arena, search);
|
||||
const slice = search.data[0..search.length];
|
||||
const search_params = URLSearchParams.initFromString(arena, slice);
|
||||
// After a call to this function, search params are tracked by
|
||||
// `search_params`. So we reset the internal's search.
|
||||
ada.clearSearch(internal);
|
||||
@@ -124,27 +123,7 @@ pub const URL = struct {
|
||||
return search_params;
|
||||
}
|
||||
|
||||
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.
|
||||
// Alias to get_href.
|
||||
pub fn _toString(self: *const URL, page: *Page) ![]const u8 {
|
||||
return self.get_href(page);
|
||||
}
|
||||
@@ -156,34 +135,30 @@ pub const URL = struct {
|
||||
}
|
||||
|
||||
pub fn get_origin(self: *const URL, page: *Page) ![]const u8 {
|
||||
// `ada.getOriginNullable` allocates memory in order to find the `origin`.
|
||||
const arena = page.arena;
|
||||
// `ada.getOrigin` 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 = ada.getOrigin(self.internal);
|
||||
// `OwnedString` itself is not heap allocated so this is safe.
|
||||
defer ada.freeOwnedString(.{ .data = origin.ptr, .length = origin.len });
|
||||
|
||||
const origin = maybe_origin.data[0..maybe_origin.length];
|
||||
return page.call_arena.dupe(u8, origin);
|
||||
return 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)) {
|
||||
const maybe_href = ada.getHrefNullable(self.internal);
|
||||
if (maybe_href.data == null) {
|
||||
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 href = maybe_href.data[0..maybe_href.length];
|
||||
|
||||
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);
|
||||
|
||||
@@ -201,57 +176,27 @@ pub const URL = struct {
|
||||
}
|
||||
|
||||
pub fn get_username(self: *const URL) []const u8 {
|
||||
const username = ada.getUsernameNullable(self.internal);
|
||||
if (username.data == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return username.data[0..username.length];
|
||||
return ada.getUsername(self.internal);
|
||||
}
|
||||
|
||||
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];
|
||||
return ada.getPassword(self.internal);
|
||||
}
|
||||
|
||||
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];
|
||||
return ada.getPort(self.internal);
|
||||
}
|
||||
|
||||
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];
|
||||
return ada.getHash(self.internal);
|
||||
}
|
||||
|
||||
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];
|
||||
return ada.getHost(self.internal);
|
||||
}
|
||||
|
||||
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];
|
||||
return ada.getHostname(self.internal);
|
||||
}
|
||||
|
||||
pub fn get_pathname(self: *const URL) []const u8 {
|
||||
@@ -264,7 +209,7 @@ pub const URL = struct {
|
||||
return path.data[0..path.length];
|
||||
}
|
||||
|
||||
/// get_search depends on the current state of `search_params`.
|
||||
// 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;
|
||||
|
||||
@@ -279,60 +224,42 @@ pub const URL = struct {
|
||||
}
|
||||
|
||||
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];
|
||||
return ada.getProtocol(self.internal);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// FIXME: reinit search_params?
|
||||
pub fn set_href(self: *const URL, input: []const u8) 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 {
|
||||
pub fn set_host(self: *const URL, input: []const u8) 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 {
|
||||
pub fn set_hostname(self: *const URL, input: []const u8) 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 {
|
||||
pub fn set_protocol(self: *const URL, input: []const u8) 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 {
|
||||
pub fn set_username(self: *const URL, input: []const u8) 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 {
|
||||
pub fn set_password(self: *const URL, input: []const u8) 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 {
|
||||
pub fn set_port(self: *const URL, input: []const u8) 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 {
|
||||
pub fn set_pathname(self: *const URL, input: []const u8) 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 {
|
||||
@@ -342,9 +269,8 @@ pub const URL = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_hash(self: *const URL, input: []const u8) !void {
|
||||
ada.setHash(self.internal, input);
|
||||
if (!ada.isValid(self.internal)) return error.Internal;
|
||||
pub fn set_hash(self: *const URL, input: []const u8) void {
|
||||
_ = ada.setHash(self.internal, input);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ pub const XMLHttpRequest = struct {
|
||||
self.reset();
|
||||
|
||||
self.method = try validMethod(method);
|
||||
self.url = try URL.stitch(page.arena, url, page.url.raw, .{ .null_terminated = true });
|
||||
self.url = try URL.stitch(page.arena, url, page.url.getHref(), .{ .null_terminated = true });
|
||||
self.sync = if (asyn) |b| !b else false;
|
||||
|
||||
self.state = .opened;
|
||||
|
||||
@@ -464,7 +464,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
|
||||
pub fn getURL(self: *const Self) ?[]const u8 {
|
||||
const page = self.session.currentPage() orelse return null;
|
||||
const raw_url = page.url.raw;
|
||||
const raw_url = page.url.getHref();
|
||||
return if (raw_url.len == 0) null else raw_url;
|
||||
}
|
||||
|
||||
@@ -702,7 +702,7 @@ const IsolatedWorld = struct {
|
||||
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
|
||||
// (assuming grantUniveralAccess will be set to True!).
|
||||
// We just created the world and the page. The page's state lives in the session, but is update on navigation.
|
||||
// This also means this pointer becomes invalid after removePage until a new page is created.
|
||||
// This also means this pointer becomes invalid after removePage untill a new page is created.
|
||||
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
|
||||
pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
|
||||
// if (self.executor.context != null) return error.Only1IsolatedContextSupported;
|
||||
|
||||
@@ -38,22 +38,16 @@ const DEV_TOOLS_WINDOW_ID = 1923710101;
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
getVersion,
|
||||
setPermission,
|
||||
setWindowBounds,
|
||||
resetPermissions,
|
||||
grantPermissions,
|
||||
getWindowForTarget,
|
||||
setDownloadBehavior,
|
||||
getWindowForTarget,
|
||||
setWindowBounds,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.getVersion => return getVersion(cmd),
|
||||
.setPermission => return setPermission(cmd),
|
||||
.setWindowBounds => return setWindowBounds(cmd),
|
||||
.resetPermissions => return resetPermissions(cmd),
|
||||
.grantPermissions => return grantPermissions(cmd),
|
||||
.getWindowForTarget => return getWindowForTarget(cmd),
|
||||
.setDownloadBehavior => return setDownloadBehavior(cmd),
|
||||
.getWindowForTarget => return getWindowForTarget(cmd),
|
||||
.setWindowBounds => return setWindowBounds(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,21 +89,6 @@ fn setWindowBounds(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn grantPermissions(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setPermission(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn resetPermissions(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "cdp.browser: getVersion" {
|
||||
var ctx = testing.context();
|
||||
|
||||
@@ -663,11 +663,11 @@ test "cdp.dom: getBoxModel" {
|
||||
.params = .{ .nodeId = 6 },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .model = BoxModel{
|
||||
.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,
|
||||
.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,
|
||||
} }, .{ .id = 5 });
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ pub fn requestIntercept(arena: Allocator, bc: anytype, intercept: *const Notific
|
||||
log.debug(.cdp, "request intercept", .{
|
||||
.state = "paused",
|
||||
.id = transfer.id,
|
||||
.url = transfer.uri,
|
||||
.url = transfer.url,
|
||||
});
|
||||
// Await either continueRequest, failRequest or fulfillRequest
|
||||
|
||||
@@ -237,7 +237,7 @@ fn continueRequest(cmd: anytype) !void {
|
||||
log.debug(.cdp, "request intercept", .{
|
||||
.state = "continue",
|
||||
.id = transfer.id,
|
||||
.url = transfer.uri,
|
||||
.url = transfer.url,
|
||||
.new_url = params.url,
|
||||
});
|
||||
|
||||
@@ -342,7 +342,7 @@ fn fulfillRequest(cmd: anytype) !void {
|
||||
log.debug(.cdp, "request intercept", .{
|
||||
.state = "fulfilled",
|
||||
.id = transfer.id,
|
||||
.url = transfer.uri,
|
||||
.url = transfer.url,
|
||||
.status = params.responseCode,
|
||||
.body = params.body != null,
|
||||
});
|
||||
@@ -376,7 +376,7 @@ fn failRequest(cmd: anytype) !void {
|
||||
log.info(.cdp, "request intercept", .{
|
||||
.state = "fail",
|
||||
.id = request_id,
|
||||
.url = transfer.uri,
|
||||
.url = transfer.url,
|
||||
.reason = params.errorReason,
|
||||
});
|
||||
return cmd.sendResult(null, .{});
|
||||
@@ -420,7 +420,7 @@ pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Noti
|
||||
log.debug(.cdp, "request auth required", .{
|
||||
.state = "paused",
|
||||
.id = transfer.id,
|
||||
.url = transfer.uri,
|
||||
.url = transfer.url,
|
||||
});
|
||||
// Await continueWithAuth
|
||||
|
||||
|
||||
@@ -23,13 +23,11 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
dispatchKeyEvent,
|
||||
dispatchMouseEvent,
|
||||
insertText,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.dispatchKeyEvent => return dispatchKeyEvent(cmd),
|
||||
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
|
||||
.insertText => return insertText(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,20 +115,6 @@ fn dispatchMouseEvent(cmd: anytype) !void {
|
||||
// result already sent
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText
|
||||
fn insertText(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
text: []const u8, // The text to insert
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return;
|
||||
const page = bc.session.currentPage() orelse return;
|
||||
|
||||
try page.insertText(params.text);
|
||||
|
||||
try cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn clickNavigate(cmd: anytype, uri: std.Uri) !void {
|
||||
const bc = cmd.browser_context.?;
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
|
||||
const CdpStorage = @import("storage.zig");
|
||||
const Transfer = @import("../../http/Client.zig").Transfer;
|
||||
const Notification = @import("../../notification.zig").Notification;
|
||||
const URL = @import("../../url.zig").URL;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
@@ -117,15 +118,20 @@ fn deleteCookies(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const cookies = &bc.session.cookie_jar.cookies;
|
||||
|
||||
const uri = if (params.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
|
||||
const uri_ptr = if (uri) |u| &u else null;
|
||||
const maybe_url: ?URL = blk: {
|
||||
if (params.url) |url| {
|
||||
break :blk URL.parse(url, null) catch return error.InvalidParams;
|
||||
}
|
||||
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
var index = cookies.items.len;
|
||||
while (index > 0) {
|
||||
index -= 1;
|
||||
const cookie = &cookies.items[index];
|
||||
const domain = try Cookie.parseDomain(cmd.arena, uri_ptr, params.domain);
|
||||
const path = try Cookie.parsePath(cmd.arena, uri_ptr, params.path);
|
||||
const domain = try Cookie.parseDomain(cmd.arena, maybe_url, params.domain);
|
||||
const path = try Cookie.parsePath(cmd.arena, maybe_url, params.path);
|
||||
|
||||
// We do not want to use Cookie.appliesTo here. As a Cookie with a shorter path would match.
|
||||
// Similar to deduplicating with areCookiesEqual, except domain and path are optional.
|
||||
@@ -133,6 +139,12 @@ fn deleteCookies(cmd: anytype) !void {
|
||||
cookies.swapRemove(index).deinit();
|
||||
}
|
||||
}
|
||||
|
||||
// Deinit URL if we had.
|
||||
if (maybe_url) |url| {
|
||||
url.deinit();
|
||||
}
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
@@ -173,17 +185,18 @@ fn getCookies(cmd: anytype) !void {
|
||||
const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{};
|
||||
|
||||
// If not specified, use the URLs of the page and all of its subframes. TODO subframes
|
||||
const page_url = if (bc.session.page) |*page| page.url.raw else null; // @speed: avoid repasing the URL
|
||||
const page_url = if (bc.session.page) |page| page.url.getHref() else null; // @speed: avoid repasing the URL
|
||||
const param_urls = params.urls orelse &[_][]const u8{page_url orelse return error.InvalidParams};
|
||||
|
||||
var urls = try std.ArrayListUnmanaged(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len);
|
||||
for (param_urls) |url| {
|
||||
const uri = std.Uri.parse(url) catch return error.InvalidParams;
|
||||
for (param_urls) |url_str| {
|
||||
const url = URL.parse(url_str, null) catch return error.InvalidParams;
|
||||
//defer url.deinit();
|
||||
|
||||
urls.appendAssumeCapacity(.{
|
||||
.host = try Cookie.parseDomain(cmd.arena, &uri, null),
|
||||
.path = try Cookie.parsePath(cmd.arena, &uri, null),
|
||||
.secure = std.mem.eql(u8, uri.scheme, "https"),
|
||||
.host = try Cookie.parseDomain(cmd.arena, url, null),
|
||||
.path = try Cookie.parsePath(cmd.arena, url, null),
|
||||
.secure = url.isSecure(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -247,7 +260,7 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification.
|
||||
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
|
||||
.frameId = target_id,
|
||||
.loaderId = bc.loader_id,
|
||||
.documentUrl = DocumentUrlWriter.init(&page.url.uri),
|
||||
.documentUrl = DocumentUrlWriter.init(page.url),
|
||||
.request = TransferAsRequestWriter.init(transfer),
|
||||
.initiator = .{ .type = "other" },
|
||||
}, .{ .session_id = session_id });
|
||||
@@ -300,23 +313,17 @@ pub const TransferAsRequestWriter = struct {
|
||||
try jws.objectField("url");
|
||||
try jws.beginWriteRaw();
|
||||
try writer.writeByte('\"');
|
||||
try transfer.uri.writeToStream(writer, .{
|
||||
.scheme = true,
|
||||
.authentication = true,
|
||||
.authority = true,
|
||||
.path = true,
|
||||
.query = true,
|
||||
});
|
||||
try transfer.url.writeToStream(writer);
|
||||
try writer.writeByte('\"');
|
||||
jws.endWriteRaw();
|
||||
}
|
||||
|
||||
{
|
||||
if (transfer.uri.fragment) |frag| {
|
||||
if (transfer.url.getFragment()) |frag| {
|
||||
try jws.objectField("urlFragment");
|
||||
try jws.beginWriteRaw();
|
||||
try writer.writeAll("\"#");
|
||||
try writer.writeAll(frag.percent_encoded);
|
||||
try writer.writeAll(frag);
|
||||
try writer.writeByte('\"');
|
||||
jws.endWriteRaw();
|
||||
}
|
||||
@@ -370,13 +377,7 @@ const TransferAsResponseWriter = struct {
|
||||
try jws.objectField("url");
|
||||
try jws.beginWriteRaw();
|
||||
try writer.writeByte('\"');
|
||||
try transfer.uri.writeToStream(writer, .{
|
||||
.scheme = true,
|
||||
.authentication = true,
|
||||
.authority = true,
|
||||
.path = true,
|
||||
.query = true,
|
||||
});
|
||||
try transfer.url.writeToStream(writer);
|
||||
try writer.writeByte('\"');
|
||||
jws.endWriteRaw();
|
||||
}
|
||||
@@ -417,29 +418,22 @@ const TransferAsResponseWriter = struct {
|
||||
};
|
||||
|
||||
const DocumentUrlWriter = struct {
|
||||
uri: *std.Uri,
|
||||
url: URL,
|
||||
|
||||
fn init(uri: *std.Uri) DocumentUrlWriter {
|
||||
return .{
|
||||
.uri = uri,
|
||||
};
|
||||
fn init(url: URL) DocumentUrlWriter {
|
||||
return .{ .url = url };
|
||||
}
|
||||
|
||||
pub fn jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void {
|
||||
self._jsonStringify(jws) catch return error.WriteFailed;
|
||||
}
|
||||
|
||||
fn _jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void {
|
||||
const writer = jws.writer;
|
||||
|
||||
try jws.beginWriteRaw();
|
||||
try writer.writeByte('\"');
|
||||
try self.uri.writeToStream(writer, .{
|
||||
.scheme = true,
|
||||
.authentication = true,
|
||||
.authority = true,
|
||||
.path = true,
|
||||
.query = true,
|
||||
});
|
||||
try self.url.writeToStream(writer);
|
||||
try writer.writeByte('\"');
|
||||
jws.endWriteRaw();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
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;
|
||||
@@ -83,33 +82,11 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void {
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
if (params.enabled == false) {
|
||||
if (params.enabled) {
|
||||
try bc.lifecycleEventsEnable();
|
||||
} else {
|
||||
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, .{});
|
||||
}
|
||||
|
||||
@@ -197,7 +174,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, .navigation => "scriptInitiated",
|
||||
.script, .history => "scriptInitiated",
|
||||
.form => switch (event.opts.method) {
|
||||
.GET => "formSubmissionGet",
|
||||
.POST => "formSubmissionPost",
|
||||
|
||||
@@ -21,48 +21,9 @@ const std = @import("std");
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
setIgnoreCertificateErrors,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
.setIgnoreCertificateErrors => return setIgnoreCertificateErrors(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
fn setIgnoreCertificateErrors(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
ignore: bool,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
if (params.ignore) {
|
||||
try cmd.cdp.browser.http_client.disableTlsVerify();
|
||||
} else {
|
||||
try cmd.cdp.browser.http_client.enableTlsVerify();
|
||||
}
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
|
||||
test "cdp.Security: setIgnoreCertificateErrors" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
_ = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 8,
|
||||
.method = "Security.setIgnoreCertificateErrors",
|
||||
.params = .{ .ignore = true },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 8 });
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 9,
|
||||
.method = "Security.setIgnoreCertificateErrors",
|
||||
.params = .{ .ignore = false },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 9 });
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
const Cookie = @import("../../browser/storage/storage.zig").Cookie;
|
||||
const CookieJar = @import("../../browser/storage/storage.zig").CookieJar;
|
||||
const URL = @import("../../url.zig").URL;
|
||||
pub const PreparedUri = @import("../../browser/storage/cookie.zig").PreparedUri;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
@@ -136,12 +137,25 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
|
||||
const a = arena.allocator();
|
||||
|
||||
// NOTE: The param.url can affect the default domain, (NOT path), secure, source port, and source scheme.
|
||||
const uri = if (param.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
|
||||
const uri_ptr = if (uri) |*u| u else null;
|
||||
const domain = try Cookie.parseDomain(a, uri_ptr, param.domain);
|
||||
const maybe_url: ?URL = blk: {
|
||||
if (param.url) |url| {
|
||||
break :blk URL.parse(url, null) catch return error.InvalidParams;
|
||||
}
|
||||
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
const domain = try Cookie.parseDomain(a, maybe_url, param.domain);
|
||||
const path = if (param.path == null) "/" else try Cookie.parsePath(a, null, param.path);
|
||||
|
||||
const secure = if (param.secure) |s| s else if (uri) |uri_| std.mem.eql(u8, uri_.scheme, "https") else false;
|
||||
const secure: bool = blk: {
|
||||
// Check if params indicate security.
|
||||
if (param.secure) |s| break :blk s;
|
||||
// Check if protocol is secure.
|
||||
if (maybe_url) |url| break :blk url.isSecure();
|
||||
// If all fails, insecure.
|
||||
break :blk false;
|
||||
};
|
||||
|
||||
const cookie = Cookie{
|
||||
.arena = arena,
|
||||
@@ -158,6 +172,7 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
|
||||
.None => .none,
|
||||
},
|
||||
};
|
||||
|
||||
try cookie_jar.add(cookie, std.time.timestamp());
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ fn disposeBrowserContext(cmd: anytype) !void {
|
||||
|
||||
fn createTarget(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
url: []const u8 = "about:blank",
|
||||
// url: []const u8,
|
||||
// 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 = params.url,
|
||||
.title = "about:blank",
|
||||
.browserContextId = bc.id,
|
||||
.url = "about:blank",
|
||||
},
|
||||
@@ -178,10 +178,6 @@ fn createTarget(cmd: anytype) !void {
|
||||
try doAttachtoTarget(cmd, target_id);
|
||||
}
|
||||
|
||||
try page.navigate(params.url, .{
|
||||
.reason = .address_bar,
|
||||
});
|
||||
|
||||
try cmd.sendResult(.{
|
||||
.targetId = target_id,
|
||||
}, .{});
|
||||
@@ -521,7 +517,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.?;
|
||||
@@ -533,7 +529,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.?;
|
||||
|
||||
@@ -22,9 +22,9 @@ const builtin = @import("builtin");
|
||||
|
||||
const Http = @import("Http.zig");
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
const CookieJar = @import("../browser/storage/storage.zig").CookieJar;
|
||||
|
||||
const urlStitch = @import("../url.zig").stitch;
|
||||
const CookieJar = @import("../browser/storage/cookie.zig").Jar;
|
||||
const URL = @import("../url.zig").URL;
|
||||
const urlStitch = URL.stitch;
|
||||
|
||||
const c = Http.c;
|
||||
const posix = std.posix;
|
||||
@@ -93,11 +93,6 @@ notification: ?*Notification = null,
|
||||
// restoring, this originally-configured value is what it goes to.
|
||||
http_proxy: ?[:0]const u8 = null,
|
||||
|
||||
// track if the client use a proxy for connections.
|
||||
// We can't use http_proxy because we want also to track proxy configured via
|
||||
// CDP.
|
||||
use_proxy: bool,
|
||||
|
||||
// The complete user-agent header line
|
||||
user_agent: [:0]const u8,
|
||||
|
||||
@@ -131,7 +126,6 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie
|
||||
.handles = handles,
|
||||
.allocator = allocator,
|
||||
.http_proxy = opts.http_proxy,
|
||||
.use_proxy = opts.http_proxy != null,
|
||||
.user_agent = opts.user_agent,
|
||||
.transfer_pool = transfer_pool,
|
||||
};
|
||||
@@ -265,7 +259,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
|
||||
errdefer req.headers.deinit();
|
||||
|
||||
// we need this for cookies
|
||||
const uri = std.Uri.parse(req.url) catch |err| {
|
||||
const url = URL.parse(req.url, null) catch |err| {
|
||||
log.warn(.http, "invalid url", .{ .err = err, .url = req.url });
|
||||
return err;
|
||||
};
|
||||
@@ -278,7 +272,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
|
||||
transfer.* = .{
|
||||
.arena = ArenaAllocator.init(self.allocator),
|
||||
.id = id,
|
||||
.uri = uri,
|
||||
.url = url,
|
||||
.req = req,
|
||||
.ctx = req.ctx,
|
||||
.client = self,
|
||||
@@ -321,7 +315,6 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void {
|
||||
for (self.handles.handles) |*h| {
|
||||
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
|
||||
}
|
||||
self.use_proxy = true;
|
||||
}
|
||||
|
||||
// Same restriction as changeProxy. Should be ok since this is only called on
|
||||
@@ -333,37 +326,6 @@ pub fn restoreOriginalProxy(self: *Client) !void {
|
||||
for (self.handles.handles) |*h| {
|
||||
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy));
|
||||
}
|
||||
self.use_proxy = proxy != null;
|
||||
}
|
||||
|
||||
// Enable TLS verification on all connections.
|
||||
pub fn enableTlsVerify(self: *const Client) !void {
|
||||
for (self.handles.handles) |*h| {
|
||||
const easy = h.conn.easy;
|
||||
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 2)));
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 1)));
|
||||
|
||||
if (self.use_proxy) {
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 2)));
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disable TLS verification on all connections.
|
||||
pub fn disableTlsVerify(self: *const Client) !void {
|
||||
for (self.handles.handles) |*h| {
|
||||
const easy = h.conn.easy;
|
||||
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0)));
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0)));
|
||||
|
||||
if (self.use_proxy) {
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 0)));
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
|
||||
@@ -633,20 +595,20 @@ pub const Handle = struct {
|
||||
pub const RequestCookie = struct {
|
||||
is_http: bool,
|
||||
is_navigation: bool,
|
||||
origin: *const std.Uri,
|
||||
jar: *@import("../browser/storage/cookie.zig").Jar,
|
||||
origin_url: URL,
|
||||
cookie_jar: *CookieJar,
|
||||
|
||||
pub fn headersForRequest(self: *const RequestCookie, temp: Allocator, url: [:0]const u8, headers: *Http.Headers) !void {
|
||||
const uri = std.Uri.parse(url) catch |err| {
|
||||
log.warn(.http, "invalid url", .{ .err = err, .url = url });
|
||||
pub fn headersForRequest(self: *const RequestCookie, temp: Allocator, url_str: [:0]const u8, headers: *Http.Headers) !void {
|
||||
const url = URL.parse(url_str, null) catch |err| {
|
||||
log.warn(.http, "invalid url", .{ .err = err, .url = url_str });
|
||||
return error.InvalidUrl;
|
||||
};
|
||||
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
try self.jar.forRequest(&uri, arr.writer(temp), .{
|
||||
try self.cookie_jar.forRequest(url, arr.writer(temp), .{
|
||||
.is_http = self.is_http,
|
||||
.is_navigation = self.is_navigation,
|
||||
.origin_uri = self.origin,
|
||||
.origin_url = self.origin_url,
|
||||
});
|
||||
|
||||
if (arr.items.len > 0) {
|
||||
@@ -726,7 +688,7 @@ pub const Transfer = struct {
|
||||
arena: ArenaAllocator,
|
||||
id: usize = 0,
|
||||
req: Request,
|
||||
uri: std.Uri, // used for setting/getting the cookie
|
||||
url: URL, // used for setting/getting the cookie
|
||||
ctx: *anyopaque, // copied from req.ctx to make it easier for callback handlers
|
||||
client: *Client,
|
||||
// total bytes received in the response, including the response status line,
|
||||
@@ -812,7 +774,7 @@ pub const Transfer = struct {
|
||||
|
||||
pub fn updateURL(self: *Transfer, url: [:0]const u8) !void {
|
||||
// for cookies
|
||||
self.uri = try std.Uri.parse(url);
|
||||
self.url = try self.url.reparse(url);
|
||||
|
||||
// for the request itself
|
||||
self.req.url = url;
|
||||
@@ -846,7 +808,7 @@ pub const Transfer = struct {
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
// abortAuthChallenge is called when an auth challenge interception is
|
||||
// abortAuthChallenge is called when an auth chanllenge interception is
|
||||
// abort. We don't call self.client.endTransfer here b/c it has been done
|
||||
// before interception process.
|
||||
pub fn abortAuthChallenge(self: *Transfer) void {
|
||||
@@ -871,7 +833,7 @@ pub const Transfer = struct {
|
||||
while (true) {
|
||||
const ct = getResponseHeader(easy, "set-cookie", i);
|
||||
if (ct == null) break;
|
||||
try req.cookie_jar.populateFromResponse(&transfer.uri, ct.?.value);
|
||||
try req.cookie_jar.populateFromResponse(transfer.url, ct.?.value);
|
||||
i += 1;
|
||||
if (i >= ct.?.amount) break;
|
||||
}
|
||||
@@ -885,14 +847,16 @@ pub const Transfer = struct {
|
||||
var baseurl: [*c]u8 = undefined;
|
||||
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_EFFECTIVE_URL, &baseurl));
|
||||
|
||||
const url = try urlStitch(arena, hlocation.?.value, std.mem.span(baseurl), .{});
|
||||
const uri = try std.Uri.parse(url);
|
||||
transfer.uri = uri;
|
||||
const stitched = try urlStitch(arena, hlocation.?.value, std.mem.span(baseurl), .{});
|
||||
// Since we're being redirected, we know url is valid.
|
||||
// An assertation won't hurt, though.
|
||||
std.debug.assert(transfer.url.isValid());
|
||||
_ = try transfer.url.reparse(stitched);
|
||||
|
||||
var cookies: std.ArrayListUnmanaged(u8) = .{};
|
||||
try req.cookie_jar.forRequest(&uri, cookies.writer(arena), .{
|
||||
try req.cookie_jar.forRequest(transfer.url, cookies.writer(arena), .{
|
||||
.is_http = true,
|
||||
.origin_uri = &transfer.uri,
|
||||
.origin_url = transfer.url,
|
||||
// used to enforce samesite cookie rules
|
||||
.is_navigation = req.resource_type == .document,
|
||||
});
|
||||
@@ -921,7 +885,7 @@ pub const Transfer = struct {
|
||||
while (true) {
|
||||
const ct = getResponseHeader(easy, "set-cookie", i);
|
||||
if (ct == null) break;
|
||||
transfer.req.cookie_jar.populateFromResponse(&transfer.uri, ct.?.value) catch |err| {
|
||||
transfer.req.cookie_jar.populateFromResponse(transfer.url, ct.?.value) catch |err| {
|
||||
log.err(.http, "set cookie", .{ .err = err, .req = transfer });
|
||||
return err;
|
||||
};
|
||||
|
||||
@@ -373,11 +373,7 @@ const Command = struct {
|
||||
\\ Defaults to
|
||||
++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++
|
||||
\\
|
||||
\\--log_filter_scopes
|
||||
\\ Filter out too verbose logs per scope:
|
||||
\\ http, unknown_prop, script_event, ...
|
||||
\\
|
||||
\\--user_agent_suffix
|
||||
\\ --user_agent_suffix
|
||||
\\ Suffix to append to the Lightpanda/X.Y User-Agent
|
||||
\\
|
||||
;
|
||||
|
||||
@@ -487,7 +487,7 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
// called by CDP
|
||||
// Websocket frames have a variable length header. For server-client,
|
||||
// Websocket frames have a variable lenght header. For server-client,
|
||||
// it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have
|
||||
// writev, so we need to get creative. We'll JSON serialize to a
|
||||
// buffer, where the first 10 bytes are reserved. We can then backfill
|
||||
|
||||
@@ -402,13 +402,19 @@ 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, .{});
|
||||
test_session.fetchWait(2000);
|
||||
|
||||
_ = page.wait(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| {
|
||||
|
||||
@@ -168,35 +168,35 @@
|
||||
|
||||
<script id=dimensions>
|
||||
const para = document.getElementById('para');
|
||||
testing.expectEqual(5, para.clientWidth);
|
||||
testing.expectEqual(5, para.clientHeight);
|
||||
testing.expectEqual(1, para.clientWidth);
|
||||
testing.expectEqual(1, para.clientHeight);
|
||||
|
||||
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 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 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 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 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);
|
||||
// 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);
|
||||
|
||||
testing.expectEqual(10, para.clientWidth);
|
||||
testing.expectEqual(5, para.clientHeight);
|
||||
// testing.expectEqual(1, para.clientWidth);
|
||||
// testing.expectEqual(1, 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>
|
||||
@@ -326,16 +326,3 @@
|
||||
testing.expectEqual("after begin", newElement.innerText);
|
||||
testing.expectEqual("afterbegin", newElement.className);
|
||||
</script>
|
||||
|
||||
<script id=nonBreakingSpace>
|
||||
// Test non-breaking space encoding (critical for React hydration)
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = 'hello\xa0world';
|
||||
testing.expectEqual('hello\xa0world', div.textContent);
|
||||
testing.expectEqual('hello world', div.innerHTML);
|
||||
|
||||
// Test that outerHTML also encodes non-breaking spaces correctly
|
||||
const p = document.createElement('p');
|
||||
p.textContent = 'XAnge\xa0Privacy';
|
||||
testing.expectEqual('<p>XAnge Privacy</p>', p.outerHTML);
|
||||
</script>
|
||||
|
||||
@@ -122,13 +122,13 @@
|
||||
testing.expectEqual(1, entry.intersectionRatio);
|
||||
testing.expectEqual(0, entry.intersectionRect.x);
|
||||
testing.expectEqual(0, entry.intersectionRect.y);
|
||||
testing.expectEqual(5, entry.intersectionRect.width);
|
||||
testing.expectEqual(5, entry.intersectionRect.height);
|
||||
testing.expectEqual(1, entry.intersectionRect.width);
|
||||
testing.expectEqual(1, entry.intersectionRect.height);
|
||||
testing.expectEqual(true, entry.isIntersecting);
|
||||
testing.expectEqual(0, entry.rootBounds.x);
|
||||
testing.expectEqual(0, entry.rootBounds.y);
|
||||
testing.expectEqual(5, entry.rootBounds.width);
|
||||
testing.expectEqual(5, entry.rootBounds.height);
|
||||
testing.expectEqual(1, entry.rootBounds.width);
|
||||
testing.expectEqual(1, entry.rootBounds.height);
|
||||
testing.expectEqual('[object HTMLDivElement]', entry.target.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<p id="para"> And</p>
|
||||
<!--comment-->
|
||||
</div>
|
||||
<div id="rootNodeComposed"></div>
|
||||
</body>
|
||||
|
||||
<script src="../testing.js"></script>
|
||||
@@ -37,26 +36,6 @@ let first_child = content.firstChild.nextSibling; // nextSibling because of line
|
||||
testing.expectEqual('HTMLDocument', content.getRootNode().__proto__.constructor.name);
|
||||
</script>
|
||||
|
||||
<script id=getRootNodeComposed>
|
||||
const testContainer = $('#rootNodeComposed');
|
||||
const shadowHost = document.createElement('div');
|
||||
testContainer.appendChild(shadowHost);
|
||||
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
|
||||
const shadowChild = document.createElement('span');
|
||||
shadowRoot.appendChild(shadowChild);
|
||||
|
||||
testing.expectEqual('ShadowRoot', shadowChild.getRootNode().__proto__.constructor.name);
|
||||
testing.expectEqual('ShadowRoot', shadowChild.getRootNode({ composed: false }).__proto__.constructor.name);
|
||||
testing.expectEqual('HTMLDocument', shadowChild.getRootNode({ composed: true }).__proto__.constructor.name);
|
||||
testing.expectEqual('HTMLDocument', shadowHost.getRootNode().__proto__.constructor.name);
|
||||
|
||||
const disconnected = document.createElement('div');
|
||||
const disconnectedChild = document.createElement('span');
|
||||
disconnected.appendChild(disconnectedChild);
|
||||
testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode().__proto__.constructor.name);
|
||||
testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode({ composed: true }).__proto__.constructor.name);
|
||||
</script>
|
||||
|
||||
<script id=firstChild>
|
||||
let body_first_child = document.body.firstChild;
|
||||
testing.expectEqual('div', body_first_child.localName);
|
||||
@@ -245,22 +224,3 @@ let first_child = content.firstChild.nextSibling; // nextSibling because of line
|
||||
testing.expectEqual(6, Node.ENTITY_NODE);
|
||||
testing.expectEqual(12, Node.NOTATION_NODE);
|
||||
</script>
|
||||
|
||||
<span id=token class="token" style="color:#ce9178">"puppeteer "</span>
|
||||
<h3 id=name>Leto
|
||||
<!-- -->
|
||||
<!-- -->
|
||||
Atreides</h3>
|
||||
<script id=normalize>
|
||||
const token = $('#token');
|
||||
testing.expectEqual('"puppeteer "', token.firstChild.nodeValue);
|
||||
|
||||
const name = $('#name');
|
||||
testing.expectEqual([
|
||||
"Leto\n ",
|
||||
" ",
|
||||
"\n ",
|
||||
" ",
|
||||
"\n Atreides"
|
||||
], Array.from(name.childNodes).map((n) => n.nodeValue));
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<!-- Test fixture -->
|
||||
<div id="container">
|
||||
<!-- comment1 -->
|
||||
<div id="outer">
|
||||
<!-- comment2 -->
|
||||
<span id="inner">
|
||||
<!-- comment3 -->
|
||||
Text content
|
||||
<!-- comment4 -->
|
||||
</span>
|
||||
<!-- comment5 -->
|
||||
</div>
|
||||
<!-- comment6 -->
|
||||
</div>
|
||||
|
||||
<script id=nodeFilter>
|
||||
testing.expectEqual(1, NodeFilter.FILTER_ACCEPT);
|
||||
testing.expectEqual(2, NodeFilter.FILTER_REJECT);
|
||||
@@ -23,197 +7,3 @@
|
||||
testing.expectEqual(4294967295, NodeFilter.SHOW_ALL);
|
||||
testing.expectEqual(129, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT);
|
||||
</script>
|
||||
|
||||
<script id=treeWalkerComments>
|
||||
{
|
||||
const container = $('#container');
|
||||
const walker = document.createTreeWalker(
|
||||
container,
|
||||
NodeFilter.SHOW_COMMENT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const comments = [];
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
comments.push(node.data.trim());
|
||||
}
|
||||
|
||||
// Should find all 6 comments, including those nested inside elements
|
||||
testing.expectEqual(6, comments.length);
|
||||
testing.expectEqual('comment1', comments[0]);
|
||||
testing.expectEqual('comment2', comments[1]);
|
||||
testing.expectEqual('comment3', comments[2]);
|
||||
testing.expectEqual('comment4', comments[3]);
|
||||
testing.expectEqual('comment5', comments[4]);
|
||||
testing.expectEqual('comment6', comments[5]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=treeWalkerElements>
|
||||
{
|
||||
const container = $('#container');
|
||||
const walker = document.createTreeWalker(
|
||||
container,
|
||||
NodeFilter.SHOW_ELEMENT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const elements = [];
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
if (node.id) {
|
||||
elements.push(node.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Should find the 2 nested elements (outer and inner)
|
||||
testing.expectEqual(2, elements.length);
|
||||
testing.expectEqual('outer', elements[0]);
|
||||
testing.expectEqual('inner', elements[1]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=treeWalkerAll>
|
||||
{
|
||||
const container = $('#container');
|
||||
const walker = document.createTreeWalker(
|
||||
container,
|
||||
NodeFilter.SHOW_ALL,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
let commentCount = 0;
|
||||
let elementCount = 0;
|
||||
let textCount = 0;
|
||||
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
if (node.nodeType === 8) commentCount++; // Comment
|
||||
else if (node.nodeType === 1) elementCount++; // Element
|
||||
else if (node.nodeType === 3) textCount++; // Text
|
||||
}
|
||||
|
||||
testing.expectEqual(6, commentCount);
|
||||
testing.expectEqual(2, elementCount);
|
||||
testing.expectEqual(true, textCount > 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=treeWalkerCombined>
|
||||
{
|
||||
const container = $('#container');
|
||||
const walker = document.createTreeWalker(
|
||||
container,
|
||||
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
let commentCount = 0;
|
||||
let elementCount = 0;
|
||||
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
if (node.nodeType === 8) commentCount++; // Comment
|
||||
else if (node.nodeType === 1) elementCount++; // Element
|
||||
}
|
||||
|
||||
// Should find 6 comments and 2 elements, but no text nodes
|
||||
testing.expectEqual(6, commentCount);
|
||||
testing.expectEqual(2, elementCount);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=treeWalkerCustomFilter>
|
||||
{
|
||||
const container = $('#container');
|
||||
|
||||
// Filter that accepts only elements with id
|
||||
const walker = document.createTreeWalker(
|
||||
container,
|
||||
NodeFilter.SHOW_ELEMENT,
|
||||
{
|
||||
acceptNode: function(node) {
|
||||
return node.id ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const elements = [];
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
elements.push(node.id);
|
||||
}
|
||||
|
||||
// Should find only elements with id (outer and inner)
|
||||
testing.expectEqual(2, elements.length);
|
||||
testing.expectEqual('outer', elements[0]);
|
||||
testing.expectEqual('inner', elements[1]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=nodeIteratorComments>
|
||||
{
|
||||
const container = $('#container');
|
||||
const iterator = document.createNodeIterator(
|
||||
container,
|
||||
NodeFilter.SHOW_COMMENT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const comments = [];
|
||||
let node;
|
||||
while (node = iterator.nextNode()) {
|
||||
comments.push(node.data.trim());
|
||||
}
|
||||
|
||||
// Should find all 6 comments, including those nested inside elements
|
||||
testing.expectEqual(6, comments.length);
|
||||
testing.expectEqual('comment1', comments[0]);
|
||||
testing.expectEqual('comment2', comments[1]);
|
||||
testing.expectEqual('comment3', comments[2]);
|
||||
testing.expectEqual('comment4', comments[3]);
|
||||
testing.expectEqual('comment5', comments[4]);
|
||||
testing.expectEqual('comment6', comments[5]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=reactLikeScenario>
|
||||
{
|
||||
// Test a React-like scenario with comment markers
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `
|
||||
<a href="/">
|
||||
<!--$-->
|
||||
<svg viewBox="0 0 10 10">
|
||||
<path d="M0,0 L10,10" />
|
||||
</svg>
|
||||
<!--/$-->
|
||||
</a>
|
||||
`;
|
||||
|
||||
const walker = document.createTreeWalker(
|
||||
div,
|
||||
NodeFilter.SHOW_COMMENT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const comments = [];
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
comments.push(node.data);
|
||||
}
|
||||
|
||||
// Should find both React markers even though they're nested inside <a>
|
||||
testing.expectEqual(2, comments.length);
|
||||
testing.expectEqual('$', comments[0]);
|
||||
testing.expectEqual('/$', comments[1]);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=noNata>
|
||||
{
|
||||
let event = new CompositionEvent("test", {});
|
||||
testing.expectEqual(true, event instanceof CompositionEvent);
|
||||
testing.expectEqual(true, event instanceof Event);
|
||||
|
||||
testing.expectEqual("test", event.type);
|
||||
testing.expectEqual("", event.data);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=withData>
|
||||
{
|
||||
let event = new CompositionEvent("test2", {data: "over 9000!"});
|
||||
testing.expectEqual("test2", event.type);
|
||||
testing.expectEqual("over 9000!", event.data);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=dispatch>
|
||||
{
|
||||
let called = 0;
|
||||
document.addEventListener('CE', (e) => {
|
||||
testing.expectEqual('test-data', e.data);
|
||||
testing.expectEqual(true, e instanceof CompositionEvent);
|
||||
called += 1
|
||||
});
|
||||
|
||||
document.dispatchEvent(new CompositionEvent('CE', {data: 'test-data'}));
|
||||
testing.expectEqual(1, called);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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(2.5, 2.5));
|
||||
testing.expectEqual([], document.elementsFromPoint(2.5, 2.5));
|
||||
testing.expectEqual(null, document.elementFromPoint(0.5, 0.5));
|
||||
testing.expectEqual([], document.elementsFromPoint(0.5, 0.5));
|
||||
|
||||
let div1 = document.createElement('div');
|
||||
document.body.appendChild(div1);
|
||||
div1.getClientRects(); // clal this to position it
|
||||
testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(2.5, 2.5).toString());
|
||||
testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(0.5, 0.5).toString());
|
||||
|
||||
let elems = document.elementsFromPoint(2.5, 2.5);
|
||||
let elems = document.elementsFromPoint(0.5, 0.5);
|
||||
testing.expectEqual(3, elems.length);
|
||||
testing.expectEqual('[object HTMLDivElement]', elems[0].toString());
|
||||
testing.expectEqual('[object HTMLBodyElement]', elems[1].toString());
|
||||
@@ -66,12 +66,12 @@
|
||||
// Note this will be placed after the div of previous test
|
||||
a.getClientRects();
|
||||
|
||||
let a_again = document.elementFromPoint(7.5, 0.5);
|
||||
let a_again = document.elementFromPoint(1.5, 0.5);
|
||||
testing.expectEqual('[object HTMLAnchorElement]', a_again.toString());
|
||||
testing.expectEqual('https://lightpanda.io', a_again.href);
|
||||
testing.expectEqual('https://lightpanda.io/', a_again.href);
|
||||
|
||||
let a_agains = document.elementsFromPoint(7.5, 0.5);
|
||||
testing.expectEqual('https://lightpanda.io', a_agains[0].href);
|
||||
let a_agains = document.elementsFromPoint(1.5, 0.5);
|
||||
testing.expectEqual('https://lightpanda.io/', a_agains[0].href);
|
||||
|
||||
|
||||
testing.expectEqual(true, !document.all);
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
testing.expectEqual('', a.host);
|
||||
a.href = 'about';
|
||||
testing.expectEqual('http://localhost:9582/src/tests/html/about', a.href);
|
||||
testing.expectEqual(true, true);
|
||||
</script>
|
||||
|
||||
<script id=focus>
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
<!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/src/tests/html/history/history_after_nav.html');
|
||||
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/xhr/json');
|
||||
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);
|
||||
|
||||
@@ -33,5 +32,10 @@
|
||||
testing.expectEqual(state, popstateEventState);
|
||||
})
|
||||
|
||||
history.back();
|
||||
testing.onPageWait(() => {
|
||||
testing.expectEqual(true, history.state && history.state.testComplete);
|
||||
testing.expectEqual(state, history.state);
|
||||
});
|
||||
|
||||
testing.expectEqual(undefined, history.go());
|
||||
</script>
|
||||
@@ -1,26 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,6 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<script id=history2>
|
||||
testing.expectEqual(true, history.state && history.state.testInProgress);
|
||||
</script>
|
||||
@@ -16,7 +16,9 @@
|
||||
testing.expectEqual('https://lightpanda.io', link.origin);
|
||||
|
||||
link.host = 'lightpanda.io:443';
|
||||
// Port is omitted if its the default one for the scheme.
|
||||
testing.expectEqual('lightpanda.io', link.host);
|
||||
// Port is omitted if its the default one for the scheme.
|
||||
testing.expectEqual('', link.port);
|
||||
testing.expectEqual('lightpanda.io', link.hostname);
|
||||
|
||||
@@ -42,6 +44,7 @@
|
||||
|
||||
testing.expectEqual('', link.port);
|
||||
link.port = '443';
|
||||
// Port is omitted if its the default one for the scheme.
|
||||
testing.expectEqual('foo.bar', link.host);
|
||||
testing.expectEqual('foo.bar', link.hostname);
|
||||
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,8 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,15 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,38 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<svg id=lower width="200" height="100" style="border:1px solid #ccc" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">
|
||||
<svg width="200" height="100" style="border:1px solid #ccc" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">
|
||||
<rect></rect>
|
||||
<text x="100" y="95" font-size="14" text-anchor="middle">OVER 9000!!</text>
|
||||
</svg>
|
||||
|
||||
<SVG ID=UPPER WIDTH="200" HEIGHT="100" STYLE="BORDER:1PX SOLID #CCC" XMLNS="http://www.w3.org/2000/svg" VIEWBOX="0 0 200 100">
|
||||
<RECT></RECT>
|
||||
<TEXT X="100" Y="95" FONT-SIZE="14" TEXT-ANCHOR="MIDDLE">OVER 9000!!!</TEXT>
|
||||
</SVG>
|
||||
|
||||
<script id=svg>
|
||||
testing.expectEqual(false, 'AString' instanceof SVGElement);
|
||||
|
||||
const svg1 = $('#lower');
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg1.getAttribute('xmlns'));
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg1.getAttributeNode('xmlns').value);
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg1.attributes.getNamedItem('xmlns').value);
|
||||
testing.expectEqual('0 0 200 100', svg1.getAttribute('viewBox'));
|
||||
testing.expectEqual('viewBox', svg1.getAttributeNode('viewBox').name);
|
||||
testing.expectEqual(true, svg1.outerHTML.includes('viewBox'));
|
||||
testing.expectEqual('svg', svg1.tagName);
|
||||
testing.expectEqual('rect', svg1.querySelector('rect').tagName);
|
||||
testing.expectEqual('text', svg1.querySelector('text').tagName);
|
||||
|
||||
const svg2 = $('#UPPER');
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg2.getAttribute('xmlns'));
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg2.getAttributeNode('xmlns').value);
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg2.attributes.getNamedItem('xmlns').value);
|
||||
testing.expectEqual('0 0 200 100', svg2.getAttribute('viewBox'));
|
||||
testing.expectEqual('viewBox', svg2.getAttributeNode('viewBox').name);
|
||||
testing.expectEqual(true, svg2.outerHTML.includes('viewBox'));
|
||||
testing.expectEqual('svg', svg2.tagName);
|
||||
testing.expectEqual('rect', svg2.querySelector('rect').tagName);
|
||||
testing.expectEqual('text', svg2.querySelector('text').tagName);
|
||||
const svg = document.querySelector('svg');
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg.getAttribute('xmlns'));
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg.getAttributeNode('xmlns').value);
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', svg.attributes.getNamedItem('xmlns').value);
|
||||
testing.expectEqual('0 0 200 100', svg.getAttribute('viewBox'));
|
||||
testing.expectEqual('viewBox', svg.getAttributeNode('viewBox').name);
|
||||
testing.expectEqual(true, svg.outerHTML.includes('viewBox'));
|
||||
</script>
|
||||
|
||||
@@ -51,6 +51,14 @@
|
||||
// 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];
|
||||
@@ -93,6 +101,18 @@
|
||||
_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;
|
||||
@@ -172,12 +192,15 @@
|
||||
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,
|
||||
|
||||
@@ -64,23 +64,6 @@
|
||||
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);
|
||||
@@ -93,17 +76,3 @@
|
||||
testing.expectEqual("", sk.hostname);
|
||||
testing.expectEqual("sveltekit-internal://", sk.href);
|
||||
</script>
|
||||
|
||||
<script id=invalidUrl>
|
||||
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>
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
</script>
|
||||
|
||||
<script id=dimensions>
|
||||
testing.expectEqual(5, innerHeight);
|
||||
// Width is 5 even if there are no elements
|
||||
testing.expectEqual(5, innerWidth);
|
||||
testing.expectEqual(1, innerHeight);
|
||||
// Width is 1 even if there are no elements
|
||||
testing.expectEqual(1, innerWidth);
|
||||
|
||||
let div1 = document.createElement('div');
|
||||
document.body.appendChild(div1);
|
||||
@@ -37,8 +37,8 @@
|
||||
document.body.appendChild(div2);
|
||||
div2.getClientRects();
|
||||
|
||||
testing.expectEqual(5, innerHeight);
|
||||
testing.expectEqual(10, innerWidth);
|
||||
testing.expectEqual(1, innerHeight);
|
||||
testing.expectEqual(2, innerWidth);
|
||||
</script>
|
||||
|
||||
<script id=setTimeout>
|
||||
@@ -149,19 +149,3 @@
|
||||
|
||||
testing.eventually(() => testing.expectEqual(true, isWindowTarget));
|
||||
</script>
|
||||
|
||||
<script id=reportError>
|
||||
let errorEventFired = false;
|
||||
let capturedError = null;
|
||||
|
||||
window.addEventListener('error', (e) => {
|
||||
errorEventFired = true;
|
||||
capturedError = e.error;
|
||||
});
|
||||
|
||||
const testError = new Error('Test error message');
|
||||
window.reportError(testError);
|
||||
|
||||
testing.expectEqual(true, errorEventFired);
|
||||
testing.expectEqual(testError, capturedError);
|
||||
</script>
|
||||
|
||||
368
src/url.zig
368
src/url.zig
@@ -1,78 +1,233 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Uri = std.Uri;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const WebApiURL = @import("browser/url/url.zig").URL;
|
||||
|
||||
const ada = @import("ada");
|
||||
|
||||
pub const stitch = URL.stitch;
|
||||
|
||||
pub const URL = struct {
|
||||
uri: Uri,
|
||||
raw: []const u8,
|
||||
/// Internal ada structure.
|
||||
internal: ada.URL,
|
||||
|
||||
pub const empty = URL{ .uri = .{ .scheme = "" }, .raw = "" };
|
||||
pub const about_blank = URL{ .uri = .{ .scheme = "" }, .raw = "about:blank" };
|
||||
pub const ParseError = ada.ParseError;
|
||||
|
||||
// We assume str will last as long as the URL
|
||||
// In some cases, this is safe to do, because we know the URL is short lived.
|
||||
// In most cases though, we assume the caller will just dupe the string URL
|
||||
// into an arena
|
||||
pub fn parse(str: []const u8, default_scheme: ?[]const u8) !URL {
|
||||
var uri = Uri.parse(str) catch try Uri.parseAfterScheme(default_scheme orelse "https", str);
|
||||
|
||||
// special case, url scheme is about, like about:blank.
|
||||
// Use an empty string as host.
|
||||
if (std.mem.eql(u8, uri.scheme, "about")) {
|
||||
uri.host = .{ .percent_encoded = "" };
|
||||
}
|
||||
|
||||
if (uri.host == null) {
|
||||
return error.MissingHost;
|
||||
}
|
||||
|
||||
std.debug.assert(uri.host.? == .percent_encoded);
|
||||
|
||||
return .{
|
||||
.uri = uri,
|
||||
.raw = str,
|
||||
/// Creates a new URL by parsing given `input`.
|
||||
/// `input` will be duped; so it can be freed after a call to this function.
|
||||
/// If `input` does not contain a scheme, `fallback_scheme` be used instead.
|
||||
/// `fallback_scheme` is `https` if not provided.
|
||||
pub fn parse(input: []const u8, fallback_scheme: ?[]const u8) ParseError!URL {
|
||||
// Try parsing directly; if it fails, we might have to provide a base.
|
||||
const internal = ada.parse(input) catch blk: {
|
||||
break :blk try ada.parseWithBase(fallback_scheme orelse "https", input);
|
||||
};
|
||||
|
||||
return .{ .internal = internal };
|
||||
}
|
||||
|
||||
pub fn fromURI(arena: Allocator, uri: *const Uri) !URL {
|
||||
// This is embarrassing.
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
try uri.writeToStream(.{
|
||||
.scheme = true,
|
||||
.authentication = true,
|
||||
.authority = true,
|
||||
.path = true,
|
||||
.query = true,
|
||||
.fragment = true,
|
||||
}, buf.writer(arena));
|
||||
|
||||
return parse(buf.items, null);
|
||||
pub fn parseWithBase(input: []const u8, base: []const u8) ParseError!URL {
|
||||
const internal = try ada.parseWithBase(input, base);
|
||||
return .{ .internal = internal };
|
||||
}
|
||||
|
||||
// Above, in `parse`, we error if a host doesn't exist
|
||||
// In other words, we can't have a URL with a null host.
|
||||
pub fn host(self: *const URL) []const u8 {
|
||||
return self.uri.host.?.percent_encoded;
|
||||
/// Uses the same URL to parse in-place.
|
||||
/// Assumes `internal` is valid.
|
||||
pub fn reparse(self: URL, str: []const u8) ParseError!URL {
|
||||
std.debug.assert(self.internal != null);
|
||||
|
||||
_ = ada.setHref(self.internal, str);
|
||||
if (!ada.isValid(self.internal)) {
|
||||
return error.Invalid;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn port(self: *const URL) ?u16 {
|
||||
return self.uri.port;
|
||||
/// Forms a `URL` from given `internal`. Memory is not copied.
|
||||
pub fn fromInternal(internal: ada.URL) URL {
|
||||
return .{ .internal = internal };
|
||||
}
|
||||
|
||||
pub fn scheme(self: *const URL) []const u8 {
|
||||
return self.uri.scheme;
|
||||
/// Deinitializes internal url.
|
||||
pub fn deinit(self: URL) void {
|
||||
std.debug.assert(self.internal != null);
|
||||
ada.free(self.internal);
|
||||
}
|
||||
|
||||
pub fn origin(self: *const URL, writer: *std.Io.Writer) !void {
|
||||
return self.uri.writeToStream(writer, .{ .scheme = true, .authority = true });
|
||||
/// Returns true if `internal` is initialized.
|
||||
pub fn isValid(self: URL) bool {
|
||||
return ada.isValid(self.internal);
|
||||
}
|
||||
|
||||
pub fn format(self: *const URL, writer: *std.Io.Writer) !void {
|
||||
return writer.writeAll(self.raw);
|
||||
pub fn setHost(self: URL, host_str: []const u8) error{InvalidHost}!void {
|
||||
const is_set = ada.setHost(self.internal, host_str);
|
||||
if (!is_set) return error.InvalidHost;
|
||||
}
|
||||
|
||||
pub fn setPort(self: URL, port_str: []const u8) error{InvalidPort}!void {
|
||||
const is_set = ada.setPort(self.internal, port_str);
|
||||
if (!is_set) return error.InvalidPort;
|
||||
}
|
||||
|
||||
pub fn getPort(self: URL) []const u8 {
|
||||
const port = ada.getPortNullable(self.internal);
|
||||
return port.data[0..port.length];
|
||||
}
|
||||
|
||||
/// Above, in `parse`, we error if a host doesn't exist
|
||||
/// In other words, we can't have a URL with a null host.
|
||||
pub fn host(self: URL) []const u8 {
|
||||
const str = ada.getHostNullable(self.internal);
|
||||
if (str.data == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return str.data[0..str.length];
|
||||
}
|
||||
|
||||
pub fn getHref(self: URL) []const u8 {
|
||||
const href = ada.getHrefNullable(self.internal);
|
||||
if (href.data == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return href.data[0..href.length];
|
||||
}
|
||||
|
||||
pub fn getHostname(self: URL) []const u8 {
|
||||
const hostname = ada.getHostnameNullable(self.internal);
|
||||
return hostname.data[0..hostname.length];
|
||||
}
|
||||
|
||||
pub fn setHostname(self: URL, hostname_str: []const u8) error{InvalidHostname}!void {
|
||||
const is_set = ada.setHostname(self.internal, hostname_str);
|
||||
if (!is_set) return error.InvalidHostname;
|
||||
}
|
||||
|
||||
pub fn getUsername(self: URL) ?[]const u8 {
|
||||
const username = ada.getUsernameNullable(self.internal);
|
||||
if (username.data == null) return null;
|
||||
return username.data[0..username.length];
|
||||
}
|
||||
|
||||
pub fn setUsername(self: URL, username: []const u8) error{InvalidUsername}!void {
|
||||
const is_set = ada.setUsername(self.internal, username);
|
||||
if (!is_set) return error.InvalidUsername;
|
||||
}
|
||||
|
||||
pub fn getPassword(self: URL) ?[]const u8 {
|
||||
const password = ada.getPasswordNullable(self.internal);
|
||||
if (password.data == null) return null;
|
||||
return password.data[0..password.length];
|
||||
}
|
||||
|
||||
pub fn setPassword(self: URL, password: []const u8) error{InvalidPassword}!void {
|
||||
const is_set = ada.setPassword(self.internal, password);
|
||||
if (!is_set) return error.InvalidPassword;
|
||||
}
|
||||
|
||||
pub fn getFragment(self: URL) ?[]const u8 {
|
||||
// Ada calls it "hash" instead of "fragment".
|
||||
const hash = ada.getHashNullable(self.internal);
|
||||
if (hash.data == null) return null;
|
||||
|
||||
return hash.data[0..hash.length];
|
||||
}
|
||||
|
||||
pub fn getSearch(self: URL) ?[]const u8 {
|
||||
const search = ada.getSearchNullable(self.internal);
|
||||
if (search.data == null) return null;
|
||||
return search.data[0..search.length];
|
||||
}
|
||||
|
||||
pub fn setSearch(self: URL, search: []const u8) void {
|
||||
return ada.setSearch(self.internal, search);
|
||||
}
|
||||
|
||||
pub fn getHash(self: URL) ?[]const u8 {
|
||||
const hash = ada.getHashNullable(self.internal);
|
||||
if (hash.data == null) return null;
|
||||
return hash.data[0..hash.length];
|
||||
}
|
||||
|
||||
pub fn setHash(self: URL, hash: []const u8) void {
|
||||
return ada.setHash(self.internal, hash);
|
||||
}
|
||||
|
||||
pub fn getProtocol(self: URL) []const u8 {
|
||||
return ada.getProtocol(self.internal);
|
||||
}
|
||||
|
||||
pub fn setProtocol(self: URL, protocol_str: []const u8) error{InvalidProtocol}!void {
|
||||
const is_set = ada.setProtocol(self.internal, protocol_str);
|
||||
if (!is_set) return error.InvalidProtocol;
|
||||
}
|
||||
|
||||
pub fn getScheme(self: URL) []const u8 {
|
||||
const proto = self.getProtocol();
|
||||
std.debug.assert(proto[proto.len - 1] == ':');
|
||||
|
||||
return proto.ptr[0 .. proto.len - 1];
|
||||
}
|
||||
|
||||
/// Returns the path.
|
||||
pub fn getPath(self: URL) []const u8 {
|
||||
const pathname = ada.getPathnameNullable(self.internal);
|
||||
// Return a slash if path is null.
|
||||
if (pathname.data == null) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
return pathname.data[0..pathname.length];
|
||||
}
|
||||
|
||||
pub fn setPath(self: URL, path: []const u8) error{InvalidPath}!void {
|
||||
const is_set = ada.setPathname(self.internal, path);
|
||||
if (!is_set) return error.InvalidPath;
|
||||
}
|
||||
|
||||
/// Returns true if the URL's protocol is secure.
|
||||
pub fn isSecure(self: URL) bool {
|
||||
const scheme = ada.getSchemeType(self.internal);
|
||||
return scheme == ada.Scheme.https or scheme == ada.Scheme.wss;
|
||||
}
|
||||
|
||||
pub fn writeToStream(self: URL, writer: anytype) !void {
|
||||
return writer.writeAll(self.getHref());
|
||||
}
|
||||
|
||||
/// Returns the origin string; caller owns the memory.
|
||||
pub fn getOrigin(self: URL, allocator: Allocator) ![]const u8 {
|
||||
const s = ada.getOriginNullable(self.internal);
|
||||
if (s.data == null) {
|
||||
return "";
|
||||
}
|
||||
defer ada.freeOwnedString(.{ .data = s.data, .length = s.length });
|
||||
|
||||
return allocator.dupe(u8, s.data[0..s.length]);
|
||||
}
|
||||
|
||||
// TODO: Skip unnecessary allocation by writing url parts directly to stream.
|
||||
pub fn origin(self: URL, writer: *std.Io.Writer) !void {
|
||||
// Ada manages its own memory for origin.
|
||||
// Here we write it to stream and free it afterwards.
|
||||
const s = ada.getOriginNullable(self.internal);
|
||||
if (s.data == null) {
|
||||
return;
|
||||
}
|
||||
defer ada.freeOwnedString(.{ .data = s.data, .length = s.length });
|
||||
|
||||
return writer.writeAll(s.data[0..s.length]);
|
||||
}
|
||||
|
||||
pub fn format(self: URL, writer: *std.Io.Writer) !void {
|
||||
return self.writeToStream(writer);
|
||||
}
|
||||
|
||||
/// Converts `URL` to `WebApiURL`.
|
||||
pub fn toWebApi(self: URL, allocator: Allocator) !WebApiURL {
|
||||
return WebApiURL.constructFromInternal(allocator, self.internal);
|
||||
}
|
||||
|
||||
/// Properly stitches two URL fragments together.
|
||||
@@ -213,26 +368,6 @@ 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 {
|
||||
@@ -569,92 +704,3 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
91
vendor/ada/root.zig
vendored
91
vendor/ada/root.zig
vendored
@@ -5,12 +5,13 @@ const c = @cImport({
|
||||
@cInclude("ada_c.h");
|
||||
});
|
||||
|
||||
/// Pointer type.
|
||||
pub const URL = c.ada_url;
|
||||
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;
|
||||
/// Pointer type.
|
||||
pub const URLSearchParams = c.ada_url_search_params;
|
||||
|
||||
pub const ParseError = error{Invalid};
|
||||
@@ -33,14 +34,6 @@ pub fn parseWithBase(input: []const u8, base: []const u8) ParseError!URL {
|
||||
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);
|
||||
}
|
||||
@@ -63,7 +56,13 @@ pub inline fn copy(url: URL) URL {
|
||||
return c.ada_copy(url);
|
||||
}
|
||||
|
||||
/// Contrary to other getters, this heap allocates.
|
||||
/// Can return an empty string.
|
||||
/// Contrary to other getters, returned slice is heap allocated.
|
||||
pub inline fn getOrigin(url: URL) []const u8 {
|
||||
const origin = c.ada_get_origin(url);
|
||||
return origin.data[0..origin.length];
|
||||
}
|
||||
|
||||
pub inline fn getOriginNullable(url: URL) OwnedString {
|
||||
return c.ada_get_origin(url);
|
||||
}
|
||||
@@ -76,6 +75,12 @@ pub inline fn getUsernameNullable(url: URL) String {
|
||||
return c.ada_get_username(url);
|
||||
}
|
||||
|
||||
/// Can return an empty string.
|
||||
pub inline fn getUsername(url: URL) []const u8 {
|
||||
const username = c.ada_get_username(url);
|
||||
return username.data[0..username.length];
|
||||
}
|
||||
|
||||
pub inline fn getPasswordNullable(url: URL) String {
|
||||
return c.ada_get_password(url);
|
||||
}
|
||||
@@ -84,18 +89,58 @@ pub inline fn getSearchNullable(url: URL) String {
|
||||
return c.ada_get_search(url);
|
||||
}
|
||||
|
||||
/// Can return an empty string.
|
||||
pub inline fn getPassword(url: URL) []const u8 {
|
||||
const password = c.ada_get_password(url);
|
||||
return password.data[0..password.length];
|
||||
}
|
||||
|
||||
pub inline fn getPortNullable(url: URL) String {
|
||||
return c.ada_get_port(url);
|
||||
}
|
||||
|
||||
pub inline fn getPort(url: URL) []const u8 {
|
||||
if (!c.ada_has_port(url)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const port = c.ada_get_port(url);
|
||||
return port.data[0..port.length];
|
||||
}
|
||||
|
||||
pub inline fn getHash(url: URL) []const u8 {
|
||||
const hash = c.ada_get_hash(url);
|
||||
return hash.data[0..hash.length];
|
||||
}
|
||||
|
||||
pub inline fn getHashNullable(url: URL) String {
|
||||
return c.ada_get_hash(url);
|
||||
}
|
||||
|
||||
/// `data` is null if host not provided.
|
||||
pub inline fn getHostNullable(url: URL) String {
|
||||
return c.ada_get_host(url);
|
||||
}
|
||||
|
||||
/// Returns an empty string if host not provided.
|
||||
pub inline fn getHost(url: URL) []const u8 {
|
||||
const host = getHostNullable(url);
|
||||
if (host.data == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return host.data[0..host.length];
|
||||
}
|
||||
|
||||
pub inline fn getHostname(url: URL) []const u8 {
|
||||
if (!c.ada_has_hostname(url)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const hostname = c.ada_get_hostname(url);
|
||||
return hostname.data[0..hostname.length];
|
||||
}
|
||||
|
||||
pub inline fn getHostnameNullable(url: URL) String {
|
||||
return c.ada_get_hostname(url);
|
||||
}
|
||||
@@ -104,10 +149,22 @@ 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 getPathname(url: URL) []const u8 {
|
||||
const pathname = c.ada_get_pathname(url);
|
||||
return pathname.data[0..pathname.length];
|
||||
}
|
||||
|
||||
pub inline fn getSearch(url: URL) String {
|
||||
return c.ada_get_search(url);
|
||||
}
|
||||
|
||||
pub inline fn getProtocol(url: URL) []const u8 {
|
||||
const protocol = c.ada_get_protocol(url);
|
||||
return protocol.data[0..protocol.length];
|
||||
}
|
||||
|
||||
/// Sets the href for given URL.
|
||||
/// Call `isInvalid` afterwards to check correctness.
|
||||
pub inline fn setHref(url: URL, input: []const u8) bool {
|
||||
return c.ada_set_href(url, input.ptr, input.len);
|
||||
}
|
||||
@@ -148,18 +205,10 @@ 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;
|
||||
|
||||
2
vendor/netsurf/libdom
vendored
2
vendor/netsurf/libdom
vendored
Submodule vendor/netsurf/libdom updated: c7f2d3cd27...ef7d5d4fab
2
vendor/netsurf/libhubbub
vendored
2
vendor/netsurf/libhubbub
vendored
Submodule vendor/netsurf/libhubbub updated: 1624ba6250...6f102212c8
Reference in New Issue
Block a user