mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 23:23:28 +00:00
Compare commits
156 Commits
speedup_te
...
nikneym/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa00a5da52 | ||
|
|
344420f708 | ||
|
|
b87a59fa49 | ||
|
|
535a21c9f2 | ||
|
|
51a328d357 | ||
|
|
146b56c8c0 | ||
|
|
8e7d8225ba | ||
|
|
a46218cbae | ||
|
|
e9755bd38b | ||
|
|
6820a00cd0 | ||
|
|
4c4feef9fc | ||
|
|
c930d942fe | ||
|
|
c371538d27 | ||
|
|
7629bf274a | ||
|
|
9e7e9b67ff | ||
|
|
9b3be14650 | ||
|
|
6af8add7ff | ||
|
|
cf9ecbd9fd | ||
|
|
cecdd47bbc | ||
|
|
900c8d2473 | ||
|
|
c1160543ad | ||
|
|
1c7971b096 | ||
|
|
45cd494298 | ||
|
|
8f99e36cde | ||
|
|
25a1d588a9 | ||
|
|
d87d782fd5 | ||
|
|
99f8fe1592 | ||
|
|
02c092a122 | ||
|
|
70ca74747f | ||
|
|
594d754022 | ||
|
|
c381e4153d | ||
|
|
e761c7e8f4 | ||
|
|
b8d4e3ac50 | ||
|
|
4c2b95d00b | ||
|
|
cea4f052ba | ||
|
|
9b4ea7a040 | ||
|
|
26c2b258b4 | ||
|
|
27c9e18535 | ||
|
|
b53c2bfa0c | ||
|
|
80605633c4 | ||
|
|
acf06fdd8f | ||
|
|
58cc5b4684 | ||
|
|
c502bd901e | ||
|
|
55027747fd | ||
|
|
f6d77afe2e | ||
|
|
cd9466dafa | ||
|
|
4bf79e4bc9 | ||
|
|
7afecf0f85 | ||
|
|
0b38b7d473 | ||
|
|
1b462da4aa | ||
|
|
07948304b2 | ||
|
|
0634acdac4 | ||
|
|
75e0637d2d | ||
|
|
852c30b2e5 | ||
|
|
dc85c6552a | ||
|
|
76e8506022 | ||
|
|
2d6e2551f6 | ||
|
|
080b1d9a7c | ||
|
|
fe008b0966 | ||
|
|
4ad10d057b | ||
|
|
a65aa9f312 | ||
|
|
5b43c16f35 | ||
|
|
9cb37dc011 | ||
|
|
2ba6737c41 | ||
|
|
33d737f957 | ||
|
|
381a18a40e | ||
|
|
207f0655dd | ||
|
|
88d64da257 | ||
|
|
cf378dfd6d | ||
|
|
a3939d9a66 | ||
|
|
ef363209a4 | ||
|
|
fe9a10c617 | ||
|
|
2e734fae57 | ||
|
|
432e3c3a5e | ||
|
|
a4b13a80ce | ||
|
|
a6997a7e85 | ||
|
|
a60d06af6b | ||
|
|
dab8012b6a | ||
|
|
66f82fd9cc | ||
|
|
0bff8ba632 | ||
|
|
32226297ab | ||
|
|
ab18c90b36 | ||
|
|
27b6fd561a | ||
|
|
15b64d5a25 | ||
|
|
08a50a8ada | ||
|
|
9d172bb29d | ||
|
|
c891322129 | ||
|
|
77434850f7 | ||
|
|
69b65dbd41 | ||
|
|
c335a545a3 | ||
|
|
5bcccec610 | ||
|
|
20ae9c3a53 | ||
|
|
92ca7c5a4b | ||
|
|
37fa41b4a2 | ||
|
|
298f959e13 | ||
|
|
1cb431f204 | ||
|
|
74dc7b278b | ||
|
|
b47d8a794c | ||
|
|
eaf845959c | ||
|
|
651521d346 | ||
|
|
fb37b29671 | ||
|
|
2ecf9016ba | ||
|
|
444b08be32 | ||
|
|
2b84712eee | ||
|
|
20cb6cdd8b | ||
|
|
477a5e5338 | ||
|
|
2a151229cb | ||
|
|
1d50e091c7 | ||
|
|
c587e380a0 | ||
|
|
54f9bfba84 | ||
|
|
489ba131c5 | ||
|
|
5eac1a146f | ||
|
|
d7ce6bdeff | ||
|
|
e88473d090 | ||
|
|
b9024ab032 | ||
|
|
98906be0f6 | ||
|
|
220775715d | ||
|
|
ecbf52157b | ||
|
|
a579977f66 | ||
|
|
418dc6fdc2 | ||
|
|
2aa4b03673 | ||
|
|
f236a65a79 | ||
|
|
f7b08a1160 | ||
|
|
eed10dd1bb | ||
|
|
9992bd0999 | ||
|
|
6912175e7e | ||
|
|
a59c32757e | ||
|
|
2438a0e60b | ||
|
|
a850a902ce | ||
|
|
b7ba993ba6 | ||
|
|
3eb0d57d5b | ||
|
|
6bf2ff9168 | ||
|
|
92226a8d06 | ||
|
|
134424dfdc | ||
|
|
58ceb66452 | ||
|
|
902b8fc789 | ||
|
|
923491a510 | ||
|
|
255b45d07b | ||
|
|
8f68b5b289 | ||
|
|
252fd78473 | ||
|
|
b692c5db60 | ||
|
|
eff7d58f4b | ||
|
|
17e9bdf8e8 | ||
|
|
22d2694b71 | ||
|
|
e74d7fa454 | ||
|
|
464f42a121 | ||
|
|
05e7079178 | ||
|
|
f03fcc9a31 | ||
|
|
c3ad054bb3 | ||
|
|
202e137d77 | ||
|
|
6b35664e37 | ||
|
|
1a7dbd56ac | ||
|
|
1a40853aae | ||
|
|
6bad2b16e4 | ||
|
|
e7d1d55170 | ||
|
|
f04754c254 |
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -17,7 +17,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.1.30'
|
||||
default: 'v0.1.33'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
|
||||
2
.github/workflows/wpt.yml
vendored
2
.github/workflows/wpt.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- name: json output
|
||||
run: zig build -Doptimize=ReleaseFast wpt -- --json > wpt.json
|
||||
run: zig build wpt -- --json > wpt.json
|
||||
|
||||
- name: write commit
|
||||
run: |
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -31,3 +31,6 @@
|
||||
[submodule "vendor/curl"]
|
||||
path = vendor/curl
|
||||
url = https://github.com/curl/curl.git
|
||||
[submodule "vendor/brotli"]
|
||||
path = vendor/brotli
|
||||
url = https://github.com/google/brotli
|
||||
|
||||
@@ -4,7 +4,7 @@ ARG MINISIG=0.12
|
||||
ARG ZIG=0.15.1
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG V8=14.0.365.4
|
||||
ARG ZIG_V8=v0.1.30
|
||||
ARG ZIG_V8=v0.1.33
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
|
||||
58
build.zig
58
build.zig
@@ -245,6 +245,7 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
|
||||
mod.addCMacro("HAVE_ASSERT_H", "1");
|
||||
mod.addCMacro("HAVE_BASENAME", "1");
|
||||
mod.addCMacro("HAVE_BOOL_T", "1");
|
||||
mod.addCMacro("HAVE_BROTLI", "1");
|
||||
mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1");
|
||||
mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
|
||||
mod.addCMacro("HAVE_DLFCN_H", "1");
|
||||
@@ -379,9 +380,11 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
|
||||
}
|
||||
|
||||
try buildZlib(b, mod);
|
||||
try buildBrotli(b, mod);
|
||||
try buildMbedtls(b, mod);
|
||||
try buildNghttp2(b, mod);
|
||||
try buildCurl(b, mod);
|
||||
try buildAda(b, mod);
|
||||
|
||||
switch (target.result.os.tag) {
|
||||
.macos => {
|
||||
@@ -484,6 +487,30 @@ fn buildZlib(b: *Build, m: *Build.Module) !void {
|
||||
} });
|
||||
}
|
||||
|
||||
fn buildBrotli(b: *Build, m: *Build.Module) !void {
|
||||
const brotli = b.addLibrary(.{
|
||||
.name = "brotli",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
const root = "vendor/brotli/c/";
|
||||
brotli.addIncludePath(b.path(root ++ "include"));
|
||||
brotli.addCSourceFiles(.{ .flags = &.{}, .files = &.{
|
||||
root ++ "common/constants.c",
|
||||
root ++ "common/context.c",
|
||||
root ++ "common/dictionary.c",
|
||||
root ++ "common/platform.c",
|
||||
root ++ "common/shared_dictionary.c",
|
||||
root ++ "common/transform.c",
|
||||
root ++ "dec/bit_reader.c",
|
||||
root ++ "dec/decode.c",
|
||||
root ++ "dec/huffman.c",
|
||||
root ++ "dec/prefix.c",
|
||||
root ++ "dec/state.c",
|
||||
root ++ "dec/static_init.c",
|
||||
} });
|
||||
}
|
||||
|
||||
fn buildMbedtls(b: *Build, m: *Build.Module) !void {
|
||||
const mbedtls = b.addLibrary(.{
|
||||
.name = "mbedtls",
|
||||
@@ -823,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);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
.fingerprint = 0xda130f3af836cea0,
|
||||
.dependencies = .{
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/7177ee1ae267a44751a0e7e012e257177699a375.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH63TCAwC1D1hEiOtbEnLBbtz9ZPHrdiGWLcBcYQB7",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/305bb3706716d32d59b2ffa674731556caa1002b.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" }
|
||||
.@"ada-singleheader" = .{
|
||||
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
|
||||
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
|
||||
},
|
||||
// .v8 = .{ .path = "../zig-v8-fork" }
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("log.zig");
|
||||
const Http = @import("http/Http.zig");
|
||||
const Platform = @import("runtime/js.zig").Platform;
|
||||
const Platform = @import("browser/js/Platform.zig");
|
||||
|
||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||
const Notification = @import("notification.zig").Notification;
|
||||
|
||||
@@ -37,8 +37,10 @@ pub fn init(allocator: Allocator) Scheduler {
|
||||
}
|
||||
|
||||
pub fn reset(self: *Scheduler) void {
|
||||
self.high_priority.clearRetainingCapacity();
|
||||
self.low_priority.clearRetainingCapacity();
|
||||
// Our allocator is the page arena, it's been reset. We cannot use
|
||||
// clearAndRetainCapacity, since that space is no longer ours
|
||||
self.high_priority.clearAndFree();
|
||||
self.low_priority.clearAndFree();
|
||||
}
|
||||
|
||||
const AddOpts = struct {
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("js/js.zig");
|
||||
const log = @import("../log.zig");
|
||||
const parser = @import("netsurf.zig");
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const Page = @import("page.zig").Page;
|
||||
const DataURI = @import("DataURI.zig");
|
||||
const Http = @import("../http/Http.zig");
|
||||
@@ -38,9 +38,6 @@ page: *Page,
|
||||
// used to prevent recursive evalutaion
|
||||
is_evaluating: bool,
|
||||
|
||||
// used to prevent executing scripts while we're doing a blocking load
|
||||
is_blocking: bool = false,
|
||||
|
||||
// Only once this is true can deferred scripts be run
|
||||
static_scripts_done: bool,
|
||||
|
||||
@@ -48,12 +45,6 @@ static_scripts_done: bool,
|
||||
// on shutdown/abort, we need to cleanup any pending ones.
|
||||
asyncs: OrderList,
|
||||
|
||||
// When an async script is ready to be evaluated, it's moved from asyncs to
|
||||
// this list. You might think we can evaluate an async script as soon as it's
|
||||
// done, but we can only evaluate scripts when `is_blocking == false`. So this
|
||||
// becomes a list of scripts to execute on the next evaluate().
|
||||
asyncs_ready: OrderList,
|
||||
|
||||
// Normal scripts (non-deferred & non-async). These must be executed in order
|
||||
scripts: OrderList,
|
||||
|
||||
@@ -67,6 +58,22 @@ client: *Http.Client,
|
||||
allocator: Allocator,
|
||||
buffer_pool: BufferPool,
|
||||
script_pool: std.heap.MemoryPool(PendingScript),
|
||||
sync_module_pool: std.heap.MemoryPool(SyncModule),
|
||||
async_module_pool: std.heap.MemoryPool(AsyncModule),
|
||||
|
||||
// We can download multiple sync modules in parallel, but we want to process
|
||||
// then in order. We can't use an OrderList, like the other script types,
|
||||
// because the order we load them might not be the order we want to process
|
||||
// them in (I'm not sure this is true, but as far as I can tell, v8 doesn't
|
||||
// make any guarantees about the list of sub-module dependencies it gives us
|
||||
// So this is more like a cache. When a SyncModule is complete, it's put here
|
||||
// and can be requested as needed.
|
||||
sync_modules: std.StringHashMapUnmanaged(*SyncModule),
|
||||
|
||||
// Mapping between module specifier and resolution.
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap
|
||||
// importmap contains resolved urls.
|
||||
importmap: std.StringHashMapUnmanaged([:0]const u8),
|
||||
|
||||
const OrderList = std.DoublyLinkedList;
|
||||
|
||||
@@ -78,27 +85,51 @@ pub fn init(browser: *Browser, page: *Page) ScriptManager {
|
||||
.asyncs = .{},
|
||||
.scripts = .{},
|
||||
.deferreds = .{},
|
||||
.asyncs_ready = .{},
|
||||
.importmap = .empty,
|
||||
.sync_modules = .empty,
|
||||
.is_evaluating = false,
|
||||
.allocator = allocator,
|
||||
.client = browser.http_client,
|
||||
.static_scripts_done = false,
|
||||
.buffer_pool = BufferPool.init(allocator, 5),
|
||||
.script_pool = std.heap.MemoryPool(PendingScript).init(allocator),
|
||||
.sync_module_pool = std.heap.MemoryPool(SyncModule).init(allocator),
|
||||
.async_module_pool = std.heap.MemoryPool(AsyncModule).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ScriptManager) void {
|
||||
self.reset();
|
||||
var it = self.sync_modules.valueIterator();
|
||||
while (it.next()) |value_ptr| {
|
||||
value_ptr.*.buffer.deinit(self.allocator);
|
||||
self.sync_module_pool.destroy(value_ptr.*);
|
||||
}
|
||||
|
||||
self.buffer_pool.deinit();
|
||||
self.script_pool.deinit();
|
||||
self.sync_module_pool.deinit();
|
||||
self.async_module_pool.deinit();
|
||||
|
||||
self.sync_modules.deinit(self.allocator);
|
||||
// we don't deinit self.importmap b/c we use the page's arena for its
|
||||
// allocations.
|
||||
}
|
||||
|
||||
pub fn reset(self: *ScriptManager) void {
|
||||
var it = self.sync_modules.valueIterator();
|
||||
while (it.next()) |value_ptr| {
|
||||
value_ptr.*.buffer.deinit(self.allocator);
|
||||
self.sync_module_pool.destroy(value_ptr.*);
|
||||
}
|
||||
self.sync_modules.clearRetainingCapacity();
|
||||
// Our allocator is the page arena, it's been reset. We cannot use
|
||||
// clearAndRetainCapacity, since that space is no longer ours
|
||||
self.importmap = .empty;
|
||||
|
||||
self.clearList(&self.asyncs);
|
||||
self.clearList(&self.scripts);
|
||||
self.clearList(&self.deferreds);
|
||||
self.clearList(&self.asyncs_ready);
|
||||
self.static_scripts_done = false;
|
||||
}
|
||||
|
||||
@@ -111,7 +142,7 @@ fn clearList(_: *const ScriptManager, list: *OrderList) void {
|
||||
std.debug.assert(list.first == null);
|
||||
}
|
||||
|
||||
pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
||||
pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime ctx: []const u8) !void {
|
||||
if (try parser.elementGetAttribute(element, "nomodule") != null) {
|
||||
// these scripts should only be loaded if we don't support modules
|
||||
// but since we do support modules, we can just skip them.
|
||||
@@ -144,6 +175,9 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
||||
if (std.ascii.eqlIgnoreCase(script_type, "module")) {
|
||||
break :blk .module;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(script_type, "importmap")) {
|
||||
break :blk .importmap;
|
||||
}
|
||||
|
||||
// "type" could be anything, but only the above are ones we need to process.
|
||||
// Common other ones are application/json, application/ld+json, text/template
|
||||
@@ -157,9 +191,10 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
||||
if (try parser.elementGetAttribute(element, "src")) |src| {
|
||||
if (try DataURI.parse(page.arena, src)) |data_uri| {
|
||||
source = .{ .@"inline" = data_uri };
|
||||
} else {
|
||||
remote_url = try URL.stitch(page.arena, src, page.url.getHref(), .{ .null_terminated = true });
|
||||
source = .{ .remote = .{} };
|
||||
}
|
||||
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true });
|
||||
source = .{ .remote = .{} };
|
||||
} else {
|
||||
const inline_source = parser.nodeTextContent(@ptrCast(element)) orelse return;
|
||||
source = .{ .@"inline" = inline_source };
|
||||
@@ -169,7 +204,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
||||
.kind = kind,
|
||||
.element = element,
|
||||
.source = source,
|
||||
.url = remote_url orelse page.url.raw,
|
||||
.url = remote_url orelse page.url.getHref(),
|
||||
.is_defer = if (remote_url == null) false else try parser.elementGetAttribute(element, "defer") != null,
|
||||
.is_async = if (remote_url == null) false else try parser.elementGetAttribute(element, "async") != null,
|
||||
};
|
||||
@@ -198,7 +233,11 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
||||
self.scripts.append(&pending_script.node);
|
||||
return;
|
||||
} else {
|
||||
log.debug(.http, "script queue", .{ .url = remote_url.? });
|
||||
log.debug(.http, "script queue", .{
|
||||
.ctx = ctx,
|
||||
.url = remote_url.?,
|
||||
.stack = page.js.stackTrace() catch "???",
|
||||
});
|
||||
}
|
||||
|
||||
pending_script.getList().append(&pending_script.node);
|
||||
@@ -223,85 +262,125 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
||||
});
|
||||
}
|
||||
|
||||
// @TODO: Improving this would have the simplest biggest performance improvement
|
||||
// for most sites.
|
||||
//
|
||||
// For JS imports (both static and dynamic), we currently block to get the
|
||||
// result (the content of the file).
|
||||
//
|
||||
// For static imports, this is necessary, since v8 is expecting the compiled module
|
||||
// as part of the function return. (we should try to pre-load the JavaScript
|
||||
// source via module.GetModuleRequests(), but that's for a later time).
|
||||
//
|
||||
// For dynamic dynamic imports, this is not strictly necessary since the v8
|
||||
// call returns a Promise; we could make this a normal get call, associated with
|
||||
// the promise, and when done, resolve the promise.
|
||||
//
|
||||
// In both cases, for now at least, we just issue a "blocking" request. We block
|
||||
// by ticking the http client until the script is complete.
|
||||
//
|
||||
// This uses the client.blockingRequest call which has a dedicated handle for
|
||||
// these blocking requests. Because they are blocking, we're guaranteed to have
|
||||
// only 1 at a time, thus the 1 reserved handle.
|
||||
//
|
||||
// You almost don't need the http client's blocking handle. In most cases, you
|
||||
// should always have 1 free handle whenever you get here, because we always
|
||||
// release the handle before executing the doneCallback. So, if a module does:
|
||||
// import * as x from 'blah'
|
||||
// And we need to load 'blah', there should always be 1 free handle - the handle
|
||||
// of the http GET we just completed before executing the module.
|
||||
// The exception to this, and the reason we need a special blocking handle, is
|
||||
// for inline modules within the HTML page itself:
|
||||
// <script type=module>import ....</script>
|
||||
// Unlike external modules which can only ever be executed after releasing an
|
||||
// http handle, these are executed without there necessarily being a free handle.
|
||||
// Thus, Http/Client.zig maintains a dedicated handle for these calls.
|
||||
pub fn blockingGet(self: *ScriptManager, url: [:0]const u8) !BlockingResult {
|
||||
std.debug.assert(self.is_blocking == false);
|
||||
|
||||
self.is_blocking = true;
|
||||
defer {
|
||||
self.is_blocking = false;
|
||||
|
||||
// we blocked evaluation while loading this script, there could be
|
||||
// scripts ready to process.
|
||||
self.evaluate();
|
||||
// Resolve a module specifier to an valid URL.
|
||||
pub fn resolveSpecifier(self: *ScriptManager, arena: Allocator, specifier: []const u8, base: []const u8) ![:0]const u8 {
|
||||
// If the specifier is mapped in the importmap, return the pre-resolved value.
|
||||
if (self.importmap.get(specifier)) |s| {
|
||||
return s;
|
||||
}
|
||||
|
||||
var blocking = Blocking{
|
||||
.allocator = self.allocator,
|
||||
.buffer_pool = &self.buffer_pool,
|
||||
return URL.stitch(
|
||||
arena,
|
||||
specifier,
|
||||
base,
|
||||
.{ .alloc = .if_needed, .null_terminated = true },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn getModule(self: *ScriptManager, url: [:0]const u8, referrer: []const u8) !void {
|
||||
const gop = try self.sync_modules.getOrPut(self.allocator, url);
|
||||
if (gop.found_existing) {
|
||||
// already requested
|
||||
return;
|
||||
}
|
||||
errdefer _ = self.sync_modules.remove(url);
|
||||
|
||||
const sync = try self.sync_module_pool.create();
|
||||
errdefer self.sync_module_pool.destroy(sync);
|
||||
|
||||
sync.* = .{ .manager = self };
|
||||
gop.value_ptr.* = sync;
|
||||
|
||||
var headers = try self.client.newHeaders();
|
||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||
|
||||
log.debug(.http, "script queue", .{
|
||||
.url = url,
|
||||
.ctx = "module",
|
||||
.referrer = referrer,
|
||||
.stack = self.page.js.stackTrace() catch "???",
|
||||
});
|
||||
|
||||
try self.client.request(.{
|
||||
.url = url,
|
||||
.ctx = sync,
|
||||
.method = .GET,
|
||||
.headers = headers,
|
||||
.cookie_jar = self.page.cookie_jar,
|
||||
.resource_type = .script,
|
||||
.start_callback = if (log.enabled(.http, .debug)) SyncModule.startCallback else null,
|
||||
.header_callback = SyncModule.headerCallback,
|
||||
.data_callback = SyncModule.dataCallback,
|
||||
.done_callback = SyncModule.doneCallback,
|
||||
.error_callback = SyncModule.errorCallback,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn waitForModule(self: *ScriptManager, url: [:0]const u8) !GetResult {
|
||||
// Normally it's dangerous to hold on to map pointers. But here, the map
|
||||
// can't change. It's possible that by calling `tick`, other entries within
|
||||
// the map will have their value change, but the map itself is immutable
|
||||
// during this tick.
|
||||
const entry = self.sync_modules.getEntry(url) orelse {
|
||||
return error.UnknownModule;
|
||||
};
|
||||
const sync = entry.value_ptr.*;
|
||||
|
||||
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);
|
||||
return .{
|
||||
.buffer = sync.buffer,
|
||||
.buffer_pool = &self.buffer_pool,
|
||||
};
|
||||
},
|
||||
.err => |err| return err,
|
||||
}
|
||||
// rely on http's timeout settings to avoid an endless/long loop.
|
||||
_ = try client.tick(200);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getAsyncModule(self: *ScriptManager, url: [:0]const u8, cb: AsyncModule.Callback, cb_data: *anyopaque, referrer: []const u8) !void {
|
||||
const async = try self.async_module_pool.create();
|
||||
errdefer self.async_module_pool.destroy(async);
|
||||
|
||||
async.* = .{
|
||||
.cb = cb,
|
||||
.manager = self,
|
||||
.cb_data = cb_data,
|
||||
};
|
||||
|
||||
var headers = try self.client.newHeaders();
|
||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||
|
||||
var client = self.client;
|
||||
try client.blockingRequest(.{
|
||||
log.debug(.http, "script queue", .{
|
||||
.url = url,
|
||||
.ctx = "dynamic module",
|
||||
.referrer = referrer,
|
||||
.stack = self.page.js.stackTrace() catch "???",
|
||||
});
|
||||
|
||||
try self.client.request(.{
|
||||
.url = url,
|
||||
.method = .GET,
|
||||
.headers = headers,
|
||||
.cookie_jar = self.page.cookie_jar,
|
||||
.ctx = &blocking,
|
||||
.ctx = async,
|
||||
.resource_type = .script,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null,
|
||||
.header_callback = Blocking.headerCallback,
|
||||
.data_callback = Blocking.dataCallback,
|
||||
.done_callback = Blocking.doneCallback,
|
||||
.error_callback = Blocking.errorCallback,
|
||||
.start_callback = if (log.enabled(.http, .debug)) AsyncModule.startCallback else null,
|
||||
.header_callback = AsyncModule.headerCallback,
|
||||
.data_callback = AsyncModule.dataCallback,
|
||||
.done_callback = AsyncModule.doneCallback,
|
||||
.error_callback = AsyncModule.errorCallback,
|
||||
});
|
||||
|
||||
// rely on http's timeout settings to avoid an endless/long loop.
|
||||
while (true) {
|
||||
_ = try client.tick(200);
|
||||
switch (blocking.state) {
|
||||
.running => {},
|
||||
.done => |result| return result,
|
||||
.err => |err| return err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn staticScriptsDone(self: *ScriptManager) void {
|
||||
std.debug.assert(self.static_scripts_done == false);
|
||||
self.static_scripts_done = true;
|
||||
@@ -318,24 +397,10 @@ fn evaluate(self: *ScriptManager) void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.is_blocking) {
|
||||
// Cannot evaluate scripts while a blocking-load is in progress. Not
|
||||
// only could that result in incorrect evaluation order, it could
|
||||
// trigger another blocking request, while we're doing a blocking request.
|
||||
return;
|
||||
}
|
||||
|
||||
const page = self.page;
|
||||
self.is_evaluating = true;
|
||||
defer self.is_evaluating = false;
|
||||
|
||||
// every script in asyncs_ready is ready to be evaluated.
|
||||
while (self.asyncs_ready.first) |n| {
|
||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
||||
defer pending_script.deinit();
|
||||
pending_script.script.eval(page);
|
||||
}
|
||||
|
||||
while (self.scripts.first) |n| {
|
||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
||||
if (pending_script.complete == false) {
|
||||
@@ -416,6 +481,38 @@ fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
script.errorCallback(err);
|
||||
}
|
||||
|
||||
fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
||||
const content = script.source.content();
|
||||
|
||||
const Imports = struct {
|
||||
imports: std.json.ArrayHashMap([]const u8),
|
||||
};
|
||||
|
||||
const imports = try std.json.parseFromSliceLeaky(
|
||||
Imports,
|
||||
self.page.arena,
|
||||
content,
|
||||
.{ .allocate = .alloc_always },
|
||||
);
|
||||
|
||||
var iter = imports.imports.map.iterator();
|
||||
while (iter.next()) |entry| {
|
||||
// > Relative URLs are resolved to absolute URL addresses using the
|
||||
// > base URL of the document containing the import map.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_modules_using_import_maps
|
||||
const resolved_url = try URL.stitch(
|
||||
self.page.arena,
|
||||
entry.value_ptr.*,
|
||||
self.page.url.getHref(),
|
||||
.{ .alloc = .if_needed, .null_terminated = true },
|
||||
);
|
||||
|
||||
try self.importmap.put(self.page.arena, entry.key_ptr.*, resolved_url);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// A script which is pending execution.
|
||||
// It could be pending because:
|
||||
// (a) we're still downloading its content or
|
||||
@@ -493,11 +590,13 @@ pub const PendingScript = struct {
|
||||
|
||||
const manager = self.manager;
|
||||
self.complete = true;
|
||||
if (self.script.is_async) {
|
||||
manager.asyncs.remove(&self.node);
|
||||
manager.asyncs_ready.append(&self.node);
|
||||
if (!self.script.is_async) {
|
||||
manager.evaluate();
|
||||
return;
|
||||
}
|
||||
manager.evaluate();
|
||||
// async script can be evaluated immediately
|
||||
defer self.deinit();
|
||||
self.script.eval(manager.page);
|
||||
}
|
||||
|
||||
fn errorCallback(self: *PendingScript, err: anyerror) void {
|
||||
@@ -521,7 +620,7 @@ pub const PendingScript = struct {
|
||||
|
||||
const script = &self.script;
|
||||
if (script.is_async) {
|
||||
return if (self.complete) &self.manager.asyncs_ready else &self.manager.asyncs;
|
||||
return &self.manager.asyncs;
|
||||
}
|
||||
|
||||
if (script.is_defer) {
|
||||
@@ -543,11 +642,12 @@ const Script = struct {
|
||||
const Kind = enum {
|
||||
module,
|
||||
javascript,
|
||||
importmap,
|
||||
};
|
||||
|
||||
const Callback = union(enum) {
|
||||
string: []const u8,
|
||||
function: Env.Function,
|
||||
function: js.Function,
|
||||
};
|
||||
|
||||
const Source = union(enum) {
|
||||
@@ -583,8 +683,25 @@ const Script = struct {
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
|
||||
const js_context = page.main_context;
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
// Handle importmap special case here: the content is a JSON containing
|
||||
// imports.
|
||||
if (self.kind == .importmap) {
|
||||
page.script_manager.parseImportmap(self) catch |err| {
|
||||
log.err(.browser, "parse importmap script", .{
|
||||
.err = err,
|
||||
.src = url,
|
||||
.kind = self.kind,
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
self.executeCallback("onerror", page);
|
||||
return;
|
||||
};
|
||||
self.executeCallback("onload", page);
|
||||
return;
|
||||
}
|
||||
|
||||
const js_context = page.js;
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
@@ -594,8 +711,9 @@ const Script = struct {
|
||||
.javascript => _ = js_context.eval(content, url) catch break :blk false,
|
||||
.module => {
|
||||
// We don't care about waiting for the evaluation here.
|
||||
_ = js_context.module(content, url, cacheable) catch break :blk false;
|
||||
js_context.module(false, content, url, cacheable) catch break :blk false;
|
||||
},
|
||||
.importmap => unreachable, // handled before the try/catch.
|
||||
}
|
||||
break :blk true;
|
||||
};
|
||||
@@ -626,11 +744,11 @@ const Script = struct {
|
||||
|
||||
switch (callback) {
|
||||
.string => |str| {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(page.main_context);
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(page.js);
|
||||
defer try_catch.deinit();
|
||||
|
||||
_ = page.main_context.exec(str, typ) catch |err| {
|
||||
_ = page.js.exec(str, typ) catch |err| {
|
||||
const msg = try_catch.err(page.arena) catch @errorName(err) orelse "unknown";
|
||||
log.warn(.user_script, "script callback", .{
|
||||
.url = self.url,
|
||||
@@ -648,7 +766,7 @@ const Script = struct {
|
||||
};
|
||||
defer parser.eventDestroy(loadevt);
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
var result: js.Function.Result = undefined;
|
||||
const iface = Event.toInterface(loadevt);
|
||||
f.tryCall(void, .{iface}, &result) catch {
|
||||
log.warn(.user_script, "script callback", .{
|
||||
@@ -751,16 +869,15 @@ const BufferPool = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const Blocking = struct {
|
||||
allocator: Allocator,
|
||||
buffer_pool: *BufferPool,
|
||||
state: State = .{ .running = {} },
|
||||
const SyncModule = struct {
|
||||
manager: *ScriptManager,
|
||||
buffer: std.ArrayListUnmanaged(u8) = .{},
|
||||
state: State = .loading,
|
||||
|
||||
const State = union(enum) {
|
||||
running: void,
|
||||
done,
|
||||
loading,
|
||||
err: anyerror,
|
||||
done: BlockingResult,
|
||||
};
|
||||
|
||||
fn startCallback(transfer: *Http.Transfer) !void {
|
||||
@@ -776,12 +893,13 @@ const Blocking = struct {
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
|
||||
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx));
|
||||
if (header.status != 200) {
|
||||
self.finished(.{ .err = error.InvalidStatusCode });
|
||||
return error.InvalidStatusCode;
|
||||
}
|
||||
|
||||
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
|
||||
self.buffer = self.buffer_pool.get();
|
||||
self.buffer = self.manager.buffer_pool.get();
|
||||
}
|
||||
|
||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
@@ -791,8 +909,8 @@ const Blocking = struct {
|
||||
// .blocking = true,
|
||||
// });
|
||||
|
||||
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
|
||||
self.buffer.appendSlice(self.allocator, data) catch |err| {
|
||||
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx));
|
||||
self.buffer.appendSlice(self.manager.allocator, data) catch |err| {
|
||||
log.err(.http, "SM.dataCallback", .{
|
||||
.err = err,
|
||||
.len = data.len,
|
||||
@@ -804,29 +922,101 @@ const Blocking = struct {
|
||||
}
|
||||
|
||||
fn doneCallback(ctx: *anyopaque) !void {
|
||||
var self: *Blocking = @ptrCast(@alignCast(ctx));
|
||||
self.state = .{ .done = .{
|
||||
.buffer = self.buffer,
|
||||
.buffer_pool = self.buffer_pool,
|
||||
} };
|
||||
var self: *SyncModule = @ptrCast(@alignCast(ctx));
|
||||
self.finished(.done);
|
||||
}
|
||||
|
||||
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
var self: *Blocking = @ptrCast(@alignCast(ctx));
|
||||
self.state = .{ .err = err };
|
||||
self.buffer_pool.release(self.buffer);
|
||||
var self: *SyncModule = @ptrCast(@alignCast(ctx));
|
||||
self.finished(.{ .err = err });
|
||||
}
|
||||
|
||||
fn finished(self: *SyncModule, state: State) void {
|
||||
self.state = state;
|
||||
}
|
||||
};
|
||||
|
||||
pub const BlockingResult = struct {
|
||||
pub const AsyncModule = struct {
|
||||
cb: Callback,
|
||||
cb_data: *anyopaque,
|
||||
manager: *ScriptManager,
|
||||
buffer: std.ArrayListUnmanaged(u8) = .{},
|
||||
|
||||
pub const Callback = *const fn (ptr: *anyopaque, result: anyerror!GetResult) void;
|
||||
|
||||
fn startCallback(transfer: *Http.Transfer) !void {
|
||||
log.debug(.http, "script fetch start", .{ .req = transfer, .async = true });
|
||||
}
|
||||
|
||||
fn headerCallback(transfer: *Http.Transfer) !void {
|
||||
const header = &transfer.response_header.?;
|
||||
log.debug(.http, "script header", .{
|
||||
.req = transfer,
|
||||
.async = true,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
|
||||
if (header.status != 200) {
|
||||
return error.InvalidStatusCode;
|
||||
}
|
||||
|
||||
var self: *AsyncModule = @ptrCast(@alignCast(transfer.ctx));
|
||||
self.buffer = self.manager.buffer_pool.get();
|
||||
}
|
||||
|
||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
// too verbose
|
||||
// log.debug(.http, "script data chunk", .{
|
||||
// .req = transfer,
|
||||
// .blocking = true,
|
||||
// });
|
||||
|
||||
var self: *AsyncModule = @ptrCast(@alignCast(transfer.ctx));
|
||||
self.buffer.appendSlice(self.manager.allocator, data) catch |err| {
|
||||
log.err(.http, "SM.dataCallback", .{
|
||||
.err = err,
|
||||
.len = data.len,
|
||||
.ascyn = true,
|
||||
.transfer = transfer,
|
||||
});
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn doneCallback(ctx: *anyopaque) !void {
|
||||
var self: *AsyncModule = @ptrCast(@alignCast(ctx));
|
||||
defer self.manager.async_module_pool.destroy(self);
|
||||
self.cb(self.cb_data, .{
|
||||
.buffer = self.buffer,
|
||||
.buffer_pool = &self.manager.buffer_pool,
|
||||
});
|
||||
}
|
||||
|
||||
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
var self: *AsyncModule = @ptrCast(@alignCast(ctx));
|
||||
|
||||
if (err != error.Abort) {
|
||||
self.cb(self.cb_data, err);
|
||||
}
|
||||
|
||||
if (self.buffer.items.len > 0) {
|
||||
self.manager.buffer_pool.release(self.buffer);
|
||||
}
|
||||
|
||||
self.manager.async_module_pool.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const GetResult = struct {
|
||||
buffer: std.ArrayListUnmanaged(u8),
|
||||
buffer_pool: *BufferPool,
|
||||
|
||||
pub fn deinit(self: *BlockingResult) void {
|
||||
pub fn deinit(self: *GetResult) void {
|
||||
self.buffer_pool.release(self.buffer);
|
||||
}
|
||||
|
||||
pub fn src(self: *const BlockingResult) []const u8 {
|
||||
pub fn src(self: *const GetResult) []const u8 {
|
||||
return self.buffer.items;
|
||||
}
|
||||
};
|
||||
|
||||
189
src/browser/SlotChangeMonitor.zig
Normal file
189
src/browser/SlotChangeMonitor.zig
Normal file
@@ -0,0 +1,189 @@
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../log.zig");
|
||||
const parser = @import("netsurf.zig");
|
||||
const collection = @import("dom/html_collection.zig");
|
||||
|
||||
const Page = @import("page.zig").Page;
|
||||
|
||||
const SlotChangeMonitor = @This();
|
||||
|
||||
page: *Page,
|
||||
event_node: parser.EventNode,
|
||||
slots_changed: std.ArrayList(*parser.Slot),
|
||||
|
||||
// Monitors the document in order to trigger slotchange events.
|
||||
pub fn init(page: *Page) !*SlotChangeMonitor {
|
||||
// on the heap, we need a stable address for event_node
|
||||
const self = try page.arena.create(SlotChangeMonitor);
|
||||
self.* = .{
|
||||
.page = page,
|
||||
.slots_changed = .empty,
|
||||
.event_node = .{ .func = mutationCallback },
|
||||
};
|
||||
const root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document));
|
||||
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, root),
|
||||
"DOMNodeInserted",
|
||||
&self.event_node,
|
||||
false,
|
||||
);
|
||||
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, root),
|
||||
"DOMNodeRemoved",
|
||||
&self.event_node,
|
||||
false,
|
||||
);
|
||||
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, root),
|
||||
"DOMAttrModified",
|
||||
&self.event_node,
|
||||
false,
|
||||
);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// Given a element, finds its slot, if any.
|
||||
pub fn findSlot(element: *parser.Element, page: *const Page) !?*parser.Slot {
|
||||
const target_name = (try parser.elementGetAttribute(element, "slot")) orelse return null;
|
||||
return findNamedSlot(element, target_name, page);
|
||||
}
|
||||
|
||||
// Given an element and a name, find the slo, if any. This is only useful for
|
||||
// MutationEvents where findSlot is unreliable because parser.elementGetAttribute(element, "slot")
|
||||
// could return the new or old value.
|
||||
fn findNamedSlot(element: *parser.Element, target_name: []const u8, page: *const Page) !?*parser.Slot {
|
||||
// I believe elements need to be added as direct descendents of the host,
|
||||
// so we don't need to go find the host, we just grab the parent.
|
||||
const host = parser.nodeParentNode(@ptrCast(element)) orelse return null;
|
||||
const state = page.getNodeState(host) orelse return null;
|
||||
const shadow_root = state.shadow_root orelse return null;
|
||||
|
||||
// if we're here, we found a host, now find the slot
|
||||
var nodes = collection.HTMLCollectionByTagName(
|
||||
@ptrCast(@alignCast(shadow_root.proto)),
|
||||
"slot",
|
||||
.{ .include_root = false },
|
||||
);
|
||||
for (0..1000) |i| {
|
||||
const n = (try nodes.item(@intCast(i))) orelse return null;
|
||||
const slot_name = (try parser.elementGetAttribute(@ptrCast(n), "name")) orelse "";
|
||||
if (std.mem.eql(u8, target_name, slot_name)) {
|
||||
return @ptrCast(n);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Event callback from the mutation event, signaling either the addition of
|
||||
// a node, removal of a node, or a change in attribute
|
||||
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
|
||||
const mutation_event = parser.eventToMutationEvent(event);
|
||||
const self: *SlotChangeMonitor = @fieldParentPtr("event_node", en);
|
||||
self._mutationCallback(mutation_event) catch |err| {
|
||||
log.err(.web_api, "slot change callback", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
fn _mutationCallback(self: *SlotChangeMonitor, event: *parser.MutationEvent) !void {
|
||||
const event_type = parser.eventType(@ptrCast(event));
|
||||
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
|
||||
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
|
||||
return self.nodeAddedOrRemoved(@ptrCast(event_target));
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
|
||||
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
|
||||
return self.nodeAddedOrRemoved(@ptrCast(event_target));
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, event_type, "DOMAttrModified")) {
|
||||
const attribute_name = try parser.mutationEventAttributeName(event);
|
||||
if (std.mem.eql(u8, attribute_name, "slot") == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const new_value = parser.mutationEventNewValue(event);
|
||||
const prev_value = parser.mutationEventPrevValue(event);
|
||||
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
|
||||
return self.nodeAttributeChanged(@ptrCast(event_target), new_value, prev_value);
|
||||
}
|
||||
}
|
||||
|
||||
// A node was removed or added. If it's an element, and if it has a slot attribute
|
||||
// then we'll dispatch a slotchange event.
|
||||
fn nodeAddedOrRemoved(self: *SlotChangeMonitor, node: *parser.Node) !void {
|
||||
if (parser.nodeType(node) != .element) {
|
||||
return;
|
||||
}
|
||||
const el: *parser.Element = @ptrCast(node);
|
||||
if (try findSlot(el, self.page)) |slot| {
|
||||
return self.scheduleSlotChange(slot);
|
||||
}
|
||||
}
|
||||
|
||||
// An attribute was modified. If the attribute is "slot", then we'll trigger 1
|
||||
// slotchange for the old slot (if there was one) and 1 slotchange for the new
|
||||
// one (if there is one)
|
||||
fn nodeAttributeChanged(self: *SlotChangeMonitor, node: *parser.Node, new_value: ?[]const u8, prev_value: ?[]const u8) !void {
|
||||
if (parser.nodeType(node) != .element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el: *parser.Element = @ptrCast(node);
|
||||
if (try findNamedSlot(el, prev_value orelse "", self.page)) |slot| {
|
||||
try self.scheduleSlotChange(slot);
|
||||
}
|
||||
|
||||
if (try findNamedSlot(el, new_value orelse "", self.page)) |slot| {
|
||||
try self.scheduleSlotChange(slot);
|
||||
}
|
||||
}
|
||||
|
||||
// OK. Our MutationEvent is not a MutationObserver - it's an older, deprecated
|
||||
// API. It gets dispatched in the middle of the change. While I'm sure it has
|
||||
// some rules, from our point of view, it fires too early. DOMAttrModified fires
|
||||
// before the attribute is actually updated and DOMNodeRemoved before the node
|
||||
// is actually removed. This is a problem if the callback will call
|
||||
// `slot.assignedNodes`, since that won't return the new state.
|
||||
// So, we use the page schedule to schedule the dispatching of the slotchange
|
||||
// event.
|
||||
fn scheduleSlotChange(self: *SlotChangeMonitor, slot: *parser.Slot) !void {
|
||||
for (self.slots_changed.items) |changed| {
|
||||
if (slot == changed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try self.slots_changed.append(self.page.arena, slot);
|
||||
if (self.slots_changed.items.len == 1) {
|
||||
// first item added, schedule the callback
|
||||
try self.page.scheduler.add(self, scheduleCallback, 0, .{ .name = "slot change" });
|
||||
}
|
||||
}
|
||||
|
||||
// Callback from the schedule. Time to dispatch the slotchange event
|
||||
fn scheduleCallback(ctx: *anyopaque) ?u32 {
|
||||
var self: *SlotChangeMonitor = @ptrCast(@alignCast(ctx));
|
||||
self._scheduleCallback() catch |err| {
|
||||
log.err(.app, "slot change schedule", .{ .err = err });
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
fn _scheduleCallback(self: *SlotChangeMonitor) !void {
|
||||
for (self.slots_changed.items) |slot| {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, "slotchange", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(parser.Element, @ptrCast(@alignCast(slot))),
|
||||
event,
|
||||
);
|
||||
}
|
||||
self.slots_changed.clearRetainingCapacity();
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
// this quickly proved necessary, since different fields are needed on the same
|
||||
// data at different levels of the prototype chain. This isn't memory efficient.
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const js = @import("js/js.zig");
|
||||
const parser = @import("netsurf.zig");
|
||||
const DataSet = @import("html/DataSet.zig");
|
||||
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
|
||||
@@ -34,8 +34,8 @@ const StyleSheet = @import("cssom/StyleSheet.zig");
|
||||
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");
|
||||
|
||||
// for HTMLScript (but probably needs to be added to more)
|
||||
onload: ?Env.Function = null,
|
||||
onerror: ?Env.Function = null,
|
||||
onload: ?js.Function = null,
|
||||
onerror: ?js.Function = null,
|
||||
|
||||
// for HTMLElement
|
||||
style: CSSStyleDeclaration = .empty,
|
||||
@@ -53,7 +53,7 @@ style_sheet: ?*StyleSheet = null,
|
||||
|
||||
// for dom/document
|
||||
active_element: ?*parser.Element = null,
|
||||
adopted_style_sheets: ?Env.JsObject = null,
|
||||
adopted_style_sheets: ?js.Object = null,
|
||||
|
||||
// for HTMLSelectElement
|
||||
// By default, if no option is explicitly selected, the first option should
|
||||
|
||||
@@ -21,8 +21,8 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const js = @import("js/js.zig");
|
||||
const State = @import("State.zig");
|
||||
const Env = @import("env.zig").Env;
|
||||
const App = @import("../app.zig").App;
|
||||
const Session = @import("session.zig").Session;
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
@@ -34,11 +34,12 @@ const HttpClient = @import("../http/Client.zig");
|
||||
// You can create multiple browser instances.
|
||||
// A browser contains only one session.
|
||||
pub const Browser = struct {
|
||||
env: *Env,
|
||||
env: *js.Env,
|
||||
app: *App,
|
||||
session: ?Session,
|
||||
allocator: Allocator,
|
||||
http_client: *HttpClient,
|
||||
call_arena: ArenaAllocator,
|
||||
page_arena: ArenaAllocator,
|
||||
session_arena: ArenaAllocator,
|
||||
transfer_arena: ArenaAllocator,
|
||||
@@ -48,7 +49,7 @@ pub const Browser = struct {
|
||||
pub fn init(app: *App) !Browser {
|
||||
const allocator = app.allocator;
|
||||
|
||||
const env = try Env.init(allocator, &app.platform, .{});
|
||||
const env = try js.Env.init(allocator, &app.platform, .{});
|
||||
errdefer env.deinit();
|
||||
|
||||
const notification = try Notification.init(allocator, app.notification);
|
||||
@@ -63,6 +64,7 @@ pub const Browser = struct {
|
||||
.allocator = allocator,
|
||||
.notification = notification,
|
||||
.http_client = app.http.client,
|
||||
.call_arena = ArenaAllocator.init(allocator),
|
||||
.page_arena = ArenaAllocator.init(allocator),
|
||||
.session_arena = ArenaAllocator.init(allocator),
|
||||
.transfer_arena = ArenaAllocator.init(allocator),
|
||||
@@ -73,6 +75,7 @@ pub const Browser = struct {
|
||||
pub fn deinit(self: *Browser) void {
|
||||
self.closeSession();
|
||||
self.env.deinit();
|
||||
self.call_arena.deinit();
|
||||
self.page_arena.deinit();
|
||||
self.session_arena.deinit();
|
||||
self.transfer_arena.deinit();
|
||||
|
||||
@@ -20,47 +20,47 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const JsObject = @import("../env.zig").Env.JsObject;
|
||||
|
||||
pub const Console = struct {
|
||||
// TODO: configurable writer
|
||||
timers: std.StringHashMapUnmanaged(u32) = .{},
|
||||
counts: std.StringHashMapUnmanaged(u32) = .{},
|
||||
|
||||
pub fn _lp(values: []JsObject, page: *Page) !void {
|
||||
pub fn _lp(values: []js.Object, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn _log(values: []JsObject, page: *Page) !void {
|
||||
pub fn _log(values: []js.Object, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn _info(values: []JsObject, page: *Page) !void {
|
||||
pub fn _info(values: []js.Object, page: *Page) !void {
|
||||
return _log(values, page);
|
||||
}
|
||||
|
||||
pub fn _debug(values: []JsObject, page: *Page) !void {
|
||||
pub fn _debug(values: []js.Object, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn _warn(values: []JsObject, page: *Page) !void {
|
||||
pub fn _warn(values: []js.Object, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn _error(values: []JsObject, page: *Page) !void {
|
||||
pub fn _error(values: []js.Object, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -71,6 +71,16 @@ pub const Console = struct {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _trace(values: []js.Object, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.debug(.console, "debug", .{
|
||||
.stack = page.js.stackTrace() catch "???",
|
||||
.args = try serializeValues(values, page),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _clear() void {}
|
||||
|
||||
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
|
||||
@@ -132,7 +142,7 @@ pub const Console = struct {
|
||||
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
|
||||
}
|
||||
|
||||
pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
|
||||
pub fn _assert(assertion: js.Object, values: []js.Object, page: *Page) !void {
|
||||
if (assertion.isTruthy()) {
|
||||
return;
|
||||
}
|
||||
@@ -143,7 +153,7 @@ pub const Console = struct {
|
||||
log.info(.console, "assertion failed", .{ .values = serialized_values });
|
||||
}
|
||||
|
||||
fn serializeValues(values: []JsObject, page: *Page) ![]const u8 {
|
||||
fn serializeValues(values: []js.Object, page: *Page) ![]const u8 {
|
||||
if (values.len == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const js = @import("../js/js.zig");
|
||||
const uuidv4 = @import("../../id.zig").uuidv4;
|
||||
|
||||
// https://w3c.github.io/webcrypto/#crypto-interface
|
||||
pub const Crypto = struct {
|
||||
_not_empty: bool = true,
|
||||
|
||||
pub fn _getRandomValues(_: *const Crypto, js_obj: Env.JsObject) !Env.JsObject {
|
||||
pub fn _getRandomValues(_: *const Crypto, js_obj: js.Object) !js.Object {
|
||||
var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues);
|
||||
const buf = into.asBuffer();
|
||||
if (buf.len > 65_536) {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const StyleSheet = @import("StyleSheet.zig");
|
||||
const CSSRuleList = @import("CSSRuleList.zig");
|
||||
@@ -73,15 +73,13 @@ pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
|
||||
_ = self.css_rules.list.orderedRemove(index);
|
||||
}
|
||||
|
||||
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !Env.Promise {
|
||||
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise {
|
||||
_ = self;
|
||||
_ = text;
|
||||
// TODO: clear self.css_rules
|
||||
// parse text and re-populate self.css_rules
|
||||
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
try resolver.resolve({});
|
||||
return resolver.promise();
|
||||
return page.js.resolvePromise({});
|
||||
}
|
||||
|
||||
pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
|
||||
|
||||
@@ -18,19 +18,17 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
const Promise = @import("../env.zig").Promise;
|
||||
const PromiseResolver = @import("../env.zig").PromiseResolver;
|
||||
|
||||
const Animation = @This();
|
||||
|
||||
effect: ?JsObject,
|
||||
timeline: ?JsObject,
|
||||
ready_resolver: ?PromiseResolver,
|
||||
finished_resolver: ?PromiseResolver,
|
||||
effect: ?js.Object,
|
||||
timeline: ?js.Object,
|
||||
ready_resolver: ?js.PromiseResolver,
|
||||
finished_resolver: ?js.PromiseResolver,
|
||||
|
||||
pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !Animation {
|
||||
pub fn constructor(effect: ?js.Object, timeline: ?js.Object) !Animation {
|
||||
return .{
|
||||
.effect = if (effect) |eo| try eo.persist() else null,
|
||||
.timeline = if (timeline) |to| try to.persist() else null,
|
||||
@@ -49,37 +47,37 @@ pub fn get_pending(self: *const Animation) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn get_finished(self: *Animation, page: *Page) !Promise {
|
||||
pub fn get_finished(self: *Animation, page: *Page) !js.Promise {
|
||||
if (self.finished_resolver == null) {
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
const resolver = page.js.createPromiseResolver(.none);
|
||||
try resolver.resolve(self);
|
||||
self.finished_resolver = resolver;
|
||||
}
|
||||
return self.finished_resolver.?.promise();
|
||||
}
|
||||
|
||||
pub fn get_ready(self: *Animation, page: *Page) !Promise {
|
||||
pub fn get_ready(self: *Animation, page: *Page) !js.Promise {
|
||||
// never resolved, because we're always "finished"
|
||||
if (self.ready_resolver == null) {
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
const resolver = page.js.createPromiseResolver(.none);
|
||||
self.ready_resolver = resolver;
|
||||
}
|
||||
return self.ready_resolver.?.promise();
|
||||
}
|
||||
|
||||
pub fn get_effect(self: *const Animation) ?JsObject {
|
||||
pub fn get_effect(self: *const Animation) ?js.Object {
|
||||
return self.effect;
|
||||
}
|
||||
|
||||
pub fn set_effect(self: *Animation, effect: JsObject) !void {
|
||||
pub fn set_effect(self: *Animation, effect: js.Object) !void {
|
||||
self.effect = try effect.persist();
|
||||
}
|
||||
|
||||
pub fn get_timeline(self: *const Animation) ?JsObject {
|
||||
pub fn get_timeline(self: *const Animation) ?js.Object {
|
||||
return self.timeline;
|
||||
}
|
||||
|
||||
pub fn set_timeline(self: *Animation, timeline: JsObject) !void {
|
||||
pub fn set_timeline(self: *Animation, timeline: js.Object) !void {
|
||||
self.timeline = try timeline.persist();
|
||||
}
|
||||
|
||||
|
||||
329
src/browser/dom/IntersectionObserver.zig
Normal file
329
src/browser/dom/IntersectionObserver.zig
Normal file
@@ -0,0 +1,329 @@
|
||||
// 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 js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Node = @import("node.zig").Node;
|
||||
const Element = @import("element.zig").Element;
|
||||
|
||||
pub const Interfaces = .{
|
||||
IntersectionObserver,
|
||||
Entry,
|
||||
};
|
||||
|
||||
// This implementation attempts to be as less wrong as possible. Since we don't
|
||||
// render, or know how things are positioned, our best guess isn't very good.
|
||||
const IntersectionObserver = @This();
|
||||
page: *Page,
|
||||
root: *parser.Node,
|
||||
callback: js.Function,
|
||||
event_node: parser.EventNode,
|
||||
observed_entries: std.ArrayList(Entry),
|
||||
pending_elements: std.ArrayList(*parser.Element),
|
||||
ready_elements: std.ArrayList(*parser.Element),
|
||||
|
||||
pub fn constructor(callback: js.Function, opts_: ?IntersectionObserverOptions, page: *Page) !*IntersectionObserver {
|
||||
const opts = opts_ orelse IntersectionObserverOptions{};
|
||||
|
||||
const self = try page.arena.create(IntersectionObserver);
|
||||
self.* = .{
|
||||
.page = page,
|
||||
.callback = callback,
|
||||
.ready_elements = .{},
|
||||
.observed_entries = .{},
|
||||
.pending_elements = .{},
|
||||
.event_node = .{ .func = mutationCallback },
|
||||
.root = opts.root orelse parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
|
||||
};
|
||||
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, self.root),
|
||||
"DOMNodeInserted",
|
||||
&self.event_node,
|
||||
false,
|
||||
);
|
||||
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, self.root),
|
||||
"DOMNodeRemoved",
|
||||
&self.event_node,
|
||||
false,
|
||||
);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn _disconnect(self: *IntersectionObserver) !void {
|
||||
// We don't free as it is on an arena
|
||||
self.ready_elements = .{};
|
||||
self.observed_entries = .{};
|
||||
self.pending_elements = .{};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element, page: *Page) !void {
|
||||
for (self.observed_entries.items) |*observer| {
|
||||
if (observer.target == target_element) {
|
||||
return; // Already observed
|
||||
}
|
||||
}
|
||||
|
||||
if (self.isPending(target_element)) {
|
||||
return; // Already pending
|
||||
}
|
||||
|
||||
for (self.ready_elements.items) |element| {
|
||||
if (element == target_element) {
|
||||
return; // Already primed
|
||||
}
|
||||
}
|
||||
|
||||
// We can never fire callbacks synchronously. Code like React expects any
|
||||
// callback to fire in the future (e.g. via microtasks).
|
||||
try self.ready_elements.append(self.page.arena, target_element);
|
||||
if (self.ready_elements.items.len == 1) {
|
||||
// this is our first ready entry, schedule a callback
|
||||
try page.scheduler.add(self, processReady, 0, .{
|
||||
.name = "intersection ready",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
||||
if (self.removeObserved(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (self.ready_elements.items, 0..) |el, index| {
|
||||
if (el == target) {
|
||||
_ = self.ready_elements.swapRemove(index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (self.pending_elements.items, 0..) |el, index| {
|
||||
if (el == target) {
|
||||
_ = self.pending_elements.swapRemove(index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _takeRecords(self: *IntersectionObserver) []Entry {
|
||||
return self.observed_entries.items;
|
||||
}
|
||||
|
||||
fn processReady(ctx: *anyopaque) ?u32 {
|
||||
const self: *IntersectionObserver = @ptrCast(@alignCast(ctx));
|
||||
self._processReady() catch |err| {
|
||||
log.err(.web_api, "intersection ready", .{ .err = err });
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
fn _processReady(self: *IntersectionObserver) !void {
|
||||
defer self.ready_elements.clearRetainingCapacity();
|
||||
for (self.ready_elements.items) |element| {
|
||||
// IntersectionObserver probably doesn't work like what your intuition
|
||||
// thinks. As long as a node has a parent, even if that parent isn't
|
||||
// connected and even if the two nodes don't intersect, it'll fire the
|
||||
// callback once.
|
||||
if (try Node.get_parentNode(@ptrCast(element)) == null) {
|
||||
if (!self.isPending(element)) {
|
||||
try self.pending_elements.append(self.page.arena, element);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
try self.forceObserve(element);
|
||||
}
|
||||
}
|
||||
|
||||
fn isPending(self: *IntersectionObserver, element: *parser.Element) bool {
|
||||
for (self.pending_elements.items) |el| {
|
||||
if (el == element) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
|
||||
const mutation_event = parser.eventToMutationEvent(event);
|
||||
const self: *IntersectionObserver = @fieldParentPtr("event_node", en);
|
||||
self._mutationCallback(mutation_event) catch |err| {
|
||||
log.err(.web_api, "mutation callback", .{ .err = err, .source = "intersection observer" });
|
||||
};
|
||||
}
|
||||
|
||||
fn _mutationCallback(self: *IntersectionObserver, event: *parser.MutationEvent) !void {
|
||||
const event_type = parser.eventType(@ptrCast(event));
|
||||
|
||||
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
|
||||
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
|
||||
if (parser.nodeType(node) != .element) {
|
||||
return;
|
||||
}
|
||||
const el: *parser.Element = @ptrCast(node);
|
||||
if (self.removePending(el)) {
|
||||
// It was pending (because it wasn't in the root), but now it is
|
||||
// we should observe it.
|
||||
try self.forceObserve(el);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
|
||||
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
|
||||
if (parser.nodeType(node) != .element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el: *parser.Element = @ptrCast(node);
|
||||
if (self.removeObserved(el)) {
|
||||
// It _was_ observed, it no longer is in our root, but if it was
|
||||
// to get re-added, it should be observed again (I think), so
|
||||
// we add it to our pending list
|
||||
try self.pending_elements.append(self.page.arena, el);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// impossible event type
|
||||
unreachable;
|
||||
}
|
||||
|
||||
// Exists to skip the checks made _observe when called from a DOMNodeInserted
|
||||
// event. In such events, the event handler has alread done the necessary
|
||||
// checks.
|
||||
fn forceObserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
||||
try self.observed_entries.append(self.page.arena, .{
|
||||
.page = self.page,
|
||||
.root = self.root,
|
||||
.target = target,
|
||||
});
|
||||
|
||||
var result: js.Function.Result = undefined;
|
||||
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.source = "intersection observer",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
fn removeObserved(self: *IntersectionObserver, target: *parser.Element) bool {
|
||||
for (self.observed_entries.items, 0..) |*observer, index| {
|
||||
if (observer.target == target) {
|
||||
_ = self.observed_entries.swapRemove(index);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn removePending(self: *IntersectionObserver, target: *parser.Element) bool {
|
||||
for (self.pending_elements.items, 0..) |el, index| {
|
||||
if (el == target) {
|
||||
_ = self.pending_elements.swapRemove(index);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const IntersectionObserverOptions = struct {
|
||||
root: ?*parser.Node = null, // Element or Document
|
||||
rootMargin: ?[]const u8 = "0px 0px 0px 0px",
|
||||
threshold: ?Threshold = .{ .single = 0.0 },
|
||||
|
||||
const Threshold = union(enum) {
|
||||
single: f32,
|
||||
list: []const f32,
|
||||
};
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Entry
|
||||
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
||||
pub const Entry = struct {
|
||||
page: *Page,
|
||||
root: *parser.Node,
|
||||
target: *parser.Element,
|
||||
|
||||
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
|
||||
pub fn get_boundingClientRect(self: *const Entry) !Element.DOMRect {
|
||||
return Element._getBoundingClientRect(self.target, self.page);
|
||||
}
|
||||
|
||||
// Returns the ratio of the intersectionRect to the boundingClientRect.
|
||||
pub fn get_intersectionRatio(_: *const Entry) f32 {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Returns a DOMRectReadOnly representing the target's visible area.
|
||||
pub fn get_intersectionRect(self: *const Entry) !Element.DOMRect {
|
||||
return Element._getBoundingClientRect(self.target, self.page);
|
||||
}
|
||||
|
||||
// A Boolean value which is true if the target element intersects with the
|
||||
// intersection observer's root. If this is true, then, the
|
||||
// Entry describes a transition into a state of
|
||||
// intersection; if it's false, then you know the transition is from
|
||||
// intersecting to not-intersecting.
|
||||
pub fn get_isIntersecting(_: *const Entry) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns a DOMRectReadOnly for the intersection observer's root.
|
||||
pub fn get_rootBounds(self: *const Entry) !Element.DOMRect {
|
||||
const root = self.root;
|
||||
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
|
||||
return self.page.renderer.boundingRect();
|
||||
}
|
||||
|
||||
const root_type = parser.nodeType(root);
|
||||
|
||||
var element: *parser.Element = undefined;
|
||||
switch (root_type) {
|
||||
.element => element = parser.nodeToElement(root),
|
||||
.document => {
|
||||
const doc = parser.nodeToDocument(root);
|
||||
element = (try parser.documentGetDocumentElement(doc)).?;
|
||||
},
|
||||
else => return error.InvalidState,
|
||||
}
|
||||
|
||||
return Element._getBoundingClientRect(element, self.page);
|
||||
}
|
||||
|
||||
// The Element whose intersection with the root changed.
|
||||
pub fn get_target(self: *const Entry) *parser.Element {
|
||||
return self.target;
|
||||
}
|
||||
|
||||
// TODO: pub fn get_time(self: *const Entry)
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.IntersectionObserver" {
|
||||
try testing.htmlRunner("dom/intersection_observer.html");
|
||||
}
|
||||
@@ -20,13 +20,11 @@ const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const JsObject = Env.JsObject;
|
||||
const Function = Env.Function;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const MAX_QUEUE_SIZE = 10;
|
||||
@@ -72,22 +70,22 @@ pub const MessagePort = struct {
|
||||
pair: *MessagePort,
|
||||
closed: bool = false,
|
||||
started: bool = false,
|
||||
onmessage_cbk: ?Function = null,
|
||||
onmessageerror_cbk: ?Function = null,
|
||||
onmessage_cbk: ?js.Function = null,
|
||||
onmessageerror_cbk: ?js.Function = null,
|
||||
// This is the queue of messages to dispatch to THIS MessagePort when the
|
||||
// MessagePort is started.
|
||||
queue: std.ArrayListUnmanaged(JsObject) = .empty,
|
||||
queue: std.ArrayListUnmanaged(js.Object) = .empty,
|
||||
|
||||
pub const PostMessageOption = union(enum) {
|
||||
transfer: JsObject,
|
||||
transfer: js.Object,
|
||||
options: Opts,
|
||||
|
||||
pub const Opts = struct {
|
||||
transfer: JsObject,
|
||||
transfer: js.Object,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn _postMessage(self: *MessagePort, obj: JsObject, opts_: ?PostMessageOption, page: *Page) !void {
|
||||
pub fn _postMessage(self: *MessagePort, obj: js.Object, opts_: ?PostMessageOption, page: *Page) !void {
|
||||
if (self.closed) {
|
||||
return;
|
||||
}
|
||||
@@ -124,10 +122,10 @@ pub const MessagePort = struct {
|
||||
self.pair.closed = true;
|
||||
}
|
||||
|
||||
pub fn get_onmessage(self: *MessagePort) ?Function {
|
||||
pub fn get_onmessage(self: *MessagePort) ?js.Function {
|
||||
return self.onmessage_cbk;
|
||||
}
|
||||
pub fn get_onmessageerror(self: *MessagePort) ?Function {
|
||||
pub fn get_onmessageerror(self: *MessagePort) ?js.Function {
|
||||
return self.onmessageerror_cbk;
|
||||
}
|
||||
|
||||
@@ -152,7 +150,7 @@ pub const MessagePort = struct {
|
||||
|
||||
// called from our pair. If port1.postMessage("x") is called, then this
|
||||
// will be called on port2.
|
||||
fn dispatchOrQueue(self: *MessagePort, obj: JsObject, arena: Allocator) !void {
|
||||
fn dispatchOrQueue(self: *MessagePort, obj: js.Object, arena: Allocator) !void {
|
||||
// our pair should have checked this already
|
||||
std.debug.assert(self.closed == false);
|
||||
|
||||
@@ -167,7 +165,7 @@ pub const MessagePort = struct {
|
||||
return self.queue.append(arena, try obj.persist());
|
||||
}
|
||||
|
||||
fn dispatch(self: *MessagePort, obj: JsObject) !void {
|
||||
fn dispatch(self: *MessagePort, obj: js.Object) !void {
|
||||
// obj is already persisted, don't use `MessageEvent.constructor`, but
|
||||
// go directly to `init`, which assumes persisted objects.
|
||||
var evt = try MessageEvent.init(.{ .data = obj });
|
||||
@@ -182,7 +180,7 @@ pub const MessagePort = struct {
|
||||
alloc: Allocator,
|
||||
typ: []const u8,
|
||||
listener: EventHandler.Listener,
|
||||
) !?Function {
|
||||
) !?js.Function {
|
||||
const target = @as(*parser.EventTarget, @ptrCast(self));
|
||||
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
|
||||
return eh.callback;
|
||||
@@ -207,12 +205,12 @@ pub const MessageEvent = struct {
|
||||
pub const union_make_copy = true;
|
||||
|
||||
proto: parser.Event,
|
||||
data: ?JsObject,
|
||||
data: ?js.Object,
|
||||
|
||||
// You would think if port1 sends to port2, the source would be port2
|
||||
// (which is how I read the documentation), but it appears to always be
|
||||
// null. It can always be set explicitly via the constructor;
|
||||
source: ?JsObject,
|
||||
source: ?js.Object,
|
||||
|
||||
origin: []const u8,
|
||||
|
||||
@@ -226,8 +224,8 @@ pub const MessageEvent = struct {
|
||||
ports: []*MessagePort,
|
||||
|
||||
const Options = struct {
|
||||
data: ?JsObject = null,
|
||||
source: ?JsObject = null,
|
||||
data: ?js.Object = null,
|
||||
source: ?js.Object = null,
|
||||
origin: []const u8 = "",
|
||||
lastEventId: []const u8 = "",
|
||||
ports: []*MessagePort = &.{},
|
||||
@@ -243,7 +241,7 @@ pub const MessageEvent = struct {
|
||||
});
|
||||
}
|
||||
|
||||
// This is like "constructor", but it assumes JsObjects have already been
|
||||
// This is like "constructor", but it assumes js.Objects have already been
|
||||
// persisted. Necessary because this `new MessageEvent()` can be called
|
||||
// directly from JS OR from a port.postMessage. In the latter case, data
|
||||
// may have already been persisted (as it might need to be queued);
|
||||
@@ -263,7 +261,7 @@ pub const MessageEvent = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_data(self: *const MessageEvent) !?JsObject {
|
||||
pub fn get_data(self: *const MessageEvent) !?js.Object {
|
||||
return self.data;
|
||||
}
|
||||
|
||||
@@ -271,7 +269,7 @@ pub const MessageEvent = struct {
|
||||
return self.origin;
|
||||
}
|
||||
|
||||
pub fn get_source(self: *const MessageEvent) ?JsObject {
|
||||
pub fn get_source(self: *const MessageEvent) ?js.Object {
|
||||
return self.source;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
@@ -37,8 +38,6 @@ const Range = @import("range.zig").Range;
|
||||
|
||||
const CustomEvent = @import("../events/custom_event.zig").CustomEvent;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
const DOMImplementation = @import("implementation.zig").DOMImplementation;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#document
|
||||
@@ -155,13 +154,13 @@ pub const Document = struct {
|
||||
// the spec changed to return an HTMLCollection instead.
|
||||
// That's why we reimplemented getElementsByTagName by using an
|
||||
// HTMLCollection in zig here.
|
||||
pub fn _getElementsByTagName(self: *parser.Document, tag_name: Env.String) !collection.HTMLCollection {
|
||||
pub fn _getElementsByTagName(self: *parser.Document, tag_name: js.String) !collection.HTMLCollection {
|
||||
return collection.HTMLCollectionByTagName(parser.documentToNode(self), tag_name.string, .{
|
||||
.include_root = true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _getElementsByClassName(self: *parser.Document, class_names: Env.String) !collection.HTMLCollection {
|
||||
pub fn _getElementsByClassName(self: *parser.Document, class_names: js.String) !collection.HTMLCollection {
|
||||
return collection.HTMLCollectionByClassName(parser.documentToNode(self), class_names.string, .{
|
||||
.include_root = true,
|
||||
});
|
||||
@@ -299,18 +298,18 @@ pub const Document = struct {
|
||||
return &.{};
|
||||
}
|
||||
|
||||
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !Env.JsObject {
|
||||
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !js.Object {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
if (state.adopted_style_sheets) |obj| {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const obj = try page.main_context.newArray(0).persist();
|
||||
const obj = try page.js.createArray(0).persist();
|
||||
state.adopted_style_sheets = obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: Env.JsObject, page: *Page) !void {
|
||||
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: js.Object, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.adopted_style_sheets = try sheets.persist();
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ const NodeList = @import("nodelist.zig");
|
||||
const Node = @import("node.zig");
|
||||
const ResizeObserver = @import("resize_observer.zig");
|
||||
const MutationObserver = @import("mutation_observer.zig");
|
||||
const IntersectionObserver = @import("intersection_observer.zig");
|
||||
const DOMParser = @import("dom_parser.zig").DOMParser;
|
||||
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
||||
@@ -44,7 +43,6 @@ pub const Interfaces = .{
|
||||
Node.Interfaces,
|
||||
ResizeObserver.Interfaces,
|
||||
MutationObserver.Interfaces,
|
||||
IntersectionObserver.Interfaces,
|
||||
DOMParser,
|
||||
TreeWalker,
|
||||
NodeIterator,
|
||||
@@ -54,4 +52,5 @@ pub const Interfaces = .{
|
||||
@import("range.zig").Interfaces,
|
||||
@import("Animation.zig"),
|
||||
@import("MessageChannel.zig").Interfaces,
|
||||
@import("IntersectionObserver.zig").Interfaces,
|
||||
};
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const css = @import("css.zig");
|
||||
@@ -34,7 +34,6 @@ const HTMLElem = @import("../html/elements.zig");
|
||||
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
|
||||
|
||||
const Animation = @import("Animation.zig");
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
|
||||
pub const Union = @import("../html/elements.zig").Union;
|
||||
|
||||
@@ -136,6 +135,10 @@ pub const Element = struct {
|
||||
return try parser.elementSetAttribute(self, "slot", slot);
|
||||
}
|
||||
|
||||
pub fn get_assignedSlot(self: *parser.Element, page: *const Page) !?*parser.Slot {
|
||||
return @import("../SlotChangeMonitor.zig").findSlot(self, page);
|
||||
}
|
||||
|
||||
pub fn get_classList(self: *parser.Element) !*parser.TokenList {
|
||||
return try parser.tokenListCreate(self, "class");
|
||||
}
|
||||
@@ -188,11 +191,9 @@ pub const Element = struct {
|
||||
// a new fragment
|
||||
const clean = try parser.documentCreateDocumentFragment(doc);
|
||||
const children = try parser.nodeGetChildNodes(body);
|
||||
const ln = parser.nodeListLength(children);
|
||||
for (0..ln) |_| {
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
const child = parser.nodeListItem(children, 0) orelse continue;
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
while (parser.nodeListItem(children, 0)) |child| {
|
||||
_ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child);
|
||||
}
|
||||
|
||||
@@ -206,27 +207,102 @@ pub const Element = struct {
|
||||
{
|
||||
// First, copy some of the head element
|
||||
const children = try parser.nodeGetChildNodes(head);
|
||||
const ln = parser.nodeListLength(children);
|
||||
for (0..ln) |_| {
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
const child = parser.nodeListItem(children, 0) orelse continue;
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
while (parser.nodeListItem(children, 0)) |child| {
|
||||
_ = try parser.nodeAppendChild(node, child);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const children = try parser.nodeGetChildNodes(body);
|
||||
const ln = parser.nodeListLength(children);
|
||||
for (0..ln) |_| {
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
const child = parser.nodeListItem(children, 0) orelse continue;
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
while (parser.nodeListItem(children, 0)) |child| {
|
||||
_ = try parser.nodeAppendChild(node, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the given `input` string and inserts its children to an element at given `position`.
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
|
||||
///
|
||||
/// TODO: Support for XML parsing and `TrustedHTML` instances.
|
||||
pub fn _insertAdjacentHTML(self: *parser.Element, position: []const u8, input: []const u8) !void {
|
||||
const self_node = parser.elementToNode(self);
|
||||
const doc = parser.nodeOwnerDocument(self_node) orelse {
|
||||
return parser.DOMError.WrongDocument;
|
||||
};
|
||||
|
||||
// Parse the fragment.
|
||||
// Should return error.Syntax on fail?
|
||||
const fragment = try parser.documentParseFragmentFromStr(doc, input);
|
||||
const fragment_node = parser.documentFragmentToNode(fragment);
|
||||
|
||||
// We always get it wrapped like so:
|
||||
// <html><head></head><body>{ ... }</body></html>
|
||||
// None of the following can be null.
|
||||
const maybe_html = parser.nodeFirstChild(fragment_node);
|
||||
std.debug.assert(maybe_html != null);
|
||||
const html = maybe_html orelse return;
|
||||
|
||||
const maybe_body = parser.nodeLastChild(html);
|
||||
std.debug.assert(maybe_body != null);
|
||||
const body = maybe_body orelse return;
|
||||
|
||||
const children = try parser.nodeGetChildNodes(body);
|
||||
|
||||
// * `target_node` is `*Node` (where we actually insert),
|
||||
// * `prev_node` is `?*Node`.
|
||||
const target_node, const prev_node = blk: {
|
||||
// Prefer case-sensitive match.
|
||||
// "beforeend" was the most common case in my tests; we might adjust the order
|
||||
// depending on which ones websites prefer most.
|
||||
if (std.mem.eql(u8, position, "beforeend")) {
|
||||
break :blk .{ self_node, null };
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, position, "afterbegin")) {
|
||||
// Get the first child; null indicates there are no children.
|
||||
const first_child = parser.nodeFirstChild(self_node);
|
||||
break :blk .{ self_node, first_child };
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, position, "beforebegin")) {
|
||||
// The node must have a parent node in order to use this variant.
|
||||
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
|
||||
// Parent cannot be Document.
|
||||
// Should have checks for document_fragment and document_type?
|
||||
if (parser.nodeType(parent) == .document) {
|
||||
return error.NoModificationAllowed;
|
||||
}
|
||||
|
||||
break :blk .{ parent, self_node };
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, position, "afterend")) {
|
||||
// The node must have a parent node in order to use this variant.
|
||||
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
|
||||
// Parent cannot be Document.
|
||||
if (parser.nodeType(parent) == .document) {
|
||||
return error.NoModificationAllowed;
|
||||
}
|
||||
// Get the next sibling or null; null indicates our node is the only one.
|
||||
const sibling = parser.nodeNextSibling(self_node);
|
||||
break :blk .{ parent, sibling };
|
||||
}
|
||||
|
||||
// Thrown if:
|
||||
// * position is not one of the four listed values.
|
||||
// * The input is XML that is not well-formed.
|
||||
return error.Syntax;
|
||||
};
|
||||
|
||||
while (parser.nodeListItem(children, 0)) |child| {
|
||||
_ = try parser.nodeInsertBefore(target_node, child, prev_node);
|
||||
}
|
||||
}
|
||||
|
||||
// The closest() method of the Element interface traverses the element and its parents (heading toward the document root) until it finds a node that matches the specified CSS selector.
|
||||
// Returns the closest ancestor Element or itself, which matches the selectors. If there are no such element, null.
|
||||
pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element {
|
||||
@@ -359,7 +435,7 @@ pub const Element = struct {
|
||||
return try parser.elementRemoveAttributeNode(self, attr);
|
||||
}
|
||||
|
||||
pub fn _getElementsByTagName(self: *parser.Element, tag_name: Env.String) !collection.HTMLCollection {
|
||||
pub fn _getElementsByTagName(self: *parser.Element, tag_name: js.String) !collection.HTMLCollection {
|
||||
return collection.HTMLCollectionByTagName(
|
||||
parser.elementToNode(self),
|
||||
tag_name.string,
|
||||
@@ -367,7 +443,7 @@ pub const Element = struct {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _getElementsByClassName(self: *parser.Element, class_names: Env.String) !collection.HTMLCollection {
|
||||
pub fn _getElementsByClassName(self: *parser.Element, class_names: js.String) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByClassName(
|
||||
parser.elementToNode(self),
|
||||
class_names.string,
|
||||
@@ -523,6 +599,8 @@ pub const Element = struct {
|
||||
contentVisibilityAuto: bool,
|
||||
opacityProperty: bool,
|
||||
visibilityProperty: bool,
|
||||
checkVisibilityCSS: bool,
|
||||
checkOpacity: bool,
|
||||
};
|
||||
|
||||
pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool {
|
||||
@@ -582,7 +660,7 @@ pub const Element = struct {
|
||||
return sr;
|
||||
}
|
||||
|
||||
pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation {
|
||||
pub fn _animate(self: *parser.Element, effect: js.Object, opts: js.Object) !Animation {
|
||||
_ = self;
|
||||
_ = opts;
|
||||
return Animation.constructor(effect, null);
|
||||
|
||||
@@ -100,6 +100,9 @@ pub const EventTarget = struct {
|
||||
page: *Page,
|
||||
) !void {
|
||||
_ = try EventHandler.register(page.arena, self, typ, listener, opts);
|
||||
if (std.mem.eql(u8, typ, "slotchange")) {
|
||||
try page.registerSlotChangeMonitor();
|
||||
}
|
||||
}
|
||||
|
||||
const RemoveEventListenerOpts = union(enum) {
|
||||
|
||||
@@ -23,7 +23,6 @@ const parser = @import("../netsurf.zig");
|
||||
|
||||
const Element = @import("element.zig").Element;
|
||||
const Union = @import("element.zig").Union;
|
||||
const JsThis = @import("../env.zig").JsThis;
|
||||
const Walker = @import("walker.zig").Walker;
|
||||
|
||||
const Matcher = union(enum) {
|
||||
@@ -429,24 +428,23 @@ pub const HTMLCollection = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn postAttach(self: *HTMLCollection, js_this: JsThis) !void {
|
||||
const len = try self.get_length();
|
||||
for (0..len) |i| {
|
||||
const node = try self.item(@intCast(i)) orelse unreachable;
|
||||
const e = @as(*parser.Element, @ptrCast(node));
|
||||
const as_interface = try Element.toInterface(e);
|
||||
try js_this.setIndex(@intCast(i), as_interface, .{});
|
||||
pub fn indexed_get(self: *HTMLCollection, index: u32, has_value: *bool) !?Union {
|
||||
return (try _item(self, index)) orelse {
|
||||
has_value.* = false;
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
if (try item_name(e)) |name| {
|
||||
// Even though an entry might have an empty id, the spec says
|
||||
// that namedItem("") should always return null
|
||||
if (name.len > 0) {
|
||||
// Named fields should not be enumerable (it is defined with
|
||||
// the LegacyUnenumerableNamedProperties flag.)
|
||||
try js_this.set(name, as_interface, .{ .DONT_ENUM = true });
|
||||
}
|
||||
}
|
||||
pub fn named_get(self: *const HTMLCollection, name: []const u8, has_value: *bool) !?Union {
|
||||
// Even though an entry might have an empty id, the spec says
|
||||
// that namedItem("") should always return null
|
||||
if (name.len == 0) {
|
||||
return null;
|
||||
}
|
||||
return (try _namedItem(self, name)) orelse {
|
||||
has_value.* = false;
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Element = @import("element.zig").Element;
|
||||
|
||||
pub const Interfaces = .{
|
||||
IntersectionObserver,
|
||||
IntersectionObserverEntry,
|
||||
};
|
||||
|
||||
// This is supposed to listen to change between the root and observation targets.
|
||||
// However, our rendered stores everything as 1 pixel sized boxes in a long row that never changes.
|
||||
// As such, there are no changes to intersections between the root and any target.
|
||||
// Instead we keep a list of all entries that are being observed.
|
||||
// The callback is called with all entries everytime a new entry is added(observed).
|
||||
// Potentially we should also call the callback at a regular interval.
|
||||
// The returned Entries are phony, they always indicate full intersection.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
|
||||
pub const IntersectionObserver = struct {
|
||||
page: *Page,
|
||||
callback: Env.Function,
|
||||
options: IntersectionObserverOptions,
|
||||
|
||||
observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry),
|
||||
|
||||
// new IntersectionObserver(callback)
|
||||
// new IntersectionObserver(callback, options) [not supported yet]
|
||||
pub fn constructor(callback: Env.Function, options_: ?IntersectionObserverOptions, page: *Page) !IntersectionObserver {
|
||||
var options = IntersectionObserverOptions{
|
||||
.root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
|
||||
.rootMargin = "0px 0px 0px 0px",
|
||||
.threshold = .{ .single = 0.0 },
|
||||
};
|
||||
if (options_) |*o| {
|
||||
if (o.root) |root| {
|
||||
options.root = root;
|
||||
} // Other properties are not used due to the way we render
|
||||
}
|
||||
|
||||
return .{
|
||||
.page = page,
|
||||
.callback = callback,
|
||||
.options = options,
|
||||
.observed_entries = .{},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _disconnect(self: *IntersectionObserver) !void {
|
||||
self.observed_entries = .{}; // We don't free as it is on an arena
|
||||
}
|
||||
|
||||
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element) !void {
|
||||
for (self.observed_entries.items) |*observer| {
|
||||
if (observer.target == target_element) {
|
||||
return; // Already observed
|
||||
}
|
||||
}
|
||||
|
||||
try self.observed_entries.append(self.page.arena, .{
|
||||
.page = self.page,
|
||||
.target = target_element,
|
||||
.options = &self.options,
|
||||
});
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.source = "intersection observer",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
|
||||
for (self.observed_entries.items, 0..) |*observer, index| {
|
||||
if (observer.target == target) {
|
||||
_ = self.observed_entries.swapRemove(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _takeRecords(self: *IntersectionObserver) []IntersectionObserverEntry {
|
||||
return self.observed_entries.items;
|
||||
}
|
||||
};
|
||||
|
||||
const IntersectionObserverOptions = struct {
|
||||
root: ?*parser.Node, // Element or Document
|
||||
rootMargin: ?[]const u8,
|
||||
threshold: ?Threshold,
|
||||
|
||||
const Threshold = union(enum) {
|
||||
single: f32,
|
||||
list: []const f32,
|
||||
};
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
|
||||
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
||||
pub const IntersectionObserverEntry = struct {
|
||||
page: *Page,
|
||||
target: *parser.Element,
|
||||
options: *IntersectionObserverOptions,
|
||||
|
||||
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
|
||||
pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
||||
return Element._getBoundingClientRect(self.target, self.page);
|
||||
}
|
||||
|
||||
// Returns the ratio of the intersectionRect to the boundingClientRect.
|
||||
pub fn get_intersectionRatio(_: *const IntersectionObserverEntry) f32 {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Returns a DOMRectReadOnly representing the target's visible area.
|
||||
pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
||||
return Element._getBoundingClientRect(self.target, self.page);
|
||||
}
|
||||
|
||||
// A Boolean value which is true if the target element intersects with the
|
||||
// intersection observer's root. If this is true, then, the
|
||||
// IntersectionObserverEntry describes a transition into a state of
|
||||
// intersection; if it's false, then you know the transition is from
|
||||
// intersecting to not-intersecting.
|
||||
pub fn get_isIntersecting(_: *const IntersectionObserverEntry) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns a DOMRectReadOnly for the intersection observer's root.
|
||||
pub fn get_rootBounds(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
||||
const root = self.options.root.?;
|
||||
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
|
||||
return self.page.renderer.boundingRect();
|
||||
}
|
||||
|
||||
const root_type = parser.nodeType(root);
|
||||
|
||||
var element: *parser.Element = undefined;
|
||||
switch (root_type) {
|
||||
.element => element = parser.nodeToElement(root),
|
||||
.document => {
|
||||
const doc = parser.nodeToDocument(root);
|
||||
element = (try parser.documentGetDocumentElement(doc)).?;
|
||||
},
|
||||
else => return error.InvalidState,
|
||||
}
|
||||
|
||||
return Element._getBoundingClientRect(element, self.page);
|
||||
}
|
||||
|
||||
// The Element whose intersection with the root changed.
|
||||
pub fn get_target(self: *const IntersectionObserverEntry) *parser.Element {
|
||||
return self.target;
|
||||
}
|
||||
|
||||
// TODO: pub fn get_time(self: *const IntersectionObserverEntry)
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.IntersectionObserver" {
|
||||
try testing.htmlRunner("dom/intersection_observer.html");
|
||||
}
|
||||
@@ -18,11 +18,11 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
|
||||
pub const Interfaces = .{
|
||||
@@ -35,21 +35,21 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
|
||||
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
||||
pub const MutationObserver = struct {
|
||||
page: *Page,
|
||||
cbk: Env.Function,
|
||||
connected: bool,
|
||||
cbk: js.Function,
|
||||
scheduled: bool,
|
||||
observers: std.ArrayListUnmanaged(*Observer),
|
||||
|
||||
// List of records which were observed. When the call scope ends, we need to
|
||||
// execute our callback with it.
|
||||
observed: std.ArrayListUnmanaged(MutationRecord),
|
||||
|
||||
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
|
||||
pub fn constructor(cbk: js.Function, page: *Page) !MutationObserver {
|
||||
return .{
|
||||
.cbk = cbk,
|
||||
.page = page,
|
||||
.observed = .{},
|
||||
.connected = true,
|
||||
.scheduled = false,
|
||||
.observers = .empty,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,15 +68,17 @@ pub const MutationObserver = struct {
|
||||
.event_node = .{ .id = self.cbk.id, .func = Observer.handle },
|
||||
};
|
||||
|
||||
try self.observers.append(arena, observer);
|
||||
|
||||
// register node's events
|
||||
if (options.childList or options.subtree) {
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
observer.dom_node_inserted_listener = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMNodeInserted",
|
||||
&observer.event_node,
|
||||
false,
|
||||
);
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
observer.dom_node_removed_listener = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMNodeRemoved",
|
||||
&observer.event_node,
|
||||
@@ -84,7 +86,7 @@ pub const MutationObserver = struct {
|
||||
);
|
||||
}
|
||||
if (options.attr()) {
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
observer.dom_node_attribute_modified_listener = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMAttrModified",
|
||||
&observer.event_node,
|
||||
@@ -92,7 +94,7 @@ pub const MutationObserver = struct {
|
||||
);
|
||||
}
|
||||
if (options.cdata()) {
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
observer.dom_cdata_modified_listener = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMCharacterDataModified",
|
||||
&observer.event_node,
|
||||
@@ -100,7 +102,7 @@ pub const MutationObserver = struct {
|
||||
);
|
||||
}
|
||||
if (options.subtree) {
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
observer.dom_subtree_modified_listener = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMSubtreeModified",
|
||||
&observer.event_node,
|
||||
@@ -111,10 +113,6 @@ pub const MutationObserver = struct {
|
||||
|
||||
fn callback(ctx: *anyopaque) ?u32 {
|
||||
const self: *MutationObserver = @ptrCast(@alignCast(ctx));
|
||||
if (self.connected == false) {
|
||||
self.scheduled = true;
|
||||
return null;
|
||||
}
|
||||
self.scheduled = false;
|
||||
|
||||
const records = self.observed.items;
|
||||
@@ -124,8 +122,8 @@ pub const MutationObserver = struct {
|
||||
|
||||
defer self.observed.clearRetainingCapacity();
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
self.cbk.tryCall(void, .{records}, &result) catch {
|
||||
var result: js.Function.Result = undefined;
|
||||
self.cbk.tryCallWithThis(void, self, .{records}, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
@@ -135,9 +133,55 @@ pub const MutationObserver = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _disconnect(self: *MutationObserver) !void {
|
||||
self.connected = false;
|
||||
for (self.observers.items) |observer| {
|
||||
const event_target = parser.toEventTarget(parser.Node, observer.node);
|
||||
if (observer.dom_node_inserted_listener) |listener| {
|
||||
try parser.eventTargetRemoveEventListener(
|
||||
event_target,
|
||||
"DOMNodeInserted",
|
||||
listener,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if (observer.dom_node_removed_listener) |listener| {
|
||||
try parser.eventTargetRemoveEventListener(
|
||||
event_target,
|
||||
"DOMNodeRemoved",
|
||||
listener,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if (observer.dom_node_attribute_modified_listener) |listener| {
|
||||
try parser.eventTargetRemoveEventListener(
|
||||
event_target,
|
||||
"DOMAttrModified",
|
||||
listener,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if (observer.dom_cdata_modified_listener) |listener| {
|
||||
try parser.eventTargetRemoveEventListener(
|
||||
event_target,
|
||||
"DOMCharacterDataModified",
|
||||
listener,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if (observer.dom_subtree_modified_listener) |listener| {
|
||||
try parser.eventTargetRemoveEventListener(
|
||||
event_target,
|
||||
"DOMSubtreeModified",
|
||||
listener,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
self.observers.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
// TODO
|
||||
@@ -222,6 +266,12 @@ const Observer = struct {
|
||||
|
||||
event_node: parser.EventNode,
|
||||
|
||||
dom_node_inserted_listener: ?*parser.EventListener = null,
|
||||
dom_node_removed_listener: ?*parser.EventListener = null,
|
||||
dom_node_attribute_modified_listener: ?*parser.EventListener = null,
|
||||
dom_cdata_modified_listener: ?*parser.EventListener = null,
|
||||
dom_subtree_modified_listener: ?*parser.EventListener = null,
|
||||
|
||||
fn appliesTo(
|
||||
self: *const Observer,
|
||||
target: *parser.Node,
|
||||
|
||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const generate = @import("../../runtime/generate.zig");
|
||||
const generate = @import("../js/generate.zig");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const EventTarget = @import("event_target.zig").EventTarget;
|
||||
@@ -119,8 +119,8 @@ pub const Node = struct {
|
||||
// --------
|
||||
|
||||
// Read-only attributes
|
||||
pub fn get_baseURI(_: *parser.Node, page: *Page) ![]const u8 {
|
||||
return page.url.raw;
|
||||
pub fn get_baseURI(_: *parser.Node, page: *Page) []const u8 {
|
||||
return page.url.getHref();
|
||||
}
|
||||
|
||||
pub fn get_firstChild(self: *parser.Node) !?Union {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
pub const NodeFilter = struct {
|
||||
@@ -43,7 +43,7 @@ pub const NodeFilter = struct {
|
||||
|
||||
const VerifyResult = enum { accept, skip, reject };
|
||||
|
||||
pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !VerifyResult {
|
||||
pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !VerifyResult {
|
||||
const node_type = parser.nodeType(node);
|
||||
|
||||
// Verify that we can show this node type.
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const NodeFilter = @import("node_filter.zig");
|
||||
const Node = @import("node.zig").Node;
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
@@ -37,7 +37,7 @@ pub const NodeIterator = struct {
|
||||
reference_node: *parser.Node,
|
||||
what_to_show: u32,
|
||||
filter: ?NodeIteratorOpts,
|
||||
filter_func: ?Env.Function,
|
||||
filter_func: ?js.Function,
|
||||
pointer_before_current: bool = true,
|
||||
// used to track / block recursive filters
|
||||
is_in_callback: bool = false,
|
||||
@@ -45,15 +45,15 @@ pub const NodeIterator = struct {
|
||||
// One of the few cases where null and undefined resolve to different default.
|
||||
// We need the raw JsObject so that we can probe the tri state:
|
||||
// null, undefined or i32.
|
||||
pub const WhatToShow = Env.JsObject;
|
||||
pub const WhatToShow = js.Object;
|
||||
|
||||
pub const NodeIteratorOpts = union(enum) {
|
||||
function: Env.Function,
|
||||
object: struct { acceptNode: Env.Function },
|
||||
function: js.Function,
|
||||
object: struct { acceptNode: js.Function },
|
||||
};
|
||||
|
||||
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator {
|
||||
var filter_func: ?Env.Function = null;
|
||||
var filter_func: ?js.Function = null;
|
||||
if (filter) |f| {
|
||||
filter_func = switch (f) {
|
||||
.function => |func| func,
|
||||
|
||||
@@ -19,12 +19,10 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const JsThis = @import("../env.zig").JsThis;
|
||||
const Function = @import("../env.zig").Function;
|
||||
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
@@ -148,10 +146,10 @@ pub const NodeList = struct {
|
||||
// };
|
||||
// }
|
||||
|
||||
pub fn _forEach(self: *NodeList, cbk: Function) !void { // TODO handle thisArg
|
||||
pub fn _forEach(self: *NodeList, cbk: js.Function) !void { // TODO handle thisArg
|
||||
for (self.nodes.items, 0..) |n, i| {
|
||||
const ii: u32 = @intCast(i);
|
||||
var result: Function.Result = undefined;
|
||||
var result: js.Function.Result = undefined;
|
||||
cbk.tryCall(void, .{ n, ii, self }, &result) catch {
|
||||
log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack });
|
||||
};
|
||||
@@ -175,7 +173,7 @@ pub const NodeList = struct {
|
||||
}
|
||||
|
||||
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
|
||||
pub fn postAttach(self: *NodeList, js_this: JsThis) !void {
|
||||
pub fn postAttach(self: *NodeList, js_this: js.This) !void {
|
||||
const len = self.get_length();
|
||||
for (0..len) |i| {
|
||||
const node = try self._item(@intCast(i)) orelse unreachable;
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
|
||||
@@ -61,7 +61,7 @@ pub const Performance = struct {
|
||||
return milliTimestamp() - self.time_origin;
|
||||
}
|
||||
|
||||
pub fn _mark(_: *Performance, name: Env.String, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
|
||||
pub fn _mark(_: *Performance, name: js.String, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
|
||||
const mark: PerformanceMark = try .constructor(name, _options, page);
|
||||
// TODO: Should store this in an entries list
|
||||
return mark;
|
||||
@@ -148,14 +148,14 @@ pub const PerformanceMark = struct {
|
||||
pub const prototype = *PerformanceEntry;
|
||||
|
||||
proto: PerformanceEntry,
|
||||
detail: ?Env.JsObject,
|
||||
detail: ?js.Object,
|
||||
|
||||
const Options = struct {
|
||||
detail: ?Env.JsObject = null,
|
||||
detail: ?js.Object = null,
|
||||
startTime: ?f64 = null,
|
||||
};
|
||||
|
||||
pub fn constructor(name: Env.String, _options: ?Options, page: *Page) !PerformanceMark {
|
||||
pub fn constructor(name: js.String, _options: ?Options, page: *Page) !PerformanceMark {
|
||||
const perf = &page.window.performance;
|
||||
|
||||
const options = _options orelse Options{};
|
||||
@@ -171,7 +171,7 @@ pub const PerformanceMark = struct {
|
||||
return .{ .proto = proto, .detail = detail };
|
||||
}
|
||||
|
||||
pub fn get_detail(self: *const PerformanceMark) ?Env.JsObject {
|
||||
pub fn get_detail(self: *const PerformanceMark) ?js.Object {
|
||||
return self.detail;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const PerformanceEntry = @import("performance.zig").PerformanceEntry;
|
||||
|
||||
@@ -25,7 +25,7 @@ const PerformanceEntry = @import("performance.zig").PerformanceEntry;
|
||||
pub const PerformanceObserver = struct {
|
||||
pub const _supportedEntryTypes = [0][]const u8{};
|
||||
|
||||
pub fn constructor(cbk: Env.Function) PerformanceObserver {
|
||||
pub fn constructor(cbk: js.Function) PerformanceObserver {
|
||||
_ = cbk;
|
||||
return .{};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
@@ -25,7 +25,7 @@ pub const Interfaces = .{
|
||||
|
||||
// WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface
|
||||
pub const ResizeObserver = struct {
|
||||
pub fn constructor(cbk: Env.Function) ResizeObserver {
|
||||
pub fn constructor(cbk: js.Function) ResizeObserver {
|
||||
_ = cbk;
|
||||
return .{};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
||||
const dump = @import("../dump.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const js = @import(".././js/js.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Node = @import("node.zig").Node;
|
||||
const Element = @import("element.zig").Element;
|
||||
@@ -34,7 +34,7 @@ pub const ShadowRoot = struct {
|
||||
mode: Mode,
|
||||
host: *parser.Element,
|
||||
proto: *parser.DocumentFragment,
|
||||
adopted_style_sheets: ?Env.JsObject = null,
|
||||
adopted_style_sheets: ?js.Object = null,
|
||||
|
||||
pub const Mode = enum {
|
||||
open,
|
||||
@@ -45,17 +45,17 @@ pub const ShadowRoot = struct {
|
||||
return Element.toInterface(self.host);
|
||||
}
|
||||
|
||||
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject {
|
||||
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object {
|
||||
if (self.adopted_style_sheets) |obj| {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const obj = try page.main_context.newArray(0).persist();
|
||||
const obj = try page.js.createArray(0).persist();
|
||||
self.adopted_style_sheets = obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
|
||||
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: js.Object) !void {
|
||||
self.adopted_style_sheets = try sheets.persist();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,11 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const iterator = @import("../iterator/iterator.zig");
|
||||
|
||||
const Function = @import("../env.zig").Function;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
pub const Interfaces = .{
|
||||
@@ -137,10 +136,10 @@ pub const DOMTokenList = struct {
|
||||
}
|
||||
|
||||
// TODO handle thisArg
|
||||
pub fn _forEach(self: *parser.TokenList, cbk: Function, this_arg: JsObject) !void {
|
||||
pub fn _forEach(self: *parser.TokenList, cbk: js.Function, this_arg: js.Object) !void {
|
||||
var entries = _entries(self);
|
||||
while (try entries._next()) |entry| {
|
||||
var result: Function.Result = undefined;
|
||||
var result: js.Function.Result = undefined;
|
||||
cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const NodeFilter = @import("node_filter.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Node = @import("node.zig").Node;
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
|
||||
@@ -30,20 +30,20 @@ pub const TreeWalker = struct {
|
||||
current_node: *parser.Node,
|
||||
what_to_show: u32,
|
||||
filter: ?TreeWalkerOpts,
|
||||
filter_func: ?Env.Function,
|
||||
filter_func: ?js.Function,
|
||||
|
||||
// One of the few cases where null and undefined resolve to different default.
|
||||
// We need the raw JsObject so that we can probe the tri state:
|
||||
// null, undefined or i32.
|
||||
pub const WhatToShow = Env.JsObject;
|
||||
pub const WhatToShow = js.Object;
|
||||
|
||||
pub const TreeWalkerOpts = union(enum) {
|
||||
function: Env.Function,
|
||||
object: struct { acceptNode: Env.Function },
|
||||
function: js.Function,
|
||||
object: struct { acceptNode: js.Function },
|
||||
};
|
||||
|
||||
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker {
|
||||
var filter_func: ?Env.Function = null;
|
||||
var filter_func: ?js.Function = null;
|
||||
|
||||
if (filter) |f| {
|
||||
filter_func = switch (f) {
|
||||
|
||||
@@ -69,8 +69,8 @@ pub fn get_fatal(self: *const TextDecoder) bool {
|
||||
const DecodeOptions = struct {
|
||||
stream: bool = false,
|
||||
};
|
||||
pub fn _decode(self: *TextDecoder, input_: ?[]const u8, opts_: ?DecodeOptions, page: *Page) ![]const u8 {
|
||||
var str = input_ orelse return "";
|
||||
pub fn _decode(self: *TextDecoder, str_: ?[]const u8, opts_: ?DecodeOptions, page: *Page) ![]const u8 {
|
||||
var str = str_ orelse return "";
|
||||
const opts: DecodeOptions = opts_ orelse .{};
|
||||
|
||||
if (self.stream.items.len > 0) {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
// https://encoding.spec.whatwg.org/#interface-textencoder
|
||||
const TextEncoder = @This();
|
||||
@@ -31,7 +31,7 @@ pub fn get_encoding(_: *const TextEncoder) []const u8 {
|
||||
return "utf-8";
|
||||
}
|
||||
|
||||
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
|
||||
pub fn _encode(_: *const TextEncoder, v: []const u8) !js.TypedArray(u8) {
|
||||
// Ensure the input is a valid utf-8
|
||||
// It seems chrome accepts invalid utf-8 sequence.
|
||||
//
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Page = @import("page.zig").Page;
|
||||
const js = @import("../runtime/js.zig");
|
||||
const generate = @import("../runtime/generate.zig");
|
||||
|
||||
const WebApis = struct {
|
||||
// Wrapped like this for debug ergonomics.
|
||||
// When we create our Env, a few lines down, we define it as:
|
||||
// pub const Env = js.Env(*Page, WebApis);
|
||||
//
|
||||
// If there's a compile time error witht he Env, it's type will be readable,
|
||||
// i.e.: runtime.js.Env(*browser.env.Page, browser.env.WebApis)
|
||||
//
|
||||
// But if we didn't wrap it in the struct, like we once didn't, and defined
|
||||
// env as:
|
||||
// pub const Env = js.Env(*Page, Interfaces);
|
||||
//
|
||||
// Because Interfaces is an anynoumous type, it doesn't have a friendly name
|
||||
// and errors would be something like:
|
||||
// runtime.js.Env(*browser.Page, .{...A HUNDRED TYPES...})
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
@import("crypto/crypto.zig").Crypto,
|
||||
@import("console/console.zig").Console,
|
||||
@import("css/css.zig").Interfaces,
|
||||
@import("cssom/cssom.zig").Interfaces,
|
||||
@import("dom/dom.zig").Interfaces,
|
||||
@import("dom/shadow_root.zig").ShadowRoot,
|
||||
@import("encoding/encoding.zig").Interfaces,
|
||||
@import("events/event.zig").Interfaces,
|
||||
@import("html/html.zig").Interfaces,
|
||||
@import("iterator/iterator.zig").Interfaces,
|
||||
@import("storage/storage.zig").Interfaces,
|
||||
@import("url/url.zig").Interfaces,
|
||||
@import("xhr/xhr.zig").Interfaces,
|
||||
@import("xhr/form_data.zig").Interfaces,
|
||||
@import("xhr/File.zig"),
|
||||
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
||||
@import("fetch/fetch.zig").Interfaces,
|
||||
@import("streams/streams.zig").Interfaces,
|
||||
});
|
||||
};
|
||||
|
||||
pub const JsThis = Env.JsThis;
|
||||
pub const JsObject = Env.JsObject;
|
||||
pub const Function = Env.Function;
|
||||
pub const Promise = Env.Promise;
|
||||
pub const PromiseResolver = Env.PromiseResolver;
|
||||
|
||||
pub const Env = js.Env(*Page, WebApis);
|
||||
pub const Global = @import("html/window.zig").Window;
|
||||
@@ -16,9 +16,10 @@
|
||||
// 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 js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Event = @import("event.zig").Event;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
|
||||
const netsurf = @import("../netsurf.zig");
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-customevent
|
||||
@@ -27,13 +28,13 @@ pub const CustomEvent = struct {
|
||||
pub const union_make_copy = true;
|
||||
|
||||
proto: parser.Event,
|
||||
detail: ?JsObject,
|
||||
detail: ?js.Object,
|
||||
|
||||
const CustomEventInit = struct {
|
||||
bubbles: bool = false,
|
||||
cancelable: bool = false,
|
||||
composed: bool = false,
|
||||
detail: ?JsObject = null,
|
||||
detail: ?js.Object = null,
|
||||
};
|
||||
|
||||
pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent {
|
||||
@@ -53,7 +54,7 @@ pub const CustomEvent = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_detail(self: *CustomEvent) ?JsObject {
|
||||
pub fn get_detail(self: *CustomEvent) ?js.Object {
|
||||
return self.detail;
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ pub const CustomEvent = struct {
|
||||
event_type: []const u8,
|
||||
can_bubble: bool,
|
||||
cancelable: bool,
|
||||
maybe_detail: ?JsObject,
|
||||
maybe_detail: ?js.Object,
|
||||
) !void {
|
||||
// This function can only be called after the constructor has called.
|
||||
// So we assume proto is initialized already by constructor.
|
||||
|
||||
@@ -21,7 +21,7 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const generate = @import("../../runtime/generate.zig");
|
||||
const generate = @import("../js/generate.zig");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
@@ -36,6 +36,7 @@ const MouseEvent = @import("mouse_event.zig").MouseEvent;
|
||||
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;
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = .{
|
||||
@@ -46,6 +47,7 @@ pub const Interfaces = .{
|
||||
KeyboardEvent,
|
||||
ErrorEvent,
|
||||
MessageEvent,
|
||||
PopStateEvent,
|
||||
};
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
@@ -73,6 +75,7 @@ pub const Event = struct {
|
||||
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
|
||||
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
|
||||
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
|
||||
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -216,18 +219,17 @@ pub const Event = struct {
|
||||
pub const EventHandler = struct {
|
||||
once: bool,
|
||||
capture: bool,
|
||||
callback: Function,
|
||||
callback: js.Function,
|
||||
node: parser.EventNode,
|
||||
listener: *parser.EventListener,
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Function = Env.Function;
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
pub const Listener = union(enum) {
|
||||
function: Function,
|
||||
object: Env.JsObject,
|
||||
function: js.Function,
|
||||
object: js.Object,
|
||||
|
||||
pub fn callback(self: Listener, target: *parser.EventTarget) !?Function {
|
||||
pub fn callback(self: Listener, target: *parser.EventTarget) !?js.Function {
|
||||
return switch (self) {
|
||||
.function => |func| try func.withThis(target),
|
||||
.object => |obj| blk: {
|
||||
@@ -328,7 +330,7 @@ pub const EventHandler = struct {
|
||||
fn handle(node: *parser.EventNode, event: *parser.Event) void {
|
||||
const ievent = Event.toInterface(event);
|
||||
const self: *EventHandler = @fieldParentPtr("node", node);
|
||||
var result: Function.Result = undefined;
|
||||
var result: js.Function.Result = undefined;
|
||||
self.callback.tryCall(void, .{ievent}, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
|
||||
@@ -53,8 +53,8 @@ pub const KeyboardEvent = struct {
|
||||
pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*parser.KeyboardEvent {
|
||||
const options: ConstructorOptions = maybe_options orelse .{};
|
||||
|
||||
var event = try parser.keyboardEventCreate();
|
||||
parser.eventSetInternalType(@ptrCast(&event), .keyboard_event);
|
||||
const event = try parser.keyboardEventCreate();
|
||||
parser.eventSetInternalType(@ptrCast(event), .keyboard_event);
|
||||
|
||||
try parser.keyboardEventInit(
|
||||
event,
|
||||
|
||||
@@ -54,8 +54,8 @@ pub const MouseEvent = struct {
|
||||
pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent {
|
||||
const opts = opts_ orelse MouseEventInit{};
|
||||
|
||||
var mouse_event = try parser.mouseEventCreate();
|
||||
parser.eventSetInternalType(@ptrCast(&mouse_event), .mouse_event);
|
||||
const mouse_event = try parser.mouseEventCreate();
|
||||
parser.eventSetInternalType(@ptrCast(mouse_event), .mouse_event);
|
||||
|
||||
try parser.mouseEventInit(mouse_event, event_type, .{
|
||||
.x = opts.clientX,
|
||||
@@ -68,7 +68,7 @@ pub const MouseEvent = struct {
|
||||
});
|
||||
|
||||
if (!std.mem.eql(u8, event_type, "click")) {
|
||||
log.warn(.mouse_event, "unsupported mouse event", .{ .event = event_type });
|
||||
log.warn(.browser, "unsupported mouse event", .{ .event = event_type });
|
||||
}
|
||||
|
||||
return mouse_event;
|
||||
|
||||
@@ -17,15 +17,13 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
const URL = @import("../../url.zig").URL;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const iterator = @import("../iterator/iterator.zig");
|
||||
|
||||
const v8 = @import("v8");
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
||||
const Headers = @This();
|
||||
|
||||
@@ -69,7 +67,7 @@ pub const HeadersInit = union(enum) {
|
||||
// Headers
|
||||
headers: *Headers,
|
||||
// Mappings
|
||||
object: Env.JsObject,
|
||||
object: js.Object,
|
||||
};
|
||||
|
||||
pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
|
||||
@@ -159,7 +157,7 @@ pub fn _entries(self: *const Headers) HeadersEntryIterable {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _forEach(self: *Headers, callback_fn: Env.Function, this_arg: ?Env.JsObject) !void {
|
||||
pub fn _forEach(self: *Headers, callback_fn: js.Function, this_arg: ?js.Object) !void {
|
||||
var iter = self.headers.iterator();
|
||||
|
||||
const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const URL = @import("../../url.zig").URL;
|
||||
@@ -26,9 +27,6 @@ const Response = @import("./Response.zig");
|
||||
const Http = @import("../../http/Http.zig");
|
||||
const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||
|
||||
const v8 = @import("v8");
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
const Headers = @import("Headers.zig");
|
||||
const HeadersInit = @import("Headers.zig").HeadersInit;
|
||||
|
||||
@@ -132,7 +130,7 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re
|
||||
|
||||
const url: [:0]const u8 = blk: switch (input) {
|
||||
.string => |str| {
|
||||
break :blk try URL.stitch(arena, str, page.url.raw, .{ .null_terminated = true });
|
||||
break :blk try URL.stitch(arena, str, page.url.getHref(), .{ .null_terminated = true });
|
||||
},
|
||||
.request => |req| {
|
||||
break :blk try arena.dupeZ(u8, req.url);
|
||||
@@ -181,7 +179,7 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re
|
||||
pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream {
|
||||
if (self.body) |body| {
|
||||
const stream = try ReadableStream.constructor(null, null, page);
|
||||
try stream.queue.append(page.arena, body);
|
||||
try stream.queue.append(page.arena, .{ .string = body });
|
||||
return stream;
|
||||
} else return null;
|
||||
}
|
||||
@@ -241,24 +239,19 @@ pub fn _clone(self: *Request) !Request {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
|
||||
pub fn _bytes(self: *Response, page: *Page) !js.Promise {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
|
||||
try resolver.resolve(self.body);
|
||||
self.body_used = true;
|
||||
return resolver.promise();
|
||||
return page.js.resolvePromise(self.body);
|
||||
}
|
||||
|
||||
pub fn _json(self: *Response, page: *Page) !Env.Promise {
|
||||
pub fn _json(self: *Response, page: *Page) !js.Promise {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
self.body_used = true;
|
||||
|
||||
if (self.body) |body| {
|
||||
const p = std.json.parseFromSliceLeaky(
|
||||
@@ -271,25 +264,17 @@ pub fn _json(self: *Response, page: *Page) !Env.Promise {
|
||||
return error.SyntaxError;
|
||||
};
|
||||
|
||||
try resolver.resolve(p);
|
||||
} else {
|
||||
try resolver.resolve(null);
|
||||
return page.js.resolvePromise(p);
|
||||
}
|
||||
|
||||
self.body_used = true;
|
||||
return resolver.promise();
|
||||
return page.js.resolvePromise(null);
|
||||
}
|
||||
|
||||
pub fn _text(self: *Response, page: *Page) !Env.Promise {
|
||||
pub fn _text(self: *Response, page: *Page) !js.Promise {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
|
||||
try resolver.resolve(self.body);
|
||||
self.body_used = true;
|
||||
return resolver.promise();
|
||||
return page.js.resolvePromise(self.body);
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -17,10 +17,9 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const v8 = @import("v8");
|
||||
|
||||
const HttpClient = @import("../../http/Client.zig");
|
||||
const Http = @import("../../http/Http.zig");
|
||||
const URL = @import("../../url.zig").URL;
|
||||
@@ -29,7 +28,6 @@ const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||
const Headers = @import("Headers.zig");
|
||||
const HeadersInit = @import("Headers.zig").HeadersInit;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Mime = @import("../mime.zig").Mime;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
@@ -109,7 +107,7 @@ pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Pag
|
||||
pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
|
||||
const stream = try ReadableStream.constructor(null, null, page);
|
||||
if (self.body) |body| {
|
||||
try stream.queue.append(page.arena, body);
|
||||
try stream.queue.append(page.arena, .{ .string = body });
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
@@ -165,29 +163,22 @@ pub fn _clone(self: *const Response) !Response {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
|
||||
pub fn _bytes(self: *Response, page: *Page) !js.Promise {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const resolver = Env.PromiseResolver{
|
||||
.js_context = page.main_context,
|
||||
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||
};
|
||||
|
||||
try resolver.resolve(self.body);
|
||||
self.body_used = true;
|
||||
return resolver.promise();
|
||||
return page.js.resolvePromise(self.body);
|
||||
}
|
||||
|
||||
pub fn _json(self: *Response, page: *Page) !Env.Promise {
|
||||
pub fn _json(self: *Response, page: *Page) !js.Promise {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
|
||||
if (self.body) |body| {
|
||||
self.body_used = true;
|
||||
const p = std.json.parseFromSliceLeaky(
|
||||
std.json.Value,
|
||||
page.call_arena,
|
||||
@@ -198,25 +189,18 @@ pub fn _json(self: *Response, page: *Page) !Env.Promise {
|
||||
return error.SyntaxError;
|
||||
};
|
||||
|
||||
try resolver.resolve(p);
|
||||
} else {
|
||||
try resolver.resolve(null);
|
||||
return page.js.resolvePromise(p);
|
||||
}
|
||||
|
||||
self.body_used = true;
|
||||
return resolver.promise();
|
||||
return page.js.resolvePromise(null);
|
||||
}
|
||||
|
||||
pub fn _text(self: *Response, page: *Page) !Env.Promise {
|
||||
pub fn _text(self: *Response, page: *Page) !js.Promise {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
|
||||
try resolver.resolve(self.body);
|
||||
self.body_used = true;
|
||||
return resolver.promise();
|
||||
|
||||
return page.js.resolvePromise(self.body);
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Http = @import("../../http/Http.zig");
|
||||
@@ -43,9 +43,9 @@ pub const Interfaces = .{
|
||||
};
|
||||
|
||||
pub const FetchContext = struct {
|
||||
page: *Page,
|
||||
arena: std.mem.Allocator,
|
||||
js_ctx: *Env.JsContext,
|
||||
promise_resolver: Env.PersistentPromiseResolver,
|
||||
promise_resolver: js.PersistentPromiseResolver,
|
||||
|
||||
method: Http.Method,
|
||||
url: []const u8,
|
||||
@@ -63,9 +63,12 @@ pub const FetchContext = struct {
|
||||
pub fn toResponse(self: *const FetchContext) !Response {
|
||||
var headers: Headers = .{};
|
||||
|
||||
// seems to be the highest priority
|
||||
const same_origin = try self.page.isSameOrigin(self.url);
|
||||
|
||||
// If the mode is "no-cors", we need to return this opaque/stripped Response.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Response/type
|
||||
if (self.mode == .@"no-cors") {
|
||||
if (!same_origin and self.mode == .@"no-cors") {
|
||||
return Response{
|
||||
.status = 0,
|
||||
.headers = headers,
|
||||
@@ -85,7 +88,7 @@ pub const FetchContext = struct {
|
||||
}
|
||||
|
||||
const resp_type: Response.ResponseType = blk: {
|
||||
if (std.mem.startsWith(u8, self.url, "data:")) {
|
||||
if (same_origin or std.mem.startsWith(u8, self.url, "data:")) {
|
||||
break :blk .basic;
|
||||
}
|
||||
|
||||
@@ -108,7 +111,7 @@ pub const FetchContext = struct {
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
|
||||
pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promise {
|
||||
pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !js.Promise {
|
||||
const arena = page.arena;
|
||||
|
||||
const req = try Request.constructor(input, options, page);
|
||||
@@ -128,12 +131,12 @@ pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promi
|
||||
|
||||
try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers);
|
||||
|
||||
const resolver = try page.main_context.createPersistentPromiseResolver(.page);
|
||||
const resolver = try page.js.createPromiseResolver(.page);
|
||||
|
||||
const fetch_ctx = try arena.create(FetchContext);
|
||||
fetch_ctx.* = .{
|
||||
.page = page,
|
||||
.arena = arena,
|
||||
.js_ctx = page.main_context,
|
||||
.promise_resolver = resolver,
|
||||
.method = req.method,
|
||||
.url = req.url,
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
@@ -113,12 +113,12 @@ pub const AbortSignal = struct {
|
||||
}
|
||||
|
||||
const ThrowIfAborted = union(enum) {
|
||||
exception: Env.Exception,
|
||||
exception: js.Exception,
|
||||
undefined: void,
|
||||
};
|
||||
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
|
||||
if (self.aborted) {
|
||||
const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON);
|
||||
const ex = page.js.throw(self.reason orelse DEFAULT_REASON);
|
||||
return .{ .exception = ex };
|
||||
}
|
||||
return .{ .undefined = {} };
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
@@ -26,7 +27,7 @@ const DataSet = @This();
|
||||
|
||||
element: *parser.Element,
|
||||
|
||||
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !Env.UndefinedOr([]const u8) {
|
||||
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !js.UndefinedOr([]const u8) {
|
||||
const normalized_name = try normalize(page.call_arena, name);
|
||||
if (try parser.elementGetAttribute(self.element, normalized_name)) |value| {
|
||||
return .{ .value = value };
|
||||
|
||||
215
src/browser/html/History.zig
Normal file
215
src/browser/html/History.zig
Normal file
@@ -0,0 +1,215 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
||||
const History = @This();
|
||||
|
||||
const HistoryEntry = struct {
|
||||
url: []const u8,
|
||||
// This is serialized as JSON because
|
||||
// History must survive a JsContext.
|
||||
state: ?[]u8,
|
||||
};
|
||||
|
||||
const ScrollRestorationMode = enum {
|
||||
auto,
|
||||
manual,
|
||||
|
||||
pub fn fromString(str: []const u8) ?ScrollRestorationMode {
|
||||
for (std.enums.values(ScrollRestorationMode)) |mode| {
|
||||
if (std.ascii.eqlIgnoreCase(str, @tagName(mode))) {
|
||||
return mode;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toString(self: ScrollRestorationMode) []const u8 {
|
||||
return @tagName(self);
|
||||
}
|
||||
};
|
||||
|
||||
scroll_restoration: ScrollRestorationMode = .auto,
|
||||
stack: std.ArrayListUnmanaged(HistoryEntry) = .empty,
|
||||
current: ?usize = null,
|
||||
|
||||
pub fn get_length(self: *History) u32 {
|
||||
return @intCast(self.stack.items.len);
|
||||
}
|
||||
|
||||
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
|
||||
return self.scroll_restoration;
|
||||
}
|
||||
|
||||
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
|
||||
self.scroll_restoration = ScrollRestorationMode.fromString(mode) orelse self.scroll_restoration;
|
||||
}
|
||||
|
||||
pub fn get_state(self: *History, page: *Page) !?js.Value {
|
||||
if (self.current) |curr| {
|
||||
const entry = self.stack.items[curr];
|
||||
if (entry.state) |state| {
|
||||
const value = try js.Value.fromJson(page.js, state);
|
||||
return value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pushNavigation(self: *History, _url: []const u8, page: *Page) !void {
|
||||
const arena = page.session.arena;
|
||||
const url = try arena.dupe(u8, _url);
|
||||
|
||||
const entry = HistoryEntry{ .state = null, .url = url };
|
||||
try self.stack.append(arena, entry);
|
||||
self.current = self.stack.items.len - 1;
|
||||
}
|
||||
|
||||
pub fn dispatchPopStateEvent(state: ?[]const u8, page: *Page) void {
|
||||
log.debug(.script_event, "dispatch popstate event", .{
|
||||
.type = "popstate",
|
||||
.source = "history",
|
||||
});
|
||||
History._dispatchPopStateEvent(state, page) catch |err| {
|
||||
log.err(.app, "dispatch popstate event error", .{
|
||||
.err = err,
|
||||
.type = "popstate",
|
||||
.source = "history",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
fn _dispatchPopStateEvent(state: ?[]const u8, page: *Page) !void {
|
||||
var evt = try PopStateEvent.constructor("popstate", .{ .state = state });
|
||||
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
@as(*parser.EventTarget, @ptrCast(&page.window)),
|
||||
&evt.proto,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _pushState(self: *History, state: js.Object, _: ?[]const u8, maybe_url: ?[]const u8, page: *Page) !void {
|
||||
const arena = page.session.arena;
|
||||
|
||||
const json = try state.toJson(arena);
|
||||
const url = if (maybe_url) |u| try arena.dupe(u8, u) else page.url.getHref();
|
||||
const entry = HistoryEntry{ .state = json, .url = url };
|
||||
try self.stack.append(arena, entry);
|
||||
self.current = self.stack.items.len - 1;
|
||||
}
|
||||
|
||||
pub fn _replaceState(self: *History, state: js.Object, _: ?[]const u8, maybe_url: ?[]const u8, page: *Page) !void {
|
||||
const arena = page.session.arena;
|
||||
|
||||
if (self.current) |curr| {
|
||||
const entry = &self.stack.items[curr];
|
||||
const json = try state.toJson(arena);
|
||||
const url = if (maybe_url) |u| try arena.dupe(u8, u) else page.url.getHref();
|
||||
entry.* = HistoryEntry{ .state = json, .url = url };
|
||||
} else {
|
||||
try self._pushState(state, "", maybe_url, page);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go(self: *History, delta: i32, page: *Page) !void {
|
||||
// 0 behaves the same as no argument, both reloading the page.
|
||||
// If this is getting called, there SHOULD be an entry, atleast from pushNavigation.
|
||||
const current = self.current.?;
|
||||
|
||||
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
|
||||
if (index_s < 0 or index_s > self.stack.items.len - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = @as(usize, @intCast(index_s));
|
||||
const entry = self.stack.items[index];
|
||||
self.current = index;
|
||||
|
||||
if (try page.isSameOrigin(entry.url)) {
|
||||
History.dispatchPopStateEvent(entry.state, page);
|
||||
}
|
||||
|
||||
try page.navigateFromWebAPI(entry.url, .{ .reason = .history });
|
||||
}
|
||||
|
||||
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
|
||||
try self.go(_delta orelse 0, page);
|
||||
}
|
||||
|
||||
pub fn _back(self: *History, page: *Page) !void {
|
||||
try self.go(-1, page);
|
||||
}
|
||||
|
||||
pub fn _forward(self: *History, page: *Page) !void {
|
||||
try self.go(1, page);
|
||||
}
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Event = @import("../events/event.zig").Event;
|
||||
|
||||
pub const PopStateEvent = struct {
|
||||
pub const prototype = *Event;
|
||||
pub const union_make_copy = true;
|
||||
|
||||
pub const EventInit = struct {
|
||||
state: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
proto: parser.Event,
|
||||
state: ?[]const u8,
|
||||
|
||||
pub fn constructor(event_type: []const u8, opts: ?EventInit) !PopStateEvent {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, event_type, .{});
|
||||
parser.eventSetInternalType(event, .pop_state);
|
||||
|
||||
const o = opts orelse EventInit{};
|
||||
|
||||
return .{
|
||||
.proto = event.*,
|
||||
.state = o.state,
|
||||
};
|
||||
}
|
||||
|
||||
// `hasUAVisualTransition` is not implemented. It isn't baseline so this is okay.
|
||||
|
||||
pub fn get_state(self: *const PopStateEvent, page: *Page) !?js.Value {
|
||||
if (self.state) |state| {
|
||||
const value = try js.Value.fromJson(page.js, state);
|
||||
return value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: HTML.History" {
|
||||
try testing.htmlRunner("html/history.html");
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -85,7 +85,7 @@ pub const HTMLDocument = struct {
|
||||
|
||||
pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{
|
||||
try page.cookie_jar.forRequest(page.url, buf.writer(page.arena), .{
|
||||
.is_http = false,
|
||||
.is_navigation = true,
|
||||
});
|
||||
@@ -95,7 +95,7 @@ pub const HTMLDocument = struct {
|
||||
pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, page: *Page) ![]const u8 {
|
||||
// we use the cookie jar's allocator to parse the cookie because it
|
||||
// outlives the page's arena.
|
||||
const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str);
|
||||
const c = try Cookie.parse(page.cookie_jar.allocator, page.url, cookie_str);
|
||||
errdefer c.deinit();
|
||||
if (c.http_only) {
|
||||
c.deinit();
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const generate = @import("../../runtime/generate.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const generate = @import("../js/generate.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const urlStitch = @import("../../url.zig").URL.stitch;
|
||||
@@ -218,12 +218,32 @@ 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);
|
||||
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *Page) !void {
|
||||
const full = try urlStitch(page.call_arena, href, page.url.getHref(), .{});
|
||||
|
||||
// Get the stored internal URL if we had one.
|
||||
if (page.getObjectData(self)) |internal_url| {
|
||||
const u = NativeURL.fromInternal(internal_url);
|
||||
// Reparse with the new href.
|
||||
_ = try u.reparse(full);
|
||||
errdefer u.deinit();
|
||||
|
||||
// TODO: Remove the entry from the map on an error situation.
|
||||
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
// We don't have internal URL stored in object_data yet.
|
||||
// Create one for this anchor element.
|
||||
const u = try NativeURL.parse(full, null);
|
||||
errdefer u.deinit();
|
||||
// Save to map.
|
||||
try page.putObjectData(self, u.internal.?);
|
||||
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
|
||||
@@ -258,193 +278,187 @@ pub const HTMLAnchorElement = struct {
|
||||
return try parser.nodeSetTextContent(parser.anchorToNode(self), v);
|
||||
}
|
||||
|
||||
fn url(self: *parser.Anchor, page: *Page) !URL {
|
||||
// Although the URL.constructor union accepts an .{.element = X}, we
|
||||
// can't use this here because the behavior is different.
|
||||
// URL.constructor(document.createElement('a')
|
||||
// should fail (a.href isn't a valid URL)
|
||||
// But
|
||||
// document.createElement('a').host
|
||||
// should not fail, it should return an empty string
|
||||
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
|
||||
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
|
||||
fn getHref(self: *parser.Anchor) !?[]const u8 {
|
||||
return parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href");
|
||||
}
|
||||
|
||||
/// Returns the URL associated with given anchor element.
|
||||
/// Creates a new URL object if not created before.
|
||||
fn getURL(self: *parser.Anchor, page: *Page) !NativeURL {
|
||||
if (page.getObjectData(self)) |internal_url| {
|
||||
return NativeURL.fromInternal(internal_url);
|
||||
}
|
||||
return .empty;
|
||||
|
||||
// Try to get href string.
|
||||
const maybe_anchor_href = try getHref(self);
|
||||
if (maybe_anchor_href) |anchor_href| {
|
||||
// Allocate a URL for this anchor element.
|
||||
const u = try NativeURL.parse(anchor_href, null);
|
||||
// Save in map.
|
||||
try page.putObjectData(self, u.internal.?);
|
||||
|
||||
return u;
|
||||
}
|
||||
|
||||
// No anchor href string found; let's just return an error.
|
||||
return error.HrefAttributeNotGiven;
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return try u.get_origin(page);
|
||||
const u = getURL(self, page) catch return "";
|
||||
// Though we store the URL in object data map, we still have to allocate
|
||||
// for origin string sadly.
|
||||
return u.getOrigin(page.arena);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return u.get_protocol(page);
|
||||
const u = getURL(self, page) catch return "";
|
||||
return u.getProtocol();
|
||||
}
|
||||
|
||||
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
var u = try url(self, page);
|
||||
|
||||
u.uri.scheme = v;
|
||||
const href = try u.toString(arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
const u = try getURL(self, page);
|
||||
try u.setProtocol(v);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
const NativeURL = @import("../../url.zig").URL;
|
||||
|
||||
// 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);
|
||||
const u = getURL(self, page) catch return "";
|
||||
return u.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;
|
||||
pub fn set_host(self: *parser.Anchor, host_str: []const u8, page: *Page) !void {
|
||||
const u = blk: {
|
||||
if (page.getObjectData(self)) |internal_url| {
|
||||
break :blk NativeURL.fromInternal(internal_url);
|
||||
}
|
||||
}
|
||||
|
||||
const arena = page.arena;
|
||||
var u = try url(self, page);
|
||||
const maybe_anchor_href = try getHref(self);
|
||||
if (maybe_anchor_href) |anchor_href| {
|
||||
const new_u = try NativeURL.parse(anchor_href, null);
|
||||
try page.putObjectData(self, new_u.internal.?);
|
||||
break :blk new_u;
|
||||
}
|
||||
|
||||
if (p) |pp| {
|
||||
u.uri.host = .{ .raw = h };
|
||||
u.uri.port = pp;
|
||||
} else {
|
||||
u.uri.host = .{ .raw = v };
|
||||
u.uri.port = null;
|
||||
}
|
||||
// Last resort; try to create URL object out of host_str.
|
||||
const new_u = try NativeURL.parse(host_str, null);
|
||||
// We can just return here since host is updated.
|
||||
return page.putObjectData(self, new_u.internal.?);
|
||||
};
|
||||
|
||||
const href = try u.toString(arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
try u.setHost(host_str);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return u.get_hostname();
|
||||
pub fn get_hostname(self: *parser.Anchor, page: *Page) []const u8 {
|
||||
const u = getURL(self, page) catch return "";
|
||||
return u.getHostname();
|
||||
}
|
||||
|
||||
pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
var u = try url(self, page);
|
||||
u.uri.host = .{ .raw = v };
|
||||
const href = try u.toString(arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
|
||||
const u = blk: {
|
||||
if (page.getObjectData(self)) |internal_url| {
|
||||
break :blk NativeURL.fromInternal(internal_url);
|
||||
}
|
||||
|
||||
const maybe_anchor_href = try getHref(self);
|
||||
if (maybe_anchor_href) |anchor_href| {
|
||||
const new_u = try NativeURL.parse(anchor_href, null);
|
||||
try page.putObjectData(self, new_u.internal.?);
|
||||
break :blk new_u;
|
||||
}
|
||||
|
||||
// Last resort; try to create URL object out of hostname.
|
||||
const new_u = try NativeURL.parse(hostname, null);
|
||||
// We can just return here since hostname is updated.
|
||||
return page.putObjectData(self, new_u.internal.?);
|
||||
};
|
||||
|
||||
try u.setHostname(hostname);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return try u.get_port(page);
|
||||
const u = getURL(self, page) catch return "";
|
||||
return u.getPort();
|
||||
}
|
||||
|
||||
pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
var u = try url(self, page);
|
||||
pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void {
|
||||
// TODO: Check for valid port (u16 integer).
|
||||
if (maybe_port) |port| {
|
||||
const u = try getURL(self, page);
|
||||
try u.setPort(port);
|
||||
|
||||
if (v != null and v.?.len > 0) {
|
||||
u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
|
||||
} else {
|
||||
u.uri.port = null;
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
const href = try u.toString(arena);
|
||||
try 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();
|
||||
const u = try getURL(self, page);
|
||||
return u.getUsername() orelse "";
|
||||
}
|
||||
|
||||
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;
|
||||
pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
|
||||
if (maybe_username) |username| {
|
||||
const u = try getURL(self, page);
|
||||
try u.setUsername(username);
|
||||
try parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
const href = try u.toString(arena);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return try page.arena.dupe(u8, u.get_password());
|
||||
const u = try getURL(self, page);
|
||||
return u.getPassword() orelse "";
|
||||
}
|
||||
|
||||
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
var u = try url(self, page);
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.password = .{ .raw = vv };
|
||||
} else {
|
||||
u.uri.password = null;
|
||||
pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
|
||||
if (maybe_password) |password| {
|
||||
const u = try getURL(self, page);
|
||||
try u.setPassword(password);
|
||||
try parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
const href = try u.toString(arena);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return u.get_pathname();
|
||||
const u = try getURL(self, page);
|
||||
return u.getPath();
|
||||
}
|
||||
|
||||
pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
var u = try url(self, page);
|
||||
u.uri.path = .{ .raw = v };
|
||||
const href = try u.toString(arena);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void {
|
||||
const u = try getURL(self, page);
|
||||
try u.setPath(pathname);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return try u.get_search(page);
|
||||
const u = try getURL(self, page);
|
||||
return u.getSearch() orelse "";
|
||||
}
|
||||
|
||||
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
||||
var u = try url(self, page);
|
||||
try u.set_search(v, page);
|
||||
|
||||
const href = try u.toString(page.call_arena);
|
||||
try parser.anchorSetHref(self, href);
|
||||
pub fn set_search(self: *parser.Anchor, search: []const u8, page: *Page) !void {
|
||||
const u = try getURL(self, page);
|
||||
u.setSearch(search);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||
var u = try url(self, page);
|
||||
return try u.get_hash(page);
|
||||
const u = try getURL(self, page);
|
||||
return u.getHash() orelse "";
|
||||
}
|
||||
|
||||
pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
var u = try url(self, page);
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.fragment = .{ .raw = vv };
|
||||
} else {
|
||||
u.uri.fragment = null;
|
||||
}
|
||||
const href = try u.toString(arena);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
pub fn set_hash(self: *parser.Anchor, hash: []const u8, page: *Page) !void {
|
||||
const u = try getURL(self, page);
|
||||
u.setHash(hash);
|
||||
return parser.anchorSetHref(self, u.getHref());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -717,7 +731,7 @@ pub const HTMLInputElement = struct {
|
||||
return try parser.inputGetSrc(self);
|
||||
}
|
||||
pub fn set_src(self: *parser.Input, src: []const u8, page: *Page) !void {
|
||||
const new_src = try urlStitch(page.call_arena, src, page.url.raw, .{ .alloc = .if_needed });
|
||||
const new_src = try urlStitch(page.call_arena, src, page.url.getHref(), .{ .alloc = .if_needed });
|
||||
try parser.inputSetSrc(self, new_src);
|
||||
}
|
||||
pub fn get_type(self: *parser.Input) ![]const u8 {
|
||||
@@ -770,7 +784,7 @@ pub const HTMLLinkElement = struct {
|
||||
}
|
||||
|
||||
pub fn set_href(self: *parser.Link, href: []const u8, page: *const Page) !void {
|
||||
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
|
||||
const full = try urlStitch(page.call_arena, href, page.url.getHref(), .{});
|
||||
return parser.linkSetHref(self, full);
|
||||
}
|
||||
};
|
||||
@@ -887,7 +901,7 @@ pub const HTMLScriptElement = struct {
|
||||
// s.src = '...';
|
||||
// This should load the script.
|
||||
// addFromElement protects against double execution.
|
||||
try page.script_manager.addFromElement(@ptrCast(@alignCast(self)));
|
||||
try page.script_manager.addFromElement(@ptrCast(@alignCast(self)), "dynamic");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1000,22 +1014,22 @@ pub const HTMLScriptElement = struct {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function {
|
||||
pub fn get_onload(self: *parser.Script, page: *Page) !?js.Function {
|
||||
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
|
||||
return state.onload;
|
||||
}
|
||||
|
||||
pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
|
||||
pub fn set_onload(self: *parser.Script, function: ?js.Function, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.onload = function;
|
||||
}
|
||||
|
||||
pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function {
|
||||
pub fn get_onerror(self: *parser.Script, page: *Page) !?js.Function {
|
||||
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
|
||||
return state.onerror;
|
||||
}
|
||||
|
||||
pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
|
||||
pub fn set_onerror(self: *parser.Script, function: ?js.Function, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.onerror = function;
|
||||
}
|
||||
@@ -1351,6 +1365,9 @@ test "Browser: HTML.HtmlStyleElement" {
|
||||
test "Browser: HTML.HtmlScriptElement" {
|
||||
try testing.htmlRunner("html/script/script.html");
|
||||
try testing.htmlRunner("html/script/inline_defer.html");
|
||||
try testing.htmlRunner("html/script/import.html");
|
||||
try testing.htmlRunner("html/script/dynamic_import.html");
|
||||
try testing.htmlRunner("html/script/importmap.html");
|
||||
}
|
||||
|
||||
test "Browser: HTML.HtmlSlotElement" {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const Env = @import("../env.zig").Env;
|
||||
const js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
|
||||
@@ -28,14 +28,14 @@ pub const ErrorEvent = struct {
|
||||
filename: []const u8,
|
||||
lineno: i32,
|
||||
colno: i32,
|
||||
@"error": ?Env.JsObject,
|
||||
@"error": ?js.Object,
|
||||
|
||||
const ErrorEventInit = struct {
|
||||
message: []const u8 = "",
|
||||
filename: []const u8 = "",
|
||||
lineno: i32 = 0,
|
||||
colno: i32 = 0,
|
||||
@"error": ?Env.JsObject = null,
|
||||
@"error": ?js.Object = null,
|
||||
};
|
||||
|
||||
pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent {
|
||||
@@ -72,7 +72,7 @@ pub const ErrorEvent = struct {
|
||||
return self.colno;
|
||||
}
|
||||
|
||||
pub fn get_error(self: *const ErrorEvent) Env.UndefinedOr(Env.JsObject) {
|
||||
pub fn get_error(self: *const ErrorEvent) js.UndefinedOr(js.Object) {
|
||||
if (self.@"error") |e| {
|
||||
return .{ .value = e };
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
||||
pub const History = struct {
|
||||
const ScrollRestorationMode = enum {
|
||||
auto,
|
||||
manual,
|
||||
};
|
||||
|
||||
scrollRestoration: ScrollRestorationMode = .auto,
|
||||
state: std.json.Value = .null,
|
||||
|
||||
// count tracks the history length until we implement correctly pushstate.
|
||||
count: u32 = 0,
|
||||
|
||||
pub fn get_length(self: *History) u32 {
|
||||
// TODO return the real history length value.
|
||||
return self.count;
|
||||
}
|
||||
|
||||
pub fn get_scrollRestoration(self: *History) []const u8 {
|
||||
return switch (self.scrollRestoration) {
|
||||
.auto => "auto",
|
||||
.manual => "manual",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
|
||||
if (std.mem.eql(u8, "manual", mode)) self.scrollRestoration = .manual;
|
||||
if (std.mem.eql(u8, "auto", mode)) self.scrollRestoration = .auto;
|
||||
}
|
||||
|
||||
pub fn get_state(self: *History) std.json.Value {
|
||||
return self.state;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
// data must handle any argument. We could expect a std.json.Value but
|
||||
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
|
||||
pub fn _pushState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
|
||||
self.count += 1;
|
||||
_ = url;
|
||||
_ = data;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
// data must handle any argument. We could expect a std.json.Value but
|
||||
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
|
||||
pub fn _replaceState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
|
||||
_ = self;
|
||||
_ = url;
|
||||
_ = data;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
pub fn _go(self: *History, delta: ?i32) void {
|
||||
_ = self;
|
||||
_ = delta;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
pub fn _back(self: *History) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
// TODO implement the function
|
||||
pub fn _forward(self: *History) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: HTML.History" {
|
||||
try testing.htmlRunner("html/history.html");
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const HTMLElem = @import("elements.zig");
|
||||
const SVGElem = @import("svg_elements.zig");
|
||||
const Window = @import("window.zig").Window;
|
||||
const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
const History = @import("History.zig");
|
||||
const Location = @import("location.zig").Location;
|
||||
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
|
||||
|
||||
|
||||
@@ -16,57 +16,57 @@
|
||||
// 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 Page = @import("../page.zig").Page;
|
||||
const Uri = @import("std").Uri;
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const URL = @import("../url/url.zig").URL;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface
|
||||
pub const Location = struct {
|
||||
url: ?URL = null,
|
||||
url: URL,
|
||||
|
||||
/// Browsers give such initial values when user not navigated yet:
|
||||
/// Chrome -> chrome://new-tab-page/
|
||||
/// Firefox -> about:newtab
|
||||
/// Safari -> favorites://
|
||||
pub fn get_href(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_href(page);
|
||||
return "";
|
||||
return self.url.get_href(page);
|
||||
}
|
||||
|
||||
pub fn get_protocol(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_protocol(page);
|
||||
return "";
|
||||
pub fn set_href(_: *const Location, href: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(href, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn get_host(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_host(page);
|
||||
return "";
|
||||
pub fn get_protocol(self: *Location) []const u8 {
|
||||
return self.url.get_protocol();
|
||||
}
|
||||
|
||||
pub fn get_host(self: *Location) []const u8 {
|
||||
return self.url.get_host();
|
||||
}
|
||||
|
||||
pub fn get_hostname(self: *Location) []const u8 {
|
||||
if (self.url) |*u| return u.get_hostname();
|
||||
return "";
|
||||
return self.url.get_hostname();
|
||||
}
|
||||
|
||||
pub fn get_port(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_port(page);
|
||||
return "";
|
||||
pub fn get_port(self: *Location) []const u8 {
|
||||
return self.url.get_port();
|
||||
}
|
||||
|
||||
pub fn get_pathname(self: *Location) []const u8 {
|
||||
if (self.url) |*u| return u.get_pathname();
|
||||
return "";
|
||||
return self.url.get_pathname();
|
||||
}
|
||||
|
||||
pub fn get_search(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_search(page);
|
||||
return "";
|
||||
return self.url.get_search(page);
|
||||
}
|
||||
|
||||
pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_hash(page);
|
||||
return "";
|
||||
pub fn get_hash(self: *Location) []const u8 {
|
||||
return self.url.get_hash();
|
||||
}
|
||||
|
||||
pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_origin(page);
|
||||
return "";
|
||||
return self.url.get_origin(page);
|
||||
}
|
||||
|
||||
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
|
||||
@@ -78,11 +78,11 @@ pub const Location = struct {
|
||||
}
|
||||
|
||||
pub fn _reload(_: *const Location, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script });
|
||||
return page.navigateFromWebAPI(page.url.getHref(), .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
|
||||
return try self.get_href(page);
|
||||
return self.get_href(page);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
// 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 js = @import("../js/js.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Function = @import("../env.zig").Function;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
// https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface
|
||||
@@ -39,7 +39,7 @@ pub const MediaQueryList = struct {
|
||||
return self.media;
|
||||
}
|
||||
|
||||
pub fn _addListener(_: *const MediaQueryList, _: Function) void {}
|
||||
pub fn _addListener(_: *const MediaQueryList, _: js.Function) void {}
|
||||
|
||||
pub fn _removeListener(_: *const MediaQueryList, _: Function) void {}
|
||||
pub fn _removeListener(_: *const MediaQueryList, _: js.Function) void {}
|
||||
};
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
const History = @import("History.zig");
|
||||
const Location = @import("location.zig").Location;
|
||||
const Crypto = @import("../crypto/crypto.zig").Crypto;
|
||||
const Console = @import("../console/console.zig").Console;
|
||||
@@ -35,10 +35,10 @@ const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
|
||||
const Screen = @import("screen.zig").Screen;
|
||||
const domcss = @import("../dom/css.zig");
|
||||
const Css = @import("../css/css.zig").Css;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
const URL = @import("../../url.zig").URL;
|
||||
const WebApiURL = @import("../url/url.zig").URL;
|
||||
|
||||
const Function = Env.Function;
|
||||
|
||||
const v8 = @import("v8");
|
||||
const Request = @import("../fetch/Request.zig");
|
||||
const fetchFn = @import("../fetch/fetch.zig").fetch;
|
||||
|
||||
@@ -54,8 +54,7 @@ pub const Window = struct {
|
||||
|
||||
document: *parser.DocumentHTML,
|
||||
target: []const u8 = "",
|
||||
history: History = .{},
|
||||
location: Location = .{},
|
||||
location: Location,
|
||||
storage_shelf: ?*storage.Shelf = null,
|
||||
|
||||
// counter for having unique timer ids
|
||||
@@ -68,6 +67,9 @@ pub const Window = struct {
|
||||
performance: Performance,
|
||||
screen: Screen = .{},
|
||||
css: Css = .{},
|
||||
scroll_x: u32 = 0,
|
||||
scroll_y: u32 = 0,
|
||||
onload_callback: ?js.Function = null,
|
||||
|
||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
|
||||
var fbs = std.io.fixedBufferStream("");
|
||||
@@ -75,17 +77,30 @@ pub const Window = struct {
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
try parser.documentSetDocumentURI(doc, "about:blank");
|
||||
|
||||
const native_url = URL.parse("about:blank", null) catch unreachable;
|
||||
|
||||
// Here we manually initialize; this is a special case and
|
||||
// one should prefer constructor functions instead.
|
||||
const url = WebApiURL{
|
||||
.internal = native_url.internal,
|
||||
.search_params = .{},
|
||||
};
|
||||
|
||||
return .{
|
||||
.document = html_doc,
|
||||
.location = .{ .url = url },
|
||||
.target = target orelse "",
|
||||
.navigator = navigator orelse .{},
|
||||
.performance = Performance.init(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn replaceLocation(self: *Window, loc: Location) !void {
|
||||
self.location = loc;
|
||||
try parser.documentHTMLSetLocation(Location, self.document, &self.location);
|
||||
pub fn replaceLocation(self: *Window, location: Location) !void {
|
||||
// Remove current.
|
||||
self.location.url.destructor();
|
||||
// Put the new one.
|
||||
self.location = location;
|
||||
return parser.documentHTMLSetLocation(Location, self.document, &self.location);
|
||||
}
|
||||
|
||||
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
|
||||
@@ -98,16 +113,42 @@ pub const Window = struct {
|
||||
self.storage_shelf = shelf;
|
||||
}
|
||||
|
||||
pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !Env.Promise {
|
||||
pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !js.Promise {
|
||||
return fetchFn(input, options, page);
|
||||
}
|
||||
|
||||
pub fn get_window(self: *Window) *Window {
|
||||
return self;
|
||||
/// Returns `onload_callback`.
|
||||
pub fn get_onload(self: *const Window) ?js.Function {
|
||||
return self.onload_callback;
|
||||
}
|
||||
|
||||
pub fn get_navigator(self: *Window) *Navigator {
|
||||
return &self.navigator;
|
||||
/// Sets `onload_callback`.
|
||||
pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
|
||||
const event_target = parser.toEventTarget(Window, self);
|
||||
const event_type = "load";
|
||||
|
||||
// Check if we have a listener set.
|
||||
if (self.onload_callback) |callback| {
|
||||
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
|
||||
std.debug.assert(listener != null);
|
||||
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
|
||||
}
|
||||
|
||||
if (maybe_listener) |listener| {
|
||||
switch (listener) {
|
||||
// If an object is given as listener, do nothing.
|
||||
.object => {},
|
||||
.function => |callback| {
|
||||
_ = try EventHandler.register(page.arena, event_target, event_type, listener, null) orelse unreachable;
|
||||
self.onload_callback = callback;
|
||||
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Just unset the listener.
|
||||
self.onload_callback = null;
|
||||
}
|
||||
|
||||
pub fn get_location(self: *Window) *Location {
|
||||
@@ -118,22 +159,6 @@ pub const Window = struct {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn get_console(self: *Window) *Console {
|
||||
return &self.console;
|
||||
}
|
||||
|
||||
pub fn get_crypto(self: *Window) *Crypto {
|
||||
return &self.crypto;
|
||||
}
|
||||
|
||||
pub fn get_self(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn get_parent(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
|
||||
// frames return the window itself, but accessing it via a pseudo
|
||||
// array returns the Window object corresponding to the given frame or
|
||||
// iframe.
|
||||
@@ -171,16 +196,12 @@ pub const Window = struct {
|
||||
return frames.get_length();
|
||||
}
|
||||
|
||||
pub fn get_top(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn get_document(self: *Window) ?*parser.DocumentHTML {
|
||||
return self.document;
|
||||
}
|
||||
|
||||
pub fn get_history(self: *Window) *History {
|
||||
return &self.history;
|
||||
pub fn get_history(_: *Window, page: *Page) *History {
|
||||
return &page.session.history;
|
||||
}
|
||||
|
||||
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
|
||||
@@ -209,19 +230,11 @@ pub const Window = struct {
|
||||
return &self.storage_shelf.?.bucket.session;
|
||||
}
|
||||
|
||||
pub fn get_performance(self: *Window) *Performance {
|
||||
return &self.performance;
|
||||
}
|
||||
|
||||
pub fn get_screen(self: *Window) *Screen {
|
||||
return &self.screen;
|
||||
}
|
||||
|
||||
pub fn get_CSS(self: *Window) *Css {
|
||||
return &self.css;
|
||||
}
|
||||
|
||||
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
pub fn _requestAnimationFrame(self: *Window, cbk: js.Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 5, page, .{
|
||||
.animation_frame = true,
|
||||
.name = "animationFrame",
|
||||
@@ -233,11 +246,11 @@ pub const Window = struct {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
pub fn _setTimeout(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
|
||||
}
|
||||
|
||||
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
pub fn _setInterval(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
|
||||
}
|
||||
|
||||
@@ -249,11 +262,11 @@ pub const Window = struct {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
|
||||
}
|
||||
|
||||
pub fn _setImmediate(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
pub fn _setImmediate(self: *Window, cbk: js.Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 0, page, .{ .name = "setImmediate" });
|
||||
}
|
||||
|
||||
@@ -261,7 +274,7 @@ pub const Window = struct {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _matchMedia(_: *const Window, media: Env.String) !MediaQueryList {
|
||||
pub fn _matchMedia(_: *const Window, media: js.String) !MediaQueryList {
|
||||
return .{
|
||||
.matches = false, // TODO?
|
||||
.media = media.string,
|
||||
@@ -285,12 +298,12 @@ pub const Window = struct {
|
||||
|
||||
const CreateTimeoutOpts = struct {
|
||||
name: []const u8,
|
||||
args: []Env.JsObject = &.{},
|
||||
args: []js.Object = &.{},
|
||||
repeat: bool = false,
|
||||
animation_frame: bool = false,
|
||||
low_priority: bool = false,
|
||||
};
|
||||
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
|
||||
fn createTimeout(self: *Window, cbk: js.Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
|
||||
const delay = delay_ orelse 0;
|
||||
if (self.timers.count() > 512) {
|
||||
return error.TooManyTimeout;
|
||||
@@ -310,9 +323,9 @@ pub const Window = struct {
|
||||
errdefer _ = self.timers.remove(timer_id);
|
||||
|
||||
const args = opts.args;
|
||||
var persisted_args: []Env.JsObject = &.{};
|
||||
var persisted_args: []js.Object = &.{};
|
||||
if (args.len > 0) {
|
||||
persisted_args = try page.arena.alloc(Env.JsObject, args.len);
|
||||
persisted_args = try page.arena.alloc(js.Object, args.len);
|
||||
for (args, persisted_args) |a, *ca| {
|
||||
ca.* = try a.persist();
|
||||
}
|
||||
@@ -353,12 +366,20 @@ pub const Window = struct {
|
||||
const Opts = struct {
|
||||
top: i32,
|
||||
left: i32,
|
||||
behavior: []const u8,
|
||||
behavior: []const u8 = "",
|
||||
};
|
||||
};
|
||||
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?u32) !void {
|
||||
_ = opts;
|
||||
_ = y;
|
||||
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32) !void {
|
||||
switch (opts) {
|
||||
.x => |x| {
|
||||
self.scroll_x = @intCast(@max(x, 0));
|
||||
self.scroll_y = @intCast(@max(0, y orelse 0));
|
||||
},
|
||||
.opts => |o| {
|
||||
self.scroll_y = @intCast(@max(0, o.top));
|
||||
self.scroll_x = @intCast(@max(0, o.left));
|
||||
},
|
||||
}
|
||||
|
||||
{
|
||||
const scroll_event = try parser.eventCreate();
|
||||
@@ -382,6 +403,28 @@ pub const Window = struct {
|
||||
);
|
||||
}
|
||||
}
|
||||
pub fn _scroll(self: *Window, opts: ScrollToOpts, y: ?i32) !void {
|
||||
// just an alias for scrollTo
|
||||
return self._scrollTo(opts, y);
|
||||
}
|
||||
|
||||
pub fn get_scrollX(self: *const Window) u32 {
|
||||
return self.scroll_x;
|
||||
}
|
||||
|
||||
pub fn get_scrollY(self: *const Window) u32 {
|
||||
return self.scroll_y;
|
||||
}
|
||||
|
||||
pub fn get_pageXOffset(self: *const Window) u32 {
|
||||
// just an alias for scrollX
|
||||
return self.get_scrollX();
|
||||
}
|
||||
|
||||
pub fn get_pageYOffset(self: *const Window) u32 {
|
||||
// just an alias for scrollY
|
||||
return self.get_scrollY();
|
||||
}
|
||||
|
||||
// libdom's document doesn't have a parent, which is correct, but
|
||||
// breaks the event bubbling that happens for many events from
|
||||
@@ -399,6 +442,18 @@ pub const Window = struct {
|
||||
// and thus the target has already been set to the document.
|
||||
return self.base.redispatchEvent(evt);
|
||||
}
|
||||
|
||||
pub fn postAttach(self: *Window, js_this: js.This) !void {
|
||||
try js_this.set("top", self, .{});
|
||||
try js_this.set("self", self, .{});
|
||||
try js_this.set("parent", self, .{});
|
||||
try js_this.set("window", self, .{});
|
||||
try js_this.set("crypto", &self.crypto, .{});
|
||||
try js_this.set("screen", &self.screen, .{});
|
||||
try js_this.set("console", &self.console, .{});
|
||||
try js_this.set("navigator", &self.navigator, .{});
|
||||
try js_this.set("performance", &self.performance, .{});
|
||||
}
|
||||
};
|
||||
|
||||
const TimerCallback = struct {
|
||||
@@ -409,13 +464,13 @@ const TimerCallback = struct {
|
||||
repeat: ?u32,
|
||||
|
||||
// The JavaScript callback to execute
|
||||
cbk: Function,
|
||||
cbk: js.Function,
|
||||
|
||||
animation_frame: bool = false,
|
||||
|
||||
window: *Window,
|
||||
|
||||
args: []Env.JsObject = &.{},
|
||||
args: []js.Object = &.{},
|
||||
|
||||
fn run(ctx: *anyopaque) ?u32 {
|
||||
const self: *TimerCallback = @ptrCast(@alignCast(ctx));
|
||||
@@ -429,7 +484,7 @@ const TimerCallback = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
var result: Function.Result = undefined;
|
||||
var result: js.Function.Result = undefined;
|
||||
|
||||
var call: anyerror!void = undefined;
|
||||
if (self.animation_frame) {
|
||||
|
||||
561
src/browser/js/Caller.zig
Normal file
561
src/browser/js/Caller.zig
Normal file
@@ -0,0 +1,561 @@
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const types = @import("types.zig");
|
||||
const Context = @import("Context.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const CALL_ARENA_RETAIN = 1024 * 16;
|
||||
|
||||
// Responsible for calling Zig functions from JS invocations. This could
|
||||
// probably just contained in ExecutionWorld, but having this specific logic, which
|
||||
// is somewhat repetitive between constructors, functions, getters, etc contained
|
||||
// here does feel like it makes it cleaner.
|
||||
const Caller = @This();
|
||||
context: *Context,
|
||||
v8_context: v8.Context,
|
||||
isolate: v8.Isolate,
|
||||
call_arena: Allocator,
|
||||
|
||||
// info is a v8.PropertyCallbackInfo or a v8.FunctionCallback
|
||||
// All we really want from it is the isolate.
|
||||
// executor = Isolate -> getCurrentContext -> getEmbedderData()
|
||||
pub fn init(info: anytype) Caller {
|
||||
const isolate = info.getIsolate();
|
||||
const v8_context = isolate.getCurrentContext();
|
||||
const context: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
|
||||
|
||||
context.call_depth += 1;
|
||||
return .{
|
||||
.context = context,
|
||||
.isolate = isolate,
|
||||
.v8_context = v8_context,
|
||||
.call_arena = context.call_arena,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Caller) void {
|
||||
const context = self.context;
|
||||
const call_depth = context.call_depth - 1;
|
||||
|
||||
// Because of callbacks, calls can be nested. Because of this, we
|
||||
// can't clear the call_arena after _every_ call. Imagine we have
|
||||
// arr.forEach((i) => { console.log(i); }
|
||||
//
|
||||
// First we call forEach. Inside of our forEach call,
|
||||
// we call console.log. If we reset the call_arena after this call,
|
||||
// it'll reset it for the `forEach` call after, which might still
|
||||
// need the data.
|
||||
//
|
||||
// Therefore, we keep a call_depth, and only reset the call_arena
|
||||
// when a top-level (call_depth == 0) function ends.
|
||||
if (call_depth == 0) {
|
||||
const arena: *ArenaAllocator = @ptrCast(@alignCast(context.call_arena.ptr));
|
||||
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
|
||||
}
|
||||
|
||||
// Set this _after_ we've executed the above code, so that if the
|
||||
// above code executes any callbacks, they aren't being executed
|
||||
// at scope 0, which would be wrong.
|
||||
context.call_depth = call_depth;
|
||||
}
|
||||
|
||||
pub fn constructor(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
|
||||
const args = try self.getArgs(Struct, named_function, 0, info);
|
||||
const res = @call(.auto, Struct.constructor, args);
|
||||
|
||||
const ReturnType = @typeInfo(@TypeOf(Struct.constructor)).@"fn".return_type orelse {
|
||||
@compileError(@typeName(Struct) ++ " has a constructor without a return type");
|
||||
};
|
||||
|
||||
const this = info.getThis();
|
||||
if (@typeInfo(ReturnType) == .error_union) {
|
||||
const non_error_res = res catch |err| return err;
|
||||
_ = try self.context.mapZigInstanceToJs(this, non_error_res);
|
||||
} else {
|
||||
_ = try self.context.mapZigInstanceToJs(this, res);
|
||||
}
|
||||
info.getReturnValue().set(this);
|
||||
}
|
||||
|
||||
pub fn method(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
|
||||
if (comptime isSelfReceiver(Struct, named_function) == false) {
|
||||
return self.function(Struct, named_function, info);
|
||||
}
|
||||
|
||||
const context = self.context;
|
||||
const func = @field(Struct, named_function.name);
|
||||
var args = try self.getArgs(Struct, named_function, 1, info);
|
||||
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
|
||||
|
||||
// inject 'self' as the first parameter
|
||||
@field(args, "0") = zig_instance;
|
||||
|
||||
const res = @call(.auto, func, args);
|
||||
info.getReturnValue().set(try context.zigValueToJs(res));
|
||||
}
|
||||
|
||||
pub fn function(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
|
||||
const context = self.context;
|
||||
const func = @field(Struct, named_function.name);
|
||||
const args = try self.getArgs(Struct, named_function, 0, info);
|
||||
const res = @call(.auto, func, args);
|
||||
info.getReturnValue().set(try context.zigValueToJs(res));
|
||||
}
|
||||
|
||||
pub fn getIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, idx: u32, info: v8.PropertyCallbackInfo) !u8 {
|
||||
const context = self.context;
|
||||
const func = @field(Struct, named_function.name);
|
||||
const IndexedGet = @TypeOf(func);
|
||||
if (@typeInfo(IndexedGet).@"fn".return_type == null) {
|
||||
@compileError(named_function.full_name ++ " must have a return type");
|
||||
}
|
||||
|
||||
var has_value = true;
|
||||
|
||||
var args: ParamterTypes(IndexedGet) = undefined;
|
||||
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
|
||||
switch (arg_fields.len) {
|
||||
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
|
||||
3, 4 => {
|
||||
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
@field(args, "0") = zig_instance;
|
||||
@field(args, "1") = idx;
|
||||
@field(args, "2") = &has_value;
|
||||
if (comptime arg_fields.len == 4) {
|
||||
comptime assertIsPageArg(Struct, named_function, 3);
|
||||
@field(args, "3") = context.page;
|
||||
}
|
||||
},
|
||||
else => @compileError(named_function.full_name ++ " has too many parmaters"),
|
||||
}
|
||||
|
||||
const res = @call(.auto, func, args);
|
||||
if (has_value == false) {
|
||||
return v8.Intercepted.No;
|
||||
}
|
||||
info.getReturnValue().set(try context.zigValueToJs(res));
|
||||
return v8.Intercepted.Yes;
|
||||
}
|
||||
|
||||
pub fn getNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
|
||||
const context = self.context;
|
||||
const func = @field(Struct, named_function.name);
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
|
||||
var has_value = true;
|
||||
var args = try self.getArgs(Struct, named_function, 3, info);
|
||||
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
|
||||
@field(args, "0") = zig_instance;
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = &has_value;
|
||||
|
||||
const res = @call(.auto, func, args);
|
||||
if (has_value == false) {
|
||||
return v8.Intercepted.No;
|
||||
}
|
||||
info.getReturnValue().set(try self.context.zigValueToJs(res));
|
||||
return v8.Intercepted.Yes;
|
||||
}
|
||||
|
||||
pub fn setNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo) !u8 {
|
||||
const context = self.context;
|
||||
const func = @field(Struct, named_function.name);
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
|
||||
var has_value = true;
|
||||
var args = try self.getArgs(Struct, named_function, 4, info);
|
||||
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
|
||||
@field(args, "0") = zig_instance;
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = try context.jsValueToZig(named_function, @TypeOf(@field(args, "2")), js_value);
|
||||
@field(args, "3") = &has_value;
|
||||
|
||||
const res = @call(.auto, func, args);
|
||||
return namedSetOrDeleteCall(res, has_value);
|
||||
}
|
||||
|
||||
pub fn deleteNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
|
||||
const context = self.context;
|
||||
const func = @field(Struct, named_function.name);
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
|
||||
var has_value = true;
|
||||
var args = try self.getArgs(Struct, named_function, 3, info);
|
||||
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
|
||||
@field(args, "0") = zig_instance;
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = &has_value;
|
||||
|
||||
const res = @call(.auto, func, args);
|
||||
return namedSetOrDeleteCall(res, has_value);
|
||||
}
|
||||
|
||||
fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 {
|
||||
if (@typeInfo(@TypeOf(res)) == .error_union) {
|
||||
_ = try res;
|
||||
}
|
||||
if (has_value == false) {
|
||||
return v8.Intercepted.No;
|
||||
}
|
||||
return v8.Intercepted.Yes;
|
||||
}
|
||||
|
||||
fn nameToString(self: *Caller, name: v8.Name) ![]const u8 {
|
||||
return self.context.valueToString(.{ .handle = name.handle }, .{});
|
||||
}
|
||||
|
||||
fn isSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) bool {
|
||||
return checkSelfReceiver(Struct, named_function, false);
|
||||
}
|
||||
fn assertSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) void {
|
||||
_ = checkSelfReceiver(Struct, named_function, true);
|
||||
}
|
||||
fn checkSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction, comptime fail: bool) bool {
|
||||
const func = @field(Struct, named_function.name);
|
||||
const params = @typeInfo(@TypeOf(func)).@"fn".params;
|
||||
if (params.len == 0) {
|
||||
if (fail) {
|
||||
@compileError(named_function.full_name ++ " must have a self parameter");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const R = types.Receiver(Struct);
|
||||
const first_param = params[0].type.?;
|
||||
if (first_param != *R and first_param != *const R) {
|
||||
if (fail) {
|
||||
@compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{
|
||||
named_function.full_name,
|
||||
@typeName(R),
|
||||
@typeName(R),
|
||||
@typeName(first_param),
|
||||
}));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn assertIsPageArg(comptime Struct: type, comptime named_function: NamedFunction, index: comptime_int) void {
|
||||
const F = @TypeOf(@field(Struct, named_function.name));
|
||||
const param = @typeInfo(F).@"fn".params[index].type.?;
|
||||
if (isPage(param)) {
|
||||
return;
|
||||
}
|
||||
@compileError(std.fmt.comptimePrint("The {d} parameter to {s} must be a *Page or *const Page. Got: {s}", .{ index, named_function.full_name, @typeName(param) }));
|
||||
}
|
||||
|
||||
pub fn handleError(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, err: anyerror, info: anytype) void {
|
||||
const isolate = self.isolate;
|
||||
|
||||
if (comptime @import("builtin").mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
|
||||
if (log.enabled(.js, .warn)) {
|
||||
self.logFunctionCallError(err, named_function.full_name, info);
|
||||
}
|
||||
}
|
||||
|
||||
var js_err: ?v8.Value = switch (err) {
|
||||
error.InvalidArgument => createTypeException(isolate, "invalid argument"),
|
||||
error.OutOfMemory => js._createException(isolate, "out of memory"),
|
||||
error.IllegalConstructor => js._createException(isolate, "Illegal Contructor"),
|
||||
else => blk: {
|
||||
const func = @field(Struct, named_function.name);
|
||||
const return_type = @typeInfo(@TypeOf(func)).@"fn".return_type orelse {
|
||||
// void return type;
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
if (@typeInfo(return_type) != .error_union) {
|
||||
// type defines a custom exception, but this function should
|
||||
// not fail. We failed somewhere inside of js.zig and
|
||||
// should return the error as-is, since it isn't related
|
||||
// to our Struct
|
||||
break :blk null;
|
||||
}
|
||||
|
||||
const function_error_set = @typeInfo(return_type).error_union.error_set;
|
||||
|
||||
const E = comptime getCustomException(Struct) orelse break :blk null;
|
||||
if (function_error_set == E or isErrorSetException(E, err)) {
|
||||
const custom_exception = E.init(self.call_arena, err, named_function.js_name) catch |init_err| {
|
||||
switch (init_err) {
|
||||
// if a custom exceptions' init wants to return a
|
||||
// different error, we need to think about how to
|
||||
// handle that failure.
|
||||
error.OutOfMemory => break :blk js._createException(isolate, "out of memory"),
|
||||
}
|
||||
};
|
||||
// ughh..how to handle an error here?
|
||||
break :blk self.context.zigValueToJs(custom_exception) catch js._createException(isolate, "internal error");
|
||||
}
|
||||
// this error isn't part of a custom exception
|
||||
break :blk null;
|
||||
},
|
||||
};
|
||||
|
||||
if (js_err == null) {
|
||||
js_err = js._createException(isolate, @errorName(err));
|
||||
}
|
||||
const js_exception = isolate.throwException(js_err.?);
|
||||
info.getReturnValue().setValueHandle(js_exception.handle);
|
||||
}
|
||||
|
||||
// walk the prototype chain to see if a type declares a custom Exception
|
||||
fn getCustomException(comptime Struct: type) ?type {
|
||||
var S = Struct;
|
||||
while (true) {
|
||||
if (@hasDecl(S, "Exception")) {
|
||||
return S.Exception;
|
||||
}
|
||||
if (@hasDecl(S, "prototype") == false) {
|
||||
return null;
|
||||
}
|
||||
// long ago, we validated that every prototype declaration
|
||||
// is a pointer.
|
||||
S = @typeInfo(S.prototype).pointer.child;
|
||||
}
|
||||
}
|
||||
|
||||
// Does the error we want to return belong to the custom exeception's ErrorSet
|
||||
fn isErrorSetException(comptime E: type, err: anytype) bool {
|
||||
const Entry = std.meta.Tuple(&.{ []const u8, void });
|
||||
|
||||
const error_set = @typeInfo(E.ErrorSet).error_set.?;
|
||||
const entries = comptime blk: {
|
||||
var kv: [error_set.len]Entry = undefined;
|
||||
for (error_set, 0..) |e, i| {
|
||||
kv[i] = .{ e.name, {} };
|
||||
}
|
||||
break :blk kv;
|
||||
};
|
||||
const lookup = std.StaticStringMap(void).initComptime(entries);
|
||||
return lookup.has(@errorName(err));
|
||||
}
|
||||
|
||||
// If we call a method in javascript: cat.lives('nine');
|
||||
//
|
||||
// Then we'd expect a Zig function with 2 parameters: a self and the string.
|
||||
// In this case, offset == 1. Offset is always 1 for setters or methods.
|
||||
//
|
||||
// Offset is always 0 for constructors.
|
||||
//
|
||||
// For constructors, setters and methods, we can further increase offset + 1
|
||||
// if the first parameter is an instance of Page.
|
||||
//
|
||||
// Finally, if the JS function is called with _more_ parameters and
|
||||
// the last parameter in Zig is an array, we'll try to slurp the additional
|
||||
// parameters into the array.
|
||||
fn getArgs(self: *const Caller, comptime Struct: type, comptime named_function: NamedFunction, comptime offset: usize, info: anytype) !ParamterTypes(@TypeOf(@field(Struct, named_function.name))) {
|
||||
const context = self.context;
|
||||
const F = @TypeOf(@field(Struct, named_function.name));
|
||||
var args: ParamterTypes(F) = undefined;
|
||||
|
||||
const params = @typeInfo(F).@"fn".params[offset..];
|
||||
// Except for the constructor, the first parameter is always `self`
|
||||
// This isn't something we'll bind from JS, so skip it.
|
||||
const params_to_map = blk: {
|
||||
if (params.len == 0) {
|
||||
return args;
|
||||
}
|
||||
|
||||
// If the last parameter is the Page, set it, and exclude it
|
||||
// from our params slice, because we don't want to bind it to
|
||||
// a JS argument
|
||||
if (comptime isPage(params[params.len - 1].type.?)) {
|
||||
@field(args, tupleFieldName(params.len - 1 + offset)) = self.context.page;
|
||||
break :blk params[0 .. params.len - 1];
|
||||
}
|
||||
|
||||
// If the last parameter is a special JsThis, set it, and exclude it
|
||||
// from our params slice, because we don't want to bind it to
|
||||
// a JS argument
|
||||
if (comptime params[params.len - 1].type.? == js.This) {
|
||||
@field(args, tupleFieldName(params.len - 1 + offset)) = .{ .obj = .{
|
||||
.context = context,
|
||||
.js_obj = info.getThis(),
|
||||
} };
|
||||
|
||||
// AND the 2nd last parameter is state
|
||||
if (params.len > 1 and comptime isPage(params[params.len - 2].type.?)) {
|
||||
@field(args, tupleFieldName(params.len - 2 + offset)) = self.context.page;
|
||||
break :blk params[0 .. params.len - 2];
|
||||
}
|
||||
|
||||
break :blk params[0 .. params.len - 1];
|
||||
}
|
||||
|
||||
// we have neither a Page nor a JsObject. All params must be
|
||||
// bound to a JavaScript value.
|
||||
break :blk params;
|
||||
};
|
||||
|
||||
if (params_to_map.len == 0) {
|
||||
return args;
|
||||
}
|
||||
|
||||
const js_parameter_count = info.length();
|
||||
const last_js_parameter = params_to_map.len - 1;
|
||||
var is_variadic = false;
|
||||
|
||||
{
|
||||
// This is going to get complicated. If the last Zig parameter
|
||||
// is a slice AND the corresponding javascript parameter is
|
||||
// NOT an an array, then we'll treat it as a variadic.
|
||||
|
||||
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
|
||||
const last_parameter_type_info = @typeInfo(last_parameter_type);
|
||||
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
|
||||
const slice_type = last_parameter_type_info.pointer.child;
|
||||
const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter)));
|
||||
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
|
||||
is_variadic = true;
|
||||
if (js_parameter_count == 0) {
|
||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||
} else if (js_parameter_count >= params_to_map.len) {
|
||||
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
|
||||
for (arr, last_js_parameter..) |*a, i| {
|
||||
const js_value = info.getArg(@as(u32, @intCast(i)));
|
||||
a.* = try context.jsValueToZig(named_function, slice_type, js_value);
|
||||
}
|
||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
|
||||
} else {
|
||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline for (params_to_map, 0..) |param, i| {
|
||||
const field_index = comptime i + offset;
|
||||
if (comptime i == params_to_map.len - 1) {
|
||||
if (is_variadic) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (comptime isPage(param.type.?)) {
|
||||
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ named_function.full_name);
|
||||
} else if (comptime param.type.? == js.This) {
|
||||
@compileError("JsThis must be the last parameter: " ++ named_function.full_name);
|
||||
} else if (i >= js_parameter_count) {
|
||||
if (@typeInfo(param.type.?) != .optional) {
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
@field(args, tupleFieldName(field_index)) = null;
|
||||
} else {
|
||||
const js_value = info.getArg(@as(u32, @intCast(i)));
|
||||
@field(args, tupleFieldName(field_index)) = context.jsValueToZig(named_function, param.type.?, js_value) catch {
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
// This is extracted to speed up compilation. When left inlined in handleError,
|
||||
// this can add as much as 10 seconds of compilation time.
|
||||
fn logFunctionCallError(self: *Caller, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void {
|
||||
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
|
||||
log.info(.js, "function call error", .{
|
||||
.name = function_name,
|
||||
.err = err,
|
||||
.args = args_dump,
|
||||
.stack = self.context.stackTrace() catch |err1| @errorName(err1),
|
||||
});
|
||||
}
|
||||
|
||||
fn serializeFunctionArgs(self: *Caller, info: v8.FunctionCallbackInfo) ![]const u8 {
|
||||
const separator = log.separator();
|
||||
const js_parameter_count = info.length();
|
||||
|
||||
const context = self.context;
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
for (0..js_parameter_count) |i| {
|
||||
const js_value = info.getArg(@intCast(i));
|
||||
const value_string = try context.valueToDetailString(js_value);
|
||||
const value_type = try context.jsStringToZig(try js_value.typeOf(self.isolate), .{});
|
||||
try std.fmt.format(arr.writer(context.call_arena), "{s}{d}: {s} ({s})", .{
|
||||
separator,
|
||||
i + 1,
|
||||
value_string,
|
||||
value_type,
|
||||
});
|
||||
}
|
||||
return arr.items;
|
||||
}
|
||||
|
||||
// We want the function name, or more precisely, the "Struct.function" for
|
||||
// displaying helpful @compileError.
|
||||
// However, there's no way to get the name from a std.Builtin.Fn, so we create
|
||||
// a NamedFunction as part of our binding, and pass it around incase we need
|
||||
// to display an error
|
||||
pub const NamedFunction = struct {
|
||||
name: []const u8,
|
||||
js_name: []const u8,
|
||||
full_name: []const u8,
|
||||
|
||||
pub fn init(comptime Struct: type, comptime name: []const u8) NamedFunction {
|
||||
return .{
|
||||
.name = name,
|
||||
.js_name = if (name[0] == '_') name[1..] else name,
|
||||
.full_name = @typeName(Struct) ++ "." ++ name,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Takes a function, and returns a tuple for its argument. Used when we
|
||||
// @call a function
|
||||
fn ParamterTypes(comptime F: type) type {
|
||||
const params = @typeInfo(F).@"fn".params;
|
||||
var fields: [params.len]std.builtin.Type.StructField = undefined;
|
||||
|
||||
inline for (params, 0..) |param, i| {
|
||||
fields[i] = .{
|
||||
.name = tupleFieldName(i),
|
||||
.type = param.type.?,
|
||||
.default_value_ptr = null,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(param.type.?),
|
||||
};
|
||||
}
|
||||
|
||||
return @Type(.{ .@"struct" = .{
|
||||
.layout = .auto,
|
||||
.decls = &.{},
|
||||
.fields = &fields,
|
||||
.is_tuple = true,
|
||||
} });
|
||||
}
|
||||
|
||||
fn tupleFieldName(comptime i: usize) [:0]const u8 {
|
||||
return switch (i) {
|
||||
0 => "0",
|
||||
1 => "1",
|
||||
2 => "2",
|
||||
3 => "3",
|
||||
4 => "4",
|
||||
5 => "5",
|
||||
6 => "6",
|
||||
7 => "7",
|
||||
8 => "8",
|
||||
9 => "9",
|
||||
else => std.fmt.comptimePrint("{d}", .{i}),
|
||||
};
|
||||
}
|
||||
|
||||
fn isPage(comptime T: type) bool {
|
||||
return T == *Page or T == *const Page;
|
||||
}
|
||||
|
||||
fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value {
|
||||
return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg));
|
||||
}
|
||||
1875
src/browser/js/Context.zig
Normal file
1875
src/browser/js/Context.zig
Normal file
File diff suppressed because it is too large
Load Diff
539
src/browser/js/Env.zig
Normal file
539
src/browser/js/Env.zig
Normal file
@@ -0,0 +1,539 @@
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const types = @import("types.zig");
|
||||
const Types = types.Types;
|
||||
const Caller = @import("Caller.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const Platform = @import("Platform.zig");
|
||||
const Inspector = @import("Inspector.zig");
|
||||
const ExecutionWorld = @import("ExecutionWorld.zig");
|
||||
const NamedFunction = Caller.NamedFunction;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
// The Env maps to a V8 isolate, which represents a isolated sandbox for
|
||||
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
||||
// and it's where we'll start ExecutionWorlds, which actually execute JavaScript.
|
||||
// The `S` parameter is arbitrary state. When we start an ExecutionWorld, an instance
|
||||
// of S must be given. This instance is available to any Zig binding.
|
||||
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
|
||||
const Env = @This();
|
||||
|
||||
allocator: Allocator,
|
||||
|
||||
platform: *const Platform,
|
||||
|
||||
// the global isolate
|
||||
isolate: v8.Isolate,
|
||||
|
||||
// just kept around because we need to free it on deinit
|
||||
isolate_params: *v8.CreateParams,
|
||||
|
||||
// Given a type, we can lookup its index in TYPE_LOOKUP and then have
|
||||
// access to its TunctionTemplate (the thing we need to create an instance
|
||||
// of it)
|
||||
// I.e.:
|
||||
// const index = @field(TYPE_LOOKUP, @typeName(type_name))
|
||||
// const template = templates[index];
|
||||
templates: [Types.len]v8.FunctionTemplate,
|
||||
|
||||
// Given a type index (retrieved via the TYPE_LOOKUP), we can retrieve
|
||||
// the index of its prototype. Types without a prototype have their own
|
||||
// index.
|
||||
prototype_lookup: [Types.len]u16,
|
||||
|
||||
meta_lookup: [Types.len]types.Meta,
|
||||
|
||||
context_id: usize,
|
||||
|
||||
const Opts = struct {};
|
||||
|
||||
pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
|
||||
// var params = v8.initCreateParams();
|
||||
var params = try allocator.create(v8.CreateParams);
|
||||
errdefer allocator.destroy(params);
|
||||
|
||||
v8.c.v8__Isolate__CreateParams__CONSTRUCT(params);
|
||||
|
||||
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
|
||||
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
|
||||
|
||||
var isolate = v8.Isolate.init(params);
|
||||
errdefer isolate.deinit();
|
||||
|
||||
// This is the callback that runs whenever a module is dynamically imported.
|
||||
isolate.setHostImportModuleDynamicallyCallback(Context.dynamicModuleCallback);
|
||||
isolate.setPromiseRejectCallback(promiseRejectCallback);
|
||||
isolate.setMicrotasksPolicy(v8.c.kExplicit);
|
||||
|
||||
isolate.enter();
|
||||
errdefer isolate.exit();
|
||||
|
||||
isolate.setHostInitializeImportMetaObjectCallback(Context.metaObjectCallback);
|
||||
|
||||
var temp_scope: v8.HandleScope = undefined;
|
||||
v8.HandleScope.init(&temp_scope, isolate);
|
||||
defer temp_scope.deinit();
|
||||
|
||||
const env = try allocator.create(Env);
|
||||
errdefer allocator.destroy(env);
|
||||
|
||||
env.* = .{
|
||||
.context_id = 0,
|
||||
.platform = platform,
|
||||
.isolate = isolate,
|
||||
.templates = undefined,
|
||||
.allocator = allocator,
|
||||
.isolate_params = params,
|
||||
.meta_lookup = undefined,
|
||||
.prototype_lookup = undefined,
|
||||
};
|
||||
|
||||
// Populate our templates lookup. generateClass creates the
|
||||
// v8.FunctionTemplate, which we store in our env.templates.
|
||||
// The ordering doesn't matter. What matters is that, given a type
|
||||
// we can get its index via: @field(types.LOOKUP, type_name)
|
||||
const templates = &env.templates;
|
||||
inline for (Types, 0..) |s, i| {
|
||||
@setEvalBranchQuota(10_000);
|
||||
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, generateClass(s.defaultValue().?, isolate)).castToFunctionTemplate();
|
||||
}
|
||||
|
||||
// Above, we've created all our our FunctionTemplates. Now that we
|
||||
// have them all, we can hook up the prototypes.
|
||||
const meta_lookup = &env.meta_lookup;
|
||||
inline for (Types, 0..) |s, i| {
|
||||
const Struct = s.defaultValue().?;
|
||||
if (@hasDecl(Struct, "prototype")) {
|
||||
const TI = @typeInfo(Struct.prototype);
|
||||
const proto_name = @typeName(types.Receiver(TI.pointer.child));
|
||||
if (@hasField(types.Lookup, proto_name) == false) {
|
||||
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ proto_name, @typeName(Struct) }));
|
||||
}
|
||||
// Hey, look! This is our first real usage of the types.LOOKUP.
|
||||
// Just like we said above, given a type, we can get its
|
||||
// template index.
|
||||
|
||||
const proto_index = @field(types.LOOKUP, proto_name);
|
||||
templates[i].inherit(templates[proto_index]);
|
||||
}
|
||||
|
||||
// while we're here, let's populate our meta lookup
|
||||
const subtype: ?types.Sub = if (@hasDecl(Struct, "subtype")) Struct.subtype else null;
|
||||
|
||||
const proto_offset = comptime blk: {
|
||||
if (!@hasField(Struct, "proto")) {
|
||||
break :blk 0;
|
||||
}
|
||||
const proto_info = std.meta.fieldInfo(Struct, .proto);
|
||||
if (@typeInfo(proto_info.type) == .pointer) {
|
||||
// we store the offset as a negative, to so that,
|
||||
// when we reverse this, we know that it's
|
||||
// behind a pointer that we need to resolve.
|
||||
break :blk -@offsetOf(Struct, "proto");
|
||||
}
|
||||
break :blk @offsetOf(Struct, "proto");
|
||||
};
|
||||
|
||||
meta_lookup[i] = .{
|
||||
.index = i,
|
||||
.subtype = subtype,
|
||||
.proto_offset = proto_offset,
|
||||
};
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Env) void {
|
||||
self.isolate.exit();
|
||||
self.isolate.deinit();
|
||||
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
|
||||
self.allocator.destroy(self.isolate_params);
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector {
|
||||
return Inspector.init(arena, self.isolate, ctx);
|
||||
}
|
||||
|
||||
pub fn runMicrotasks(self: *const Env) void {
|
||||
self.isolate.performMicrotasksCheckpoint();
|
||||
}
|
||||
|
||||
pub fn pumpMessageLoop(self: *const Env) bool {
|
||||
return self.platform.inner.pumpMessageLoop(self.isolate, false);
|
||||
}
|
||||
|
||||
pub fn runIdleTasks(self: *const Env) void {
|
||||
return self.platform.inner.runIdleTasks(self.isolate, 1);
|
||||
}
|
||||
|
||||
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
|
||||
return .{
|
||||
.env = self,
|
||||
.context = null,
|
||||
.context_arena = ArenaAllocator.init(self.allocator),
|
||||
};
|
||||
}
|
||||
|
||||
// V8 doesn't immediately free memory associated with
|
||||
// a Context, it's managed by the garbage collector. We use the
|
||||
// `lowMemoryNotification` call on the isolate to encourage v8 to free
|
||||
// any contexts which have been freed.
|
||||
pub fn lowMemoryNotification(self: *Env) void {
|
||||
var handle_scope: v8.HandleScope = undefined;
|
||||
v8.HandleScope.init(&handle_scope, self.isolate);
|
||||
defer handle_scope.deinit();
|
||||
self.isolate.lowMemoryNotification();
|
||||
}
|
||||
|
||||
pub fn dumpMemoryStats(self: *Env) void {
|
||||
const stats = self.isolate.getHeapStatistics();
|
||||
std.debug.print(
|
||||
\\ Total Heap Size: {d}
|
||||
\\ Total Heap Size Executable: {d}
|
||||
\\ Total Physical Size: {d}
|
||||
\\ Total Available Size: {d}
|
||||
\\ Used Heap Size: {d}
|
||||
\\ Heap Size Limit: {d}
|
||||
\\ Malloced Memory: {d}
|
||||
\\ External Memory: {d}
|
||||
\\ Peak Malloced Memory: {d}
|
||||
\\ Number Of Native Contexts: {d}
|
||||
\\ Number Of Detached Contexts: {d}
|
||||
\\ Total Global Handles Size: {d}
|
||||
\\ Used Global Handles Size: {d}
|
||||
\\ Zap Garbage: {any}
|
||||
\\
|
||||
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
|
||||
}
|
||||
|
||||
fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
|
||||
const msg = v8.PromiseRejectMessage.initFromC(v8_msg);
|
||||
const isolate = msg.getPromise().toObject().getIsolate();
|
||||
const context = Context.fromIsolate(isolate);
|
||||
|
||||
const value =
|
||||
if (msg.getValue()) |v8_value| context.valueToString(v8_value, .{}) catch |err| @errorName(err) else "no value";
|
||||
|
||||
log.debug(.js, "unhandled rejection", .{ .value = value });
|
||||
}
|
||||
|
||||
// Give it a Zig struct, get back a v8.FunctionTemplate.
|
||||
// The FunctionTemplate is a bit like a struct container - it's where
|
||||
// we'll attach functions/getters/setters and where we'll "inherit" a
|
||||
// prototype type (if there is any)
|
||||
fn generateClass(comptime Struct: type, isolate: v8.Isolate) v8.FunctionTemplate {
|
||||
const template = generateConstructor(Struct, isolate);
|
||||
attachClass(Struct, isolate, template);
|
||||
return template;
|
||||
}
|
||||
|
||||
// Normally this is called from generateClass. Where generateClass creates
|
||||
// the constructor (hence, the FunctionTemplate), attachClass adds all
|
||||
// of its functions, getters, setters, ...
|
||||
// But it's extracted from generateClass because we also have 1 global
|
||||
// object (i.e. the Window), which gets attached not only to the Window
|
||||
// constructor/FunctionTemplate as normal, but also through the default
|
||||
// FunctionTemplate of the isolate (in createContext)
|
||||
pub fn attachClass(comptime Struct: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
|
||||
const template_proto = template.getPrototypeTemplate();
|
||||
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
|
||||
const name = declaration.name;
|
||||
if (comptime name[0] == '_') {
|
||||
switch (@typeInfo(@TypeOf(@field(Struct, name)))) {
|
||||
.@"fn" => generateMethod(Struct, name, isolate, template_proto),
|
||||
else => |ti| if (!comptime js.isComplexAttributeType(ti)) {
|
||||
generateAttribute(Struct, name, isolate, template, template_proto);
|
||||
},
|
||||
}
|
||||
} else if (comptime std.mem.startsWith(u8, name, "get_")) {
|
||||
generateProperty(Struct, name[4..], isolate, template_proto);
|
||||
} else if (comptime std.mem.startsWith(u8, name, "static_")) {
|
||||
generateFunction(Struct, name[7..], isolate, template);
|
||||
}
|
||||
}
|
||||
|
||||
if (@hasDecl(Struct, "get_symbol_toStringTag") == false) {
|
||||
// If this WAS defined, then we would have created it in generateProperty.
|
||||
// But if it isn't, we create a default one
|
||||
const string_tag_callback = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||
fn stringTag(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
const class_name = v8.String.initUtf8(info.getIsolate(), comptime js.classNameForStruct(Struct));
|
||||
info.getReturnValue().set(class_name);
|
||||
}
|
||||
}.stringTag);
|
||||
const key = v8.Symbol.getToStringTag(isolate).toName();
|
||||
template_proto.setAccessorGetter(key, string_tag_callback);
|
||||
}
|
||||
|
||||
generateIndexer(Struct, template_proto);
|
||||
generateNamedIndexer(Struct, template.getInstanceTemplate());
|
||||
generateUndetectable(Struct, template.getInstanceTemplate());
|
||||
}
|
||||
|
||||
// Even if a struct doesn't have a `constructor` function, we still
|
||||
// `generateConstructor`, because this is how we create our
|
||||
// FunctionTemplate. Such classes exist, but they can't be instantiated
|
||||
// via `new ClassName()` - but they could, for example, be created in
|
||||
// Zig and returned from a function call, which is why we need the
|
||||
// FunctionTemplate.
|
||||
fn generateConstructor(comptime Struct: type, isolate: v8.Isolate) v8.FunctionTemplate {
|
||||
const template = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
// See comment above. We generateConstructor on all types
|
||||
// in order to create the FunctionTemplate, but there might
|
||||
// not be an actual "constructor" function. So if someone
|
||||
// does `new ClassName()` where ClassName doesn't have
|
||||
// a constructor function, we'll return an error.
|
||||
if (@hasDecl(Struct, "constructor") == false) {
|
||||
const iso = caller.isolate;
|
||||
log.warn(.js, "Illegal constructor call", .{ .name = @typeName(Struct) });
|
||||
const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
|
||||
info.getReturnValue().set(js_exception);
|
||||
return;
|
||||
}
|
||||
|
||||
// Safe to call now, because if Struct.constructor didn't
|
||||
// exist, the above if block would have returned.
|
||||
const named_function = comptime NamedFunction.init(Struct, "constructor");
|
||||
caller.constructor(Struct, named_function, info) catch |err| {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
};
|
||||
}
|
||||
}.callback);
|
||||
|
||||
if (comptime types.isEmpty(types.Receiver(Struct)) == false) {
|
||||
// If the struct is empty, we won't store a Zig reference inside
|
||||
// the JS object, so we don't need to set the internal field count
|
||||
template.getInstanceTemplate().setInternalFieldCount(1);
|
||||
}
|
||||
|
||||
const class_name = v8.String.initUtf8(isolate, comptime js.classNameForStruct(Struct));
|
||||
template.setClassName(class_name);
|
||||
return template;
|
||||
}
|
||||
|
||||
fn generateMethod(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void {
|
||||
var js_name: v8.Name = undefined;
|
||||
if (comptime std.mem.eql(u8, name, "_symbol_iterator")) {
|
||||
js_name = v8.Symbol.getIterator(isolate).toName();
|
||||
} else {
|
||||
js_name = v8.String.initUtf8(isolate, name[1..]).toName();
|
||||
}
|
||||
const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, name);
|
||||
caller.method(Struct, named_function, info) catch |err| {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
};
|
||||
}
|
||||
}.callback);
|
||||
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
|
||||
}
|
||||
|
||||
fn generateFunction(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
|
||||
const js_name = v8.String.initUtf8(isolate, name).toName();
|
||||
const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "static_" ++ name);
|
||||
caller.function(Struct, named_function, info) catch |err| {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
};
|
||||
}
|
||||
}.callback);
|
||||
template.set(js_name, function_template, v8.PropertyAttribute.None);
|
||||
}
|
||||
|
||||
fn generateAttribute(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void {
|
||||
const zig_value = @field(Struct, name);
|
||||
const js_value = js.simpleZigValueToJs(isolate, zig_value, true);
|
||||
|
||||
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
|
||||
|
||||
// apply it both to the type itself
|
||||
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
||||
|
||||
// and to instances of the type
|
||||
template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
||||
}
|
||||
|
||||
fn generateProperty(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void {
|
||||
var js_name: v8.Name = undefined;
|
||||
if (comptime std.mem.eql(u8, name, "symbol_toStringTag")) {
|
||||
js_name = v8.Symbol.getToStringTag(isolate).toName();
|
||||
} else {
|
||||
js_name = v8.String.initUtf8(isolate, name).toName();
|
||||
}
|
||||
|
||||
const getter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "get_" ++ name);
|
||||
caller.method(Struct, named_function, info) catch |err| {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
};
|
||||
}
|
||||
}.callback);
|
||||
|
||||
const setter_name = "set_" ++ name;
|
||||
if (@hasDecl(Struct, setter_name) == false) {
|
||||
template_proto.setAccessorGetter(js_name, getter_callback);
|
||||
return;
|
||||
}
|
||||
|
||||
const setter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
std.debug.assert(info.length() == 1);
|
||||
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "set_" ++ name);
|
||||
caller.method(Struct, named_function, info) catch |err| {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
};
|
||||
}
|
||||
}.callback);
|
||||
|
||||
template_proto.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
|
||||
}
|
||||
|
||||
fn generateIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
|
||||
if (@hasDecl(Struct, "indexed_get") == false) {
|
||||
return;
|
||||
}
|
||||
const configuration = v8.IndexedPropertyHandlerConfiguration{
|
||||
.getter = struct {
|
||||
fn callback(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "indexed_get");
|
||||
return caller.getIndex(Struct, named_function, idx, info) catch |err| blk: {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
break :blk v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
}.callback,
|
||||
};
|
||||
|
||||
// If you're trying to implement setter, read:
|
||||
// https://groups.google.com/g/v8-users/c/8tahYBsHpgY/m/IteS7Wn2AAAJ
|
||||
// The issue I had was
|
||||
// (a) where to attache it: does it go on the instance_template
|
||||
// instead of the prototype?
|
||||
// (b) defining the getter or query to respond with the
|
||||
// PropertyAttribute to indicate if the property can be set
|
||||
template_proto.setIndexedProperty(configuration, null);
|
||||
}
|
||||
|
||||
fn generateNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
|
||||
if (@hasDecl(Struct, "named_get") == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
var configuration = v8.NamedPropertyHandlerConfiguration{
|
||||
.getter = struct {
|
||||
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "named_get");
|
||||
return caller.getNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
break :blk v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
}.callback,
|
||||
|
||||
// This is really cool. Without this, we'd intercept _all_ properties
|
||||
// even those explicitly set. So, node.length for example would get routed
|
||||
// to our `named_get`, rather than a `get_length`. This might be
|
||||
// useful if we run into a type that we can't model properly in Zig.
|
||||
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
|
||||
};
|
||||
|
||||
if (@hasDecl(Struct, "named_set")) {
|
||||
configuration.setter = struct {
|
||||
fn callback(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "named_set");
|
||||
return caller.setNamedIndex(Struct, named_function, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info) catch |err| blk: {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
break :blk v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
}.callback;
|
||||
}
|
||||
|
||||
if (@hasDecl(Struct, "named_delete")) {
|
||||
configuration.deleter = struct {
|
||||
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "named_delete");
|
||||
return caller.deleteNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
break :blk v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
}.callback;
|
||||
}
|
||||
template_proto.setNamedProperty(configuration, null);
|
||||
}
|
||||
|
||||
fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void {
|
||||
const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction");
|
||||
|
||||
if (has_js_call_as_function) {
|
||||
template.setCallAsFunctionHandler(struct {
|
||||
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "jsCallAsFunction");
|
||||
caller.method(Struct, named_function, info) catch |err| {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
};
|
||||
}
|
||||
}.callback);
|
||||
}
|
||||
|
||||
if (@hasDecl(Struct, "mark_as_undetectable") and Struct.mark_as_undetectable) {
|
||||
if (!has_js_call_as_function) {
|
||||
@compileError(@typeName(Struct) ++ ": mark_as_undetectable required jsCallAsFunction to be defined. This is a hard-coded requirement in V8, because mark_as_undetectable only exists for HTMLAllCollection which is also callable.");
|
||||
}
|
||||
template.markAsUndetectable();
|
||||
}
|
||||
}
|
||||
251
src/browser/js/ExecutionWorld.zig
Normal file
251
src/browser/js/ExecutionWorld.zig
Normal file
@@ -0,0 +1,251 @@
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const ScriptManager = @import("../ScriptManager.zig");
|
||||
|
||||
const types = @import("types.zig");
|
||||
const Types = types.Types;
|
||||
const Env = @import("Env.zig");
|
||||
const Context = @import("Context.zig");
|
||||
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const CONTEXT_ARENA_RETAIN = 1024 * 64;
|
||||
|
||||
// ExecutionWorld closely models a JS World.
|
||||
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#World
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
|
||||
const ExecutionWorld = @This();
|
||||
env: *Env,
|
||||
|
||||
// Arena whose lifetime is for a single page load. Where
|
||||
// the call_arena lives for a single function call, the context_arena
|
||||
// lives for the lifetime of the entire page. The allocator will be
|
||||
// owned by the Context, but the arena itself is owned by the ExecutionWorld
|
||||
// so that we can re-use it from context to context.
|
||||
context_arena: ArenaAllocator,
|
||||
|
||||
// Currently a context maps to a Browser's Page. Here though, it's only a
|
||||
// mechanism to organization page-specific memory. The ExecutionWorld
|
||||
// does all the work, but having all page-specific data structures
|
||||
// grouped together helps keep things clean.
|
||||
context: ?Context = null,
|
||||
|
||||
// no init, must be initialized via env.newExecutionWorld()
|
||||
|
||||
pub fn deinit(self: *ExecutionWorld) void {
|
||||
if (self.context != null) {
|
||||
self.removeContext();
|
||||
}
|
||||
|
||||
self.context_arena.deinit();
|
||||
}
|
||||
|
||||
// Only the top Context in the Main ExecutionWorld should hold a handle_scope.
|
||||
// A v8.HandleScope is like an arena. Once created, any "Local" that
|
||||
// v8 creates will be released (or at least, releasable by the v8 GC)
|
||||
// when the handle_scope is freed.
|
||||
// We also maintain our own "context_arena" which allows us to have
|
||||
// all page related memory easily managed.
|
||||
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_callback: ?js.GlobalMissingCallback) !*Context {
|
||||
std.debug.assert(self.context == null);
|
||||
|
||||
const env = self.env;
|
||||
const isolate = env.isolate;
|
||||
const Global = @TypeOf(page.window);
|
||||
const templates = &self.env.templates;
|
||||
|
||||
var v8_context: v8.Context = blk: {
|
||||
var temp_scope: v8.HandleScope = undefined;
|
||||
v8.HandleScope.init(&temp_scope, isolate);
|
||||
defer temp_scope.deinit();
|
||||
|
||||
const js_global = v8.FunctionTemplate.initDefault(isolate);
|
||||
Env.attachClass(Global, isolate, js_global);
|
||||
|
||||
const global_template = js_global.getInstanceTemplate();
|
||||
global_template.setInternalFieldCount(1);
|
||||
|
||||
// Configure the missing property callback on the global
|
||||
// object.
|
||||
if (global_callback != null) {
|
||||
const configuration = v8.NamedPropertyHandlerConfiguration{
|
||||
.getter = struct {
|
||||
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
const context = Context.fromIsolate(info.getIsolate());
|
||||
|
||||
const property = context.valueToString(.{ .handle = c_name.? }, .{}) catch "???";
|
||||
if (context.global_callback.?.missing(property, context)) {
|
||||
return v8.Intercepted.Yes;
|
||||
}
|
||||
return v8.Intercepted.No;
|
||||
}
|
||||
}.callback,
|
||||
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
|
||||
};
|
||||
global_template.setNamedProperty(configuration, null);
|
||||
}
|
||||
|
||||
// All the FunctionTemplates that we created and setup in Env.init
|
||||
// are now going to get associated with our global instance.
|
||||
inline for (Types, 0..) |s, i| {
|
||||
const Struct = s.defaultValue().?;
|
||||
const class_name = v8.String.initUtf8(isolate, comptime js.classNameForStruct(Struct));
|
||||
global_template.set(class_name.toName(), templates[i], v8.PropertyAttribute.None);
|
||||
}
|
||||
|
||||
// The global object (Window) has already been hooked into the v8
|
||||
// engine when the Env was initialized - like every other type.
|
||||
// But the V8 global is its own FunctionTemplate instance so even
|
||||
// though it's also a Window, we need to set the prototype for this
|
||||
// specific instance of the the Window.
|
||||
if (@hasDecl(Global, "prototype")) {
|
||||
const proto_type = types.Receiver(@typeInfo(Global.prototype).pointer.child);
|
||||
const proto_name = @typeName(proto_type);
|
||||
const proto_index = @field(types.LOOKUP, proto_name);
|
||||
js_global.inherit(templates[proto_index]);
|
||||
}
|
||||
|
||||
const context_local = v8.Context.init(isolate, global_template, null);
|
||||
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
|
||||
v8_context.enter();
|
||||
errdefer if (enter) v8_context.exit();
|
||||
defer if (!enter) v8_context.exit();
|
||||
|
||||
// This shouldn't be necessary, but it is:
|
||||
// https://groups.google.com/g/v8-users/c/qAQQBmbi--8
|
||||
// TODO: see if newer V8 engines have a way around this.
|
||||
inline for (Types, 0..) |s, i| {
|
||||
const Struct = s.defaultValue().?;
|
||||
|
||||
if (@hasDecl(Struct, "prototype")) {
|
||||
const proto_type = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
|
||||
const proto_name = @typeName(proto_type);
|
||||
if (@hasField(types.Lookup, proto_name) == false) {
|
||||
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name);
|
||||
}
|
||||
|
||||
const proto_index = @field(types.LOOKUP, proto_name);
|
||||
const proto_obj = templates[proto_index].getFunction(v8_context).toObject();
|
||||
|
||||
const self_obj = templates[i].getFunction(v8_context).toObject();
|
||||
_ = self_obj.setPrototype(v8_context, proto_obj);
|
||||
}
|
||||
}
|
||||
break :blk v8_context;
|
||||
};
|
||||
|
||||
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
|
||||
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
|
||||
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
|
||||
var handle_scope: ?v8.HandleScope = null;
|
||||
if (enter) {
|
||||
handle_scope = @as(v8.HandleScope, undefined);
|
||||
v8.HandleScope.init(&handle_scope.?, isolate);
|
||||
}
|
||||
errdefer if (enter) handle_scope.?.deinit();
|
||||
|
||||
{
|
||||
// If we want to overwrite the built-in console, we have to
|
||||
// delete the built-in one.
|
||||
const js_obj = v8_context.getGlobal();
|
||||
const console_key = v8.String.initUtf8(isolate, "console");
|
||||
if (js_obj.deleteValue(v8_context, console_key) == false) {
|
||||
return error.ConsoleDeleteError;
|
||||
}
|
||||
}
|
||||
const context_id = env.context_id;
|
||||
env.context_id = context_id + 1;
|
||||
|
||||
self.context = Context{
|
||||
.page = page,
|
||||
.id = context_id,
|
||||
.isolate = isolate,
|
||||
.v8_context = v8_context,
|
||||
.templates = &env.templates,
|
||||
.meta_lookup = &env.meta_lookup,
|
||||
.handle_scope = handle_scope,
|
||||
.script_manager = &page.script_manager,
|
||||
.call_arena = page.call_arena,
|
||||
.arena = self.context_arena.allocator(),
|
||||
.global_callback = global_callback,
|
||||
};
|
||||
|
||||
var context = &self.context.?;
|
||||
{
|
||||
// Store a pointer to our context inside the v8 context so that, given
|
||||
// a v8 context, we can get our context out
|
||||
const data = isolate.initBigIntU64(@intCast(@intFromPtr(context)));
|
||||
v8_context.setEmbedderData(1, data);
|
||||
}
|
||||
|
||||
// Custom exception
|
||||
// NOTE: there is no way in v8 to subclass the Error built-in type
|
||||
// TODO: this is an horrible hack
|
||||
inline for (Types) |s| {
|
||||
const Struct = s.defaultValue().?;
|
||||
if (@hasDecl(Struct, "ErrorSet")) {
|
||||
const script = comptime js.classNameForStruct(Struct) ++ ".prototype.__proto__ = Error.prototype";
|
||||
_ = try context.exec(script, "errorSubclass");
|
||||
}
|
||||
}
|
||||
|
||||
// Primitive attributes are set directly on the FunctionTemplate
|
||||
// when we setup the environment. But we cannot set more complex
|
||||
// types (v8 will crash).
|
||||
//
|
||||
// Plus, just to create more complex types, we always need a
|
||||
// context, i.e. an Array has to have a Context to exist.
|
||||
//
|
||||
// As far as I can tell, getting the FunctionTemplate's object
|
||||
// and setting values directly on it, for each context, is the
|
||||
// way to do this.
|
||||
inline for (Types, 0..) |s, i| {
|
||||
const Struct = s.defaultValue().?;
|
||||
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
|
||||
const name = declaration.name;
|
||||
if (comptime name[0] == '_') {
|
||||
const value = @field(Struct, name);
|
||||
|
||||
if (comptime js.isComplexAttributeType(@typeInfo(@TypeOf(value)))) {
|
||||
const js_obj = templates[i].getFunction(v8_context).toObject();
|
||||
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
|
||||
const js_val = try context.zigValueToJs(value);
|
||||
if (!js_obj.setValue(v8_context, js_name, js_val)) {
|
||||
log.fatal(.app, "set class attribute", .{
|
||||
.@"struct" = @typeName(Struct),
|
||||
.name = name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try context.setupGlobal();
|
||||
return context;
|
||||
}
|
||||
|
||||
pub fn removeContext(self: *ExecutionWorld) void {
|
||||
// Force running the micro task to drain the queue before reseting the
|
||||
// context arena.
|
||||
// Tasks in the queue are relying to the arena memory could be present in
|
||||
// the queue. Running them later could lead to invalid memory accesses.
|
||||
self.env.runMicrotasks();
|
||||
|
||||
self.context.?.deinit();
|
||||
self.context = null;
|
||||
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
|
||||
}
|
||||
|
||||
pub fn terminateExecution(self: *const ExecutionWorld) void {
|
||||
self.env.isolate.terminateExecution();
|
||||
}
|
||||
|
||||
pub fn resumeExecution(self: *const ExecutionWorld) void {
|
||||
self.env.isolate.cancelTerminateExecution();
|
||||
}
|
||||
144
src/browser/js/Function.zig
Normal file
144
src/browser/js/Function.zig
Normal file
@@ -0,0 +1,144 @@
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Caller = @import("Caller.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const PersistentFunction = v8.Persistent(v8.Function);
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Function = @This();
|
||||
|
||||
id: usize,
|
||||
context: *js.Context,
|
||||
this: ?v8.Object = null,
|
||||
func: PersistentFunction,
|
||||
|
||||
pub const Result = struct {
|
||||
stack: ?[]const u8,
|
||||
exception: []const u8,
|
||||
};
|
||||
|
||||
pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 {
|
||||
const name = self.func.castToFunction().getName();
|
||||
return self.context.valueToString(name, .{ .allocator = allocator });
|
||||
}
|
||||
|
||||
pub fn setName(self: *const Function, name: []const u8) void {
|
||||
const v8_name = v8.String.initUtf8(self.context.isolate, name);
|
||||
self.func.castToFunction().setName(v8_name);
|
||||
}
|
||||
|
||||
pub fn withThis(self: *const Function, value: anytype) !Function {
|
||||
const this_obj = if (@TypeOf(value) == js.Object)
|
||||
value.js_obj
|
||||
else
|
||||
(try self.context.zigValueToJs(value)).castTo(v8.Object);
|
||||
|
||||
return .{
|
||||
.id = self.id,
|
||||
.this = this_obj,
|
||||
.func = self.func,
|
||||
.context = self.context,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn newInstance(self: *const Function, result: *Result) !js.Object {
|
||||
const context = self.context;
|
||||
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
// This creates a new instance using this Function as a constructor.
|
||||
// This returns a generic Object
|
||||
const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse {
|
||||
if (try_catch.hasCaught()) {
|
||||
const allocator = context.call_arena;
|
||||
result.stack = try_catch.stack(allocator) catch null;
|
||||
result.exception = (try_catch.exception(allocator) catch "???") orelse "???";
|
||||
} else {
|
||||
result.stack = null;
|
||||
result.exception = "???";
|
||||
}
|
||||
return error.JsConstructorFailed;
|
||||
};
|
||||
|
||||
return .{
|
||||
.context = context,
|
||||
.js_obj = js_obj,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
||||
return self.callWithThis(T, self.getThis(), args);
|
||||
}
|
||||
|
||||
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T {
|
||||
return self.tryCallWithThis(T, self.getThis(), args, result);
|
||||
}
|
||||
|
||||
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !T {
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(self.context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
return self.callWithThis(T, this, args) catch |err| {
|
||||
if (try_catch.hasCaught()) {
|
||||
const allocator = self.context.call_arena;
|
||||
result.stack = try_catch.stack(allocator) catch null;
|
||||
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
|
||||
} else {
|
||||
result.stack = null;
|
||||
result.exception = @errorName(err);
|
||||
}
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
||||
const context = self.context;
|
||||
|
||||
const js_this = try context.valueToExistingObject(this);
|
||||
|
||||
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
|
||||
|
||||
const js_args: []const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
|
||||
.@"struct" => |s| blk: {
|
||||
const fields = s.fields;
|
||||
var js_args: [fields.len]v8.Value = undefined;
|
||||
inline for (fields, 0..) |f, i| {
|
||||
js_args[i] = try context.zigValueToJs(@field(aargs, f.name));
|
||||
}
|
||||
const cargs: [fields.len]v8.Value = js_args;
|
||||
break :blk &cargs;
|
||||
},
|
||||
.pointer => blk: {
|
||||
var values = try context.call_arena.alloc(v8.Value, args.len);
|
||||
for (args, 0..) |a, i| {
|
||||
values[i] = try context.zigValueToJs(a);
|
||||
}
|
||||
break :blk values;
|
||||
},
|
||||
else => @compileError("JS Function called with invalid paremter type"),
|
||||
};
|
||||
|
||||
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
|
||||
if (result == null) {
|
||||
return error.JSExecCallback;
|
||||
}
|
||||
|
||||
if (@typeInfo(T) == .void) return {};
|
||||
const named_function = comptime Caller.NamedFunction.init(T, "callResult");
|
||||
return context.jsValueToZig(named_function, T, result.?);
|
||||
}
|
||||
|
||||
fn getThis(self: *const Function) v8.Object {
|
||||
return self.this orelse self.context.v8_context.getGlobal();
|
||||
}
|
||||
|
||||
pub fn src(self: *const Function) ![]const u8 {
|
||||
const value = self.func.castToFunction().toValue();
|
||||
return self.context.valueToString(value, .{});
|
||||
}
|
||||
125
src/browser/js/Inspector.zig
Normal file
125
src/browser/js/Inspector.zig
Normal file
@@ -0,0 +1,125 @@
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Context = @import("Context.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Inspector = @This();
|
||||
|
||||
pub const RemoteObject = v8.RemoteObject;
|
||||
|
||||
isolate: v8.Isolate,
|
||||
inner: *v8.Inspector,
|
||||
session: v8.InspectorSession,
|
||||
|
||||
// We expect allocator to be an arena
|
||||
pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector {
|
||||
const ContextT = @TypeOf(ctx);
|
||||
|
||||
const InspectorContainer = switch (@typeInfo(ContextT)) {
|
||||
.@"struct" => ContextT,
|
||||
.pointer => |ptr| ptr.child,
|
||||
.void => NoopInspector,
|
||||
else => @compileError("invalid context type"),
|
||||
};
|
||||
|
||||
// If necessary, turn a void context into something we can safely ptrCast
|
||||
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
|
||||
|
||||
const channel = v8.InspectorChannel.init(safe_context, InspectorContainer.onInspectorResponse, InspectorContainer.onInspectorEvent, isolate);
|
||||
|
||||
const client = v8.InspectorClient.init();
|
||||
|
||||
const inner = try allocator.create(v8.Inspector);
|
||||
v8.Inspector.init(inner, client, channel, isolate);
|
||||
return .{ .inner = inner, .isolate = isolate, .session = inner.connect() };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Inspector) void {
|
||||
self.session.deinit();
|
||||
self.inner.deinit();
|
||||
}
|
||||
|
||||
pub fn send(self: *const Inspector, msg: []const u8) void {
|
||||
// Can't assume the main Context exists (with its HandleScope)
|
||||
// available when doing this. Pages (and thus the HandleScope)
|
||||
// comes and goes, but CDP can keep sending messages.
|
||||
const isolate = self.isolate;
|
||||
var temp_scope: v8.HandleScope = undefined;
|
||||
v8.HandleScope.init(&temp_scope, isolate);
|
||||
defer temp_scope.deinit();
|
||||
|
||||
self.session.dispatchProtocolMessage(isolate, msg);
|
||||
}
|
||||
|
||||
// From CDP docs
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-ExecutionContextDescription
|
||||
// ----
|
||||
// - name: Human readable name describing given context.
|
||||
// - origin: Execution context origin (ie. URL who initialised the request)
|
||||
// - auxData: Embedder-specific auxiliary data likely matching
|
||||
// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
|
||||
// - is_default_context: Whether the execution context is default, should match the auxData
|
||||
pub fn contextCreated(
|
||||
self: *const Inspector,
|
||||
context: *const Context,
|
||||
name: []const u8,
|
||||
origin: []const u8,
|
||||
aux_data: ?[]const u8,
|
||||
is_default_context: bool,
|
||||
) void {
|
||||
self.inner.contextCreated(context.v8_context, name, origin, aux_data, is_default_context);
|
||||
}
|
||||
|
||||
// Retrieves the RemoteObject for a given value.
|
||||
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
|
||||
// just like a method return value. Therefore, if we've mapped this
|
||||
// value before, we'll get the existing JS PersistedObject and if not
|
||||
// we'll create it and track it for cleanup when the context ends.
|
||||
pub fn getRemoteObject(
|
||||
self: *const Inspector,
|
||||
context: *Context,
|
||||
group: []const u8,
|
||||
value: anytype,
|
||||
) !RemoteObject {
|
||||
const js_value = try context.zigValueToJs(value);
|
||||
|
||||
// We do not want to expose this as a parameter for now
|
||||
const generate_preview = false;
|
||||
return self.session.wrapObject(
|
||||
context.isolate,
|
||||
context.v8_context,
|
||||
js_value,
|
||||
group,
|
||||
generate_preview,
|
||||
);
|
||||
}
|
||||
|
||||
// Gets a value by object ID regardless of which context it is in.
|
||||
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !?*anyopaque {
|
||||
const unwrapped = try self.session.unwrapObject(allocator, object_id);
|
||||
// The values context and groupId are not used here
|
||||
const toa = getTaggedAnyOpaque(unwrapped.value) orelse return null;
|
||||
if (toa.subtype == null or toa.subtype != .node) return error.ObjectIdIsNotANode;
|
||||
return toa.ptr;
|
||||
}
|
||||
|
||||
const NoopInspector = struct {
|
||||
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
||||
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
|
||||
};
|
||||
|
||||
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
|
||||
if (value.isObject() == false) {
|
||||
return null;
|
||||
}
|
||||
const obj = value.castTo(v8.Object);
|
||||
if (obj.internalFieldCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const external_data = obj.getInternalField(0).castTo(v8.External).get().?;
|
||||
return @ptrCast(@alignCast(external_data));
|
||||
}
|
||||
149
src/browser/js/Object.zig
Normal file
149
src/browser/js/Object.zig
Normal file
@@ -0,0 +1,149 @@
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Caller = @import("Caller.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const PersistentObject = v8.Persistent(v8.Object);
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Object = @This();
|
||||
js_obj: v8.Object,
|
||||
context: *js.Context,
|
||||
|
||||
pub const SetOpts = packed struct(u32) {
|
||||
READ_ONLY: bool = false,
|
||||
DONT_ENUM: bool = false,
|
||||
DONT_DELETE: bool = false,
|
||||
_: u29 = 0,
|
||||
};
|
||||
pub fn setIndex(self: Object, index: u32, value: anytype, opts: SetOpts) !void {
|
||||
@setEvalBranchQuota(10000);
|
||||
const key = switch (index) {
|
||||
inline 0...20 => |i| std.fmt.comptimePrint("{d}", .{i}),
|
||||
else => try std.fmt.allocPrint(self.context.arena, "{d}", .{index}),
|
||||
};
|
||||
return self.set(key, value, opts);
|
||||
}
|
||||
|
||||
pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{ FailedToSet, OutOfMemory }!void {
|
||||
const context = self.context;
|
||||
|
||||
const js_key = v8.String.initUtf8(context.isolate, key);
|
||||
const js_value = try context.zigValueToJs(value);
|
||||
|
||||
const res = self.js_obj.defineOwnProperty(context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false;
|
||||
if (!res) {
|
||||
return error.FailedToSet;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: Object, key: []const u8) !js.Value {
|
||||
const context = self.context;
|
||||
const js_key = v8.String.initUtf8(context.isolate, key);
|
||||
const js_val = try self.js_obj.getValue(context.v8_context, js_key);
|
||||
return context.createValue(js_val);
|
||||
}
|
||||
|
||||
pub fn isTruthy(self: Object) bool {
|
||||
const js_value = self.js_obj.toValue();
|
||||
return js_value.toBool(self.context.isolate);
|
||||
}
|
||||
|
||||
pub fn toString(self: Object) ![]const u8 {
|
||||
const js_value = self.js_obj.toValue();
|
||||
return self.context.valueToString(js_value, .{});
|
||||
}
|
||||
|
||||
pub fn toDetailString(self: Object) ![]const u8 {
|
||||
const js_value = self.js_obj.toValue();
|
||||
return self.context.valueToDetailString(js_value);
|
||||
}
|
||||
|
||||
pub fn format(self: Object, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
return writer.writeAll(try self.toString());
|
||||
}
|
||||
|
||||
pub fn toJson(self: Object, allocator: Allocator) ![]u8 {
|
||||
const json_string = try v8.Json.stringify(self.context.v8_context, self.js_obj.toValue(), null);
|
||||
const str = try self.context.jsStringToZig(json_string, .{ .allocator = allocator });
|
||||
return str;
|
||||
}
|
||||
|
||||
pub fn persist(self: Object) !Object {
|
||||
var context = self.context;
|
||||
const js_obj = self.js_obj;
|
||||
|
||||
const persisted = PersistentObject.init(context.isolate, js_obj);
|
||||
try context.js_object_list.append(context.arena, persisted);
|
||||
|
||||
return .{
|
||||
.context = context,
|
||||
.js_obj = persisted.castToObject(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
|
||||
if (self.isNullOrUndefined()) {
|
||||
return null;
|
||||
}
|
||||
const context = self.context;
|
||||
|
||||
const js_name = v8.String.initUtf8(context.isolate, name);
|
||||
|
||||
const js_value = try self.js_obj.getValue(context.v8_context, js_name.toName());
|
||||
if (!js_value.isFunction()) {
|
||||
return null;
|
||||
}
|
||||
return try context.createFunction(js_value);
|
||||
}
|
||||
|
||||
pub fn isNull(self: Object) bool {
|
||||
return self.js_obj.toValue().isNull();
|
||||
}
|
||||
|
||||
pub fn isUndefined(self: Object) bool {
|
||||
return self.js_obj.toValue().isUndefined();
|
||||
}
|
||||
|
||||
pub fn triState(self: Object, comptime Struct: type, comptime name: []const u8, comptime T: type) !TriState(T) {
|
||||
if (self.isNull()) {
|
||||
return .{ .null = {} };
|
||||
}
|
||||
if (self.isUndefined()) {
|
||||
return .{ .undefined = {} };
|
||||
}
|
||||
return .{ .value = try self.toZig(Struct, name, T) };
|
||||
}
|
||||
|
||||
pub fn isNullOrUndefined(self: Object) bool {
|
||||
return self.js_obj.toValue().isNullOrUndefined();
|
||||
}
|
||||
|
||||
pub fn nameIterator(self: Object) js.ValueIterator {
|
||||
const context = self.context;
|
||||
const js_obj = self.js_obj;
|
||||
|
||||
const array = js_obj.getPropertyNames(context.v8_context);
|
||||
const count = array.length();
|
||||
|
||||
return .{
|
||||
.count = count,
|
||||
.context = context,
|
||||
.js_obj = array.castTo(v8.Object),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toZig(self: Object, comptime Struct: type, comptime name: []const u8, comptime T: type) !T {
|
||||
const named_function = comptime Caller.NamedFunction.init(Struct, name);
|
||||
return self.context.jsValueToZig(named_function, T, self.js_obj.toValue());
|
||||
}
|
||||
|
||||
pub fn TriState(comptime T: type) type {
|
||||
return union(enum) {
|
||||
null: void,
|
||||
undefined: void,
|
||||
value: T,
|
||||
};
|
||||
}
|
||||
21
src/browser/js/Platform.zig
Normal file
21
src/browser/js/Platform.zig
Normal file
@@ -0,0 +1,21 @@
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Platform = @This();
|
||||
inner: v8.Platform,
|
||||
|
||||
pub fn init() !Platform {
|
||||
if (v8.initV8ICU() == false) {
|
||||
return error.FailedToInitializeICU;
|
||||
}
|
||||
const platform = v8.Platform.initDefault(0, true);
|
||||
v8.initV8Platform(platform);
|
||||
v8.initV8();
|
||||
return .{ .inner = platform };
|
||||
}
|
||||
|
||||
pub fn deinit(self: Platform) void {
|
||||
_ = v8.deinitV8();
|
||||
v8.deinitV8Platform();
|
||||
self.inner.deinit();
|
||||
}
|
||||
25
src/browser/js/This.zig
Normal file
25
src/browser/js/This.zig
Normal file
@@ -0,0 +1,25 @@
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// This only exists so that we know whether a function wants the opaque
|
||||
// JS argument (js.Object), or if it wants the receiver as an opaque
|
||||
// value.
|
||||
// js.Object is normally used when a method wants an opaque JS object
|
||||
// that it'll pass into a callback.
|
||||
// This is used when the function wants to do advanced manipulation
|
||||
// of the v8.Object bound to the instance. For example, postAttach is an
|
||||
// example of using This.
|
||||
|
||||
const This = @This();
|
||||
obj: js.Object,
|
||||
|
||||
pub fn setIndex(self: This, index: u32, value: anytype, opts: js.Object.SetOpts) !void {
|
||||
return self.obj.setIndex(index, value, opts);
|
||||
}
|
||||
|
||||
pub fn set(self: This, key: []const u8, value: anytype, opts: js.Object.SetOpts) !void {
|
||||
return self.obj.set(key, value, opts);
|
||||
}
|
||||
64
src/browser/js/TryCatch.zig
Normal file
64
src/browser/js/TryCatch.zig
Normal file
@@ -0,0 +1,64 @@
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const TryCatch = @This();
|
||||
|
||||
inner: v8.TryCatch,
|
||||
context: *const js.Context,
|
||||
|
||||
pub fn init(self: *TryCatch, context: *const js.Context) void {
|
||||
self.context = context;
|
||||
self.inner.init(context.isolate);
|
||||
}
|
||||
|
||||
pub fn hasCaught(self: TryCatch) bool {
|
||||
return self.inner.hasCaught();
|
||||
}
|
||||
|
||||
// the caller needs to deinit the string returned
|
||||
pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
||||
const msg = self.inner.getException() orelse return null;
|
||||
return try self.context.valueToString(msg, .{ .allocator = allocator });
|
||||
}
|
||||
|
||||
// the caller needs to deinit the string returned
|
||||
pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
||||
const context = self.context;
|
||||
const s = self.inner.getStackTrace(context.v8_context) orelse return null;
|
||||
return try context.valueToString(s, .{ .allocator = allocator });
|
||||
}
|
||||
|
||||
// the caller needs to deinit the string returned
|
||||
pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
||||
const context = self.context;
|
||||
const msg = self.inner.getMessage() orelse return null;
|
||||
const sl = msg.getSourceLine(context.v8_context) orelse return null;
|
||||
return try context.jsStringToZig(sl, .{ .allocator = allocator });
|
||||
}
|
||||
|
||||
pub fn sourceLineNumber(self: TryCatch) ?u32 {
|
||||
const context = self.context;
|
||||
const msg = self.inner.getMessage() orelse return null;
|
||||
return msg.getLineNumber(context.v8_context);
|
||||
}
|
||||
|
||||
// a shorthand method to return either the entire stack message
|
||||
// or just the exception message
|
||||
// - in Debug mode return the stack if available
|
||||
// - otherwise return the exception if available
|
||||
// the caller needs to deinit the string returned
|
||||
pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
||||
if (comptime @import("builtin").mode == .Debug) {
|
||||
if (try self.stack(allocator)) |msg| {
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
return try self.exception(allocator);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TryCatch) void {
|
||||
self.inner.deinit();
|
||||
}
|
||||
@@ -190,7 +190,7 @@ test "generate: Union" {
|
||||
const value = Union(.{ Astruct, Bstruct, .{Cstruct} });
|
||||
const ti = @typeInfo(value).@"union";
|
||||
try std.testing.expectEqual(3, ti.fields.len);
|
||||
try std.testing.expectEqualStrings("*runtime.generate.test.generate: Union.Astruct.Other", @typeName(ti.fields[0].type));
|
||||
try std.testing.expectEqualStrings("*browser.js.generate.test.generate: Union.Astruct.Other", @typeName(ti.fields[0].type));
|
||||
try std.testing.expectEqualStrings(ti.fields[0].name, "Astruct");
|
||||
try std.testing.expectEqual(*Bstruct, ti.fields[1].type);
|
||||
try std.testing.expectEqualStrings(ti.fields[1].name, "Bstruct");
|
||||
499
src/browser/js/js.zig
Normal file
499
src/browser/js/js.zig
Normal file
@@ -0,0 +1,499 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
pub const v8 = @import("v8");
|
||||
|
||||
const types = @import("types.zig");
|
||||
const log = @import("../../log.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const Env = @import("Env.zig");
|
||||
pub const ExecutionWorld = @import("ExecutionWorld.zig");
|
||||
pub const Context = @import("Context.zig");
|
||||
pub const Inspector = @import("Inspector.zig");
|
||||
|
||||
// TODO: Is "This" really necessary?
|
||||
pub const This = @import("This.zig");
|
||||
pub const Object = @import("Object.zig");
|
||||
pub const TryCatch = @import("TryCatch.zig");
|
||||
pub const Function = @import("Function.zig");
|
||||
|
||||
const Caller = @import("Caller.zig");
|
||||
const NamedFunction = Context.NamedFunction;
|
||||
|
||||
// If a function returns a []i32, should that map to a plain-old
|
||||
// JavaScript array, or a Int32Array? It's ambiguous. By default, we'll
|
||||
// map arrays/slices to the JavaScript arrays. If you want a TypedArray
|
||||
// wrap it in this.
|
||||
// Also, this type has nothing to do with the Env. But we place it here
|
||||
// for consistency. Want a callback? Env.Callback. Want a JsObject?
|
||||
// Env.JsObject. Want a TypedArray? Env.TypedArray.
|
||||
pub fn TypedArray(comptime T: type) type {
|
||||
return struct {
|
||||
pub const _TYPED_ARRAY_ID_KLUDGE = true;
|
||||
|
||||
values: []const T,
|
||||
|
||||
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
|
||||
return .{ .values = try allocator.dupe(T, self.values) };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const PromiseResolver = struct {
|
||||
context: *Context,
|
||||
resolver: v8.PromiseResolver,
|
||||
|
||||
pub fn promise(self: PromiseResolver) Promise {
|
||||
return self.resolver.getPromise();
|
||||
}
|
||||
|
||||
pub fn resolve(self: PromiseResolver, value: anytype) !void {
|
||||
const context = self.context;
|
||||
const js_value = try context.zigValueToJs(value);
|
||||
|
||||
// resolver.resolve will return null if the promise isn't pending
|
||||
const ok = self.resolver.resolve(context.v8_context, js_value) orelse return;
|
||||
if (!ok) {
|
||||
return error.FailedToResolvePromise;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reject(self: PromiseResolver, value: anytype) !void {
|
||||
const context = self.context;
|
||||
const js_value = try context.zigValueToJs(value);
|
||||
|
||||
// resolver.reject will return null if the promise isn't pending
|
||||
const ok = self.resolver.reject(context.v8_context, js_value) orelse return;
|
||||
if (!ok) {
|
||||
return error.FailedToRejectPromise;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const PersistentPromiseResolver = struct {
|
||||
context: *Context,
|
||||
resolver: v8.Persistent(v8.PromiseResolver),
|
||||
|
||||
pub fn deinit(self: *PersistentPromiseResolver) void {
|
||||
self.resolver.deinit();
|
||||
}
|
||||
|
||||
pub fn promise(self: PersistentPromiseResolver) Promise {
|
||||
return self.resolver.castToPromiseResolver().getPromise();
|
||||
}
|
||||
|
||||
pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void {
|
||||
const context = self.context;
|
||||
const js_value = try context.zigValueToJs(value);
|
||||
|
||||
// resolver.resolve will return null if the promise isn't pending
|
||||
const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return;
|
||||
if (!ok) {
|
||||
return error.FailedToResolvePromise;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reject(self: PersistentPromiseResolver, value: anytype) !void {
|
||||
const context = self.context;
|
||||
const js_value = try context.zigValueToJs(value);
|
||||
|
||||
// resolver.reject will return null if the promise isn't pending
|
||||
const ok = self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) orelse return;
|
||||
if (!ok) {
|
||||
return error.FailedToRejectPromise;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const Promise = v8.Promise;
|
||||
|
||||
// When doing jsValueToZig, string ([]const u8) are managed by the
|
||||
// call_arena. That means that if the API wants to persist the string
|
||||
// (which is relatively common), it needs to dupe it again.
|
||||
// If the parameter is an Env.String rather than a []const u8, then
|
||||
// the page's arena will be used (rather than the call arena).
|
||||
pub const String = struct {
|
||||
string: []const u8,
|
||||
};
|
||||
|
||||
pub const Exception = struct {
|
||||
inner: v8.Value,
|
||||
context: *const Context,
|
||||
|
||||
// the caller needs to deinit the string returned
|
||||
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
|
||||
return self.context.valueToString(self.inner, .{ .allocator = allocator });
|
||||
}
|
||||
};
|
||||
|
||||
pub const Value = struct {
|
||||
value: v8.Value,
|
||||
context: *const Context,
|
||||
|
||||
// the caller needs to deinit the string returned
|
||||
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
|
||||
return self.context.valueToString(self.value, .{ .allocator = allocator });
|
||||
}
|
||||
|
||||
pub fn fromJson(ctx: *Context, json: []const u8) !Value {
|
||||
const json_string = v8.String.initUtf8(ctx.isolate, json);
|
||||
const value = try v8.Json.parse(ctx.v8_context, json_string);
|
||||
return Value{ .context = ctx, .value = value };
|
||||
}
|
||||
};
|
||||
|
||||
pub const ValueIterator = struct {
|
||||
count: u32,
|
||||
idx: u32 = 0,
|
||||
js_obj: v8.Object,
|
||||
context: *const Context,
|
||||
|
||||
pub fn next(self: *ValueIterator) !?Value {
|
||||
const idx = self.idx;
|
||||
if (idx == self.count) {
|
||||
return null;
|
||||
}
|
||||
self.idx += 1;
|
||||
|
||||
const context = self.context;
|
||||
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx);
|
||||
return context.createValue(js_val);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn UndefinedOr(comptime T: type) type {
|
||||
return union(enum) {
|
||||
undefined: void,
|
||||
value: T,
|
||||
};
|
||||
}
|
||||
|
||||
// An interface for types that want to have their jsScopeEnd function be
|
||||
// called when the call context ends
|
||||
const CallScopeEndCallback = struct {
|
||||
ptr: *anyopaque,
|
||||
callScopeEndFn: *const fn (ptr: *anyopaque) void,
|
||||
|
||||
fn init(ptr: anytype) CallScopeEndCallback {
|
||||
const T = @TypeOf(ptr);
|
||||
const ptr_info = @typeInfo(T);
|
||||
|
||||
const gen = struct {
|
||||
pub fn callScopeEnd(pointer: *anyopaque) void {
|
||||
const self: T = @ptrCast(@alignCast(pointer));
|
||||
return ptr_info.pointer.child.jsCallScopeEnd(self);
|
||||
}
|
||||
};
|
||||
|
||||
return .{
|
||||
.ptr = ptr,
|
||||
.callScopeEndFn = gen.callScopeEnd,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn callScopeEnd(self: CallScopeEndCallback) void {
|
||||
self.callScopeEndFn(self.ptr);
|
||||
}
|
||||
};
|
||||
|
||||
// Callback called on global's property missing.
|
||||
// Return true to intercept the execution or false to let the call
|
||||
// continue the chain.
|
||||
pub const GlobalMissingCallback = struct {
|
||||
ptr: *anyopaque,
|
||||
missingFn: *const fn (ptr: *anyopaque, name: []const u8, ctx: *Context) bool,
|
||||
|
||||
pub fn init(ptr: anytype) GlobalMissingCallback {
|
||||
const T = @TypeOf(ptr);
|
||||
const ptr_info = @typeInfo(T);
|
||||
|
||||
const gen = struct {
|
||||
pub fn missing(pointer: *anyopaque, name: []const u8, ctx: *Context) bool {
|
||||
const self: T = @ptrCast(@alignCast(pointer));
|
||||
return ptr_info.pointer.child.missing(self, name, ctx);
|
||||
}
|
||||
};
|
||||
|
||||
return .{
|
||||
.ptr = ptr,
|
||||
.missingFn = gen.missing,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn missing(self: GlobalMissingCallback, name: []const u8, ctx: *Context) bool {
|
||||
return self.missingFn(self.ptr, name, ctx);
|
||||
}
|
||||
};
|
||||
|
||||
// Attributes that return a primitive type are setup directly on the
|
||||
// FunctionTemplate when the Env is setup. More complex types need a v8.Context
|
||||
// and cannot be set directly on the FunctionTemplate.
|
||||
// We default to saying types are primitives because that's mostly what
|
||||
// we have. If we add a new complex type that isn't explictly handled here,
|
||||
// we'll get a compiler error in simpleZigValueToJs, and can then explicitly
|
||||
// add the type here.
|
||||
pub fn isComplexAttributeType(ti: std.builtin.Type) bool {
|
||||
return switch (ti) {
|
||||
.array => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
// These are simple types that we can convert to JS with only an isolate. This
|
||||
// is separated from the Caller's zigValueToJs to make it available when we
|
||||
// don't have a caller (i.e., when setting static attributes on types)
|
||||
pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool) if (fail) v8.Value else ?v8.Value {
|
||||
switch (@typeInfo(@TypeOf(value))) {
|
||||
.void => return v8.initUndefined(isolate).toValue(),
|
||||
.null => return v8.initNull(isolate).toValue(),
|
||||
.bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)),
|
||||
.int => |n| switch (n.signedness) {
|
||||
.signed => {
|
||||
if (value >= -2_147_483_648 and value <= 2_147_483_647) {
|
||||
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
|
||||
}
|
||||
if (comptime n.bits <= 64) {
|
||||
return v8.getValue(v8.BigInt.initI64(isolate, @intCast(value)));
|
||||
}
|
||||
@compileError(@typeName(value) ++ " is not supported");
|
||||
},
|
||||
.unsigned => {
|
||||
if (value <= 4_294_967_295) {
|
||||
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
||||
}
|
||||
if (comptime n.bits <= 64) {
|
||||
return v8.getValue(v8.BigInt.initU64(isolate, @intCast(value)));
|
||||
}
|
||||
@compileError(@typeName(value) ++ " is not supported");
|
||||
},
|
||||
},
|
||||
.comptime_int => {
|
||||
if (value >= 0) {
|
||||
if (value <= 4_294_967_295) {
|
||||
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
||||
}
|
||||
return v8.BigInt.initU64(isolate, @intCast(value)).toValue();
|
||||
}
|
||||
if (value >= -2_147_483_648) {
|
||||
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
|
||||
}
|
||||
return v8.BigInt.initI64(isolate, @intCast(value)).toValue();
|
||||
},
|
||||
.comptime_float => return v8.Number.init(isolate, value).toValue(),
|
||||
.float => |f| switch (f.bits) {
|
||||
64 => return v8.Number.init(isolate, value).toValue(),
|
||||
32 => return v8.Number.init(isolate, @floatCast(value)).toValue(),
|
||||
else => @compileError(@typeName(value) ++ " is not supported"),
|
||||
},
|
||||
.pointer => |ptr| {
|
||||
if (ptr.size == .slice and ptr.child == u8) {
|
||||
return v8.String.initUtf8(isolate, value).toValue();
|
||||
}
|
||||
if (ptr.size == .one) {
|
||||
const one_info = @typeInfo(ptr.child);
|
||||
if (one_info == .array and one_info.array.child == u8) {
|
||||
return v8.String.initUtf8(isolate, value).toValue();
|
||||
}
|
||||
}
|
||||
},
|
||||
.array => return simpleZigValueToJs(isolate, &value, fail),
|
||||
.optional => {
|
||||
if (value) |v| {
|
||||
return simpleZigValueToJs(isolate, v, fail);
|
||||
}
|
||||
return v8.initNull(isolate).toValue();
|
||||
},
|
||||
.@"struct" => {
|
||||
const T = @TypeOf(value);
|
||||
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
|
||||
const values = value.values;
|
||||
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
|
||||
const len = values.len;
|
||||
const bits = switch (@typeInfo(value_type)) {
|
||||
.int => |n| n.bits,
|
||||
.float => |f| f.bits,
|
||||
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
|
||||
};
|
||||
|
||||
var array_buffer: v8.ArrayBuffer = undefined;
|
||||
if (len == 0) {
|
||||
array_buffer = v8.ArrayBuffer.init(isolate, 0);
|
||||
} else {
|
||||
const buffer_len = len * bits / 8;
|
||||
const backing_store = v8.BackingStore.init(isolate, buffer_len);
|
||||
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
|
||||
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
|
||||
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
||||
}
|
||||
|
||||
switch (@typeInfo(value_type)) {
|
||||
.int => |n| switch (n.signedness) {
|
||||
.unsigned => switch (n.bits) {
|
||||
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
|
||||
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
|
||||
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
|
||||
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
|
||||
else => {},
|
||||
},
|
||||
.signed => switch (n.bits) {
|
||||
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
|
||||
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
|
||||
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
|
||||
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
|
||||
else => {},
|
||||
},
|
||||
},
|
||||
.float => |f| switch (f.bits) {
|
||||
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
|
||||
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
// We normally don't fail in this function unless fail == true
|
||||
// but this can never be valid.
|
||||
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
|
||||
}
|
||||
},
|
||||
.@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail),
|
||||
.@"enum" => {
|
||||
const T = @TypeOf(value);
|
||||
if (@hasDecl(T, "toString")) {
|
||||
return simpleZigValueToJs(isolate, value.toString(), fail);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
if (fail) {
|
||||
@compileError("Unsupported Zig type " ++ @typeName(@TypeOf(value)));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _createException(isolate: v8.Isolate, msg: []const u8) v8.Value {
|
||||
return v8.Exception.initError(v8.String.initUtf8(isolate, msg));
|
||||
}
|
||||
|
||||
pub fn classNameForStruct(comptime Struct: type) []const u8 {
|
||||
if (@hasDecl(Struct, "js_name")) {
|
||||
return Struct.js_name;
|
||||
}
|
||||
@setEvalBranchQuota(10_000);
|
||||
const full_name = @typeName(Struct);
|
||||
const last = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse return full_name;
|
||||
return full_name[last + 1 ..];
|
||||
}
|
||||
|
||||
// When we return a Zig object to V8, we put it on the heap and pass it into
|
||||
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
|
||||
// function parameter, we know what type it _should_ be. Above, in Caller.method
|
||||
// (for example), we know all the parameter types. So if a Zig function takes
|
||||
// a single parameter (its receiver), we know what that type is.
|
||||
//
|
||||
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
|
||||
// to the parameter type:
|
||||
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
|
||||
//
|
||||
// But there are 2 reasons we can't do that.
|
||||
//
|
||||
// == Reason 1 ==
|
||||
// The JS code might pass the wrong type:
|
||||
//
|
||||
// var cat = new Cat();
|
||||
// cat.setOwner(new Cat());
|
||||
//
|
||||
// The zig _setOwner method expects the 2nd parameter to be an *Owner, but
|
||||
// the JS code passed a *Cat.
|
||||
//
|
||||
// To solve this issue, we tag every returned value so that we can check what
|
||||
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
|
||||
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
|
||||
//
|
||||
// == Reason 2 ==
|
||||
// Because of prototype inheritance, even "correct" code can be a challenge. For
|
||||
// example, say the above JavaScript is fixed:
|
||||
//
|
||||
// var cat = new Cat();
|
||||
// cat.setOwner(new Owner("Leto"));
|
||||
//
|
||||
// The issue is that setOwner might not expect an *Owner, but rather a
|
||||
// *Person, which is the prototype for Owner. Now our Zig code is expecting
|
||||
// a *Person, but it was (correctly) given an *Owner.
|
||||
// For this reason, we also store the prototype's type index.
|
||||
//
|
||||
// One of the prototype mechanisms that we support is via composition. Owner
|
||||
// can have a "proto: *Person" field. For this reason, we also store the offset
|
||||
// of the proto field, so that, given an intFromPtr(*Owner) we can access its
|
||||
// proto field.
|
||||
//
|
||||
// The other prototype mechanism that we support is for netsurf, where we just
|
||||
// cast one type to another. In this case, we'll store an offset of -1 (as a
|
||||
// sentinel to indicate that we should just cast directly).
|
||||
pub const TaggedAnyOpaque = struct {
|
||||
// The type of object this is. The type is captured as an index, which
|
||||
// corresponds to both a field in TYPE_LOOKUP and the index of
|
||||
// PROTOTYPE_TABLE
|
||||
index: u16,
|
||||
|
||||
// Ptr to the Zig instance. Between the context where it's called (i.e.
|
||||
// we have the comptime parameter info for all functions), and the index field
|
||||
// we can figure out what type this is.
|
||||
ptr: *anyopaque,
|
||||
|
||||
// When we're asked to describe an object via the Inspector, we _must_ include
|
||||
// the proper subtype (and description) fields in the returned JSON.
|
||||
// V8 will give us a Value and ask us for the subtype. From the v8.Value we
|
||||
// can get a v8.Object, and from the v8.Object, we can get out TaggedAnyOpaque
|
||||
// which is where we store the subtype.
|
||||
subtype: ?types.Sub,
|
||||
};
|
||||
|
||||
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
|
||||
// included (e.g. in the wpt build).
|
||||
|
||||
// This is called from V8. Whenever the v8 inspector has to describe a value
|
||||
// it'll call this function to gets its [optional] subtype - which, from V8's
|
||||
// point of view, is an arbitrary string.
|
||||
pub export fn v8_inspector__Client__IMPL__valueSubtype(
|
||||
_: *v8.c.InspectorClientImpl,
|
||||
c_value: *const v8.C_Value,
|
||||
) callconv(.c) [*c]const u8 {
|
||||
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
|
||||
return if (external_entry.subtype) |st| @tagName(st) else null;
|
||||
}
|
||||
|
||||
// Same as valueSubType above, but for the optional description field.
|
||||
// From what I can tell, some drivers _need_ the description field to be
|
||||
// present, even if it's empty. So if we have a subType for the value, we'll
|
||||
// put an empty description.
|
||||
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
|
||||
_: *v8.c.InspectorClientImpl,
|
||||
v8_context: *const v8.C_Context,
|
||||
c_value: *const v8.C_Value,
|
||||
) callconv(.c) [*c]const u8 {
|
||||
_ = v8_context;
|
||||
|
||||
// We _must_ include a non-null description in order for the subtype value
|
||||
// to be included. Besides that, I don't know if the value has any meaning
|
||||
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
|
||||
return if (external_entry.subtype == null) null else "";
|
||||
}
|
||||
183
src/browser/js/types.zig
Normal file
183
src/browser/js/types.zig
Normal file
@@ -0,0 +1,183 @@
|
||||
const std = @import("std");
|
||||
|
||||
const generate = @import("generate.zig");
|
||||
|
||||
const Interfaces = generate.Tuple(.{
|
||||
@import("../crypto/crypto.zig").Crypto,
|
||||
@import("../console/console.zig").Console,
|
||||
@import("../css/css.zig").Interfaces,
|
||||
@import("../cssom/cssom.zig").Interfaces,
|
||||
@import("../dom/dom.zig").Interfaces,
|
||||
@import("../dom/shadow_root.zig").ShadowRoot,
|
||||
@import("../encoding/encoding.zig").Interfaces,
|
||||
@import("../events/event.zig").Interfaces,
|
||||
@import("../html/html.zig").Interfaces,
|
||||
@import("../iterator/iterator.zig").Interfaces,
|
||||
@import("../storage/storage.zig").Interfaces,
|
||||
@import("../url/url.zig").Interfaces,
|
||||
@import("../xhr/xhr.zig").Interfaces,
|
||||
@import("../xhr/form_data.zig").Interfaces,
|
||||
@import("../xhr/File.zig"),
|
||||
@import("../xmlserializer/xmlserializer.zig").Interfaces,
|
||||
@import("../fetch/fetch.zig").Interfaces,
|
||||
@import("../streams/streams.zig").Interfaces,
|
||||
});
|
||||
|
||||
pub const Types = @typeInfo(Interfaces).@"struct".fields;
|
||||
|
||||
// Imagine we have a type Cat which has a getter:
|
||||
//
|
||||
// fn get_owner(self: *Cat) *Owner {
|
||||
// return self.owner;
|
||||
// }
|
||||
//
|
||||
// When we execute caller.getter, we'll end up doing something like:
|
||||
// const res = @call(.auto, Cat.get_owner, .{cat_instance});
|
||||
//
|
||||
// How do we turn `res`, which is an *Owner, into something we can return
|
||||
// to v8? We need the ObjectTemplate associated with Owner. How do we
|
||||
// get that? Well, we store all the ObjectTemplates in an array that's
|
||||
// tied to env. So we do something like:
|
||||
//
|
||||
// env.templates[index_of_owner].initInstance(...);
|
||||
//
|
||||
// But how do we get that `index_of_owner`? `Lookup` is a struct
|
||||
// that looks like:
|
||||
//
|
||||
// const Lookup = struct {
|
||||
// comptime cat: usize = 0,
|
||||
// comptime owner: usize = 1,
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// So to get the template index of `owner`, we can do:
|
||||
//
|
||||
// const index_id = @field(type_lookup, @typeName(@TypeOf(res));
|
||||
//
|
||||
pub const Lookup = blk: {
|
||||
var fields: [Types.len]std.builtin.Type.StructField = undefined;
|
||||
for (Types, 0..) |s, i| {
|
||||
|
||||
// This prototype type check has nothing to do with building our
|
||||
// Lookup. But we put it here, early, so that the rest of the
|
||||
// code doesn't have to worry about checking if Struct.prototype is
|
||||
// a pointer.
|
||||
const Struct = s.defaultValue().?;
|
||||
if (@hasDecl(Struct, "prototype") and @typeInfo(Struct.prototype) != .pointer) {
|
||||
@compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s} must be a pointer", .{ @typeName(Struct.prototype), @typeName(Struct) }));
|
||||
}
|
||||
|
||||
fields[i] = .{
|
||||
.name = @typeName(Receiver(Struct)),
|
||||
.type = usize,
|
||||
.is_comptime = true,
|
||||
.alignment = @alignOf(usize),
|
||||
.default_value_ptr = &i,
|
||||
};
|
||||
}
|
||||
break :blk @Type(.{ .@"struct" = .{
|
||||
.layout = .auto,
|
||||
.decls = &.{},
|
||||
.is_tuple = false,
|
||||
.fields = &fields,
|
||||
} });
|
||||
};
|
||||
|
||||
pub const LOOKUP = Lookup{};
|
||||
|
||||
// Creates a list where the index of a type contains its prototype index
|
||||
// const Animal = struct{};
|
||||
// const Cat = struct{
|
||||
// pub const prototype = *Animal;
|
||||
// };
|
||||
//
|
||||
// Would create an array: [0, 0]
|
||||
// Animal, at index, 0, has no prototype, so we set it to itself
|
||||
// Cat, at index 1, has an Animal prototype, so we set it to 0.
|
||||
//
|
||||
// When we're trying to pass an argument to a Zig function, we'll know the
|
||||
// target type (the function parameter type), and we'll have a
|
||||
// TaggedAnyOpaque which will have the index of the type of that parameter.
|
||||
// We'll use the PROTOTYPE_TABLE to see if the TaggedAnyType should be
|
||||
// cast to a prototype.
|
||||
pub const PROTOTYPE_TABLE = blk: {
|
||||
var table: [Types.len]u16 = undefined;
|
||||
for (Types, 0..) |s, i| {
|
||||
var prototype_index = i;
|
||||
const Struct = s.defaultValue().?;
|
||||
if (@hasDecl(Struct, "prototype")) {
|
||||
const TI = @typeInfo(Struct.prototype);
|
||||
const proto_name = @typeName(Receiver(TI.pointer.child));
|
||||
prototype_index = @field(LOOKUP, proto_name);
|
||||
}
|
||||
table[i] = prototype_index;
|
||||
}
|
||||
break :blk table;
|
||||
};
|
||||
|
||||
// This is essentially meta data for each type. Each is stored in env.meta_lookup
|
||||
// The index for a type can be retrieved via:
|
||||
// const index = @field(TYPE_LOOKUP, @typeName(Receiver(Struct)));
|
||||
// const meta = env.meta_lookup[index];
|
||||
pub const Meta = struct {
|
||||
// Every type is given a unique index. That index is used to lookup various
|
||||
// things, i.e. the prototype chain.
|
||||
index: u16,
|
||||
|
||||
// We store the type's subtype here, so that when we create an instance of
|
||||
// the type, and bind it to JavaScript, we can store the subtype along with
|
||||
// the created TaggedAnyOpaque.s
|
||||
subtype: ?Sub,
|
||||
|
||||
// If this type has composition-based prototype, represents the byte-offset
|
||||
// from ptr where the `proto` field is located. A negative offsets is used
|
||||
// to indicate that the prototype field is behind a pointer.
|
||||
proto_offset: i32,
|
||||
};
|
||||
|
||||
pub const Sub = enum {
|
||||
@"error",
|
||||
array,
|
||||
arraybuffer,
|
||||
dataview,
|
||||
date,
|
||||
generator,
|
||||
iterator,
|
||||
map,
|
||||
node,
|
||||
promise,
|
||||
proxy,
|
||||
regexp,
|
||||
set,
|
||||
typedarray,
|
||||
wasmvalue,
|
||||
weakmap,
|
||||
weakset,
|
||||
webassemblymemory,
|
||||
};
|
||||
|
||||
// When we map a Zig instance into a JsObject, we'll normally store the a
|
||||
// TaggedAnyOpaque (TAO) inside of the JsObject's internal field. This requires
|
||||
// ensuring that the instance template has an InternalFieldCount of 1. However,
|
||||
// for empty objects, we don't need to store the TAO, because we can't just cast
|
||||
// one empty object to another, so for those, as an optimization, we do not set
|
||||
// the InternalFieldCount.
|
||||
pub fn isEmpty(comptime T: type) bool {
|
||||
return @typeInfo(T) != .@"opaque" and @sizeOf(T) == 0 and @hasDecl(T, "js_legacy_factory") == false;
|
||||
}
|
||||
|
||||
// If we have a struct:
|
||||
// const Cat = struct {
|
||||
// pub fn meow(self: *Cat) void { ... }
|
||||
// }
|
||||
// Then obviously, the receiver of its methods are going to be a *Cat (or *const Cat)
|
||||
//
|
||||
// However, we can also do:
|
||||
// const Cat = struct {
|
||||
// pub const Self = OtherImpl;
|
||||
// pub fn meow(self: *OtherImpl) void { ... }
|
||||
// }
|
||||
// In which case, as we see above, the receiver is derived from the Self declaration
|
||||
pub fn Receiver(comptime Struct: type) type {
|
||||
return if (@hasDecl(Struct, "Self")) Struct.Self else Struct;
|
||||
}
|
||||
@@ -558,6 +558,7 @@ pub const EventType = enum(u8) {
|
||||
xhr_event = 6,
|
||||
message_event = 7,
|
||||
keyboard_event = 8,
|
||||
pop_state = 9,
|
||||
};
|
||||
|
||||
pub const MutationEvent = c.dom_mutation_event;
|
||||
@@ -581,6 +582,14 @@ pub fn mutationEventPrevValue(evt: *MutationEvent) ?[]const u8 {
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
pub fn mutationEventNewValue(evt: *MutationEvent) ?[]const u8 {
|
||||
var s: ?*String = null;
|
||||
const err = c._dom_mutation_event_get_new_value(evt, &s);
|
||||
std.debug.assert(err == c.DOM_NO_ERR);
|
||||
if (s == null) return null;
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
pub fn mutationEventRelatedNode(evt: *MutationEvent) !?*Node {
|
||||
var n: NodeExternal = undefined;
|
||||
const err = c._dom_mutation_event_get_related_node(evt, &n);
|
||||
@@ -1355,7 +1364,7 @@ pub fn nodeHasChildNodes(node: *Node) bool {
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn nodeInsertBefore(node: *Node, new_node: *Node, ref_node: *Node) !*Node {
|
||||
pub fn nodeInsertBefore(node: *Node, new_node: *Node, ref_node: ?*Node) !*Node {
|
||||
var res: ?*Node = null;
|
||||
const err = nodeVtable(node).dom_node_insert_before.?(node, new_node, ref_node, &res);
|
||||
try DOMErr(err);
|
||||
@@ -2491,31 +2500,22 @@ fn parseParams(enc: ?[:0]const u8) c.dom_hubbub_parser_params {
|
||||
}
|
||||
|
||||
fn parseData(parser: *c.dom_hubbub_parser, reader: anytype) !void {
|
||||
var err: c.hubbub_error = undefined;
|
||||
const TI = @typeInfo(@TypeOf(reader));
|
||||
if (TI == .pointer and @hasDecl(TI.pointer.child, "next")) {
|
||||
while (try reader.next()) |data| {
|
||||
err = c.dom_hubbub_parser_parse_chunk(parser, data.ptr, data.len);
|
||||
try parserErr(err);
|
||||
}
|
||||
} else {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var ln = buffer.len;
|
||||
while (ln > 0) {
|
||||
ln = try reader.read(&buffer);
|
||||
err = c.dom_hubbub_parser_parse_chunk(parser, &buffer, ln);
|
||||
// TODO handle encoding change error return.
|
||||
// When the HTML contains a META tag with a different encoding than the
|
||||
// original one, a c.DOM_HUBBUB_HUBBUB_ERR_ENCODINGCHANGE error is
|
||||
// returned.
|
||||
// In this case, we must restart the parsing with the new detected
|
||||
// encoding. The detected encoding is stored in the document and we can
|
||||
// get it with documentGetInputEncoding().
|
||||
try parserErr(err);
|
||||
}
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var ln = buffer.len;
|
||||
while (ln > 0) {
|
||||
ln = try reader.read(&buffer);
|
||||
const err = c.dom_hubbub_parser_parse_chunk(parser, &buffer, ln);
|
||||
// TODO handle encoding change error return.
|
||||
// When the HTML contains a META tag with a different encoding than the
|
||||
// original one, a c.DOM_HUBBUB_HUBBUB_ERR_ENCODINGCHANGE error is
|
||||
// returned.
|
||||
// In this case, we must restart the parsing with the new detected
|
||||
// encoding. The detected encoding is stored in the document and we can
|
||||
// get it with documentGetInputEncoding().
|
||||
try parserErr(err);
|
||||
}
|
||||
err = c.dom_hubbub_parser_completed(parser);
|
||||
try parserErr(err);
|
||||
const err = c.dom_hubbub_parser_completed(parser);
|
||||
return parserErr(err);
|
||||
}
|
||||
|
||||
// documentHTMLClose closes the document.
|
||||
|
||||
@@ -23,8 +23,8 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const Dump = @import("dump.zig");
|
||||
const State = @import("State.zig");
|
||||
const Env = @import("env.zig").Env;
|
||||
const Mime = @import("mime.zig").Mime;
|
||||
const Browser = @import("browser.zig").Browser;
|
||||
const Session = @import("session.zig").Session;
|
||||
const Renderer = @import("renderer.zig").Renderer;
|
||||
const Window = @import("html/window.zig").Window;
|
||||
@@ -32,8 +32,10 @@ const Walker = @import("dom/walker.zig").WalkerDepthFirst;
|
||||
const Scheduler = @import("Scheduler.zig");
|
||||
const Http = @import("../http/Http.zig");
|
||||
const ScriptManager = @import("ScriptManager.zig");
|
||||
const SlotChangeMonitor = @import("SlotChangeMonitor.zig");
|
||||
const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
||||
|
||||
const js = @import("js/js.zig");
|
||||
const URL = @import("../url.zig").URL;
|
||||
|
||||
const log = @import("../log.zig");
|
||||
@@ -73,7 +75,7 @@ pub const Page = struct {
|
||||
|
||||
// Our JavaScript context for this specific page. This is what we use to
|
||||
// execute any JavaScript
|
||||
main_context: *Env.JsContext,
|
||||
js: *js.Context,
|
||||
|
||||
// indicates intention to navigate to another page on the next loop execution.
|
||||
delayed_navigation: bool = false,
|
||||
@@ -82,6 +84,11 @@ pub const Page = struct {
|
||||
|
||||
polyfill_loader: polyfill.Loader = .{},
|
||||
|
||||
/// KV map for various object data; use pointers as unsigned integer keys
|
||||
/// and store any `*anyopaque` as values. If a key or value will be
|
||||
/// deinitialized (freed), it should be removed from the map too.
|
||||
object_data: ObjectDataMap = .{},
|
||||
|
||||
scheduler: Scheduler,
|
||||
http_client: *Http.Client,
|
||||
script_manager: ScriptManager,
|
||||
@@ -90,6 +97,10 @@ pub const Page = struct {
|
||||
|
||||
load_state: LoadState = .parsing,
|
||||
|
||||
// expensive, adds a a global MutationObserver, so we only do it if there's
|
||||
// an "slotchange" event registered
|
||||
slot_change_monitor: ?*SlotChangeMonitor = null,
|
||||
|
||||
notified_network_idle: IdleNotification = .init,
|
||||
notified_network_almost_idle: IdleNotification = .init,
|
||||
|
||||
@@ -116,17 +127,35 @@ pub const Page = struct {
|
||||
complete,
|
||||
};
|
||||
|
||||
pub fn init(self: *Page, arena: Allocator, session: *Session) !void {
|
||||
const ObjectDataMap = std.HashMapUnmanaged(
|
||||
usize,
|
||||
*anyopaque,
|
||||
struct {
|
||||
pub fn hash(_: @This(), key: usize) usize {
|
||||
return key;
|
||||
}
|
||||
|
||||
pub fn eql(_: @This(), a: usize, b: usize) bool {
|
||||
return a == b;
|
||||
}
|
||||
},
|
||||
std.hash_map.default_max_load_percentage,
|
||||
);
|
||||
|
||||
pub fn init(self: *Page, arena: Allocator, call_arena: Allocator, session: *Session) !void {
|
||||
const browser = session.browser;
|
||||
const script_manager = ScriptManager.init(browser, self);
|
||||
|
||||
const url = try URL.parse("about:blank", null);
|
||||
errdefer url.deinit();
|
||||
|
||||
self.* = .{
|
||||
.url = URL.empty,
|
||||
.url = url,
|
||||
.mode = .{ .pre = {} },
|
||||
.window = try Window.create(null, null),
|
||||
.arena = arena,
|
||||
.session = session,
|
||||
.call_arena = undefined,
|
||||
.call_arena = call_arena,
|
||||
.renderer = Renderer.init(arena),
|
||||
.state_pool = &browser.state_pool,
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
@@ -135,46 +164,82 @@ pub const Page = struct {
|
||||
.scheduler = Scheduler.init(arena),
|
||||
.keydown_event_node = .{ .func = keydownCallback },
|
||||
.window_clicked_event_node = .{ .func = windowClicked },
|
||||
.main_context = undefined,
|
||||
.js = undefined,
|
||||
};
|
||||
|
||||
self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader));
|
||||
try polyfill.preload(self.arena, self.main_context);
|
||||
self.js = try session.executor.createContext(self, true, js.GlobalMissingCallback.init(&self.polyfill_loader));
|
||||
try polyfill.preload(self.arena, self.js);
|
||||
|
||||
try self.scheduler.add(self, runMicrotasks, 5, .{ .name = "page.microtasks" });
|
||||
// message loop must run only non-test env
|
||||
if (comptime !builtin.is_test) {
|
||||
try self.scheduler.add(self, runMessageLoop, 5, .{ .name = "page.messageLoop" });
|
||||
}
|
||||
try self.registerBackgroundTasks();
|
||||
}
|
||||
|
||||
// FIXME: Deinit self.url.
|
||||
pub fn deinit(self: *Page) void {
|
||||
self.script_manager.shutdown = true;
|
||||
|
||||
self.http_client.abort();
|
||||
self.script_manager.deinit();
|
||||
self.url.deinit();
|
||||
self.object_data.deinit(self.arena);
|
||||
}
|
||||
|
||||
fn reset(self: *Page) void {
|
||||
fn reset(self: *Page) !void {
|
||||
// Force running the micro task to drain the queue.
|
||||
self.session.browser.env.runMicrotasks();
|
||||
|
||||
self.scheduler.reset();
|
||||
self.http_client.abort();
|
||||
self.script_manager.reset();
|
||||
|
||||
_ = try self.url.reparse("about:blank");
|
||||
errdefer self.url.deinit();
|
||||
|
||||
self.object_data.deinit(self.arena);
|
||||
self.object_data = .{};
|
||||
|
||||
self.load_state = .parsing;
|
||||
self.mode = .{ .pre = {} };
|
||||
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
|
||||
try self.registerBackgroundTasks();
|
||||
}
|
||||
|
||||
fn runMicrotasks(ctx: *anyopaque) ?u32 {
|
||||
const self: *Page = @ptrCast(@alignCast(ctx));
|
||||
self.session.browser.runMicrotasks();
|
||||
return 5;
|
||||
fn registerBackgroundTasks(self: *Page) !void {
|
||||
if (comptime builtin.is_test) {
|
||||
// HTML test runner manually calls these as necessary
|
||||
return;
|
||||
}
|
||||
|
||||
try self.scheduler.add(self.session.browser, struct {
|
||||
fn runMicrotasks(ctx: *anyopaque) ?u32 {
|
||||
const b: *Browser = @ptrCast(@alignCast(ctx));
|
||||
b.runMicrotasks();
|
||||
return 5;
|
||||
}
|
||||
}.runMicrotasks, 5, .{ .name = "page.microtasks" });
|
||||
|
||||
try self.scheduler.add(self.session.browser, struct {
|
||||
fn runMessageLoop(ctx: *anyopaque) ?u32 {
|
||||
const b: *Browser = @ptrCast(@alignCast(ctx));
|
||||
b.runMessageLoop();
|
||||
return 100;
|
||||
}
|
||||
}.runMessageLoop, 5, .{ .name = "page.messageLoop" });
|
||||
}
|
||||
|
||||
fn runMessageLoop(ctx: *anyopaque) ?u32 {
|
||||
const self: *Page = @ptrCast(@alignCast(ctx));
|
||||
self.session.browser.runMessageLoop();
|
||||
return 100;
|
||||
/// Returns the object data by given key.
|
||||
/// `key` must be a pointer type.
|
||||
/// Type of value is unknown to map; so the caller must do the type casting.
|
||||
pub fn getObjectData(self: *Page, key: anytype) ?*anyopaque {
|
||||
std.debug.assert(@typeInfo(@TypeOf(key)) == .pointer);
|
||||
return self.object_data.get(@intFromPtr(key));
|
||||
}
|
||||
|
||||
/// Puts the object data by given key.
|
||||
/// `key` must be a pointer type.
|
||||
pub fn putObjectData(self: *Page, key: anytype, value: *anyopaque) Allocator.Error!void {
|
||||
std.debug.assert(@typeInfo(@TypeOf(key)) == .pointer);
|
||||
return self.object_data.put(self.arena, @intFromPtr(key), value);
|
||||
}
|
||||
|
||||
pub const DumpOpts = struct {
|
||||
@@ -221,7 +286,7 @@ pub const Page = struct {
|
||||
|
||||
const doc = parser.documentHTMLToDocument(self.window.document);
|
||||
|
||||
// if the base si requested, add the base's node in the document's headers.
|
||||
// if the base is requested, add the base's node in the document's headers.
|
||||
if (opts.with_base) {
|
||||
try self.addDOMTreeBase();
|
||||
}
|
||||
@@ -244,17 +309,12 @@ pub const Page = struct {
|
||||
const head = parser.nodeListItem(list, 0) orelse return;
|
||||
|
||||
const base = try parser.documentCreateElement(doc, "base");
|
||||
try parser.elementSetAttribute(base, "href", self.url.raw);
|
||||
try parser.elementSetAttribute(base, "href", self.url.getHref());
|
||||
|
||||
const Node = @import("dom/node.zig").Node;
|
||||
try Node.prepend(head, &[_]Node.NodeOrText{.{ .node = parser.elementToNode(base) }});
|
||||
}
|
||||
|
||||
pub fn fetchModuleSource(ctx: *anyopaque, src: [:0]const u8) !ScriptManager.BlockingResult {
|
||||
const self: *Page = @ptrCast(@alignCast(ctx));
|
||||
return self.script_manager.blockingGet(src);
|
||||
}
|
||||
|
||||
pub fn wait(self: *Page, wait_ms: i32) Session.WaitResult {
|
||||
return self._wait(wait_ms) catch |err| {
|
||||
switch (err) {
|
||||
@@ -276,8 +336,8 @@ pub const Page = struct {
|
||||
var timer = try std.time.Timer.start();
|
||||
var ms_remaining = wait_ms;
|
||||
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(self.main_context);
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(self.js);
|
||||
defer try_catch.deinit();
|
||||
|
||||
var scheduler = &self.scheduler;
|
||||
@@ -353,8 +413,7 @@ pub const Page = struct {
|
||||
std.debug.assert(http_client.intercepted == 0);
|
||||
|
||||
const ms = ms_to_next_task orelse blk: {
|
||||
const min_wait = if (comptime builtin.is_test) 50 else 100;
|
||||
if (wait_ms - ms_remaining < min_wait) {
|
||||
if (wait_ms - ms_remaining < 100) {
|
||||
// Look, we want to exit ASAP, but we don't want
|
||||
// to exit so fast that we've run none of the
|
||||
// background jobs.
|
||||
@@ -504,19 +563,18 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
pub fn origin(self: *const Page, arena: Allocator) ![]const u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(arena);
|
||||
try self.url.origin(&aw.writer);
|
||||
return aw.written();
|
||||
return self.url.getOrigin(arena);
|
||||
}
|
||||
|
||||
const RequestCookieOpts = struct {
|
||||
is_http: bool = true,
|
||||
is_navigation: bool = false,
|
||||
};
|
||||
|
||||
pub fn requestCookie(self: *const Page, opts: RequestCookieOpts) Http.Client.RequestCookie {
|
||||
return .{
|
||||
.jar = self.cookie_jar,
|
||||
.origin = &self.url.uri,
|
||||
.cookie_jar = self.cookie_jar,
|
||||
.origin_url = self.url,
|
||||
.is_http = opts.is_http,
|
||||
.is_navigation = opts.is_navigation,
|
||||
};
|
||||
@@ -527,7 +585,7 @@ pub const Page = struct {
|
||||
if (self.mode != .pre) {
|
||||
// it's possible for navigate to be called multiple times on the
|
||||
// same page (via CDP). We want to reset the page between each call.
|
||||
self.reset();
|
||||
try self.reset();
|
||||
}
|
||||
|
||||
log.info(.http, "navigate", .{
|
||||
@@ -624,7 +682,7 @@ pub const Page = struct {
|
||||
};
|
||||
|
||||
self.session.browser.notification.dispatch(.page_navigated, &.{
|
||||
.url = self.url.raw,
|
||||
.url = self.url.getHref(),
|
||||
.timestamp = timestamp(),
|
||||
});
|
||||
}
|
||||
@@ -786,7 +844,7 @@ pub const Page = struct {
|
||||
// ignore non-js script.
|
||||
continue;
|
||||
}
|
||||
try self.script_manager.addFromElement(@ptrCast(node));
|
||||
try self.script_manager.addFromElement(@ptrCast(node), "page");
|
||||
}
|
||||
|
||||
self.script_manager.staticScriptsDone();
|
||||
@@ -802,6 +860,9 @@ pub const Page = struct {
|
||||
unreachable;
|
||||
},
|
||||
}
|
||||
|
||||
// Push the navigation after a successful load.
|
||||
try self.session.history.pushNavigation(self.url.getHref(), self);
|
||||
}
|
||||
|
||||
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
@@ -834,10 +895,10 @@ pub const Page = struct {
|
||||
_ = self.session.browser.transfer_arena.reset(.{ .retain_with_limit = 4 * 1024 });
|
||||
}
|
||||
|
||||
// extracted because this sis called from tests to set things up.
|
||||
// extracted because this is called from tests to set things up.
|
||||
pub fn setDocument(self: *Page, html_doc: *parser.DocumentHTML) !void {
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
try parser.documentSetDocumentURI(doc, self.url.raw);
|
||||
try parser.documentSetDocumentURI(doc, self.url.getHref());
|
||||
|
||||
// TODO set the referrer to the document.
|
||||
try self.window.replaceDocument(html_doc);
|
||||
@@ -1027,7 +1088,7 @@ pub const Page = struct {
|
||||
|
||||
session.queued_navigation = .{
|
||||
.opts = opts,
|
||||
.url = try URL.stitch(session.transfer_arena, url, self.url.raw, .{ .alloc = .always }),
|
||||
.url = try URL.stitch(session.transfer_arena, url, self.url.getHref(), .{ .alloc = .always }),
|
||||
};
|
||||
|
||||
self.http_client.abort();
|
||||
@@ -1067,7 +1128,7 @@ pub const Page = struct {
|
||||
try form_data.write(encoding, buf.writer(transfer_arena));
|
||||
|
||||
const method = try parser.elementGetAttribute(@ptrCast(@alignCast(form)), "method") orelse "";
|
||||
var action = try parser.elementGetAttribute(@ptrCast(@alignCast(form)), "action") orelse self.url.raw;
|
||||
var action = try parser.elementGetAttribute(@ptrCast(@alignCast(form)), "action") orelse self.url.getHref();
|
||||
|
||||
var opts = NavigateOpts{
|
||||
.reason = .form,
|
||||
@@ -1114,10 +1175,22 @@ pub const Page = struct {
|
||||
|
||||
pub fn stackTrace(self: *Page) !?[]const u8 {
|
||||
if (comptime builtin.mode == .Debug) {
|
||||
return self.main_context.stackTrace();
|
||||
return self.js.stackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn registerSlotChangeMonitor(self: *Page) !void {
|
||||
if (self.slot_change_monitor != null) {
|
||||
return;
|
||||
}
|
||||
self.slot_change_monitor = try SlotChangeMonitor.init(self);
|
||||
}
|
||||
|
||||
pub fn isSameOrigin(self: *const Page, url: []const u8) !bool {
|
||||
const current_origin = try self.origin(self.call_arena);
|
||||
return std.mem.startsWith(u8, url, current_origin);
|
||||
}
|
||||
};
|
||||
|
||||
pub const NavigateReason = enum {
|
||||
@@ -1125,6 +1198,7 @@ pub const NavigateReason = enum {
|
||||
address_bar,
|
||||
form,
|
||||
script,
|
||||
history,
|
||||
};
|
||||
|
||||
pub const NavigateOpts = struct {
|
||||
@@ -1235,7 +1309,7 @@ pub export fn scriptAddedCallback(ctx: ?*anyopaque, element: ?*parser.Element) c
|
||||
// here, else the script_manager will flag it as already-processed.
|
||||
_ = parser.elementGetAttribute(element.?, "src") catch return orelse return;
|
||||
|
||||
self.script_manager.addFromElement(element.?) catch |err| {
|
||||
self.script_manager.addFromElement(element.?, "dynamic") catch |err| {
|
||||
log.warn(.browser, "dynamic script", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
pub const Loader = struct {
|
||||
state: enum { empty, loading } = .empty,
|
||||
@@ -30,8 +30,8 @@ pub const Loader = struct {
|
||||
webcomponents: bool = false,
|
||||
} = .{},
|
||||
|
||||
fn load(self: *Loader, comptime name: []const u8, source: []const u8, js_context: *Env.JsContext) void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
fn load(self: *Loader, comptime name: []const u8, source: []const u8, js_context: *js.Context) void {
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
@@ -49,7 +49,7 @@ pub const Loader = struct {
|
||||
@field(self.done, name) = true;
|
||||
}
|
||||
|
||||
pub fn missing(self: *Loader, name: []const u8, js_context: *Env.JsContext) bool {
|
||||
pub fn missing(self: *Loader, name: []const u8, js_context: *js.Context) bool {
|
||||
// Avoid recursive calls during polyfill loading.
|
||||
if (self.state == .loading) {
|
||||
return false;
|
||||
@@ -69,6 +69,7 @@ pub const Loader = struct {
|
||||
if (comptime builtin.mode == .Debug) {
|
||||
log.debug(.unknown_prop, "unkown global property", .{
|
||||
.info = "but the property can exist in pure JS",
|
||||
.stack = js_context.stackTrace() catch "???",
|
||||
.property = name,
|
||||
});
|
||||
}
|
||||
@@ -82,8 +83,8 @@ pub const Loader = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub fn preload(allocator: Allocator, js_context: *Env.JsContext) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
pub fn preload(allocator: Allocator, js_context: *js.Context) !void {
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
|
||||
@@ -20,10 +20,11 @@ const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const js = @import("js/js.zig");
|
||||
const Page = @import("page.zig").Page;
|
||||
const Browser = @import("browser.zig").Browser;
|
||||
const NavigateOpts = @import("page.zig").NavigateOpts;
|
||||
const History = @import("html/History.zig");
|
||||
|
||||
const log = @import("../log.zig");
|
||||
const parser = @import("netsurf.zig");
|
||||
@@ -49,10 +50,14 @@ pub const Session = struct {
|
||||
// page and start another.
|
||||
transfer_arena: Allocator,
|
||||
|
||||
executor: Env.ExecutionWorld,
|
||||
executor: js.ExecutionWorld,
|
||||
storage_shed: storage.Shed,
|
||||
cookie_jar: storage.CookieJar,
|
||||
|
||||
// History is persistent across the "tab".
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/History
|
||||
history: History = .{},
|
||||
|
||||
page: ?Page = null,
|
||||
|
||||
// If the current page want to navigate to a new page
|
||||
@@ -101,7 +106,7 @@ pub const Session = struct {
|
||||
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, page_arena.allocator(), self);
|
||||
try Page.init(page, page_arena.allocator(), self.browser.call_arena.allocator(), self);
|
||||
|
||||
log.debug(.browser, "create page", .{});
|
||||
// start JS env
|
||||
@@ -121,7 +126,7 @@ pub const Session = struct {
|
||||
// registered a destructor (e.g. XMLHttpRequest).
|
||||
// Should be called before we deinit the page, because these objects
|
||||
// could be referencing it.
|
||||
self.executor.removeJsContext();
|
||||
self.executor.removeContext();
|
||||
|
||||
self.page.?.deinit();
|
||||
self.page = null;
|
||||
@@ -143,36 +148,63 @@ pub const Session = struct {
|
||||
};
|
||||
|
||||
pub fn wait(self: *Session, wait_ms: i32) WaitResult {
|
||||
if (self.queued_navigation) |qn| {
|
||||
// This was already aborted on the page, but it would be pretty
|
||||
// bad if old requests went to the new page, so let's make double sure
|
||||
self.browser.http_client.abort();
|
||||
|
||||
// Page.navigateFromWebAPI terminatedExecution. If we don't resume
|
||||
// it before doing a shutdown we'll get an error.
|
||||
self.executor.resumeExecution();
|
||||
self.removePage();
|
||||
self.queued_navigation = null;
|
||||
|
||||
const page = self.createPage() catch |err| {
|
||||
log.err(.browser, "queued navigation page error", .{
|
||||
.err = err,
|
||||
.url = qn.url,
|
||||
});
|
||||
return .done;
|
||||
};
|
||||
|
||||
page.navigate(qn.url, qn.opts) catch |err| {
|
||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
||||
return .done;
|
||||
};
|
||||
}
|
||||
_ = self.processQueuedNavigation() catch {
|
||||
// There was an error processing the queue navigation. This already
|
||||
// logged the error, just return.
|
||||
return .done;
|
||||
};
|
||||
|
||||
if (self.page) |*page| {
|
||||
return page.wait(wait_ms);
|
||||
}
|
||||
return .no_page;
|
||||
}
|
||||
|
||||
pub fn fetchWait(self: *Session, wait_ms: i32) void {
|
||||
while (true) {
|
||||
if (self.page == null) {
|
||||
return;
|
||||
}
|
||||
_ = self.page.?.wait(wait_ms);
|
||||
const navigated = self.processQueuedNavigation() catch {
|
||||
// There was an error processing the queue navigation. This already
|
||||
// logged the error, just return.
|
||||
return;
|
||||
};
|
||||
|
||||
if (navigated == false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn processQueuedNavigation(self: *Session) !bool {
|
||||
const qn = self.queued_navigation orelse return false;
|
||||
// This was already aborted on the page, but it would be pretty
|
||||
// bad if old requests went to the new page, so let's make double sure
|
||||
self.browser.http_client.abort();
|
||||
|
||||
// Page.navigateFromWebAPI terminatedExecution. If we don't resume
|
||||
// it before doing a shutdown we'll get an error.
|
||||
self.executor.resumeExecution();
|
||||
self.removePage();
|
||||
self.queued_navigation = null;
|
||||
|
||||
const page = self.createPage() catch |err| {
|
||||
log.err(.browser, "queued navigation page error", .{
|
||||
.err = err,
|
||||
.url = qn.url,
|
||||
});
|
||||
return err;
|
||||
};
|
||||
|
||||
page.navigate(qn.url, qn.opts) catch |err| {
|
||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
||||
return err;
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const QueuedNavigation = struct {
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
const std = @import("std");
|
||||
const Uri = std.Uri;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const DateTime = @import("../../datetime.zig").DateTime;
|
||||
const public_suffix_list = @import("../../data/public_suffix_list.zig").lookup;
|
||||
|
||||
pub const LookupOpts = struct {
|
||||
request_time: ?i64 = null,
|
||||
origin_uri: ?*const Uri = null,
|
||||
is_http: bool,
|
||||
is_navigation: bool = true,
|
||||
prefix: ?[]const u8 = null,
|
||||
};
|
||||
const URL = @import("../../url.zig").URL;
|
||||
|
||||
pub const Jar = struct {
|
||||
allocator: Allocator,
|
||||
@@ -80,13 +72,22 @@ pub const Jar = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forRequest(self: *Jar, target_uri: *const Uri, writer: anytype, opts: LookupOpts) !void {
|
||||
pub const LookupOpts = struct {
|
||||
request_time: ?i64 = null,
|
||||
origin_url: ?URL = null,
|
||||
is_http: bool,
|
||||
is_navigation: bool = true,
|
||||
prefix: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
// FIXME: Invalid behavior.
|
||||
pub fn forRequest(self: *Jar, target_url: URL, writer: anytype, opts: LookupOpts) !void {
|
||||
const target = PreparedUri{
|
||||
.host = (target_uri.host orelse return error.InvalidURI).percent_encoded,
|
||||
.path = target_uri.path.percent_encoded,
|
||||
.secure = std.mem.eql(u8, target_uri.scheme, "https"),
|
||||
.host = target_url.getHostname(),
|
||||
.path = target_url.getPath(),
|
||||
.secure = target_url.isSecure(),
|
||||
};
|
||||
const same_site = try areSameSite(opts.origin_uri, target.host);
|
||||
const same_site = try areSameSite(opts.origin_url, target.host);
|
||||
|
||||
removeExpired(self, opts.request_time);
|
||||
|
||||
@@ -109,8 +110,8 @@ pub const Jar = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn populateFromResponse(self: *Jar, uri: *const Uri, set_cookie: []const u8) !void {
|
||||
const c = Cookie.parse(self.allocator, uri, set_cookie) catch |err| {
|
||||
pub fn populateFromResponse(self: *Jar, url: URL, set_cookie: []const u8) !void {
|
||||
const c = Cookie.parse(self.allocator, url, set_cookie) catch |err| {
|
||||
log.warn(.web_api, "cookie parse failed", .{ .raw = set_cookie, .err = err });
|
||||
return;
|
||||
};
|
||||
@@ -148,9 +149,9 @@ fn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
fn areSameSite(origin_uri_: ?*const std.Uri, target_host: []const u8) !bool {
|
||||
const origin_uri = origin_uri_ orelse return true;
|
||||
const origin_host = (origin_uri.host orelse return error.InvalidURI).percent_encoded;
|
||||
fn areSameSite(maybe_origin_url: ?URL, target_host: []const u8) !bool {
|
||||
const origin_url = maybe_origin_url orelse return true;
|
||||
const origin_host = origin_url.host();
|
||||
|
||||
// common case
|
||||
if (std.mem.eql(u8, target_host, origin_host)) {
|
||||
@@ -161,6 +162,7 @@ fn areSameSite(origin_uri_: ?*const std.Uri, target_host: []const u8) !bool {
|
||||
}
|
||||
|
||||
fn findSecondLevelDomain(host: []const u8) []const u8 {
|
||||
// TODO: maybe reverseIterator?
|
||||
var i = std.mem.lastIndexOfScalar(u8, host, '.') orelse return host;
|
||||
while (true) {
|
||||
i = std.mem.lastIndexOfScalar(u8, host[0..i], '.') orelse return host;
|
||||
@@ -206,17 +208,8 @@ pub const Cookie = struct {
|
||||
// Invalid attribute values? Ignore.
|
||||
// Duplicate attributes - use the last valid
|
||||
// Value-less attributes with a value? Ignore the value
|
||||
pub fn parse(allocator: Allocator, uri: *const std.Uri, str: []const u8) !Cookie {
|
||||
if (str.len == 0) {
|
||||
// this check is necessary, `std.mem.minMax` asserts len > 0
|
||||
return error.Empty;
|
||||
}
|
||||
{
|
||||
const min, const max = std.mem.minMax(u8, str);
|
||||
if (min < 32 or max > 126) {
|
||||
return error.InvalidByteSequence;
|
||||
}
|
||||
}
|
||||
pub fn parse(allocator: Allocator, url: URL, str: []const u8) !Cookie {
|
||||
try validateCookieString(str);
|
||||
|
||||
const cookie_name, const cookie_value, const rest = parseNameValue(str) catch {
|
||||
return error.InvalidNameValue;
|
||||
@@ -278,8 +271,8 @@ pub const Cookie = struct {
|
||||
const aa = arena.allocator();
|
||||
const owned_name = try aa.dupe(u8, cookie_name);
|
||||
const owned_value = try aa.dupe(u8, cookie_value);
|
||||
const owned_path = try parsePath(aa, uri, path);
|
||||
const owned_domain = try parseDomain(aa, uri, domain);
|
||||
const owned_path = try parsePath(aa, url, path);
|
||||
const owned_domain = try parseDomain(aa, url, domain);
|
||||
|
||||
var normalized_expires: ?f64 = null;
|
||||
if (max_age) |ma| {
|
||||
@@ -321,37 +314,89 @@ pub const Cookie = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parsePath(arena: Allocator, uri: ?*const std.Uri, explicit_path: ?[]const u8) ![]const u8 {
|
||||
// path attribute value either begins with a '/' or we
|
||||
// ignore it and use the "default-path" algorithm
|
||||
if (explicit_path) |path| {
|
||||
if (path.len > 0 and path[0] == '/') {
|
||||
return try arena.dupe(u8, path);
|
||||
const ValidateCookieError = error{ Empty, InvalidByteSequence };
|
||||
|
||||
/// Returns an error if cookie str length is 0
|
||||
/// or contains characters outside of the ascii range 32...126.
|
||||
fn validateCookieString(str: []const u8) ValidateCookieError!void {
|
||||
if (str.len == 0) {
|
||||
return error.Empty;
|
||||
}
|
||||
|
||||
const vec_size_suggestion = std.simd.suggestVectorLength(u8);
|
||||
var offset: usize = 0;
|
||||
|
||||
// Fast path if possible.
|
||||
if (comptime vec_size_suggestion) |size| {
|
||||
while (str.len - offset >= size) : (offset += size) {
|
||||
const Vec = @Vector(size, u8);
|
||||
const space: Vec = @splat(32);
|
||||
const tilde: Vec = @splat(126);
|
||||
const chunk: Vec = str[offset..][0..size].*;
|
||||
|
||||
// This creates a mask where invalid characters represented
|
||||
// as ones and valid characters as zeros. We then bitCast this
|
||||
// into an unsigned integer. If the integer is not equal to 0,
|
||||
// we know that we've invalid characters in this chunk.
|
||||
// @popCount can also be used but using integers are simpler.
|
||||
const mask = (@intFromBool(chunk < space) | @intFromBool(chunk > tilde));
|
||||
const reduced: std.meta.Int(.unsigned, size) = @bitCast(mask);
|
||||
|
||||
// Got match.
|
||||
if (reduced != 0) {
|
||||
return error.InvalidByteSequence;
|
||||
}
|
||||
}
|
||||
|
||||
// Means str.len % size == 0; we also know str.len != 0.
|
||||
// Cookie is valid.
|
||||
if (offset == str.len) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// default-path
|
||||
const url_path = (uri orelse return "/").path;
|
||||
// Either remaining slice or the original if fast path not taken.
|
||||
const slice = str[offset..];
|
||||
// Slow path.
|
||||
const min, const max = std.mem.minMax(u8, slice);
|
||||
if (min < 32 or max > 126) {
|
||||
return error.InvalidByteSequence;
|
||||
}
|
||||
}
|
||||
|
||||
const either = url_path.percent_encoded;
|
||||
if (either.len == 0 or (either.len == 1 and either[0] == '/')) {
|
||||
pub fn parsePath(arena: Allocator, maybe_url: ?URL, maybe_explicit_path: ?[]const u8) ![]const u8 {
|
||||
// path attribute value either begins with a '/' or we
|
||||
// ignore it and use the "default-path" algorithm
|
||||
if (maybe_explicit_path) |path| {
|
||||
if (path.len > 0 and path[0] == '/') {
|
||||
return arena.dupe(u8, path);
|
||||
}
|
||||
}
|
||||
|
||||
const url_path = blk: {
|
||||
if (maybe_url) |url| {
|
||||
break :blk url.getPath();
|
||||
}
|
||||
|
||||
return "/";
|
||||
};
|
||||
|
||||
if (url_path.len == 0 or (url_path.len == 1 and url_path[0] == '/')) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
var owned_path: []const u8 = try percentEncode(arena, url_path, isPathChar);
|
||||
const last = std.mem.lastIndexOfScalar(u8, owned_path[1..], '/') orelse {
|
||||
const last = std.mem.lastIndexOfScalar(u8, url_path[1..], '/') orelse {
|
||||
return "/";
|
||||
};
|
||||
return try arena.dupe(u8, owned_path[0 .. last + 1]);
|
||||
|
||||
return arena.dupe(u8, url_path[0 .. last + 1]);
|
||||
}
|
||||
|
||||
pub fn parseDomain(arena: Allocator, uri: ?*const std.Uri, explicit_domain: ?[]const u8) ![]const u8 {
|
||||
pub fn parseDomain(arena: Allocator, maybe_url: ?URL, explicit_domain: ?[]const u8) ![]const u8 {
|
||||
var encoded_host: ?[]const u8 = null;
|
||||
if (uri) |uri_| {
|
||||
const uri_host = uri_.host orelse return error.InvalidURI;
|
||||
const host = try percentEncode(arena, uri_host, isHostChar);
|
||||
_ = toLower(host);
|
||||
encoded_host = host;
|
||||
if (maybe_url) |url| {
|
||||
const url_host = url.getHostname();
|
||||
encoded_host = url_host;
|
||||
}
|
||||
|
||||
if (explicit_domain) |domain| {
|
||||
@@ -380,19 +425,6 @@ pub const Cookie = struct {
|
||||
return encoded_host orelse return error.InvalidDomain; // default-domain
|
||||
}
|
||||
|
||||
pub fn percentEncode(arena: Allocator, component: std.Uri.Component, comptime isValidChar: fn (u8) bool) ![]u8 {
|
||||
switch (component) {
|
||||
.raw => |str| {
|
||||
var aw = try std.Io.Writer.Allocating.initCapacity(arena, str.len);
|
||||
try std.Uri.Component.percentEncode(&aw.writer, str, isValidChar);
|
||||
return aw.written(); // @memory retains memory used before growing
|
||||
},
|
||||
.percent_encoded => |str| {
|
||||
return try arena.dupe(u8, str);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isHostChar(c: u8) bool {
|
||||
return switch (c) {
|
||||
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
|
||||
@@ -560,37 +592,40 @@ test "Jar: add" {
|
||||
defer jar.deinit();
|
||||
try expectCookies(&.{}, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9000;Max-Age=0"), now);
|
||||
const test_url = try URL.parse("http://lightpanda.io/", null);
|
||||
defer test_url.deinit();
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9000;Max-Age=0"), now);
|
||||
try expectCookies(&.{}, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9000"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9000"), now);
|
||||
try expectCookies(&.{.{ "over", "9000" }}, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9000!!"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9000!!"), now);
|
||||
try expectCookies(&.{.{ "over", "9000!!" }}, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "spice=flow"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "spice=flow"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flow" } }, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "spice=flows;Path=/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "spice=flows;Path=/"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" } }, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9001;Path=/other"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9001;Path=/other"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9001" } }, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9002;Path=/;Domain=lightpanda.io"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9002;Path=/;Domain=lightpanda.io"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9001" }, .{ "over", "9002" } }, jar);
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=x;Path=/other;Max-Age=-200"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=x;Path=/other;Max-Age=-200"), now);
|
||||
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9002" } }, jar);
|
||||
}
|
||||
|
||||
test "Jar: forRequest" {
|
||||
const expectCookies = struct {
|
||||
fn expect(expected: []const u8, jar: *Jar, target_uri: Uri, opts: LookupOpts) !void {
|
||||
fn expect(expected: []const u8, jar: *Jar, target_url: URL, opts: Jar.LookupOpts) !void {
|
||||
var arr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer arr.deinit(testing.allocator);
|
||||
try jar.forRequest(&target_uri, arr.writer(testing.allocator), opts);
|
||||
try jar.forRequest(target_url, arr.writer(testing.allocator), opts);
|
||||
try testing.expectEqual(expected, arr.items);
|
||||
}
|
||||
}.expect;
|
||||
@@ -600,131 +635,142 @@ test "Jar: forRequest" {
|
||||
var jar = Jar.init(testing.allocator);
|
||||
defer jar.deinit();
|
||||
|
||||
const test_uri_2 = Uri.parse("http://test.lightpanda.io/") catch unreachable;
|
||||
const test_url = try URL.parse("http://lightpanda.io/", null);
|
||||
defer test_url.deinit();
|
||||
|
||||
const test_url_2 = try URL.parse("http://test.lightpanda.io/", null);
|
||||
defer test_url_2.deinit();
|
||||
|
||||
{
|
||||
// test with no cookies
|
||||
try expectCookies("", &jar, test_uri, .{ .is_http = true });
|
||||
try expectCookies("", &jar, test_url, .{ .is_http = true });
|
||||
}
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global1=1"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global2=2;Max-Age=30;domain=lightpanda.io"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "path1=3;Path=/about"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "path2=4;Path=/docs/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "secure=5;Secure"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "sitenone=6;SameSite=None;Path=/x/;Secure"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "sitelax=7;SameSite=Lax;Path=/x/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "sitestrict=8;SameSite=Strict;Path=/x/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri_2, "domain1=9;domain=test.lightpanda.io"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "global1=1"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "global2=2;Max-Age=30;domain=lightpanda.io"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "path1=3;Path=/about"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "path2=4;Path=/docs/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "secure=5;Secure"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "sitenone=6;SameSite=None;Path=/x/;Secure"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "sitelax=7;SameSite=Lax;Path=/x/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url, "sitestrict=8;SameSite=Strict;Path=/x/"), now);
|
||||
try jar.add(try Cookie.parse(testing.allocator, test_url_2, "domain1=9;domain=test.lightpanda.io"), now);
|
||||
|
||||
// nothing fancy here
|
||||
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .is_http = true });
|
||||
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .is_navigation = false, .is_http = true });
|
||||
try expectCookies("global1=1; global2=2", &jar, test_url, .{ .is_http = true });
|
||||
try expectCookies("global1=1; global2=2", &jar, test_url, .{ .origin_url = test_url, .is_navigation = false, .is_http = true });
|
||||
|
||||
// We reuse this URL to reparse.
|
||||
const reuse_url = try URL.parse("http://anothersitelightpanda.io/", null);
|
||||
defer reuse_url.deinit();
|
||||
|
||||
// We have a cookie where Domain=lightpanda.io
|
||||
// This should _not_ match xyxlightpanda.io
|
||||
try expectCookies("", &jar, try std.Uri.parse("http://anothersitelightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("", &jar, reuse_url, .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// matching path without trailing /
|
||||
try expectCookies("global1=1; global2=2; path1=3", &jar, try std.Uri.parse("http://lightpanda.io/about"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2; path1=3", &jar, try reuse_url.reparse("http://lightpanda.io/about"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// incomplete prefix path
|
||||
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/abou"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2", &jar, try reuse_url.reparse("http://lightpanda.io/abou"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// path doesn't match
|
||||
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/aboutus"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2", &jar, try reuse_url.reparse("http://lightpanda.io/aboutus"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// path doesn't match cookie directory
|
||||
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/docs"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2", &jar, try reuse_url.reparse("http://lightpanda.io/docs"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// exact directory match
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, try reuse_url.reparse("http://lightpanda.io/docs/"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// sub directory match
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/more"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, try reuse_url.reparse("http://lightpanda.io/docs/more"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// secure
|
||||
try expectCookies("global1=1; global2=2; secure=5", &jar, try std.Uri.parse("https://lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global1=1; global2=2; secure=5", &jar, try reuse_url.reparse("https://lightpanda.io/"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// navigational cross domain, secure
|
||||
try expectCookies("global1=1; global2=2; secure=5; sitenone=6; sitelax=7", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
const example_com_url = try URL.parse("https://example.com/", null);
|
||||
defer example_com_url.deinit();
|
||||
|
||||
try expectCookies("global1=1; global2=2; secure=5; sitenone=6; sitelax=7", &jar, try reuse_url.reparse("https://lightpanda.io/x/"), .{
|
||||
.origin_url = example_com_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// navigational cross domain, insecure
|
||||
try expectCookies("global1=1; global2=2; sitelax=7", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
try expectCookies("global1=1; global2=2; sitelax=7", &jar, try reuse_url.reparse("http://lightpanda.io/x/"), .{
|
||||
.origin_url = example_com_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// non-navigational cross domain, insecure
|
||||
try expectCookies("", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
try expectCookies("", &jar, try reuse_url.reparse("http://lightpanda.io/x/"), .{
|
||||
.origin_url = example_com_url,
|
||||
.is_http = true,
|
||||
.is_navigation = false,
|
||||
});
|
||||
|
||||
// non-navigational cross domain, secure
|
||||
try expectCookies("sitenone=6", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
try expectCookies("sitenone=6", &jar, try reuse_url.reparse("https://lightpanda.io/x/"), .{
|
||||
.origin_url = example_com_url,
|
||||
.is_http = true,
|
||||
.is_navigation = false,
|
||||
});
|
||||
|
||||
// non-navigational same origin
|
||||
try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://lightpanda.io/")),
|
||||
try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try reuse_url.reparse("http://lightpanda.io/x/"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
.is_navigation = false,
|
||||
});
|
||||
|
||||
// exact domain match + suffix
|
||||
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://test.lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global2=2; domain1=9", &jar, try reuse_url.reparse("http://test.lightpanda.io/"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// domain suffix match + suffix
|
||||
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://1.test.lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global2=2; domain1=9", &jar, try reuse_url.reparse("http://1.test.lightpanda.io/"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// non-matching domain
|
||||
try expectCookies("global2=2", &jar, try std.Uri.parse("http://other.lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
try expectCookies("global2=2", &jar, try reuse_url.reparse("http://other.lightpanda.io/"), .{
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
const l = jar.cookies.items.len;
|
||||
try expectCookies("global1=1", &jar, test_uri, .{
|
||||
try expectCookies("global1=1", &jar, test_url, .{
|
||||
.request_time = now + 100,
|
||||
.origin_uri = &test_uri,
|
||||
.origin_url = test_url,
|
||||
.is_http = true,
|
||||
});
|
||||
try testing.expectEqual(l - 1, jar.cookies.items.len);
|
||||
@@ -920,9 +966,11 @@ const ExpectedCookie = struct {
|
||||
same_site: Cookie.SameSite = .lax,
|
||||
};
|
||||
|
||||
fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u8) !void {
|
||||
const uri = try Uri.parse(url);
|
||||
var cookie = try Cookie.parse(testing.allocator, &uri, set_cookie);
|
||||
fn expectCookie(expected: ExpectedCookie, url_str: []const u8, set_cookie: []const u8) !void {
|
||||
const url = try URL.parse(url_str, null);
|
||||
defer url.deinit();
|
||||
|
||||
var cookie = try Cookie.parse(testing.allocator, url, set_cookie);
|
||||
defer cookie.deinit();
|
||||
|
||||
try testing.expectEqual(expected.name, cookie.name);
|
||||
@@ -936,9 +984,11 @@ fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u
|
||||
try testing.expectDelta(expected.expires, cookie.expires, 2.0);
|
||||
}
|
||||
|
||||
fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) !void {
|
||||
const uri = if (url) |u| try Uri.parse(u) else test_uri;
|
||||
var cookie = try Cookie.parse(testing.allocator, &uri, set_cookie);
|
||||
fn expectAttribute(expected: anytype, maybe_url_str: ?[]const u8, set_cookie: []const u8) !void {
|
||||
const url = try URL.parse(if (maybe_url_str) |url_str| url_str else "https://lightpanda.io/", null);
|
||||
defer url.deinit();
|
||||
|
||||
var cookie = try Cookie.parse(testing.allocator, url, set_cookie);
|
||||
defer cookie.deinit();
|
||||
|
||||
inline for (@typeInfo(@TypeOf(expected)).@"struct".fields) |f| {
|
||||
@@ -953,9 +1003,7 @@ fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8)
|
||||
}
|
||||
}
|
||||
|
||||
fn expectError(expected: anyerror, url: ?[]const u8, set_cookie: []const u8) !void {
|
||||
const uri = if (url) |u| try Uri.parse(u) else test_uri;
|
||||
try testing.expectError(expected, Cookie.parse(testing.allocator, &uri, set_cookie));
|
||||
fn expectError(expected: anyerror, maybe_url_str: ?[]const u8, set_cookie: []const u8) !void {
|
||||
const url = try URL.parse(if (maybe_url_str) |url_str| url_str else "https://lightpanda.io/", null);
|
||||
try testing.expectError(expected, Cookie.parse(testing.allocator, url, set_cookie));
|
||||
}
|
||||
|
||||
const test_uri = Uri.parse("http://lightpanda.io/") catch unreachable;
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
const ReadableStream = @This();
|
||||
const ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig");
|
||||
@@ -30,31 +31,57 @@ const State = union(enum) {
|
||||
readable,
|
||||
closed: ?[]const u8,
|
||||
cancelled: ?[]const u8,
|
||||
errored: Env.JsObject,
|
||||
errored: js.Object,
|
||||
};
|
||||
|
||||
// This promise resolves when a stream is canceled.
|
||||
cancel_resolver: Env.PersistentPromiseResolver,
|
||||
closed_resolver: Env.PersistentPromiseResolver,
|
||||
reader_resolver: ?Env.PersistentPromiseResolver = null,
|
||||
cancel_resolver: js.PersistentPromiseResolver,
|
||||
closed_resolver: js.PersistentPromiseResolver,
|
||||
reader_resolver: ?js.PersistentPromiseResolver = null,
|
||||
|
||||
locked: bool = false,
|
||||
state: State = .readable,
|
||||
|
||||
cancel_fn: ?Env.Function = null,
|
||||
pull_fn: ?Env.Function = null,
|
||||
cancel_fn: ?js.Function = null,
|
||||
pull_fn: ?js.Function = null,
|
||||
|
||||
strategy: QueueingStrategy,
|
||||
queue: std.ArrayListUnmanaged([]const u8) = .empty,
|
||||
queue: std.ArrayListUnmanaged(Chunk) = .empty,
|
||||
|
||||
pub const Chunk = union(enum) {
|
||||
// the order matters, sorry.
|
||||
uint8array: js.TypedArray(u8),
|
||||
string: []const u8,
|
||||
|
||||
pub fn dupe(self: Chunk, allocator: Allocator) !Chunk {
|
||||
return switch (self) {
|
||||
.string => |str| .{ .string = try allocator.dupe(u8, str) },
|
||||
.uint8array => |arr| .{ .uint8array = try arr.dupe(allocator) },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const ReadableStreamReadResult = struct {
|
||||
const ValueUnion =
|
||||
union(enum) { data: []const u8, empty: void };
|
||||
|
||||
value: ValueUnion,
|
||||
done: bool,
|
||||
value: Value = .empty,
|
||||
|
||||
pub fn get_value(self: *const ReadableStreamReadResult) ValueUnion {
|
||||
const Value = union(enum) {
|
||||
empty,
|
||||
data: Chunk,
|
||||
};
|
||||
|
||||
pub fn init(chunk: Chunk, done: bool) ReadableStreamReadResult {
|
||||
if (done) {
|
||||
return .{ .done = true, .value = .empty };
|
||||
}
|
||||
|
||||
return .{
|
||||
.done = false,
|
||||
.value = .{ .data = chunk },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_value(self: *const ReadableStreamReadResult) Value {
|
||||
return self.value;
|
||||
}
|
||||
|
||||
@@ -64,22 +91,22 @@ pub const ReadableStreamReadResult = struct {
|
||||
};
|
||||
|
||||
const UnderlyingSource = struct {
|
||||
start: ?Env.Function = null,
|
||||
pull: ?Env.Function = null,
|
||||
cancel: ?Env.Function = null,
|
||||
start: ?js.Function = null,
|
||||
pull: ?js.Function = null,
|
||||
cancel: ?js.Function = null,
|
||||
type: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
const QueueingStrategy = struct {
|
||||
size: ?Env.Function = null,
|
||||
size: ?js.Function = null,
|
||||
high_water_mark: u32 = 1,
|
||||
};
|
||||
|
||||
pub fn constructor(underlying: ?UnderlyingSource, _strategy: ?QueueingStrategy, page: *Page) !*ReadableStream {
|
||||
const strategy: QueueingStrategy = _strategy orelse .{};
|
||||
|
||||
const cancel_resolver = try page.main_context.createPersistentPromiseResolver(.self);
|
||||
const closed_resolver = try page.main_context.createPersistentPromiseResolver(.self);
|
||||
const cancel_resolver = try page.js.createPromiseResolver(.self);
|
||||
const closed_resolver = try page.js.createPromiseResolver(.self);
|
||||
|
||||
const stream = try page.arena.create(ReadableStream);
|
||||
stream.* = ReadableStream{
|
||||
@@ -119,7 +146,7 @@ pub fn get_locked(self: *const ReadableStream) bool {
|
||||
return self.locked;
|
||||
}
|
||||
|
||||
pub fn _cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !Env.Promise {
|
||||
pub fn _cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !js.Promise {
|
||||
if (self.locked) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
const ReadableStream = @import("./ReadableStream.zig");
|
||||
const ReadableStreamReadResult = @import("./ReadableStream.zig").ReadableStreamReadResult;
|
||||
@@ -51,17 +51,17 @@ pub fn _close(self: *ReadableStreamDefaultController, _reason: ?[]const u8, page
|
||||
// to discard, must use cancel.
|
||||
}
|
||||
|
||||
pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: []const u8, page: *Page) !void {
|
||||
pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: ReadableStream.Chunk, page: *Page) !void {
|
||||
const stream = self.stream;
|
||||
|
||||
if (stream.state != .readable) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const duped_chunk = try page.arena.dupe(u8, chunk);
|
||||
const duped_chunk = try chunk.dupe(page.arena);
|
||||
|
||||
if (self.stream.reader_resolver) |*rr| {
|
||||
try rr.resolve(ReadableStreamReadResult{ .value = .{ .data = duped_chunk }, .done = false });
|
||||
try rr.resolve(ReadableStreamReadResult.init(duped_chunk, false));
|
||||
self.stream.reader_resolver = null;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: []const u8, page:
|
||||
try self.stream.pullIf();
|
||||
}
|
||||
|
||||
pub fn _error(self: *ReadableStreamDefaultController, err: Env.JsObject) !void {
|
||||
pub fn _error(self: *ReadableStreamDefaultController, err: js.Object) !void {
|
||||
self.stream.state = .{ .errored = err };
|
||||
|
||||
if (self.stream.reader_resolver) |*rr| {
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const log = @import("../../log.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const ReadableStream = @import("./ReadableStream.zig");
|
||||
const ReadableStreamReadResult = @import("./ReadableStream.zig").ReadableStreamReadResult;
|
||||
@@ -32,58 +32,41 @@ pub fn constructor(stream: *ReadableStream) ReadableStreamDefaultReader {
|
||||
return .{ .stream = stream };
|
||||
}
|
||||
|
||||
pub fn get_closed(self: *const ReadableStreamDefaultReader) Env.Promise {
|
||||
pub fn get_closed(self: *const ReadableStreamDefaultReader) js.Promise {
|
||||
return self.stream.closed_resolver.promise();
|
||||
}
|
||||
|
||||
pub fn _cancel(self: *ReadableStreamDefaultReader, reason: ?[]const u8, page: *Page) !Env.Promise {
|
||||
pub fn _cancel(self: *ReadableStreamDefaultReader, reason: ?[]const u8, page: *Page) !js.Promise {
|
||||
return try self.stream._cancel(reason, page);
|
||||
}
|
||||
|
||||
pub fn _read(self: *const ReadableStreamDefaultReader, page: *Page) !Env.Promise {
|
||||
pub fn _read(self: *const ReadableStreamDefaultReader, page: *Page) !js.Promise {
|
||||
const stream = self.stream;
|
||||
|
||||
switch (stream.state) {
|
||||
.readable => {
|
||||
if (stream.queue.items.len > 0) {
|
||||
const data = self.stream.queue.orderedRemove(0);
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
|
||||
try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false });
|
||||
const promise = page.js.resolvePromise(ReadableStreamReadResult.init(data, false));
|
||||
try self.stream.pullIf();
|
||||
return resolver.promise();
|
||||
} else {
|
||||
if (self.stream.reader_resolver) |rr| {
|
||||
return rr.promise();
|
||||
} else {
|
||||
const persistent_resolver = try page.main_context.createPersistentPromiseResolver(.page);
|
||||
self.stream.reader_resolver = persistent_resolver;
|
||||
return persistent_resolver.promise();
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
if (self.stream.reader_resolver) |rr| {
|
||||
return rr.promise();
|
||||
}
|
||||
const persistent_resolver = try page.js.createPromiseResolver(.page);
|
||||
self.stream.reader_resolver = persistent_resolver;
|
||||
return persistent_resolver.promise();
|
||||
},
|
||||
.closed => |_| {
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
|
||||
if (stream.queue.items.len > 0) {
|
||||
const data = self.stream.queue.orderedRemove(0);
|
||||
try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false });
|
||||
} else {
|
||||
try resolver.resolve(ReadableStreamReadResult{ .value = .empty, .done = true });
|
||||
return page.js.resolvePromise(ReadableStreamReadResult.init(data, false));
|
||||
}
|
||||
|
||||
return resolver.promise();
|
||||
},
|
||||
.cancelled => |_| {
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
try resolver.resolve(ReadableStreamReadResult{ .value = .empty, .done = true });
|
||||
return resolver.promise();
|
||||
},
|
||||
.errored => |err| {
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
try resolver.reject(err);
|
||||
return resolver.promise();
|
||||
return page.js.resolvePromise(ReadableStreamReadResult{ .done = true });
|
||||
},
|
||||
.cancelled => |_| return page.js.resolvePromise(ReadableStreamReadResult{ .value = .empty, .done = true }),
|
||||
.errored => |err| return page.js.rejectPromise(err),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
|
||||
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");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const FormData = @import("../xhr/form_data.zig").FormData;
|
||||
|
||||
@@ -35,175 +37,180 @@ 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 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();
|
||||
pub fn destructor(self: *const URL) void {
|
||||
// Not tracked by arena.
|
||||
return ada.free(self.internal);
|
||||
}
|
||||
|
||||
// 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);
|
||||
/// Initializes a `URL` from given `internal`.
|
||||
/// Note that this copies the given `internal`; meaning 2 instances
|
||||
/// of it has to be tracked separately.
|
||||
pub fn constructFromInternal(arena: Allocator, internal: ada.URL) !URL {
|
||||
const copy = ada.copy(internal);
|
||||
|
||||
return .{
|
||||
.internal = copy,
|
||||
.search_params = try prepareSearchParams(arena, copy),
|
||||
};
|
||||
}
|
||||
|
||||
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 search = ada.getSearch(internal);
|
||||
// Empty.
|
||||
if (search.data == null) return .{};
|
||||
|
||||
const slice = search.data[0..search.length];
|
||||
const search_params = URLSearchParams.initFromString(arena, slice);
|
||||
// After a call to this function, search params are tracked by
|
||||
// `search_params`. So we reset the internal's search.
|
||||
ada.clearSearch(internal);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
if (self.search_params.get_size() > 0) {
|
||||
try aw.writer.writeByte('?');
|
||||
try self.search_params.write(&aw.writer);
|
||||
}
|
||||
|
||||
{
|
||||
const fragment = uriComponentNullStr(self.uri.fragment);
|
||||
if (fragment.len > 0) {
|
||||
try aw.writer.writeByte('#');
|
||||
try aw.writer.writeAll(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
return aw.written();
|
||||
// Alias to get_href.
|
||||
pub fn _toString(self: *const URL, page: *Page) ![]const u8 {
|
||||
return self.get_href(page);
|
||||
}
|
||||
|
||||
pub fn get_protocol(self: *URL, page: *Page) ![]const u8 {
|
||||
return try std.mem.concat(page.arena, u8, &[_][]const u8{ self.uri.scheme, ":" });
|
||||
// Getters.
|
||||
|
||||
pub fn get_searchParams(self: *URL) *URLSearchParams {
|
||||
return &self.search_params;
|
||||
}
|
||||
|
||||
pub fn get_username(self: *URL) []const u8 {
|
||||
return uriComponentNullStr(self.uri.user);
|
||||
}
|
||||
|
||||
pub fn get_password(self: *URL) []const u8 {
|
||||
return uriComponentNullStr(self.uri.password);
|
||||
}
|
||||
|
||||
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_hostname(self: *URL) []const u8 {
|
||||
return uriComponentNullStr(self.uri.host);
|
||||
}
|
||||
|
||||
pub fn get_port(self: *URL, page: *Page) ![]const u8 {
|
||||
pub fn get_origin(self: *const URL, page: *Page) ![]const u8 {
|
||||
const arena = page.arena;
|
||||
if (self.uri.port == null) return try arena.dupe(u8, "");
|
||||
// `ada.getOrigin` allocates memory in order to find the `origin`.
|
||||
// We'd like to use our arena allocator for such case;
|
||||
// so here we allocate the `origin` in page arena and free the original.
|
||||
const origin = ada.getOrigin(self.internal);
|
||||
// `OwnedString` itself is not heap allocated so this is safe.
|
||||
defer ada.freeOwnedString(.{ .data = origin.ptr, .length = origin.len });
|
||||
|
||||
var aw = std.Io.Writer.Allocating.init(arena);
|
||||
try aw.writer.printInt(self.uri.port.?, 10, .lower, .{});
|
||||
return aw.written();
|
||||
return arena.dupe(u8, origin);
|
||||
}
|
||||
|
||||
pub fn get_pathname(self: *URL) []const u8 {
|
||||
if (uriComponentStr(self.uri.path).len == 0) return "/";
|
||||
return uriComponentStr(self.uri.path);
|
||||
pub fn get_href(self: *const URL, page: *Page) ![]const u8 {
|
||||
var w: Writer.Allocating = .init(page.arena);
|
||||
|
||||
const maybe_href = ada.getHrefNullable(self.internal);
|
||||
if (maybe_href.data == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const href = maybe_href.data[0..maybe_href.length];
|
||||
|
||||
const comps = ada.getComponents(self.internal);
|
||||
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 w.writer.writeByte('?');
|
||||
try self.search_params.write(&w.writer);
|
||||
}
|
||||
|
||||
// Write hash if provided before.
|
||||
const hash = self.get_hash();
|
||||
try w.writer.writeAll(hash);
|
||||
|
||||
return w.written();
|
||||
}
|
||||
|
||||
pub fn get_search(self: *URL, page: *Page) ![]const u8 {
|
||||
pub fn get_username(self: *const URL) []const u8 {
|
||||
return ada.getUsername(self.internal);
|
||||
}
|
||||
|
||||
pub fn get_password(self: *const URL) []const u8 {
|
||||
return ada.getPassword(self.internal);
|
||||
}
|
||||
|
||||
pub fn get_port(self: *const URL) []const u8 {
|
||||
return ada.getPort(self.internal);
|
||||
}
|
||||
|
||||
pub fn get_hash(self: *const URL) []const u8 {
|
||||
return ada.getHash(self.internal);
|
||||
}
|
||||
|
||||
pub fn get_host(self: *const URL) []const u8 {
|
||||
return ada.getHost(self.internal);
|
||||
}
|
||||
|
||||
pub fn get_hostname(self: *const URL) []const u8 {
|
||||
return ada.getHostname(self.internal);
|
||||
}
|
||||
|
||||
pub fn get_pathname(self: *const URL) []const u8 {
|
||||
const path = ada.getPathnameNullable(self.internal);
|
||||
// Return a slash if path is null.
|
||||
if (path.data == null) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
return path.data[0..path.length];
|
||||
}
|
||||
|
||||
// get_search depends on the current state of `search_params`.
|
||||
pub fn get_search(self: *const URL, page: *Page) ![]const u8 {
|
||||
const arena = page.arena;
|
||||
|
||||
if (self.search_params.get_size() == 0) {
|
||||
@@ -216,72 +223,85 @@ 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 {
|
||||
return ada.getProtocol(self.internal);
|
||||
}
|
||||
|
||||
// Setters.
|
||||
|
||||
// FIXME: reinit search_params?
|
||||
pub fn set_href(self: *const URL, input: []const u8) void {
|
||||
_ = ada.setHref(self.internal, input);
|
||||
}
|
||||
|
||||
pub fn set_host(self: *const URL, input: []const u8) void {
|
||||
_ = ada.setHost(self.internal, input);
|
||||
}
|
||||
|
||||
pub fn set_hostname(self: *const URL, input: []const u8) void {
|
||||
_ = ada.setHostname(self.internal, input);
|
||||
}
|
||||
|
||||
pub fn set_protocol(self: *const URL, input: []const u8) void {
|
||||
_ = ada.setProtocol(self.internal, input);
|
||||
}
|
||||
|
||||
pub fn set_username(self: *const URL, input: []const u8) void {
|
||||
_ = ada.setUsername(self.internal, input);
|
||||
}
|
||||
|
||||
pub fn set_password(self: *const URL, input: []const u8) void {
|
||||
_ = ada.setPassword(self.internal, input);
|
||||
}
|
||||
|
||||
pub fn set_port(self: *const URL, input: []const u8) void {
|
||||
_ = ada.setPort(self.internal, input);
|
||||
}
|
||||
|
||||
pub fn set_pathname(self: *const URL, input: []const u8) void {
|
||||
_ = ada.setPathname(self.internal, input);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// 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: Env.JsObject,
|
||||
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 };
|
||||
@@ -289,10 +309,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 {
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Function = Env.Function;
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
@@ -33,20 +31,20 @@ pub const XMLHttpRequestEventTarget = struct {
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .xhr },
|
||||
|
||||
onloadstart_cbk: ?Function = null,
|
||||
onprogress_cbk: ?Function = null,
|
||||
onabort_cbk: ?Function = null,
|
||||
onload_cbk: ?Function = null,
|
||||
ontimeout_cbk: ?Function = null,
|
||||
onloadend_cbk: ?Function = null,
|
||||
onreadystatechange_cbk: ?Function = null,
|
||||
onloadstart_cbk: ?js.Function = null,
|
||||
onprogress_cbk: ?js.Function = null,
|
||||
onabort_cbk: ?js.Function = null,
|
||||
onload_cbk: ?js.Function = null,
|
||||
ontimeout_cbk: ?js.Function = null,
|
||||
onloadend_cbk: ?js.Function = null,
|
||||
onreadystatechange_cbk: ?js.Function = null,
|
||||
|
||||
fn register(
|
||||
self: *XMLHttpRequestEventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
typ: []const u8,
|
||||
listener: EventHandler.Listener,
|
||||
) !?Function {
|
||||
) !?js.Function {
|
||||
const target = @as(*parser.EventTarget, @ptrCast(self));
|
||||
|
||||
// The only time this can return null if the listener is already
|
||||
@@ -69,25 +67,25 @@ pub const XMLHttpRequestEventTarget = struct {
|
||||
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
|
||||
}
|
||||
|
||||
pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Function {
|
||||
pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?js.Function {
|
||||
return self.onloadstart_cbk;
|
||||
}
|
||||
pub fn get_onprogress(self: *XMLHttpRequestEventTarget) ?Function {
|
||||
pub fn get_onprogress(self: *XMLHttpRequestEventTarget) ?js.Function {
|
||||
return self.onprogress_cbk;
|
||||
}
|
||||
pub fn get_onabort(self: *XMLHttpRequestEventTarget) ?Function {
|
||||
pub fn get_onabort(self: *XMLHttpRequestEventTarget) ?js.Function {
|
||||
return self.onabort_cbk;
|
||||
}
|
||||
pub fn get_onload(self: *XMLHttpRequestEventTarget) ?Function {
|
||||
pub fn get_onload(self: *XMLHttpRequestEventTarget) ?js.Function {
|
||||
return self.onload_cbk;
|
||||
}
|
||||
pub fn get_ontimeout(self: *XMLHttpRequestEventTarget) ?Function {
|
||||
pub fn get_ontimeout(self: *XMLHttpRequestEventTarget) ?js.Function {
|
||||
return self.ontimeout_cbk;
|
||||
}
|
||||
pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Function {
|
||||
pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?js.Function {
|
||||
return self.onloadend_cbk;
|
||||
}
|
||||
pub fn get_onreadystatechange(self: *XMLHttpRequestEventTarget) ?Function {
|
||||
pub fn get_onreadystatechange(self: *XMLHttpRequestEventTarget) ?js.Function {
|
||||
return self.onreadystatechange_cbk;
|
||||
}
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ pub const XMLHttpRequest = struct {
|
||||
self.reset();
|
||||
|
||||
self.method = try validMethod(method);
|
||||
self.url = try URL.stitch(page.arena, url, page.url.raw, .{ .null_terminated = true });
|
||||
self.url = try URL.stitch(page.arena, url, page.url.getHref(), .{ .null_terminated = true });
|
||||
self.sync = if (asyn) |b| !b else false;
|
||||
|
||||
self.state = .opened;
|
||||
|
||||
@@ -21,18 +21,18 @@ const Allocator = std.mem.Allocator;
|
||||
const json = std.json;
|
||||
|
||||
const log = @import("../log.zig");
|
||||
const js = @import("../browser/js/js.zig");
|
||||
const polyfill = @import("../browser/polyfill/polyfill.zig");
|
||||
|
||||
const App = @import("../app.zig").App;
|
||||
const Env = @import("../browser/env.zig").Env;
|
||||
const Browser = @import("../browser/browser.zig").Browser;
|
||||
const Session = @import("../browser/session.zig").Session;
|
||||
const Page = @import("../browser/page.zig").Page;
|
||||
const Inspector = @import("../browser/env.zig").Env.Inspector;
|
||||
const Incrementing = @import("../id.zig").Incrementing;
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
const LogInterceptor = @import("domains/log.zig").LogInterceptor;
|
||||
const InterceptState = @import("domains/fetch.zig").InterceptState;
|
||||
|
||||
const polyfill = @import("../browser/polyfill/polyfill.zig");
|
||||
|
||||
pub const URL_BASE = "chrome://newtab/";
|
||||
pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C";
|
||||
|
||||
@@ -329,7 +329,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
node_registry: Node.Registry,
|
||||
node_search_list: Node.Search.List,
|
||||
|
||||
inspector: Inspector,
|
||||
inspector: js.Inspector,
|
||||
isolated_worlds: std.ArrayListUnmanaged(IsolatedWorld),
|
||||
|
||||
http_proxy_changed: bool = false,
|
||||
@@ -339,6 +339,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
|
||||
intercept_state: InterceptState,
|
||||
|
||||
log_interceptor: LogInterceptor(Self),
|
||||
|
||||
// When network is enabled, we'll capture the transfer.id -> body
|
||||
// This is awfully memory intensive, but our underlying http client and
|
||||
// its users (script manager and page) correctly do not hold the body
|
||||
@@ -379,6 +381,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
.notification_arena = cdp.notification_arena.allocator(),
|
||||
.intercept_state = try InterceptState.init(allocator),
|
||||
.captured_responses = .empty,
|
||||
.log_interceptor = LogInterceptor(Self).init(allocator, self),
|
||||
};
|
||||
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
|
||||
errdefer self.deinit();
|
||||
@@ -390,6 +393,14 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
// safe to call even if never registered
|
||||
log.unregisterInterceptor();
|
||||
self.log_interceptor.deinit();
|
||||
|
||||
// Drain microtasks makes sure we don't have inspector's callback
|
||||
// in progress before deinit.
|
||||
self.cdp.browser.env.runMicrotasks();
|
||||
|
||||
self.inspector.deinit();
|
||||
|
||||
// abort all intercepted requests before closing the sesion/page
|
||||
@@ -453,7 +464,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
|
||||
pub fn getURL(self: *const Self) ?[]const u8 {
|
||||
const page = self.session.currentPage() orelse return null;
|
||||
const raw_url = page.url.raw;
|
||||
const raw_url = page.url.getHref();
|
||||
return if (raw_url.len == 0) null else raw_url;
|
||||
}
|
||||
|
||||
@@ -497,6 +508,18 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
self.cdp.browser.notification.unregister(.page_network_almost_idle, self);
|
||||
}
|
||||
|
||||
pub fn logEnable(self: *Self) void {
|
||||
log.registerInterceptor(.{
|
||||
.ctx = &self.log_interceptor,
|
||||
.done = LogInterceptor(Self).done,
|
||||
.writer = LogInterceptor(Self).writer,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn logDisable(_: *const Self) void {
|
||||
log.unregisterInterceptor();
|
||||
}
|
||||
|
||||
pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
try @import("domains/page.zig").pageRemove(self);
|
||||
@@ -661,7 +684,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
/// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.
|
||||
const IsolatedWorld = struct {
|
||||
name: []const u8,
|
||||
executor: Env.ExecutionWorld,
|
||||
executor: js.ExecutionWorld,
|
||||
grant_universal_access: bool,
|
||||
|
||||
// Polyfill loader for the isolated world.
|
||||
@@ -672,8 +695,8 @@ const IsolatedWorld = struct {
|
||||
self.executor.deinit();
|
||||
}
|
||||
pub fn removeContext(self: *IsolatedWorld) !void {
|
||||
if (self.executor.js_context == null) return error.NoIsolatedContextToRemove;
|
||||
self.executor.removeJsContext();
|
||||
if (self.executor.context == null) return error.NoIsolatedContextToRemove;
|
||||
self.executor.removeContext();
|
||||
}
|
||||
|
||||
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
|
||||
@@ -682,20 +705,18 @@ const IsolatedWorld = struct {
|
||||
// This also means this pointer becomes invalid after removePage untill a new page is created.
|
||||
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
|
||||
pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
|
||||
// if (self.executor.js_context != null) return error.Only1IsolatedContextSupported;
|
||||
if (self.executor.js_context != null) {
|
||||
// if (self.executor.context != null) return error.Only1IsolatedContextSupported;
|
||||
if (self.executor.context != null) {
|
||||
log.warn(.cdp, "not implemented", .{
|
||||
.feature = "createContext: Not implemented second isolated context creation",
|
||||
.info = "reuse existing context",
|
||||
});
|
||||
return;
|
||||
}
|
||||
_ = try self.executor.createJsContext(
|
||||
&page.window,
|
||||
_ = try self.executor.createContext(
|
||||
page,
|
||||
{},
|
||||
false,
|
||||
Env.GlobalMissingCallback.init(&self.polyfill_loader),
|
||||
js.GlobalMissingCallback.init(&self.polyfill_loader),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -704,7 +725,7 @@ const IsolatedWorld = struct {
|
||||
try self.createContext(page);
|
||||
|
||||
const loader = @import("../browser/polyfill/polyfill.zig");
|
||||
try loader.preload(arena, &self.executor.js_context.?);
|
||||
try loader.preload(arena, &self.executor.context.?);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -274,11 +274,11 @@ fn resolveNode(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
|
||||
var js_context = page.main_context;
|
||||
var js_context = page.js;
|
||||
if (params.executionContextId) |context_id| {
|
||||
if (js_context.v8_context.debugContextId() != context_id) {
|
||||
for (bc.isolated_worlds.items) |*isolated_world| {
|
||||
js_context = &(isolated_world.executor.js_context orelse return error.ContextNotFound);
|
||||
js_context = &(isolated_world.executor.context orelse return error.ContextNotFound);
|
||||
if (js_context.v8_context.debugContextId() == context_id) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ pub fn requestIntercept(arena: Allocator, bc: anytype, intercept: *const Notific
|
||||
log.debug(.cdp, "request intercept", .{
|
||||
.state = "paused",
|
||||
.id = transfer.id,
|
||||
.url = transfer.uri,
|
||||
.url = transfer.url,
|
||||
});
|
||||
// Await either continueRequest, failRequest or fulfillRequest
|
||||
|
||||
@@ -237,7 +237,7 @@ fn continueRequest(cmd: anytype) !void {
|
||||
log.debug(.cdp, "request intercept", .{
|
||||
.state = "continue",
|
||||
.id = transfer.id,
|
||||
.url = transfer.uri,
|
||||
.url = transfer.url,
|
||||
.new_url = params.url,
|
||||
});
|
||||
|
||||
@@ -342,7 +342,7 @@ fn fulfillRequest(cmd: anytype) !void {
|
||||
log.debug(.cdp, "request intercept", .{
|
||||
.state = "fulfilled",
|
||||
.id = transfer.id,
|
||||
.url = transfer.uri,
|
||||
.url = transfer.url,
|
||||
.status = params.responseCode,
|
||||
.body = params.body != null,
|
||||
});
|
||||
@@ -376,7 +376,7 @@ fn failRequest(cmd: anytype) !void {
|
||||
log.info(.cdp, "request intercept", .{
|
||||
.state = "fail",
|
||||
.id = request_id,
|
||||
.url = transfer.uri,
|
||||
.url = transfer.url,
|
||||
.reason = params.errorReason,
|
||||
});
|
||||
return cmd.sendResult(null, .{});
|
||||
@@ -420,7 +420,7 @@ pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Noti
|
||||
log.debug(.cdp, "request auth required", .{
|
||||
.state = "paused",
|
||||
.id = transfer.id,
|
||||
.url = transfer.uri,
|
||||
.url = transfer.url,
|
||||
});
|
||||
// Await continueWithAuth
|
||||
|
||||
|
||||
@@ -17,13 +17,97 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
disable,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
.enable => return enable(cmd),
|
||||
.disable => return disable(cmd),
|
||||
}
|
||||
}
|
||||
fn enable(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
bc.logEnable();
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn disable(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
bc.logDisable();
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
pub fn LogInterceptor(comptime BC: type) type {
|
||||
return struct {
|
||||
bc: *BC,
|
||||
allocating: std.Io.Writer.Allocating,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(allocator: Allocator, bc: *BC) Self {
|
||||
return .{
|
||||
.bc = bc,
|
||||
.allocating = .init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
return self.allocating.deinit();
|
||||
}
|
||||
|
||||
pub fn writer(ctx: *anyopaque, scope: log.Scope, level: log.Level) ?*std.Io.Writer {
|
||||
if (scope == .unknown_prop or scope == .telemetry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// DO NOT REMOVE this. This prevents a log message caused from a failure
|
||||
// to intercept to trigger another intercept, which could result in an
|
||||
// endless cycle.
|
||||
if (scope == .interceptor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (level == .debug) {
|
||||
return null;
|
||||
}
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
return &self.allocating.writer;
|
||||
}
|
||||
|
||||
pub fn done(ctx: *anyopaque, scope: log.Scope, level: log.Level) void {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
defer self.allocating.clearRetainingCapacity();
|
||||
|
||||
self.bc.cdp.sendEvent("Log.entryAdded", .{
|
||||
.entry = .{
|
||||
.source = switch (scope) {
|
||||
.js, .user_script, .console, .web_api, .script_event => "javascript",
|
||||
.http, .fetch, .xhr => "network",
|
||||
.telemetry, .unknown_prop, .interceptor => unreachable, // filtered out in writer above
|
||||
else => "other",
|
||||
},
|
||||
.level = switch (level) {
|
||||
.debug => "verbose",
|
||||
.info => "info",
|
||||
.warn => "warning",
|
||||
.err => "error",
|
||||
.fatal => "error",
|
||||
},
|
||||
.text = self.allocating.written(),
|
||||
.timestamp = @import("../../datetime.zig").milliTimestamp(),
|
||||
},
|
||||
}, .{
|
||||
.session_id = self.bc.session_id,
|
||||
}) catch |err| {
|
||||
log.err(.interceptor, "failed to send", .{ .err = err });
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
|
||||
const CdpStorage = @import("storage.zig");
|
||||
const Transfer = @import("../../http/Client.zig").Transfer;
|
||||
const Notification = @import("../../notification.zig").Notification;
|
||||
const URL = @import("../../url.zig").URL;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
@@ -117,15 +118,20 @@ fn deleteCookies(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const cookies = &bc.session.cookie_jar.cookies;
|
||||
|
||||
const uri = if (params.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
|
||||
const uri_ptr = if (uri) |u| &u else null;
|
||||
const maybe_url: ?URL = blk: {
|
||||
if (params.url) |url| {
|
||||
break :blk URL.parse(url, null) catch return error.InvalidParams;
|
||||
}
|
||||
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
var index = cookies.items.len;
|
||||
while (index > 0) {
|
||||
index -= 1;
|
||||
const cookie = &cookies.items[index];
|
||||
const domain = try Cookie.parseDomain(cmd.arena, uri_ptr, params.domain);
|
||||
const path = try Cookie.parsePath(cmd.arena, uri_ptr, params.path);
|
||||
const domain = try Cookie.parseDomain(cmd.arena, maybe_url, params.domain);
|
||||
const path = try Cookie.parsePath(cmd.arena, maybe_url, params.path);
|
||||
|
||||
// We do not want to use Cookie.appliesTo here. As a Cookie with a shorter path would match.
|
||||
// Similar to deduplicating with areCookiesEqual, except domain and path are optional.
|
||||
@@ -133,6 +139,12 @@ fn deleteCookies(cmd: anytype) !void {
|
||||
cookies.swapRemove(index).deinit();
|
||||
}
|
||||
}
|
||||
|
||||
// Deinit URL if we had.
|
||||
if (maybe_url) |url| {
|
||||
url.deinit();
|
||||
}
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
@@ -173,17 +185,18 @@ fn getCookies(cmd: anytype) !void {
|
||||
const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{};
|
||||
|
||||
// If not specified, use the URLs of the page and all of its subframes. TODO subframes
|
||||
const page_url = if (bc.session.page) |*page| page.url.raw else null; // @speed: avoid repasing the URL
|
||||
const page_url = if (bc.session.page) |page| page.url.getHref() else null; // @speed: avoid repasing the URL
|
||||
const param_urls = params.urls orelse &[_][]const u8{page_url orelse return error.InvalidParams};
|
||||
|
||||
var urls = try std.ArrayListUnmanaged(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len);
|
||||
for (param_urls) |url| {
|
||||
const uri = std.Uri.parse(url) catch return error.InvalidParams;
|
||||
for (param_urls) |url_str| {
|
||||
const url = URL.parse(url_str, null) catch return error.InvalidParams;
|
||||
//defer url.deinit();
|
||||
|
||||
urls.appendAssumeCapacity(.{
|
||||
.host = try Cookie.parseDomain(cmd.arena, &uri, null),
|
||||
.path = try Cookie.parsePath(cmd.arena, &uri, null),
|
||||
.secure = std.mem.eql(u8, uri.scheme, "https"),
|
||||
.host = try Cookie.parseDomain(cmd.arena, url, null),
|
||||
.path = try Cookie.parsePath(cmd.arena, url, null),
|
||||
.secure = url.isSecure(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -247,7 +260,7 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification.
|
||||
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
|
||||
.frameId = target_id,
|
||||
.loaderId = bc.loader_id,
|
||||
.documentUrl = DocumentUrlWriter.init(&page.url.uri),
|
||||
.documentUrl = DocumentUrlWriter.init(page.url),
|
||||
.request = TransferAsRequestWriter.init(transfer),
|
||||
.initiator = .{ .type = "other" },
|
||||
}, .{ .session_id = session_id });
|
||||
@@ -300,23 +313,17 @@ pub const TransferAsRequestWriter = struct {
|
||||
try jws.objectField("url");
|
||||
try jws.beginWriteRaw();
|
||||
try writer.writeByte('\"');
|
||||
try transfer.uri.writeToStream(writer, .{
|
||||
.scheme = true,
|
||||
.authentication = true,
|
||||
.authority = true,
|
||||
.path = true,
|
||||
.query = true,
|
||||
});
|
||||
try transfer.url.writeToStream(writer);
|
||||
try writer.writeByte('\"');
|
||||
jws.endWriteRaw();
|
||||
}
|
||||
|
||||
{
|
||||
if (transfer.uri.fragment) |frag| {
|
||||
if (transfer.url.getFragment()) |frag| {
|
||||
try jws.objectField("urlFragment");
|
||||
try jws.beginWriteRaw();
|
||||
try writer.writeAll("\"#");
|
||||
try writer.writeAll(frag.percent_encoded);
|
||||
try writer.writeAll(frag);
|
||||
try writer.writeByte('\"');
|
||||
jws.endWriteRaw();
|
||||
}
|
||||
@@ -370,13 +377,7 @@ const TransferAsResponseWriter = struct {
|
||||
try jws.objectField("url");
|
||||
try jws.beginWriteRaw();
|
||||
try writer.writeByte('\"');
|
||||
try transfer.uri.writeToStream(writer, .{
|
||||
.scheme = true,
|
||||
.authentication = true,
|
||||
.authority = true,
|
||||
.path = true,
|
||||
.query = true,
|
||||
});
|
||||
try transfer.url.writeToStream(writer);
|
||||
try writer.writeByte('\"');
|
||||
jws.endWriteRaw();
|
||||
}
|
||||
@@ -417,29 +418,22 @@ const TransferAsResponseWriter = struct {
|
||||
};
|
||||
|
||||
const DocumentUrlWriter = struct {
|
||||
uri: *std.Uri,
|
||||
url: URL,
|
||||
|
||||
fn init(uri: *std.Uri) DocumentUrlWriter {
|
||||
return .{
|
||||
.uri = uri,
|
||||
};
|
||||
fn init(url: URL) DocumentUrlWriter {
|
||||
return .{ .url = url };
|
||||
}
|
||||
|
||||
pub fn jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void {
|
||||
self._jsonStringify(jws) catch return error.WriteFailed;
|
||||
}
|
||||
|
||||
fn _jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void {
|
||||
const writer = jws.writer;
|
||||
|
||||
try jws.beginWriteRaw();
|
||||
try writer.writeByte('\"');
|
||||
try self.uri.writeToStream(writer, .{
|
||||
.scheme = true,
|
||||
.authentication = true,
|
||||
.authority = true,
|
||||
.path = true,
|
||||
.query = true,
|
||||
});
|
||||
try self.url.writeToStream(writer);
|
||||
try writer.writeByte('\"');
|
||||
jws.endWriteRaw();
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ fn createIsolatedWorld(cmd: anytype) !void {
|
||||
const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
try world.createContextAndLoadPolyfills(bc.arena, page);
|
||||
const js_context = &world.executor.js_context.?;
|
||||
const js_context = &world.executor.context.?;
|
||||
|
||||
// Create the auxdata json for the contextCreated event
|
||||
// Calling contextCreated will assign a Id to the context and send the contextCreated event
|
||||
@@ -174,7 +174,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
||||
var cdp = bc.cdp;
|
||||
const reason_: ?[]const u8 = switch (event.opts.reason) {
|
||||
.anchor => "anchorClick",
|
||||
.script => "scriptInitiated",
|
||||
.script, .history => "scriptInitiated",
|
||||
.form => switch (event.opts.method) {
|
||||
.GET => "formSubmissionGet",
|
||||
.POST => "formSubmissionPost",
|
||||
@@ -251,7 +251,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
bc.inspector.contextCreated(
|
||||
page.main_context,
|
||||
page.js,
|
||||
"",
|
||||
try page.origin(arena),
|
||||
aux_data,
|
||||
@@ -262,7 +262,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
||||
const aux_json = try std.fmt.allocPrint(arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
// Calling contextCreated will assign a new Id to the context and send the contextCreated event
|
||||
bc.inspector.contextCreated(
|
||||
&isolated_world.executor.js_context.?,
|
||||
&isolated_world.executor.context.?,
|
||||
isolated_world.name,
|
||||
"://",
|
||||
aux_json,
|
||||
|
||||
@@ -21,6 +21,7 @@ const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
const Cookie = @import("../../browser/storage/storage.zig").Cookie;
|
||||
const CookieJar = @import("../../browser/storage/storage.zig").CookieJar;
|
||||
const URL = @import("../../url.zig").URL;
|
||||
pub const PreparedUri = @import("../../browser/storage/cookie.zig").PreparedUri;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
@@ -136,12 +137,25 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
|
||||
const a = arena.allocator();
|
||||
|
||||
// NOTE: The param.url can affect the default domain, (NOT path), secure, source port, and source scheme.
|
||||
const uri = if (param.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
|
||||
const uri_ptr = if (uri) |*u| u else null;
|
||||
const domain = try Cookie.parseDomain(a, uri_ptr, param.domain);
|
||||
const maybe_url: ?URL = blk: {
|
||||
if (param.url) |url| {
|
||||
break :blk URL.parse(url, null) catch return error.InvalidParams;
|
||||
}
|
||||
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
const domain = try Cookie.parseDomain(a, maybe_url, param.domain);
|
||||
const path = if (param.path == null) "/" else try Cookie.parsePath(a, null, param.path);
|
||||
|
||||
const secure = if (param.secure) |s| s else if (uri) |uri_| std.mem.eql(u8, uri_.scheme, "https") else false;
|
||||
const secure: bool = blk: {
|
||||
// Check if params indicate security.
|
||||
if (param.secure) |s| break :blk s;
|
||||
// Check if protocol is secure.
|
||||
if (maybe_url) |url| break :blk url.isSecure();
|
||||
// If all fails, insecure.
|
||||
break :blk false;
|
||||
};
|
||||
|
||||
const cookie = Cookie{
|
||||
.arena = arena,
|
||||
@@ -158,6 +172,7 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
|
||||
.None => .none,
|
||||
},
|
||||
};
|
||||
|
||||
try cookie_jar.add(cookie, std.time.timestamp());
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ fn createTarget(cmd: anytype) !void {
|
||||
{
|
||||
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
bc.inspector.contextCreated(
|
||||
page.main_context,
|
||||
page.js,
|
||||
"",
|
||||
try page.origin(cmd.arena),
|
||||
aux_data,
|
||||
|
||||
@@ -22,9 +22,9 @@ const builtin = @import("builtin");
|
||||
|
||||
const Http = @import("Http.zig");
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
const CookieJar = @import("../browser/storage/storage.zig").CookieJar;
|
||||
|
||||
const urlStitch = @import("../url.zig").stitch;
|
||||
const CookieJar = @import("../browser/storage/cookie.zig").Jar;
|
||||
const URL = @import("../url.zig").URL;
|
||||
const urlStitch = URL.stitch;
|
||||
|
||||
const c = Http.c;
|
||||
const posix = std.posix;
|
||||
@@ -86,9 +86,6 @@ allocator: Allocator,
|
||||
// request. These wil come and go with each request.
|
||||
transfer_pool: std.heap.MemoryPool(Transfer),
|
||||
|
||||
// see ScriptManager.blockingGet
|
||||
blocking: Handle,
|
||||
|
||||
// To notify registered subscribers of events, the browser sets/nulls this for us.
|
||||
notification: ?*Notification = null,
|
||||
|
||||
@@ -121,16 +118,12 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie
|
||||
var handles = try Handles.init(allocator, client, ca_blob, &opts);
|
||||
errdefer handles.deinit(allocator);
|
||||
|
||||
var blocking = try Handle.init(client, ca_blob, &opts);
|
||||
errdefer blocking.deinit();
|
||||
|
||||
client.* = .{
|
||||
.queue = .{},
|
||||
.active = 0,
|
||||
.intercepted = 0,
|
||||
.multi = multi,
|
||||
.handles = handles,
|
||||
.blocking = blocking,
|
||||
.allocator = allocator,
|
||||
.http_proxy = opts.http_proxy,
|
||||
.user_agent = opts.user_agent,
|
||||
@@ -142,7 +135,6 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie
|
||||
|
||||
pub fn deinit(self: *Client) void {
|
||||
self.abort();
|
||||
self.blocking.deinit();
|
||||
self.handles.deinit(self.allocator);
|
||||
|
||||
_ = c.curl_multi_cleanup(self.multi);
|
||||
@@ -263,17 +255,11 @@ pub fn fulfillTransfer(self: *Client, transfer: *Transfer, status: u16, headers:
|
||||
return transfer.fulfill(status, headers, body);
|
||||
}
|
||||
|
||||
// See ScriptManager.blockingGet
|
||||
pub fn blockingRequest(self: *Client, req: Request) !void {
|
||||
const transfer = try self.makeTransfer(req);
|
||||
return self.makeRequest(&self.blocking, transfer);
|
||||
}
|
||||
|
||||
fn makeTransfer(self: *Client, req: Request) !*Transfer {
|
||||
errdefer req.headers.deinit();
|
||||
|
||||
// we need this for cookies
|
||||
const uri = std.Uri.parse(req.url) catch |err| {
|
||||
const url = URL.parse(req.url, null) catch |err| {
|
||||
log.warn(.http, "invalid url", .{ .err = err, .url = req.url });
|
||||
return err;
|
||||
};
|
||||
@@ -286,7 +272,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
|
||||
transfer.* = .{
|
||||
.arena = ArenaAllocator.init(self.allocator),
|
||||
.id = id,
|
||||
.uri = uri,
|
||||
.url = url,
|
||||
.req = req,
|
||||
.ctx = req.ctx,
|
||||
.client = self,
|
||||
@@ -329,7 +315,6 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void {
|
||||
for (self.handles.handles) |*h| {
|
||||
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
|
||||
}
|
||||
try errorCheck(c.curl_easy_setopt(self.blocking.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
|
||||
}
|
||||
|
||||
// Same restriction as changeProxy. Should be ok since this is only called on
|
||||
@@ -341,7 +326,6 @@ pub fn restoreOriginalProxy(self: *Client) !void {
|
||||
for (self.handles.handles) |*h| {
|
||||
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy));
|
||||
}
|
||||
try errorCheck(c.curl_easy_setopt(self.blocking.conn.easy, c.CURLOPT_PROXY, proxy));
|
||||
}
|
||||
|
||||
fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
|
||||
@@ -409,7 +393,9 @@ fn perform(self: *Client, timeout_ms: c_int) !PerformStatus {
|
||||
|
||||
// We're potentially going to block for a while until we get data. Process
|
||||
// whatever messages we have waiting ahead of time.
|
||||
try self.processMessages();
|
||||
if (try self.processMessages()) {
|
||||
return .normal;
|
||||
}
|
||||
|
||||
var status = PerformStatus.normal;
|
||||
if (self.extra_socket) |s| {
|
||||
@@ -427,12 +413,13 @@ fn perform(self: *Client, timeout_ms: c_int) !PerformStatus {
|
||||
try errorMCheck(c.curl_multi_poll(multi, null, 0, timeout_ms, null));
|
||||
}
|
||||
|
||||
try self.processMessages();
|
||||
_ = try self.processMessages();
|
||||
return status;
|
||||
}
|
||||
|
||||
fn processMessages(self: *Client) !void {
|
||||
fn processMessages(self: *Client) !bool {
|
||||
const multi = self.multi;
|
||||
var processed = false;
|
||||
var messages_count: c_int = 0;
|
||||
while (c.curl_multi_info_read(multi, &messages_count)) |msg_| {
|
||||
const msg: *c.CURLMsg = @ptrCast(msg_);
|
||||
@@ -491,10 +478,12 @@ fn processMessages(self: *Client) !void {
|
||||
.transfer = transfer,
|
||||
});
|
||||
}
|
||||
processed = true;
|
||||
} else |err| {
|
||||
self.requestFailed(transfer, err);
|
||||
}
|
||||
}
|
||||
return processed;
|
||||
}
|
||||
|
||||
fn endTransfer(self: *Client, transfer: *Transfer) void {
|
||||
@@ -504,7 +493,7 @@ fn endTransfer(self: *Client, transfer: *Transfer) void {
|
||||
log.fatal(.http, "Failed to remove handle", .{ .err = err });
|
||||
};
|
||||
|
||||
self.handles.release(self, handle);
|
||||
self.handles.release(handle);
|
||||
transfer._handle = null;
|
||||
self.active -= 1;
|
||||
}
|
||||
@@ -563,13 +552,7 @@ const Handles = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
fn release(self: *Handles, client: *Client, handle: *Handle) void {
|
||||
if (handle == &client.blocking) {
|
||||
// the handle we've reserved for blocking request doesn't participate
|
||||
// int he in_use/available pools
|
||||
return;
|
||||
}
|
||||
|
||||
fn release(self: *Handles, handle: *Handle) void {
|
||||
var node = &handle.node;
|
||||
self.in_use.remove(node);
|
||||
node.prev = null;
|
||||
@@ -612,20 +595,20 @@ pub const Handle = struct {
|
||||
pub const RequestCookie = struct {
|
||||
is_http: bool,
|
||||
is_navigation: bool,
|
||||
origin: *const std.Uri,
|
||||
jar: *@import("../browser/storage/cookie.zig").Jar,
|
||||
origin_url: URL,
|
||||
cookie_jar: *CookieJar,
|
||||
|
||||
pub fn headersForRequest(self: *const RequestCookie, temp: Allocator, url: [:0]const u8, headers: *Http.Headers) !void {
|
||||
const uri = std.Uri.parse(url) catch |err| {
|
||||
log.warn(.http, "invalid url", .{ .err = err, .url = url });
|
||||
pub fn headersForRequest(self: *const RequestCookie, temp: Allocator, url_str: [:0]const u8, headers: *Http.Headers) !void {
|
||||
const url = URL.parse(url_str, null) catch |err| {
|
||||
log.warn(.http, "invalid url", .{ .err = err, .url = url_str });
|
||||
return error.InvalidUrl;
|
||||
};
|
||||
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
try self.jar.forRequest(&uri, arr.writer(temp), .{
|
||||
try self.cookie_jar.forRequest(url, arr.writer(temp), .{
|
||||
.is_http = self.is_http,
|
||||
.is_navigation = self.is_navigation,
|
||||
.origin_uri = self.origin,
|
||||
.origin_url = self.origin_url,
|
||||
});
|
||||
|
||||
if (arr.items.len > 0) {
|
||||
@@ -705,7 +688,7 @@ pub const Transfer = struct {
|
||||
arena: ArenaAllocator,
|
||||
id: usize = 0,
|
||||
req: Request,
|
||||
uri: std.Uri, // used for setting/getting the cookie
|
||||
url: URL, // used for setting/getting the cookie
|
||||
ctx: *anyopaque, // copied from req.ctx to make it easier for callback handlers
|
||||
client: *Client,
|
||||
// total bytes received in the response, including the response status line,
|
||||
@@ -747,7 +730,7 @@ pub const Transfer = struct {
|
||||
fn deinit(self: *Transfer) void {
|
||||
self.req.headers.deinit();
|
||||
if (self._handle) |handle| {
|
||||
self.client.handles.release(self.client, handle);
|
||||
self.client.handles.release(handle);
|
||||
}
|
||||
self.arena.deinit();
|
||||
self.client.transfer_pool.destroy(self);
|
||||
@@ -791,7 +774,7 @@ pub const Transfer = struct {
|
||||
|
||||
pub fn updateURL(self: *Transfer, url: [:0]const u8) !void {
|
||||
// for cookies
|
||||
self.uri = try std.Uri.parse(url);
|
||||
self.url = try self.url.reparse(url);
|
||||
|
||||
// for the request itself
|
||||
self.req.url = url;
|
||||
@@ -850,7 +833,7 @@ pub const Transfer = struct {
|
||||
while (true) {
|
||||
const ct = getResponseHeader(easy, "set-cookie", i);
|
||||
if (ct == null) break;
|
||||
try req.cookie_jar.populateFromResponse(&transfer.uri, ct.?.value);
|
||||
try req.cookie_jar.populateFromResponse(transfer.url, ct.?.value);
|
||||
i += 1;
|
||||
if (i >= ct.?.amount) break;
|
||||
}
|
||||
@@ -864,14 +847,16 @@ pub const Transfer = struct {
|
||||
var baseurl: [*c]u8 = undefined;
|
||||
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_EFFECTIVE_URL, &baseurl));
|
||||
|
||||
const url = try urlStitch(arena, hlocation.?.value, std.mem.span(baseurl), .{});
|
||||
const uri = try std.Uri.parse(url);
|
||||
transfer.uri = uri;
|
||||
const stitched = try urlStitch(arena, hlocation.?.value, std.mem.span(baseurl), .{});
|
||||
// Since we're being redirected, we know url is valid.
|
||||
// An assertation won't hurt, though.
|
||||
std.debug.assert(transfer.url.isValid());
|
||||
_ = try transfer.url.reparse(stitched);
|
||||
|
||||
var cookies: std.ArrayListUnmanaged(u8) = .{};
|
||||
try req.cookie_jar.forRequest(&uri, cookies.writer(arena), .{
|
||||
try req.cookie_jar.forRequest(transfer.url, cookies.writer(arena), .{
|
||||
.is_http = true,
|
||||
.origin_uri = &transfer.uri,
|
||||
.origin_url = transfer.url,
|
||||
// used to enforce samesite cookie rules
|
||||
.is_navigation = req.resource_type == .document,
|
||||
});
|
||||
@@ -900,7 +885,7 @@ pub const Transfer = struct {
|
||||
while (true) {
|
||||
const ct = getResponseHeader(easy, "set-cookie", i);
|
||||
if (ct == null) break;
|
||||
transfer.req.cookie_jar.populateFromResponse(&transfer.uri, ct.?.value) catch |err| {
|
||||
transfer.req.cookie_jar.populateFromResponse(transfer.url, ct.?.value) catch |err| {
|
||||
log.err(.http, "set cookie", .{ .err = err, .req = transfer });
|
||||
return err;
|
||||
};
|
||||
|
||||
@@ -168,6 +168,13 @@ pub const Connection = struct {
|
||||
// debug
|
||||
if (comptime Http.ENABLE_DEBUG) {
|
||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_VERBOSE, @as(c_long, 1)));
|
||||
|
||||
// Sometimes the default debug output hides some useful data. You can
|
||||
// uncomment the following line (BUT KEEP THE LIVE ABOVE AS-IS), to
|
||||
// get more control over the data (specifically, the `CURLINFO_TEXT`
|
||||
// can include useful data).
|
||||
|
||||
// try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_DEBUGFUNCTION, debugCallback));
|
||||
}
|
||||
|
||||
return .{
|
||||
@@ -435,3 +442,14 @@ const LineWriter = struct {
|
||||
self.col = col + remain.len;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn debugCallback(_: *c.CURL, msg_type: c.curl_infotype, raw: [*c]u8, len: usize, _: *anyopaque) callconv(.c) void {
|
||||
const data = raw[0..len];
|
||||
switch (msg_type) {
|
||||
c.CURLINFO_TEXT => std.debug.print("libcurl [text]: {s}\n", .{data}),
|
||||
c.CURLINFO_HEADER_OUT => std.debug.print("libcurl [req-h]: {s}\n", .{data}),
|
||||
c.CURLINFO_HEADER_IN => std.debug.print("libcurl [res-h]: {s}\n", .{data}),
|
||||
// c.CURLINFO_DATA_IN => std.debug.print("libcurl [res-b]: {s}\n", .{data}),
|
||||
else => std.debug.print("libcurl ?? {d}\n", .{msg_type}),
|
||||
}
|
||||
}
|
||||
|
||||
28
src/log.zig
28
src/log.zig
@@ -29,7 +29,6 @@ pub const Scope = enum {
|
||||
cdp,
|
||||
console,
|
||||
http,
|
||||
http_client,
|
||||
js,
|
||||
loop,
|
||||
script_event,
|
||||
@@ -40,7 +39,7 @@ pub const Scope = enum {
|
||||
xhr,
|
||||
fetch,
|
||||
polyfill,
|
||||
mouse_event,
|
||||
interceptor,
|
||||
};
|
||||
|
||||
const Opts = struct {
|
||||
@@ -148,6 +147,13 @@ fn logTo(comptime scope: Scope, level: Level, comptime msg: []const u8, data: an
|
||||
.pretty => try logPretty(scope, level, msg, data, out),
|
||||
}
|
||||
out.flush() catch return;
|
||||
|
||||
const interceptor = _interceptor orelse return;
|
||||
if (interceptor.writer(interceptor.ctx, scope, level)) |iwriter| {
|
||||
try logLogfmt(scope, level, msg, data, iwriter);
|
||||
try iwriter.flush();
|
||||
interceptor.done(interceptor.ctx, scope, level);
|
||||
}
|
||||
}
|
||||
|
||||
fn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
|
||||
@@ -346,6 +352,24 @@ fn elapsed() struct { time: f64, unit: []const u8 } {
|
||||
return .{ .time = @as(f64, @floatFromInt(e)) / @as(f64, 1000), .unit = "s" };
|
||||
}
|
||||
|
||||
var _interceptor: ?Interceptor = null;
|
||||
pub fn registerInterceptor(interceptor: Interceptor) void {
|
||||
_interceptor = interceptor;
|
||||
}
|
||||
|
||||
pub fn unregisterInterceptor() void {
|
||||
_interceptor = null;
|
||||
}
|
||||
|
||||
const Interceptor = struct {
|
||||
ctx: *anyopaque,
|
||||
done: DoneFunc,
|
||||
writer: WriterFunc,
|
||||
|
||||
const DoneFunc = *const fn (ctx: *anyopaque, scope: Scope, level: Level) void;
|
||||
const WriterFunc = *const fn (ctx: *anyopaque, scope: Scope, level: Level) ?*std.Io.Writer;
|
||||
};
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "log: data" {
|
||||
opts.format = .logfmt;
|
||||
|
||||
20
src/main.zig
20
src/main.zig
@@ -165,6 +165,24 @@ fn run(alloc: Allocator) !void {
|
||||
// page
|
||||
const page = try session.createPage();
|
||||
|
||||
// // Comment this out to get a profile of the JS code in v8/profile.json.
|
||||
// // You can open this in Chrome's profiler.
|
||||
// // I've seen it generate invalid JSON, but I'm not sure why. It only
|
||||
// // happens rarely, and I manually fix the file.
|
||||
// page.js.startCpuProfiler();
|
||||
// defer {
|
||||
// if (page.js.stopCpuProfiler()) |profile| {
|
||||
// std.fs.cwd().writeFile(.{
|
||||
// .sub_path = "v8/profile.json",
|
||||
// .data = profile,
|
||||
// }) catch |err| {
|
||||
// log.err(.app, "profile write error", .{ .err = err });
|
||||
// };
|
||||
// } else |err| {
|
||||
// log.err(.app, "profile error", .{ .err = err });
|
||||
// }
|
||||
// }
|
||||
|
||||
_ = page.navigate(url, .{}) catch |err| switch (err) {
|
||||
error.UnsupportedUriScheme, error.UriMissingHost => {
|
||||
log.fatal(.app, "invalid fetch URL", .{ .err = err, .url = url });
|
||||
@@ -176,7 +194,7 @@ fn run(alloc: Allocator) !void {
|
||||
},
|
||||
};
|
||||
|
||||
_ = session.wait(5000); // 5 seconds
|
||||
_ = session.fetchWait(5000); // 5 seconds
|
||||
|
||||
// dump
|
||||
if (opts.dump) {
|
||||
|
||||
@@ -19,16 +19,20 @@
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("log.zig");
|
||||
const js = @import("browser/js/js.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const App = @import("app.zig").App;
|
||||
const Env = @import("browser/env.zig").Env;
|
||||
const Browser = @import("browser/browser.zig").Browser;
|
||||
const TestHTTPServer = @import("TestHTTPServer.zig");
|
||||
|
||||
const WPT_DIR = "tests/wpt";
|
||||
|
||||
// use in custom panic handler
|
||||
var current_test: ?[]const u8 = null;
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer _ = gpa.deinit();
|
||||
@@ -77,6 +81,9 @@ pub fn main() !void {
|
||||
while (try it.next()) |test_file| {
|
||||
defer _ = test_arena.reset(.retain_capacity);
|
||||
|
||||
defer current_test = null;
|
||||
current_test = test_file;
|
||||
|
||||
var err_out: ?[]const u8 = null;
|
||||
const result = run(
|
||||
test_arena.allocator(),
|
||||
@@ -116,8 +123,8 @@ fn run(
|
||||
|
||||
_ = page.wait(2000);
|
||||
|
||||
const js_context = page.main_context;
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
const js_context = page.js;
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
@@ -448,3 +455,12 @@ fn httpHandler(req: *std.http.Server.Request) !void {
|
||||
const file_path = try std.fmt.bufPrint(&buf, WPT_DIR ++ "{s}", .{path});
|
||||
return TestHTTPServer.sendFile(req, file_path);
|
||||
}
|
||||
|
||||
pub const panic = std.debug.FullPanic(struct {
|
||||
pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn {
|
||||
if (current_test) |ct| {
|
||||
std.debug.print("===panic running: {s}===\n", .{ct});
|
||||
}
|
||||
std.debug.defaultPanic(msg, first_trace_addr);
|
||||
}
|
||||
}.panicFn);
|
||||
|
||||
4157
src/runtime/js.zig
4157
src/runtime/js.zig
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
pub const SubType = enum {
|
||||
@"error",
|
||||
array,
|
||||
arraybuffer,
|
||||
dataview,
|
||||
date,
|
||||
generator,
|
||||
iterator,
|
||||
map,
|
||||
node,
|
||||
promise,
|
||||
proxy,
|
||||
regexp,
|
||||
set,
|
||||
typedarray,
|
||||
wasmvalue,
|
||||
weakmap,
|
||||
weakset,
|
||||
webassemblymemory,
|
||||
};
|
||||
@@ -1,295 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const MyList = struct {
|
||||
items: []u8,
|
||||
|
||||
pub fn constructor(elem1: u8, elem2: u8, elem3: u8, state: State) MyList {
|
||||
var items = state.arena.alloc(u8, 3) catch unreachable;
|
||||
items[0] = elem1;
|
||||
items[1] = elem2;
|
||||
items[2] = elem3;
|
||||
return .{ .items = items };
|
||||
}
|
||||
|
||||
pub fn _first(self: *const MyList) u8 {
|
||||
return self.items[0];
|
||||
}
|
||||
|
||||
pub fn _symbol_iterator(self: *const MyList) IterableU8 {
|
||||
return IterableU8.init(self.items);
|
||||
}
|
||||
};
|
||||
|
||||
const MyVariadic = struct {
|
||||
member: u8,
|
||||
|
||||
pub fn constructor() MyVariadic {
|
||||
return .{ .member = 0 };
|
||||
}
|
||||
|
||||
pub fn _len(_: *const MyVariadic, variadic: []bool) u64 {
|
||||
return @as(u64, variadic.len);
|
||||
}
|
||||
|
||||
pub fn _first(_: *const MyVariadic, _: []const u8, variadic: []bool) bool {
|
||||
return variadic[0];
|
||||
}
|
||||
|
||||
pub fn _last(_: *const MyVariadic, variadic: []bool) bool {
|
||||
return variadic[variadic.len - 1];
|
||||
}
|
||||
|
||||
pub fn _empty(_: *const MyVariadic, _: []bool) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn _myListLen(_: *const MyVariadic, variadic: []*const MyList) u8 {
|
||||
return @as(u8, @intCast(variadic.len));
|
||||
}
|
||||
|
||||
pub fn _myListFirst(_: *const MyVariadic, variadic: []*const MyList) ?u8 {
|
||||
if (variadic.len == 0) return null;
|
||||
return variadic[0]._first();
|
||||
}
|
||||
};
|
||||
|
||||
const MyErrorUnion = struct {
|
||||
pub fn constructor(is_err: bool) !MyErrorUnion {
|
||||
if (is_err) return error.MyError;
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn get_withoutError(_: *const MyErrorUnion) !u8 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn get_withError(_: *const MyErrorUnion) !u8 {
|
||||
return error.MyError;
|
||||
}
|
||||
|
||||
pub fn set_withoutError(_: *const MyErrorUnion, _: bool) !void {}
|
||||
|
||||
pub fn set_withError(_: *const MyErrorUnion, _: bool) !void {
|
||||
return error.MyError;
|
||||
}
|
||||
|
||||
pub fn _funcWithoutError(_: *const MyErrorUnion) !void {}
|
||||
|
||||
pub fn _funcWithError(_: *const MyErrorUnion) !void {
|
||||
return error.MyError;
|
||||
}
|
||||
};
|
||||
|
||||
pub const MyException = struct {
|
||||
err: ErrorSet,
|
||||
|
||||
const errorNames = [_][]const u8{
|
||||
"MyCustomError",
|
||||
};
|
||||
const errorMsgs = [_][]const u8{
|
||||
"Some custom message.",
|
||||
};
|
||||
fn errorStrings(comptime i: usize) []const u8 {
|
||||
return errorNames[0] ++ ": " ++ errorMsgs[i];
|
||||
}
|
||||
|
||||
// interface definition
|
||||
|
||||
pub const ErrorSet = error{
|
||||
MyCustomError,
|
||||
};
|
||||
|
||||
pub fn init(_: Allocator, err: anyerror, _: []const u8) !MyException {
|
||||
return .{ .err = @as(ErrorSet, @errorCast(err)) };
|
||||
}
|
||||
|
||||
pub fn get_name(self: *const MyException) []const u8 {
|
||||
return switch (self.err) {
|
||||
ErrorSet.MyCustomError => errorNames[0],
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_message(self: *const MyException) []const u8 {
|
||||
return switch (self.err) {
|
||||
ErrorSet.MyCustomError => errorMsgs[0],
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _toString(self: *const MyException) []const u8 {
|
||||
return switch (self.err) {
|
||||
ErrorSet.MyCustomError => errorStrings(0),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const MyTypeWithException = struct {
|
||||
pub const Exception = MyException;
|
||||
|
||||
pub fn constructor() MyTypeWithException {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn _withoutError(_: *const MyTypeWithException) MyException.ErrorSet!void {}
|
||||
|
||||
pub fn _withError(_: *const MyTypeWithException) MyException.ErrorSet!void {
|
||||
return MyException.ErrorSet.MyCustomError;
|
||||
}
|
||||
|
||||
pub fn _superSetError(_: *const MyTypeWithException) !void {
|
||||
return MyException.ErrorSet.MyCustomError;
|
||||
}
|
||||
|
||||
pub fn _outOfMemory(_: *const MyTypeWithException) !void {
|
||||
return error.OutOfMemory;
|
||||
}
|
||||
};
|
||||
|
||||
const MyUnionType = struct {
|
||||
pub const Choices = union(enum) {
|
||||
color: []const u8,
|
||||
number: usize,
|
||||
boolean: bool,
|
||||
obj1: *MyList,
|
||||
obj2: MyUnionType,
|
||||
};
|
||||
|
||||
pub fn constructor() MyUnionType {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn _choices(_: *const MyUnionType, u: Choices) Choices {
|
||||
return switch (u) {
|
||||
.color => .{ .color = "nice" },
|
||||
.number => |n| .{ .number = n + 10 },
|
||||
.boolean => |b| .{ .boolean = !b },
|
||||
.obj1 => |l| .{ .number = l.items.len },
|
||||
.obj2 => .{ .color = "meta" },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const IterableU8 = Iterable(u8);
|
||||
|
||||
pub fn Iterable(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
items: []T,
|
||||
index: usize = 0,
|
||||
|
||||
pub fn init(items: []T) Self {
|
||||
return .{ .items = items };
|
||||
}
|
||||
|
||||
pub const Return = struct {
|
||||
value: ?T,
|
||||
done: bool,
|
||||
};
|
||||
|
||||
pub fn _next(self: *Self) Return {
|
||||
if (self.items.len > self.index) {
|
||||
const val = self.items[self.index];
|
||||
self.index += 1;
|
||||
return .{ .value = val, .done = false };
|
||||
} else {
|
||||
return .{ .value = null, .done = true };
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const State = struct {
|
||||
arena: Allocator,
|
||||
};
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "JS: complex types" {
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
var runner = try testing.Runner(State, void, .{
|
||||
MyList,
|
||||
IterableU8,
|
||||
MyVariadic,
|
||||
MyErrorUnion,
|
||||
MyException,
|
||||
MyTypeWithException,
|
||||
MyUnionType,
|
||||
}).init(.{ .arena = arena.allocator() }, {});
|
||||
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let myList = new MyList(1, 2, 3);", "undefined" },
|
||||
.{ "myList.first();", "1" },
|
||||
.{ "let iter = myList[Symbol.iterator]();", "undefined" },
|
||||
.{ "iter.next().value;", "1" },
|
||||
.{ "iter.next().value;", "2" },
|
||||
.{ "iter.next().value;", "3" },
|
||||
.{ "iter.next().done;", "true" },
|
||||
.{ "let arr = Array.from(myList);", "undefined" },
|
||||
.{ "arr.length;", "3" },
|
||||
.{ "arr[0];", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let myVariadic = new MyVariadic();", "undefined" },
|
||||
.{ "myVariadic.len(true, false, true)", "3" },
|
||||
.{ "myVariadic.first('a_str', true, false, true, false)", "true" },
|
||||
.{ "myVariadic.last(true, false)", "false" },
|
||||
.{ "myVariadic.empty()", "true" },
|
||||
.{ "myVariadic.myListLen(myList)", "1" },
|
||||
.{ "myVariadic.myListFirst(myList)", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var myErrorCstr = ''; try {new MyErrorUnion(true)} catch (error) {myErrorCstr = error}; myErrorCstr", "Error: MyError" },
|
||||
.{ "let myErrorUnion = new MyErrorUnion(false);", "undefined" },
|
||||
.{ "myErrorUnion.withoutError", "0" },
|
||||
.{ "var myErrorGetter = ''; try {myErrorUnion.withError} catch (error) {myErrorGetter = error}; myErrorGetter", "Error: MyError" },
|
||||
.{ "myErrorUnion.withoutError = true", "true" },
|
||||
.{ "var myErrorSetter = ''; try {myErrorUnion.withError = true} catch (error) {myErrorSetter = error}; myErrorSetter", "Error: MyError" },
|
||||
.{ "myErrorUnion.funcWithoutError()", "undefined" },
|
||||
.{ "var myErrorFunc = ''; try {myErrorUnion.funcWithError()} catch (error) {myErrorFunc = error}; myErrorFunc", "Error: MyError" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "MyException.prototype.__proto__ === Error.prototype", "true" },
|
||||
.{ "let myTypeWithException = new MyTypeWithException();", "undefined" },
|
||||
.{ "myTypeWithException.withoutError()", "undefined" },
|
||||
.{ "var myCustomError = ''; try {myTypeWithException.withError()} catch (error) {myCustomError = error}", "MyCustomError: Some custom message." },
|
||||
.{ "myCustomError instanceof MyException", "true" },
|
||||
.{ "myCustomError instanceof Error", "true" },
|
||||
.{ "var mySuperError = ''; try {myTypeWithException.superSetError()} catch (error) {mySuperError = error}", "MyCustomError: Some custom message." },
|
||||
.{ "var oomError = ''; try {myTypeWithException.outOfMemory()} catch (error) {oomError = error}; oomError", "Error: out of memory" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var mut = new MyUnionType()", "undefined" },
|
||||
.{ "mut.choices(3)", "13" },
|
||||
.{ "mut.choices('blue')", "nice" },
|
||||
.{ "mut.choices(true)", "false" },
|
||||
.{ "mut.choices(false)", "true" },
|
||||
.{ "mut.choices(mut)", "meta" },
|
||||
.{ "mut.choices(myList)", "3" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const Other = struct {
|
||||
val: u8,
|
||||
|
||||
fn init(val: u8) Other {
|
||||
return .{ .val = val };
|
||||
}
|
||||
|
||||
pub fn _val(self: *const Other) u8 {
|
||||
return self.val;
|
||||
}
|
||||
};
|
||||
|
||||
pub const OtherUnion = union(enum) {
|
||||
Other: Other,
|
||||
Bool: bool,
|
||||
};
|
||||
|
||||
pub const MyObject = struct {
|
||||
val: bool,
|
||||
|
||||
pub fn constructor(do_set: bool) MyObject {
|
||||
return .{
|
||||
.val = do_set,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn named_get(_: *const MyObject, name: []const u8, has_value: *bool) ?OtherUnion {
|
||||
if (std.mem.eql(u8, name, "a")) {
|
||||
has_value.* = true;
|
||||
return .{ .Other = .{ .val = 4 } };
|
||||
}
|
||||
if (std.mem.eql(u8, name, "c")) {
|
||||
has_value.* = true;
|
||||
return .{ .Bool = true };
|
||||
}
|
||||
has_value.* = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn get_val(self: *const MyObject) bool {
|
||||
return self.val;
|
||||
}
|
||||
|
||||
pub fn set_val(self: *MyObject, val: bool) void {
|
||||
self.val = val;
|
||||
}
|
||||
};
|
||||
|
||||
pub const MyAPI = struct {
|
||||
pub fn constructor() MyAPI {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn _obj(_: *const MyAPI) !MyObject {
|
||||
return MyObject.constructor(true);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Parent = packed struct {
|
||||
parent_id: i32 = 0,
|
||||
|
||||
pub fn get_parent(self: *const Parent) i32 {
|
||||
return self.parent_id;
|
||||
}
|
||||
pub fn set_parent(self: *Parent, id: i32) void {
|
||||
self.parent_id = id;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Middle = struct {
|
||||
pub const prototype = *Parent;
|
||||
|
||||
middle_id: i32 = 0,
|
||||
_padding_1: u8 = 0,
|
||||
_padding_2: u8 = 1,
|
||||
_padding_3: u8 = 2,
|
||||
proto: Parent,
|
||||
|
||||
pub fn constructor() Middle {
|
||||
return .{
|
||||
.middle_id = 0,
|
||||
.proto = .{ .parent_id = 0 },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_middle(self: *const Middle) i32 {
|
||||
return self.middle_id;
|
||||
}
|
||||
pub fn set_middle(self: *Middle, id: i32) void {
|
||||
self.middle_id = id;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Child = struct {
|
||||
pub const prototype = *Middle;
|
||||
|
||||
child_id: i32 = 0,
|
||||
_padding_1: u8 = 0,
|
||||
proto: Middle,
|
||||
|
||||
pub fn constructor() Child {
|
||||
return .{
|
||||
.child_id = 0,
|
||||
.proto = .{ .middle_id = 0, .proto = .{ .parent_id = 0 } },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_child(self: *const Child) i32 {
|
||||
return self.child_id;
|
||||
}
|
||||
pub fn set_child(self: *Child, id: i32) void {
|
||||
self.child_id = id;
|
||||
}
|
||||
};
|
||||
|
||||
pub const MiddlePtr = packed struct {
|
||||
pub const prototype = *Parent;
|
||||
|
||||
middle_id: i32 = 0,
|
||||
_padding_1: u8 = 0,
|
||||
_padding_2: u8 = 1,
|
||||
_padding_3: u8 = 2,
|
||||
proto: *Parent,
|
||||
|
||||
pub fn constructor(state: State) !MiddlePtr {
|
||||
const parent = try state.arena.create(Parent);
|
||||
parent.* = .{ .parent_id = 0 };
|
||||
return .{
|
||||
.middle_id = 0,
|
||||
.proto = parent,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_middle(self: *const MiddlePtr) i32 {
|
||||
return self.middle_id;
|
||||
}
|
||||
pub fn set_middle(self: *MiddlePtr, id: i32) void {
|
||||
self.middle_id = id;
|
||||
}
|
||||
};
|
||||
|
||||
pub const ChildPtr = packed struct {
|
||||
pub const prototype = *MiddlePtr;
|
||||
|
||||
child_id: i32 = 0,
|
||||
_padding_1: u8 = 0,
|
||||
_padding_2: u8 = 1,
|
||||
proto: *MiddlePtr,
|
||||
|
||||
pub fn constructor(state: State) !ChildPtr {
|
||||
const parent = try state.arena.create(Parent);
|
||||
const middle = try state.arena.create(MiddlePtr);
|
||||
|
||||
parent.* = .{ .parent_id = 0 };
|
||||
middle.* = .{ .middle_id = 0, .proto = parent };
|
||||
return .{
|
||||
.child_id = 0,
|
||||
.proto = middle,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_child(self: *const ChildPtr) i32 {
|
||||
return self.child_id;
|
||||
}
|
||||
pub fn set_child(self: *ChildPtr, id: i32) void {
|
||||
self.child_id = id;
|
||||
}
|
||||
};
|
||||
|
||||
const State = struct {
|
||||
arena: Allocator,
|
||||
};
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "JS: object types" {
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
var runner = try testing.Runner(State, void, .{
|
||||
Other,
|
||||
MyObject,
|
||||
MyAPI,
|
||||
Parent,
|
||||
Middle,
|
||||
Child,
|
||||
MiddlePtr,
|
||||
ChildPtr,
|
||||
}).init(.{ .arena = arena.allocator() }, {});
|
||||
|
||||
defer runner.deinit();
|
||||
|
||||
// v8 has 3 default "own" properties
|
||||
const own_base = "3";
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "Object.getOwnPropertyNames(MyObject).length;", own_base },
|
||||
.{ "let myObj = new MyObject(true);", "undefined" },
|
||||
// check object property
|
||||
.{ "myObj.a.val()", "4" },
|
||||
.{ "myObj.b", "undefined" },
|
||||
.{ "Object.getOwnPropertyNames(myObj).length;", "0" },
|
||||
|
||||
// check if setter (pointer) still works
|
||||
.{ "myObj.val", "true" },
|
||||
.{ "myObj.val = false", "false" },
|
||||
.{ "myObj.val", "false" },
|
||||
|
||||
.{ "let myObj2 = new MyObject(false);", "undefined" },
|
||||
.{ "myObj2.c", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let myAPI = new MyAPI();", "undefined" },
|
||||
.{ "let myObjIndirect = myAPI.obj();", "undefined" },
|
||||
// check object property
|
||||
.{ "myObjIndirect.a.val()", "4" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let m1 = new Middle();", null },
|
||||
.{ "m1.middle = 2", null },
|
||||
.{ "m1.parent = 3", null },
|
||||
.{ "m1.middle", "2" },
|
||||
.{ "m1.parent", "3" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let c1 = new Child();", null },
|
||||
.{ "c1.child = 1", null },
|
||||
.{ "c1.middle = 2", null },
|
||||
.{ "c1.parent = 3", null },
|
||||
.{ "c1.child", "1" },
|
||||
.{ "c1.middle", "2" },
|
||||
.{ "c1.parent", "3" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let m2 = new MiddlePtr();", null },
|
||||
.{ "m2.middle = 2", null },
|
||||
.{ "m2.parent = 3", null },
|
||||
.{ "m2.middle", "2" },
|
||||
.{ "m2.parent", "3" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let c2 = new ChildPtr();", null },
|
||||
.{ "c2.child = 1", null },
|
||||
.{ "c2.middle = 2", null },
|
||||
.{ "c2.parent = 3", null },
|
||||
.{ "c2.child", "1" },
|
||||
.{ "c2.middle", "2" },
|
||||
.{ "c2.parent", "3" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// TODO: use functions instead of "fake" struct once we handle function API generation
|
||||
|
||||
const Runner = testing.Runner(void, void, .{Primitives});
|
||||
const Env = Runner.Env;
|
||||
|
||||
const Primitives = struct {
|
||||
pub fn constructor() Primitives {
|
||||
return .{};
|
||||
}
|
||||
|
||||
// List of bytes (string)
|
||||
pub fn _checkString(_: *const Primitives, v: []u8) []u8 {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Integers signed
|
||||
|
||||
pub fn _checkI32(_: *const Primitives, v: i32) i32 {
|
||||
return v;
|
||||
}
|
||||
|
||||
pub fn _checkI64(_: *const Primitives, v: i64) i64 {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Integers unsigned
|
||||
|
||||
pub fn _checkU32(_: *const Primitives, v: u32) u32 {
|
||||
return v;
|
||||
}
|
||||
|
||||
pub fn _checkU64(_: *const Primitives, v: u64) u64 {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Floats
|
||||
|
||||
pub fn _checkF32(_: *const Primitives, v: f32) f32 {
|
||||
return v;
|
||||
}
|
||||
|
||||
pub fn _checkF64(_: *const Primitives, v: f64) f64 {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Bool
|
||||
pub fn _checkBool(_: *const Primitives, v: bool) bool {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Undefined
|
||||
// TODO: there is a bug with this function
|
||||
// void paramater does not work => avoid for now
|
||||
// pub fn _checkUndefined(_: *const Primitives, v: void) void {
|
||||
// return v;
|
||||
// }
|
||||
|
||||
// Null
|
||||
pub fn _checkNullEmpty(_: *const Primitives, v: ?u32) bool {
|
||||
return (v == null);
|
||||
}
|
||||
pub fn _checkNullNotEmpty(_: *const Primitives, v: ?u32) bool {
|
||||
return (v != null);
|
||||
}
|
||||
|
||||
// Optionals
|
||||
pub fn _checkOptional(_: *const Primitives, _: ?u8, v: u8, _: ?u8, _: ?u8) u8 {
|
||||
return v;
|
||||
}
|
||||
pub fn _checkNonOptional(_: *const Primitives, v: u8) u8 {
|
||||
return v;
|
||||
}
|
||||
pub fn _checkOptionalReturn(_: *const Primitives) ?bool {
|
||||
return true;
|
||||
}
|
||||
pub fn _checkOptionalReturnNull(_: *const Primitives) ?bool {
|
||||
return null;
|
||||
}
|
||||
pub fn _checkOptionalReturnString(_: *const Primitives) ?[]const u8 {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
pub fn _echoString(_: *const Primitives, a: []const u8) []const u8 {
|
||||
return a;
|
||||
}
|
||||
|
||||
pub fn _echoStringZ(_: *const Primitives, a: [:0]const u8) []const u8 {
|
||||
return a;
|
||||
}
|
||||
|
||||
pub fn _int8(_: *const Primitives, arr: []i8) void {
|
||||
for (arr) |*a| {
|
||||
a.* -= @intCast(arr.len);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _uint8(_: *const Primitives, arr: []u8) void {
|
||||
for (arr) |*a| {
|
||||
a.* += @intCast(arr.len);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _returnEmptyUint8(_: *const Primitives) Env.TypedArray(u8) {
|
||||
return .{ .values = &.{} };
|
||||
}
|
||||
|
||||
pub fn _returnUint8(_: *const Primitives) Env.TypedArray(u8) {
|
||||
return .{ .values = &.{ 10, 20, 250 } };
|
||||
}
|
||||
|
||||
pub fn _returnInt8(_: *const Primitives) Env.TypedArray(i8) {
|
||||
return .{ .values = &.{ 10, -20, -120 } };
|
||||
}
|
||||
|
||||
pub fn _returnUint16(_: *const Primitives) Env.TypedArray(u16) {
|
||||
return .{ .values = &.{ 10, 200, 2050 } };
|
||||
}
|
||||
|
||||
pub fn _returnInt16(_: *const Primitives) Env.TypedArray(i16) {
|
||||
return .{ .values = &.{ 10, -420, 0 } };
|
||||
}
|
||||
|
||||
pub fn _returnUint32(_: *const Primitives) Env.TypedArray(u32) {
|
||||
return .{ .values = &.{ 10, 2444343, 43432432 } };
|
||||
}
|
||||
|
||||
pub fn _returnInt32(_: *const Primitives) Env.TypedArray(i32) {
|
||||
return .{ .values = &.{ 10, -20, -495929123 } };
|
||||
}
|
||||
|
||||
pub fn _returnUint64(_: *const Primitives) Env.TypedArray(u64) {
|
||||
return .{ .values = &.{ 10, 495812375924, 0 } };
|
||||
}
|
||||
|
||||
pub fn _returnInt64(_: *const Primitives) Env.TypedArray(i64) {
|
||||
return .{ .values = &.{ 10, -49283838122, -2 } };
|
||||
}
|
||||
|
||||
pub fn _returnFloat32(_: *const Primitives) Env.TypedArray(f32) {
|
||||
return .{ .values = &.{ 1.1, -200.035, 0.0003 } };
|
||||
}
|
||||
|
||||
pub fn _returnFloat64(_: *const Primitives) Env.TypedArray(f64) {
|
||||
return .{ .values = &.{ 8881.22284, -4928.3838122, -0.00004 } };
|
||||
}
|
||||
|
||||
pub fn _int16(_: *const Primitives, arr: []i16) void {
|
||||
for (arr) |*a| {
|
||||
a.* -= @intCast(arr.len);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _uint16(_: *const Primitives, arr: []u16) void {
|
||||
for (arr) |*a| {
|
||||
a.* += @intCast(arr.len);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _int32(_: *const Primitives, arr: []i32) void {
|
||||
for (arr) |*a| {
|
||||
a.* -= @intCast(arr.len);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _uint32(_: *const Primitives, arr: []u32) void {
|
||||
for (arr) |*a| {
|
||||
a.* += @intCast(arr.len);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _int64(_: *const Primitives, arr: []i64) void {
|
||||
for (arr) |*a| {
|
||||
a.* -= @intCast(arr.len);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _uint64(_: *const Primitives, arr: []u64) void {
|
||||
for (arr) |*a| {
|
||||
a.* += @intCast(arr.len);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "JS: primitive types" {
|
||||
var runner = try Runner.init({}, {});
|
||||
defer runner.deinit();
|
||||
|
||||
// constructor
|
||||
try runner.testCases(&.{
|
||||
.{ "let p = new Primitives();", "undefined" },
|
||||
}, .{});
|
||||
|
||||
// JS <> Native translation of primitive types
|
||||
try runner.testCases(&.{
|
||||
.{ "p.checkString('ok ascii') === 'ok ascii';", "true" },
|
||||
.{ "p.checkString('ok emoji 🚀') === 'ok emoji 🚀';", "true" },
|
||||
.{ "p.checkString('ok chinese 鿍') === 'ok chinese 鿍';", "true" },
|
||||
|
||||
// String (JS liberal cases)
|
||||
.{ "p.checkString(1) === '1';", "true" },
|
||||
.{ "p.checkString(null) === 'null';", "true" },
|
||||
.{ "p.checkString(undefined) === 'undefined';", "true" },
|
||||
|
||||
// Integers
|
||||
|
||||
// signed
|
||||
.{ "const min_i32 = -2147483648", "undefined" },
|
||||
.{ "p.checkI32(min_i32) === min_i32;", "true" },
|
||||
.{ "p.checkI32(min_i32-1) === min_i32-1;", "false" },
|
||||
.{ "try { p.checkI32(9007199254740995n) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
|
||||
// unsigned
|
||||
.{ "const max_u32 = 4294967295", "undefined" },
|
||||
.{ "p.checkU32(max_u32) === max_u32;", "true" },
|
||||
.{ "p.checkU32(max_u32+1) === max_u32+1;", "false" },
|
||||
|
||||
// int64 (with BigInt)
|
||||
.{ "const big_int = 9007199254740995n", "undefined" },
|
||||
.{ "p.checkI64(big_int) === big_int", "true" },
|
||||
.{ "p.checkU64(big_int) === big_int;", "true" },
|
||||
.{ "p.checkI64(0) === 0;", "true" },
|
||||
.{ "p.checkI64(-1) === -1;", "true" },
|
||||
.{ "p.checkU64(0) === 0;", "true" },
|
||||
|
||||
// Floats
|
||||
// use round 2 decimals for float to ensure equality
|
||||
.{ "const r = function(x) {return Math.round(x * 100) / 100};", "undefined" },
|
||||
.{ "const double = 10.02;", "undefined" },
|
||||
.{ "r(p.checkF32(double)) === double;", "true" },
|
||||
.{ "r(p.checkF64(double)) === double;", "true" },
|
||||
|
||||
// Bool
|
||||
.{ "p.checkBool(true);", "true" },
|
||||
.{ "p.checkBool(false);", "false" },
|
||||
.{ "p.checkBool(0);", "false" },
|
||||
.{ "p.checkBool(1);", "true" },
|
||||
|
||||
// Bool (JS liberal cases)
|
||||
.{ "p.checkBool(null);", "false" },
|
||||
.{ "p.checkBool(undefined);", "false" },
|
||||
|
||||
// Undefined
|
||||
// see TODO on Primitives.checkUndefined
|
||||
// .{ "p.checkUndefined(undefined) === undefined;", "true" },
|
||||
|
||||
// Null
|
||||
.{ "p.checkNullEmpty(null);", "true" },
|
||||
.{ "p.checkNullEmpty(undefined);", "true" },
|
||||
.{ "p.checkNullNotEmpty(1);", "true" },
|
||||
|
||||
// Optional
|
||||
.{ "p.checkOptional(null, 3);", "3" },
|
||||
.{ "p.checkNonOptional();", "TypeError" },
|
||||
.{ "p.checkOptionalReturn() === true;", "true" },
|
||||
.{ "p.checkOptionalReturnNull() === null;", "true" },
|
||||
.{ "p.checkOptionalReturnString() === 'ok';", "true" },
|
||||
|
||||
// strings
|
||||
.{ "p.echoString('over 9000!');", "over 9000!" },
|
||||
.{ "p.echoStringZ('Teg');", "Teg" },
|
||||
}, .{});
|
||||
|
||||
// typed arrays
|
||||
try runner.testCases(&.{
|
||||
.{ "let empty_arr = new Int8Array([]);", "undefined" },
|
||||
.{ "p.int8(empty_arr)", "undefined" },
|
||||
.{ "empty_arr;", "" },
|
||||
|
||||
.{ "let arr_i8 = new Int8Array([-10, -20, -30]);", "undefined" },
|
||||
.{ "p.int8(arr_i8)", "undefined" },
|
||||
.{ "arr_i8;", "-13,-23,-33" },
|
||||
|
||||
.{ "let arr_u8 = new Uint8Array([10, 20, 30]);", "undefined" },
|
||||
.{ "p.uint8(arr_u8)", "undefined" },
|
||||
.{ "arr_u8;", "13,23,33" },
|
||||
|
||||
.{ "let arr_i16 = new Int16Array([-1000, -2000, -3000]);", "undefined" },
|
||||
.{ "p.int16(arr_i16)", "undefined" },
|
||||
.{ "arr_i16;", "-1003,-2003,-3003" },
|
||||
|
||||
.{ "let arr_u16 = new Uint16Array([1000, 2000, 3000]);", "undefined" },
|
||||
.{ "p.uint16(arr_u16)", "undefined" },
|
||||
.{ "arr_u16;", "1003,2003,3003" },
|
||||
|
||||
.{ "let arr_i32 = new Int32Array([-1000000, -2000000, -3000000]);", "undefined" },
|
||||
.{ "p.int32(arr_i32)", "undefined" },
|
||||
.{ "arr_i32;", "-1000003,-2000003,-3000003" },
|
||||
|
||||
.{ "let arr_u32 = new Uint32Array([1000000, 2000000, 3000000]);", "undefined" },
|
||||
.{ "p.uint32(arr_u32)", "undefined" },
|
||||
.{ "arr_u32;", "1000003,2000003,3000003" },
|
||||
|
||||
.{ "let arr_i64 = new BigInt64Array([-1000000000n, -2000000000n, -3000000000n]);", "undefined" },
|
||||
.{ "p.int64(arr_i64)", "undefined" },
|
||||
.{ "arr_i64;", "-1000000003,-2000000003,-3000000003" },
|
||||
|
||||
.{ "let arr_u64 = new BigUint64Array([1000000000n, 2000000000n, 3000000000n]);", "undefined" },
|
||||
.{ "p.uint64(arr_u64)", "undefined" },
|
||||
.{ "arr_u64;", "1000000003,2000000003,3000000003" },
|
||||
|
||||
.{ "try { p.int8(arr_u8) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
.{ "try { p.intu8(arr_i8) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
.{ "try { p.intu8(arr_u32) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
|
||||
.{ "try { p.int16(arr_u8) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
.{ "try { p.intu16(arr_i16) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
.{ "try { p.int16(arr_i64) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
|
||||
.{ "try { p.int32(arr_u32) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
.{ "try { p.intu32(arr_i32) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
.{ "try { p.intu32(arr_u32) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
|
||||
.{ "try { p.int64(arr_u64) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
.{ "try { p.intu64(arr_i64) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
.{ "try { p.intu64(arr_u32) } catch(e) { e instanceof TypeError; }", "true" },
|
||||
|
||||
.{ "p.returnEmptyUint8()", "" },
|
||||
.{ "p.returnUint8()", "10,20,250" },
|
||||
.{ "p.returnInt8()", "10,-20,-120" },
|
||||
.{ "p.returnUint16()", "10,200,2050" },
|
||||
.{ "p.returnInt16()", "10,-420,0" },
|
||||
.{ "p.returnUint32()", "10,2444343,43432432" },
|
||||
.{ "p.returnInt32()", "10,-20,-495929123" },
|
||||
.{ "p.returnUint64()", "10,495812375924,0" },
|
||||
.{ "p.returnInt64()", "10,-49283838122,-2" },
|
||||
.{ "p.returnFloat32()", "1.100000023841858,-200.03500366210938,0.0003000000142492354" },
|
||||
.{ "p.returnFloat64()", "8881.22284,-4928.3838122,-0.00004" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "'foo\\\\:bar'", "foo\\:bar" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const base = @import("../testing.zig");
|
||||
const generate = @import("generate.zig");
|
||||
|
||||
pub const allocator = std.testing.allocator;
|
||||
|
||||
// Very similar to the JSRunner in src/testing.zig, but it isn't tied to the
|
||||
// browser.Env or the *Page state
|
||||
pub fn Runner(comptime State: type, comptime Global: type, comptime types: anytype) type {
|
||||
const AdjustedTypes = if (Global == void) generate.Tuple(.{ types, DefaultGlobal }) else types;
|
||||
|
||||
return struct {
|
||||
env: *Env,
|
||||
js_context: *Env.JsContext,
|
||||
executor: Env.ExecutionWorld,
|
||||
|
||||
pub const Env = js.Env(State, struct {
|
||||
pub const Interfaces = AdjustedTypes;
|
||||
});
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(state: State, global: Global) !*Self {
|
||||
const self = try allocator.create(Self);
|
||||
errdefer allocator.destroy(self);
|
||||
|
||||
self.env = try Env.init(allocator, &base.test_app.platform, .{});
|
||||
errdefer self.env.deinit();
|
||||
|
||||
self.executor = try self.env.newExecutionWorld();
|
||||
errdefer self.executor.deinit();
|
||||
|
||||
self.js_context = try self.executor.createJsContext(
|
||||
if (Global == void) &default_global else global,
|
||||
state,
|
||||
{},
|
||||
true,
|
||||
null,
|
||||
);
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.executor.deinit();
|
||||
self.env.deinit();
|
||||
allocator.destroy(self);
|
||||
}
|
||||
|
||||
const RunOpts = struct {};
|
||||
pub const Case = std.meta.Tuple(&.{ []const u8, ?[]const u8 });
|
||||
pub fn testCases(self: *Self, cases: []const Case, _: RunOpts) !void {
|
||||
for (cases, 0..) |case, i| {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(self.js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const value = self.js_context.exec(case.@"0", null) catch |err| {
|
||||
if (try try_catch.err(allocator)) |msg| {
|
||||
defer allocator.free(msg);
|
||||
if (isExpectedTypeError(case.@"1", msg)) {
|
||||
continue;
|
||||
}
|
||||
std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" });
|
||||
}
|
||||
return err;
|
||||
};
|
||||
|
||||
if (case.@"1") |expected| {
|
||||
const actual = try value.toString(allocator);
|
||||
defer allocator.free(actual);
|
||||
if (std.mem.eql(u8, expected, actual) == false) {
|
||||
std.debug.print("Expected:\n{s}\n\nGot:\n{s}\n\nCase: {d}\n{s}\n", .{ expected, actual, i + 1, case.@"0" });
|
||||
return error.UnexpectedResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn isExpectedTypeError(expected_: ?[]const u8, msg: []const u8) bool {
|
||||
const expected = expected_ orelse return false;
|
||||
|
||||
if (!std.mem.eql(u8, expected, "TypeError")) {
|
||||
return false;
|
||||
}
|
||||
return std.mem.startsWith(u8, msg, "TypeError: ");
|
||||
}
|
||||
|
||||
var default_global = DefaultGlobal{};
|
||||
const DefaultGlobal = struct {};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user