2 Commits

Author SHA1 Message Date
Karl Seguin
5604affd0b slower for CI 2025-09-23 17:51:36 +08:00
Karl Seguin
60f1b1160e reduce test wait time 2025-09-23 11:47:32 +08:00
137 changed files with 6664 additions and 8098 deletions

View File

@@ -5,7 +5,7 @@ inputs:
zig: zig:
description: 'Zig version to install' description: 'Zig version to install'
required: false required: false
default: '0.15.2' default: '0.15.1'
arch: arch:
description: 'CPU arch used to select the v8 lib' description: 'CPU arch used to select the v8 lib'
required: false required: false
@@ -17,7 +17,7 @@ inputs:
zig-v8: zig-v8:
description: 'zig v8 version to install' description: 'zig v8 version to install'
required: false required: false
default: 'v0.1.33' default: 'v0.1.30'
v8: v8:
description: 'v8 version to install' description: 'v8 version to install'
required: false required: false

View File

@@ -31,7 +31,7 @@ jobs:
- uses: ./.github/actions/install - uses: ./.github/actions/install
- name: json output - name: json output
run: zig build wpt -- --json > wpt.json run: zig build -Doptimize=ReleaseFast wpt -- --json > wpt.json
- name: write commit - name: write commit
run: | run: |

View File

@@ -1,7 +1,7 @@
name: zig-fmt name: zig-fmt
env: env:
ZIG_VERSION: 0.15.2 ZIG_VERSION: 0.15.1
on: on:
pull_request: pull_request:

3
.gitmodules vendored
View File

@@ -31,6 +31,3 @@
[submodule "vendor/curl"] [submodule "vendor/curl"]
path = vendor/curl path = vendor/curl
url = https://github.com/curl/curl.git url = https://github.com/curl/curl.git
[submodule "vendor/brotli"]
path = vendor/brotli
url = https://github.com/google/brotli

View File

@@ -1,10 +1,10 @@
FROM debian:stable FROM debian:stable
ARG MINISIG=0.12 ARG MINISIG=0.12
ARG ZIG=0.15.2 ARG ZIG=0.15.1
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.0.365.4 ARG V8=14.0.365.4
ARG ZIG_V8=v0.1.33 ARG ZIG_V8=v0.1.30
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN apt-get update -yq && \ RUN apt-get update -yq && \

View File

@@ -47,7 +47,7 @@ help:
# $(ZIG) commands # $(ZIG) commands
# ------------ # ------------
.PHONY: build build-dev run run-release shell test bench download-zig wpt data .PHONY: build build-dev run run-release shell test bench download-zig wpt data get-v8 build-v8 build-v8-dev
.PHONY: end2end .PHONY: end2end
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2) zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
@@ -96,22 +96,28 @@ wpt-summary:
@printf "\e[36mBuilding wpt...\e[0m\n" @printf "\e[36mBuilding wpt...\e[0m\n"
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) @$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
## Test - `grep` is used to filter out the huge compile command on build ## Test
ifeq ($(OS), macos)
test: test:
@script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \ @TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
| grep --line-buffered -v "^/.*zig test -freference-trace"
else
test:
@script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' /dev/null 2>&1 \
| grep --line-buffered -v "^/.*zig test -freference-trace"
endif
## Run demo/runner end to end tests ## Run demo/runner end to end tests
end2end: end2end:
@test -d ../demo @test -d ../demo
cd ../demo && go run runner/main.go cd ../demo && go run runner/main.go
## v8
get-v8:
@printf "\e[36mGetting v8 source...\e[0m\n"
@$(ZIG) build get-v8
build-v8-dev:
@printf "\e[36mBuilding v8 (dev)...\e[0m\n"
@$(ZIG) build build-v8
build-v8:
@printf "\e[36mBuilding v8...\e[0m\n"
@$(ZIG) build -Doptimize=ReleaseSafe build-v8
# Install and build required dependencies commands # Install and build required dependencies commands
# ------------ # ------------
.PHONY: install-submodule .PHONY: install-submodule

View File

@@ -164,7 +164,7 @@ You can also follow the progress of our Javascript support in our dedicated [zig
### Prerequisites ### Prerequisites
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to
install it with the right version in order to build the project. install it with the right version in order to build the project.
Lightpanda also depends on Lightpanda also depends on

View File

@@ -23,7 +23,7 @@ const Build = std.Build;
/// Do not rename this constant. It is scanned by some scripts to determine /// Do not rename this constant. It is scanned by some scripts to determine
/// which zig version to install. /// which zig version to install.
const recommended_zig_version = "0.15.2"; const recommended_zig_version = "0.15.1";
pub fn build(b: *Build) !void { pub fn build(b: *Build) !void {
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) { switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
@@ -46,8 +46,6 @@ pub fn build(b: *Build) !void {
b.option([]const u8, "git_commit", "Current git commit") orelse "dev", b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
); );
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
@@ -61,7 +59,7 @@ pub fn build(b: *Build) !void {
.link_libc = true, .link_libc = true,
.link_libcpp = true, .link_libcpp = true,
}); });
try addDependencies(b, lightpanda_module, opts, prebuilt_v8_path); try addDependencies(b, lightpanda_module, opts);
{ {
// browser // browser
@@ -115,7 +113,7 @@ pub fn build(b: *Build) !void {
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
try addDependencies(b, wpt_module, opts, prebuilt_v8_path); try addDependencies(b, wpt_module, opts);
// compile and install // compile and install
const wpt = b.addExecutable(.{ const wpt = b.addExecutable(.{
@@ -133,9 +131,27 @@ pub fn build(b: *Build) !void {
const wpt_step = b.step("wpt", "WPT tests"); const wpt_step = b.step("wpt", "WPT tests");
wpt_step.dependOn(&wpt_cmd.step); wpt_step.dependOn(&wpt_cmd.step);
} }
{
// get v8
// -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
const get_v8 = b.addRunArtifact(v8.artifact("get-v8"));
const get_step = b.step("get-v8", "Get v8");
get_step.dependOn(&get_v8.step);
}
{
// build v8
// -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
const build_v8 = b.addRunArtifact(v8.artifact("build-v8"));
const build_step = b.step("build-v8", "Build v8");
build_step.dependOn(&build_v8.step);
}
} }
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, prebuilt_v8_path: ?[]const u8) !void { fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !void {
try moduleNetSurf(b, mod); try moduleNetSurf(b, mod);
mod.addImport("build_config", opts.createModule()); mod.addImport("build_config", opts.createModule());
@@ -143,7 +159,6 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, pre
const dep_opts = .{ const dep_opts = .{
.target = target, .target = target,
.optimize = mod.optimize.?, .optimize = mod.optimize.?,
.prebuilt_v8_path = prebuilt_v8_path,
}; };
mod.addIncludePath(b.path("vendor/lightpanda")); mod.addIncludePath(b.path("vendor/lightpanda"));
@@ -156,6 +171,36 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, pre
const v8_mod = b.dependency("v8", dep_opts).module("v8"); const v8_mod = b.dependency("v8", dep_opts).module("v8");
v8_mod.addOptions("default_exports", v8_opts); v8_mod.addOptions("default_exports", v8_opts);
mod.addImport("v8", v8_mod); mod.addImport("v8", v8_mod);
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
const os = switch (target.result.os.tag) {
.linux => "linux",
.macos => "macos",
else => return error.UnsupportedPlatform,
};
var lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/out/{s}/{s}/obj/zig/libc_v8.a",
.{ os, release_dir },
);
std.fs.cwd().access(lib_path, .{}) catch {
// legacy path
lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/out/{s}/obj/zig/libc_v8.a",
.{release_dir},
);
};
mod.addObjectFile(mod.owner.path(lib_path));
switch (target.result.os.tag) {
.macos => {
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
mod.linkFramework("CoreFoundation", .{});
},
else => {},
}
} }
{ {
@@ -200,7 +245,6 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, pre
mod.addCMacro("HAVE_ASSERT_H", "1"); mod.addCMacro("HAVE_ASSERT_H", "1");
mod.addCMacro("HAVE_BASENAME", "1"); mod.addCMacro("HAVE_BASENAME", "1");
mod.addCMacro("HAVE_BOOL_T", "1"); mod.addCMacro("HAVE_BOOL_T", "1");
mod.addCMacro("HAVE_BROTLI", "1");
mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1"); mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1");
mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1"); mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
mod.addCMacro("HAVE_DLFCN_H", "1"); mod.addCMacro("HAVE_DLFCN_H", "1");
@@ -335,7 +379,6 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, pre
} }
try buildZlib(b, mod); try buildZlib(b, mod);
try buildBrotli(b, mod);
try buildMbedtls(b, mod); try buildMbedtls(b, mod);
try buildNghttp2(b, mod); try buildNghttp2(b, mod);
try buildCurl(b, mod); try buildCurl(b, mod);
@@ -441,30 +484,6 @@ 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 { fn buildMbedtls(b: *Build, m: *Build.Module) !void {
const mbedtls = b.addLibrary(.{ const mbedtls = b.addLibrary(.{
.name = "mbedtls", .name = "mbedtls",

View File

@@ -5,9 +5,9 @@
.fingerprint = 0xda130f3af836cea0, .fingerprint = 0xda130f3af836cea0,
.dependencies = .{ .dependencies = .{
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/543fb7b40a0e139ebe38e1401942b6506222daf3.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/7177ee1ae267a44751a0e7e012e257177699a375.tar.gz",
.hash = "v8-0.0.0-xddH6-kmBAAdoG6goGoo3pMwzfL73XiFgcj82vYbLym4", .hash = "v8-0.0.0-xddH63TCAwC1D1hEiOtbEnLBbtz9ZPHrdiGWLcBcYQB7",
}, },
// .v8 = .{ .path = "../zig-v8-fork" }, // .v8 = .{ .path = "../zig-v8-fork" }
}, },
} }

12
flake.lock generated
View File

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

View File

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

View File

@@ -4,7 +4,7 @@ const Allocator = std.mem.Allocator;
const log = @import("log.zig"); const log = @import("log.zig");
const Http = @import("http/Http.zig"); const Http = @import("http/Http.zig");
const Platform = @import("browser/js/Platform.zig"); const Platform = @import("runtime/js.zig").Platform;
const Telemetry = @import("telemetry/telemetry.zig").Telemetry; const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
const Notification = @import("notification.zig").Notification; const Notification = @import("notification.zig").Notification;

View File

@@ -37,10 +37,8 @@ pub fn init(allocator: Allocator) Scheduler {
} }
pub fn reset(self: *Scheduler) void { pub fn reset(self: *Scheduler) void {
// Our allocator is the page arena, it's been reset. We cannot use self.high_priority.clearRetainingCapacity();
// clearAndRetainCapacity, since that space is no longer ours self.low_priority.clearRetainingCapacity();
self.high_priority.clearAndFree();
self.low_priority.clearAndFree();
} }
const AddOpts = struct { const AddOpts = struct {

View File

@@ -18,10 +18,10 @@
const std = @import("std"); const std = @import("std");
const js = @import("js/js.zig");
const log = @import("../log.zig"); const log = @import("../log.zig");
const parser = @import("netsurf.zig"); const parser = @import("netsurf.zig");
const Env = @import("env.zig").Env;
const Page = @import("page.zig").Page; const Page = @import("page.zig").Page;
const DataURI = @import("DataURI.zig"); const DataURI = @import("DataURI.zig");
const Http = @import("../http/Http.zig"); const Http = @import("../http/Http.zig");
@@ -38,6 +38,9 @@ page: *Page,
// used to prevent recursive evalutaion // used to prevent recursive evalutaion
is_evaluating: bool, 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 // Only once this is true can deferred scripts be run
static_scripts_done: bool, static_scripts_done: bool,
@@ -45,6 +48,12 @@ static_scripts_done: bool,
// on shutdown/abort, we need to cleanup any pending ones. // on shutdown/abort, we need to cleanup any pending ones.
asyncs: OrderList, 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 // Normal scripts (non-deferred & non-async). These must be executed in order
scripts: OrderList, scripts: OrderList,
@@ -58,22 +67,6 @@ client: *Http.Client,
allocator: Allocator, allocator: Allocator,
buffer_pool: BufferPool, buffer_pool: BufferPool,
script_pool: std.heap.MemoryPool(PendingScript), 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; const OrderList = std.DoublyLinkedList;
@@ -85,51 +78,27 @@ pub fn init(browser: *Browser, page: *Page) ScriptManager {
.asyncs = .{}, .asyncs = .{},
.scripts = .{}, .scripts = .{},
.deferreds = .{}, .deferreds = .{},
.importmap = .empty, .asyncs_ready = .{},
.sync_modules = .empty,
.is_evaluating = false, .is_evaluating = false,
.allocator = allocator, .allocator = allocator,
.client = browser.http_client, .client = browser.http_client,
.static_scripts_done = false, .static_scripts_done = false,
.buffer_pool = BufferPool.init(allocator, 5), .buffer_pool = BufferPool.init(allocator, 5),
.script_pool = std.heap.MemoryPool(PendingScript).init(allocator), .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 { pub fn deinit(self: *ScriptManager) void {
self.reset(); 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.buffer_pool.deinit();
self.script_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 { 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.asyncs);
self.clearList(&self.scripts); self.clearList(&self.scripts);
self.clearList(&self.deferreds); self.clearList(&self.deferreds);
self.clearList(&self.asyncs_ready);
self.static_scripts_done = false; self.static_scripts_done = false;
} }
@@ -142,7 +111,7 @@ fn clearList(_: *const ScriptManager, list: *OrderList) void {
std.debug.assert(list.first == null); std.debug.assert(list.first == null);
} }
pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime ctx: []const u8) !void { pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
if (try parser.elementGetAttribute(element, "nomodule") != null) { if (try parser.elementGetAttribute(element, "nomodule") != null) {
// these scripts should only be loaded if we don't support modules // these scripts should only be loaded if we don't support modules
// but since we do support modules, we can just skip them. // but since we do support modules, we can just skip them.
@@ -175,9 +144,6 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
if (std.ascii.eqlIgnoreCase(script_type, "module")) { if (std.ascii.eqlIgnoreCase(script_type, "module")) {
break :blk .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. // "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 // Common other ones are application/json, application/ld+json, text/template
@@ -191,10 +157,9 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
if (try parser.elementGetAttribute(element, "src")) |src| { if (try parser.elementGetAttribute(element, "src")) |src| {
if (try DataURI.parse(page.arena, src)) |data_uri| { if (try DataURI.parse(page.arena, src)) |data_uri| {
source = .{ .@"inline" = data_uri }; source = .{ .@"inline" = data_uri };
} else {
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true });
source = .{ .remote = .{} };
} }
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true });
source = .{ .remote = .{} };
} else { } else {
const inline_source = parser.nodeTextContent(@ptrCast(element)) orelse return; const inline_source = parser.nodeTextContent(@ptrCast(element)) orelse return;
source = .{ .@"inline" = inline_source }; source = .{ .@"inline" = inline_source };
@@ -212,7 +177,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
if (source == .@"inline" and self.scripts.first == null) { if (source == .@"inline" and self.scripts.first == null) {
// inline script with no pending scripts, execute it immediately. // inline script with no pending scripts, execute it immediately.
// (if there is a pending script, then we cannot execute this immediately // (if there is a pending script, then we cannot execute this immediately
// as it needs to be executed in order) // as it needs to best executed in order)
return script.eval(page); return script.eval(page);
} }
@@ -233,11 +198,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
self.scripts.append(&pending_script.node); self.scripts.append(&pending_script.node);
return; return;
} else { } else {
log.debug(.http, "script queue", .{ log.debug(.http, "script queue", .{ .url = remote_url.? });
.ctx = ctx,
.url = remote_url.?,
.stack = page.js.stackTrace() catch "???",
});
} }
pending_script.getList().append(&pending_script.node); pending_script.getList().append(&pending_script.node);
@@ -262,144 +223,88 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
}); });
} }
// Resolve a module specifier to an valid URL. // @TODO: Improving this would have the simplest biggest performance improvement
pub fn resolveSpecifier(self: *ScriptManager, arena: Allocator, specifier: []const u8, base: []const u8) ![:0]const u8 { // for most sites.
// If the specifier is mapped in the importmap, return the pre-resolved value. //
if (self.importmap.get(specifier)) |s| { // For JS imports (both static and dynamic), we currently block to get the
return s; // 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();
} }
return URL.stitch( var blocking = Blocking{
arena, .allocator = self.allocator,
specifier, .buffer_pool = &self.buffer_pool,
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(); var headers = try self.client.newHeaders();
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers); 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.*;
// We can have multiple scripts waiting for the same module in concurrency.
// We use the waiters to ensures only the last waiter deinit the resources.
sync.waiters += 1;
defer sync.waiters -= 1;
var client = self.client; var client = self.client;
while (true) { try client.blockingRequest(.{
switch (sync.state) {
.loading => {},
.done => {
if (sync.waiters == 1) {
// Our caller has its own higher level cache (caching the
// actual compiled module). There's no reason for us to keep
// this if we are the last waiter.
defer self.sync_module_pool.destroy(sync);
defer self.sync_modules.removeByPtr(entry.key_ptr);
return .{
.shared = false,
.buffer = sync.buffer,
.buffer_pool = &self.buffer_pool,
};
}
return .{
.shared = true,
.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);
log.debug(.http, "script queue", .{
.url = url,
.ctx = "dynamic module",
.referrer = referrer,
.stack = self.page.js.stackTrace() catch "???",
});
try self.client.request(.{
.url = url, .url = url,
.method = .GET, .method = .GET,
.headers = headers, .headers = headers,
.cookie_jar = self.page.cookie_jar, .cookie_jar = self.page.cookie_jar,
.ctx = async, .ctx = &blocking,
.resource_type = .script, .resource_type = .script,
.start_callback = if (log.enabled(.http, .debug)) AsyncModule.startCallback else null, .start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null,
.header_callback = AsyncModule.headerCallback, .header_callback = Blocking.headerCallback,
.data_callback = AsyncModule.dataCallback, .data_callback = Blocking.dataCallback,
.done_callback = AsyncModule.doneCallback, .done_callback = Blocking.doneCallback,
.error_callback = AsyncModule.errorCallback, .error_callback = Blocking.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 { pub fn staticScriptsDone(self: *ScriptManager) void {
std.debug.assert(self.static_scripts_done == false); std.debug.assert(self.static_scripts_done == false);
self.static_scripts_done = true; self.static_scripts_done = true;
self.evaluate();
} }
// try to evaluate completed scripts (in order). This is called whenever a script // try to evaluate completed scripts (in order). This is called whenever a script
@@ -413,10 +318,24 @@ fn evaluate(self: *ScriptManager) void {
return; 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; const page = self.page;
self.is_evaluating = true; self.is_evaluating = true;
defer self.is_evaluating = false; 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| { while (self.scripts.first) |n| {
var pending_script: *PendingScript = @fieldParentPtr("node", n); var pending_script: *PendingScript = @fieldParentPtr("node", n);
if (pending_script.complete == false) { if (pending_script.complete == false) {
@@ -466,12 +385,6 @@ pub fn isDone(self: *const ScriptManager) bool {
self.deferreds.first == null; // and there are no more <script defer src=> to wait for self.deferreds.first == null; // and there are no more <script defer src=> to wait for
} }
fn asyncScriptIsDone(self: *ScriptManager) void {
if (self.isDone()) {
self.page.documentIsComplete();
}
}
fn startCallback(transfer: *Http.Transfer) !void { fn startCallback(transfer: *Http.Transfer) !void {
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx)); const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
script.startCallback(transfer) catch |err| { script.startCallback(transfer) catch |err| {
@@ -503,38 +416,6 @@ fn errorCallback(ctx: *anyopaque, err: anyerror) void {
script.errorCallback(err); 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.raw,
.{ .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. // A script which is pending execution.
// It could be pending because: // It could be pending because:
// (a) we're still downloading its content or // (a) we're still downloading its content or
@@ -612,15 +493,11 @@ pub const PendingScript = struct {
const manager = self.manager; const manager = self.manager;
self.complete = true; self.complete = true;
if (!self.script.is_async) { if (self.script.is_async) {
manager.evaluate(); manager.asyncs.remove(&self.node);
return; manager.asyncs_ready.append(&self.node);
} }
// async script can be evaluated immediately manager.evaluate();
self.script.eval(manager.page);
self.deinit();
// asyncScriptIsDone must be run after the pending script is deinit.
manager.asyncScriptIsDone();
} }
fn errorCallback(self: *PendingScript, err: anyerror) void { fn errorCallback(self: *PendingScript, err: anyerror) void {
@@ -644,7 +521,7 @@ pub const PendingScript = struct {
const script = &self.script; const script = &self.script;
if (script.is_async) { if (script.is_async) {
return &self.manager.asyncs; return if (self.complete) &self.manager.asyncs_ready else &self.manager.asyncs;
} }
if (script.is_defer) { if (script.is_defer) {
@@ -666,12 +543,11 @@ const Script = struct {
const Kind = enum { const Kind = enum {
module, module,
javascript, javascript,
importmap,
}; };
const Callback = union(enum) { const Callback = union(enum) {
string: []const u8, string: []const u8,
function: js.Function, function: Env.Function,
}; };
const Source = union(enum) { const Source = union(enum) {
@@ -707,25 +583,8 @@ const Script = struct {
.cacheable = cacheable, .cacheable = cacheable,
}); });
// Handle importmap special case here: the content is a JSON containing const js_context = page.main_context;
// imports. var try_catch: Env.TryCatch = undefined;
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); try_catch.init(js_context);
defer try_catch.deinit(); defer try_catch.deinit();
@@ -735,9 +594,8 @@ const Script = struct {
.javascript => _ = js_context.eval(content, url) catch break :blk false, .javascript => _ = js_context.eval(content, url) catch break :blk false,
.module => { .module => {
// We don't care about waiting for the evaluation here. // We don't care about waiting for the evaluation here.
js_context.module(false, content, url, cacheable) catch break :blk false; _ = js_context.module(content, url, cacheable) catch break :blk false;
}, },
.importmap => unreachable, // handled before the try/catch.
} }
break :blk true; break :blk true;
}; };
@@ -768,11 +626,11 @@ const Script = struct {
switch (callback) { switch (callback) {
.string => |str| { .string => |str| {
var try_catch: js.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(page.js); try_catch.init(page.main_context);
defer try_catch.deinit(); defer try_catch.deinit();
_ = page.js.exec(str, typ) catch |err| { _ = page.main_context.exec(str, typ) catch |err| {
const msg = try_catch.err(page.arena) catch @errorName(err) orelse "unknown"; const msg = try_catch.err(page.arena) catch @errorName(err) orelse "unknown";
log.warn(.user_script, "script callback", .{ log.warn(.user_script, "script callback", .{
.url = self.url, .url = self.url,
@@ -790,7 +648,7 @@ const Script = struct {
}; };
defer parser.eventDestroy(loadevt); defer parser.eventDestroy(loadevt);
var result: js.Function.Result = undefined; var result: Env.Function.Result = undefined;
const iface = Event.toInterface(loadevt); const iface = Event.toInterface(loadevt);
f.tryCall(void, .{iface}, &result) catch { f.tryCall(void, .{iface}, &result) catch {
log.warn(.user_script, "script callback", .{ log.warn(.user_script, "script callback", .{
@@ -893,17 +751,16 @@ const BufferPool = struct {
} }
}; };
const SyncModule = struct { const Blocking = struct {
manager: *ScriptManager, allocator: Allocator,
buffer_pool: *BufferPool,
state: State = .{ .running = {} },
buffer: std.ArrayListUnmanaged(u8) = .{}, buffer: std.ArrayListUnmanaged(u8) = .{},
state: State = .loading,
// number of waiters for the module.
waiters: u8 = 0,
const State = union(enum) { const State = union(enum) {
done, running: void,
loading,
err: anyerror, err: anyerror,
done: BlockingResult,
}; };
fn startCallback(transfer: *Http.Transfer) !void { fn startCallback(transfer: *Http.Transfer) !void {
@@ -919,13 +776,12 @@ const SyncModule = struct {
.content_type = header.contentType(), .content_type = header.contentType(),
}); });
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx));
if (header.status != 200) { if (header.status != 200) {
self.finished(.{ .err = error.InvalidStatusCode });
return error.InvalidStatusCode; return error.InvalidStatusCode;
} }
self.buffer = self.manager.buffer_pool.get(); var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
self.buffer = self.buffer_pool.get();
} }
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void { fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
@@ -935,8 +791,8 @@ const SyncModule = struct {
// .blocking = true, // .blocking = true,
// }); // });
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx)); var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
self.buffer.appendSlice(self.manager.allocator, data) catch |err| { self.buffer.appendSlice(self.allocator, data) catch |err| {
log.err(.http, "SM.dataCallback", .{ log.err(.http, "SM.dataCallback", .{
.err = err, .err = err,
.len = data.len, .len = data.len,
@@ -948,107 +804,29 @@ const SyncModule = struct {
} }
fn doneCallback(ctx: *anyopaque) !void { fn doneCallback(ctx: *anyopaque) !void {
var self: *SyncModule = @ptrCast(@alignCast(ctx)); var self: *Blocking = @ptrCast(@alignCast(ctx));
self.finished(.done); self.state = .{ .done = .{
}
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
var self: *SyncModule = @ptrCast(@alignCast(ctx));
self.finished(.{ .err = err });
}
fn finished(self: *SyncModule, state: State) void {
self.state = state;
}
};
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, .{
.shared = false,
.buffer = self.buffer, .buffer = self.buffer,
.buffer_pool = &self.manager.buffer_pool, .buffer_pool = self.buffer_pool,
}); } };
} }
fn errorCallback(ctx: *anyopaque, err: anyerror) void { fn errorCallback(ctx: *anyopaque, err: anyerror) void {
var self: *AsyncModule = @ptrCast(@alignCast(ctx)); var self: *Blocking = @ptrCast(@alignCast(ctx));
self.state = .{ .err = err };
if (err != error.Abort) { self.buffer_pool.release(self.buffer);
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 { pub const BlockingResult = struct {
buffer: std.ArrayListUnmanaged(u8), buffer: std.ArrayListUnmanaged(u8),
buffer_pool: *BufferPool, buffer_pool: *BufferPool,
shared: bool,
pub fn deinit(self: *GetResult) void { pub fn deinit(self: *BlockingResult) void {
// if the result is shared, don't deinit.
if (self.shared) {
return;
}
self.buffer_pool.release(self.buffer); self.buffer_pool.release(self.buffer);
} }
pub fn src(self: *const GetResult) []const u8 { pub fn src(self: *const BlockingResult) []const u8 {
return self.buffer.items; return self.buffer.items;
} }
}; };

View File

@@ -1,189 +0,0 @@
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();
}

View File

@@ -26,7 +26,7 @@
// this quickly proved necessary, since different fields are needed on the same // 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. // data at different levels of the prototype chain. This isn't memory efficient.
const js = @import("js/js.zig"); const Env = @import("env.zig").Env;
const parser = @import("netsurf.zig"); const parser = @import("netsurf.zig");
const DataSet = @import("html/DataSet.zig"); const DataSet = @import("html/DataSet.zig");
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot; const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
@@ -34,8 +34,8 @@ const StyleSheet = @import("cssom/StyleSheet.zig");
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig"); const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");
// for HTMLScript (but probably needs to be added to more) // for HTMLScript (but probably needs to be added to more)
onload: ?js.Function = null, onload: ?Env.Function = null,
onerror: ?js.Function = null, onerror: ?Env.Function = null,
// for HTMLElement // for HTMLElement
style: CSSStyleDeclaration = .empty, style: CSSStyleDeclaration = .empty,
@@ -53,7 +53,7 @@ style_sheet: ?*StyleSheet = null,
// for dom/document // for dom/document
active_element: ?*parser.Element = null, active_element: ?*parser.Element = null,
adopted_style_sheets: ?js.Object = null, adopted_style_sheets: ?Env.JsObject = null,
// for HTMLSelectElement // for HTMLSelectElement
// By default, if no option is explicitly selected, the first option should // By default, if no option is explicitly selected, the first option should

View File

@@ -21,8 +21,8 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const js = @import("js/js.zig");
const State = @import("State.zig"); const State = @import("State.zig");
const Env = @import("env.zig").Env;
const App = @import("../app.zig").App; const App = @import("../app.zig").App;
const Session = @import("session.zig").Session; const Session = @import("session.zig").Session;
const Notification = @import("../notification.zig").Notification; const Notification = @import("../notification.zig").Notification;
@@ -34,12 +34,11 @@ const HttpClient = @import("../http/Client.zig");
// You can create multiple browser instances. // You can create multiple browser instances.
// A browser contains only one session. // A browser contains only one session.
pub const Browser = struct { pub const Browser = struct {
env: *js.Env, env: *Env,
app: *App, app: *App,
session: ?Session, session: ?Session,
allocator: Allocator, allocator: Allocator,
http_client: *HttpClient, http_client: *HttpClient,
call_arena: ArenaAllocator,
page_arena: ArenaAllocator, page_arena: ArenaAllocator,
session_arena: ArenaAllocator, session_arena: ArenaAllocator,
transfer_arena: ArenaAllocator, transfer_arena: ArenaAllocator,
@@ -49,7 +48,7 @@ pub const Browser = struct {
pub fn init(app: *App) !Browser { pub fn init(app: *App) !Browser {
const allocator = app.allocator; const allocator = app.allocator;
const env = try js.Env.init(allocator, &app.platform, .{}); const env = try Env.init(allocator, &app.platform, .{});
errdefer env.deinit(); errdefer env.deinit();
const notification = try Notification.init(allocator, app.notification); const notification = try Notification.init(allocator, app.notification);
@@ -64,7 +63,6 @@ pub const Browser = struct {
.allocator = allocator, .allocator = allocator,
.notification = notification, .notification = notification,
.http_client = app.http.client, .http_client = app.http.client,
.call_arena = ArenaAllocator.init(allocator),
.page_arena = ArenaAllocator.init(allocator), .page_arena = ArenaAllocator.init(allocator),
.session_arena = ArenaAllocator.init(allocator), .session_arena = ArenaAllocator.init(allocator),
.transfer_arena = ArenaAllocator.init(allocator), .transfer_arena = ArenaAllocator.init(allocator),
@@ -75,7 +73,6 @@ pub const Browser = struct {
pub fn deinit(self: *Browser) void { pub fn deinit(self: *Browser) void {
self.closeSession(); self.closeSession();
self.env.deinit(); self.env.deinit();
self.call_arena.deinit();
self.page_arena.deinit(); self.page_arena.deinit();
self.session_arena.deinit(); self.session_arena.deinit();
self.transfer_arena.deinit(); self.transfer_arena.deinit();

View File

@@ -20,47 +20,47 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const JsObject = @import("../env.zig").Env.JsObject;
pub const Console = struct { pub const Console = struct {
// TODO: configurable writer // TODO: configurable writer
timers: std.StringHashMapUnmanaged(u32) = .{}, timers: std.StringHashMapUnmanaged(u32) = .{},
counts: std.StringHashMapUnmanaged(u32) = .{}, counts: std.StringHashMapUnmanaged(u32) = .{},
pub fn _lp(values: []js.Object, page: *Page) !void { pub fn _lp(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) }); log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
} }
pub fn _log(values: []js.Object, page: *Page) !void { pub fn _log(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.info(.console, "info", .{ .args = try serializeValues(values, page) }); log.info(.console, "info", .{ .args = try serializeValues(values, page) });
} }
pub fn _info(values: []js.Object, page: *Page) !void { pub fn _info(values: []JsObject, page: *Page) !void {
return _log(values, page); return _log(values, page);
} }
pub fn _debug(values: []js.Object, page: *Page) !void { pub fn _debug(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) }); log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
} }
pub fn _warn(values: []js.Object, page: *Page) !void { pub fn _warn(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) }); log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
} }
pub fn _error(values: []js.Object, page: *Page) !void { pub fn _error(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
@@ -71,16 +71,6 @@ 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 _clear() void {}
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void { pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
@@ -142,7 +132,7 @@ pub const Console = struct {
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value }); log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
} }
pub fn _assert(assertion: js.Object, values: []js.Object, page: *Page) !void { pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
if (assertion.isTruthy()) { if (assertion.isTruthy()) {
return; return;
} }
@@ -153,7 +143,7 @@ pub const Console = struct {
log.info(.console, "assertion failed", .{ .values = serialized_values }); log.info(.console, "assertion failed", .{ .values = serialized_values });
} }
fn serializeValues(values: []js.Object, page: *Page) ![]const u8 { fn serializeValues(values: []JsObject, page: *Page) ![]const u8 {
if (values.len == 0) { if (values.len == 0) {
return ""; return "";
} }

View File

@@ -17,14 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const uuidv4 = @import("../../id.zig").uuidv4; const uuidv4 = @import("../../id.zig").uuidv4;
// https://w3c.github.io/webcrypto/#crypto-interface // https://w3c.github.io/webcrypto/#crypto-interface
pub const Crypto = struct { pub const Crypto = struct {
_not_empty: bool = true, _not_empty: bool = true,
pub fn _getRandomValues(_: *const Crypto, js_obj: js.Object) !js.Object { pub fn _getRandomValues(_: *const Crypto, js_obj: Env.JsObject) !Env.JsObject {
var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues); var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues);
const buf = into.asBuffer(); const buf = into.asBuffer();
if (buf.len > 65_536) { if (buf.len > 65_536) {

View File

@@ -562,7 +562,7 @@ pub const Selector = union(enum) {
const ntag = try n.tag(); const ntag = try n.tag();
if (std.ascii.eqlIgnoreCase("input", ntag)) { if (std.ascii.eqlIgnoreCase("intput", ntag)) {
const ntype = try n.attr("type"); const ntype = try n.attr("type");
if (ntype == null) return false; if (ntype == null) return false;

View File

@@ -18,7 +18,7 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const StyleSheet = @import("StyleSheet.zig"); const StyleSheet = @import("StyleSheet.zig");
const CSSRuleList = @import("CSSRuleList.zig"); const CSSRuleList = @import("CSSRuleList.zig");
@@ -73,13 +73,15 @@ pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
_ = self.css_rules.list.orderedRemove(index); _ = self.css_rules.list.orderedRemove(index);
} }
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise { pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !Env.Promise {
_ = self; _ = self;
_ = text; _ = text;
// TODO: clear self.css_rules // TODO: clear self.css_rules
// parse text and re-populate self.css_rules // parse text and re-populate self.css_rules
return page.js.resolvePromise({}); const resolver = page.main_context.createPromiseResolver();
try resolver.resolve({});
return resolver.promise();
} }
pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void { pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {

View File

@@ -18,17 +18,19 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page; 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(); const Animation = @This();
effect: ?js.Object, effect: ?JsObject,
timeline: ?js.Object, timeline: ?JsObject,
ready_resolver: ?js.PromiseResolver, ready_resolver: ?PromiseResolver,
finished_resolver: ?js.PromiseResolver, finished_resolver: ?PromiseResolver,
pub fn constructor(effect: ?js.Object, timeline: ?js.Object) !Animation { pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !Animation {
return .{ return .{
.effect = if (effect) |eo| try eo.persist() else null, .effect = if (effect) |eo| try eo.persist() else null,
.timeline = if (timeline) |to| try to.persist() else null, .timeline = if (timeline) |to| try to.persist() else null,
@@ -47,37 +49,37 @@ pub fn get_pending(self: *const Animation) bool {
return false; return false;
} }
pub fn get_finished(self: *Animation, page: *Page) !js.Promise { pub fn get_finished(self: *Animation, page: *Page) !Promise {
if (self.finished_resolver == null) { if (self.finished_resolver == null) {
const resolver = page.js.createPromiseResolver(.none); const resolver = page.main_context.createPromiseResolver();
try resolver.resolve(self); try resolver.resolve(self);
self.finished_resolver = resolver; self.finished_resolver = resolver;
} }
return self.finished_resolver.?.promise(); return self.finished_resolver.?.promise();
} }
pub fn get_ready(self: *Animation, page: *Page) !js.Promise { pub fn get_ready(self: *Animation, page: *Page) !Promise {
// never resolved, because we're always "finished" // never resolved, because we're always "finished"
if (self.ready_resolver == null) { if (self.ready_resolver == null) {
const resolver = page.js.createPromiseResolver(.none); const resolver = page.main_context.createPromiseResolver();
self.ready_resolver = resolver; self.ready_resolver = resolver;
} }
return self.ready_resolver.?.promise(); return self.ready_resolver.?.promise();
} }
pub fn get_effect(self: *const Animation) ?js.Object { pub fn get_effect(self: *const Animation) ?JsObject {
return self.effect; return self.effect;
} }
pub fn set_effect(self: *Animation, effect: js.Object) !void { pub fn set_effect(self: *Animation, effect: JsObject) !void {
self.effect = try effect.persist(); self.effect = try effect.persist();
} }
pub fn get_timeline(self: *const Animation) ?js.Object { pub fn get_timeline(self: *const Animation) ?JsObject {
return self.timeline; return self.timeline;
} }
pub fn set_timeline(self: *Animation, timeline: js.Object) !void { pub fn set_timeline(self: *Animation, timeline: JsObject) !void {
self.timeline = try timeline.persist(); self.timeline = try timeline.persist();
} }

View File

@@ -1,329 +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 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");
}

View File

@@ -20,11 +20,13 @@ const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler; const EventHandler = @import("../events/event.zig").EventHandler;
const JsObject = Env.JsObject;
const Function = Env.Function;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const MAX_QUEUE_SIZE = 10; const MAX_QUEUE_SIZE = 10;
@@ -70,22 +72,22 @@ pub const MessagePort = struct {
pair: *MessagePort, pair: *MessagePort,
closed: bool = false, closed: bool = false,
started: bool = false, started: bool = false,
onmessage_cbk: ?js.Function = null, onmessage_cbk: ?Function = null,
onmessageerror_cbk: ?js.Function = null, onmessageerror_cbk: ?Function = null,
// This is the queue of messages to dispatch to THIS MessagePort when the // This is the queue of messages to dispatch to THIS MessagePort when the
// MessagePort is started. // MessagePort is started.
queue: std.ArrayListUnmanaged(js.Object) = .empty, queue: std.ArrayListUnmanaged(JsObject) = .empty,
pub const PostMessageOption = union(enum) { pub const PostMessageOption = union(enum) {
transfer: js.Object, transfer: JsObject,
options: Opts, options: Opts,
pub const Opts = struct { pub const Opts = struct {
transfer: js.Object, transfer: JsObject,
}; };
}; };
pub fn _postMessage(self: *MessagePort, obj: js.Object, opts_: ?PostMessageOption, page: *Page) !void { pub fn _postMessage(self: *MessagePort, obj: JsObject, opts_: ?PostMessageOption, page: *Page) !void {
if (self.closed) { if (self.closed) {
return; return;
} }
@@ -122,10 +124,10 @@ pub const MessagePort = struct {
self.pair.closed = true; self.pair.closed = true;
} }
pub fn get_onmessage(self: *MessagePort) ?js.Function { pub fn get_onmessage(self: *MessagePort) ?Function {
return self.onmessage_cbk; return self.onmessage_cbk;
} }
pub fn get_onmessageerror(self: *MessagePort) ?js.Function { pub fn get_onmessageerror(self: *MessagePort) ?Function {
return self.onmessageerror_cbk; return self.onmessageerror_cbk;
} }
@@ -150,7 +152,7 @@ pub const MessagePort = struct {
// called from our pair. If port1.postMessage("x") is called, then this // called from our pair. If port1.postMessage("x") is called, then this
// will be called on port2. // will be called on port2.
fn dispatchOrQueue(self: *MessagePort, obj: js.Object, arena: Allocator) !void { fn dispatchOrQueue(self: *MessagePort, obj: JsObject, arena: Allocator) !void {
// our pair should have checked this already // our pair should have checked this already
std.debug.assert(self.closed == false); std.debug.assert(self.closed == false);
@@ -165,7 +167,7 @@ pub const MessagePort = struct {
return self.queue.append(arena, try obj.persist()); return self.queue.append(arena, try obj.persist());
} }
fn dispatch(self: *MessagePort, obj: js.Object) !void { fn dispatch(self: *MessagePort, obj: JsObject) !void {
// obj is already persisted, don't use `MessageEvent.constructor`, but // obj is already persisted, don't use `MessageEvent.constructor`, but
// go directly to `init`, which assumes persisted objects. // go directly to `init`, which assumes persisted objects.
var evt = try MessageEvent.init(.{ .data = obj }); var evt = try MessageEvent.init(.{ .data = obj });
@@ -180,7 +182,7 @@ pub const MessagePort = struct {
alloc: Allocator, alloc: Allocator,
typ: []const u8, typ: []const u8,
listener: EventHandler.Listener, listener: EventHandler.Listener,
) !?js.Function { ) !?Function {
const target = @as(*parser.EventTarget, @ptrCast(self)); const target = @as(*parser.EventTarget, @ptrCast(self));
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable; const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
return eh.callback; return eh.callback;
@@ -205,12 +207,12 @@ pub const MessageEvent = struct {
pub const union_make_copy = true; pub const union_make_copy = true;
proto: parser.Event, proto: parser.Event,
data: ?js.Object, data: ?JsObject,
// You would think if port1 sends to port2, the source would be port2 // 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 // (which is how I read the documentation), but it appears to always be
// null. It can always be set explicitly via the constructor; // null. It can always be set explicitly via the constructor;
source: ?js.Object, source: ?JsObject,
origin: []const u8, origin: []const u8,
@@ -224,8 +226,8 @@ pub const MessageEvent = struct {
ports: []*MessagePort, ports: []*MessagePort,
const Options = struct { const Options = struct {
data: ?js.Object = null, data: ?JsObject = null,
source: ?js.Object = null, source: ?JsObject = null,
origin: []const u8 = "", origin: []const u8 = "",
lastEventId: []const u8 = "", lastEventId: []const u8 = "",
ports: []*MessagePort = &.{}, ports: []*MessagePort = &.{},
@@ -241,7 +243,7 @@ pub const MessageEvent = struct {
}); });
} }
// This is like "constructor", but it assumes js.Objects have already been // This is like "constructor", but it assumes JsObjects have already been
// persisted. Necessary because this `new MessageEvent()` can be called // persisted. Necessary because this `new MessageEvent()` can be called
// directly from JS OR from a port.postMessage. In the latter case, data // 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); // may have already been persisted (as it might need to be queued);
@@ -261,7 +263,7 @@ pub const MessageEvent = struct {
}; };
} }
pub fn get_data(self: *const MessageEvent) !?js.Object { pub fn get_data(self: *const MessageEvent) !?JsObject {
return self.data; return self.data;
} }
@@ -269,7 +271,7 @@ pub const MessageEvent = struct {
return self.origin; return self.origin;
} }
pub fn get_source(self: *const MessageEvent) ?js.Object { pub fn get_source(self: *const MessageEvent) ?JsObject {
return self.source; return self.source;
} }

View File

@@ -17,9 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
@@ -39,6 +37,8 @@ const Range = @import("range.zig").Range;
const CustomEvent = @import("../events/custom_event.zig").CustomEvent; const CustomEvent = @import("../events/custom_event.zig").CustomEvent;
const Env = @import("../env.zig").Env;
const DOMImplementation = @import("implementation.zig").DOMImplementation; const DOMImplementation = @import("implementation.zig").DOMImplementation;
// WEB IDL https://dom.spec.whatwg.org/#document // WEB IDL https://dom.spec.whatwg.org/#document
@@ -155,13 +155,13 @@ pub const Document = struct {
// the spec changed to return an HTMLCollection instead. // the spec changed to return an HTMLCollection instead.
// That's why we reimplemented getElementsByTagName by using an // That's why we reimplemented getElementsByTagName by using an
// HTMLCollection in zig here. // HTMLCollection in zig here.
pub fn _getElementsByTagName(self: *parser.Document, tag_name: js.String) !collection.HTMLCollection { pub fn _getElementsByTagName(self: *parser.Document, tag_name: Env.String) !collection.HTMLCollection {
return collection.HTMLCollectionByTagName(parser.documentToNode(self), tag_name.string, .{ return collection.HTMLCollectionByTagName(parser.documentToNode(self), tag_name.string, .{
.include_root = true, .include_root = true,
}); });
} }
pub fn _getElementsByClassName(self: *parser.Document, class_names: js.String) !collection.HTMLCollection { pub fn _getElementsByClassName(self: *parser.Document, class_names: Env.String) !collection.HTMLCollection {
return collection.HTMLCollectionByClassName(parser.documentToNode(self), class_names.string, .{ return collection.HTMLCollectionByClassName(parser.documentToNode(self), class_names.string, .{
.include_root = true, .include_root = true,
}); });
@@ -299,26 +299,21 @@ pub const Document = struct {
return &.{}; return &.{};
} }
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !js.Object { pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !Env.JsObject {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
if (state.adopted_style_sheets) |obj| { if (state.adopted_style_sheets) |obj| {
return obj; return obj;
} }
const obj = try page.js.createArray(0).persist(); const obj = try page.main_context.newArray(0).persist();
state.adopted_style_sheets = obj; state.adopted_style_sheets = obj;
return obj; return obj;
} }
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: js.Object, page: *Page) !void { pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: Env.JsObject, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.adopted_style_sheets = try sheets.persist(); state.adopted_style_sheets = try sheets.persist();
} }
pub fn _hasFocus(_: *parser.Document) bool {
log.debug(.web_api, "not implemented", .{ .feature = "Document hasFocus" });
return true;
}
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

@@ -25,6 +25,7 @@ const NodeList = @import("nodelist.zig");
const Node = @import("node.zig"); const Node = @import("node.zig");
const ResizeObserver = @import("resize_observer.zig"); const ResizeObserver = @import("resize_observer.zig");
const MutationObserver = @import("mutation_observer.zig"); const MutationObserver = @import("mutation_observer.zig");
const IntersectionObserver = @import("intersection_observer.zig");
const DOMParser = @import("dom_parser.zig").DOMParser; const DOMParser = @import("dom_parser.zig").DOMParser;
const TreeWalker = @import("tree_walker.zig").TreeWalker; const TreeWalker = @import("tree_walker.zig").TreeWalker;
const NodeIterator = @import("node_iterator.zig").NodeIterator; const NodeIterator = @import("node_iterator.zig").NodeIterator;
@@ -43,6 +44,7 @@ pub const Interfaces = .{
Node.Interfaces, Node.Interfaces,
ResizeObserver.Interfaces, ResizeObserver.Interfaces,
MutationObserver.Interfaces, MutationObserver.Interfaces,
IntersectionObserver.Interfaces,
DOMParser, DOMParser,
TreeWalker, TreeWalker,
NodeIterator, NodeIterator,
@@ -52,5 +54,4 @@ pub const Interfaces = .{
@import("range.zig").Interfaces, @import("range.zig").Interfaces,
@import("Animation.zig"), @import("Animation.zig"),
@import("MessageChannel.zig").Interfaces, @import("MessageChannel.zig").Interfaces,
@import("IntersectionObserver.zig").Interfaces,
}; };

View File

@@ -18,8 +18,8 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const css = @import("css.zig"); const css = @import("css.zig");
@@ -34,6 +34,7 @@ const HTMLElem = @import("../html/elements.zig");
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot; const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
const Animation = @import("Animation.zig"); const Animation = @import("Animation.zig");
const JsObject = @import("../env.zig").JsObject;
pub const Union = @import("../html/elements.zig").Union; pub const Union = @import("../html/elements.zig").Union;
@@ -135,10 +136,6 @@ pub const Element = struct {
return try parser.elementSetAttribute(self, "slot", slot); 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 { pub fn get_classList(self: *parser.Element) !*parser.TokenList {
return try parser.tokenListCreate(self, "class"); return try parser.tokenListCreate(self, "class");
} }
@@ -191,9 +188,11 @@ pub const Element = struct {
// a new fragment // a new fragment
const clean = try parser.documentCreateDocumentFragment(doc); const clean = try parser.documentCreateDocumentFragment(doc);
const children = try parser.nodeGetChildNodes(body); const children = try parser.nodeGetChildNodes(body);
// always index 0, because nodeAppendChild moves the node out of const ln = parser.nodeListLength(children);
// the nodeList and into the new tree for (0..ln) |_| {
while (parser.nodeListItem(children, 0)) |child| { // 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;
_ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child); _ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child);
} }
@@ -207,102 +206,27 @@ pub const Element = struct {
{ {
// First, copy some of the head element // First, copy some of the head element
const children = try parser.nodeGetChildNodes(head); const children = try parser.nodeGetChildNodes(head);
// always index 0, because nodeAppendChild moves the node out of const ln = parser.nodeListLength(children);
// the nodeList and into the new tree for (0..ln) |_| {
while (parser.nodeListItem(children, 0)) |child| { // 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;
_ = try parser.nodeAppendChild(node, child); _ = try parser.nodeAppendChild(node, child);
} }
} }
{ {
const children = try parser.nodeGetChildNodes(body); const children = try parser.nodeGetChildNodes(body);
// always index 0, because nodeAppendChild moves the node out of const ln = parser.nodeListLength(children);
// the nodeList and into the new tree for (0..ln) |_| {
while (parser.nodeListItem(children, 0)) |child| { // 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;
_ = try parser.nodeAppendChild(node, 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. // 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. // 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 { pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element {
@@ -435,7 +359,7 @@ pub const Element = struct {
return try parser.elementRemoveAttributeNode(self, attr); return try parser.elementRemoveAttributeNode(self, attr);
} }
pub fn _getElementsByTagName(self: *parser.Element, tag_name: js.String) !collection.HTMLCollection { pub fn _getElementsByTagName(self: *parser.Element, tag_name: Env.String) !collection.HTMLCollection {
return collection.HTMLCollectionByTagName( return collection.HTMLCollectionByTagName(
parser.elementToNode(self), parser.elementToNode(self),
tag_name.string, tag_name.string,
@@ -443,7 +367,7 @@ pub const Element = struct {
); );
} }
pub fn _getElementsByClassName(self: *parser.Element, class_names: js.String) !collection.HTMLCollection { pub fn _getElementsByClassName(self: *parser.Element, class_names: Env.String) !collection.HTMLCollection {
return try collection.HTMLCollectionByClassName( return try collection.HTMLCollectionByClassName(
parser.elementToNode(self), parser.elementToNode(self),
class_names.string, class_names.string,
@@ -599,8 +523,6 @@ pub const Element = struct {
contentVisibilityAuto: bool, contentVisibilityAuto: bool,
opacityProperty: bool, opacityProperty: bool,
visibilityProperty: bool, visibilityProperty: bool,
checkVisibilityCSS: bool,
checkOpacity: bool,
}; };
pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool { pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool {
@@ -660,7 +582,7 @@ pub const Element = struct {
return sr; return sr;
} }
pub fn _animate(self: *parser.Element, effect: js.Object, opts: js.Object) !Animation { pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation {
_ = self; _ = self;
_ = opts; _ = opts;
return Animation.constructor(effect, null); return Animation.constructor(effect, null);

View File

@@ -100,9 +100,6 @@ pub const EventTarget = struct {
page: *Page, page: *Page,
) !void { ) !void {
_ = try EventHandler.register(page.arena, self, typ, listener, opts); _ = try EventHandler.register(page.arena, self, typ, listener, opts);
if (std.mem.eql(u8, typ, "slotchange")) {
try page.registerSlotChangeMonitor();
}
} }
const RemoveEventListenerOpts = union(enum) { const RemoveEventListenerOpts = union(enum) {

View File

@@ -23,6 +23,7 @@ const parser = @import("../netsurf.zig");
const Element = @import("element.zig").Element; const Element = @import("element.zig").Element;
const Union = @import("element.zig").Union; const Union = @import("element.zig").Union;
const JsThis = @import("../env.zig").JsThis;
const Walker = @import("walker.zig").Walker; const Walker = @import("walker.zig").Walker;
const Matcher = union(enum) { const Matcher = union(enum) {
@@ -286,7 +287,7 @@ const Opts = struct {
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection // WEB IDL https://dom.spec.whatwg.org/#htmlcollection
// HTMLCollection is re implemented in zig here because libdom // HTMLCollection is re implemented in zig here because libdom
// dom_html_collection expects a comparison function callback as argument. // dom_html_collection expects a comparison function callback as arguement.
// But we wanted a dynamically comparison here, according to the match tagname. // But we wanted a dynamically comparison here, according to the match tagname.
pub const HTMLCollection = struct { pub const HTMLCollection = struct {
matcher: Matcher, matcher: Matcher,
@@ -428,23 +429,24 @@ pub const HTMLCollection = struct {
return null; return null;
} }
pub fn indexed_get(self: *HTMLCollection, index: u32, has_value: *bool) !?Union { pub fn postAttach(self: *HTMLCollection, js_this: JsThis) !void {
return (try _item(self, index)) orelse { const len = try self.get_length();
has_value.* = false; for (0..len) |i| {
return undefined; 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 named_get(self: *const HTMLCollection, name: []const u8, has_value: *bool) !?Union { if (try item_name(e)) |name| {
// Even though an entry might have an empty id, the spec says // Even though an entry might have an empty id, the spec says
// that namedItem("") should always return null // that namedItem("") should always return null
if (name.len == 0) { if (name.len > 0) {
return null; // Named fields should not be enumerable (it is defined with
// the LegacyUnenumerableNamedProperties flag.)
try js_this.set(name, as_interface, .{ .DONT_ENUM = true });
}
}
} }
return (try _namedItem(self, name)) orelse {
has_value.* = false;
return undefined;
};
} }
}; };

View File

@@ -0,0 +1,186 @@
// 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");
}

View File

@@ -18,11 +18,11 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Env = @import("../env.zig").Env;
const NodeList = @import("nodelist.zig").NodeList; const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = .{ pub const Interfaces = .{
@@ -35,21 +35,21 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver // WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
pub const MutationObserver = struct { pub const MutationObserver = struct {
page: *Page, page: *Page,
cbk: js.Function, cbk: Env.Function,
connected: bool,
scheduled: bool, scheduled: bool,
observers: std.ArrayListUnmanaged(*Observer),
// List of records which were observed. When the call scope ends, we need to // List of records which were observed. When the call scope ends, we need to
// execute our callback with it. // execute our callback with it.
observed: std.ArrayListUnmanaged(MutationRecord), observed: std.ArrayListUnmanaged(MutationRecord),
pub fn constructor(cbk: js.Function, page: *Page) !MutationObserver { pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
return .{ return .{
.cbk = cbk, .cbk = cbk,
.page = page, .page = page,
.observed = .{}, .observed = .{},
.connected = true,
.scheduled = false, .scheduled = false,
.observers = .empty,
}; };
} }
@@ -68,17 +68,15 @@ pub const MutationObserver = struct {
.event_node = .{ .id = self.cbk.id, .func = Observer.handle }, .event_node = .{ .id = self.cbk.id, .func = Observer.handle },
}; };
try self.observers.append(arena, observer);
// register node's events // register node's events
if (options.childList or options.subtree) { if (options.childList or options.subtree) {
observer.dom_node_inserted_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMNodeInserted", "DOMNodeInserted",
&observer.event_node, &observer.event_node,
false, false,
); );
observer.dom_node_removed_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMNodeRemoved", "DOMNodeRemoved",
&observer.event_node, &observer.event_node,
@@ -86,7 +84,7 @@ pub const MutationObserver = struct {
); );
} }
if (options.attr()) { if (options.attr()) {
observer.dom_node_attribute_modified_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMAttrModified", "DOMAttrModified",
&observer.event_node, &observer.event_node,
@@ -94,7 +92,7 @@ pub const MutationObserver = struct {
); );
} }
if (options.cdata()) { if (options.cdata()) {
observer.dom_cdata_modified_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMCharacterDataModified", "DOMCharacterDataModified",
&observer.event_node, &observer.event_node,
@@ -102,7 +100,7 @@ pub const MutationObserver = struct {
); );
} }
if (options.subtree) { if (options.subtree) {
observer.dom_subtree_modified_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMSubtreeModified", "DOMSubtreeModified",
&observer.event_node, &observer.event_node,
@@ -113,6 +111,10 @@ pub const MutationObserver = struct {
fn callback(ctx: *anyopaque) ?u32 { fn callback(ctx: *anyopaque) ?u32 {
const self: *MutationObserver = @ptrCast(@alignCast(ctx)); const self: *MutationObserver = @ptrCast(@alignCast(ctx));
if (self.connected == false) {
self.scheduled = true;
return null;
}
self.scheduled = false; self.scheduled = false;
const records = self.observed.items; const records = self.observed.items;
@@ -122,8 +124,8 @@ pub const MutationObserver = struct {
defer self.observed.clearRetainingCapacity(); defer self.observed.clearRetainingCapacity();
var result: js.Function.Result = undefined; var result: Env.Function.Result = undefined;
self.cbk.tryCallWithThis(void, self, .{records}, &result) catch { self.cbk.tryCall(void, .{records}, &result) catch {
log.debug(.user_script, "callback error", .{ log.debug(.user_script, "callback error", .{
.err = result.exception, .err = result.exception,
.stack = result.stack, .stack = result.stack,
@@ -133,55 +135,9 @@ pub const MutationObserver = struct {
return null; return null;
} }
// TODO
pub fn _disconnect(self: *MutationObserver) !void { pub fn _disconnect(self: *MutationObserver) !void {
for (self.observers.items) |observer| { self.connected = false;
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 // TODO
@@ -266,12 +222,6 @@ const Observer = struct {
event_node: parser.EventNode, 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( fn appliesTo(
self: *const Observer, self: *const Observer,
target: *parser.Node, target: *parser.Node,

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const generate = @import("../js/generate.zig"); const generate = @import("../../runtime/generate.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const EventTarget = @import("event_target.zig").EventTarget; const EventTarget = @import("event_target.zig").EventTarget;
@@ -360,30 +360,18 @@ pub const Node = struct {
node: Union, node: Union,
}; };
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult { pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
const composed = if (options) |opts| opts.composed else false; if (options) |options_| if (options_.composed) {
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
};
var current_root = parser.nodeGetRootNode(self); const root = parser.nodeGetRootNode(self);
if (page.getNodeState(root)) |state| {
while (true) { if (state.shadow_root) |sr| {
const node_type = parser.nodeType(current_root); return .{ .shadow_root = sr };
if (node_type == .document_fragment) {
if (parser.documentFragmentGetHost(@ptrCast(current_root))) |host| {
if (page.getNodeState(host)) |state| {
if (state.shadow_root) |sr| {
if (!composed) {
return .{ .shadow_root = sr };
}
current_root = parser.nodeGetRootNode(@ptrCast(sr.host));
continue;
}
}
}
} }
break;
} }
return .{ .node = try Node.toInterface(current_root) }; return .{ .node = try Node.toInterface(root) };
} }
pub fn _hasChildNodes(self: *parser.Node) bool { pub fn _hasChildNodes(self: *parser.Node) bool {
@@ -473,7 +461,7 @@ pub const Node = struct {
// Check if the hierarchy node tree constraints are respected. // Check if the hierarchy node tree constraints are respected.
// For now, it checks only if new nodes are not self. // For now, it checks only if new nodes are not self.
// TODO implements the others constraints. // TODO implements the others contraints.
// see https://dom.spec.whatwg.org/#concept-node-tree // see https://dom.spec.whatwg.org/#concept-node-tree
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool { pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
for (nodes) |n| { for (nodes) |n| {

View File

@@ -17,8 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
pub const NodeFilter = struct { pub const NodeFilter = struct {
@@ -43,13 +43,10 @@ pub const NodeFilter = struct {
const VerifyResult = enum { accept, skip, reject }; const VerifyResult = enum { accept, skip, reject };
pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !VerifyResult { pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !VerifyResult {
const node_type = parser.nodeType(node); const node_type = parser.nodeType(node);
// Verify that we can show this node type. // Verify that we can show this node type.
// Per the DOM spec, what_to_show filters which nodes to return, but should
// still traverse children. So we return .skip (not .reject) when the node
// type doesn't match.
if (!switch (node_type) { if (!switch (node_type) {
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0, .attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0, .cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
@@ -63,7 +60,7 @@ pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !Veri
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0, .notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0, .processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
.text => what_to_show & NodeFilter._SHOW_TEXT != 0, .text => what_to_show & NodeFilter._SHOW_TEXT != 0,
}) return .skip; }) return .reject;
// Verify that we aren't filtering it out. // Verify that we aren't filtering it out.
if (filter) |f| { if (filter) |f| {

View File

@@ -18,8 +18,8 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const NodeFilter = @import("node_filter.zig"); const NodeFilter = @import("node_filter.zig");
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
const NodeUnion = @import("node.zig").Union; const NodeUnion = @import("node.zig").Union;
@@ -37,7 +37,7 @@ pub const NodeIterator = struct {
reference_node: *parser.Node, reference_node: *parser.Node,
what_to_show: u32, what_to_show: u32,
filter: ?NodeIteratorOpts, filter: ?NodeIteratorOpts,
filter_func: ?js.Function, filter_func: ?Env.Function,
pointer_before_current: bool = true, pointer_before_current: bool = true,
// used to track / block recursive filters // used to track / block recursive filters
is_in_callback: bool = false, 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. // 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: // We need the raw JsObject so that we can probe the tri state:
// null, undefined or i32. // null, undefined or i32.
pub const WhatToShow = js.Object; pub const WhatToShow = Env.JsObject;
pub const NodeIteratorOpts = union(enum) { pub const NodeIteratorOpts = union(enum) {
function: js.Function, function: Env.Function,
object: struct { acceptNode: js.Function }, object: struct { acceptNode: Env.Function },
}; };
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator { pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator {
var filter_func: ?js.Function = null; var filter_func: ?Env.Function = null;
if (filter) |f| { if (filter) |f| {
filter_func = switch (f) { filter_func = switch (f) {
.function => |func| func, .function => |func| func,
@@ -74,10 +74,10 @@ pub const NodeIterator = struct {
return .{ return .{
.root = node, .root = node,
.filter = filter,
.reference_node = node, .reference_node = node,
.filter_func = filter_func,
.what_to_show = what_to_show, .what_to_show = what_to_show,
.filter = filter,
.filter_func = filter_func,
}; };
} }
@@ -115,27 +115,14 @@ pub const NodeIterator = struct {
if (try self.firstChild(self.reference_node)) |child| { if (try self.firstChild(self.reference_node)) |child| {
self.reference_node = child; self.reference_node = child;
self.pointer_before_current = false;
return try Node.toInterface(child); return try Node.toInterface(child);
} }
var current = self.reference_node; var current = self.reference_node;
while (current != self.root) { while (current != self.root) {
// Try to get next sibling (including .skip/.reject nodes we need to descend into) if (try self.nextSibling(current)) |sibling| {
if (try self.nextSiblingOrSkipReject(current)) |result| { self.reference_node = sibling;
if (result.should_descend) { return try Node.toInterface(sibling);
// This is a .skip/.reject node - try to find acceptable children within it
if (try self.firstChild(result.node)) |child| {
self.reference_node = child;
return try Node.toInterface(child);
}
// No acceptable children, continue looking at this node's siblings
current = result.node;
continue;
}
// This is an .accept node - return it
self.reference_node = result.node;
return try Node.toInterface(result.node);
} }
current = (parser.nodeParentNode(current)) orelse break; current = (parser.nodeParentNode(current)) orelse break;
@@ -267,22 +254,6 @@ pub const NodeIterator = struct {
return null; return null;
} }
// Get the next sibling that is either acceptable or should be descended into (skip/reject)
fn nextSiblingOrSkipReject(self: *const NodeIterator, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
var current = node;
while (true) {
current = (parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return .{ .node = current, .should_descend = false },
.skip, .reject => return .{ .node = current, .should_descend = true },
}
}
return null;
}
fn callbackStart(self: *NodeIterator) !void { fn callbackStart(self: *NodeIterator) !void {
if (self.is_in_callback) { if (self.is_in_callback) {
// this is the correct DOMExeption // this is the correct DOMExeption

View File

@@ -19,10 +19,12 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.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 NodeUnion = @import("node.zig").Union;
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
@@ -146,10 +148,10 @@ pub const NodeList = struct {
// }; // };
// } // }
pub fn _forEach(self: *NodeList, cbk: js.Function) !void { // TODO handle thisArg pub fn _forEach(self: *NodeList, cbk: Function) !void { // TODO handle thisArg
for (self.nodes.items, 0..) |n, i| { for (self.nodes.items, 0..) |n, i| {
const ii: u32 = @intCast(i); const ii: u32 = @intCast(i);
var result: js.Function.Result = undefined; var result: Function.Result = undefined;
cbk.tryCall(void, .{ n, ii, self }, &result) catch { cbk.tryCall(void, .{ n, ii, self }, &result) catch {
log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack }); log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack });
}; };
@@ -173,7 +175,7 @@ pub const NodeList = struct {
} }
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries // TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
pub fn postAttach(self: *NodeList, js_this: js.This) !void { pub fn postAttach(self: *NodeList, js_this: JsThis) !void {
const len = self.get_length(); const len = self.get_length();
for (0..len) |i| { for (0..len) |i| {
const node = try self._item(@intCast(i)) orelse unreachable; const node = try self._item(@intCast(i)) orelse unreachable;

View File

@@ -18,9 +18,9 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const milliTimestamp = @import("../../datetime.zig").milliTimestamp; const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
@@ -61,7 +61,7 @@ pub const Performance = struct {
return milliTimestamp() - self.time_origin; return milliTimestamp() - self.time_origin;
} }
pub fn _mark(_: *Performance, name: js.String, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark { pub fn _mark(_: *Performance, name: Env.String, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
const mark: PerformanceMark = try .constructor(name, _options, page); const mark: PerformanceMark = try .constructor(name, _options, page);
// TODO: Should store this in an entries list // TODO: Should store this in an entries list
return mark; return mark;
@@ -148,14 +148,14 @@ pub const PerformanceMark = struct {
pub const prototype = *PerformanceEntry; pub const prototype = *PerformanceEntry;
proto: PerformanceEntry, proto: PerformanceEntry,
detail: ?js.Object, detail: ?Env.JsObject,
const Options = struct { const Options = struct {
detail: ?js.Object = null, detail: ?Env.JsObject = null,
startTime: ?f64 = null, startTime: ?f64 = null,
}; };
pub fn constructor(name: js.String, _options: ?Options, page: *Page) !PerformanceMark { pub fn constructor(name: Env.String, _options: ?Options, page: *Page) !PerformanceMark {
const perf = &page.window.performance; const perf = &page.window.performance;
const options = _options orelse Options{}; const options = _options orelse Options{};
@@ -171,7 +171,7 @@ pub const PerformanceMark = struct {
return .{ .proto = proto, .detail = detail }; return .{ .proto = proto, .detail = detail };
} }
pub fn get_detail(self: *const PerformanceMark) ?js.Object { pub fn get_detail(self: *const PerformanceMark) ?Env.JsObject {
return self.detail; return self.detail;
} }
}; };
@@ -195,7 +195,7 @@ test "Performance: now" {
} }
var after = perf._now(); var after = perf._now();
while (after <= now) { // Loop until after > now while (after <= now) { // Loop untill after > now
try testing.expectEqual(after, now); try testing.expectEqual(after, now);
after = perf._now(); after = perf._now();
} }

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const PerformanceEntry = @import("performance.zig").PerformanceEntry; const PerformanceEntry = @import("performance.zig").PerformanceEntry;
@@ -25,7 +25,7 @@ const PerformanceEntry = @import("performance.zig").PerformanceEntry;
pub const PerformanceObserver = struct { pub const PerformanceObserver = struct {
pub const _supportedEntryTypes = [0][]const u8{}; pub const _supportedEntryTypes = [0][]const u8{};
pub fn constructor(cbk: js.Function) PerformanceObserver { pub fn constructor(cbk: Env.Function) PerformanceObserver {
_ = cbk; _ = cbk;
return .{}; return .{};
} }

View File

@@ -92,7 +92,7 @@ pub const Range = struct {
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void { pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
try ensureValidOffset(node, offset_); try ensureValidOffset(node, offset_);
const offset: u32 = @intCast(offset_); const offset: u32 = @intCast(offset_);
const position = compare(node, offset, self.proto.end_node, self.proto.end_offset) catch |err| switch (err) { const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
error.WrongDocument => blk: { error.WrongDocument => blk: {
// allow a node with a different root than the current, or // allow a node with a different root than the current, or
// a disconnected one. Treat it as if it's "after", so that // a disconnected one. Treat it as if it's "after", so that
@@ -103,7 +103,7 @@ pub const Range = struct {
}; };
if (position == 1) { if (position == 1) {
// if we're setting the node after the current end, the end must // if we're setting the node after the current start, the end must
// be set too. // be set too.
self.proto.end_offset = offset; self.proto.end_offset = offset;
self.proto.end_node = node; self.proto.end_node = node;
@@ -378,7 +378,7 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
const child_parent, const child_index = try getParentAndIndex(child); const child_parent, const child_index = try getParentAndIndex(child);
std.debug.assert(node_a == child_parent); std.debug.assert(node_a == child_parent);
return if (offset_a <= child_index) -1 else 1; return if (child_index < offset_a) -1 else 1;
} }
return -1; return -1;

View File

@@ -16,7 +16,7 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
pub const Interfaces = .{ pub const Interfaces = .{
@@ -25,7 +25,7 @@ pub const Interfaces = .{
// WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface // WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface
pub const ResizeObserver = struct { pub const ResizeObserver = struct {
pub fn constructor(cbk: js.Function) ResizeObserver { pub fn constructor(cbk: Env.Function) ResizeObserver {
_ = cbk; _ = cbk;
return .{}; return .{};
} }

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const dump = @import("../dump.zig"); const dump = @import("../dump.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const js = @import(".././js/js.zig"); const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
const Element = @import("element.zig").Element; const Element = @import("element.zig").Element;
@@ -34,7 +34,7 @@ pub const ShadowRoot = struct {
mode: Mode, mode: Mode,
host: *parser.Element, host: *parser.Element,
proto: *parser.DocumentFragment, proto: *parser.DocumentFragment,
adopted_style_sheets: ?js.Object = null, adopted_style_sheets: ?Env.JsObject = null,
pub const Mode = enum { pub const Mode = enum {
open, open,
@@ -45,17 +45,17 @@ pub const ShadowRoot = struct {
return Element.toInterface(self.host); return Element.toInterface(self.host);
} }
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object { pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject {
if (self.adopted_style_sheets) |obj| { if (self.adopted_style_sheets) |obj| {
return obj; return obj;
} }
const obj = try page.js.createArray(0).persist(); const obj = try page.main_context.newArray(0).persist();
self.adopted_style_sheets = obj; self.adopted_style_sheets = obj;
return obj; return obj;
} }
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: js.Object) !void { pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
self.adopted_style_sheets = try sheets.persist(); self.adopted_style_sheets = try sheets.persist();
} }

View File

@@ -18,11 +18,12 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const iterator = @import("../iterator/iterator.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; const DOMException = @import("exceptions.zig").DOMException;
pub const Interfaces = .{ pub const Interfaces = .{
@@ -136,10 +137,10 @@ pub const DOMTokenList = struct {
} }
// TODO handle thisArg // TODO handle thisArg
pub fn _forEach(self: *parser.TokenList, cbk: js.Function, this_arg: js.Object) !void { pub fn _forEach(self: *parser.TokenList, cbk: Function, this_arg: JsObject) !void {
var entries = _entries(self); var entries = _entries(self);
while (try entries._next()) |entry| { while (try entries._next()) |entry| {
var result: js.Function.Result = undefined; var result: Function.Result = undefined;
cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch { cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
log.debug(.user_script, "callback error", .{ log.debug(.user_script, "callback error", .{
.err = result.exception, .err = result.exception,

View File

@@ -17,10 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const NodeFilter = @import("node_filter.zig"); const NodeFilter = @import("node_filter.zig");
const Env = @import("../env.zig").Env;
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
const NodeUnion = @import("node.zig").Union; const NodeUnion = @import("node.zig").Union;
@@ -30,20 +30,20 @@ pub const TreeWalker = struct {
current_node: *parser.Node, current_node: *parser.Node,
what_to_show: u32, what_to_show: u32,
filter: ?TreeWalkerOpts, filter: ?TreeWalkerOpts,
filter_func: ?js.Function, filter_func: ?Env.Function,
// One of the few cases where null and undefined resolve to different default. // 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: // We need the raw JsObject so that we can probe the tri state:
// null, undefined or i32. // null, undefined or i32.
pub const WhatToShow = js.Object; pub const WhatToShow = Env.JsObject;
pub const TreeWalkerOpts = union(enum) { pub const TreeWalkerOpts = union(enum) {
function: js.Function, function: Env.Function,
object: struct { acceptNode: js.Function }, object: struct { acceptNode: Env.Function },
}; };
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker { pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker {
var filter_func: ?js.Function = null; var filter_func: ?Env.Function = null;
if (filter) |f| { if (filter) |f| {
filter_func = switch (f) { filter_func = switch (f) {
@@ -144,23 +144,6 @@ pub const TreeWalker = struct {
return null; return null;
} }
// Get the next sibling that is either acceptable or should be descended into (skip)
fn nextSiblingOrSkip(self: *const TreeWalker, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
var current = node;
while (true) {
current = (parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return .{ .node = current, .should_descend = false },
.skip => return .{ .node = current, .should_descend = true },
.reject => continue,
}
}
return null;
}
fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node { fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
var current = node; var current = node;
@@ -210,36 +193,19 @@ pub const TreeWalker = struct {
} }
pub fn _nextNode(self: *TreeWalker) !?NodeUnion { pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
var current = self.current_node; if (try self.firstChild(self.current_node)) |child| {
// First, try to go to first child of current node
if (try self.firstChild(current)) |child| {
self.current_node = child; self.current_node = child;
return try Node.toInterface(child); return try Node.toInterface(child);
} }
// No acceptable children, move to next node in tree var current = self.current_node;
while (current != self.root) { while (current != self.root) {
const result = try self.nextSiblingOrSkip(current) orelse { if (try self.nextSibling(current)) |sibling| {
// No next sibling, go up to parent and continue self.current_node = sibling;
// or, if there is no parent, we're done return try Node.toInterface(sibling);
current = (parser.nodeParentNode(current)) orelse break;
continue;
};
if (!result.should_descend) {
// This is an .accept node - return it
self.current_node = result.node;
return try Node.toInterface(result.node);
} }
// This is a .skip node - try to find acceptable children within it current = (parser.nodeParentNode(current)) orelse break;
if (try self.firstChild(result.node)) |child| {
self.current_node = child;
return try Node.toInterface(child);
}
// No acceptable children, continue looking at this node's siblings
current = result.node;
} }
return null; return null;

View File

@@ -236,10 +236,10 @@ fn isVoid(elem: *parser.Element) !bool {
}; };
} }
fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void { fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
var v = value; var v = value;
while (v.len > 0) { while (v.len > 0) {
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', 194 }) orelse { const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
return writer.writeAll(v); return writer.writeAll(v);
}; };
try writer.writeAll(v[0..index]); try writer.writeAll(v[0..index]);
@@ -247,22 +247,13 @@ fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void {
'&' => try writer.writeAll("&amp;"), '&' => try writer.writeAll("&amp;"),
'<' => try writer.writeAll("&lt;"), '<' => try writer.writeAll("&lt;"),
'>' => try writer.writeAll("&gt;"), '>' => try writer.writeAll("&gt;"),
194 => {
// non breaking space
if (v.len > index + 1 and v[index + 1] == 160) {
try writer.writeAll("&nbsp;");
v = v[index + 2 ..];
continue;
}
try writer.writeByte(194);
},
else => unreachable, else => unreachable,
} }
v = v[index + 1 ..]; v = v[index + 1 ..];
} }
} }
fn writeEscapedAttributeValue(writer: *std.Io.Writer, value: []const u8) !void { fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
var v = value; var v = value;
while (v.len > 0) { while (v.len > 0) {
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse { const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {

View File

@@ -69,8 +69,8 @@ pub fn get_fatal(self: *const TextDecoder) bool {
const DecodeOptions = struct { const DecodeOptions = struct {
stream: bool = false, stream: bool = false,
}; };
pub fn _decode(self: *TextDecoder, str_: ?[]const u8, opts_: ?DecodeOptions, page: *Page) ![]const u8 { pub fn _decode(self: *TextDecoder, input_: ?[]const u8, opts_: ?DecodeOptions, page: *Page) ![]const u8 {
var str = str_ orelse return ""; var str = input_ orelse return "";
const opts: DecodeOptions = opts_ orelse .{}; const opts: DecodeOptions = opts_ orelse .{};
if (self.stream.items.len > 0) { if (self.stream.items.len > 0) {

View File

@@ -18,7 +18,7 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
// https://encoding.spec.whatwg.org/#interface-textencoder // https://encoding.spec.whatwg.org/#interface-textencoder
const TextEncoder = @This(); const TextEncoder = @This();
@@ -31,7 +31,7 @@ pub fn get_encoding(_: *const TextEncoder) []const u8 {
return "utf-8"; return "utf-8";
} }
pub fn _encode(_: *const TextEncoder, v: []const u8) !js.TypedArray(u8) { pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
// Ensure the input is a valid utf-8 // Ensure the input is a valid utf-8
// It seems chrome accepts invalid utf-8 sequence. // It seems chrome accepts invalid utf-8 sequence.
// //

51
src/browser/env.zig Normal file
View File

@@ -0,0 +1,51 @@
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;

View File

@@ -1,57 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
pub const CompositionEvent = struct {
data: []const u8,
proto: parser.Event,
pub const union_make_copy = true;
pub const prototype = *parser.Event;
pub const ConstructorOptions = struct {
data: []const u8 = "",
};
pub fn constructor(event_type: []const u8, options_: ?ConstructorOptions) !CompositionEvent {
const options: ConstructorOptions = options_ orelse .{};
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .composition_event);
return .{
.proto = event.*,
.data = options.data,
};
}
pub fn get_data(self: *const CompositionEvent) []const u8 {
return self.data;
}
};
const testing = @import("../../testing.zig");
test "Browser: Events.Composition" {
try testing.htmlRunner("events/composition.html");
}

View File

@@ -16,10 +16,9 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Event = @import("event.zig").Event; const Event = @import("event.zig").Event;
const JsObject = @import("../env.zig").JsObject;
const netsurf = @import("../netsurf.zig"); const netsurf = @import("../netsurf.zig");
// https://dom.spec.whatwg.org/#interface-customevent // https://dom.spec.whatwg.org/#interface-customevent
@@ -28,13 +27,13 @@ pub const CustomEvent = struct {
pub const union_make_copy = true; pub const union_make_copy = true;
proto: parser.Event, proto: parser.Event,
detail: ?js.Object, detail: ?JsObject,
const CustomEventInit = struct { const CustomEventInit = struct {
bubbles: bool = false, bubbles: bool = false,
cancelable: bool = false, cancelable: bool = false,
composed: bool = false, composed: bool = false,
detail: ?js.Object = null, detail: ?JsObject = null,
}; };
pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent { pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent {
@@ -54,7 +53,7 @@ pub const CustomEvent = struct {
}; };
} }
pub fn get_detail(self: *CustomEvent) ?js.Object { pub fn get_detail(self: *CustomEvent) ?JsObject {
return self.detail; return self.detail;
} }
@@ -65,7 +64,7 @@ pub const CustomEvent = struct {
event_type: []const u8, event_type: []const u8,
can_bubble: bool, can_bubble: bool,
cancelable: bool, cancelable: bool,
maybe_detail: ?js.Object, maybe_detail: ?JsObject,
) !void { ) !void {
// This function can only be called after the constructor has called. // This function can only be called after the constructor has called.
// So we assume proto is initialized already by constructor. // So we assume proto is initialized already by constructor.

View File

@@ -21,7 +21,7 @@ const Allocator = std.mem.Allocator;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const generate = @import("../js/generate.zig"); const generate = @import("../../runtime/generate.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Node = @import("../dom/node.zig").Node; const Node = @import("../dom/node.zig").Node;
@@ -36,8 +36,6 @@ const MouseEvent = @import("mouse_event.zig").MouseEvent;
const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent; const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent; const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent; const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
// Event interfaces // Event interfaces
pub const Interfaces = .{ pub const Interfaces = .{
@@ -48,8 +46,6 @@ pub const Interfaces = .{
KeyboardEvent, KeyboardEvent,
ErrorEvent, ErrorEvent,
MessageEvent, MessageEvent,
PopStateEvent,
CompositionEvent,
}; };
pub const Union = generate.Union(Interfaces); pub const Union = generate.Union(Interfaces);
@@ -74,11 +70,9 @@ pub const Event = struct {
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* }, .custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* }, .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) }, .mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
.error_event => .{ .ErrorEvent = (@as(*ErrorEvent, @fieldParentPtr("proto", evt))).* }, .error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* }, .message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) }, .keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
.composition_event => .{ .CompositionEvent = (@as(*CompositionEvent, @fieldParentPtr("proto", evt))).* },
}; };
} }
@@ -222,17 +216,18 @@ pub const Event = struct {
pub const EventHandler = struct { pub const EventHandler = struct {
once: bool, once: bool,
capture: bool, capture: bool,
callback: js.Function, callback: Function,
node: parser.EventNode, node: parser.EventNode,
listener: *parser.EventListener, listener: *parser.EventListener,
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const Function = Env.Function;
pub const Listener = union(enum) { pub const Listener = union(enum) {
function: js.Function, function: Function,
object: js.Object, object: Env.JsObject,
pub fn callback(self: Listener, target: *parser.EventTarget) !?js.Function { pub fn callback(self: Listener, target: *parser.EventTarget) !?Function {
return switch (self) { return switch (self) {
.function => |func| try func.withThis(target), .function => |func| try func.withThis(target),
.object => |obj| blk: { .object => |obj| blk: {
@@ -333,7 +328,7 @@ pub const EventHandler = struct {
fn handle(node: *parser.EventNode, event: *parser.Event) void { fn handle(node: *parser.EventNode, event: *parser.Event) void {
const ievent = Event.toInterface(event); const ievent = Event.toInterface(event);
const self: *EventHandler = @fieldParentPtr("node", node); const self: *EventHandler = @fieldParentPtr("node", node);
var result: js.Function.Result = undefined; var result: Function.Result = undefined;
self.callback.tryCall(void, .{ievent}, &result) catch { self.callback.tryCall(void, .{ievent}, &result) catch {
log.debug(.user_script, "callback error", .{ log.debug(.user_script, "callback error", .{
.err = result.exception, .err = result.exception,

View File

@@ -53,8 +53,8 @@ pub const KeyboardEvent = struct {
pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*parser.KeyboardEvent { pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*parser.KeyboardEvent {
const options: ConstructorOptions = maybe_options orelse .{}; const options: ConstructorOptions = maybe_options orelse .{};
const event = try parser.keyboardEventCreate(); var event = try parser.keyboardEventCreate();
parser.eventSetInternalType(@ptrCast(event), .keyboard_event); parser.eventSetInternalType(@ptrCast(&event), .keyboard_event);
try parser.keyboardEventInit( try parser.keyboardEventInit(
event, event,

View File

@@ -54,8 +54,8 @@ pub const MouseEvent = struct {
pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent { pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent {
const opts = opts_ orelse MouseEventInit{}; const opts = opts_ orelse MouseEventInit{};
const mouse_event = try parser.mouseEventCreate(); var mouse_event = try parser.mouseEventCreate();
parser.eventSetInternalType(@ptrCast(mouse_event), .mouse_event); parser.eventSetInternalType(@ptrCast(&mouse_event), .mouse_event);
try parser.mouseEventInit(mouse_event, event_type, .{ try parser.mouseEventInit(mouse_event, event_type, .{
.x = opts.clientX, .x = opts.clientX,
@@ -68,7 +68,7 @@ pub const MouseEvent = struct {
}); });
if (!std.mem.eql(u8, event_type, "click")) { if (!std.mem.eql(u8, event_type, "click")) {
log.warn(.browser, "unsupported mouse event", .{ .event = event_type }); log.warn(.mouse_event, "unsupported mouse event", .{ .event = event_type });
} }
return mouse_event; return mouse_event;

View File

@@ -17,13 +17,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const URL = @import("../../url.zig").URL; const URL = @import("../../url.zig").URL;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const iterator = @import("../iterator/iterator.zig"); 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 // https://developer.mozilla.org/en-US/docs/Web/API/Headers
const Headers = @This(); const Headers = @This();
@@ -67,7 +69,7 @@ pub const HeadersInit = union(enum) {
// Headers // Headers
headers: *Headers, headers: *Headers,
// Mappings // Mappings
object: js.Object, object: Env.JsObject,
}; };
pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers { pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
@@ -157,7 +159,7 @@ pub fn _entries(self: *const Headers) HeadersEntryIterable {
}; };
} }
pub fn _forEach(self: *Headers, callback_fn: js.Function, this_arg: ?js.Object) !void { pub fn _forEach(self: *Headers, callback_fn: Env.Function, this_arg: ?Env.JsObject) !void {
var iter = self.headers.iterator(); var iter = self.headers.iterator();
const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn; const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn;

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const URL = @import("../../url.zig").URL; const URL = @import("../../url.zig").URL;
@@ -27,6 +26,9 @@ const Response = @import("./Response.zig");
const Http = @import("../../http/Http.zig"); const Http = @import("../../http/Http.zig");
const ReadableStream = @import("../streams/ReadableStream.zig"); const ReadableStream = @import("../streams/ReadableStream.zig");
const v8 = @import("v8");
const Env = @import("../env.zig").Env;
const Headers = @import("Headers.zig"); const Headers = @import("Headers.zig");
const HeadersInit = @import("Headers.zig").HeadersInit; const HeadersInit = @import("Headers.zig").HeadersInit;
@@ -179,7 +181,7 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re
pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream { pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream {
if (self.body) |body| { if (self.body) |body| {
const stream = try ReadableStream.constructor(null, null, page); const stream = try ReadableStream.constructor(null, null, page);
try stream.queue.append(page.arena, .{ .string = body }); try stream.queue.append(page.arena, body);
return stream; return stream;
} else return null; } else return null;
} }
@@ -239,19 +241,24 @@ pub fn _clone(self: *Request) !Request {
}; };
} }
pub fn _bytes(self: *Response, page: *Page) !js.Promise { pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) { if (self.body_used) {
return error.TypeError; return error.TypeError;
} }
const resolver = page.main_context.createPromiseResolver();
try resolver.resolve(self.body);
self.body_used = true; self.body_used = true;
return page.js.resolvePromise(self.body); return resolver.promise();
} }
pub fn _json(self: *Response, page: *Page) !js.Promise { pub fn _json(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) { if (self.body_used) {
return error.TypeError; return error.TypeError;
} }
self.body_used = true;
const resolver = page.main_context.createPromiseResolver();
if (self.body) |body| { if (self.body) |body| {
const p = std.json.parseFromSliceLeaky( const p = std.json.parseFromSliceLeaky(
@@ -264,17 +271,25 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
return error.SyntaxError; return error.SyntaxError;
}; };
return page.js.resolvePromise(p); try resolver.resolve(p);
} else {
try resolver.resolve(null);
} }
return page.js.resolvePromise(null);
self.body_used = true;
return resolver.promise();
} }
pub fn _text(self: *Response, page: *Page) !js.Promise { pub fn _text(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) { if (self.body_used) {
return error.TypeError; return error.TypeError;
} }
const resolver = page.main_context.createPromiseResolver();
try resolver.resolve(self.body);
self.body_used = true; self.body_used = true;
return page.js.resolvePromise(self.body); return resolver.promise();
} }
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

@@ -17,9 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const v8 = @import("v8");
const HttpClient = @import("../../http/Client.zig"); const HttpClient = @import("../../http/Client.zig");
const Http = @import("../../http/Http.zig"); const Http = @import("../../http/Http.zig");
const URL = @import("../../url.zig").URL; const URL = @import("../../url.zig").URL;
@@ -28,6 +29,7 @@ const ReadableStream = @import("../streams/ReadableStream.zig");
const Headers = @import("Headers.zig"); const Headers = @import("Headers.zig");
const HeadersInit = @import("Headers.zig").HeadersInit; const HeadersInit = @import("Headers.zig").HeadersInit;
const Env = @import("../env.zig").Env;
const Mime = @import("../mime.zig").Mime; const Mime = @import("../mime.zig").Mime;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
@@ -107,7 +109,7 @@ pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Pag
pub fn get_body(self: *const Response, page: *Page) !*ReadableStream { pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
const stream = try ReadableStream.constructor(null, null, page); const stream = try ReadableStream.constructor(null, null, page);
if (self.body) |body| { if (self.body) |body| {
try stream.queue.append(page.arena, .{ .string = body }); try stream.queue.append(page.arena, body);
} }
return stream; return stream;
} }
@@ -163,22 +165,29 @@ pub fn _clone(self: *const Response) !Response {
}; };
} }
pub fn _bytes(self: *Response, page: *Page) !js.Promise { pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) { if (self.body_used) {
return error.TypeError; 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; self.body_used = true;
return page.js.resolvePromise(self.body); return resolver.promise();
} }
pub fn _json(self: *Response, page: *Page) !js.Promise { pub fn _json(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) { if (self.body_used) {
return error.TypeError; return error.TypeError;
} }
const resolver = page.main_context.createPromiseResolver();
if (self.body) |body| { if (self.body) |body| {
self.body_used = true;
const p = std.json.parseFromSliceLeaky( const p = std.json.parseFromSliceLeaky(
std.json.Value, std.json.Value,
page.call_arena, page.call_arena,
@@ -189,18 +198,25 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
return error.SyntaxError; return error.SyntaxError;
}; };
return page.js.resolvePromise(p); try resolver.resolve(p);
} else {
try resolver.resolve(null);
} }
return page.js.resolvePromise(null);
self.body_used = true;
return resolver.promise();
} }
pub fn _text(self: *Response, page: *Page) !js.Promise { pub fn _text(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) { if (self.body_used) {
return error.TypeError; return error.TypeError;
} }
self.body_used = true;
return page.js.resolvePromise(self.body); const resolver = page.main_context.createPromiseResolver();
try resolver.resolve(self.body);
self.body_used = true;
return resolver.promise();
} }
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

@@ -19,7 +19,7 @@
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Http = @import("../../http/Http.zig"); const Http = @import("../../http/Http.zig");
@@ -43,9 +43,9 @@ pub const Interfaces = .{
}; };
pub const FetchContext = struct { pub const FetchContext = struct {
page: *Page,
arena: std.mem.Allocator, arena: std.mem.Allocator,
promise_resolver: js.PersistentPromiseResolver, js_ctx: *Env.JsContext,
promise_resolver: Env.PersistentPromiseResolver,
method: Http.Method, method: Http.Method,
url: []const u8, url: []const u8,
@@ -63,12 +63,9 @@ pub const FetchContext = struct {
pub fn toResponse(self: *const FetchContext) !Response { pub fn toResponse(self: *const FetchContext) !Response {
var headers: Headers = .{}; 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. // 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 // https://developer.mozilla.org/en-US/docs/Web/API/Response/type
if (!same_origin and self.mode == .@"no-cors") { if (self.mode == .@"no-cors") {
return Response{ return Response{
.status = 0, .status = 0,
.headers = headers, .headers = headers,
@@ -88,7 +85,7 @@ pub const FetchContext = struct {
} }
const resp_type: Response.ResponseType = blk: { const resp_type: Response.ResponseType = blk: {
if (same_origin or std.mem.startsWith(u8, self.url, "data:")) { if (std.mem.startsWith(u8, self.url, "data:")) {
break :blk .basic; break :blk .basic;
} }
@@ -111,7 +108,7 @@ pub const FetchContext = struct {
}; };
// https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch // https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !js.Promise { pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promise {
const arena = page.arena; const arena = page.arena;
const req = try Request.constructor(input, options, page); const req = try Request.constructor(input, options, page);
@@ -131,12 +128,12 @@ pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !js.Promis
try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers); try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers);
const resolver = try page.js.createPromiseResolver(.page); const resolver = try page.main_context.createPersistentPromiseResolver(.page);
const fetch_ctx = try arena.create(FetchContext); const fetch_ctx = try arena.create(FetchContext);
fetch_ctx.* = .{ fetch_ctx.* = .{
.page = page,
.arena = arena, .arena = arena,
.js_ctx = page.main_context,
.promise_resolver = resolver, .promise_resolver = resolver,
.method = req.method, .method = req.method,
.url = req.url, .url = req.url,

View File

@@ -17,9 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
@@ -113,12 +113,12 @@ pub const AbortSignal = struct {
} }
const ThrowIfAborted = union(enum) { const ThrowIfAborted = union(enum) {
exception: js.Exception, exception: Env.Exception,
undefined: void, undefined: void,
}; };
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted { pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
if (self.aborted) { if (self.aborted) {
const ex = page.js.throw(self.reason orelse DEFAULT_REASON); const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON);
return .{ .exception = ex }; return .{ .exception = ex };
} }
return .{ .undefined = {} }; return .{ .undefined = {} };

View File

@@ -17,8 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -27,7 +26,7 @@ const DataSet = @This();
element: *parser.Element, element: *parser.Element,
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !js.UndefinedOr([]const u8) { pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !Env.UndefinedOr([]const u8) {
const normalized_name = try normalize(page.call_arena, name); const normalized_name = try normalize(page.call_arena, name);
if (try parser.elementGetAttribute(self.element, normalized_name)) |value| { if (try parser.elementGetAttribute(self.element, normalized_name)) |value| {
return .{ .value = value }; return .{ .value = value };

View File

@@ -1,215 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const 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, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
const json = try state.toJson(arena);
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
const entry = HistoryEntry{ .state = json, .url = url };
try self.stack.append(arena, entry);
self.current = self.stack.items.len - 1;
}
pub fn _replaceState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
if (self.current) |curr| {
const entry = &self.stack.items[curr];
const json = try state.toJson(arena);
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
entry.* = HistoryEntry{ .state = json, .url = url };
} else {
try self._pushState(state, "", _url, page);
}
}
pub fn go(self: *History, delta: i32, page: *Page) !void {
// 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");
}

View File

@@ -18,9 +18,9 @@
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const generate = @import("../js/generate.zig"); const generate = @import("../../runtime/generate.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const urlStitch = @import("../../url.zig").URL.stitch; const urlStitch = @import("../../url.zig").URL.stitch;
@@ -281,7 +281,7 @@ pub const HTMLAnchorElement = struct {
// TODO return a disposable string // TODO return a disposable string
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = try url(self, page);
return u.get_protocol(); return u.get_protocol(page);
} }
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
@@ -732,9 +732,6 @@ pub const HTMLInputElement = struct {
pub fn set_value(self: *parser.Input, value: []const u8) !void { pub fn set_value(self: *parser.Input, value: []const u8) !void {
try parser.inputSetValue(self, value); try parser.inputSetValue(self, value);
} }
pub fn _select(_: *parser.Input) void {
log.debug(.web_api, "not implemented", .{ .feature = "HTMLInputElement select" });
}
}; };
pub const HTMLLIElement = struct { pub const HTMLLIElement = struct {
@@ -890,7 +887,7 @@ pub const HTMLScriptElement = struct {
// s.src = '...'; // s.src = '...';
// This should load the script. // This should load the script.
// addFromElement protects against double execution. // addFromElement protects against double execution.
try page.script_manager.addFromElement(@ptrCast(@alignCast(self)), "dynamic"); try page.script_manager.addFromElement(@ptrCast(@alignCast(self)));
} }
} }
@@ -1003,22 +1000,22 @@ pub const HTMLScriptElement = struct {
); );
} }
pub fn get_onload(self: *parser.Script, page: *Page) !?js.Function { pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function {
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null; const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
return state.onload; return state.onload;
} }
pub fn set_onload(self: *parser.Script, function: ?js.Function, page: *Page) !void { pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.onload = function; state.onload = function;
} }
pub fn get_onerror(self: *parser.Script, page: *Page) !?js.Function { pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function {
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null; const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
return state.onerror; return state.onerror;
} }
pub fn set_onerror(self: *parser.Script, function: ?js.Function, page: *Page) !void { pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.onerror = function; state.onerror = function;
} }
@@ -1354,9 +1351,6 @@ test "Browser: HTML.HtmlStyleElement" {
test "Browser: HTML.HtmlScriptElement" { test "Browser: HTML.HtmlScriptElement" {
try testing.htmlRunner("html/script/script.html"); try testing.htmlRunner("html/script/script.html");
try testing.htmlRunner("html/script/inline_defer.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" { test "Browser: HTML.HtmlSlotElement" {

View File

@@ -15,7 +15,7 @@
// //
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent // https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
@@ -28,21 +28,21 @@ pub const ErrorEvent = struct {
filename: []const u8, filename: []const u8,
lineno: i32, lineno: i32,
colno: i32, colno: i32,
@"error": ?js.Object, @"error": ?Env.JsObject,
const ErrorEventInit = struct { const ErrorEventInit = struct {
message: []const u8 = "", message: []const u8 = "",
filename: []const u8 = "", filename: []const u8 = "",
lineno: i32 = 0, lineno: i32 = 0,
colno: i32 = 0, colno: i32 = 0,
@"error": ?js.Object = null, @"error": ?Env.JsObject = null,
}; };
pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent { pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent {
const event = try parser.eventCreate(); const event = try parser.eventCreate();
defer parser.eventDestroy(event); defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{}); try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .error_event); parser.eventSetInternalType(event, .event);
const o = opts orelse ErrorEventInit{}; const o = opts orelse ErrorEventInit{};
@@ -72,7 +72,7 @@ pub const ErrorEvent = struct {
return self.colno; return self.colno;
} }
pub fn get_error(self: *const ErrorEvent) js.UndefinedOr(js.Object) { pub fn get_error(self: *const ErrorEvent) Env.UndefinedOr(Env.JsObject) {
if (self.@"error") |e| { if (self.@"error") |e| {
return .{ .value = e }; return .{ .value = e };
} }

View File

@@ -0,0 +1,93 @@
// 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");
}

View File

@@ -21,7 +21,7 @@ const HTMLElem = @import("elements.zig");
const SVGElem = @import("svg_elements.zig"); const SVGElem = @import("svg_elements.zig");
const Window = @import("window.zig").Window; const Window = @import("window.zig").Window;
const Navigator = @import("navigator.zig").Navigator; const Navigator = @import("navigator.zig").Navigator;
const History = @import("History.zig"); const History = @import("history.zig").History;
const Location = @import("location.zig").Location; const Location = @import("location.zig").Location;
const MediaQueryList = @import("media_query_list.zig").MediaQueryList; const MediaQueryList = @import("media_query_list.zig").MediaQueryList;

View File

@@ -16,61 +16,57 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const Uri = @import("std").Uri;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const URL = @import("../url/url.zig").URL; const URL = @import("../url/url.zig").URL;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface
pub const Location = struct { pub const Location = struct {
url: URL, url: ?URL = null,
/// Browsers give such initial values when user not navigated yet:
/// Chrome -> chrome://new-tab-page/
/// Firefox -> about:newtab
/// Safari -> favorites://
pub const default = Location{
.url = .initWithoutSearchParams(Uri.parse("about:blank") catch unreachable),
};
pub fn get_href(self: *Location, page: *Page) ![]const u8 { pub fn get_href(self: *Location, page: *Page) ![]const u8 {
return self.url.get_href(page); if (self.url) |*u| return u.get_href(page);
return "";
} }
pub fn set_href(_: *const Location, href: []const u8, page: *Page) !void { pub fn get_protocol(self: *Location, page: *Page) ![]const u8 {
return page.navigateFromWebAPI(href, .{ .reason = .script }); if (self.url) |*u| return u.get_protocol(page);
} return "";
pub fn get_protocol(self: *Location) []const u8 {
return self.url.get_protocol();
} }
pub fn get_host(self: *Location, page: *Page) ![]const u8 { pub fn get_host(self: *Location, page: *Page) ![]const u8 {
return self.url.get_host(page); if (self.url) |*u| return u.get_host(page);
return "";
} }
pub fn get_hostname(self: *Location) []const u8 { pub fn get_hostname(self: *Location) []const u8 {
return self.url.get_hostname(); if (self.url) |*u| return u.get_hostname();
return "";
} }
pub fn get_port(self: *Location, page: *Page) ![]const u8 { pub fn get_port(self: *Location, page: *Page) ![]const u8 {
return self.url.get_port(page); if (self.url) |*u| return u.get_port(page);
return "";
} }
pub fn get_pathname(self: *Location) []const u8 { pub fn get_pathname(self: *Location) []const u8 {
return self.url.get_pathname(); if (self.url) |*u| return u.get_pathname();
return "";
} }
pub fn get_search(self: *Location, page: *Page) ![]const u8 { pub fn get_search(self: *Location, page: *Page) ![]const u8 {
return self.url.get_search(page); if (self.url) |*u| return u.get_search(page);
return "";
} }
pub fn get_hash(self: *Location, page: *Page) ![]const u8 { pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
return self.url.get_hash(page); if (self.url) |*u| return u.get_hash(page);
return "";
} }
pub fn get_origin(self: *Location, page: *Page) ![]const u8 { pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
return self.url.get_origin(page); if (self.url) |*u| return u.get_origin(page);
return "";
} }
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void { pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {

View File

@@ -16,8 +16,8 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Function = @import("../env.zig").Function;
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
// https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface // https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface
@@ -39,7 +39,7 @@ pub const MediaQueryList = struct {
return self.media; return self.media;
} }
pub fn _addListener(_: *const MediaQueryList, _: js.Function) void {} pub fn _addListener(_: *const MediaQueryList, _: Function) void {}
pub fn _removeListener(_: *const MediaQueryList, _: js.Function) void {} pub fn _removeListener(_: *const MediaQueryList, _: Function) void {}
}; };

View File

@@ -25,7 +25,7 @@ pub const SVGElement = struct {
// Currently the prototype chain is not implemented (will not be returned by toInterface()) // Currently the prototype chain is not implemented (will not be returned by toInterface())
// For that we need parser.SvgElement and the derived types with tags in the v-table. // For that we need parser.SvgElement and the derived types with tags in the v-table.
pub const prototype = *Element; pub const prototype = *Element;
// While this is a Node, could consider not exposing the subtype until we have // While this is a Node, could consider not exposing the subtype untill we have
// a Self type to cast to. // a Self type to cast to.
pub const subtype = .node; pub const subtype = .node;
}; };

View File

@@ -18,13 +18,13 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Navigator = @import("navigator.zig").Navigator; const Navigator = @import("navigator.zig").Navigator;
const History = @import("History.zig"); const History = @import("history.zig").History;
const Location = @import("location.zig").Location; const Location = @import("location.zig").Location;
const Crypto = @import("../crypto/crypto.zig").Crypto; const Crypto = @import("../crypto/crypto.zig").Crypto;
const Console = @import("../console/console.zig").Console; const Console = @import("../console/console.zig").Console;
@@ -35,13 +35,14 @@ const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
const Screen = @import("screen.zig").Screen; const Screen = @import("screen.zig").Screen;
const domcss = @import("../dom/css.zig"); const domcss = @import("../dom/css.zig");
const Css = @import("../css/css.zig").Css; const Css = @import("../css/css.zig").Css;
const EventHandler = @import("../events/event.zig").EventHandler;
const Function = Env.Function;
const v8 = @import("v8");
const Request = @import("../fetch/Request.zig"); const Request = @import("../fetch/Request.zig");
const fetchFn = @import("../fetch/fetch.zig").fetch; const fetchFn = @import("../fetch/fetch.zig").fetch;
const storage = @import("../storage/storage.zig"); const storage = @import("../storage/storage.zig");
const ErrorEvent = @import("error_event.zig").ErrorEvent;
// https://dom.spec.whatwg.org/#interface-window-extensions // https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window // https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
@@ -53,7 +54,8 @@ pub const Window = struct {
document: *parser.DocumentHTML, document: *parser.DocumentHTML,
target: []const u8 = "", target: []const u8 = "",
location: Location = .default, history: History = .{},
location: Location = .{},
storage_shelf: ?*storage.Shelf = null, storage_shelf: ?*storage.Shelf = null,
// counter for having unique timer ids // counter for having unique timer ids
@@ -66,9 +68,6 @@ pub const Window = struct {
performance: Performance, performance: Performance,
screen: Screen = .{}, screen: Screen = .{},
css: Css = .{}, css: Css = .{},
scroll_x: u32 = 0,
scroll_y: u32 = 0,
onload_callback: ?js.Function = null,
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window { pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
var fbs = std.io.fixedBufferStream(""); var fbs = std.io.fixedBufferStream("");
@@ -99,42 +98,16 @@ pub const Window = struct {
self.storage_shelf = shelf; self.storage_shelf = shelf;
} }
pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !js.Promise { pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !Env.Promise {
return fetchFn(input, options, page); return fetchFn(input, options, page);
} }
/// Returns `onload_callback`. pub fn get_window(self: *Window) *Window {
pub fn get_onload(self: *const Window) ?js.Function { return self;
return self.onload_callback;
} }
/// Sets `onload_callback`. pub fn get_navigator(self: *Window) *Navigator {
pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void { return &self.navigator;
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 { pub fn get_location(self: *Window) *Location {
@@ -145,6 +118,22 @@ pub const Window = struct {
return page.navigateFromWebAPI(url, .{ .reason = .script }); 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 // frames return the window itself, but accessing it via a pseudo
// array returns the Window object corresponding to the given frame or // array returns the Window object corresponding to the given frame or
// iframe. // iframe.
@@ -182,12 +171,16 @@ pub const Window = struct {
return frames.get_length(); return frames.get_length();
} }
pub fn get_top(self: *Window) *Window {
return self;
}
pub fn get_document(self: *Window) ?*parser.DocumentHTML { pub fn get_document(self: *Window) ?*parser.DocumentHTML {
return self.document; return self.document;
} }
pub fn get_history(_: *Window, page: *Page) *History { pub fn get_history(self: *Window) *History {
return &page.session.history; return &self.history;
} }
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present. // The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
@@ -216,11 +209,19 @@ pub const Window = struct {
return &self.storage_shelf.?.bucket.session; 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 { pub fn get_CSS(self: *Window) *Css {
return &self.css; return &self.css;
} }
pub fn _requestAnimationFrame(self: *Window, cbk: js.Function, page: *Page) !u32 { pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
return self.createTimeout(cbk, 5, page, .{ return self.createTimeout(cbk, 5, page, .{
.animation_frame = true, .animation_frame = true,
.name = "animationFrame", .name = "animationFrame",
@@ -232,11 +233,11 @@ pub const Window = struct {
_ = self.timers.remove(id); _ = self.timers.remove(id);
} }
pub fn _setTimeout(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 { pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" }); return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
} }
pub fn _setInterval(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 { pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" }); return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
} }
@@ -248,11 +249,11 @@ pub const Window = struct {
_ = self.timers.remove(id); _ = self.timers.remove(id);
} }
pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !u32 { pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" }); return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
} }
pub fn _setImmediate(self: *Window, cbk: js.Function, page: *Page) !u32 { pub fn _setImmediate(self: *Window, cbk: Function, page: *Page) !u32 {
return self.createTimeout(cbk, 0, page, .{ .name = "setImmediate" }); return self.createTimeout(cbk, 0, page, .{ .name = "setImmediate" });
} }
@@ -260,7 +261,7 @@ pub const Window = struct {
_ = self.timers.remove(id); _ = self.timers.remove(id);
} }
pub fn _matchMedia(_: *const Window, media: js.String) !MediaQueryList { pub fn _matchMedia(_: *const Window, media: Env.String) !MediaQueryList {
return .{ return .{
.matches = false, // TODO? .matches = false, // TODO?
.media = media.string, .media = media.string,
@@ -282,33 +283,14 @@ pub const Window = struct {
return out; return out;
} }
pub fn _reportError(self: *Window, err: js.Object, page: *Page) !void {
var error_event = try ErrorEvent.constructor("error", .{
.@"error" = err,
});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, self),
@as(*parser.Event, &error_event.proto),
);
if (parser.eventDefaultPrevented(&error_event.proto) == false) {
const err_string = err.toString() catch "Unknown error";
log.info(.user_script, "error", .{
.err = err_string,
.stack = page.stackTrace() catch "???",
.source = "window.reportError",
});
}
}
const CreateTimeoutOpts = struct { const CreateTimeoutOpts = struct {
name: []const u8, name: []const u8,
args: []js.Object = &.{}, args: []Env.JsObject = &.{},
repeat: bool = false, repeat: bool = false,
animation_frame: bool = false, animation_frame: bool = false,
low_priority: bool = false, low_priority: bool = false,
}; };
fn createTimeout(self: *Window, cbk: js.Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 { fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
const delay = delay_ orelse 0; const delay = delay_ orelse 0;
if (self.timers.count() > 512) { if (self.timers.count() > 512) {
return error.TooManyTimeout; return error.TooManyTimeout;
@@ -328,9 +310,9 @@ pub const Window = struct {
errdefer _ = self.timers.remove(timer_id); errdefer _ = self.timers.remove(timer_id);
const args = opts.args; const args = opts.args;
var persisted_args: []js.Object = &.{}; var persisted_args: []Env.JsObject = &.{};
if (args.len > 0) { if (args.len > 0) {
persisted_args = try page.arena.alloc(js.Object, args.len); persisted_args = try page.arena.alloc(Env.JsObject, args.len);
for (args, persisted_args) |a, *ca| { for (args, persisted_args) |a, *ca| {
ca.* = try a.persist(); ca.* = try a.persist();
} }
@@ -371,20 +353,12 @@ pub const Window = struct {
const Opts = struct { const Opts = struct {
top: i32, top: i32,
left: i32, left: i32,
behavior: []const u8 = "", behavior: []const u8,
}; };
}; };
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32) !void { pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?u32) !void {
switch (opts) { _ = opts;
.x => |x| { _ = y;
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(); const scroll_event = try parser.eventCreate();
@@ -408,28 +382,6 @@ 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 // libdom's document doesn't have a parent, which is correct, but
// breaks the event bubbling that happens for many events from // breaks the event bubbling that happens for many events from
@@ -447,18 +399,6 @@ pub const Window = struct {
// and thus the target has already been set to the document. // and thus the target has already been set to the document.
return self.base.redispatchEvent(evt); 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 { const TimerCallback = struct {
@@ -469,13 +409,13 @@ const TimerCallback = struct {
repeat: ?u32, repeat: ?u32,
// The JavaScript callback to execute // The JavaScript callback to execute
cbk: js.Function, cbk: Function,
animation_frame: bool = false, animation_frame: bool = false,
window: *Window, window: *Window,
args: []js.Object = &.{}, args: []Env.JsObject = &.{},
fn run(ctx: *anyopaque) ?u32 { fn run(ctx: *anyopaque) ?u32 {
const self: *TimerCallback = @ptrCast(@alignCast(ctx)); const self: *TimerCallback = @ptrCast(@alignCast(ctx));
@@ -489,7 +429,7 @@ const TimerCallback = struct {
return null; return null;
} }
var result: js.Function.Result = undefined; var result: Function.Result = undefined;
var call: anyerror!void = undefined; var call: anyerror!void = undefined;
if (self.animation_frame) { if (self.animation_frame) {

View File

@@ -1,561 +0,0 @@
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));
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,539 +0,0 @@
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();
}
}

View File

@@ -1,251 +0,0 @@
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();
}

View File

@@ -1,144 +0,0 @@
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, .{});
}

View File

@@ -1,125 +0,0 @@
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));
}

View File

@@ -1,149 +0,0 @@
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,
};
}

View File

@@ -1,21 +0,0 @@
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();
}

View File

@@ -1,25 +0,0 @@
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);
}

View File

@@ -1,64 +0,0 @@
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();
}

View File

@@ -1,499 +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");
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 "";
}

View File

@@ -1,183 +0,0 @@
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;
}

View File

@@ -558,8 +558,6 @@ pub const EventType = enum(u8) {
xhr_event = 6, xhr_event = 6,
message_event = 7, message_event = 7,
keyboard_event = 8, keyboard_event = 8,
pop_state = 9,
composition_event = 10,
}; };
pub const MutationEvent = c.dom_mutation_event; pub const MutationEvent = c.dom_mutation_event;
@@ -583,14 +581,6 @@ pub fn mutationEventPrevValue(evt: *MutationEvent) ?[]const u8 {
return strToData(s.?); 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 { pub fn mutationEventRelatedNode(evt: *MutationEvent) !?*Node {
var n: NodeExternal = undefined; var n: NodeExternal = undefined;
const err = c._dom_mutation_event_get_related_node(evt, &n); const err = c._dom_mutation_event_get_related_node(evt, &n);
@@ -1365,7 +1355,7 @@ pub fn nodeHasChildNodes(node: *Node) bool {
return res; 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; var res: ?*Node = null;
const err = nodeVtable(node).dom_node_insert_before.?(node, new_node, ref_node, &res); const err = nodeVtable(node).dom_node_insert_before.?(node, new_node, ref_node, &res);
try DOMErr(err); try DOMErr(err);
@@ -2501,22 +2491,31 @@ fn parseParams(enc: ?[:0]const u8) c.dom_hubbub_parser_params {
} }
fn parseData(parser: *c.dom_hubbub_parser, reader: anytype) !void { fn parseData(parser: *c.dom_hubbub_parser, reader: anytype) !void {
var buffer: [1024]u8 = undefined; var err: c.hubbub_error = undefined;
var ln = buffer.len; const TI = @typeInfo(@TypeOf(reader));
while (ln > 0) { if (TI == .pointer and @hasDecl(TI.pointer.child, "next")) {
ln = try reader.read(&buffer); while (try reader.next()) |data| {
const err = c.dom_hubbub_parser_parse_chunk(parser, &buffer, ln); err = c.dom_hubbub_parser_parse_chunk(parser, data.ptr, data.len);
// TODO handle encoding change error return. try parserErr(err);
// When the HTML contains a META tag with a different encoding than the }
// original one, a c.DOM_HUBBUB_HUBBUB_ERR_ENCODINGCHANGE error is } else {
// returned. var buffer: [1024]u8 = undefined;
// In this case, we must restart the parsing with the new detected var ln = buffer.len;
// encoding. The detected encoding is stored in the document and we can while (ln > 0) {
// get it with documentGetInputEncoding(). ln = try reader.read(&buffer);
try parserErr(err); 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);
}
} }
const err = c.dom_hubbub_parser_completed(parser); err = c.dom_hubbub_parser_completed(parser);
return parserErr(err); try parserErr(err);
} }
// documentHTMLClose closes the document. // documentHTMLClose closes the document.

View File

@@ -23,8 +23,8 @@ const Allocator = std.mem.Allocator;
const Dump = @import("dump.zig"); const Dump = @import("dump.zig");
const State = @import("State.zig"); const State = @import("State.zig");
const Env = @import("env.zig").Env;
const Mime = @import("mime.zig").Mime; const Mime = @import("mime.zig").Mime;
const Browser = @import("browser.zig").Browser;
const Session = @import("session.zig").Session; const Session = @import("session.zig").Session;
const Renderer = @import("renderer.zig").Renderer; const Renderer = @import("renderer.zig").Renderer;
const Window = @import("html/window.zig").Window; const Window = @import("html/window.zig").Window;
@@ -32,10 +32,8 @@ const Walker = @import("dom/walker.zig").WalkerDepthFirst;
const Scheduler = @import("Scheduler.zig"); const Scheduler = @import("Scheduler.zig");
const Http = @import("../http/Http.zig"); const Http = @import("../http/Http.zig");
const ScriptManager = @import("ScriptManager.zig"); const ScriptManager = @import("ScriptManager.zig");
const SlotChangeMonitor = @import("SlotChangeMonitor.zig");
const HTMLDocument = @import("html/document.zig").HTMLDocument; const HTMLDocument = @import("html/document.zig").HTMLDocument;
const js = @import("js/js.zig");
const URL = @import("../url.zig").URL; const URL = @import("../url.zig").URL;
const log = @import("../log.zig"); const log = @import("../log.zig");
@@ -75,7 +73,7 @@ pub const Page = struct {
// Our JavaScript context for this specific page. This is what we use to // Our JavaScript context for this specific page. This is what we use to
// execute any JavaScript // execute any JavaScript
js: *js.Context, main_context: *Env.JsContext,
// indicates intention to navigate to another page on the next loop execution. // indicates intention to navigate to another page on the next loop execution.
delayed_navigation: bool = false, delayed_navigation: bool = false,
@@ -92,10 +90,6 @@ pub const Page = struct {
load_state: LoadState = .parsing, 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_idle: IdleNotification = .init,
notified_network_almost_idle: IdleNotification = .init, notified_network_almost_idle: IdleNotification = .init,
@@ -122,7 +116,7 @@ pub const Page = struct {
complete, complete,
}; };
pub fn init(self: *Page, arena: Allocator, call_arena: Allocator, session: *Session) !void { pub fn init(self: *Page, arena: Allocator, session: *Session) !void {
const browser = session.browser; const browser = session.browser;
const script_manager = ScriptManager.init(browser, self); const script_manager = ScriptManager.init(browser, self);
@@ -132,7 +126,7 @@ pub const Page = struct {
.window = try Window.create(null, null), .window = try Window.create(null, null),
.arena = arena, .arena = arena,
.session = session, .session = session,
.call_arena = call_arena, .call_arena = undefined,
.renderer = Renderer.init(arena), .renderer = Renderer.init(arena),
.state_pool = &browser.state_pool, .state_pool = &browser.state_pool,
.cookie_jar = &session.cookie_jar, .cookie_jar = &session.cookie_jar,
@@ -141,13 +135,17 @@ pub const Page = struct {
.scheduler = Scheduler.init(arena), .scheduler = Scheduler.init(arena),
.keydown_event_node = .{ .func = keydownCallback }, .keydown_event_node = .{ .func = keydownCallback },
.window_clicked_event_node = .{ .func = windowClicked }, .window_clicked_event_node = .{ .func = windowClicked },
.js = undefined, .main_context = undefined,
}; };
self.js = try session.executor.createContext(self, true, js.GlobalMissingCallback.init(&self.polyfill_loader)); self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader));
try polyfill.preload(self.arena, self.js); try polyfill.preload(self.arena, self.main_context);
try self.registerBackgroundTasks(); 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" });
}
} }
pub fn deinit(self: *Page) void { pub fn deinit(self: *Page) void {
@@ -157,10 +155,7 @@ pub const Page = struct {
self.script_manager.deinit(); self.script_manager.deinit();
} }
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.scheduler.reset();
self.http_client.abort(); self.http_client.abort();
self.script_manager.reset(); self.script_manager.reset();
@@ -168,31 +163,18 @@ pub const Page = struct {
self.load_state = .parsing; self.load_state = .parsing;
self.mode = .{ .pre = {} }; self.mode = .{ .pre = {} };
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); _ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
try self.registerBackgroundTasks();
} }
fn registerBackgroundTasks(self: *Page) !void { fn runMicrotasks(ctx: *anyopaque) ?u32 {
if (comptime builtin.is_test) { const self: *Page = @ptrCast(@alignCast(ctx));
// HTML test runner manually calls these as necessary self.session.browser.runMicrotasks();
return; return 5;
} }
try self.scheduler.add(self.session.browser, struct { fn runMessageLoop(ctx: *anyopaque) ?u32 {
fn runMicrotasks(ctx: *anyopaque) ?u32 { const self: *Page = @ptrCast(@alignCast(ctx));
const b: *Browser = @ptrCast(@alignCast(ctx)); self.session.browser.runMessageLoop();
b.runMicrotasks(); return 100;
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" });
} }
pub const DumpOpts = struct { pub const DumpOpts = struct {
@@ -268,6 +250,11 @@ pub const Page = struct {
try Node.prepend(head, &[_]Node.NodeOrText{.{ .node = parser.elementToNode(base) }}); 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 { pub fn wait(self: *Page, wait_ms: i32) Session.WaitResult {
return self._wait(wait_ms) catch |err| { return self._wait(wait_ms) catch |err| {
switch (err) { switch (err) {
@@ -289,8 +276,8 @@ pub const Page = struct {
var timer = try std.time.Timer.start(); var timer = try std.time.Timer.start();
var ms_remaining = wait_ms; var ms_remaining = wait_ms;
var try_catch: js.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(self.js); try_catch.init(self.main_context);
defer try_catch.deinit(); defer try_catch.deinit();
var scheduler = &self.scheduler; var scheduler = &self.scheduler;
@@ -366,7 +353,8 @@ pub const Page = struct {
std.debug.assert(http_client.intercepted == 0); std.debug.assert(http_client.intercepted == 0);
const ms = ms_to_next_task orelse blk: { const ms = ms_to_next_task orelse blk: {
if (wait_ms - ms_remaining < 100) { const min_wait = if (comptime builtin.is_test) 50 else 100;
if (wait_ms - ms_remaining < min_wait) {
// Look, we want to exit ASAP, but we don't want // Look, we want to exit ASAP, but we don't want
// to exit so fast that we've run none of the // to exit so fast that we've run none of the
// background jobs. // background jobs.
@@ -539,7 +527,7 @@ pub const Page = struct {
if (self.mode != .pre) { if (self.mode != .pre) {
// it's possible for navigate to be called multiple times on the // 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. // same page (via CDP). We want to reset the page between each call.
try self.reset(); self.reset();
} }
log.info(.http, "navigate", .{ log.info(.http, "navigate", .{
@@ -798,7 +786,7 @@ pub const Page = struct {
// ignore non-js script. // ignore non-js script.
continue; continue;
} }
try self.script_manager.addFromElement(@ptrCast(node), "page"); try self.script_manager.addFromElement(@ptrCast(node));
} }
self.script_manager.staticScriptsDone(); self.script_manager.staticScriptsDone();
@@ -814,9 +802,6 @@ pub const Page = struct {
unreachable; unreachable;
}, },
} }
// Push the navigation after a successful load.
try self.session.history.pushNavigation(self.url.raw, self);
} }
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void { fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
@@ -849,7 +834,7 @@ pub const Page = struct {
_ = self.session.browser.transfer_arena.reset(.{ .retain_with_limit = 4 * 1024 }); _ = self.session.browser.transfer_arena.reset(.{ .retain_with_limit = 4 * 1024 });
} }
// extracted because this is called from tests to set things up. // extracted because this sis called from tests to set things up.
pub fn setDocument(self: *Page, html_doc: *parser.DocumentHTML) !void { pub fn setDocument(self: *Page, html_doc: *parser.DocumentHTML) !void {
const doc = parser.documentHTMLToDocument(html_doc); const doc = parser.documentHTMLToDocument(html_doc);
try parser.documentSetDocumentURI(doc, self.url.raw); try parser.documentSetDocumentURI(doc, self.url.raw);
@@ -1013,31 +998,6 @@ pub const Page = struct {
} }
} }
// insertText is a shortcut to insert text into the active element.
pub fn insertText(self: *Page, v: []const u8) !void {
const Document = @import("dom/document.zig").Document;
const element = (try Document.getActiveElement(@ptrCast(self.window.document), self)) orelse return;
const node = parser.elementToNode(element);
const tag = (try parser.nodeHTMLGetTagType(node)) orelse return;
switch (tag) {
.input => {
const input_type = try parser.inputGetType(@ptrCast(element));
if (std.mem.eql(u8, input_type, "text")) {
const value = try parser.inputGetValue(@ptrCast(element));
const new_value = try std.mem.concat(self.arena, u8, &.{ value, v });
try parser.inputSetValue(@ptrCast(element), new_value);
}
},
.textarea => {
const value = try parser.textareaGetValue(@ptrCast(node));
const new_value = try std.mem.concat(self.arena, u8, &.{ value, v });
try parser.textareaSetValue(@ptrCast(node), new_value);
},
else => {},
}
}
// We cannot navigate immediately as navigating will delete the DOM tree, // We cannot navigate immediately as navigating will delete the DOM tree,
// which holds this event's node. // which holds this event's node.
// As such we schedule the function to be called as soon as possible. // As such we schedule the function to be called as soon as possible.
@@ -1154,22 +1114,10 @@ pub const Page = struct {
pub fn stackTrace(self: *Page) !?[]const u8 { pub fn stackTrace(self: *Page) !?[]const u8 {
if (comptime builtin.mode == .Debug) { if (comptime builtin.mode == .Debug) {
return self.js.stackTrace(); return self.main_context.stackTrace();
} }
return null; 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 { pub const NavigateReason = enum {
@@ -1177,7 +1125,6 @@ pub const NavigateReason = enum {
address_bar, address_bar,
form, form,
script, script,
history,
}; };
pub const NavigateOpts = struct { pub const NavigateOpts = struct {
@@ -1288,7 +1235,7 @@ pub export fn scriptAddedCallback(ctx: ?*anyopaque, element: ?*parser.Element) c
// here, else the script_manager will flag it as already-processed. // here, else the script_manager will flag it as already-processed.
_ = parser.elementGetAttribute(element.?, "src") catch return orelse return; _ = parser.elementGetAttribute(element.?, "src") catch return orelse return;
self.script_manager.addFromElement(element.?, "dynamic") catch |err| { self.script_manager.addFromElement(element.?) catch |err| {
log.warn(.browser, "dynamic script", .{ .err = err }); log.warn(.browser, "dynamic script", .{ .err = err });
}; };
} }

View File

@@ -19,9 +19,9 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Env = @import("../env.zig").Env;
pub const Loader = struct { pub const Loader = struct {
state: enum { empty, loading } = .empty, state: enum { empty, loading } = .empty,
@@ -30,8 +30,8 @@ pub const Loader = struct {
webcomponents: bool = false, webcomponents: bool = false,
} = .{}, } = .{},
fn load(self: *Loader, comptime name: []const u8, source: []const u8, js_context: *js.Context) void { fn load(self: *Loader, comptime name: []const u8, source: []const u8, js_context: *Env.JsContext) void {
var try_catch: js.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(js_context); try_catch.init(js_context);
defer try_catch.deinit(); defer try_catch.deinit();
@@ -49,7 +49,7 @@ pub const Loader = struct {
@field(self.done, name) = true; @field(self.done, name) = true;
} }
pub fn missing(self: *Loader, name: []const u8, js_context: *js.Context) bool { pub fn missing(self: *Loader, name: []const u8, js_context: *Env.JsContext) bool {
// Avoid recursive calls during polyfill loading. // Avoid recursive calls during polyfill loading.
if (self.state == .loading) { if (self.state == .loading) {
return false; return false;
@@ -69,7 +69,6 @@ pub const Loader = struct {
if (comptime builtin.mode == .Debug) { if (comptime builtin.mode == .Debug) {
log.debug(.unknown_prop, "unkown global property", .{ log.debug(.unknown_prop, "unkown global property", .{
.info = "but the property can exist in pure JS", .info = "but the property can exist in pure JS",
.stack = js_context.stackTrace() catch "???",
.property = name, .property = name,
}); });
} }
@@ -83,8 +82,8 @@ pub const Loader = struct {
} }
}; };
pub fn preload(allocator: Allocator, js_context: *js.Context) !void { pub fn preload(allocator: Allocator, js_context: *Env.JsContext) !void {
var try_catch: js.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(js_context); try_catch.init(js_context);
defer try_catch.deinit(); defer try_catch.deinit();

View File

@@ -20,11 +20,10 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const js = @import("js/js.zig"); const Env = @import("env.zig").Env;
const Page = @import("page.zig").Page; const Page = @import("page.zig").Page;
const Browser = @import("browser.zig").Browser; const Browser = @import("browser.zig").Browser;
const NavigateOpts = @import("page.zig").NavigateOpts; const NavigateOpts = @import("page.zig").NavigateOpts;
const History = @import("html/History.zig");
const log = @import("../log.zig"); const log = @import("../log.zig");
const parser = @import("netsurf.zig"); const parser = @import("netsurf.zig");
@@ -50,14 +49,10 @@ pub const Session = struct {
// page and start another. // page and start another.
transfer_arena: Allocator, transfer_arena: Allocator,
executor: js.ExecutionWorld, executor: Env.ExecutionWorld,
storage_shed: storage.Shed, storage_shed: storage.Shed,
cookie_jar: storage.CookieJar, 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, page: ?Page = null,
// If the current page want to navigate to a new page // If the current page want to navigate to a new page
@@ -106,7 +101,7 @@ pub const Session = struct {
self.page = @as(Page, undefined); self.page = @as(Page, undefined);
const page = &self.page.?; const page = &self.page.?;
try Page.init(page, page_arena.allocator(), self.browser.call_arena.allocator(), self); try Page.init(page, page_arena.allocator(), self);
log.debug(.browser, "create page", .{}); log.debug(.browser, "create page", .{});
// start JS env // start JS env
@@ -126,7 +121,7 @@ pub const Session = struct {
// registered a destructor (e.g. XMLHttpRequest). // registered a destructor (e.g. XMLHttpRequest).
// Should be called before we deinit the page, because these objects // Should be called before we deinit the page, because these objects
// could be referencing it. // could be referencing it.
self.executor.removeContext(); self.executor.removeJsContext();
self.page.?.deinit(); self.page.?.deinit();
self.page = null; self.page = null;
@@ -148,63 +143,36 @@ pub const Session = struct {
}; };
pub fn wait(self: *Session, wait_ms: i32) WaitResult { pub fn wait(self: *Session, wait_ms: i32) WaitResult {
_ = self.processQueuedNavigation() catch { if (self.queued_navigation) |qn| {
// There was an error processing the queue navigation. This already // This was already aborted on the page, but it would be pretty
// logged the error, just return. // bad if old requests went to the new page, so let's make double sure
return .done; 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;
};
}
if (self.page) |*page| { if (self.page) |*page| {
return page.wait(wait_ms); return page.wait(wait_ms);
} }
return .no_page; 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 { const QueuedNavigation = struct {

View File

@@ -207,7 +207,16 @@ pub const Cookie = struct {
// Duplicate attributes - use the last valid // Duplicate attributes - use the last valid
// Value-less attributes with a value? Ignore the value // Value-less attributes with a value? Ignore the value
pub fn parse(allocator: Allocator, uri: *const std.Uri, str: []const u8) !Cookie { pub fn parse(allocator: Allocator, uri: *const std.Uri, str: []const u8) !Cookie {
try validateCookieString(str); 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;
}
}
const cookie_name, const cookie_value, const rest = parseNameValue(str) catch { const cookie_name, const cookie_value, const rest = parseNameValue(str) catch {
return error.InvalidNameValue; return error.InvalidNameValue;
@@ -312,56 +321,6 @@ pub const Cookie = struct {
}; };
} }
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;
}
}
// 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;
}
}
pub fn parsePath(arena: Allocator, uri: ?*const std.Uri, explicit_path: ?[]const u8) ![]const u8 { pub fn parsePath(arena: Allocator, uri: ?*const std.Uri, explicit_path: ?[]const u8) ![]const u8 {
// path attribute value either begins with a '/' or we // path attribute value either begins with a '/' or we
// ignore it and use the "default-path" algorithm // ignore it and use the "default-path" algorithm

View File

@@ -17,11 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Allocator = std.mem.Allocator;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Env = @import("../env.zig").Env;
const ReadableStream = @This(); const ReadableStream = @This();
const ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig"); const ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig");
@@ -31,57 +30,31 @@ const State = union(enum) {
readable, readable,
closed: ?[]const u8, closed: ?[]const u8,
cancelled: ?[]const u8, cancelled: ?[]const u8,
errored: js.Object, errored: Env.JsObject,
}; };
// This promise resolves when a stream is canceled. // This promise resolves when a stream is canceled.
cancel_resolver: js.PersistentPromiseResolver, cancel_resolver: Env.PersistentPromiseResolver,
closed_resolver: js.PersistentPromiseResolver, closed_resolver: Env.PersistentPromiseResolver,
reader_resolver: ?js.PersistentPromiseResolver = null, reader_resolver: ?Env.PersistentPromiseResolver = null,
locked: bool = false, locked: bool = false,
state: State = .readable, state: State = .readable,
cancel_fn: ?js.Function = null, cancel_fn: ?Env.Function = null,
pull_fn: ?js.Function = null, pull_fn: ?Env.Function = null,
strategy: QueueingStrategy, strategy: QueueingStrategy,
queue: std.ArrayListUnmanaged(Chunk) = .empty, queue: std.ArrayListUnmanaged([]const u8) = .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 { pub const ReadableStreamReadResult = struct {
const ValueUnion =
union(enum) { data: []const u8, empty: void };
value: ValueUnion,
done: bool, done: bool,
value: Value = .empty,
const Value = union(enum) { pub fn get_value(self: *const ReadableStreamReadResult) ValueUnion {
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; return self.value;
} }
@@ -91,22 +64,22 @@ pub const ReadableStreamReadResult = struct {
}; };
const UnderlyingSource = struct { const UnderlyingSource = struct {
start: ?js.Function = null, start: ?Env.Function = null,
pull: ?js.Function = null, pull: ?Env.Function = null,
cancel: ?js.Function = null, cancel: ?Env.Function = null,
type: ?[]const u8 = null, type: ?[]const u8 = null,
}; };
const QueueingStrategy = struct { const QueueingStrategy = struct {
size: ?js.Function = null, size: ?Env.Function = null,
high_water_mark: u32 = 1, high_water_mark: u32 = 1,
}; };
pub fn constructor(underlying: ?UnderlyingSource, _strategy: ?QueueingStrategy, page: *Page) !*ReadableStream { pub fn constructor(underlying: ?UnderlyingSource, _strategy: ?QueueingStrategy, page: *Page) !*ReadableStream {
const strategy: QueueingStrategy = _strategy orelse .{}; const strategy: QueueingStrategy = _strategy orelse .{};
const cancel_resolver = try page.js.createPromiseResolver(.self); const cancel_resolver = try page.main_context.createPersistentPromiseResolver(.self);
const closed_resolver = try page.js.createPromiseResolver(.self); const closed_resolver = try page.main_context.createPersistentPromiseResolver(.self);
const stream = try page.arena.create(ReadableStream); const stream = try page.arena.create(ReadableStream);
stream.* = ReadableStream{ stream.* = ReadableStream{
@@ -146,7 +119,7 @@ pub fn get_locked(self: *const ReadableStream) bool {
return self.locked; return self.locked;
} }
pub fn _cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !js.Promise { pub fn _cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !Env.Promise {
if (self.locked) { if (self.locked) {
return error.TypeError; return error.TypeError;
} }

View File

@@ -17,10 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Env = @import("../env.zig").Env;
const ReadableStream = @import("./ReadableStream.zig"); const ReadableStream = @import("./ReadableStream.zig");
const ReadableStreamReadResult = @import("./ReadableStream.zig").ReadableStreamReadResult; const ReadableStreamReadResult = @import("./ReadableStream.zig").ReadableStreamReadResult;
@@ -51,17 +51,17 @@ pub fn _close(self: *ReadableStreamDefaultController, _reason: ?[]const u8, page
// to discard, must use cancel. // to discard, must use cancel.
} }
pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: ReadableStream.Chunk, page: *Page) !void { pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: []const u8, page: *Page) !void {
const stream = self.stream; const stream = self.stream;
if (stream.state != .readable) { if (stream.state != .readable) {
return error.TypeError; return error.TypeError;
} }
const duped_chunk = try chunk.dupe(page.arena); const duped_chunk = try page.arena.dupe(u8, chunk);
if (self.stream.reader_resolver) |*rr| { if (self.stream.reader_resolver) |*rr| {
try rr.resolve(ReadableStreamReadResult.init(duped_chunk, false)); try rr.resolve(ReadableStreamReadResult{ .value = .{ .data = duped_chunk }, .done = false });
self.stream.reader_resolver = null; self.stream.reader_resolver = null;
} }
@@ -69,7 +69,7 @@ pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: ReadableStream.Ch
try self.stream.pullIf(); try self.stream.pullIf();
} }
pub fn _error(self: *ReadableStreamDefaultController, err: js.Object) !void { pub fn _error(self: *ReadableStreamDefaultController, err: Env.JsObject) !void {
self.stream.state = .{ .errored = err }; self.stream.state = .{ .errored = err };
if (self.stream.reader_resolver) |*rr| { if (self.stream.reader_resolver) |*rr| {

View File

@@ -18,8 +18,8 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const ReadableStream = @import("./ReadableStream.zig"); const ReadableStream = @import("./ReadableStream.zig");
const ReadableStreamReadResult = @import("./ReadableStream.zig").ReadableStreamReadResult; const ReadableStreamReadResult = @import("./ReadableStream.zig").ReadableStreamReadResult;
@@ -32,41 +32,58 @@ pub fn constructor(stream: *ReadableStream) ReadableStreamDefaultReader {
return .{ .stream = stream }; return .{ .stream = stream };
} }
pub fn get_closed(self: *const ReadableStreamDefaultReader) js.Promise { pub fn get_closed(self: *const ReadableStreamDefaultReader) Env.Promise {
return self.stream.closed_resolver.promise(); return self.stream.closed_resolver.promise();
} }
pub fn _cancel(self: *ReadableStreamDefaultReader, reason: ?[]const u8, page: *Page) !js.Promise { pub fn _cancel(self: *ReadableStreamDefaultReader, reason: ?[]const u8, page: *Page) !Env.Promise {
return try self.stream._cancel(reason, page); return try self.stream._cancel(reason, page);
} }
pub fn _read(self: *const ReadableStreamDefaultReader, page: *Page) !js.Promise { pub fn _read(self: *const ReadableStreamDefaultReader, page: *Page) !Env.Promise {
const stream = self.stream; const stream = self.stream;
switch (stream.state) { switch (stream.state) {
.readable => { .readable => {
if (stream.queue.items.len > 0) { if (stream.queue.items.len > 0) {
const data = self.stream.queue.orderedRemove(0); const data = self.stream.queue.orderedRemove(0);
const promise = page.js.resolvePromise(ReadableStreamReadResult.init(data, false)); const resolver = page.main_context.createPromiseResolver();
try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false });
try self.stream.pullIf(); try self.stream.pullIf();
return promise; 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();
}
} }
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 => |_| { .closed => |_| {
const resolver = page.main_context.createPromiseResolver();
if (stream.queue.items.len > 0) { if (stream.queue.items.len > 0) {
const data = self.stream.queue.orderedRemove(0); const data = self.stream.queue.orderedRemove(0);
return page.js.resolvePromise(ReadableStreamReadResult.init(data, false)); try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false });
} else {
try resolver.resolve(ReadableStreamReadResult{ .value = .empty, .done = true });
} }
return page.js.resolvePromise(ReadableStreamReadResult{ .done = true });
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();
}, },
.cancelled => |_| return page.js.resolvePromise(ReadableStreamReadResult{ .value = .empty, .done = true }),
.errored => |err| return page.js.rejectPromise(err),
} }
} }

View File

@@ -19,8 +19,8 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const FormData = @import("../xhr/form_data.zig").FormData; const FormData = @import("../xhr/form_data.zig").FormData;
@@ -112,10 +112,6 @@ pub const URL = struct {
}; };
} }
pub fn initWithoutSearchParams(uri: std.Uri) URL {
return .{ .uri = uri, .search_params = .{} };
}
pub fn get_origin(self: *URL, page: *Page) ![]const u8 { pub fn get_origin(self: *URL, page: *Page) ![]const u8 {
var aw = std.Io.Writer.Allocating.init(page.arena); var aw = std.Io.Writer.Allocating.init(page.arena);
try self.uri.writeToStream(&aw.writer, .{ try self.uri.writeToStream(&aw.writer, .{
@@ -164,11 +160,8 @@ pub const URL = struct {
return aw.written(); return aw.written();
} }
pub fn get_protocol(self: *const URL) []const u8 { pub fn get_protocol(self: *URL, page: *Page) ![]const u8 {
// std.Uri keeps a pointer to "https", "http" (scheme part) so we know return try std.mem.concat(page.arena, u8, &[_][]const u8{ self.uri.scheme, ":" });
// its followed by ':'.
const scheme = self.uri.scheme;
return scheme.ptr[0 .. scheme.len + 1];
} }
pub fn get_username(self: *URL) []const u8 { pub fn get_username(self: *URL) []const u8 {
@@ -268,7 +261,7 @@ pub const URLSearchParams = struct {
const URLSearchParamsOpts = union(enum) { const URLSearchParamsOpts = union(enum) {
qs: []const u8, qs: []const u8,
form_data: *const FormData, form_data: *const FormData,
js_obj: js.Object, js_obj: Env.JsObject,
}; };
pub fn constructor(opts_: ?URLSearchParamsOpts, page: *Page) !URLSearchParams { pub fn constructor(opts_: ?URLSearchParamsOpts, page: *Page) !URLSearchParams {
const opts = opts_ orelse return .{ .entries = .{} }; const opts = opts_ orelse return .{ .entries = .{} };

View File

@@ -17,7 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const Env = @import("../env.zig").Env;
const Function = Env.Function;
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler; const EventHandler = @import("../events/event.zig").EventHandler;
@@ -31,20 +33,20 @@ pub const XMLHttpRequestEventTarget = struct {
// Extend libdom event target for pure zig struct. // Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .xhr }, base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .xhr },
onloadstart_cbk: ?js.Function = null, onloadstart_cbk: ?Function = null,
onprogress_cbk: ?js.Function = null, onprogress_cbk: ?Function = null,
onabort_cbk: ?js.Function = null, onabort_cbk: ?Function = null,
onload_cbk: ?js.Function = null, onload_cbk: ?Function = null,
ontimeout_cbk: ?js.Function = null, ontimeout_cbk: ?Function = null,
onloadend_cbk: ?js.Function = null, onloadend_cbk: ?Function = null,
onreadystatechange_cbk: ?js.Function = null, onreadystatechange_cbk: ?Function = null,
fn register( fn register(
self: *XMLHttpRequestEventTarget, self: *XMLHttpRequestEventTarget,
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
typ: []const u8, typ: []const u8,
listener: EventHandler.Listener, listener: EventHandler.Listener,
) !?js.Function { ) !?Function {
const target = @as(*parser.EventTarget, @ptrCast(self)); const target = @as(*parser.EventTarget, @ptrCast(self));
// The only time this can return null if the listener is already // The only time this can return null if the listener is already
@@ -67,25 +69,25 @@ pub const XMLHttpRequestEventTarget = struct {
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false); try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
} }
pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?js.Function { pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Function {
return self.onloadstart_cbk; return self.onloadstart_cbk;
} }
pub fn get_onprogress(self: *XMLHttpRequestEventTarget) ?js.Function { pub fn get_onprogress(self: *XMLHttpRequestEventTarget) ?Function {
return self.onprogress_cbk; return self.onprogress_cbk;
} }
pub fn get_onabort(self: *XMLHttpRequestEventTarget) ?js.Function { pub fn get_onabort(self: *XMLHttpRequestEventTarget) ?Function {
return self.onabort_cbk; return self.onabort_cbk;
} }
pub fn get_onload(self: *XMLHttpRequestEventTarget) ?js.Function { pub fn get_onload(self: *XMLHttpRequestEventTarget) ?Function {
return self.onload_cbk; return self.onload_cbk;
} }
pub fn get_ontimeout(self: *XMLHttpRequestEventTarget) ?js.Function { pub fn get_ontimeout(self: *XMLHttpRequestEventTarget) ?Function {
return self.ontimeout_cbk; return self.ontimeout_cbk;
} }
pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?js.Function { pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Function {
return self.onloadend_cbk; return self.onloadend_cbk;
} }
pub fn get_onreadystatechange(self: *XMLHttpRequestEventTarget) ?js.Function { pub fn get_onreadystatechange(self: *XMLHttpRequestEventTarget) ?Function {
return self.onreadystatechange_cbk; return self.onreadystatechange_cbk;
} }

View File

@@ -21,18 +21,18 @@ const Allocator = std.mem.Allocator;
const json = std.json; const json = std.json;
const log = @import("../log.zig"); 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 App = @import("../app.zig").App;
const Env = @import("../browser/env.zig").Env;
const Browser = @import("../browser/browser.zig").Browser; const Browser = @import("../browser/browser.zig").Browser;
const Session = @import("../browser/session.zig").Session; const Session = @import("../browser/session.zig").Session;
const Page = @import("../browser/page.zig").Page; const Page = @import("../browser/page.zig").Page;
const Inspector = @import("../browser/env.zig").Env.Inspector;
const Incrementing = @import("../id.zig").Incrementing; const Incrementing = @import("../id.zig").Incrementing;
const Notification = @import("../notification.zig").Notification; const Notification = @import("../notification.zig").Notification;
const LogInterceptor = @import("domains/log.zig").LogInterceptor;
const InterceptState = @import("domains/fetch.zig").InterceptState; const InterceptState = @import("domains/fetch.zig").InterceptState;
const polyfill = @import("../browser/polyfill/polyfill.zig");
pub const URL_BASE = "chrome://newtab/"; pub const URL_BASE = "chrome://newtab/";
pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C"; pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C";
@@ -329,7 +329,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
node_registry: Node.Registry, node_registry: Node.Registry,
node_search_list: Node.Search.List, node_search_list: Node.Search.List,
inspector: js.Inspector, inspector: Inspector,
isolated_worlds: std.ArrayListUnmanaged(IsolatedWorld), isolated_worlds: std.ArrayListUnmanaged(IsolatedWorld),
http_proxy_changed: bool = false, http_proxy_changed: bool = false,
@@ -339,8 +339,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
intercept_state: InterceptState, intercept_state: InterceptState,
log_interceptor: LogInterceptor(Self),
// When network is enabled, we'll capture the transfer.id -> body // When network is enabled, we'll capture the transfer.id -> body
// This is awfully memory intensive, but our underlying http client and // This is awfully memory intensive, but our underlying http client and
// its users (script manager and page) correctly do not hold the body // its users (script manager and page) correctly do not hold the body
@@ -381,7 +379,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
.notification_arena = cdp.notification_arena.allocator(), .notification_arena = cdp.notification_arena.allocator(),
.intercept_state = try InterceptState.init(allocator), .intercept_state = try InterceptState.init(allocator),
.captured_responses = .empty, .captured_responses = .empty,
.log_interceptor = LogInterceptor(Self).init(allocator, self),
}; };
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
errdefer self.deinit(); errdefer self.deinit();
@@ -393,14 +390,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
} }
pub fn deinit(self: *Self) void { 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(); self.inspector.deinit();
// abort all intercepted requests before closing the sesion/page // abort all intercepted requests before closing the sesion/page
@@ -508,18 +497,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
self.cdp.browser.notification.unregister(.page_network_almost_idle, self); 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 { pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
const self: *Self = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
try @import("domains/page.zig").pageRemove(self); try @import("domains/page.zig").pageRemove(self);
@@ -684,7 +661,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. /// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts.
const IsolatedWorld = struct { const IsolatedWorld = struct {
name: []const u8, name: []const u8,
executor: js.ExecutionWorld, executor: Env.ExecutionWorld,
grant_universal_access: bool, grant_universal_access: bool,
// Polyfill loader for the isolated world. // Polyfill loader for the isolated world.
@@ -695,28 +672,30 @@ const IsolatedWorld = struct {
self.executor.deinit(); self.executor.deinit();
} }
pub fn removeContext(self: *IsolatedWorld) !void { pub fn removeContext(self: *IsolatedWorld) !void {
if (self.executor.context == null) return error.NoIsolatedContextToRemove; if (self.executor.js_context == null) return error.NoIsolatedContextToRemove;
self.executor.removeContext(); self.executor.removeJsContext();
} }
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML // The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
// (assuming grantUniveralAccess will be set to True!). // (assuming grantUniveralAccess will be set to True!).
// We just created the world and the page. The page's state lives in the session, but is update on navigation. // We just created the world and the page. The page's state lives in the session, but is update on navigation.
// This also means this pointer becomes invalid after removePage until a new page is created. // This also means this pointer becomes invalid after removePage untill a new page is created.
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world. // 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 { pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
// if (self.executor.context != null) return error.Only1IsolatedContextSupported; // if (self.executor.js_context != null) return error.Only1IsolatedContextSupported;
if (self.executor.context != null) { if (self.executor.js_context != null) {
log.warn(.cdp, "not implemented", .{ log.warn(.cdp, "not implemented", .{
.feature = "createContext: Not implemented second isolated context creation", .feature = "createContext: Not implemented second isolated context creation",
.info = "reuse existing context", .info = "reuse existing context",
}); });
return; return;
} }
_ = try self.executor.createContext( _ = try self.executor.createJsContext(
&page.window,
page, page,
{},
false, false,
js.GlobalMissingCallback.init(&self.polyfill_loader), Env.GlobalMissingCallback.init(&self.polyfill_loader),
); );
} }
@@ -725,7 +704,7 @@ const IsolatedWorld = struct {
try self.createContext(page); try self.createContext(page);
const loader = @import("../browser/polyfill/polyfill.zig"); const loader = @import("../browser/polyfill/polyfill.zig");
try loader.preload(arena, &self.executor.context.?); try loader.preload(arena, &self.executor.js_context.?);
} }
}; };

View File

@@ -274,11 +274,11 @@ fn resolveNode(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const page = bc.session.currentPage() orelse return error.PageNotLoaded; const page = bc.session.currentPage() orelse return error.PageNotLoaded;
var js_context = page.js; var js_context = page.main_context;
if (params.executionContextId) |context_id| { if (params.executionContextId) |context_id| {
if (js_context.v8_context.debugContextId() != context_id) { if (js_context.v8_context.debugContextId() != context_id) {
for (bc.isolated_worlds.items) |*isolated_world| { for (bc.isolated_worlds.items) |*isolated_world| {
js_context = &(isolated_world.executor.context orelse return error.ContextNotFound); js_context = &(isolated_world.executor.js_context orelse return error.ContextNotFound);
if (js_context.v8_context.debugContextId() == context_id) { if (js_context.v8_context.debugContextId() == context_id) {
break; break;
} }

View File

@@ -23,13 +23,11 @@ pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
dispatchKeyEvent, dispatchKeyEvent,
dispatchMouseEvent, dispatchMouseEvent,
insertText,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
.dispatchKeyEvent => return dispatchKeyEvent(cmd), .dispatchKeyEvent => return dispatchKeyEvent(cmd),
.dispatchMouseEvent => return dispatchMouseEvent(cmd), .dispatchMouseEvent => return dispatchMouseEvent(cmd),
.insertText => return insertText(cmd),
} }
} }
@@ -117,20 +115,6 @@ fn dispatchMouseEvent(cmd: anytype) !void {
// result already sent // result already sent
} }
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText
fn insertText(cmd: anytype) !void {
const params = (try cmd.params(struct {
text: []const u8, // The text to insert
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return;
const page = bc.session.currentPage() orelse return;
try page.insertText(params.text);
try cmd.sendResult(null, .{});
}
fn clickNavigate(cmd: anytype, uri: std.Uri) !void { fn clickNavigate(cmd: anytype, uri: std.Uri) !void {
const bc = cmd.browser_context.?; const bc = cmd.browser_context.?;

View File

@@ -17,97 +17,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig");
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
disable,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
.enable => return enable(cmd), .enable => return cmd.sendResult(null, .{}),
.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 });
};
}
};
}

View File

@@ -122,7 +122,7 @@ fn createIsolatedWorld(cmd: anytype) !void {
const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess); const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
const page = bc.session.currentPage() orelse return error.PageNotLoaded; const page = bc.session.currentPage() orelse return error.PageNotLoaded;
try world.createContextAndLoadPolyfills(bc.arena, page); try world.createContextAndLoadPolyfills(bc.arena, page);
const js_context = &world.executor.context.?; const js_context = &world.executor.js_context.?;
// Create the auxdata json for the contextCreated event // Create the auxdata json for the contextCreated event
// Calling contextCreated will assign a Id to the context and send 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; var cdp = bc.cdp;
const reason_: ?[]const u8 = switch (event.opts.reason) { const reason_: ?[]const u8 = switch (event.opts.reason) {
.anchor => "anchorClick", .anchor => "anchorClick",
.script, .history => "scriptInitiated", .script => "scriptInitiated",
.form => switch (event.opts.method) { .form => switch (event.opts.method) {
.GET => "formSubmissionGet", .GET => "formSubmissionGet",
.POST => "formSubmissionPost", .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 page = bc.session.currentPage() orelse return error.PageNotLoaded;
const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id}); const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
bc.inspector.contextCreated( bc.inspector.contextCreated(
page.js, page.main_context,
"", "",
try page.origin(arena), try page.origin(arena),
aux_data, 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}); 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 // Calling contextCreated will assign a new Id to the context and send the contextCreated event
bc.inspector.contextCreated( bc.inspector.contextCreated(
&isolated_world.executor.context.?, &isolated_world.executor.js_context.?,
isolated_world.name, isolated_world.name,
"://", "://",
aux_json, aux_json,

View File

@@ -21,48 +21,9 @@ const std = @import("std");
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
setIgnoreCertificateErrors,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
.enable => return cmd.sendResult(null, .{}), .enable => return cmd.sendResult(null, .{}),
.setIgnoreCertificateErrors => return setIgnoreCertificateErrors(cmd),
} }
} }
fn setIgnoreCertificateErrors(cmd: anytype) !void {
const params = (try cmd.params(struct {
ignore: bool,
})) orelse return error.InvalidParams;
if (params.ignore) {
try cmd.cdp.browser.http_client.disableTlsVerify();
} else {
try cmd.cdp.browser.http_client.enableTlsVerify();
}
return cmd.sendResult(null, .{});
}
const testing = @import("../testing.zig");
test "cdp.Security: setIgnoreCertificateErrors" {
var ctx = testing.context();
defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-9" });
try ctx.processMessage(.{
.id = 8,
.method = "Security.setIgnoreCertificateErrors",
.params = .{ .ignore = true },
});
try ctx.expectSentResult(null, .{ .id = 8 });
try ctx.processMessage(.{
.id = 9,
.method = "Security.setIgnoreCertificateErrors",
.params = .{ .ignore = false },
});
try ctx.expectSentResult(null, .{ .id = 9 });
}

View File

@@ -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}); const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
bc.inspector.contextCreated( bc.inspector.contextCreated(
page.js, page.main_context,
"", "",
try page.origin(cmd.arena), try page.origin(cmd.arena),
aux_data, aux_data,

View File

@@ -86,6 +86,9 @@ allocator: Allocator,
// request. These wil come and go with each request. // request. These wil come and go with each request.
transfer_pool: std.heap.MemoryPool(Transfer), transfer_pool: std.heap.MemoryPool(Transfer),
// see ScriptManager.blockingGet
blocking: Handle,
// To notify registered subscribers of events, the browser sets/nulls this for us. // To notify registered subscribers of events, the browser sets/nulls this for us.
notification: ?*Notification = null, notification: ?*Notification = null,
@@ -93,11 +96,6 @@ notification: ?*Notification = null,
// restoring, this originally-configured value is what it goes to. // restoring, this originally-configured value is what it goes to.
http_proxy: ?[:0]const u8 = null, http_proxy: ?[:0]const u8 = null,
// track if the client use a proxy for connections.
// We can't use http_proxy because we want also to track proxy configured via
// CDP.
use_proxy: bool,
// The complete user-agent header line // The complete user-agent header line
user_agent: [:0]const u8, user_agent: [:0]const u8,
@@ -123,15 +121,18 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie
var handles = try Handles.init(allocator, client, ca_blob, &opts); var handles = try Handles.init(allocator, client, ca_blob, &opts);
errdefer handles.deinit(allocator); errdefer handles.deinit(allocator);
var blocking = try Handle.init(client, ca_blob, &opts);
errdefer blocking.deinit();
client.* = .{ client.* = .{
.queue = .{}, .queue = .{},
.active = 0, .active = 0,
.intercepted = 0, .intercepted = 0,
.multi = multi, .multi = multi,
.handles = handles, .handles = handles,
.blocking = blocking,
.allocator = allocator, .allocator = allocator,
.http_proxy = opts.http_proxy, .http_proxy = opts.http_proxy,
.use_proxy = opts.http_proxy != null,
.user_agent = opts.user_agent, .user_agent = opts.user_agent,
.transfer_pool = transfer_pool, .transfer_pool = transfer_pool,
}; };
@@ -141,6 +142,7 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie
pub fn deinit(self: *Client) void { pub fn deinit(self: *Client) void {
self.abort(); self.abort();
self.blocking.deinit();
self.handles.deinit(self.allocator); self.handles.deinit(self.allocator);
_ = c.curl_multi_cleanup(self.multi); _ = c.curl_multi_cleanup(self.multi);
@@ -261,6 +263,12 @@ pub fn fulfillTransfer(self: *Client, transfer: *Transfer, status: u16, headers:
return transfer.fulfill(status, headers, body); 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 { fn makeTransfer(self: *Client, req: Request) !*Transfer {
errdefer req.headers.deinit(); errdefer req.headers.deinit();
@@ -321,7 +329,7 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void {
for (self.handles.handles) |*h| { 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(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
} }
self.use_proxy = true; 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 // Same restriction as changeProxy. Should be ok since this is only called on
@@ -333,41 +341,7 @@ pub fn restoreOriginalProxy(self: *Client) !void {
for (self.handles.handles) |*h| { for (self.handles.handles) |*h| {
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy)); try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy));
} }
self.use_proxy = proxy != null; try errorCheck(c.curl_easy_setopt(self.blocking.conn.easy, c.CURLOPT_PROXY, proxy));
}
// Enable TLS verification on all connections.
pub fn enableTlsVerify(self: *const Client) !void {
try self.ensureNoActiveConnection();
for (self.handles.handles) |*h| {
const easy = h.conn.easy;
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 2)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 1)));
if (self.use_proxy) {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 2)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 1)));
}
}
}
// Disable TLS verification on all connections.
pub fn disableTlsVerify(self: *const Client) !void {
try self.ensureNoActiveConnection();
for (self.handles.handles) |*h| {
const easy = h.conn.easy;
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0)));
if (self.use_proxy) {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 0)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 0)));
}
}
} }
fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void { fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
@@ -435,9 +409,7 @@ fn perform(self: *Client, timeout_ms: c_int) !PerformStatus {
// We're potentially going to block for a while until we get data. Process // We're potentially going to block for a while until we get data. Process
// whatever messages we have waiting ahead of time. // whatever messages we have waiting ahead of time.
if (try self.processMessages()) { try self.processMessages();
return .normal;
}
var status = PerformStatus.normal; var status = PerformStatus.normal;
if (self.extra_socket) |s| { if (self.extra_socket) |s| {
@@ -455,13 +427,12 @@ fn perform(self: *Client, timeout_ms: c_int) !PerformStatus {
try errorMCheck(c.curl_multi_poll(multi, null, 0, timeout_ms, null)); try errorMCheck(c.curl_multi_poll(multi, null, 0, timeout_ms, null));
} }
_ = try self.processMessages(); try self.processMessages();
return status; return status;
} }
fn processMessages(self: *Client) !bool { fn processMessages(self: *Client) !void {
const multi = self.multi; const multi = self.multi;
var processed = false;
var messages_count: c_int = 0; var messages_count: c_int = 0;
while (c.curl_multi_info_read(multi, &messages_count)) |msg_| { while (c.curl_multi_info_read(multi, &messages_count)) |msg_| {
const msg: *c.CURLMsg = @ptrCast(msg_); const msg: *c.CURLMsg = @ptrCast(msg_);
@@ -520,12 +491,10 @@ fn processMessages(self: *Client) !bool {
.transfer = transfer, .transfer = transfer,
}); });
} }
processed = true;
} else |err| { } else |err| {
self.requestFailed(transfer, err); self.requestFailed(transfer, err);
} }
} }
return processed;
} }
fn endTransfer(self: *Client, transfer: *Transfer) void { fn endTransfer(self: *Client, transfer: *Transfer) void {
@@ -535,7 +504,7 @@ fn endTransfer(self: *Client, transfer: *Transfer) void {
log.fatal(.http, "Failed to remove handle", .{ .err = err }); log.fatal(.http, "Failed to remove handle", .{ .err = err });
}; };
self.handles.release(handle); self.handles.release(self, handle);
transfer._handle = null; transfer._handle = null;
self.active -= 1; self.active -= 1;
} }
@@ -594,7 +563,13 @@ const Handles = struct {
return null; return null;
} }
fn release(self: *Handles, handle: *Handle) void { 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;
}
var node = &handle.node; var node = &handle.node;
self.in_use.remove(node); self.in_use.remove(node);
node.prev = null; node.prev = null;
@@ -772,7 +747,7 @@ pub const Transfer = struct {
fn deinit(self: *Transfer) void { fn deinit(self: *Transfer) void {
self.req.headers.deinit(); self.req.headers.deinit();
if (self._handle) |handle| { if (self._handle) |handle| {
self.client.handles.release(handle); self.client.handles.release(self.client, handle);
} }
self.arena.deinit(); self.arena.deinit();
self.client.transfer_pool.destroy(self); self.client.transfer_pool.destroy(self);
@@ -850,7 +825,7 @@ pub const Transfer = struct {
self.deinit(); self.deinit();
} }
// abortAuthChallenge is called when an auth challenge interception is // abortAuthChallenge is called when an auth chanllenge interception is
// abort. We don't call self.client.endTransfer here b/c it has been done // abort. We don't call self.client.endTransfer here b/c it has been done
// before interception process. // before interception process.
pub fn abortAuthChallenge(self: *Transfer) void { pub fn abortAuthChallenge(self: *Transfer) void {

View File

@@ -168,13 +168,6 @@ pub const Connection = struct {
// debug // debug
if (comptime Http.ENABLE_DEBUG) { if (comptime Http.ENABLE_DEBUG) {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_VERBOSE, @as(c_long, 1))); 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 .{ return .{
@@ -442,14 +435,3 @@ const LineWriter = struct {
self.col = col + remain.len; 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}),
}
}

View File

@@ -29,6 +29,7 @@ pub const Scope = enum {
cdp, cdp,
console, console,
http, http,
http_client,
js, js,
loop, loop,
script_event, script_event,
@@ -39,7 +40,7 @@ pub const Scope = enum {
xhr, xhr,
fetch, fetch,
polyfill, polyfill,
interceptor, mouse_event,
}; };
const Opts = struct { const Opts = struct {
@@ -147,13 +148,6 @@ fn logTo(comptime scope: Scope, level: Level, comptime msg: []const u8, data: an
.pretty => try logPretty(scope, level, msg, data, out), .pretty => try logPretty(scope, level, msg, data, out),
} }
out.flush() catch return; 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 { fn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
@@ -352,24 +346,6 @@ fn elapsed() struct { time: f64, unit: []const u8 } {
return .{ .time = @as(f64, @floatFromInt(e)) / @as(f64, 1000), .unit = "s" }; 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"); const testing = @import("testing.zig");
test "log: data" { test "log: data" {
opts.format = .logfmt; opts.format = .logfmt;

View File

@@ -165,24 +165,6 @@ fn run(alloc: Allocator) !void {
// page // page
const page = try session.createPage(); 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) { _ = page.navigate(url, .{}) catch |err| switch (err) {
error.UnsupportedUriScheme, error.UriMissingHost => { error.UnsupportedUriScheme, error.UriMissingHost => {
log.fatal(.app, "invalid fetch URL", .{ .err = err, .url = url }); log.fatal(.app, "invalid fetch URL", .{ .err = err, .url = url });
@@ -194,7 +176,7 @@ fn run(alloc: Allocator) !void {
}, },
}; };
_ = session.fetchWait(5000); // 5 seconds _ = session.wait(5000); // 5 seconds
// dump // dump
if (opts.dump) { if (opts.dump) {

View File

@@ -19,20 +19,16 @@
const std = @import("std"); const std = @import("std");
const log = @import("log.zig"); const log = @import("log.zig");
const js = @import("browser/js/js.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const App = @import("app.zig").App; const App = @import("app.zig").App;
const Env = @import("browser/env.zig").Env;
const Browser = @import("browser/browser.zig").Browser; const Browser = @import("browser/browser.zig").Browser;
const TestHTTPServer = @import("TestHTTPServer.zig"); const TestHTTPServer = @import("TestHTTPServer.zig");
const WPT_DIR = "tests/wpt"; const WPT_DIR = "tests/wpt";
// use in custom panic handler
var current_test: ?[]const u8 = null;
pub fn main() !void { pub fn main() !void {
var gpa: std.heap.DebugAllocator(.{}) = .init; var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit(); defer _ = gpa.deinit();
@@ -81,9 +77,6 @@ pub fn main() !void {
while (try it.next()) |test_file| { while (try it.next()) |test_file| {
defer _ = test_arena.reset(.retain_capacity); defer _ = test_arena.reset(.retain_capacity);
defer current_test = null;
current_test = test_file;
var err_out: ?[]const u8 = null; var err_out: ?[]const u8 = null;
const result = run( const result = run(
test_arena.allocator(), test_arena.allocator(),
@@ -123,8 +116,8 @@ fn run(
_ = page.wait(2000); _ = page.wait(2000);
const js_context = page.js; const js_context = page.main_context;
var try_catch: js.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(js_context); try_catch.init(js_context);
defer try_catch.deinit(); defer try_catch.deinit();
@@ -455,12 +448,3 @@ fn httpHandler(req: *std.http.Server.Request) !void {
const file_path = try std.fmt.bufPrint(&buf, WPT_DIR ++ "{s}", .{path}); const file_path = try std.fmt.bufPrint(&buf, WPT_DIR ++ "{s}", .{path});
return TestHTTPServer.sendFile(req, file_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);

Some files were not shown because too many files have changed in this diff Show More