mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 14:43:28 +00:00
Compare commits
69 Commits
nikneym/ad
...
1015fc09ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1015fc09ee | ||
|
|
1c37b1c70e | ||
|
|
2422c8718c | ||
|
|
8d4cf400ce | ||
|
|
c6a0368c61 | ||
|
|
033eb82ae5 | ||
|
|
2d14452dda | ||
|
|
d4d35670a0 | ||
|
|
7d39bc979f | ||
|
|
d60d3ebaac | ||
|
|
ba66b7c5db | ||
|
|
8342f0c394 | ||
|
|
69884b9d8d | ||
|
|
c568a75599 | ||
|
|
9deb5249a9 | ||
|
|
fb6fbffe3f | ||
|
|
510c61cc20 | ||
|
|
6915738e02 | ||
|
|
4f62cc833b | ||
|
|
46ffb801db | ||
|
|
d2065f713f | ||
|
|
6f8c3abb55 | ||
|
|
163a0e8b70 | ||
|
|
ca3efb3ad9 | ||
|
|
4468932346 | ||
|
|
9a03ba61c5 | ||
|
|
fe3777041d | ||
|
|
1c579a98b4 | ||
|
|
3e10cf0a64 | ||
|
|
ef9784a7d4 | ||
|
|
6f1c3c8fd2 | ||
|
|
e12c650ea5 | ||
|
|
9373cbb440 | ||
|
|
fd6d038956 | ||
|
|
9845392b71 | ||
|
|
0795b7a583 | ||
|
|
29f0e71f10 | ||
|
|
1a47f7b5a8 | ||
|
|
6a30ab7a57 | ||
|
|
758f7deb93 | ||
|
|
9f4e3bf792 | ||
|
|
a5dfe8ab28 | ||
|
|
c52dce1c48 | ||
|
|
288379aa7d | ||
|
|
a9739bf361 | ||
|
|
c69adcb163 | ||
|
|
0b4a1b4a1b | ||
|
|
cc0c1bcf3a | ||
|
|
55746f1a1d | ||
|
|
7bb8581a95 | ||
|
|
521c0f8460 | ||
|
|
14a23123c0 | ||
|
|
09be5e23f1 | ||
|
|
0aaed08c1e | ||
|
|
4bfe3b6fe1 | ||
|
|
b610aa1c0c | ||
|
|
73da04bea2 | ||
|
|
18c851e53f | ||
|
|
41f4533bc0 | ||
|
|
4db8a967b6 | ||
|
|
ff70f4e79f | ||
|
|
c9517aff7d | ||
|
|
3657a49a2c | ||
|
|
71e7aa5262 | ||
|
|
2e435f5d4e | ||
|
|
859b03c4a6 | ||
|
|
ee8786444f | ||
|
|
afac4fc37f | ||
|
|
de83521e08 |
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.1'
|
||||
default: '0.15.2'
|
||||
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.1
|
||||
ZIG_VERSION: 0.15.2
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM debian:stable
|
||||
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG=0.15.1
|
||||
ARG ZIG=0.15.2
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG V8=14.0.365.4
|
||||
ARG ZIG_V8=v0.1.33
|
||||
|
||||
11
Makefile
11
Makefile
@@ -96,9 +96,16 @@ wpt-summary:
|
||||
@printf "\e[36mBuilding wpt...\e[0m\n"
|
||||
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
|
||||
## Test
|
||||
## Test - `grep` is used to filter out the huge compile command on build
|
||||
ifeq ($(OS), macos)
|
||||
test:
|
||||
@TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
|
||||
@script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \
|
||||
| grep --line-buffered -v "^/.*zig test -freference-trace"
|
||||
else
|
||||
test:
|
||||
@script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' /dev/null 2>&1 \
|
||||
| grep --line-buffered -v "^/.*zig test -freference-trace"
|
||||
endif
|
||||
|
||||
## Run demo/runner end to end tests
|
||||
end2end:
|
||||
|
||||
@@ -164,7 +164,7 @@ You can also follow the progress of our Javascript support in our dedicated [zig
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to
|
||||
install it with the right version in order to build the project.
|
||||
|
||||
Lightpanda also depends on
|
||||
|
||||
34
build.zig
34
build.zig
@@ -23,7 +23,7 @@ const Build = std.Build;
|
||||
|
||||
/// Do not rename this constant. It is scanned by some scripts to determine
|
||||
/// which zig version to install.
|
||||
const recommended_zig_version = "0.15.1";
|
||||
const recommended_zig_version = "0.15.2";
|
||||
|
||||
pub fn build(b: *Build) !void {
|
||||
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
||||
@@ -384,6 +384,7 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
|
||||
try buildMbedtls(b, mod);
|
||||
try buildNghttp2(b, mod);
|
||||
try buildCurl(b, mod);
|
||||
try buildAda(b, mod);
|
||||
|
||||
switch (target.result.os.tag) {
|
||||
.macos => {
|
||||
@@ -849,3 +850,34 @@ fn buildCurl(b: *Build, m: *Build.Module) !void {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub fn buildAda(b: *Build, m: *Build.Module) !void {
|
||||
const ada_dep = b.dependency("ada-singleheader", .{});
|
||||
|
||||
const ada_mod = b.createModule(.{
|
||||
.root_source_file = b.path("vendor/ada/root.zig"),
|
||||
});
|
||||
|
||||
const ada_lib = b.addLibrary(.{
|
||||
.name = "ada",
|
||||
.root_module = b.createModule(.{
|
||||
.link_libcpp = true,
|
||||
.target = m.resolved_target,
|
||||
.optimize = m.optimize,
|
||||
}),
|
||||
.linkage = .static,
|
||||
});
|
||||
|
||||
ada_lib.addCSourceFile(.{
|
||||
.file = ada_dep.path("ada.cpp"),
|
||||
.flags = &.{ "-std=c++20", "-O3" },
|
||||
.language = .cpp,
|
||||
});
|
||||
|
||||
ada_lib.installHeader(ada_dep.path("ada_c.h"), "ada_c.h");
|
||||
|
||||
// Link the library to ada module.
|
||||
ada_mod.linkLibrary(ada_lib);
|
||||
// Expose ada module to main module.
|
||||
m.addImport("ada", ada_mod);
|
||||
}
|
||||
|
||||
@@ -9,5 +9,9 @@
|
||||
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" }
|
||||
.@"ada-singleheader" = .{
|
||||
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
|
||||
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
12
flake.lock
generated
12
flake.lock
generated
@@ -75,11 +75,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1756822655,
|
||||
"narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
|
||||
"lastModified": 1760968520,
|
||||
"narHash": "sha256-EjGslHDzCBKOVr+dnDB1CAD7wiQSHfUt3suOpFj9O1Q=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
|
||||
"rev": "e755547441a0413942a37692f7bf7fc6315bb7f6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -136,11 +136,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756555914,
|
||||
"narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=",
|
||||
"lastModified": 1760747435,
|
||||
"narHash": "sha256-wNB/W3x+or4mdNxFPNOH5/WFckNpKgFRZk7OnOsLtm0=",
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6",
|
||||
"rev": "d0f239b887b1ac736c0f3dde91bf5bf2ecf3a420",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
targetPkgs =
|
||||
pkgs: with pkgs; [
|
||||
# Build Tools
|
||||
zigpkgs."0.15.1"
|
||||
zigpkgs."0.15.2"
|
||||
zls
|
||||
python3
|
||||
pkg-config
|
||||
|
||||
@@ -212,7 +212,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
|
||||
if (source == .@"inline" and self.scripts.first == null) {
|
||||
// inline script with no pending scripts, execute it immediately.
|
||||
// (if there is a pending script, then we cannot execute this immediately
|
||||
// as it needs to best executed in order)
|
||||
// as it needs to be executed in order)
|
||||
return script.eval(page);
|
||||
}
|
||||
|
||||
@@ -326,16 +326,31 @@ pub fn waitForModule(self: *ScriptManager, url: [:0]const u8) !GetResult {
|
||||
};
|
||||
const sync = entry.value_ptr.*;
|
||||
|
||||
// We can have multiple scripts waiting for the same module in concurrency.
|
||||
// We use the waiters to ensures only the last waiter deinit the resources.
|
||||
sync.waiters += 1;
|
||||
defer sync.waiters -= 1;
|
||||
|
||||
var client = self.client;
|
||||
while (true) {
|
||||
switch (sync.state) {
|
||||
.loading => {},
|
||||
.done => {
|
||||
// Our caller has its own higher level cache (caching the
|
||||
// actual compiled module). There's no reason for us to keep this
|
||||
defer self.sync_module_pool.destroy(sync);
|
||||
defer self.sync_modules.removeByPtr(entry.key_ptr);
|
||||
if (sync.waiters == 1) {
|
||||
// Our caller has its own higher level cache (caching the
|
||||
// actual compiled module). There's no reason for us to keep
|
||||
// this if we are the last waiter.
|
||||
defer self.sync_module_pool.destroy(sync);
|
||||
defer self.sync_modules.removeByPtr(entry.key_ptr);
|
||||
return .{
|
||||
.shared = false,
|
||||
.buffer = sync.buffer,
|
||||
.buffer_pool = &self.buffer_pool,
|
||||
};
|
||||
}
|
||||
|
||||
return .{
|
||||
.shared = true,
|
||||
.buffer = sync.buffer,
|
||||
.buffer_pool = &self.buffer_pool,
|
||||
};
|
||||
@@ -384,6 +399,7 @@ 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
|
||||
@@ -450,6 +466,12 @@ pub fn isDone(self: *const ScriptManager) bool {
|
||||
self.deferreds.first == null; // and there are no more <script defer src=> to wait for
|
||||
}
|
||||
|
||||
fn asyncScriptIsDone(self: *ScriptManager) void {
|
||||
if (self.isDone()) {
|
||||
self.page.documentIsComplete();
|
||||
}
|
||||
}
|
||||
|
||||
fn startCallback(transfer: *Http.Transfer) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||
script.startCallback(transfer) catch |err| {
|
||||
@@ -595,8 +617,10 @@ 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 {
|
||||
@@ -873,6 +897,8 @@ const SyncModule = struct {
|
||||
manager: *ScriptManager,
|
||||
buffer: std.ArrayListUnmanaged(u8) = .{},
|
||||
state: State = .loading,
|
||||
// number of waiters for the module.
|
||||
waiters: u8 = 0,
|
||||
|
||||
const State = union(enum) {
|
||||
done,
|
||||
@@ -988,6 +1014,7 @@ pub const AsyncModule = struct {
|
||||
var self: *AsyncModule = @ptrCast(@alignCast(ctx));
|
||||
defer self.manager.async_module_pool.destroy(self);
|
||||
self.cb(self.cb_data, .{
|
||||
.shared = false,
|
||||
.buffer = self.buffer,
|
||||
.buffer_pool = &self.manager.buffer_pool,
|
||||
});
|
||||
@@ -1011,8 +1038,13 @@ pub const AsyncModule = struct {
|
||||
pub const GetResult = struct {
|
||||
buffer: std.ArrayListUnmanaged(u8),
|
||||
buffer_pool: *BufferPool,
|
||||
shared: bool,
|
||||
|
||||
pub fn deinit(self: *GetResult) void {
|
||||
// if the result is shared, don't deinit.
|
||||
if (self.shared) {
|
||||
return;
|
||||
}
|
||||
self.buffer_pool.release(self.buffer);
|
||||
}
|
||||
|
||||
|
||||
@@ -562,7 +562,7 @@ pub const Selector = union(enum) {
|
||||
|
||||
const ntag = try n.tag();
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("intput", ntag)) {
|
||||
if (std.ascii.eqlIgnoreCase("input", ntag)) {
|
||||
const ntype = try n.attr("type");
|
||||
if (ntype == null) return false;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
@@ -313,6 +314,11 @@ pub const Document = struct {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.adopted_style_sheets = try sheets.persist();
|
||||
}
|
||||
|
||||
pub fn _hasFocus(_: *parser.Document) bool {
|
||||
log.debug(.web_api, "not implemented", .{ .feature = "Document hasFocus" });
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -286,7 +286,7 @@ const Opts = struct {
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection
|
||||
// HTMLCollection is re implemented in zig here because libdom
|
||||
// dom_html_collection expects a comparison function callback as arguement.
|
||||
// dom_html_collection expects a comparison function callback as argument.
|
||||
// But we wanted a dynamically comparison here, according to the match tagname.
|
||||
pub const HTMLCollection = struct {
|
||||
matcher: Matcher,
|
||||
|
||||
@@ -360,18 +360,30 @@ pub const Node = struct {
|
||||
node: Union,
|
||||
};
|
||||
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
|
||||
if (options) |options_| if (options_.composed) {
|
||||
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
|
||||
};
|
||||
const composed = if (options) |opts| opts.composed else false;
|
||||
|
||||
const root = parser.nodeGetRootNode(self);
|
||||
if (page.getNodeState(root)) |state| {
|
||||
if (state.shadow_root) |sr| {
|
||||
return .{ .shadow_root = sr };
|
||||
var current_root = parser.nodeGetRootNode(self);
|
||||
|
||||
while (true) {
|
||||
const node_type = parser.nodeType(current_root);
|
||||
|
||||
if (node_type == .document_fragment) {
|
||||
if (parser.documentFragmentGetHost(@ptrCast(current_root))) |host| {
|
||||
if (page.getNodeState(host)) |state| {
|
||||
if (state.shadow_root) |sr| {
|
||||
if (!composed) {
|
||||
return .{ .shadow_root = sr };
|
||||
}
|
||||
current_root = parser.nodeGetRootNode(@ptrCast(sr.host));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return .{ .node = try Node.toInterface(root) };
|
||||
return .{ .node = try Node.toInterface(current_root) };
|
||||
}
|
||||
|
||||
pub fn _hasChildNodes(self: *parser.Node) bool {
|
||||
@@ -461,7 +473,7 @@ pub const Node = struct {
|
||||
|
||||
// Check if the hierarchy node tree constraints are respected.
|
||||
// For now, it checks only if new nodes are not self.
|
||||
// TODO implements the others contraints.
|
||||
// TODO implements the others constraints.
|
||||
// see https://dom.spec.whatwg.org/#concept-node-tree
|
||||
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
|
||||
for (nodes) |n| {
|
||||
|
||||
@@ -47,6 +47,9 @@ 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,
|
||||
@@ -60,7 +63,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 .reject;
|
||||
}) return .skip;
|
||||
|
||||
// Verify that we aren't filtering it out.
|
||||
if (filter) |f| {
|
||||
|
||||
@@ -74,10 +74,10 @@ pub const NodeIterator = struct {
|
||||
|
||||
return .{
|
||||
.root = node,
|
||||
.reference_node = node,
|
||||
.what_to_show = what_to_show,
|
||||
.filter = filter,
|
||||
.reference_node = node,
|
||||
.filter_func = filter_func,
|
||||
.what_to_show = what_to_show,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,14 +115,27 @@ pub const NodeIterator = struct {
|
||||
|
||||
if (try self.firstChild(self.reference_node)) |child| {
|
||||
self.reference_node = child;
|
||||
self.pointer_before_current = false;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
var current = self.reference_node;
|
||||
while (current != self.root) {
|
||||
if (try self.nextSibling(current)) |sibling| {
|
||||
self.reference_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
// Try to get next sibling (including .skip/.reject nodes we need to descend into)
|
||||
if (try self.nextSiblingOrSkipReject(current)) |result| {
|
||||
if (result.should_descend) {
|
||||
// This is a .skip/.reject node - try to find acceptable children within it
|
||||
if (try self.firstChild(result.node)) |child| {
|
||||
self.reference_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
// No acceptable children, continue looking at this node's siblings
|
||||
current = result.node;
|
||||
continue;
|
||||
}
|
||||
// This is an .accept node - return it
|
||||
self.reference_node = result.node;
|
||||
return try Node.toInterface(result.node);
|
||||
}
|
||||
|
||||
current = (parser.nodeParentNode(current)) orelse break;
|
||||
@@ -254,6 +267,22 @@ pub const NodeIterator = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the next sibling that is either acceptable or should be descended into (skip/reject)
|
||||
fn nextSiblingOrSkipReject(self: *const NodeIterator, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
|
||||
var current = node;
|
||||
|
||||
while (true) {
|
||||
current = (parser.nodeNextSibling(current)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return .{ .node = current, .should_descend = false },
|
||||
.skip, .reject => return .{ .node = current, .should_descend = true },
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn callbackStart(self: *NodeIterator) !void {
|
||||
if (self.is_in_callback) {
|
||||
// this is the correct DOMExeption
|
||||
|
||||
@@ -195,7 +195,7 @@ test "Performance: now" {
|
||||
}
|
||||
|
||||
var after = perf._now();
|
||||
while (after <= now) { // Loop untill after > now
|
||||
while (after <= now) { // Loop until after > now
|
||||
try testing.expectEqual(after, now);
|
||||
after = perf._now();
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ pub const Range = struct {
|
||||
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
|
||||
try ensureValidOffset(node, offset_);
|
||||
const offset: u32 = @intCast(offset_);
|
||||
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
|
||||
const position = compare(node, offset, self.proto.end_node, self.proto.end_offset) catch |err| switch (err) {
|
||||
error.WrongDocument => blk: {
|
||||
// allow a node with a different root than the current, or
|
||||
// a disconnected one. Treat it as if it's "after", so that
|
||||
@@ -103,7 +103,7 @@ pub const Range = struct {
|
||||
};
|
||||
|
||||
if (position == 1) {
|
||||
// if we're setting the node after the current start, the end must
|
||||
// if we're setting the node after the current end, the end must
|
||||
// be set too.
|
||||
self.proto.end_offset = offset;
|
||||
self.proto.end_node = node;
|
||||
@@ -378,7 +378,7 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
|
||||
|
||||
const child_parent, const child_index = try getParentAndIndex(child);
|
||||
std.debug.assert(node_a == child_parent);
|
||||
return if (child_index < offset_a) -1 else 1;
|
||||
return if (offset_a <= child_index) -1 else 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
|
||||
@@ -144,6 +144,23 @@ 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;
|
||||
|
||||
@@ -193,19 +210,36 @@ pub const TreeWalker = struct {
|
||||
}
|
||||
|
||||
pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.firstChild(self.current_node)) |child| {
|
||||
var current = self.current_node;
|
||||
|
||||
// First, try to go to first child of current node
|
||||
if (try self.firstChild(current)) |child| {
|
||||
self.current_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
var current = self.current_node;
|
||||
// No acceptable children, move to next node in tree
|
||||
while (current != self.root) {
|
||||
if (try self.nextSibling(current)) |sibling| {
|
||||
self.current_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
const result = try self.nextSiblingOrSkip(current) orelse {
|
||||
// No next sibling, go up to parent and continue
|
||||
// or, if there is no parent, we're done
|
||||
current = (parser.nodeParentNode(current)) orelse break;
|
||||
continue;
|
||||
};
|
||||
|
||||
if (!result.should_descend) {
|
||||
// This is an .accept node - return it
|
||||
self.current_node = result.node;
|
||||
return try Node.toInterface(result.node);
|
||||
}
|
||||
|
||||
current = (parser.nodeParentNode(current)) orelse break;
|
||||
// This is a .skip node - try to find acceptable children within it
|
||||
if (try self.firstChild(result.node)) |child| {
|
||||
self.current_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
// No acceptable children, continue looking at this node's siblings
|
||||
current = result.node;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -236,10 +236,10 @@ fn isVoid(elem: *parser.Element) !bool {
|
||||
};
|
||||
}
|
||||
|
||||
fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
|
||||
fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void {
|
||||
var v = value;
|
||||
while (v.len > 0) {
|
||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
|
||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', 194 }) orelse {
|
||||
return writer.writeAll(v);
|
||||
};
|
||||
try writer.writeAll(v[0..index]);
|
||||
@@ -247,13 +247,22 @@ fn writeEscapedTextNode(writer: anytype, 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: anytype, value: []const u8) !void {
|
||||
fn writeEscapedAttributeValue(writer: *std.Io.Writer, value: []const u8) !void {
|
||||
var v = value;
|
||||
while (v.len > 0) {
|
||||
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
|
||||
|
||||
57
src/browser/events/composition_event.zig
Normal file
57
src/browser/events/composition_event.zig
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
|
||||
pub const CompositionEvent = struct {
|
||||
data: []const u8,
|
||||
proto: parser.Event,
|
||||
|
||||
pub const union_make_copy = true;
|
||||
pub const prototype = *parser.Event;
|
||||
|
||||
pub const ConstructorOptions = struct {
|
||||
data: []const u8 = "",
|
||||
};
|
||||
|
||||
pub fn constructor(event_type: []const u8, options_: ?ConstructorOptions) !CompositionEvent {
|
||||
const options: ConstructorOptions = options_ orelse .{};
|
||||
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, event_type, .{});
|
||||
parser.eventSetInternalType(event, .composition_event);
|
||||
|
||||
return .{
|
||||
.proto = event.*,
|
||||
.data = options.data,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_data(self: *const CompositionEvent) []const u8 {
|
||||
return self.data;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Events.Composition" {
|
||||
try testing.htmlRunner("events/composition.html");
|
||||
}
|
||||
@@ -37,6 +37,7 @@ const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
|
||||
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
||||
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
||||
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
|
||||
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = .{
|
||||
@@ -48,6 +49,7 @@ pub const Interfaces = .{
|
||||
ErrorEvent,
|
||||
MessageEvent,
|
||||
PopStateEvent,
|
||||
CompositionEvent,
|
||||
};
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
@@ -72,10 +74,11 @@ pub const Event = struct {
|
||||
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
|
||||
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
|
||||
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
|
||||
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
|
||||
.error_event => .{ .ErrorEvent = (@as(*ErrorEvent, @fieldParentPtr("proto", evt))).* },
|
||||
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
|
||||
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
|
||||
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
|
||||
.composition_event => .{ .CompositionEvent = (@as(*CompositionEvent, @fieldParentPtr("proto", evt))).* },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -42,12 +42,12 @@ pub const HTMLDocument = struct {
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 {
|
||||
// libdom's document_html get_domain always returns null, this is
|
||||
// the way MDN recommends getting the domain anyways, since document.domain
|
||||
// is deprecated.
|
||||
const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
|
||||
return location.get_host(page);
|
||||
return location.get_host();
|
||||
}
|
||||
|
||||
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
|
||||
|
||||
@@ -218,36 +218,36 @@ pub const HTMLAnchorElement = struct {
|
||||
}
|
||||
|
||||
pub fn get_href(self: *parser.Anchor) ![]const u8 {
|
||||
return try parser.anchorGetHref(self);
|
||||
return parser.anchorGetHref(self);
|
||||
}
|
||||
|
||||
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void {
|
||||
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
|
||||
return try parser.anchorSetHref(self, full);
|
||||
return parser.anchorSetHref(self, full);
|
||||
}
|
||||
|
||||
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
|
||||
return try parser.anchorGetHrefLang(self);
|
||||
return parser.anchorGetHrefLang(self);
|
||||
}
|
||||
|
||||
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
|
||||
return try parser.anchorSetHrefLang(self, href);
|
||||
return parser.anchorSetHrefLang(self, href);
|
||||
}
|
||||
|
||||
pub fn get_type(self: *parser.Anchor) ![]const u8 {
|
||||
return try parser.anchorGetType(self);
|
||||
return parser.anchorGetType(self);
|
||||
}
|
||||
|
||||
pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
|
||||
return try parser.anchorSetType(self, t);
|
||||
return parser.anchorSetType(self, t);
|
||||
}
|
||||
|
||||
pub fn get_rel(self: *parser.Anchor) ![]const u8 {
|
||||
return try parser.anchorGetRel(self);
|
||||
return parser.anchorGetRel(self);
|
||||
}
|
||||
|
||||
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
|
||||
return try parser.anchorSetRel(self, t);
|
||||
return parser.anchorSetRel(self, t);
|
||||
}
|
||||
|
||||
pub fn get_text(self: *parser.Anchor) !?[]const u8 {
|
||||
@@ -269,182 +269,175 @@ pub const HTMLAnchorElement = struct {
|
||||
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
|
||||
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
|
||||
}
|
||||
return .empty;
|
||||
return error.NotProvided;
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return try u.get_origin(page);
|
||||
defer u.destructor();
|
||||
return u.get_origin(page);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return u.get_protocol();
|
||||
defer u.destructor();
|
||||
|
||||
return page.call_arena.dupe(u8, u.get_protocol());
|
||||
}
|
||||
|
||||
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
pub fn set_protocol(self: *parser.Anchor, protocol: []const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
try u.set_protocol(protocol);
|
||||
|
||||
u.uri.scheme = v;
|
||||
const href = try u.toString(arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return try u.get_host(page);
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
|
||||
return page.call_arena.dupe(u8, u.get_host());
|
||||
}
|
||||
|
||||
pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
||||
// search : separator
|
||||
var p: ?u16 = null;
|
||||
var h: []const u8 = undefined;
|
||||
for (v, 0..) |c, i| {
|
||||
if (c == ':') {
|
||||
h = v[0..i];
|
||||
p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const arena = page.arena;
|
||||
pub fn set_host(self: *parser.Anchor, host: []const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
try u.set_host(host);
|
||||
|
||||
if (p) |pp| {
|
||||
u.uri.host = .{ .raw = h };
|
||||
u.uri.port = pp;
|
||||
} else {
|
||||
u.uri.host = .{ .raw = v };
|
||||
u.uri.port = null;
|
||||
}
|
||||
|
||||
const href = try u.toString(arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return u.get_hostname();
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
return page.call_arena.dupe(u8, u.get_hostname());
|
||||
}
|
||||
|
||||
pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
u.uri.host = .{ .raw = v };
|
||||
const href = try u.toString(arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
defer u.destructor();
|
||||
try u.set_hostname(hostname);
|
||||
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return try u.get_port(page);
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
return page.call_arena.dupe(u8, u.get_port());
|
||||
}
|
||||
|
||||
pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
|
||||
if (v != null and v.?.len > 0) {
|
||||
u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
|
||||
if (maybe_port) |port| {
|
||||
try u.set_port(port);
|
||||
} else {
|
||||
u.uri.port = null;
|
||||
u.clearPort();
|
||||
}
|
||||
|
||||
const href = try u.toString(arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return u.get_username();
|
||||
}
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
|
||||
pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
var u = try url(self, page);
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.user = .{ .raw = vv };
|
||||
} else {
|
||||
u.uri.user = null;
|
||||
const username = u.get_username();
|
||||
if (username.len == 0) {
|
||||
return "";
|
||||
}
|
||||
const href = try u.toString(arena);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
return page.call_arena.dupe(u8, username);
|
||||
}
|
||||
|
||||
pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
|
||||
const username = if (maybe_username) |username| username else "";
|
||||
try u.set_username(username);
|
||||
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return try page.arena.dupe(u8, u.get_password());
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
|
||||
return page.call_arena.dupe(u8, u.get_password());
|
||||
}
|
||||
|
||||
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.password = .{ .raw = vv };
|
||||
} else {
|
||||
u.uri.password = null;
|
||||
}
|
||||
const href = try u.toString(arena);
|
||||
const password = if (maybe_password) |password| password else "";
|
||||
try u.set_password(password);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return u.get_pathname();
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
|
||||
return page.call_arena.dupe(u8, u.get_pathname());
|
||||
}
|
||||
|
||||
pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
u.uri.path = .{ .raw = v };
|
||||
const href = try u.toString(arena);
|
||||
defer u.destructor();
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
try u.set_pathname(pathname);
|
||||
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return try u.get_search(page);
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
// This allocates in page arena so no need to dupe.
|
||||
return u.get_search(page);
|
||||
}
|
||||
|
||||
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
try u.set_search(v, page);
|
||||
|
||||
const href = try u.toString(page.call_arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return try u.get_hash(page);
|
||||
var u = url(self, page) catch return "";
|
||||
defer u.destructor();
|
||||
|
||||
return page.call_arena.dupe(u8, u.get_hash());
|
||||
}
|
||||
|
||||
pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
pub fn set_hash(self: *parser.Anchor, maybe_hash: ?[]const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
defer u.destructor();
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.fragment = .{ .raw = vv };
|
||||
if (maybe_hash) |hash| {
|
||||
try u.set_hash(hash);
|
||||
} else {
|
||||
u.uri.fragment = null;
|
||||
u.clearHash();
|
||||
}
|
||||
const href = try u.toString(arena);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
const href = try u._toString(page);
|
||||
return parser.anchorSetHref(self, href);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -732,6 +725,9 @@ pub const HTMLInputElement = struct {
|
||||
pub fn set_value(self: *parser.Input, value: []const u8) !void {
|
||||
try parser.inputSetValue(self, value);
|
||||
}
|
||||
pub fn _select(_: *parser.Input) void {
|
||||
log.debug(.web_api, "not implemented", .{ .feature = "HTMLInputElement select" });
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLLIElement = struct {
|
||||
|
||||
@@ -42,7 +42,7 @@ pub const ErrorEvent = struct {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, event_type, .{});
|
||||
parser.eventSetInternalType(event, .event);
|
||||
parser.eventSetInternalType(event, .error_event);
|
||||
|
||||
const o = opts orelse ErrorEventInit{};
|
||||
|
||||
|
||||
@@ -25,13 +25,14 @@ const URL = @import("../url/url.zig").URL;
|
||||
pub const Location = struct {
|
||||
url: URL,
|
||||
|
||||
/// Initializes the `Location` to be used in `Window`.
|
||||
/// Browsers give such initial values when user not navigated yet:
|
||||
/// Chrome -> chrome://new-tab-page/
|
||||
/// Firefox -> about:newtab
|
||||
/// Safari -> favorites://
|
||||
pub const default = Location{
|
||||
.url = .initWithoutSearchParams(Uri.parse("about:blank") catch unreachable),
|
||||
};
|
||||
pub fn init(url: []const u8) !Location {
|
||||
return .{ .url = try .initForLocation(url) };
|
||||
}
|
||||
|
||||
pub fn get_href(self: *Location, page: *Page) ![]const u8 {
|
||||
return self.url.get_href(page);
|
||||
@@ -45,16 +46,16 @@ pub const Location = struct {
|
||||
return self.url.get_protocol();
|
||||
}
|
||||
|
||||
pub fn get_host(self: *Location, page: *Page) ![]const u8 {
|
||||
return self.url.get_host(page);
|
||||
pub fn get_host(self: *Location) []const u8 {
|
||||
return self.url.get_host();
|
||||
}
|
||||
|
||||
pub fn get_hostname(self: *Location) []const u8 {
|
||||
return self.url.get_hostname();
|
||||
}
|
||||
|
||||
pub fn get_port(self: *Location, page: *Page) ![]const u8 {
|
||||
return self.url.get_port(page);
|
||||
pub fn get_port(self: *Location) []const u8 {
|
||||
return self.url.get_port();
|
||||
}
|
||||
|
||||
pub fn get_pathname(self: *Location) []const u8 {
|
||||
@@ -65,8 +66,8 @@ pub const Location = struct {
|
||||
return self.url.get_search(page);
|
||||
}
|
||||
|
||||
pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
|
||||
return self.url.get_hash(page);
|
||||
pub fn get_hash(self: *Location) []const u8 {
|
||||
return self.url.get_hash();
|
||||
}
|
||||
|
||||
pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
|
||||
@@ -86,7 +87,7 @@ pub const Location = struct {
|
||||
}
|
||||
|
||||
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
|
||||
return try self.get_href(page);
|
||||
return self.get_href(page);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ pub const SVGElement = struct {
|
||||
// Currently the prototype chain is not implemented (will not be returned by toInterface())
|
||||
// For that we need parser.SvgElement and the derived types with tags in the v-table.
|
||||
pub const prototype = *Element;
|
||||
// While this is a Node, could consider not exposing the subtype untill we have
|
||||
// While this is a Node, could consider not exposing the subtype until we have
|
||||
// a Self type to cast to.
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
@@ -41,6 +41,7 @@ const Request = @import("../fetch/Request.zig");
|
||||
const fetchFn = @import("../fetch/fetch.zig").fetch;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
const ErrorEvent = @import("error_event.zig").ErrorEvent;
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-window-extensions
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
|
||||
@@ -52,7 +53,7 @@ pub const Window = struct {
|
||||
|
||||
document: *parser.DocumentHTML,
|
||||
target: []const u8 = "",
|
||||
location: Location = .default,
|
||||
location: Location,
|
||||
storage_shelf: ?*storage.Shelf = null,
|
||||
|
||||
// counter for having unique timer ids
|
||||
@@ -78,6 +79,7 @@ pub const Window = struct {
|
||||
return .{
|
||||
.document = html_doc,
|
||||
.target = target orelse "",
|
||||
.location = try .init("about:blank"),
|
||||
.navigator = navigator orelse .{},
|
||||
.performance = Performance.init(),
|
||||
};
|
||||
@@ -88,6 +90,10 @@ pub const Window = struct {
|
||||
try parser.documentHTMLSetLocation(Location, self.document, &self.location);
|
||||
}
|
||||
|
||||
pub fn changeLocation(self: *Window, new_url: []const u8, page: *Page) !void {
|
||||
return self.location.url.reinit(new_url, page);
|
||||
}
|
||||
|
||||
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
|
||||
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
|
||||
self.document = doc;
|
||||
@@ -281,6 +287,25 @@ pub const Window = struct {
|
||||
return out;
|
||||
}
|
||||
|
||||
pub fn _reportError(self: *Window, err: js.Object, page: *Page) !void {
|
||||
var error_event = try ErrorEvent.constructor("error", .{
|
||||
.@"error" = err,
|
||||
});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(Window, self),
|
||||
@as(*parser.Event, &error_event.proto),
|
||||
);
|
||||
|
||||
if (parser.eventDefaultPrevented(&error_event.proto) == false) {
|
||||
const err_string = err.toString() catch "Unknown error";
|
||||
log.info(.user_script, "error", .{
|
||||
.err = err_string,
|
||||
.stack = page.stackTrace() catch "???",
|
||||
.source = "window.reportError",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const CreateTimeoutOpts = struct {
|
||||
name: []const u8,
|
||||
args: []js.Object = &.{},
|
||||
|
||||
@@ -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, src);
|
||||
try self.script_manager.?.getModule(owned_specifier, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,7 +271,18 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
|
||||
return error.ModuleInstantiationError;
|
||||
}
|
||||
|
||||
const evaluated = try m.evaluate(v8_context);
|
||||
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;
|
||||
};
|
||||
|
||||
// https://v8.github.io/api/head/classv8_1_1Module.html#a1f1758265a4082595757c3251bb40e0f
|
||||
// Must be a promise that gets returned here.
|
||||
std.debug.assert(evaluated.isPromise());
|
||||
@@ -1171,7 +1182,7 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
|
||||
};
|
||||
|
||||
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
|
||||
self.call_arena,
|
||||
self.arena, // might need to survive until the module is loaded
|
||||
specifier,
|
||||
referrer_path,
|
||||
);
|
||||
@@ -1206,13 +1217,21 @@ 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| {
|
||||
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),
|
||||
});
|
||||
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),
|
||||
}),
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// entry.module is always set when returning from self.module()
|
||||
|
||||
@@ -559,6 +559,7 @@ pub const EventType = enum(u8) {
|
||||
message_event = 7,
|
||||
keyboard_event = 8,
|
||||
pop_state = 9,
|
||||
composition_event = 10,
|
||||
};
|
||||
|
||||
pub const MutationEvent = c.dom_mutation_event;
|
||||
|
||||
@@ -859,7 +859,7 @@ pub const Page = struct {
|
||||
self.window.setStorageShelf(
|
||||
try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
|
||||
);
|
||||
try self.window.replaceLocation(.{ .url = try self.url.toWebApi(self.arena) });
|
||||
try self.window.changeLocation(self.url.raw, self);
|
||||
}
|
||||
|
||||
pub const MouseEvent = struct {
|
||||
@@ -1013,6 +1013,31 @@ 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.
|
||||
|
||||
@@ -41,6 +41,10 @@ const FlatRenderer = struct {
|
||||
|
||||
const Element = @import("dom/element.zig").Element;
|
||||
|
||||
// Define the size of each element in the grid.
|
||||
const default_w = 5;
|
||||
const default_h = 5;
|
||||
|
||||
// we expect allocator to be an arena
|
||||
pub fn init(allocator: Allocator) FlatRenderer {
|
||||
return .{
|
||||
@@ -62,10 +66,10 @@ const FlatRenderer = struct {
|
||||
gop.value_ptr.* = x;
|
||||
}
|
||||
|
||||
const _x: f64 = @floatFromInt(x);
|
||||
const _x: f64 = @floatFromInt(x * default_w);
|
||||
const y: f64 = 0.0;
|
||||
const w: f64 = 1.0;
|
||||
const h: f64 = 1.0;
|
||||
const w: f64 = default_w;
|
||||
const h: f64 = default_h;
|
||||
|
||||
return .{
|
||||
.x = _x,
|
||||
@@ -98,18 +102,20 @@ const FlatRenderer = struct {
|
||||
}
|
||||
|
||||
pub fn width(self: *const FlatRenderer) u32 {
|
||||
return @max(@as(u32, @intCast(self.elements.items.len)), 1); // At least 1 pixel even if empty
|
||||
return @max(@as(u32, @intCast(self.elements.items.len * default_w)), default_w); // At least default width pixels even if empty
|
||||
}
|
||||
|
||||
pub fn height(_: *const FlatRenderer) u32 {
|
||||
return 1;
|
||||
return 5;
|
||||
}
|
||||
|
||||
pub fn getElementAtPosition(self: *const FlatRenderer, x: i32, y: i32) ?*parser.Element {
|
||||
if (y != 0 or x < 0) {
|
||||
pub fn getElementAtPosition(self: *const FlatRenderer, _x: i32, y: i32) ?*parser.Element {
|
||||
if (y < 0 or y > default_h or _x < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const x = @divFloor(_x, default_w);
|
||||
|
||||
const elements = self.elements.items;
|
||||
return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Writer = std.Io.Writer;
|
||||
const ada = @import("ada");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
@@ -35,182 +37,223 @@ pub const Interfaces = .{
|
||||
EntryIterable,
|
||||
};
|
||||
|
||||
// https://url.spec.whatwg.org/#url
|
||||
//
|
||||
// TODO we could avoid many of these getter string allocatoration in two differents
|
||||
// way:
|
||||
//
|
||||
// 1. We can eventually get the slice of scheme *with* the following char in
|
||||
// the underlying string. But I don't know if it's possible and how to do that.
|
||||
// I mean, if the rawuri contains `https://foo.bar`, uri.scheme is a slice
|
||||
// containing only `https`. I want `https:` so, in theory, I don't need to
|
||||
// allocatorate data, I should be able to retrieve the scheme + the following `:`
|
||||
// from rawuri.
|
||||
//
|
||||
// 2. The other way would be to copy the `std.Uri` code to have a dedicated
|
||||
// parser including the characters we want for the web API.
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
|
||||
pub const URL = struct {
|
||||
uri: std.Uri,
|
||||
internal: ada.URL,
|
||||
/// We prefer in-house search params solution here;
|
||||
/// ada's search params impl use more memory.
|
||||
/// It also offers it's own iterator implementation
|
||||
/// where we'd like to use ours.
|
||||
search_params: URLSearchParams,
|
||||
|
||||
pub const empty = URL{
|
||||
.uri = .{ .scheme = "" },
|
||||
.internal = null,
|
||||
.search_params = .{},
|
||||
};
|
||||
|
||||
const URLArg = union(enum) {
|
||||
url: *URL,
|
||||
element: *parser.ElementHTML,
|
||||
// You can use an existing URL object for either argument, and it will be
|
||||
// stringified from the object's href property.
|
||||
const ConstructorArg = union(enum) {
|
||||
string: []const u8,
|
||||
url: *const URL,
|
||||
element: *parser.Element,
|
||||
|
||||
fn toString(self: URLArg, arena: Allocator) !?[]const u8 {
|
||||
switch (self) {
|
||||
.string => |s| return s,
|
||||
.url => |url| return try url.toString(arena),
|
||||
.element => |e| return try parser.elementGetAttribute(@ptrCast(e), "href"),
|
||||
}
|
||||
fn toString(self: ConstructorArg, page: *Page) ![]const u8 {
|
||||
return switch (self) {
|
||||
.string => |s| s,
|
||||
.url => |url| url._toString(page),
|
||||
.element => |e| {
|
||||
const attrib = try parser.elementGetAttribute(@ptrCast(e), "href") orelse {
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
return attrib;
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn constructor(url: URLArg, base: ?URLArg, page: *Page) !URL {
|
||||
const arena = page.arena;
|
||||
const url_str = try url.toString(arena) orelse return error.InvalidArgument;
|
||||
pub fn constructor(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !URL {
|
||||
const url_str = try url.toString(page);
|
||||
|
||||
var raw: ?[]const u8 = null;
|
||||
if (base) |b| {
|
||||
if (try b.toString(arena)) |bb| {
|
||||
raw = try @import("../../url.zig").URL.stitch(arena, url_str, bb, .{});
|
||||
const internal = try blk: {
|
||||
if (maybe_base) |base| {
|
||||
break :blk ada.parseWithBase(url_str, try base.toString(page));
|
||||
}
|
||||
}
|
||||
|
||||
if (raw == null) {
|
||||
// if it was a URL, then it's already be owned by the arena
|
||||
raw = if (url == .url) url_str else try arena.dupe(u8, url_str);
|
||||
}
|
||||
|
||||
const uri = std.Uri.parse(raw.?) catch blk: {
|
||||
if (!std.mem.endsWith(u8, raw.?, "://")) {
|
||||
return error.TypeError;
|
||||
}
|
||||
// schema only is valid!
|
||||
break :blk std.Uri{
|
||||
.scheme = raw.?[0 .. raw.?.len - 3],
|
||||
.host = .{ .percent_encoded = "" },
|
||||
};
|
||||
break :blk ada.parse(url_str);
|
||||
};
|
||||
|
||||
return init(arena, uri);
|
||||
}
|
||||
|
||||
pub fn init(arena: Allocator, uri: std.Uri) !URL {
|
||||
return .{
|
||||
.uri = uri,
|
||||
.search_params = try URLSearchParams.init(
|
||||
arena,
|
||||
uriComponentNullStr(uri.query),
|
||||
),
|
||||
.internal = internal,
|
||||
.search_params = try prepareSearchParams(page.arena, internal),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initWithoutSearchParams(uri: std.Uri) URL {
|
||||
return .{ .uri = uri, .search_params = .{} };
|
||||
pub fn destructor(self: *const URL) void {
|
||||
// Not tracked by arena.
|
||||
return ada.free(self.internal);
|
||||
}
|
||||
|
||||
pub fn get_origin(self: *URL, page: *Page) ![]const u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(page.arena);
|
||||
try self.uri.writeToStream(&aw.writer, .{
|
||||
.scheme = true,
|
||||
.authentication = false,
|
||||
.authority = true,
|
||||
.path = false,
|
||||
.query = false,
|
||||
.fragment = false,
|
||||
});
|
||||
return aw.written();
|
||||
/// Only to be used by `Location` API. `url` MUST NOT provide search params.
|
||||
pub fn initForLocation(url: []const u8) !URL {
|
||||
return .{ .internal = try ada.parse(url), .search_params = .{} };
|
||||
}
|
||||
|
||||
// get_href returns the URL by writing all its components.
|
||||
pub fn get_href(self: *URL, page: *Page) ![]const u8 {
|
||||
return self.toString(page.arena);
|
||||
/// Reinitializes the URL by parsing given `url`. Search params can be provided.
|
||||
pub fn reinit(self: *URL, url: []const u8, page: *Page) !void {
|
||||
_ = ada.setHref(self.internal, url);
|
||||
if (!ada.isValid(self.internal)) return error.Internal;
|
||||
|
||||
self.search_params = try prepareSearchParams(page.arena, self.internal);
|
||||
}
|
||||
|
||||
pub fn _toString(self: *URL, page: *Page) ![]const u8 {
|
||||
return self.toString(page.arena);
|
||||
/// Prepares a `URLSearchParams` from given `internal`.
|
||||
/// Resets `search` of `internal`.
|
||||
fn prepareSearchParams(arena: Allocator, internal: ada.URL) !URLSearchParams {
|
||||
const maybe_search = ada.getSearchNullable(internal);
|
||||
// Empty.
|
||||
if (maybe_search.data == null) return .{};
|
||||
|
||||
const search = maybe_search.data[0..maybe_search.length];
|
||||
const search_params = URLSearchParams.initFromString(arena, search);
|
||||
// After a call to this function, search params are tracked by
|
||||
// `search_params`. So we reset the internal's search.
|
||||
ada.clearSearch(internal);
|
||||
|
||||
return search_params;
|
||||
}
|
||||
|
||||
// format the url with all its components.
|
||||
pub fn toString(self: *const URL, arena: Allocator) ![]const u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(arena);
|
||||
try self.uri.writeToStream(&aw.writer, .{
|
||||
.scheme = true,
|
||||
.authentication = true,
|
||||
.authority = true,
|
||||
.path = uriComponentNullStr(self.uri.path).len > 0,
|
||||
});
|
||||
pub fn clearPort(self: *const URL) void {
|
||||
return ada.clearPort(self.internal);
|
||||
}
|
||||
|
||||
pub fn clearHash(self: *const URL) void {
|
||||
return ada.clearHash(self.internal);
|
||||
}
|
||||
|
||||
/// Alias to get_href.
|
||||
pub fn _toString(self: *const URL, page: *Page) ![]const u8 {
|
||||
return self.get_href(page);
|
||||
}
|
||||
|
||||
// Getters.
|
||||
|
||||
pub fn get_searchParams(self: *URL) *URLSearchParams {
|
||||
return &self.search_params;
|
||||
}
|
||||
|
||||
pub fn get_origin(self: *const URL, page: *Page) ![]const u8 {
|
||||
// `ada.getOriginNullable` allocates memory in order to find the `origin`.
|
||||
// We'd like to use our arena allocator for such case;
|
||||
// so here we allocate the `origin` in page arena and free the original.
|
||||
const maybe_origin = ada.getOriginNullable(self.internal);
|
||||
if (maybe_origin.data == null) {
|
||||
return "";
|
||||
}
|
||||
defer ada.freeOwnedString(maybe_origin);
|
||||
|
||||
const origin = maybe_origin.data[0..maybe_origin.length];
|
||||
return page.call_arena.dupe(u8, origin);
|
||||
}
|
||||
|
||||
pub fn get_href(self: *const URL, page: *Page) ![]const u8 {
|
||||
var w: Writer.Allocating = .init(page.arena);
|
||||
|
||||
// If URL is not valid, return immediately.
|
||||
if (!ada.isValid(self.internal)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Since the earlier check passed, this can't be null.
|
||||
const str = ada.getHrefNullable(self.internal);
|
||||
const href = str.data[0..str.length];
|
||||
// This can't be null either.
|
||||
const comps = ada.getComponents(self.internal);
|
||||
// If hash provided, we write it after we fit-in the search params.
|
||||
const has_hash = comps.hash_start != ada.URLOmitted;
|
||||
const href_part = if (has_hash) href[0..comps.hash_start] else href;
|
||||
try w.writer.writeAll(href_part);
|
||||
|
||||
// Write search params if provided.
|
||||
if (self.search_params.get_size() > 0) {
|
||||
try aw.writer.writeByte('?');
|
||||
try self.search_params.write(&aw.writer);
|
||||
try w.writer.writeByte('?');
|
||||
try self.search_params.write(&w.writer);
|
||||
}
|
||||
|
||||
{
|
||||
const fragment = uriComponentNullStr(self.uri.fragment);
|
||||
if (fragment.len > 0) {
|
||||
try aw.writer.writeByte('#');
|
||||
try aw.writer.writeAll(fragment);
|
||||
}
|
||||
// Write hash if provided before.
|
||||
const hash = self.get_hash();
|
||||
try w.writer.writeAll(hash);
|
||||
|
||||
return w.written();
|
||||
}
|
||||
|
||||
pub fn get_username(self: *const URL) []const u8 {
|
||||
const username = ada.getUsernameNullable(self.internal);
|
||||
if (username.data == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return aw.written();
|
||||
return username.data[0..username.length];
|
||||
}
|
||||
|
||||
pub fn get_protocol(self: *const URL) []const u8 {
|
||||
// std.Uri keeps a pointer to "https", "http" (scheme part) so we know
|
||||
// its followed by ':'.
|
||||
const scheme = self.uri.scheme;
|
||||
return scheme.ptr[0 .. scheme.len + 1];
|
||||
pub fn get_password(self: *const URL) []const u8 {
|
||||
const password = ada.getPasswordNullable(self.internal);
|
||||
if (password.data == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return password.data[0..password.length];
|
||||
}
|
||||
|
||||
pub fn get_username(self: *URL) []const u8 {
|
||||
return uriComponentNullStr(self.uri.user);
|
||||
pub fn get_port(self: *const URL) []const u8 {
|
||||
const port = ada.getPortNullable(self.internal);
|
||||
if (port.data == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return port.data[0..port.length];
|
||||
}
|
||||
|
||||
pub fn get_password(self: *URL) []const u8 {
|
||||
return uriComponentNullStr(self.uri.password);
|
||||
pub fn get_hash(self: *const URL) []const u8 {
|
||||
const hash = ada.getHashNullable(self.internal);
|
||||
if (hash.data == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return hash.data[0..hash.length];
|
||||
}
|
||||
|
||||
pub fn get_host(self: *URL, page: *Page) ![]const u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(page.arena);
|
||||
try self.uri.writeToStream(&aw.writer, .{
|
||||
.scheme = false,
|
||||
.authentication = false,
|
||||
.authority = true,
|
||||
.path = false,
|
||||
.query = false,
|
||||
.fragment = false,
|
||||
});
|
||||
return aw.written();
|
||||
pub fn get_host(self: *const URL) []const u8 {
|
||||
const host = ada.getHostNullable(self.internal);
|
||||
if (host.data == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return host.data[0..host.length];
|
||||
}
|
||||
|
||||
pub fn get_hostname(self: *URL) []const u8 {
|
||||
return uriComponentNullStr(self.uri.host);
|
||||
pub fn get_hostname(self: *const URL) []const u8 {
|
||||
const hostname = ada.getHostnameNullable(self.internal);
|
||||
if (hostname.data == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return hostname.data[0..hostname.length];
|
||||
}
|
||||
|
||||
pub fn get_port(self: *URL, page: *Page) ![]const u8 {
|
||||
const arena = page.arena;
|
||||
if (self.uri.port == null) return try arena.dupe(u8, "");
|
||||
pub fn get_pathname(self: *const URL) []const u8 {
|
||||
const path = ada.getPathnameNullable(self.internal);
|
||||
// Return a slash if path is null.
|
||||
if (path.data == null) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
var aw = std.Io.Writer.Allocating.init(arena);
|
||||
try aw.writer.printInt(self.uri.port.?, 10, .lower, .{});
|
||||
return aw.written();
|
||||
return path.data[0..path.length];
|
||||
}
|
||||
|
||||
pub fn get_pathname(self: *URL) []const u8 {
|
||||
if (uriComponentStr(self.uri.path).len == 0) return "/";
|
||||
return uriComponentStr(self.uri.path);
|
||||
}
|
||||
|
||||
pub fn get_search(self: *URL, page: *Page) ![]const u8 {
|
||||
/// get_search depends on the current state of `search_params`.
|
||||
pub fn get_search(self: *const URL, page: *Page) ![]const u8 {
|
||||
const arena = page.arena;
|
||||
|
||||
if (self.search_params.get_size() == 0) {
|
||||
@@ -223,72 +266,104 @@ pub const URL = struct {
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn set_search(self: *URL, qs_: ?[]const u8, page: *Page) !void {
|
||||
pub fn get_protocol(self: *const URL) []const u8 {
|
||||
const protocol = ada.getProtocolNullable(self.internal);
|
||||
if (protocol.data == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return protocol.data[0..protocol.length];
|
||||
}
|
||||
|
||||
// Setters.
|
||||
|
||||
/// Ada-url don't define any errors, so we just prefer one unified
|
||||
/// `Internal` error for failing cases.
|
||||
const SetterError = error{Internal};
|
||||
|
||||
pub fn set_href(self: *URL, input: []const u8, page: *Page) !void {
|
||||
_ = ada.setHref(self.internal, input);
|
||||
if (!ada.isValid(self.internal)) return error.Internal;
|
||||
// Can't call `get_search` here since it uses `search_params`.
|
||||
self.search_params = try prepareSearchParams(page.arena, self.internal);
|
||||
}
|
||||
|
||||
pub fn set_host(self: *const URL, input: []const u8) SetterError!void {
|
||||
_ = ada.setHost(self.internal, input);
|
||||
if (!ada.isValid(self.internal)) return error.Internal;
|
||||
}
|
||||
|
||||
pub fn set_hostname(self: *const URL, input: []const u8) SetterError!void {
|
||||
_ = ada.setHostname(self.internal, input);
|
||||
if (!ada.isValid(self.internal)) return error.Internal;
|
||||
}
|
||||
|
||||
pub fn set_protocol(self: *const URL, input: []const u8) SetterError!void {
|
||||
_ = ada.setProtocol(self.internal, input);
|
||||
if (!ada.isValid(self.internal)) return error.Internal;
|
||||
}
|
||||
|
||||
pub fn set_username(self: *const URL, input: []const u8) SetterError!void {
|
||||
_ = ada.setUsername(self.internal, input);
|
||||
if (!ada.isValid(self.internal)) return error.Internal;
|
||||
}
|
||||
|
||||
pub fn set_password(self: *const URL, input: []const u8) SetterError!void {
|
||||
_ = ada.setPassword(self.internal, input);
|
||||
if (!ada.isValid(self.internal)) return error.Internal;
|
||||
}
|
||||
|
||||
pub fn set_port(self: *const URL, input: []const u8) SetterError!void {
|
||||
_ = ada.setPort(self.internal, input);
|
||||
if (!ada.isValid(self.internal)) return error.Internal;
|
||||
}
|
||||
|
||||
pub fn set_pathname(self: *const URL, input: []const u8) SetterError!void {
|
||||
_ = ada.setPathname(self.internal, input);
|
||||
if (!ada.isValid(self.internal)) return error.Internal;
|
||||
}
|
||||
|
||||
pub fn set_search(self: *URL, maybe_input: ?[]const u8, page: *Page) !void {
|
||||
self.search_params = .{};
|
||||
if (qs_) |qs| {
|
||||
self.search_params = try URLSearchParams.init(page.arena, qs);
|
||||
if (maybe_input) |input| {
|
||||
self.search_params = try .initFromString(page.arena, input);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_hash(self: *URL, page: *Page) ![]const u8 {
|
||||
const arena = page.arena;
|
||||
if (self.uri.fragment == null) return try arena.dupe(u8, "");
|
||||
|
||||
return try std.mem.concat(arena, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) });
|
||||
}
|
||||
|
||||
pub fn get_searchParams(self: *URL) *URLSearchParams {
|
||||
return &self.search_params;
|
||||
}
|
||||
|
||||
pub fn _toJSON(self: *URL, page: *Page) ![]const u8 {
|
||||
return self.get_href(page);
|
||||
pub fn set_hash(self: *const URL, input: []const u8) !void {
|
||||
ada.setHash(self.internal, input);
|
||||
if (!ada.isValid(self.internal)) return error.Internal;
|
||||
}
|
||||
};
|
||||
|
||||
// uriComponentNullStr converts an optional std.Uri.Component to string value.
|
||||
// The string value can be undecoded.
|
||||
fn uriComponentNullStr(c: ?std.Uri.Component) []const u8 {
|
||||
if (c == null) return "";
|
||||
|
||||
return uriComponentStr(c.?);
|
||||
}
|
||||
|
||||
fn uriComponentStr(c: std.Uri.Component) []const u8 {
|
||||
return switch (c) {
|
||||
.raw => |v| v,
|
||||
.percent_encoded => |v| v,
|
||||
};
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#interface-urlsearchparams
|
||||
pub const URLSearchParams = struct {
|
||||
entries: kv.List = .{},
|
||||
|
||||
const URLSearchParamsOpts = union(enum) {
|
||||
qs: []const u8,
|
||||
pub const ConstructorOptions = union(enum) {
|
||||
query_string: []const u8,
|
||||
form_data: *const FormData,
|
||||
js_obj: js.Object,
|
||||
object: js.Object,
|
||||
};
|
||||
pub fn constructor(opts_: ?URLSearchParamsOpts, page: *Page) !URLSearchParams {
|
||||
const opts = opts_ orelse return .{ .entries = .{} };
|
||||
return switch (opts) {
|
||||
.qs => |qs| init(page.arena, qs),
|
||||
.form_data => |fd| .{ .entries = try fd.entries.clone(page.arena) },
|
||||
.js_obj => |js_obj| {
|
||||
const arena = page.arena;
|
||||
var it = js_obj.nameIterator();
|
||||
|
||||
var entries: kv.List = .{};
|
||||
pub fn constructor(maybe_options: ?ConstructorOptions, page: *Page) !URLSearchParams {
|
||||
const options = maybe_options orelse return .{};
|
||||
|
||||
const arena = page.arena;
|
||||
return switch (options) {
|
||||
.query_string => |string| .{ .entries = try parseQuery(arena, string) },
|
||||
.form_data => |form_data| .{ .entries = try form_data.entries.clone(arena) },
|
||||
.object => |object| {
|
||||
var it = object.nameIterator();
|
||||
|
||||
var entries = kv.List{};
|
||||
try entries.ensureTotalCapacity(arena, it.count);
|
||||
|
||||
while (try it.next()) |js_name| {
|
||||
const name = try js_name.toString(arena);
|
||||
const js_val = try js_obj.get(name);
|
||||
entries.appendOwnedAssumeCapacity(
|
||||
name,
|
||||
try js_val.toString(arena),
|
||||
);
|
||||
const js_value = try object.get(name);
|
||||
const value = try js_value.toString(arena);
|
||||
|
||||
entries.appendOwnedAssumeCapacity(name, value);
|
||||
}
|
||||
|
||||
return .{ .entries = entries };
|
||||
@@ -296,10 +371,9 @@ pub const URLSearchParams = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(arena: Allocator, qs_: ?[]const u8) !URLSearchParams {
|
||||
return .{
|
||||
.entries = if (qs_) |qs| try parseQuery(arena, qs) else .{},
|
||||
};
|
||||
/// Initializes URLSearchParams from a query string.
|
||||
pub fn initFromString(arena: Allocator, query_string: []const u8) !URLSearchParams {
|
||||
return .{ .entries = try parseQuery(arena, query_string) };
|
||||
}
|
||||
|
||||
pub fn get_size(self: *const URLSearchParams) u32 {
|
||||
|
||||
@@ -702,7 +702,7 @@ const IsolatedWorld = struct {
|
||||
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
|
||||
// (assuming grantUniveralAccess will be set to True!).
|
||||
// We just created the world and the page. The page's state lives in the session, but is update on navigation.
|
||||
// This also means this pointer becomes invalid after removePage untill a new page is created.
|
||||
// This also means this pointer becomes invalid after removePage until a new page is created.
|
||||
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
|
||||
pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
|
||||
// if (self.executor.context != null) return error.Only1IsolatedContextSupported;
|
||||
|
||||
@@ -663,11 +663,11 @@ test "cdp.dom: getBoxModel" {
|
||||
.params = .{ .nodeId = 6 },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .model = BoxModel{
|
||||
.content = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
|
||||
.padding = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
|
||||
.border = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
|
||||
.margin = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
|
||||
.width = 1,
|
||||
.height = 1,
|
||||
.content = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
|
||||
.padding = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
|
||||
.border = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
|
||||
.margin = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
|
||||
.width = 5,
|
||||
.height = 5,
|
||||
} }, .{ .id = 5 });
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
dispatchKeyEvent,
|
||||
dispatchMouseEvent,
|
||||
insertText,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.dispatchKeyEvent => return dispatchKeyEvent(cmd),
|
||||
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
|
||||
.insertText => return insertText(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +117,20 @@ fn dispatchMouseEvent(cmd: anytype) !void {
|
||||
// result already sent
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText
|
||||
fn insertText(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
text: []const u8, // The text to insert
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return;
|
||||
const page = bc.session.currentPage() orelse return;
|
||||
|
||||
try page.insertText(params.text);
|
||||
|
||||
try cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn clickNavigate(cmd: anytype, uri: std.Uri) !void {
|
||||
const bc = cmd.browser_context.?;
|
||||
|
||||
|
||||
@@ -21,9 +21,48 @@ const std = @import("std");
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
setIgnoreCertificateErrors,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
.setIgnoreCertificateErrors => return setIgnoreCertificateErrors(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
fn setIgnoreCertificateErrors(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
ignore: bool,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
if (params.ignore) {
|
||||
try cmd.cdp.browser.http_client.disableTlsVerify();
|
||||
} else {
|
||||
try cmd.cdp.browser.http_client.enableTlsVerify();
|
||||
}
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
|
||||
test "cdp.Security: setIgnoreCertificateErrors" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
_ = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 8,
|
||||
.method = "Security.setIgnoreCertificateErrors",
|
||||
.params = .{ .ignore = true },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 8 });
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 9,
|
||||
.method = "Security.setIgnoreCertificateErrors",
|
||||
.params = .{ .ignore = false },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 9 });
|
||||
}
|
||||
|
||||
@@ -93,6 +93,11 @@ notification: ?*Notification = null,
|
||||
// restoring, this originally-configured value is what it goes to.
|
||||
http_proxy: ?[:0]const u8 = null,
|
||||
|
||||
// track if the client use a proxy for connections.
|
||||
// We can't use http_proxy because we want also to track proxy configured via
|
||||
// CDP.
|
||||
use_proxy: bool,
|
||||
|
||||
// The complete user-agent header line
|
||||
user_agent: [:0]const u8,
|
||||
|
||||
@@ -126,6 +131,7 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie
|
||||
.handles = handles,
|
||||
.allocator = allocator,
|
||||
.http_proxy = opts.http_proxy,
|
||||
.use_proxy = opts.http_proxy != null,
|
||||
.user_agent = opts.user_agent,
|
||||
.transfer_pool = transfer_pool,
|
||||
};
|
||||
@@ -315,6 +321,7 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void {
|
||||
for (self.handles.handles) |*h| {
|
||||
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
|
||||
}
|
||||
self.use_proxy = true;
|
||||
}
|
||||
|
||||
// Same restriction as changeProxy. Should be ok since this is only called on
|
||||
@@ -326,6 +333,41 @@ 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 {
|
||||
try self.ensureNoActiveConnection();
|
||||
|
||||
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 {
|
||||
try self.ensureNoActiveConnection();
|
||||
|
||||
for (self.handles.handles) |*h| {
|
||||
const easy = h.conn.easy;
|
||||
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0)));
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0)));
|
||||
|
||||
if (self.use_proxy) {
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 0)));
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
|
||||
@@ -808,7 +850,7 @@ pub const Transfer = struct {
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
// abortAuthChallenge is called when an auth chanllenge interception is
|
||||
// abortAuthChallenge is called when an auth challenge interception is
|
||||
// abort. We don't call self.client.endTransfer here b/c it has been done
|
||||
// before interception process.
|
||||
pub fn abortAuthChallenge(self: *Transfer) void {
|
||||
|
||||
@@ -487,7 +487,7 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
// called by CDP
|
||||
// Websocket frames have a variable lenght header. For server-client,
|
||||
// Websocket frames have a variable length header. For server-client,
|
||||
// it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have
|
||||
// writev, so we need to get creative. We'll JSON serialize to a
|
||||
// buffer, where the first 10 bytes are reserved. We can then backfill
|
||||
|
||||
@@ -168,35 +168,35 @@
|
||||
|
||||
<script id=dimensions>
|
||||
const para = document.getElementById('para');
|
||||
testing.expectEqual(1, para.clientWidth);
|
||||
testing.expectEqual(1, para.clientHeight);
|
||||
testing.expectEqual(5, para.clientWidth);
|
||||
testing.expectEqual(5, para.clientHeight);
|
||||
|
||||
// let r1 = document.getElementById('para').getBoundingClientRect();
|
||||
// testing.expectEqual(0, r1.x);
|
||||
// testing.expectEqual(0, r1.y);
|
||||
// testing.expectEqual(1, r1.width);
|
||||
// testing.expectEqual(2, r1.height);
|
||||
let r1 = document.getElementById('para').getBoundingClientRect();
|
||||
testing.expectEqual(0, r1.x);
|
||||
testing.expectEqual(0, r1.y);
|
||||
testing.expectEqual(5, r1.width);
|
||||
testing.expectEqual(5, r1.height);
|
||||
|
||||
// let r2 = document.getElementById('content').getBoundingClientRect();
|
||||
// testing.expectEqual(1, r2.x);
|
||||
// testing.expectEqual(0, r2.y);
|
||||
// testing.expectEqual(1, r2.width);
|
||||
// testing.expectEqual(1, r2.height);
|
||||
let r2 = document.getElementById('content').getBoundingClientRect();
|
||||
testing.expectEqual(5, r2.x);
|
||||
testing.expectEqual(0, r2.y);
|
||||
testing.expectEqual(5, r2.width);
|
||||
testing.expectEqual(5, r2.height);
|
||||
|
||||
// let r3 = document.getElementById('para').getBoundingClientRect();
|
||||
// testing.expectEqual(0, r3.x);
|
||||
// testing.expectEqual(0, r3.y);
|
||||
// testing.expectEqual(1, r3.width);
|
||||
// testing.expectEqual(1, r3.height);
|
||||
let r3 = document.getElementById('para').getBoundingClientRect();
|
||||
testing.expectEqual(0, r3.x);
|
||||
testing.expectEqual(0, r3.y);
|
||||
testing.expectEqual(5, r3.width);
|
||||
testing.expectEqual(5, r3.height);
|
||||
|
||||
// testing.expectEqual(1, para.clientWidth);
|
||||
// testing.expectEqual(1, para.clientHeight);
|
||||
testing.expectEqual(10, para.clientWidth);
|
||||
testing.expectEqual(5, para.clientHeight);
|
||||
|
||||
// let r4 = document.createElement('div').getBoundingClientRect();
|
||||
// testing.expectEqual(0, r4.x);
|
||||
// testing.expectEqual(0, r4.y);
|
||||
// testing.expectEqual(0, r4.width);
|
||||
// testing.expectEqual(0, r4.height);
|
||||
let r4 = document.createElement('div').getBoundingClientRect();
|
||||
testing.expectEqual(0, r4.x);
|
||||
testing.expectEqual(0, r4.y);
|
||||
testing.expectEqual(0, r4.width);
|
||||
testing.expectEqual(0, r4.height);
|
||||
</script>
|
||||
|
||||
<script id=matches>
|
||||
@@ -326,3 +326,16 @@
|
||||
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(1, entry.intersectionRect.width);
|
||||
testing.expectEqual(1, entry.intersectionRect.height);
|
||||
testing.expectEqual(5, entry.intersectionRect.width);
|
||||
testing.expectEqual(5, entry.intersectionRect.height);
|
||||
testing.expectEqual(true, entry.isIntersecting);
|
||||
testing.expectEqual(0, entry.rootBounds.x);
|
||||
testing.expectEqual(0, entry.rootBounds.y);
|
||||
testing.expectEqual(1, entry.rootBounds.width);
|
||||
testing.expectEqual(1, entry.rootBounds.height);
|
||||
testing.expectEqual(5, entry.rootBounds.width);
|
||||
testing.expectEqual(5, entry.rootBounds.height);
|
||||
testing.expectEqual('[object HTMLDivElement]', entry.target.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<p id="para"> And</p>
|
||||
<!--comment-->
|
||||
</div>
|
||||
<div id="rootNodeComposed"></div>
|
||||
</body>
|
||||
|
||||
<script src="../testing.js"></script>
|
||||
@@ -36,6 +37,26 @@ let first_child = content.firstChild.nextSibling; // nextSibling because of line
|
||||
testing.expectEqual('HTMLDocument', content.getRootNode().__proto__.constructor.name);
|
||||
</script>
|
||||
|
||||
<script id=getRootNodeComposed>
|
||||
const testContainer = $('#rootNodeComposed');
|
||||
const shadowHost = document.createElement('div');
|
||||
testContainer.appendChild(shadowHost);
|
||||
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
|
||||
const shadowChild = document.createElement('span');
|
||||
shadowRoot.appendChild(shadowChild);
|
||||
|
||||
testing.expectEqual('ShadowRoot', shadowChild.getRootNode().__proto__.constructor.name);
|
||||
testing.expectEqual('ShadowRoot', shadowChild.getRootNode({ composed: false }).__proto__.constructor.name);
|
||||
testing.expectEqual('HTMLDocument', shadowChild.getRootNode({ composed: true }).__proto__.constructor.name);
|
||||
testing.expectEqual('HTMLDocument', shadowHost.getRootNode().__proto__.constructor.name);
|
||||
|
||||
const disconnected = document.createElement('div');
|
||||
const disconnectedChild = document.createElement('span');
|
||||
disconnected.appendChild(disconnectedChild);
|
||||
testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode().__proto__.constructor.name);
|
||||
testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode({ composed: true }).__proto__.constructor.name);
|
||||
</script>
|
||||
|
||||
<script id=firstChild>
|
||||
let body_first_child = document.body.firstChild;
|
||||
testing.expectEqual('div', body_first_child.localName);
|
||||
@@ -224,3 +245,22 @@ 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,5 +1,21 @@
|
||||
<!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);
|
||||
@@ -7,3 +23,197 @@
|
||||
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>
|
||||
|
||||
36
src/tests/events/composition.html
Normal file
36
src/tests/events/composition.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=noNata>
|
||||
{
|
||||
let event = new CompositionEvent("test", {});
|
||||
testing.expectEqual(true, event instanceof CompositionEvent);
|
||||
testing.expectEqual(true, event instanceof Event);
|
||||
|
||||
testing.expectEqual("test", event.type);
|
||||
testing.expectEqual("", event.data);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=withData>
|
||||
{
|
||||
let event = new CompositionEvent("test2", {data: "over 9000!"});
|
||||
testing.expectEqual("test2", event.type);
|
||||
testing.expectEqual("over 9000!", event.data);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=dispatch>
|
||||
{
|
||||
let called = 0;
|
||||
document.addEventListener('CE', (e) => {
|
||||
testing.expectEqual('test-data', e.data);
|
||||
testing.expectEqual(true, e instanceof CompositionEvent);
|
||||
called += 1
|
||||
});
|
||||
|
||||
document.dispatchEvent(new CompositionEvent('CE', {data: 'test-data'}));
|
||||
testing.expectEqual(1, called);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -46,15 +46,15 @@
|
||||
testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);
|
||||
|
||||
// Return null since we only return elements when they have previously been localized
|
||||
testing.expectEqual(null, document.elementFromPoint(0.5, 0.5));
|
||||
testing.expectEqual([], document.elementsFromPoint(0.5, 0.5));
|
||||
testing.expectEqual(null, document.elementFromPoint(2.5, 2.5));
|
||||
testing.expectEqual([], document.elementsFromPoint(2.5, 2.5));
|
||||
|
||||
let div1 = document.createElement('div');
|
||||
document.body.appendChild(div1);
|
||||
div1.getClientRects(); // clal this to position it
|
||||
testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(0.5, 0.5).toString());
|
||||
testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(2.5, 2.5).toString());
|
||||
|
||||
let elems = document.elementsFromPoint(0.5, 0.5);
|
||||
let elems = document.elementsFromPoint(2.5, 2.5);
|
||||
testing.expectEqual(3, elems.length);
|
||||
testing.expectEqual('[object HTMLDivElement]', elems[0].toString());
|
||||
testing.expectEqual('[object HTMLBodyElement]', elems[1].toString());
|
||||
@@ -66,11 +66,11 @@
|
||||
// Note this will be placed after the div of previous test
|
||||
a.getClientRects();
|
||||
|
||||
let a_again = document.elementFromPoint(1.5, 0.5);
|
||||
let a_again = document.elementFromPoint(7.5, 0.5);
|
||||
testing.expectEqual('[object HTMLAnchorElement]', a_again.toString());
|
||||
testing.expectEqual('https://lightpanda.io', a_again.href);
|
||||
|
||||
let a_agains = document.elementsFromPoint(1.5, 0.5);
|
||||
let a_agains = document.elementsFromPoint(7.5, 0.5);
|
||||
testing.expectEqual('https://lightpanda.io', a_agains[0].href);
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
testing.expectEqual('https://lightpanda.io', link.origin);
|
||||
|
||||
link.host = 'lightpanda.io:443';
|
||||
testing.expectEqual('lightpanda.io:443', link.host);
|
||||
testing.expectEqual('443', link.port);
|
||||
testing.expectEqual('lightpanda.io', link.host);
|
||||
testing.expectEqual('', link.port);
|
||||
testing.expectEqual('lightpanda.io', link.hostname);
|
||||
|
||||
link.host = 'lightpanda.io';
|
||||
@@ -42,9 +42,9 @@
|
||||
|
||||
testing.expectEqual('', link.port);
|
||||
link.port = '443';
|
||||
testing.expectEqual('foo.bar:443', link.host);
|
||||
testing.expectEqual('foo.bar', link.host);
|
||||
testing.expectEqual('foo.bar', link.hostname);
|
||||
testing.expectEqual('https://foo.bar:443/?q=bar#frag', link.href);
|
||||
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
|
||||
link.port = null;
|
||||
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
|
||||
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<svg width="200" height="100" style="border:1px solid #ccc" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">
|
||||
<svg id=lower 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 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'));
|
||||
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);
|
||||
</script>
|
||||
|
||||
@@ -64,6 +64,23 @@
|
||||
testing.expectEqual(null, url.searchParams.get('a'));
|
||||
</script>
|
||||
|
||||
<script id=searchParamsSetHref>
|
||||
url = new URL("https://foo.bar");
|
||||
const searchParams = url.searchParams;
|
||||
|
||||
// SearchParams should be empty.
|
||||
testing.expectEqual(0, searchParams.size);
|
||||
|
||||
url.href = "https://lightpanda.io?over=9000&light=panda";
|
||||
// It won't hurt to check href and host too.
|
||||
testing.expectEqual("https://lightpanda.io/?over=9000&light=panda", url.href);
|
||||
testing.expectEqual("lightpanda.io", url.host);
|
||||
// SearchParams should be updated too when URL is set.
|
||||
testing.expectEqual(2, searchParams.size);
|
||||
testing.expectEqual("9000", searchParams.get("over"));
|
||||
testing.expectEqual("panda", searchParams.get("light"));
|
||||
</script>
|
||||
|
||||
<script id=base>
|
||||
url = new URL('over?9000', 'https://lightpanda.io');
|
||||
testing.expectEqual("https://lightpanda.io/over?9000", url.href);
|
||||
@@ -78,6 +95,7 @@
|
||||
</script>
|
||||
|
||||
<script id=invalidUrl>
|
||||
let u = new URL("://foo.bar/path?query#fragment");
|
||||
testing.expectEqual(":", u.protocol);
|
||||
testing.expectError("Error: Invalid", () => {
|
||||
_ = new URL("://foo.bar/path?query#fragment");
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
</script>
|
||||
|
||||
<script id=dimensions>
|
||||
testing.expectEqual(1, innerHeight);
|
||||
// Width is 1 even if there are no elements
|
||||
testing.expectEqual(1, innerWidth);
|
||||
testing.expectEqual(5, innerHeight);
|
||||
// Width is 5 even if there are no elements
|
||||
testing.expectEqual(5, innerWidth);
|
||||
|
||||
let div1 = document.createElement('div');
|
||||
document.body.appendChild(div1);
|
||||
@@ -37,8 +37,8 @@
|
||||
document.body.appendChild(div2);
|
||||
div2.getClientRects();
|
||||
|
||||
testing.expectEqual(1, innerHeight);
|
||||
testing.expectEqual(2, innerWidth);
|
||||
testing.expectEqual(5, innerHeight);
|
||||
testing.expectEqual(10, innerWidth);
|
||||
</script>
|
||||
|
||||
<script id=setTimeout>
|
||||
@@ -149,3 +149,19 @@
|
||||
|
||||
testing.eventually(() => testing.expectEqual(true, isWindowTarget));
|
||||
</script>
|
||||
|
||||
<script id=reportError>
|
||||
let errorEventFired = false;
|
||||
let capturedError = null;
|
||||
|
||||
window.addEventListener('error', (e) => {
|
||||
errorEventFired = true;
|
||||
capturedError = e.error;
|
||||
});
|
||||
|
||||
const testError = new Error('Test error message');
|
||||
window.reportError(testError);
|
||||
|
||||
testing.expectEqual(true, errorEventFired);
|
||||
testing.expectEqual(testError, capturedError);
|
||||
</script>
|
||||
|
||||
@@ -75,10 +75,6 @@ pub const URL = struct {
|
||||
return writer.writeAll(self.raw);
|
||||
}
|
||||
|
||||
pub fn toWebApi(self: *const URL, allocator: Allocator) !WebApiURL {
|
||||
return WebApiURL.init(allocator, self.uri);
|
||||
}
|
||||
|
||||
/// Properly stitches two URL fragments together.
|
||||
///
|
||||
/// For URLs with a path, it will replace the last entry with the src.
|
||||
|
||||
168
vendor/ada/root.zig
vendored
Normal file
168
vendor/ada/root.zig
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
//! Wrappers for ada URL parser.
|
||||
//! https://github.com/ada-url/ada
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("ada_c.h");
|
||||
});
|
||||
|
||||
pub const URLComponents = c.ada_url_components;
|
||||
pub const URLOmitted = c.ada_url_omitted;
|
||||
pub const String = c.ada_string;
|
||||
pub const OwnedString = c.ada_owned_string;
|
||||
/// Pointer types.
|
||||
pub const URL = c.ada_url;
|
||||
pub const URLSearchParams = c.ada_url_search_params;
|
||||
|
||||
pub const ParseError = error{Invalid};
|
||||
|
||||
pub fn parse(input: []const u8) ParseError!URL {
|
||||
const url = c.ada_parse(input.ptr, input.len);
|
||||
if (!c.ada_is_valid(url)) {
|
||||
return error.Invalid;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
pub fn parseWithBase(input: []const u8, base: []const u8) ParseError!URL {
|
||||
const url = c.ada_parse_with_base(input.ptr, input.len, base.ptr, base.len);
|
||||
if (!c.ada_is_valid(url)) {
|
||||
return error.Invalid;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
pub inline fn getComponents(url: URL) *const URLComponents {
|
||||
return c.ada_get_components(url);
|
||||
}
|
||||
|
||||
pub inline fn free(url: URL) void {
|
||||
return c.ada_free(url);
|
||||
}
|
||||
|
||||
pub inline fn freeOwnedString(owned: OwnedString) void {
|
||||
return c.ada_free_owned_string(owned);
|
||||
}
|
||||
|
||||
/// Returns true if given URL is valid.
|
||||
pub inline fn isValid(url: URL) bool {
|
||||
return c.ada_is_valid(url);
|
||||
}
|
||||
|
||||
/// Creates a new `URL` from given `URL`.
|
||||
pub inline fn copy(url: URL) URL {
|
||||
return c.ada_copy(url);
|
||||
}
|
||||
|
||||
/// Contrary to other getters, this heap allocates.
|
||||
pub inline fn getOriginNullable(url: URL) OwnedString {
|
||||
return c.ada_get_origin(url);
|
||||
}
|
||||
|
||||
pub inline fn getHrefNullable(url: URL) String {
|
||||
return c.ada_get_href(url);
|
||||
}
|
||||
|
||||
pub inline fn getUsernameNullable(url: URL) String {
|
||||
return c.ada_get_username(url);
|
||||
}
|
||||
|
||||
pub inline fn getPasswordNullable(url: URL) String {
|
||||
return c.ada_get_password(url);
|
||||
}
|
||||
|
||||
pub inline fn getSearchNullable(url: URL) String {
|
||||
return c.ada_get_search(url);
|
||||
}
|
||||
|
||||
pub inline fn getPortNullable(url: URL) String {
|
||||
return c.ada_get_port(url);
|
||||
}
|
||||
|
||||
pub inline fn getHashNullable(url: URL) String {
|
||||
return c.ada_get_hash(url);
|
||||
}
|
||||
|
||||
pub inline fn getHostNullable(url: URL) String {
|
||||
return c.ada_get_host(url);
|
||||
}
|
||||
|
||||
pub inline fn getHostnameNullable(url: URL) String {
|
||||
return c.ada_get_hostname(url);
|
||||
}
|
||||
|
||||
pub inline fn getPathnameNullable(url: URL) String {
|
||||
return c.ada_get_pathname(url);
|
||||
}
|
||||
|
||||
pub inline fn getProtocolNullable(url: URL) String {
|
||||
return c.ada_get_protocol(url);
|
||||
}
|
||||
|
||||
pub inline fn setHref(url: URL, input: []const u8) bool {
|
||||
return c.ada_set_href(url, input.ptr, input.len);
|
||||
}
|
||||
|
||||
pub inline fn setHost(url: URL, input: []const u8) bool {
|
||||
return c.ada_set_host(url, input.ptr, input.len);
|
||||
}
|
||||
|
||||
pub inline fn setHostname(url: URL, input: []const u8) bool {
|
||||
return c.ada_set_hostname(url, input.ptr, input.len);
|
||||
}
|
||||
|
||||
pub inline fn setProtocol(url: URL, input: []const u8) bool {
|
||||
return c.ada_set_protocol(url, input.ptr, input.len);
|
||||
}
|
||||
|
||||
pub inline fn setUsername(url: URL, input: []const u8) bool {
|
||||
return c.ada_set_username(url, input.ptr, input.len);
|
||||
}
|
||||
|
||||
pub inline fn setPassword(url: URL, input: []const u8) bool {
|
||||
return c.ada_set_password(url, input.ptr, input.len);
|
||||
}
|
||||
|
||||
pub inline fn setPort(url: URL, input: []const u8) bool {
|
||||
return c.ada_set_port(url, input.ptr, input.len);
|
||||
}
|
||||
|
||||
pub inline fn setPathname(url: URL, input: []const u8) bool {
|
||||
return c.ada_set_pathname(url, input.ptr, input.len);
|
||||
}
|
||||
|
||||
pub inline fn setSearch(url: URL, input: []const u8) void {
|
||||
return c.ada_set_search(url, input.ptr, input.len);
|
||||
}
|
||||
|
||||
pub inline fn setHash(url: URL, input: []const u8) void {
|
||||
return c.ada_set_hash(url, input.ptr, input.len);
|
||||
}
|
||||
|
||||
pub inline fn clearHash(url: URL) void {
|
||||
return c.ada_clear_hash(url);
|
||||
}
|
||||
|
||||
pub inline fn clearSearch(url: URL) void {
|
||||
return c.ada_clear_search(url);
|
||||
}
|
||||
|
||||
pub inline fn clearPort(url: URL) void {
|
||||
return c.ada_clear_port(url);
|
||||
}
|
||||
|
||||
pub const Scheme = struct {
|
||||
pub const http: u8 = 0;
|
||||
pub const not_special: u8 = 1;
|
||||
pub const https: u8 = 2;
|
||||
pub const ws: u8 = 3;
|
||||
pub const ftp: u8 = 4;
|
||||
pub const wss: u8 = 5;
|
||||
pub const file: u8 = 6;
|
||||
};
|
||||
|
||||
/// Returns one of the constants defined in `Scheme`.
|
||||
pub inline fn getSchemeType(url: URL) u8 {
|
||||
return c.ada_get_scheme_type(url);
|
||||
}
|
||||
2
vendor/netsurf/libdom
vendored
2
vendor/netsurf/libdom
vendored
Submodule vendor/netsurf/libdom updated: ef7d5d4fab...c7f2d3cd27
2
vendor/netsurf/libhubbub
vendored
2
vendor/netsurf/libhubbub
vendored
Submodule vendor/netsurf/libhubbub updated: 6f102212c8...1624ba6250
Reference in New Issue
Block a user