diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml
index 2b5fa598..bfe8fa40 100644
--- a/.github/actions/install/action.yml
+++ b/.github/actions/install/action.yml
@@ -59,11 +59,11 @@ runs:
- name: install v8
shell: bash
run: |
- mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug
- ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug/libc_v8.a
+ mkdir -p v8/build/${{inputs.arch}}-${{inputs.os}}/debug/ninja/obj/zig/
+ ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/build/${{inputs.arch}}-${{inputs.os}}/debug/ninja/obj/zig/libc_v8.a
- mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release
- ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release/libc_v8.a
+ mkdir -p v8/build/${{inputs.arch}}-${{inputs.os}}/release/ninja/obj/zig/
+ ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/build/${{inputs.arch}}-${{inputs.os}}/release/ninja/obj/zig/libc_v8.a
- name: libiconv
shell: bash
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9010b503..36493448 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -31,7 +31,7 @@ jobs:
arch: ${{env.ARCH}}
- name: zig build
- run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
+ run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -63,7 +63,7 @@ jobs:
arch: ${{env.ARCH}}
- name: zig build
- run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
+ run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -95,7 +95,7 @@ jobs:
arch: ${{env.ARCH}}
- name: zig build
- run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
+ run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -127,7 +127,7 @@ jobs:
arch: ${{env.ARCH}}
- name: zig build
- run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
+ run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml
index 53427e09..732c6fa6 100644
--- a/.github/workflows/e2e-test.yml
+++ b/.github/workflows/e2e-test.yml
@@ -47,7 +47,7 @@ jobs:
- uses: ./.github/actions/install
- name: zig build release
- run: zig build -Doptimize=ReleaseSafe -Dengine=v8
+ run: zig build -Doptimize=ReleaseSafe
- name: upload artifact
uses: actions/upload-artifact@v4
@@ -86,7 +86,7 @@ jobs:
- name: run puppeteer
run: |
python3 -m http.server 1234 -d ./public & echo $! > PYTHON.pid
- ./lightpanda serve & echo $! > LPD.pid
+ ./lightpanda serve --gc_hints & echo $! > LPD.pid
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
kill `cat LPD.pid` `cat PYTHON.pid`
diff --git a/.github/workflows/wpt.yml b/.github/workflows/wpt.yml
index a3416928..349c94c1 100644
--- a/.github/workflows/wpt.yml
+++ b/.github/workflows/wpt.yml
@@ -55,7 +55,7 @@ jobs:
- uses: ./.github/actions/install
- - run: zig build wpt -Dengine=v8 -- --safe --summary
+ - run: zig build wpt -- --safe --summary
# For now WPT tests doesn't pass at all.
# We accept then to continue the job on failure.
@@ -80,7 +80,7 @@ jobs:
- uses: ./.github/actions/install
- name: json output
- run: zig build wpt -Dengine=v8 -- --safe --json > wpt.json
+ run: zig build wpt -- --safe --json > wpt.json
- name: write commit
run: |
diff --git a/.github/workflows/zig-test.yml b/.github/workflows/zig-test.yml
index c462eb0c..bbe8afae 100644
--- a/.github/workflows/zig-test.yml
+++ b/.github/workflows/zig-test.yml
@@ -56,7 +56,7 @@ jobs:
- uses: ./.github/actions/install
- name: zig build debug
- run: zig build -Dengine=v8
+ run: zig build
- name: upload artifact
uses: actions/upload-artifact@v4
@@ -102,11 +102,8 @@ jobs:
- uses: ./.github/actions/install
- - name: zig build unittest
- run: zig build unittest -freference-trace --summary all
-
- name: zig build test
- run: zig build test -Dengine=v8 -- --json > bench.json
+ run: zig build test -- --json > bench.json
- name: write commit
run: |
diff --git a/.gitignore b/.gitignore
index 4c5a0f9f..ad9ae7b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ zig-out
/vendor/netsurf/out
/vendor/libiconv/
lightpanda.id
+/v8/
diff --git a/.gitmodules b/.gitmodules
index 9ee19adb..f025f0bd 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,7 +1,3 @@
-[submodule "vendor/zig-js-runtime"]
- path = vendor/zig-js-runtime
- url = https://github.com/lightpanda-io/zig-js-runtime.git/
- branch = zig-0.14
[submodule "vendor/netsurf/libwapcaplet"]
path = vendor/netsurf/libwapcaplet
url = https://github.com/lightpanda-io/libwapcaplet.git/
diff --git a/Dockerfile b/Dockerfile
index 7c0095d6..539dfdc6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -61,8 +61,8 @@ RUN make install-libiconv && \
# download and install v8
RUN curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
- mkdir -p vendor/zig-js-runtime/vendor/v8/${ARCH}-linux/release && \
- mv libc_v8.a vendor/zig-js-runtime/vendor/v8/${ARCH}-linux/release/libc_v8.a
+ mkdir -p v8/build/${ARCH}-linux/release/ninja/obj/zig/ && \
+ mv libc_v8.a v8/build/${ARCH}-linux/release/ninja/obj/zig/libc_v8.a
# build release
RUN make build
diff --git a/Makefile b/Makefile
index e9bc5716..309bbb42 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@
ZIG := zig
BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
-# option test filter make unittest F="server"
+# option test filter make test F="server"
F=
# OS and ARCH
@@ -47,7 +47,7 @@ help:
# $(ZIG) commands
# ------------
-.PHONY: build build-dev run run-release shell test bench download-zig wpt unittest data
+.PHONY: build build-dev run run-release shell test bench download-zig wpt data get-v8 build-v8 build-v8-dev
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
@@ -62,13 +62,13 @@ download-zig:
## Build in release-safe mode
build:
@printf "\e[36mBuilding (release safe)...\e[0m\n"
- $(ZIG) build -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
+ $(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mBuild OK\e[0m\n"
## Build in debug mode
build-dev:
@printf "\e[36mBuilding (debug)...\e[0m\n"
- @$(ZIG) build -Dengine=v8 -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
+ @$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mBuild OK\e[0m\n"
## Run the server in debug mode
@@ -79,39 +79,47 @@ run: build
## Run a JS shell in debug mode
shell:
@printf "\e[36mBuilding shell...\e[0m\n"
- @$(ZIG) build shell -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
+ @$(ZIG) build shell || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
## Run WPT tests
wpt:
@printf "\e[36mBuilding wpt...\e[0m\n"
- @$(ZIG) build wpt -Dengine=v8 -- --safe $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
+ @$(ZIG) build wpt -- --safe $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
wpt-summary:
@printf "\e[36mBuilding wpt...\e[0m\n"
- @$(ZIG) build wpt -Dengine=v8 -- --safe --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
+ @$(ZIG) build wpt -- --safe --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
## Test
test:
- @printf "\e[36mTesting...\e[0m\n"
- @$(ZIG) build test -Dengine=v8 || (printf "\e[33mTest ERROR\e[0m\n"; exit 1;)
- @printf "\e[33mTest OK\e[0m\n"
+ @TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
-unittest:
- @TEST_FILTER='${F}' $(ZIG) build unittest -freference-trace --summary all
+## v8
+get-v8:
+ @printf "\e[36mGetting v8 source...\e[0m\n"
+ @$(ZIG) build get-v8
+
+build-v8-dev:
+ @printf "\e[36mBuilding v8 (dev)...\e[0m\n"
+ @$(ZIG) build build-v8
+
+build-v8:
+ @printf "\e[36mBuilding v8...\e[0m\n"
+ @$(ZIG) build -Doptimize=ReleaseSafe build-v8
# Install and build required dependencies commands
# ------------
.PHONY: install-submodule
-.PHONY: install-zig-js-runtime install-zig-js-runtime-dev install-libiconv
+.PHONY: install-libiconv
.PHONY: _install-netsurf install-netsurf clean-netsurf test-netsurf install-netsurf-dev
.PHONY: install-mimalloc install-mimalloc-dev clean-mimalloc
.PHONY: install-dev install
## Install and build dependencies for release
-install: install-submodule install-zig-js-runtime install-libiconv install-netsurf install-mimalloc
+install: install-submodule install-libiconv install-netsurf install-mimalloc
## Install and build dependencies for dev
-install-dev: install-submodule install-zig-js-runtime-dev install-libiconv install-netsurf-dev install-mimalloc-dev
+install-dev: install-submodule install-libiconv install-netsurf-dev install-mimalloc-dev
install-netsurf-dev: _install-netsurf
install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
@@ -194,14 +202,6 @@ ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
make clean
endif
-install-zig-js-runtime-dev:
- @cd vendor/zig-js-runtime && \
- make install-dev
-
-install-zig-js-runtime:
- @cd vendor/zig-js-runtime && \
- make install
-
data:
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
diff --git a/README.md b/README.md
index eadf865e..5587c815 100644
--- a/README.md
+++ b/README.md
@@ -221,17 +221,21 @@ Note: when Mimalloc is built in dev mode, you can dump memory stats with the
env var `MIMALLOC_SHOW_STATS=1`. See
[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html).
-**zig-js-runtime**
+**v8**
-Our own Zig/Javascript runtime, which includes the v8 Javascript engine.
-
-This build task is very long and cpu consuming, as you will build v8 from sources.
+First, get the tools necessary for building V8, as well as the V8 source code:
```
-make install-zig-js-runtime
+make get-v8
```
-For dev env, use `make install-zig-js-runtime-dev`.
+Next, build v8. This build task is very long and cpu consuming, as you will build v8 from sources.
+
+```
+make build-v8
+```
+
+For dev env, use `make build-v8-dev`.
## Test
diff --git a/build.zig b/build.zig
index c26988c6..160e3c3f 100644
--- a/build.zig
+++ b/build.zig
@@ -17,16 +17,11 @@
// along with this program. If not, see .
const std = @import("std");
-
const builtin = @import("builtin");
-const jsruntime_path = "vendor/zig-js-runtime/";
-const jsruntime = @import("vendor/zig-js-runtime/build.zig");
-const jsruntime_pkgs = jsruntime.packages(jsruntime_path);
-
/// Do not rename this constant. It is scanned by some scripts to determine
/// which zig version to install.
-const recommended_zig_version = jsruntime.recommended_zig_version;
+const recommended_zig_version = "0.14.0";
pub fn build(b: *std.Build) !void {
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
@@ -42,193 +37,190 @@ pub fn build(b: *std.Build) !void {
},
}
+ var opts = b.addOptions();
+ opts.addOption(
+ []const u8,
+ "git_commit",
+ b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
+ );
+
const target = b.standardTargetOptions(.{});
- const mode = b.standardOptimizeOption(.{});
+ const optimize = b.standardOptimizeOption(.{});
- const options = jsruntime.buildOptions(b);
-
- // browser
- // -------
-
- // compile and install
- const exe = b.addExecutable(.{
- .name = "lightpanda",
- .root_source_file = b.path("src/main.zig"),
- .target = target,
- .optimize = mode,
- });
- try common(b, exe, options);
{
- var opt = b.addOptions();
- opt.addOption(
- []const u8,
- "git_commit",
- b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
- );
- exe.root_module.addImport("build_info", opt.createModule());
- }
- b.installArtifact(exe);
+ // browser
+ // -------
- // run
- const run_cmd = b.addRunArtifact(exe);
- if (b.args) |args| {
- run_cmd.addArgs(args);
+ // compile and install
+ const exe = b.addExecutable(.{
+ .name = "lightpanda",
+ .target = target,
+ .optimize = optimize,
+ .root_source_file = b.path("src/main.zig"),
+ });
+
+ try common(b, opts, exe);
+ b.installArtifact(exe);
+
+ // run
+ const run_cmd = b.addRunArtifact(exe);
+ if (b.args) |args| {
+ run_cmd.addArgs(args);
+ }
+
+ // step
+ const run_step = b.step("run", "Run the app");
+ run_step.dependOn(&run_cmd.step);
}
- // step
- const run_step = b.step("run", "Run the app");
- run_step.dependOn(&run_cmd.step);
-
- // shell
- // -----
-
- // compile and install
- const shell = b.addExecutable(.{
- .name = "lightpanda-shell",
- .root_source_file = b.path("src/main_shell.zig"),
- .target = target,
- .optimize = mode,
- });
- try common(b, shell, options);
- try jsruntime_pkgs.add_shell(shell);
-
- // run
- const shell_cmd = b.addRunArtifact(shell);
- if (b.args) |args| {
- shell_cmd.addArgs(args);
+ {
+ // 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);
}
- // step
- const shell_step = b.step("shell", "Run JS shell");
- shell_step.dependOn(&shell_cmd.step);
-
- // test
- // ----
-
- // compile
- const tests = b.addTest(.{
- .root_source_file = b.path("src/main_tests.zig"),
- .test_runner = .{ .path = b.path("src/main_tests.zig"), .mode = .simple },
- .target = target,
- .optimize = mode,
- });
- try common(b, tests, options);
-
- // add jsruntime pretty deps
- tests.root_module.addAnonymousImport("pretty", .{
- .root_source_file = b.path("vendor/zig-js-runtime/src/pretty.zig"),
- });
-
- const run_tests = b.addRunArtifact(tests);
- if (b.args) |args| {
- run_tests.addArgs(args);
+ {
+ // 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);
}
- // step
- const test_step = b.step("test", "Run unit tests");
- test_step.dependOn(&run_tests.step);
+ {
+ // tests
+ // ----
- // unittest
- // ----
+ // compile
+ const tests = b.addTest(.{
+ .root_source_file = b.path("src/main.zig"),
+ .test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
+ .target = target,
+ .optimize = optimize,
+ });
+ try common(b, opts, tests);
- // compile
- const unit_tests = b.addTest(.{
- .root_source_file = b.path("src/main_unit_tests.zig"),
- .test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
- .target = target,
- .optimize = mode,
- });
- try common(b, unit_tests, options);
+ const run_tests = b.addRunArtifact(tests);
+ if (b.args) |args| {
+ run_tests.addArgs(args);
+ }
- const run_unit_tests = b.addRunArtifact(unit_tests);
- if (b.args) |args| {
- run_unit_tests.addArgs(args);
+ // step
+ const tests_step = b.step("test", "Run unit tests");
+ tests_step.dependOn(&run_tests.step);
}
- // step
- const unit_test_step = b.step("unittest", "Run unit tests");
- unit_test_step.dependOn(&run_unit_tests.step);
+ {
+ // wpt
+ // -----
- // wpt
- // -----
+ // compile and install
+ const wpt = b.addExecutable(.{
+ .name = "lightpanda-wpt",
+ .root_source_file = b.path("src/main_wpt.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+ try common(b, opts, wpt);
- // compile and install
- const wpt = b.addExecutable(.{
- .name = "lightpanda-wpt",
- .root_source_file = b.path("src/main_wpt.zig"),
- .target = target,
- .optimize = mode,
- });
- try common(b, wpt, options);
-
- // run
- const wpt_cmd = b.addRunArtifact(wpt);
- if (b.args) |args| {
- wpt_cmd.addArgs(args);
+ // run
+ const wpt_cmd = b.addRunArtifact(wpt);
+ if (b.args) |args| {
+ wpt_cmd.addArgs(args);
+ }
+ // step
+ const wpt_step = b.step("wpt", "WPT tests");
+ wpt_step.dependOn(&wpt_cmd.step);
}
- // step
- const wpt_step = b.step("wpt", "WPT tests");
- wpt_step.dependOn(&wpt_cmd.step);
}
-fn common(
- b: *std.Build,
- step: *std.Build.Step.Compile,
- options: jsruntime.Options,
-) !void {
- const target = step.root_module.resolved_target.?;
- const optimize = step.root_module.optimize.?;
+fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Compile) !void {
+ const mod = step.root_module;
+ const target = mod.resolved_target.?;
+ const optimize = mod.optimize.?;
const dep_opts = .{ .target = target, .optimize = optimize };
- const jsruntimemod = try jsruntime_pkgs.module(
- b,
- options,
- step.root_module.optimize.?,
- target,
+ try moduleNetSurf(b, step, target);
+ mod.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
+ mod.addImport("tigerbeetle-io", b.dependency("tigerbeetle_io", .{}).module("tigerbeetle_io"));
+
+ {
+ // v8
+ const v8_opts = b.addOptions();
+ v8_opts.addOption(bool, "inspector_subtype", false);
+
+ const v8_mod = b.dependency("v8", dep_opts).module("v8");
+ v8_mod.addOptions("default_exports", v8_opts);
+ mod.addImport("v8", v8_mod);
+ }
+
+ const mode_str: []const u8 = if (mod.optimize.? == .Debug) "debug" else "release";
+
+ // FIXME: we are tied to native v8 builds, currently:
+ // - aarch64-macos
+ // - x86_64-linux
+ const os = target.result.os.tag;
+ const arch = target.result.cpu.arch;
+ switch (os) {
+ .macos => {},
+ .linux => {
+ // TODO: why do we need it? It should be linked already when we built v8
+ mod.link_libcpp = true;
+ },
+ else => return error.OsNotSupported,
+ }
+
+ const lib_path = try std.fmt.allocPrint(
+ mod.owner.allocator,
+ "v8/build/{s}-{s}/{s}/ninja/obj/zig/libc_v8.a",
+ .{ @tagName(arch), @tagName(os), mode_str },
);
- step.root_module.addImport("jsruntime", jsruntimemod);
-
- const netsurf = try moduleNetSurf(b, target);
- netsurf.addImport("jsruntime", jsruntimemod);
- step.root_module.addImport("netsurf", netsurf);
-
- step.root_module.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
+ mod.addObjectFile(mod.owner.path(lib_path));
+ mod.addImport("build_info", opts.createModule());
}
-fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
- const mod = b.addModule("netsurf", .{
- .root_source_file = b.path("src/netsurf/netsurf.zig"),
- .target = target,
- });
-
+fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build.ResolvedTarget) !void {
const os = target.result.os.tag;
const arch = target.result.cpu.arch;
// iconv
const libiconv_lib_path = try std.fmt.allocPrint(
- mod.owner.allocator,
+ b.allocator,
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
.{ @tagName(os), @tagName(arch) },
);
const libiconv_include_path = try std.fmt.allocPrint(
- mod.owner.allocator,
+ b.allocator,
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
.{ @tagName(os), @tagName(arch) },
);
- mod.addObjectFile(b.path(libiconv_lib_path));
- mod.addIncludePath(b.path(libiconv_include_path));
+ step.addObjectFile(b.path(libiconv_lib_path));
+ step.addIncludePath(b.path(libiconv_include_path));
- // mimalloc
- mod.addImport("mimalloc", (try moduleMimalloc(b, target)));
+ {
+ // mimalloc
+ const mimalloc = "vendor/mimalloc";
+ const lib_path = try std.fmt.allocPrint(
+ b.allocator,
+ mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
+ .{ @tagName(os), @tagName(arch) },
+ );
+ step.addObjectFile(b.path(lib_path));
+ step.addIncludePath(b.path(mimalloc ++ "/include"));
+ }
// netsurf libs
const ns = "vendor/netsurf";
const ns_include_path = try std.fmt.allocPrint(
- mod.owner.allocator,
+ b.allocator,
ns ++ "/out/{s}-{s}/include",
.{ @tagName(os), @tagName(arch) },
);
- mod.addIncludePath(b.path(ns_include_path));
+ step.addIncludePath(b.path(ns_include_path));
const libs: [4][]const u8 = .{
"libdom",
@@ -238,34 +230,11 @@ fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Mo
};
inline for (libs) |lib| {
const ns_lib_path = try std.fmt.allocPrint(
- mod.owner.allocator,
+ b.allocator,
ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a",
.{ @tagName(os), @tagName(arch) },
);
- mod.addObjectFile(b.path(ns_lib_path));
- mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
+ step.addObjectFile(b.path(ns_lib_path));
+ step.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
}
-
- return mod;
-}
-
-fn moduleMimalloc(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
- const mod = b.addModule("mimalloc", .{
- .root_source_file = b.path("src/mimalloc/mimalloc.zig"),
- .target = target,
- });
-
- const os = target.result.os.tag;
- const arch = target.result.cpu.arch;
-
- const mimalloc = "vendor/mimalloc";
- const lib_path = try std.fmt.allocPrint(
- mod.owner.allocator,
- mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
- .{ @tagName(os), @tagName(arch) },
- );
- mod.addObjectFile(b.path(lib_path));
- mod.addIncludePath(b.path(mimalloc ++ "/include"));
-
- return mod;
}
diff --git a/build.zig.zon b/build.zig.zon
index 7a3981e4..757b2e28 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -6,7 +6,17 @@
.dependencies = .{
.tls = .{
.url = "https://github.com/ianic/tls.zig/archive/b29a8b45fc59fc2d202769c4f54509bb9e17d0a2.tar.gz",
- .hash = "1220e6fd39920dd6e28b2bc06688787a39430f8856f0597cd77c44ca868c6c54fb86",
+ .hash = "tls-0.1.0-ER2e0uAxBQDm_TmSDdbiiyvAZoh4ejlDD4hW8Fl813xE",
},
+ .tigerbeetle_io = .{
+ .url = "https://github.com/lightpanda-io/tigerbeetle-io/archive/61d9652f1a957b7f4db723ea6aa0ce9635e840ce.tar.gz",
+ .hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
+ },
+ .v8 = .{
+ .url = "https://github.com/karlseguin/zig-v8-fork/archive/e5f1c0c9f1ed147617427f22cdaf11df4ab60b79.tar.gz",
+ .hash = "v8-0.0.0-xddH61vYIACI2pT1t-dUbXm18cHAKy-KWT_Qft4sBwam",
+ },
+ //.v8 = .{ .path = "../zig-v8-fork" },
+ //.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
},
}
diff --git a/src/apiweb.zig b/src/apiweb.zig
deleted file mode 100644
index 8df1bd6d..00000000
--- a/src/apiweb.zig
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
-//
-// Francis Bouvier
-// Pierre Tachoire
-//
-// 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 .
-
-const generate = @import("generate.zig");
-
-const Console = @import("jsruntime").Console;
-
-const DOM = @import("dom/dom.zig");
-const HTML = @import("html/html.zig");
-const Events = @import("events/event.zig");
-const XHR = @import("xhr/xhr.zig");
-const Storage = @import("storage/storage.zig");
-const URL = @import("url/url.zig");
-const Iterators = @import("iterator/iterator.zig");
-const XMLSerializer = @import("xmlserializer/xmlserializer.zig");
-
-pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
-
-// Interfaces
-pub const Interfaces = generate.Tuple(.{
- Console,
- DOM.Interfaces,
- Events.Interfaces,
- HTML.Interfaces,
- XHR.Interfaces,
- Storage.Interfaces,
- URL.Interfaces,
- Iterators.Interfaces,
- XMLSerializer.Interfaces,
-}){};
-
-pub const UserContext = @import("user_context.zig").UserContext;
diff --git a/src/app.zig b/src/app.zig
index 5c90b546..6bb883f9 100644
--- a/src/app.zig
+++ b/src/app.zig
@@ -1,7 +1,8 @@
const std = @import("std");
-
-const Loop = @import("jsruntime").Loop;
const Allocator = std.mem.Allocator;
+
+const js = @import("runtime/js.zig");
+const Loop = @import("runtime/loop.zig").Loop;
const HttpClient = @import("http/client.zig").Client;
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
@@ -11,6 +12,7 @@ const log = std.log.scoped(.app);
// might need.
pub const App = struct {
loop: *Loop,
+ config: Config,
allocator: Allocator,
telemetry: Telemetry,
http_client: HttpClient,
@@ -24,8 +26,9 @@ pub const App = struct {
};
pub const Config = struct {
- tls_verify_host: bool = true,
run_mode: RunMode,
+ gc_hints: bool = false,
+ tls_verify_host: bool = true,
};
pub fn init(allocator: Allocator, config: Config) !*App {
@@ -48,6 +51,7 @@ pub const App = struct {
.http_client = try HttpClient.init(allocator, 5, .{
.tls_verify_host = config.tls_verify_host,
}),
+ .config = config,
};
app.telemetry = Telemetry.init(app, config.run_mode);
diff --git a/src/browser/browser.zig b/src/browser/browser.zig
index 8181e350..679fdc52 100644
--- a/src/browser/browser.zig
+++ b/src/browser/browser.zig
@@ -21,31 +21,26 @@ const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
-const Types = @import("root").Types;
-
-const parser = @import("netsurf");
const Dump = @import("dump.zig");
const Mime = @import("mime.zig").Mime;
+const parser = @import("netsurf.zig");
-const jsruntime = @import("jsruntime");
-const Loop = jsruntime.Loop;
-const Env = jsruntime.Env;
-const Module = jsruntime.Module;
+const Window = @import("html/window.zig").Window;
+const Walker = @import("dom/walker.zig").WalkerDepthFirst;
+const Env = @import("env.zig").Env;
const App = @import("../app.zig").App;
-const apiweb = @import("../apiweb.zig");
-
-const Window = @import("../html/window.zig").Window;
-const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
const URL = @import("../url.zig").URL;
-const storage = @import("../storage/storage.zig");
-const Notification = @import("../notification.zig").Notification;
const http = @import("../http/client.zig");
-const UserContext = @import("../user_context.zig").UserContext;
+const storage = @import("storage/storage.zig");
+const Loop = @import("../runtime/loop.zig").Loop;
+const SessionState = @import("env.zig").SessionState;
+const HttpClient = @import("../http/client.zig").Client;
+const Notification = @import("../notification.zig").Notification;
-const polyfill = @import("../polyfill/polyfill.zig");
+const polyfill = @import("polyfill/polyfill.zig");
const log = std.log.scoped(.browser);
@@ -56,6 +51,7 @@ pub const user_agent = "Lightpanda/1.0";
// A browser contains only one session.
// TODO allow multiple sessions per browser.
pub const Browser = struct {
+ env: *Env,
app: *App,
session: ?*Session,
allocator: Allocator,
@@ -65,10 +61,17 @@ pub const Browser = struct {
const SessionPool = std.heap.MemoryPool(Session);
- pub fn init(app: *App) Browser {
+ pub fn init(app: *App) !Browser {
const allocator = app.allocator;
+
+ const env = try Env.init(allocator, .{
+ .gc_hints = app.config.gc_hints,
+ });
+ errdefer env.deinit();
+
return .{
.app = app,
+ .env = env,
.session = null,
.allocator = allocator,
.http_client = &app.http_client,
@@ -79,6 +82,7 @@ pub const Browser = struct {
pub fn deinit(self: *Browser) void {
self.closeSession();
+ self.env.deinit();
self.session_pool.deinit();
self.page_arena.deinit();
}
@@ -101,10 +105,7 @@ pub const Browser = struct {
}
pub fn runMicrotasks(self: *const Browser) void {
- // if no session exists, there is nothing to do.
- if (self.session == null) return;
-
- return self.session.?.env.runMicrotasks();
+ return self.env.runMicrotasks();
}
};
@@ -113,8 +114,11 @@ pub const Browser = struct {
// You can create successively multiple pages for a session, but you must
// deinit a page before running another one.
pub const Session = struct {
- app: *App,
+ state: SessionState,
+ executor: *Env.Executor,
+ inspector: Env.Inspector,
+ app: *App,
browser: *Browser,
// The arena is used only to bound the js env init b/c it leaks memory.
@@ -124,9 +128,6 @@ pub const Session = struct {
// all others Session deps use directly self.alloc and not the arena.
arena: std.heap.ArenaAllocator,
- env: Env,
- inspector: jsruntime.Inspector,
-
window: Window,
// TODO move the shed/jar to the browser?
@@ -136,8 +137,6 @@ pub const Session = struct {
page: ?Page = null,
http_client: *http.Client,
- jstypes: [Types.len]usize = undefined,
-
// recipient of notification, passed as the first parameter to notify
notify_ctx: *anyopaque,
notify_func: *const fn (ctx: *anyopaque, notification: *const Notification) anyerror!void,
@@ -159,48 +158,59 @@ pub const Session = struct {
const allocator = app.allocator;
self.* = .{
.app = app,
- .env = undefined,
.browser = browser,
.notify_ctx = any_ctx,
.inspector = undefined,
.notify_func = ContextStruct.notify,
.http_client = browser.http_client,
+ .executor = undefined,
.storage_shed = storage.Shed.init(allocator),
.arena = std.heap.ArenaAllocator.init(allocator),
.cookie_jar = storage.CookieJar.init(allocator),
.window = Window.create(null, .{ .agent = user_agent }),
+ .state = .{
+ .loop = app.loop,
+ .document = null,
+ .http_client = browser.http_client,
+
+ // we'll set this immediately after
+ .cookie_jar = undefined,
+
+ // nothing should be used on the state until we have a page
+ // at which point we'll set these fields
+ .renderer = undefined,
+ .url = undefined,
+ .arena = undefined,
+ },
};
+ self.state.cookie_jar = &self.cookie_jar;
+ errdefer self.arena.deinit();
- const arena = self.arena.allocator();
- Env.init(&self.env, arena, app.loop, null);
- errdefer self.env.deinit();
- try self.env.load(&self.jstypes);
+ self.executor = try browser.env.startExecutor(Window, &self.state, self);
+ errdefer browser.env.stopExecutor(self.executor);
+ self.inspector = try Env.Inspector.init(self.arena.allocator(), self.executor, ctx);
- // const ctx_opaque = @as(*anyopaque, @ptrCast(ctx));
- self.inspector = try jsruntime.Inspector.init(
- arena,
- &self.env,
- any_ctx,
- ContextStruct.onInspectorResponse,
- ContextStruct.onInspectorEvent,
- );
- self.env.setInspector(self.inspector);
- try self.env.setModuleLoadFn(self, Session.fetchModule);
+ self.microtaskLoop();
}
fn deinit(self: *Session) void {
+ self.app.loop.resetZig();
if (self.page != null) {
self.removePage();
}
- self.env.deinit();
+ self.inspector.deinit();
self.arena.deinit();
self.cookie_jar.deinit();
self.storage_shed.deinit();
+ self.browser.env.stopExecutor(self.executor);
}
- fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module {
- _ = referrer;
+ fn microtaskLoop(self: *Session) void {
+ self.browser.runMicrotasks();
+ self.app.loop.zigTimeout(1 * std.time.ns_per_ms, *Session, self, microtaskLoop);
+ }
+ pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
const self: *Session = @ptrCast(@alignCast(ctx));
const page = &(self.page orelse return error.NoPage);
@@ -209,16 +219,15 @@ pub const Session = struct {
// Use the page_arena for this, which has a more appropriate lifetime
// and which has more retained memory between sessions and pages.
const arena = self.browser.page_arena.allocator();
- const body = try page.fetchData(
+ return try page.fetchData(
arena,
specifier,
if (page.current_script) |s| s.src else null,
);
- return self.env.compileModule(body, specifier);
}
- pub fn callInspector(self: *Session, msg: []const u8) void {
- self.inspector.send(self.env, msg);
+ pub fn callInspector(self: *const Session, msg: []const u8) void {
+ self.inspector.send(msg);
}
// NOTE: the caller is not the owner of the returned value,
@@ -232,19 +241,14 @@ pub const Session = struct {
const page = &self.page.?;
// start JS env
- log.debug("start js env", .{});
- try self.env.start();
+ log.debug("start new js scope", .{});
+ self.state.arena = self.browser.page_arena.allocator();
+ errdefer self.state.arena = undefined;
- if (comptime builtin.is_test == false) {
- // By not loading this during tests, we aren't required to load
- // all of the interfaces into zig-js-runtime.
- log.debug("setup global env", .{});
- try self.env.bindGlobal(&self.window);
- }
+ try self.executor.startScope(&self.window);
// load polyfills
- // TODO: change to 'env' when https://github.com/lightpanda-io/zig-js-runtime/pull/285 lands
- try polyfill.load(self.arena.allocator(), &self.env);
+ try polyfill.load(self.arena.allocator(), self.executor);
// inspector
self.contextCreated(page, aux_data);
@@ -254,11 +258,10 @@ pub const Session = struct {
pub fn removePage(self: *Session) void {
std.debug.assert(self.page != null);
-
// Reset all existing callbacks.
self.app.loop.resetJS();
+ self.executor.endScope();
- self.env.stop();
// TODO unload document: https://html.spec.whatwg.org/#unloading-documents
self.window.replaceLocation(.{ .url = null }) catch |e| {
@@ -267,6 +270,7 @@ pub const Session = struct {
// clear netsurf memory arena.
parser.deinit();
+ self.state.arena = undefined;
self.page = null;
}
@@ -277,7 +281,7 @@ pub const Session = struct {
fn contextCreated(self: *Session, page: *Page, aux_data: ?[]const u8) void {
log.debug("inspector context created", .{});
- self.inspector.contextCreated(&self.env, "", (page.origin() catch "://") orelse "://", aux_data);
+ self.inspector.contextCreated(self.executor, "", (page.origin() catch "://") orelse "://", aux_data);
}
fn notify(self: *const Session, notification: *const Notification) void {
@@ -332,19 +336,16 @@ pub const Page = struct {
pub fn wait(self: *Page) !void {
// try catch
- var try_catch: jsruntime.TryCatch = undefined;
- try_catch.init(&self.session.env);
+ var try_catch: Env.TryCatch = undefined;
+ try_catch.init(self.session.executor);
defer try_catch.deinit();
- self.session.env.wait() catch |err| {
- // the js env could not be started if the document wasn't an HTML.
- if (err == error.EnvNotStarted) return;
-
- const arena = self.arena;
- if (try try_catch.err(arena, &self.session.env)) |msg| {
- defer arena.free(msg);
+ self.session.app.loop.run() catch |err| {
+ if (try try_catch.err(self.arena)) |msg| {
log.info("wait error: {s}", .{msg});
return;
+ } else {
+ log.info("wait error: {any}", .{err});
}
};
log.debug("wait: OK", .{});
@@ -397,7 +398,7 @@ pub const Page = struct {
try session.cookie_jar.populateFromResponse(&url.uri, &header);
// TODO handle fragment in url.
- try self.session.window.replaceLocation(.{ .url = try url.toWebApi(arena) });
+ try session.window.replaceLocation(.{ .url = try url.toWebApi(arena) });
log.info("GET {any} {d}", .{ url, header.status });
@@ -414,7 +415,6 @@ pub const Page = struct {
log.debug("header content-type: {s}", .{ct});
var mime = try Mime.parse(arena, ct);
- defer mime.deinit();
if (mime.isHTML()) {
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8", aux_data);
@@ -485,7 +485,7 @@ pub const Page = struct {
// https://html.spec.whatwg.org/#reporting-document-loading-status
// inject the URL to the document including the fragment.
- try parser.documentSetDocumentURI(doc, if (self.url) |*url| url.raw else "about:blank");
+ try parser.documentSetDocumentURI(doc, self.url.?.raw);
const session = self.session;
// TODO set the referrer to the document.
@@ -499,14 +499,13 @@ pub const Page = struct {
// inspector
session.contextCreated(self, aux_data);
- // replace the user context document with the new one.
- try session.env.setUserContext(.{
- .url = @ptrCast(&self.url.?),
- .document = html_doc,
- .renderer = @ptrCast(&self.renderer),
- .cookie_jar = @ptrCast(&self.session.cookie_jar),
- .http_client = @ptrCast(self.session.http_client),
- });
+ {
+ // update the sessions state
+ const state = &session.state;
+ state.url = &self.url.?;
+ state.document = html_doc;
+ state.renderer = &self.renderer;
+ }
// browse the DOM tree to retrieve scripts
// TODO execute the synchronous scripts during the HTL parsing.
@@ -643,7 +642,7 @@ pub const Page = struct {
// TODO handle charset attribute
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
if (opt_text) |text| {
- try s.eval(self.arena, &self.session.env, text);
+ try s.eval(self.arena, self.session, text);
return;
}
@@ -711,7 +710,7 @@ pub const Page = struct {
fn fetchScript(self: *const Page, s: *const Script) !void {
const arena = self.arena;
const body = try self.fetchData(arena, s.src, null);
- try s.eval(arena, &self.session.env, body);
+ try s.eval(arena, self.session, body);
}
fn newHTTPRequest(self: *const Page, method: http.Request.Method, url: *const URL, opts: storage.cookie.LookupOpts) !http.Request {
@@ -769,24 +768,24 @@ pub const Page = struct {
return .unknown;
}
- fn eval(self: Script, arena: Allocator, env: *const Env, body: []const u8) !void {
- var try_catch: jsruntime.TryCatch = undefined;
- try_catch.init(env);
+ fn eval(self: Script, arena: Allocator, session: *Session, body: []const u8) !void {
+ var try_catch: Env.TryCatch = undefined;
+ try_catch.init(session.executor);
defer try_catch.deinit();
const res = switch (self.kind) {
.unknown => return error.UnknownScript,
- .javascript => env.exec(body, self.src),
- .module => env.module(body, self.src),
+ .javascript => session.executor.exec(body, self.src),
+ .module => session.executor.module(body, self.src),
} catch {
- if (try try_catch.err(arena, env)) |msg| {
+ if (try try_catch.err(arena)) |msg| {
log.info("eval script {s}: {s}", .{ self.src, msg });
}
return FetchError.JsErr;
};
if (builtin.mode == .Debug) {
- const msg = try res.toString(arena, env);
+ const msg = try res.toString(arena);
log.debug("eval script {s}: {s}", .{ self.src, msg });
}
}
@@ -810,7 +809,7 @@ const FlatRenderer = struct {
// given an index, get the element
elements: std.ArrayListUnmanaged(u64),
- const Element = @import("../dom/element.zig").Element;
+ const Element = @import("dom/element.zig").Element;
// we expect allocator to be an arena
pub fn init(allocator: Allocator) FlatRenderer {
diff --git a/src/browser/console/console.zig b/src/browser/console/console.zig
new file mode 100644
index 00000000..59f56594
--- /dev/null
+++ b/src/browser/console/console.zig
@@ -0,0 +1,28 @@
+// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+const log = std.log.scoped(.console);
+
+pub const Console = struct {
+ // TODO: configurable writer
+
+ pub fn _log(_: *const Console, str: []const u8) void {
+ log.debug("{s}\n", .{str});
+ }
+};
diff --git a/src/css/README.md b/src/browser/css/README.md
similarity index 100%
rename from src/css/README.md
rename to src/browser/css/README.md
diff --git a/src/css/css.zig b/src/browser/css/css.zig
similarity index 100%
rename from src/css/css.zig
rename to src/browser/css/css.zig
diff --git a/src/css/libdom.zig b/src/browser/css/libdom.zig
similarity index 98%
rename from src/css/libdom.zig
rename to src/browser/css/libdom.zig
index 21333726..44307c63 100644
--- a/src/css/libdom.zig
+++ b/src/browser/css/libdom.zig
@@ -18,7 +18,7 @@
const std = @import("std");
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
// Node implementation with Netsurf Libdom C lib.
pub const Node = struct {
diff --git a/src/css/libdom_test.zig b/src/browser/css/libdom_test.zig
similarity index 99%
rename from src/css/libdom_test.zig
rename to src/browser/css/libdom_test.zig
index c0cdbb3f..4cd267e0 100644
--- a/src/css/libdom_test.zig
+++ b/src/browser/css/libdom_test.zig
@@ -19,7 +19,7 @@
const std = @import("std");
const css = @import("css.zig");
const Node = @import("libdom.zig").Node;
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
const Matcher = struct {
const Nodes = std.ArrayList(Node);
diff --git a/src/css/match_test.zig b/src/browser/css/match_test.zig
similarity index 100%
rename from src/css/match_test.zig
rename to src/browser/css/match_test.zig
diff --git a/src/css/parser.zig b/src/browser/css/parser.zig
similarity index 100%
rename from src/css/parser.zig
rename to src/browser/css/parser.zig
diff --git a/src/css/selector.zig b/src/browser/css/selector.zig
similarity index 100%
rename from src/css/selector.zig
rename to src/browser/css/selector.zig
diff --git a/src/dom/attribute.zig b/src/browser/dom/attribute.zig
similarity index 62%
rename from src/dom/attribute.zig
rename to src/browser/dom/attribute.zig
index d85f87a0..7153a509 100644
--- a/src/dom/attribute.zig
+++ b/src/browser/dom/attribute.zig
@@ -18,11 +18,7 @@
const std = @import("std");
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
const Node = @import("node.zig").Node;
const DOMException = @import("exceptions.zig").DOMException;
@@ -31,7 +27,6 @@ const DOMException = @import("exceptions.zig").DOMException;
pub const Attr = struct {
pub const Self = parser.Attribute;
pub const prototype = *Node;
- pub const mem_guarantied = true;
pub fn get_namespaceURI(self: *parser.Attribute) !?[]const u8 {
return try parser.nodeGetNamespace(parser.attributeToNode(self));
@@ -70,34 +65,33 @@ pub const Attr = struct {
// Tests
// -----
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var getters = [_]Case{
- .{ .src = "let a = document.createAttributeNS('foo', 'bar')", .ex = "undefined" },
- .{ .src = "a.namespaceURI", .ex = "foo" },
- .{ .src = "a.prefix", .ex = "null" },
- .{ .src = "a.localName", .ex = "bar" },
- .{ .src = "a.name", .ex = "bar" },
- .{ .src = "a.value", .ex = "" },
+const testing = @import("../../testing.zig");
+test "Browser.DOM.Attribute" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
+
+ try runner.testCases(&.{
+ .{ "let a = document.createAttributeNS('foo', 'bar')", "undefined" },
+ .{ "a.namespaceURI", "foo" },
+ .{ "a.prefix", "null" },
+ .{ "a.localName", "bar" },
+ .{ "a.name", "bar" },
+ .{ "a.value", "" },
// TODO: libdom has a bug here: the created attr has no parent, it
// causes a panic w/ libdom when setting the value.
- //.{ .src = "a.value = 'nok'", .ex = "nok" },
- .{ .src = "a.ownerElement", .ex = "null" },
- };
- try checkCases(js_env, &getters);
+ //.{ "a.value = 'nok'", "nok" },
+ .{ "a.ownerElement", "null" },
+ }, .{});
- var attr = [_]Case{
- .{ .src = "let b = document.getElementById('link').getAttributeNode('class')", .ex = "undefined" },
- .{ .src = "b.name", .ex = "class" },
- .{ .src = "b.value", .ex = "ok" },
- .{ .src = "b.value = 'nok'", .ex = "nok" },
- .{ .src = "b.value", .ex = "nok" },
- .{ .src = "b.value = null", .ex = "null" },
- .{ .src = "b.value", .ex = "null" },
- .{ .src = "b.value = 'ok'", .ex = "ok" },
- .{ .src = "b.ownerElement.id", .ex = "link" },
- };
- try checkCases(js_env, &attr);
+ try runner.testCases(&.{
+ .{ "let b = document.getElementById('link').getAttributeNode('class')", "undefined" },
+ .{ "b.name", "class" },
+ .{ "b.value", "ok" },
+ .{ "b.value = 'nok'", "nok" },
+ .{ "b.value", "nok" },
+ .{ "b.value = null", "null" },
+ .{ "b.value", "null" },
+ .{ "b.value = 'ok'", "ok" },
+ .{ "b.ownerElement.id", "link" },
+ }, .{});
}
diff --git a/src/dom/cdata_section.zig b/src/browser/dom/cdata_section.zig
similarity index 93%
rename from src/dom/cdata_section.zig
rename to src/browser/dom/cdata_section.zig
index c8ff6107..51017b2e 100644
--- a/src/dom/cdata_section.zig
+++ b/src/browser/dom/cdata_section.zig
@@ -18,7 +18,7 @@
const std = @import("std");
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
const Text = @import("text.zig").Text;
@@ -26,5 +26,4 @@ const Text = @import("text.zig").Text;
pub const CDATASection = struct {
pub const Self = parser.CDATASection;
pub const prototype = *Text;
- pub const mem_guarantied = true;
};
diff --git a/src/dom/character_data.zig b/src/browser/dom/character_data.zig
similarity index 53%
rename from src/dom/character_data.zig
rename to src/browser/dom/character_data.zig
index 372395c2..fd4d4bec 100644
--- a/src/dom/character_data.zig
+++ b/src/browser/dom/character_data.zig
@@ -18,11 +18,7 @@
const std = @import("std");
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
const Node = @import("node.zig").Node;
const Comment = @import("comment.zig").Comment;
@@ -42,7 +38,6 @@ pub const Interfaces = .{
pub const CharacterData = struct {
pub const Self = parser.CharacterData;
pub const prototype = *Node;
- pub const mem_guarantied = true;
// JS funcs
// --------
@@ -106,74 +101,65 @@ pub const CharacterData = struct {
// Tests
// -----
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var get_data = [_]Case{
- .{ .src = "let link = document.getElementById('link')", .ex = "undefined" },
- .{ .src = "let cdata = link.firstChild", .ex = "undefined" },
- .{ .src = "cdata.data", .ex = "OK" },
- };
- try checkCases(js_env, &get_data);
+const testing = @import("../../testing.zig");
+test "Browser.DOM.CharacterData" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
- var set_data = [_]Case{
- .{ .src = "cdata.data = 'OK modified'", .ex = "OK modified" },
- .{ .src = "cdata.data === 'OK modified'", .ex = "true" },
- .{ .src = "cdata.data = 'OK'", .ex = "OK" },
- };
- try checkCases(js_env, &set_data);
+ try runner.testCases(&.{
+ .{ "let link = document.getElementById('link')", "undefined" },
+ .{ "let cdata = link.firstChild", "undefined" },
+ .{ "cdata.data", "OK" },
+ }, .{});
- var get_length = [_]Case{
- .{ .src = "cdata.length === 2", .ex = "true" },
- };
- try checkCases(js_env, &get_length);
+ try runner.testCases(&.{
+ .{ "cdata.data = 'OK modified'", "OK modified" },
+ .{ "cdata.data === 'OK modified'", "true" },
+ .{ "cdata.data = 'OK'", "OK" },
+ }, .{});
- var get_next_elem_sibling = [_]Case{
- .{ .src = "cdata.nextElementSibling === null", .ex = "true" },
+ try runner.testCases(&.{
+ .{ "cdata.length === 2", "true" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "cdata.nextElementSibling === null", "true" },
// create a next element
- .{ .src = "let next = document.createElement('a')", .ex = "undefined" },
- .{ .src = "link.appendChild(next, cdata) !== undefined", .ex = "true" },
- .{ .src = "cdata.nextElementSibling.localName === 'a' ", .ex = "true" },
- };
- try checkCases(js_env, &get_next_elem_sibling);
+ .{ "let next = document.createElement('a')", "undefined" },
+ .{ "link.appendChild(next, cdata) !== undefined", "true" },
+ .{ "cdata.nextElementSibling.localName === 'a' ", "true" },
+ }, .{});
- var get_prev_elem_sibling = [_]Case{
- .{ .src = "cdata.previousElementSibling === null", .ex = "true" },
+ try runner.testCases(&.{
+ .{ "cdata.previousElementSibling === null", "true" },
// create a prev element
- .{ .src = "let prev = document.createElement('div')", .ex = "undefined" },
- .{ .src = "link.insertBefore(prev, cdata) !== undefined", .ex = "true" },
- .{ .src = "cdata.previousElementSibling.localName === 'div' ", .ex = "true" },
- };
- try checkCases(js_env, &get_prev_elem_sibling);
+ .{ "let prev = document.createElement('div')", "undefined" },
+ .{ "link.insertBefore(prev, cdata) !== undefined", "true" },
+ .{ "cdata.previousElementSibling.localName === 'div' ", "true" },
+ }, .{});
- var append_data = [_]Case{
- .{ .src = "cdata.appendData(' modified')", .ex = "undefined" },
- .{ .src = "cdata.data === 'OK modified' ", .ex = "true" },
- };
- try checkCases(js_env, &append_data);
+ try runner.testCases(&.{
+ .{ "cdata.appendData(' modified')", "undefined" },
+ .{ "cdata.data === 'OK modified' ", "true" },
+ }, .{});
- var delete_data = [_]Case{
- .{ .src = "cdata.deleteData('OK'.length, ' modified'.length)", .ex = "undefined" },
- .{ .src = "cdata.data == 'OK'", .ex = "true" },
- };
- try checkCases(js_env, &delete_data);
+ try runner.testCases(&.{
+ .{ "cdata.deleteData('OK'.length, ' modified'.length)", "undefined" },
+ .{ "cdata.data == 'OK'", "true" },
+ }, .{});
- var insert_data = [_]Case{
- .{ .src = "cdata.insertData('OK'.length-1, 'modified')", .ex = "undefined" },
- .{ .src = "cdata.data == 'OmodifiedK'", .ex = "true" },
- };
- try checkCases(js_env, &insert_data);
+ try runner.testCases(&.{
+ .{ "cdata.insertData('OK'.length-1, 'modified')", "undefined" },
+ .{ "cdata.data == 'OmodifiedK'", "true" },
+ }, .{});
- var replace_data = [_]Case{
- .{ .src = "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", .ex = "undefined" },
- .{ .src = "cdata.data == 'OreplacedK'", .ex = "true" },
- };
- try checkCases(js_env, &replace_data);
+ try runner.testCases(&.{
+ .{ "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", "undefined" },
+ .{ "cdata.data == 'OreplacedK'", "true" },
+ }, .{});
- var substring_data = [_]Case{
- .{ .src = "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", .ex = "true" },
- .{ .src = "cdata.substringData('OK'.length-1, 0) == ''", .ex = "true" },
- };
- try checkCases(js_env, &substring_data);
+ try runner.testCases(&.{
+ .{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
+ .{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
+ }, .{});
}
diff --git a/src/dom/comment.zig b/src/browser/dom/comment.zig
similarity index 58%
rename from src/dom/comment.zig
rename to src/browser/dom/comment.zig
index fe4111bc..25734253 100644
--- a/src/dom/comment.zig
+++ b/src/browser/dom/comment.zig
@@ -17,25 +17,20 @@
// along with this program. If not, see .
const std = @import("std");
-const parser = @import("netsurf");
-
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
+const parser = @import("../netsurf.zig");
const CharacterData = @import("character_data.zig").CharacterData;
-const UserContext = @import("../user_context.zig").UserContext;
+const SessionState = @import("../env.zig").SessionState;
// https://dom.spec.whatwg.org/#interface-comment
pub const Comment = struct {
pub const Self = parser.Comment;
pub const prototype = *CharacterData;
- pub const mem_guarantied = true;
- pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Comment {
+ pub fn constructor(data: ?[]const u8, state: *const SessionState) !*parser.Comment {
return parser.documentCreateComment(
- parser.documentHTMLToDocument(userctx.document),
+ parser.documentHTMLToDocument(state.document.?),
data orelse "",
);
}
@@ -44,16 +39,16 @@ pub const Comment = struct {
// Tests
// -----
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var constructor = [_]Case{
- .{ .src = "let comment = new Comment('foo')", .ex = "undefined" },
- .{ .src = "comment.data", .ex = "foo" },
+const testing = @import("../../testing.zig");
+test "Browser.DOM.Comment" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
- .{ .src = "let emptycomment = new Comment()", .ex = "undefined" },
- .{ .src = "emptycomment.data", .ex = "" },
- };
- try checkCases(js_env, &constructor);
+ try runner.testCases(&.{
+ .{ "let comment = new Comment('foo')", "undefined" },
+ .{ "comment.data", "foo" },
+
+ .{ "let emptycomment = new Comment()", "undefined" },
+ .{ "emptycomment.data", "" },
+ }, .{});
}
diff --git a/src/dom/css.zig b/src/browser/dom/css.zig
similarity index 98%
rename from src/dom/css.zig
rename to src/browser/dom/css.zig
index 0432e83e..50c262e4 100644
--- a/src/dom/css.zig
+++ b/src/browser/dom/css.zig
@@ -18,7 +18,7 @@
const std = @import("std");
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
const css = @import("../css/css.zig");
const Node = @import("../css/libdom.zig").Node;
diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig
new file mode 100644
index 00000000..1ac56df4
--- /dev/null
+++ b/src/browser/dom/document.zig
@@ -0,0 +1,444 @@
+// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+
+const parser = @import("../netsurf.zig");
+const SessionState = @import("../env.zig").SessionState;
+
+const Node = @import("node.zig").Node;
+const NodeList = @import("nodelist.zig").NodeList;
+const NodeUnion = @import("node.zig").Union;
+
+const collection = @import("html_collection.zig");
+const css = @import("css.zig");
+
+const Element = @import("element.zig").Element;
+const ElementUnion = @import("element.zig").Union;
+
+const DocumentType = @import("document_type.zig").DocumentType;
+const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
+const DOMImplementation = @import("implementation.zig").DOMImplementation;
+
+// WEB IDL https://dom.spec.whatwg.org/#document
+pub const Document = struct {
+ pub const Self = parser.Document;
+ pub const prototype = *Node;
+
+ pub fn constructor(state: *const SessionState) !*parser.DocumentHTML {
+ const doc = try parser.documentCreateDocument(
+ try parser.documentHTMLGetTitle(state.document.?),
+ );
+
+ // we have to work w/ document instead of html document.
+ const ddoc = parser.documentHTMLToDocument(doc);
+ const ccur = parser.documentHTMLToDocument(state.document.?);
+ try parser.documentSetDocumentURI(ddoc, try parser.documentGetDocumentURI(ccur));
+ try parser.documentSetInputEncoding(ddoc, try parser.documentGetInputEncoding(ccur));
+
+ return doc;
+ }
+
+ // JS funcs
+ // --------
+ pub fn get_implementation(_: *parser.Document) DOMImplementation {
+ return DOMImplementation{};
+ }
+
+ pub fn get_documentElement(self: *parser.Document) !?ElementUnion {
+ const e = try parser.documentGetDocumentElement(self);
+ if (e == null) return null;
+ return try Element.toInterface(e.?);
+ }
+
+ pub fn get_documentURI(self: *parser.Document) ![]const u8 {
+ return try parser.documentGetDocumentURI(self);
+ }
+
+ pub fn get_URL(self: *parser.Document) ![]const u8 {
+ return try get_documentURI(self);
+ }
+
+ // TODO implement contentType
+ pub fn get_contentType(self: *parser.Document) []const u8 {
+ _ = self;
+ return "text/html";
+ }
+
+ // TODO implement compactMode
+ pub fn get_compatMode(self: *parser.Document) []const u8 {
+ _ = self;
+ return "CSS1Compat";
+ }
+
+ pub fn get_characterSet(self: *parser.Document) ![]const u8 {
+ return try parser.documentGetInputEncoding(self);
+ }
+
+ // alias of get_characterSet
+ pub fn get_charset(self: *parser.Document) ![]const u8 {
+ return try get_characterSet(self);
+ }
+
+ // alias of get_characterSet
+ pub fn get_inputEncoding(self: *parser.Document) ![]const u8 {
+ return try get_characterSet(self);
+ }
+
+ pub fn get_doctype(self: *parser.Document) !?*parser.DocumentType {
+ return try parser.documentGetDoctype(self);
+ }
+
+ pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !*parser.Event {
+ // TODO: for now only "Event" constructor is supported
+ // see table on https://dom.spec.whatwg.org/#dom-document-createevent $2
+ if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) {
+ return try parser.eventCreate();
+ }
+ return parser.DOMError.NotSupported;
+ }
+
+ pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion {
+ const e = try parser.documentGetElementById(self, id) orelse return null;
+ return try Element.toInterface(e);
+ }
+
+ pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
+ const e = try parser.documentCreateElement(self, tag_name);
+ return try Element.toInterface(e);
+ }
+
+ pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {
+ const e = try parser.documentCreateElementNS(self, ns, tag_name);
+ return try Element.toInterface(e);
+ }
+
+ // We can't simply use libdom dom_document_get_elements_by_tag_name here.
+ // Indeed, netsurf implemented a previous dom spec when
+ // getElementsByTagName returned a NodeList.
+ // But since
+ // https://github.com/whatwg/dom/commit/190700b7c12ecfd3b5ebdb359ab1d6ea9cbf7749
+ // the spec changed to return an HTMLCollection instead.
+ // That's why we reimplemented getElementsByTagName by using an
+ // HTMLCollection in zig here.
+ pub fn _getElementsByTagName(
+ self: *parser.Document,
+ tag_name: []const u8,
+ state: *SessionState,
+ ) !collection.HTMLCollection {
+ return try collection.HTMLCollectionByTagName(state.arena, parser.documentToNode(self), tag_name, true);
+ }
+
+ pub fn _getElementsByClassName(
+ self: *parser.Document,
+ classNames: []const u8,
+ state: *SessionState,
+ ) !collection.HTMLCollection {
+ const allocator = state.arena;
+ return try collection.HTMLCollectionByClassName(allocator, parser.documentToNode(self), classNames, true);
+ }
+
+ pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment {
+ return try parser.documentCreateDocumentFragment(self);
+ }
+
+ pub fn _createTextNode(self: *parser.Document, data: []const u8) !*parser.Text {
+ return try parser.documentCreateTextNode(self, data);
+ }
+
+ pub fn _createCDATASection(self: *parser.Document, data: []const u8) !*parser.CDATASection {
+ return try parser.documentCreateCDATASection(self, data);
+ }
+
+ pub fn _createComment(self: *parser.Document, data: []const u8) !*parser.Comment {
+ return try parser.documentCreateComment(self, data);
+ }
+
+ pub fn _createProcessingInstruction(self: *parser.Document, target: []const u8, data: []const u8) !*parser.ProcessingInstruction {
+ return try parser.documentCreateProcessingInstruction(self, target, data);
+ }
+
+ pub fn _importNode(self: *parser.Document, node: *parser.Node, deep: ?bool) !NodeUnion {
+ const n = try parser.documentImportNode(self, node, deep orelse false);
+ return try Node.toInterface(n);
+ }
+
+ pub fn _adoptNode(self: *parser.Document, node: *parser.Node) !NodeUnion {
+ const n = try parser.documentAdoptNode(self, node);
+ return try Node.toInterface(n);
+ }
+
+ pub fn _createAttribute(self: *parser.Document, name: []const u8) !*parser.Attribute {
+ return try parser.documentCreateAttribute(self, name);
+ }
+
+ pub fn _createAttributeNS(self: *parser.Document, ns: []const u8, qname: []const u8) !*parser.Attribute {
+ return try parser.documentCreateAttributeNS(self, ns, qname);
+ }
+
+ // ParentNode
+ // https://dom.spec.whatwg.org/#parentnode
+ pub fn get_children(self: *parser.Document) !collection.HTMLCollection {
+ return try collection.HTMLCollectionChildren(parser.documentToNode(self), false);
+ }
+
+ pub fn get_firstElementChild(self: *parser.Document) !?ElementUnion {
+ const elt = try parser.documentGetDocumentElement(self) orelse return null;
+ return try Element.toInterface(elt);
+ }
+
+ pub fn get_lastElementChild(self: *parser.Document) !?ElementUnion {
+ const elt = try parser.documentGetDocumentElement(self) orelse return null;
+ return try Element.toInterface(elt);
+ }
+
+ pub fn get_childElementCount(self: *parser.Document) !u32 {
+ _ = try parser.documentGetDocumentElement(self) orelse return 0;
+ return 1;
+ }
+
+ pub fn _querySelector(self: *parser.Document, selector: []const u8, state: *SessionState) !?ElementUnion {
+ if (selector.len == 0) return null;
+
+ const allocator = state.arena;
+ const n = try css.querySelector(allocator, parser.documentToNode(self), selector);
+
+ if (n == null) return null;
+
+ return try Element.toInterface(parser.nodeToElement(n.?));
+ }
+
+ pub fn _querySelectorAll(self: *parser.Document, selector: []const u8, state: *SessionState) !NodeList {
+ const allocator = state.arena;
+ return css.querySelectorAll(allocator, parser.documentToNode(self), selector);
+ }
+
+ // TODO according with https://dom.spec.whatwg.org/#parentnode, the
+ // function must accept either node or string.
+ // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
+ pub fn _prepend(self: *parser.Document, nodes: []const *parser.Node) !void {
+ return Node.prepend(parser.documentToNode(self), nodes);
+ }
+
+ // TODO according with https://dom.spec.whatwg.org/#parentnode, the
+ // function must accept either node or string.
+ // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
+ pub fn _append(self: *parser.Document, nodes: []const *parser.Node) !void {
+ return Node.append(parser.documentToNode(self), nodes);
+ }
+
+ // TODO according with https://dom.spec.whatwg.org/#parentnode, the
+ // function must accept either node or string.
+ // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
+ pub fn _replaceChildren(self: *parser.Document, nodes: []const *parser.Node) !void {
+ return Node.replaceChildren(parser.documentToNode(self), nodes);
+ }
+
+ pub fn deinit(_: *parser.Document, _: std.mem.Allocator) void {}
+};
+
+const testing = @import("../../testing.zig");
+test "Browser.DOM.Document" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
+
+ try runner.testCases(&.{
+ .{ "document.__proto__.__proto__.constructor.name", "Document" },
+ .{ "document.__proto__.__proto__.__proto__.constructor.name", "Node" },
+ .{ "document.__proto__.__proto__.__proto__.__proto__.constructor.name", "EventTarget" },
+
+ .{ "let newdoc = new Document()", "undefined" },
+ .{ "newdoc.documentElement", "null" },
+ .{ "newdoc.children.length", "0" },
+ .{ "newdoc.getElementsByTagName('*').length", "0" },
+ .{ "newdoc.getElementsByTagName('*').item(0)", "null" },
+ .{ "newdoc.inputEncoding === document.inputEncoding", "true" },
+ .{ "newdoc.documentURI === document.documentURI", "true" },
+ .{ "newdoc.URL === document.URL", "true" },
+ .{ "newdoc.compatMode === document.compatMode", "true" },
+ .{ "newdoc.characterSet === document.characterSet", "true" },
+ .{ "newdoc.charset === document.charset", "true" },
+ .{ "newdoc.contentType === document.contentType", "true" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "let getElementById = document.getElementById('content')", "undefined" },
+ .{ "getElementById.constructor.name", "HTMLDivElement" },
+ .{ "getElementById.localName", "div" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
+ .{ "getElementsByTagName.length", "2" },
+ .{ "getElementsByTagName.item(0).localName", "p" },
+ .{ "getElementsByTagName.item(1).localName", "p" },
+ .{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
+ .{ "getElementsByTagNameAll.length", "8" },
+ .{ "getElementsByTagNameAll.item(0).localName", "html" },
+ .{ "getElementsByTagNameAll.item(7).localName", "p" },
+ .{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "let ok = document.getElementsByClassName('ok')", "undefined" },
+ .{ "ok.length", "2" },
+ .{ "let empty = document.getElementsByClassName('empty')", "undefined" },
+ .{ "empty.length", "1" },
+ .{ "let emptyok = document.getElementsByClassName('empty ok')", "undefined" },
+ .{ "emptyok.length", "1" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "let e = document.documentElement", "undefined" },
+ .{ "e.localName", "html" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "document.characterSet", "UTF-8" },
+ .{ "document.charset", "UTF-8" },
+ .{ "document.inputEncoding", "UTF-8" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "document.compatMode", "CSS1Compat" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "document.contentType", "text/html" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "document.documentURI", "about:blank" },
+ .{ "document.URL", "about:blank" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "let impl = document.implementation", "undefined" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "let d = new Document()", "undefined" },
+ .{ "d.characterSet", "UTF-8" },
+ .{ "d.URL", "about:blank" },
+ .{ "d.documentURI", "about:blank" },
+ .{ "d.compatMode", "CSS1Compat" },
+ .{ "d.contentType", "text/html" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "var v = document.createDocumentFragment()", "undefined" },
+ .{ "v.nodeName", "#document-fragment" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "var v = document.createTextNode('foo')", "undefined" },
+ .{ "v.nodeName", "#text" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "var v = document.createCDATASection('foo')", "undefined" },
+ .{ "v.nodeName", "#cdata-section" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "var v = document.createComment('foo')", "undefined" },
+ .{ "v.nodeName", "#comment" },
+ .{ "let v2 = v.cloneNode()", "undefined" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" },
+ .{ "pi.target", "foo" },
+ .{ "let pi2 = pi.cloneNode()", "undefined" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "let nimp = document.getElementById('content')", "undefined" },
+ .{ "var v = document.importNode(nimp)", "undefined" },
+ .{ "v.nodeName", "DIV" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "var v = document.createAttribute('foo')", "undefined" },
+ .{ "v.nodeName", "foo" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "document.children.length", "1" },
+ .{ "document.children.item(0).nodeName", "HTML" },
+ .{ "document.firstElementChild.nodeName", "HTML" },
+ .{ "document.lastElementChild.nodeName", "HTML" },
+ .{ "document.childElementCount", "1" },
+
+ .{ "let nd = new Document()", "undefined" },
+ .{ "nd.children.length", "0" },
+ .{ "nd.children.item(0)", "null" },
+ .{ "nd.firstElementChild", "null" },
+ .{ "nd.lastElementChild", "null" },
+ .{ "nd.childElementCount", "0" },
+
+ .{ "let emptydoc = document.createElement('html')", "undefined" },
+ .{ "emptydoc.prepend(document.createElement('html'))", "undefined" },
+
+ .{ "let emptydoc2 = document.createElement('html')", "undefined" },
+ .{ "emptydoc2.append(document.createElement('html'))", "undefined" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "document.querySelector('')", "null" },
+ .{ "document.querySelector('*').nodeName", "HTML" },
+ .{ "document.querySelector('#content').id", "content" },
+ .{ "document.querySelector('#para').id", "para" },
+ .{ "document.querySelector('.ok').id", "link" },
+ .{ "document.querySelector('a ~ p').id", "para-empty" },
+ .{ "document.querySelector(':root').nodeName", "HTML" },
+
+ .{ "document.querySelectorAll('p').length", "2" },
+ .{
+ \\ Array.from(document.querySelectorAll('#content > p#para-empty'))
+ \\ .map(row => row.querySelector('span').textContent)
+ \\ .length;
+ ,
+ "1",
+ },
+ }, .{});
+
+ // this test breaks the doc structure, keep it at the end of the test
+ // suite.
+ try runner.testCases(&.{
+ .{ "let nadop = document.getElementById('content')", "undefined" },
+ .{ "var v = document.adoptNode(nadop)", "undefined" },
+ .{ "v.nodeName", "DIV" },
+ }, .{});
+
+ const Case = testing.JsRunner.Case;
+ const tags = comptime parser.Tag.all();
+ var createElements: [(tags.len) * 2]Case = undefined;
+ inline for (tags, 0..) |tag, i| {
+ const tag_name = @tagName(tag);
+ createElements[i * 2] = Case{
+ "var " ++ tag_name ++ "Elem = document.createElement('" ++ tag_name ++ "')",
+ "undefined",
+ };
+ createElements[(i * 2) + 1] = Case{
+ tag_name ++ "Elem.localName",
+ tag_name,
+ };
+ }
+ try runner.testCases(&createElements, .{});
+}
diff --git a/src/dom/document_fragment.zig b/src/browser/dom/document_fragment.zig
similarity index 60%
rename from src/dom/document_fragment.zig
rename to src/browser/dom/document_fragment.zig
index 574e8eb1..2ae1a8a5 100644
--- a/src/dom/document_fragment.zig
+++ b/src/browser/dom/document_fragment.zig
@@ -18,39 +18,30 @@
const std = @import("std");
-const parser = @import("netsurf");
-
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
+const parser = @import("../netsurf.zig");
+const SessionState = @import("../env.zig").SessionState;
const Node = @import("node.zig").Node;
-const UserContext = @import("../user_context.zig").UserContext;
-
// WEB IDL https://dom.spec.whatwg.org/#documentfragment
pub const DocumentFragment = struct {
pub const Self = parser.DocumentFragment;
pub const prototype = *Node;
- pub const mem_guarantied = true;
- pub fn constructor(userctx: UserContext) !*parser.DocumentFragment {
+ pub fn constructor(state: *const SessionState) !*parser.DocumentFragment {
return parser.documentCreateDocumentFragment(
- parser.documentHTMLToDocument(userctx.document),
+ parser.documentHTMLToDocument(state.document.?),
);
}
};
-// Tests
-// -----
+const testing = @import("../../testing.zig");
+test "Browser.DOM.DocumentFragment" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var constructor = [_]Case{
- .{ .src = "const dc = new DocumentFragment()", .ex = "undefined" },
- .{ .src = "dc.constructor.name", .ex = "DocumentFragment" },
- };
- try checkCases(js_env, &constructor);
+ try runner.testCases(&.{
+ .{ "const dc = new DocumentFragment()", "undefined" },
+ .{ "dc.constructor.name", "DocumentFragment" },
+ }, .{});
}
diff --git a/src/dom/document_type.zig b/src/browser/dom/document_type.zig
similarity index 95%
rename from src/dom/document_type.zig
rename to src/browser/dom/document_type.zig
index cd40a732..0749b8bc 100644
--- a/src/dom/document_type.zig
+++ b/src/browser/dom/document_type.zig
@@ -18,7 +18,7 @@
const std = @import("std");
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
const Node = @import("node.zig").Node;
@@ -26,7 +26,6 @@ const Node = @import("node.zig").Node;
pub const DocumentType = struct {
pub const Self = parser.DocumentType;
pub const prototype = *Node;
- pub const mem_guarantied = true;
pub fn get_name(self: *parser.DocumentType) ![]const u8 {
return try parser.documentTypeGetName(self);
diff --git a/src/dom/dom.zig b/src/browser/dom/dom.zig
similarity index 95%
rename from src/dom/dom.zig
rename to src/browser/dom/dom.zig
index 76a4a185..07e57c06 100644
--- a/src/dom/dom.zig
+++ b/src/browser/dom/dom.zig
@@ -22,7 +22,7 @@ const DOMImplementation = @import("implementation.zig").DOMImplementation;
const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
const DOMTokenList = @import("token_list.zig").DOMTokenList;
const NodeList = @import("nodelist.zig");
-const Nod = @import("node.zig");
+const Node = @import("node.zig");
const MutationObserver = @import("mutation_observer.zig");
pub const Interfaces = .{
@@ -32,7 +32,7 @@ pub const Interfaces = .{
NamedNodeMap,
DOMTokenList,
NodeList.Interfaces,
- Nod.Node,
- Nod.Interfaces,
+ Node.Node,
+ Node.Interfaces,
MutationObserver.Interfaces,
};
diff --git a/src/dom/element.zig b/src/browser/dom/element.zig
similarity index 54%
rename from src/dom/element.zig
rename to src/browser/dom/element.zig
index 0453a283..ffd937e9 100644
--- a/src/dom/element.zig
+++ b/src/browser/dom/element.zig
@@ -18,22 +18,17 @@
const std = @import("std");
-const parser = @import("netsurf");
-
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-const Variadic = jsruntime.Variadic;
+const parser = @import("../netsurf.zig");
+const SessionState = @import("../env.zig").SessionState;
const collection = @import("html_collection.zig");
-const dump = @import("../browser/dump.zig");
+const dump = @import("../dump.zig");
const css = @import("css.zig");
const Node = @import("node.zig").Node;
const Walker = @import("walker.zig").WalkerDepthFirst;
const NodeList = @import("nodelist.zig").NodeList;
const HTMLElem = @import("../html/elements.zig");
-const UserContext = @import("../user_context.zig").UserContext;
pub const Union = @import("../html/elements.zig").Union;
const DOMException = @import("exceptions.zig").DOMException;
@@ -42,7 +37,6 @@ const DOMException = @import("exceptions.zig").DOMException;
pub const Element = struct {
pub const Self = parser.Element;
pub const prototype = *Node;
- pub const mem_guarantied = true;
pub const DOMRect = struct {
x: f64,
@@ -106,8 +100,8 @@ pub const Element = struct {
return try parser.nodeGetAttributes(parser.elementToNode(self));
}
- pub fn get_innerHTML(self: *parser.Element, alloc: std.mem.Allocator) ![]const u8 {
- var buf = std.ArrayList(u8).init(alloc);
+ pub fn get_innerHTML(self: *parser.Element, state: *SessionState) ![]const u8 {
+ var buf = std.ArrayList(u8).init(state.arena);
defer buf.deinit();
try dump.writeChildren(parser.elementToNode(self), buf.writer());
@@ -116,8 +110,8 @@ pub const Element = struct {
return buf.toOwnedSlice();
}
- pub fn get_outerHTML(self: *parser.Element, alloc: std.mem.Allocator) ![]const u8 {
- var buf = std.ArrayList(u8).init(alloc);
+ pub fn get_outerHTML(self: *parser.Element, state: *SessionState) ![]const u8 {
+ var buf = std.ArrayList(u8).init(state.arena);
defer buf.deinit();
try dump.writeNode(parser.elementToNode(self), buf.writer());
@@ -232,11 +226,11 @@ pub const Element = struct {
pub fn _getElementsByTagName(
self: *parser.Element,
- alloc: std.mem.Allocator,
tag_name: []const u8,
+ state: *SessionState,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(
- alloc,
+ state.arena,
parser.elementToNode(self),
tag_name,
false,
@@ -245,11 +239,11 @@ pub const Element = struct {
pub fn _getElementsByClassName(
self: *parser.Element,
- alloc: std.mem.Allocator,
classNames: []const u8,
+ state: *SessionState,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByClassName(
- alloc,
+ state.arena,
parser.elementToNode(self),
classNames,
false,
@@ -312,51 +306,51 @@ pub const Element = struct {
}
}
- pub fn _querySelector(self: *parser.Element, alloc: std.mem.Allocator, selector: []const u8) !?Union {
+ pub fn _querySelector(self: *parser.Element, selector: []const u8, state: *SessionState) !?Union {
if (selector.len == 0) return null;
- const n = try css.querySelector(alloc, parser.elementToNode(self), selector);
+ const n = try css.querySelector(state.arena, parser.elementToNode(self), selector);
if (n == null) return null;
return try toInterface(parser.nodeToElement(n.?));
}
- pub fn _querySelectorAll(self: *parser.Element, alloc: std.mem.Allocator, selector: []const u8) !NodeList {
- return css.querySelectorAll(alloc, parser.elementToNode(self), selector);
+ pub fn _querySelectorAll(self: *parser.Element, selector: []const u8, state: *SessionState) !NodeList {
+ return css.querySelectorAll(state.arena, parser.elementToNode(self), selector);
}
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
- pub fn _prepend(self: *parser.Element, nodes: ?Variadic(*parser.Node)) !void {
+ pub fn _prepend(self: *parser.Element, nodes: []const *parser.Node) !void {
return Node.prepend(parser.elementToNode(self), nodes);
}
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
- pub fn _append(self: *parser.Element, nodes: ?Variadic(*parser.Node)) !void {
+ pub fn _append(self: *parser.Element, nodes: []const *parser.Node) !void {
return Node.append(parser.elementToNode(self), nodes);
}
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
- pub fn _replaceChildren(self: *parser.Element, nodes: ?Variadic(*parser.Node)) !void {
+ pub fn _replaceChildren(self: *parser.Element, nodes: []const *parser.Node) !void {
return Node.replaceChildren(parser.elementToNode(self), nodes);
}
- pub fn _getBoundingClientRect(self: *parser.Element, user_context: UserContext) !DOMRect {
- return user_context.renderer.getRect(self);
+ pub fn _getBoundingClientRect(self: *parser.Element, state: *SessionState) !DOMRect {
+ return state.renderer.getRect(self);
}
- pub fn get_clientWidth(_: *parser.Element, user_context: UserContext) u32 {
- return user_context.renderer.width();
+ pub fn get_clientWidth(_: *parser.Element, state: *SessionState) u32 {
+ return state.renderer.width();
}
- pub fn get_clientHeight(_: *parser.Element, user_context: UserContext) u32 {
- return user_context.renderer.height();
+ pub fn get_clientHeight(_: *parser.Element, state: *SessionState) u32 {
+ return state.renderer.height();
}
pub fn deinit(_: *parser.Element, _: std.mem.Allocator) void {}
@@ -365,172 +359,161 @@ pub const Element = struct {
// Tests
// -----
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var getters = [_]Case{
- .{ .src = "let g = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "g.namespaceURI", .ex = "http://www.w3.org/1999/xhtml" },
- .{ .src = "g.prefix", .ex = "null" },
- .{ .src = "g.localName", .ex = "div" },
- .{ .src = "g.tagName", .ex = "DIV" },
- };
- try checkCases(js_env, &getters);
+const testing = @import("../../testing.zig");
+test "Browser.DOM.Element" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
- var gettersetters = [_]Case{
- .{ .src = "let gs = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "gs.id", .ex = "content" },
- .{ .src = "gs.id = 'foo'", .ex = "foo" },
- .{ .src = "gs.id", .ex = "foo" },
- .{ .src = "gs.id = 'content'", .ex = "content" },
- .{ .src = "gs.className", .ex = "" },
- .{ .src = "let gs2 = document.getElementById('para-empty')", .ex = "undefined" },
- .{ .src = "gs2.className", .ex = "ok empty" },
- .{ .src = "gs2.className = 'foo bar baz'", .ex = "foo bar baz" },
- .{ .src = "gs2.className", .ex = "foo bar baz" },
- .{ .src = "gs2.className = 'ok empty'", .ex = "ok empty" },
- .{ .src = "let cl = gs2.classList", .ex = "undefined" },
- .{ .src = "cl.length", .ex = "2" },
- };
- try checkCases(js_env, &gettersetters);
+ try runner.testCases(&.{
+ .{ "let g = document.getElementById('content')", "undefined" },
+ .{ "g.namespaceURI", "http://www.w3.org/1999/xhtml" },
+ .{ "g.prefix", "null" },
+ .{ "g.localName", "div" },
+ .{ "g.tagName", "DIV" },
+ }, .{});
- var attribute = [_]Case{
- .{ .src = "let a = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "a.hasAttributes()", .ex = "true" },
- .{ .src = "a.attributes.length", .ex = "1" },
+ try runner.testCases(&.{
+ .{ "let gs = document.getElementById('content')", "undefined" },
+ .{ "gs.id", "content" },
+ .{ "gs.id = 'foo'", "foo" },
+ .{ "gs.id", "foo" },
+ .{ "gs.id = 'content'", "content" },
+ .{ "gs.className", "" },
+ .{ "let gs2 = document.getElementById('para-empty')", "undefined" },
+ .{ "gs2.className", "ok empty" },
+ .{ "gs2.className = 'foo bar baz'", "foo bar baz" },
+ .{ "gs2.className", "foo bar baz" },
+ .{ "gs2.className = 'ok empty'", "ok empty" },
+ .{ "let cl = gs2.classList", "undefined" },
+ .{ "cl.length", "2" },
+ }, .{});
- .{ .src = "a.getAttribute('id')", .ex = "content" },
+ try runner.testCases(&.{
+ .{ "let a = document.getElementById('content')", "undefined" },
+ .{ "a.hasAttributes()", "true" },
+ .{ "a.attributes.length", "1" },
- .{ .src = "a.hasAttribute('foo')", .ex = "false" },
- .{ .src = "a.getAttribute('foo')", .ex = "null" },
+ .{ "a.getAttribute('id')", "content" },
- .{ .src = "a.setAttribute('foo', 'bar')", .ex = "undefined" },
- .{ .src = "a.hasAttribute('foo')", .ex = "true" },
- .{ .src = "a.getAttribute('foo')", .ex = "bar" },
+ .{ "a.hasAttribute('foo')", "false" },
+ .{ "a.getAttribute('foo')", "null" },
- .{ .src = "a.setAttribute('foo', 'baz')", .ex = "undefined" },
- .{ .src = "a.hasAttribute('foo')", .ex = "true" },
- .{ .src = "a.getAttribute('foo')", .ex = "baz" },
+ .{ "a.setAttribute('foo', 'bar')", "undefined" },
+ .{ "a.hasAttribute('foo')", "true" },
+ .{ "a.getAttribute('foo')", "bar" },
- .{ .src = "a.removeAttribute('foo')", .ex = "undefined" },
- .{ .src = "a.hasAttribute('foo')", .ex = "false" },
- .{ .src = "a.getAttribute('foo')", .ex = "null" },
- };
- try checkCases(js_env, &attribute);
+ .{ "a.setAttribute('foo', 'baz')", "undefined" },
+ .{ "a.hasAttribute('foo')", "true" },
+ .{ "a.getAttribute('foo')", "baz" },
- var toggleAttr = [_]Case{
- .{ .src = "let b = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "b.toggleAttribute('foo')", .ex = "true" },
- .{ .src = "b.hasAttribute('foo')", .ex = "true" },
- .{ .src = "b.getAttribute('foo')", .ex = "" },
+ .{ "a.removeAttribute('foo')", "undefined" },
+ .{ "a.hasAttribute('foo')", "false" },
+ .{ "a.getAttribute('foo')", "null" },
+ }, .{});
- .{ .src = "b.toggleAttribute('foo')", .ex = "false" },
- .{ .src = "b.hasAttribute('foo')", .ex = "false" },
- };
- try checkCases(js_env, &toggleAttr);
+ try runner.testCases(&.{
+ .{ "let b = document.getElementById('content')", "undefined" },
+ .{ "b.toggleAttribute('foo')", "true" },
+ .{ "b.hasAttribute('foo')", "true" },
+ .{ "b.getAttribute('foo')", "" },
- var parentNode = [_]Case{
- .{ .src = "let c = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "c.children.length", .ex = "3" },
- .{ .src = "c.firstElementChild.nodeName", .ex = "A" },
- .{ .src = "c.lastElementChild.nodeName", .ex = "P" },
- .{ .src = "c.childElementCount", .ex = "3" },
+ .{ "b.toggleAttribute('foo')", "false" },
+ .{ "b.hasAttribute('foo')", "false" },
+ }, .{});
- .{ .src = "c.prepend(document.createTextNode('foo'))", .ex = "undefined" },
- .{ .src = "c.append(document.createTextNode('bar'))", .ex = "undefined" },
- };
- try checkCases(js_env, &parentNode);
+ try runner.testCases(&.{
+ .{ "let c = document.getElementById('content')", "undefined" },
+ .{ "c.children.length", "3" },
+ .{ "c.firstElementChild.nodeName", "A" },
+ .{ "c.lastElementChild.nodeName", "P" },
+ .{ "c.childElementCount", "3" },
- var elementSibling = [_]Case{
- .{ .src = "let d = document.getElementById('para')", .ex = "undefined" },
- .{ .src = "d.previousElementSibling.nodeName", .ex = "P" },
- .{ .src = "d.nextElementSibling", .ex = "null" },
- };
- try checkCases(js_env, &elementSibling);
+ .{ "c.prepend(document.createTextNode('foo'))", "undefined" },
+ .{ "c.append(document.createTextNode('bar'))", "undefined" },
+ }, .{});
- var querySelector = [_]Case{
- .{ .src = "let e = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "e.querySelector('foo')", .ex = "null" },
- .{ .src = "e.querySelector('#foo')", .ex = "null" },
- .{ .src = "e.querySelector('#link').id", .ex = "link" },
- .{ .src = "e.querySelector('#para').id", .ex = "para" },
- .{ .src = "e.querySelector('*').id", .ex = "link" },
- .{ .src = "e.querySelector('')", .ex = "null" },
- .{ .src = "e.querySelector('*').id", .ex = "link" },
- .{ .src = "e.querySelector('#content')", .ex = "null" },
- .{ .src = "e.querySelector('#para').id", .ex = "para" },
- .{ .src = "e.querySelector('.ok').id", .ex = "link" },
- .{ .src = "e.querySelector('a ~ p').id", .ex = "para-empty" },
+ try runner.testCases(&.{
+ .{ "let d = document.getElementById('para')", "undefined" },
+ .{ "d.previousElementSibling.nodeName", "P" },
+ .{ "d.nextElementSibling", "null" },
+ }, .{});
- .{ .src = "e.querySelectorAll('foo').length", .ex = "0" },
- .{ .src = "e.querySelectorAll('#foo').length", .ex = "0" },
- .{ .src = "e.querySelectorAll('#link').length", .ex = "1" },
- .{ .src = "e.querySelectorAll('#link').item(0).id", .ex = "link" },
- .{ .src = "e.querySelectorAll('#para').length", .ex = "1" },
- .{ .src = "e.querySelectorAll('#para').item(0).id", .ex = "para" },
- .{ .src = "e.querySelectorAll('*').length", .ex = "4" },
- .{ .src = "e.querySelectorAll('p').length", .ex = "2" },
- .{ .src = "e.querySelectorAll('.ok').item(0).id", .ex = "link" },
- };
- try checkCases(js_env, &querySelector);
+ try runner.testCases(&.{
+ .{ "let e = document.getElementById('content')", "undefined" },
+ .{ "e.querySelector('foo')", "null" },
+ .{ "e.querySelector('#foo')", "null" },
+ .{ "e.querySelector('#link').id", "link" },
+ .{ "e.querySelector('#para').id", "para" },
+ .{ "e.querySelector('*').id", "link" },
+ .{ "e.querySelector('')", "null" },
+ .{ "e.querySelector('*').id", "link" },
+ .{ "e.querySelector('#content')", "null" },
+ .{ "e.querySelector('#para').id", "para" },
+ .{ "e.querySelector('.ok').id", "link" },
+ .{ "e.querySelector('a ~ p').id", "para-empty" },
- var attrNode = [_]Case{
- .{ .src = "let f = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "let ff = document.createAttribute('foo')", .ex = "undefined" },
- .{ .src = "f.setAttributeNode(ff)", .ex = "null" },
- .{ .src = "f.getAttributeNode('foo').name", .ex = "foo" },
- .{ .src = "f.removeAttributeNode(ff).name", .ex = "foo" },
- .{ .src = "f.getAttributeNode('bar')", .ex = "null" },
- };
- try checkCases(js_env, &attrNode);
+ .{ "e.querySelectorAll('foo').length", "0" },
+ .{ "e.querySelectorAll('#foo').length", "0" },
+ .{ "e.querySelectorAll('#link').length", "1" },
+ .{ "e.querySelectorAll('#link').item(0).id", "link" },
+ .{ "e.querySelectorAll('#para').length", "1" },
+ .{ "e.querySelectorAll('#para').item(0).id", "para" },
+ .{ "e.querySelectorAll('*').length", "4" },
+ .{ "e.querySelectorAll('p').length", "2" },
+ .{ "e.querySelectorAll('.ok').item(0).id", "link" },
+ }, .{});
- var innerHTML = [_]Case{
- .{ .src = "document.getElementById('para').innerHTML", .ex = " And" },
- .{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "" },
+ try runner.testCases(&.{
+ .{ "let f = document.getElementById('content')", "undefined" },
+ .{ "let ff = document.createAttribute('foo')", "undefined" },
+ .{ "f.setAttributeNode(ff)", "null" },
+ .{ "f.getAttributeNode('foo').name", "foo" },
+ .{ "f.removeAttributeNode(ff).name", "foo" },
+ .{ "f.getAttributeNode('bar')", "null" },
+ }, .{});
- .{ .src = "let h = document.getElementById('para-empty')", .ex = "undefined" },
- .{ .src = "const prev = h.innerHTML", .ex = "undefined" },
- .{ .src = "h.innerHTML = 'hello world
'", .ex = "hello world
" },
- .{ .src = "h.innerHTML", .ex = "hello world
" },
- .{ .src = "h.firstChild.nodeName", .ex = "P" },
- .{ .src = "h.firstChild.id", .ex = "hello" },
- .{ .src = "h.firstChild.textContent", .ex = "hello world" },
- .{ .src = "h.innerHTML = prev; true", .ex = "true" },
- .{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "" },
- };
- try checkCases(js_env, &innerHTML);
+ try runner.testCases(&.{
+ .{ "document.getElementById('para').innerHTML", " And" },
+ .{ "document.getElementById('para-empty').innerHTML.trim()", "" },
- var outerHTML = [_]Case{
- .{ .src = "document.getElementById('para').outerHTML", .ex = " And
" },
- };
+ .{ "let h = document.getElementById('para-empty')", "undefined" },
+ .{ "const prev = h.innerHTML", "undefined" },
+ .{ "h.innerHTML = 'hello world
'", "hello world
" },
+ .{ "h.innerHTML", "hello world
" },
+ .{ "h.firstChild.nodeName", "P" },
+ .{ "h.firstChild.id", "hello" },
+ .{ "h.firstChild.textContent", "hello world" },
+ .{ "h.innerHTML = prev; true", "true" },
+ .{ "document.getElementById('para-empty').innerHTML.trim()", "" },
+ }, .{});
- var getBoundingClientRect = [_]Case{
- .{ .src = "document.getElementById('para').clientWidth", .ex = "0" },
- .{ .src = "document.getElementById('para').clientHeight", .ex = "1" },
+ try runner.testCases(&.{
+ .{ "document.getElementById('para').outerHTML", " And
" },
+ }, .{});
- .{ .src = "let r1 = document.getElementById('para').getBoundingClientRect()", .ex = "undefined" },
- .{ .src = "r1.x", .ex = "1" },
- .{ .src = "r1.y", .ex = "0" },
- .{ .src = "r1.width", .ex = "1" },
- .{ .src = "r1.height", .ex = "1" },
+ try runner.testCases(&.{
+ .{ "document.getElementById('para').clientWidth", "0" },
+ .{ "document.getElementById('para').clientHeight", "1" },
- .{ .src = "let r2 = document.getElementById('content').getBoundingClientRect()", .ex = "undefined" },
- .{ .src = "r2.x", .ex = "2" },
- .{ .src = "r2.y", .ex = "0" },
- .{ .src = "r2.width", .ex = "1" },
- .{ .src = "r2.height", .ex = "1" },
+ .{ "let r1 = document.getElementById('para').getBoundingClientRect()", "undefined" },
+ .{ "r1.x", "1" },
+ .{ "r1.y", "0" },
+ .{ "r1.width", "1" },
+ .{ "r1.height", "1" },
- .{ .src = "let r3 = document.getElementById('para').getBoundingClientRect()", .ex = "undefined" },
- .{ .src = "r3.x", .ex = "1" },
- .{ .src = "r3.y", .ex = "0" },
- .{ .src = "r3.width", .ex = "1" },
- .{ .src = "r3.height", .ex = "1" },
+ .{ "let r2 = document.getElementById('content').getBoundingClientRect()", "undefined" },
+ .{ "r2.x", "2" },
+ .{ "r2.y", "0" },
+ .{ "r2.width", "1" },
+ .{ "r2.height", "1" },
- .{ .src = "document.getElementById('para').clientWidth", .ex = "2" },
- .{ .src = "document.getElementById('para').clientHeight", .ex = "1" },
- };
- try checkCases(js_env, &getBoundingClientRect);
+ .{ "let r3 = document.getElementById('para').getBoundingClientRect()", "undefined" },
+ .{ "r3.x", "1" },
+ .{ "r3.y", "0" },
+ .{ "r3.width", "1" },
+ .{ "r3.height", "1" },
- try checkCases(js_env, &outerHTML);
+ .{ "document.getElementById('para').clientWidth", "2" },
+ .{ "document.getElementById('para').clientHeight", "1" },
+ }, .{});
}
diff --git a/src/browser/dom/event_target.zig b/src/browser/dom/event_target.zig
new file mode 100644
index 00000000..959a35ca
--- /dev/null
+++ b/src/browser/dom/event_target.zig
@@ -0,0 +1,226 @@
+// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+
+const Env = @import("../env.zig").Env;
+const parser = @import("../netsurf.zig");
+const SessionState = @import("../env.zig").SessionState;
+
+const EventHandler = @import("../events/event.zig").EventHandler;
+
+const DOMException = @import("exceptions.zig").DOMException;
+const Nod = @import("node.zig");
+
+// EventTarget interfaces
+pub const Union = Nod.Union;
+
+// EventTarget implementation
+pub const EventTarget = struct {
+ pub const Self = parser.EventTarget;
+ pub const Exception = DOMException;
+
+ pub fn toInterface(et: *parser.EventTarget) !Union {
+ // NOTE: for now we state that all EventTarget are Nodes
+ // TODO: handle other types (eg. Window)
+ return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
+ }
+
+ // JS funcs
+ // --------
+
+ pub fn _addEventListener(
+ self: *parser.EventTarget,
+ eventType: []const u8,
+ cbk: Env.Callback,
+ capture: ?bool,
+ state: *SessionState,
+ // TODO: hanle EventListenerOptions
+ // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
+ ) !void {
+ // check if event target has already this listener
+ const lst = try parser.eventTargetHasListener(
+ self,
+ eventType,
+ capture orelse false,
+ cbk.id,
+ );
+ if (lst != null) {
+ return;
+ }
+
+ try parser.eventTargetAddEventListener(
+ self,
+ state.arena,
+ eventType,
+ EventHandler,
+ .{ .cbk = cbk },
+ capture orelse false,
+ );
+ }
+
+ pub fn _removeEventListener(
+ self: *parser.EventTarget,
+ eventType: []const u8,
+ cbk: Env.Callback,
+ capture: ?bool,
+ state: *SessionState,
+ // TODO: hanle EventListenerOptions
+ // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
+ ) !void {
+ // check if event target has already this listener
+ const lst = try parser.eventTargetHasListener(
+ self,
+ eventType,
+ capture orelse false,
+ cbk.id,
+ );
+ if (lst == null) {
+ return;
+ }
+
+ // remove listener
+ try parser.eventTargetRemoveEventListener(
+ self,
+ state.arena,
+ eventType,
+ lst.?,
+ capture orelse false,
+ );
+ }
+
+ pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool {
+ return try parser.eventTargetDispatchEvent(self, event);
+ }
+
+ pub fn deinit(self: *parser.EventTarget, state: *SessionState) void {
+ parser.eventTargetRemoveAllEventListeners(self, state.arena) catch unreachable;
+ }
+};
+
+const testing = @import("../../testing.zig");
+test "Browser.DOM.EventTarget" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
+
+ try runner.testCases(&.{
+ .{ "let content = document.getElementById('content')", "undefined" },
+ .{ "let para = document.getElementById('para')", "undefined" },
+ // NOTE: as some event properties will change during the event dispatching phases
+ // we need to copy thoses values in order to check them afterwards
+ .{
+ \\ var nb = 0; var evt; var phase; var cur;
+ \\ function cbk(event) {
+ \\ evt = event;
+ \\ phase = event.eventPhase;
+ \\ cur = event.currentTarget;
+ \\ nb ++;
+ \\ }
+ ,
+ "undefined",
+ },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "content.addEventListener('basic', cbk)", "undefined" },
+ .{ "content.dispatchEvent(new Event('basic'))", "true" },
+ .{ "nb", "1" },
+ .{ "evt instanceof Event", "true" },
+ .{ "evt.type", "basic" },
+ .{ "phase", "2" },
+ .{ "cur.getAttribute('id')", "content" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
+ .{ "para.dispatchEvent(new Event('basic'))", "true" },
+ .{ "nb", "0" }, // handler is not called, no capture, not the target, no bubbling
+ .{ "evt === undefined", "true" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "nb = 0", "0" },
+ .{ "content.addEventListener('basic', cbk)", "undefined" },
+ .{ "content.dispatchEvent(new Event('basic'))", "true" },
+ .{ "nb", "1" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "nb = 0", "0" },
+ .{ "content.addEventListener('basic', cbk, true)", "undefined" },
+ .{ "content.dispatchEvent(new Event('basic'))", "true" },
+ .{ "nb", "2" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "nb = 0", "0" },
+ .{ "content.removeEventListener('basic', cbk)", "undefined" },
+ .{ "content.dispatchEvent(new Event('basic'))", "true" },
+ .{ "nb", "1" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "nb = 0", "0" },
+ .{ "content.removeEventListener('basic', cbk, true)", "undefined" },
+ .{ "content.dispatchEvent(new Event('basic'))", "true" },
+ .{ "nb", "0" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
+ .{ "content.addEventListener('capture', cbk, true)", "undefined" },
+ .{ "content.dispatchEvent(new Event('capture'))", "true" },
+ .{ "nb", "1" },
+ .{ "evt instanceof Event", "true" },
+ .{ "evt.type", "capture" },
+ .{ "phase", "2" },
+ .{ "cur.getAttribute('id')", "content" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
+ .{ "para.dispatchEvent(new Event('capture'))", "true" },
+ .{ "nb", "1" },
+ .{ "evt instanceof Event", "true" },
+ .{ "evt.type", "capture" },
+ .{ "phase", "1" },
+ .{ "cur.getAttribute('id')", "content" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
+ .{ "content.addEventListener('bubbles', cbk)", "undefined" },
+ .{ "content.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" },
+ .{ "nb", "1" },
+ .{ "evt instanceof Event", "true" },
+ .{ "evt.type", "bubbles" },
+ .{ "evt.bubbles", "true" },
+ .{ "phase", "2" },
+ .{ "cur.getAttribute('id')", "content" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
+ .{ "para.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" },
+ .{ "nb", "1" },
+ .{ "evt instanceof Event", "true" },
+ .{ "evt.type", "bubbles" },
+ .{ "phase", "3" },
+ .{ "cur.getAttribute('id')", "content" },
+ }, .{});
+}
diff --git a/src/dom/exceptions.zig b/src/browser/dom/exceptions.zig
similarity index 81%
rename from src/dom/exceptions.zig
rename to src/browser/dom/exceptions.zig
index 209f4345..441fe92c 100644
--- a/src/dom/exceptions.zig
+++ b/src/browser/dom/exceptions.zig
@@ -19,19 +19,13 @@
const std = @import("std");
const allocPrint = std.fmt.allocPrint;
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
// https://webidl.spec.whatwg.org/#idl-DOMException
pub const DOMException = struct {
err: parser.DOMError,
str: []const u8,
- pub const mem_guarantied = true;
-
pub const ErrorSet = parser.DOMError;
// static attributes
@@ -62,7 +56,7 @@ pub const DOMException = struct {
pub const _DATA_CLONE_ERR = 25;
// TODO: deinit
- pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) anyerror!DOMException {
+ pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) !DOMException {
const errCast = @as(parser.DOMError, @errorCast(err));
const errName = DOMException.name(errCast);
const str = switch (errCast) {
@@ -120,7 +114,7 @@ pub const DOMException = struct {
// JS properties and methods
- pub fn get_code(self: DOMException) u8 {
+ pub fn get_code(self: *const DOMException) u8 {
return switch (self.err) {
error.IndexSize => 1,
error.StringSize => 2,
@@ -157,38 +151,41 @@ pub const DOMException = struct {
};
}
- pub fn get_name(self: DOMException) []const u8 {
+ pub fn get_name(self: *const DOMException) []const u8 {
return DOMException.name(self.err);
}
- pub fn get_message(self: DOMException) []const u8 {
+ pub fn get_message(self: *const DOMException) []const u8 {
const errName = DOMException.name(self.err);
return self.str[errName.len + 2 ..];
}
- pub fn _toString(self: DOMException) []const u8 {
+ pub fn _toString(self: *const DOMException) []const u8 {
return self.str;
}
};
-// Tests
-// -----
+const testing = @import("../../testing.zig");
+test "Browser.DOM.Exception" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
const err = "Failed to execute 'appendChild' on 'Node': The new child element contains the parent.";
- var cases = [_]Case{
- .{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "let link = document.getElementById('link')", .ex = "undefined" },
+ try runner.testCases(&.{
+ .{ "let content = document.getElementById('content')", "undefined" },
+ .{ "let link = document.getElementById('link')", "undefined" },
// HierarchyRequestError
- .{ .src = "var HierarchyRequestError; try {link.appendChild(content)} catch (error) {HierarchyRequestError = error} HierarchyRequestError.name", .ex = "HierarchyRequestError" },
- .{ .src = "HierarchyRequestError.code", .ex = "3" },
- .{ .src = "HierarchyRequestError.message", .ex = err },
- .{ .src = "HierarchyRequestError.toString()", .ex = "HierarchyRequestError: " ++ err },
- .{ .src = "HierarchyRequestError instanceof DOMException", .ex = "true" },
- .{ .src = "HierarchyRequestError instanceof Error", .ex = "true" },
- };
- try checkCases(js_env, &cases);
+ .{
+ \\ var he;
+ \\ try { link.appendChild(content) } catch (error) { he = error}
+ \\ he.name
+ ,
+ "HierarchyRequestError",
+ },
+ .{ "he.code", "3" },
+ .{ "he.message", err },
+ .{ "he.toString()", "HierarchyRequestError: " ++ err },
+ .{ "he instanceof DOMException", "true" },
+ .{ "he instanceof Error", "true" },
+ }, .{});
}
diff --git a/src/dom/html_collection.zig b/src/browser/dom/html_collection.zig
similarity index 76%
rename from src/dom/html_collection.zig
rename to src/browser/dom/html_collection.zig
index 298d6b72..3a7aa8fa 100644
--- a/src/dom/html_collection.zig
+++ b/src/browser/dom/html_collection.zig
@@ -18,17 +18,14 @@
const std = @import("std");
-const parser = @import("netsurf");
-
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-const generate = @import("../generate.zig");
+const parser = @import("../netsurf.zig");
const utils = @import("utils.z");
const Element = @import("element.zig").Element;
const Union = @import("element.zig").Union;
+const JsObject = @import("../env.zig").JsObject;
+
const Walker = @import("walker.zig").Walker;
const WalkerDepthFirst = @import("walker.zig").WalkerDepthFirst;
const WalkerChildren = @import("walker.zig").WalkerChildren;
@@ -279,8 +276,6 @@ pub fn HTMLCollectionByAnchors(
}
pub const HTMLCollectionIterator = struct {
- pub const mem_guarantied = true;
-
coll: *HTMLCollection,
index: u32 = 0,
@@ -311,8 +306,6 @@ pub const HTMLCollectionIterator = struct {
// dom_html_collection expects a comparison function callback as arguement.
// But we wanted a dynamically comparison here, according to the match tagname.
pub const HTMLCollection = struct {
- pub const mem_guarantied = true;
-
matcher: Matcher,
walker: Walker,
@@ -327,10 +320,6 @@ pub const HTMLCollection = struct {
cur_idx: ?u32 = undefined,
cur_node: ?*parser.Node = undefined,
- // array_like_keys is used to keep reference to array like interface implementation.
- // the collection generates keys string which must be free on deinit.
- array_like_keys: std.ArrayListUnmanaged([]u8) = .{},
-
// start returns the first node to walk on.
fn start(self: HTMLCollection) !?*parser.Node {
if (self.root == null) return null;
@@ -412,7 +401,7 @@ pub const HTMLCollection = struct {
return try Element.toInterface(e);
}
- pub fn _namedItem(self: *HTMLCollection, name: []const u8) !?Union {
+ pub fn _namedItem(self: *const HTMLCollection, name: []const u8) !?Union {
if (self.root == null) return null;
if (name.len == 0) return null;
@@ -454,81 +443,67 @@ pub const HTMLCollection = struct {
return null;
}
- pub fn postAttach(self: *HTMLCollection, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void {
- const ln = try self.get_length();
- var i: u32 = 0;
- while (i < ln) {
- defer i += 1;
- const k = try std.fmt.allocPrint(alloc, "{d}", .{i});
- try self.array_like_keys.append(alloc, k);
-
- const node = try self.item(i) orelse unreachable;
+ pub fn postAttach(self: *HTMLCollection, js_obj: JsObject) !void {
+ const len = try self.get_length();
+ for (0..len) |i| {
+ const node = try self.item(@intCast(i)) orelse unreachable;
const e = @as(*parser.Element, @ptrCast(node));
- try js_obj.set(k, e);
+ try js_obj.setIndex(@intCast(i), e);
if (try item_name(e)) |name| {
try js_obj.set(name, e);
}
}
}
-
- pub fn deinit(self: *HTMLCollection, alloc: std.mem.Allocator) void {
- for (self.array_like_keys_) |k| alloc.free(k);
- self.array_like_keys.deinit(alloc);
- self.matcher.deinit(alloc);
- }
};
-// Tests
-// -----
+const testing = @import("../../testing.zig");
+test "Browser.DOM.HTMLCollection" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var getElementsByTagName = [_]Case{
- .{ .src = "let getElementsByTagName = document.getElementsByTagName('p')", .ex = "undefined" },
- .{ .src = "getElementsByTagName.length", .ex = "2" },
- .{ .src = "let getElementsByTagNameCI = document.getElementsByTagName('P')", .ex = "undefined" },
- .{ .src = "getElementsByTagNameCI.length", .ex = "2" },
- .{ .src = "getElementsByTagName.item(0).localName", .ex = "p" },
- .{ .src = "getElementsByTagName.item(1).localName", .ex = "p" },
- .{ .src = "let getElementsByTagNameAll = document.getElementsByTagName('*')", .ex = "undefined" },
- .{ .src = "getElementsByTagNameAll.length", .ex = "8" },
- .{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" },
- .{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" },
- .{ .src = "getElementsByTagNameAll.item(1).localName", .ex = "head" },
- .{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" },
- .{ .src = "getElementsByTagNameAll.item(2).localName", .ex = "body" },
- .{ .src = "getElementsByTagNameAll.item(3).localName", .ex = "div" },
- .{ .src = "getElementsByTagNameAll.item(7).localName", .ex = "p" },
- .{ .src = "getElementsByTagNameAll.namedItem('para-empty-child').localName", .ex = "span" },
+ try runner.testCases(&.{
+ .{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
+ .{ "getElementsByTagName.length", "2" },
+ .{ "let getElementsByTagNameCI = document.getElementsByTagName('P')", "undefined" },
+ .{ "getElementsByTagNameCI.length", "2" },
+ .{ "getElementsByTagName.item(0).localName", "p" },
+ .{ "getElementsByTagName.item(1).localName", "p" },
+ .{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
+ .{ "getElementsByTagNameAll.length", "8" },
+ .{ "getElementsByTagNameAll.item(0).localName", "html" },
+ .{ "getElementsByTagNameAll.item(0).localName", "html" },
+ .{ "getElementsByTagNameAll.item(1).localName", "head" },
+ .{ "getElementsByTagNameAll.item(0).localName", "html" },
+ .{ "getElementsByTagNameAll.item(2).localName", "body" },
+ .{ "getElementsByTagNameAll.item(3).localName", "div" },
+ .{ "getElementsByTagNameAll.item(7).localName", "p" },
+ .{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
// array like
- .{ .src = "getElementsByTagNameAll[0].localName", .ex = "html" },
- .{ .src = "getElementsByTagNameAll[7].localName", .ex = "p" },
- .{ .src = "getElementsByTagNameAll[8]", .ex = "undefined" },
- .{ .src = "getElementsByTagNameAll['para-empty-child'].localName", .ex = "span" },
- .{ .src = "getElementsByTagNameAll['foo']", .ex = "undefined" },
+ .{ "getElementsByTagNameAll[0].localName", "html" },
+ .{ "getElementsByTagNameAll[7].localName", "p" },
+ .{ "getElementsByTagNameAll[8]", "undefined" },
+ .{ "getElementsByTagNameAll['para-empty-child'].localName", "span" },
+ .{ "getElementsByTagNameAll['foo']", "undefined" },
- .{ .src = "document.getElementById('content').getElementsByTagName('*').length", .ex = "4" },
- .{ .src = "document.getElementById('content').getElementsByTagName('p').length", .ex = "2" },
- .{ .src = "document.getElementById('content').getElementsByTagName('div').length", .ex = "0" },
+ .{ "document.getElementById('content').getElementsByTagName('*').length", "4" },
+ .{ "document.getElementById('content').getElementsByTagName('p').length", "2" },
+ .{ "document.getElementById('content').getElementsByTagName('div').length", "0" },
- .{ .src = "document.children.length", .ex = "1" },
- .{ .src = "document.getElementById('content').children.length", .ex = "3" },
+ .{ "document.children.length", "1" },
+ .{ "document.getElementById('content').children.length", "3" },
// check liveness
- .{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "let pe = document.getElementById('para-empty')", .ex = "undefined" },
- .{ .src = "let p = document.createElement('p')", .ex = "undefined" },
- .{ .src = "p.textContent = 'OK live'", .ex = "OK live" },
- .{ .src = "getElementsByTagName.item(1).textContent", .ex = " And" },
- .{ .src = "content.appendChild(p) != undefined", .ex = "true" },
- .{ .src = "getElementsByTagName.length", .ex = "3" },
- .{ .src = "getElementsByTagName.item(2).textContent", .ex = "OK live" },
- .{ .src = "content.insertBefore(p, pe) != undefined", .ex = "true" },
- .{ .src = "getElementsByTagName.item(0).textContent", .ex = "OK live" },
- };
- try checkCases(js_env, &getElementsByTagName);
+ .{ "let content = document.getElementById('content')", "undefined" },
+ .{ "let pe = document.getElementById('para-empty')", "undefined" },
+ .{ "let p = document.createElement('p')", "undefined" },
+ .{ "p.textContent = 'OK live'", "OK live" },
+ .{ "getElementsByTagName.item(1).textContent", " And" },
+ .{ "content.appendChild(p) != undefined", "true" },
+ .{ "getElementsByTagName.length", "3" },
+ .{ "getElementsByTagName.item(2).textContent", "OK live" },
+ .{ "content.insertBefore(p, pe) != undefined", "true" },
+ .{ "getElementsByTagName.item(0).textContent", "OK live" },
+ }, .{});
}
diff --git a/src/dom/implementation.zig b/src/browser/dom/implementation.zig
similarity index 61%
rename from src/dom/implementation.zig
rename to src/browser/dom/implementation.zig
index ec90014f..b97b37e5 100644
--- a/src/dom/implementation.zig
+++ b/src/browser/dom/implementation.zig
@@ -18,11 +18,8 @@
const std = @import("std");
-const parser = @import("netsurf");
-
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
+const parser = @import("../netsurf.zig");
+const SessionState = @import("../env.zig").SessionState;
const Document = @import("document.zig").Document;
const DocumentType = @import("document_type.zig").DocumentType;
@@ -30,47 +27,47 @@ const DOMException = @import("exceptions.zig").DOMException;
// WEB IDL https://dom.spec.whatwg.org/#domimplementation
pub const DOMImplementation = struct {
- pub const mem_guarantied = true;
-
pub const Exception = DOMException;
pub fn _createDocumentType(
_: *DOMImplementation,
- alloc: std.mem.Allocator,
qname: []const u8,
publicId: []const u8,
systemId: []const u8,
+ state: *SessionState,
) !*parser.DocumentType {
- const cqname = try alloc.dupeZ(u8, qname);
- defer alloc.free(cqname);
+ const allocator = state.arena;
+ const cqname = try allocator.dupeZ(u8, qname);
+ defer allocator.free(cqname);
- const cpublicId = try alloc.dupeZ(u8, publicId);
- defer alloc.free(cpublicId);
+ const cpublicId = try allocator.dupeZ(u8, publicId);
+ defer allocator.free(cpublicId);
- const csystemId = try alloc.dupeZ(u8, systemId);
- defer alloc.free(csystemId);
+ const csystemId = try allocator.dupeZ(u8, systemId);
+ defer allocator.free(csystemId);
return try parser.domImplementationCreateDocumentType(cqname, cpublicId, csystemId);
}
pub fn _createDocument(
_: *DOMImplementation,
- alloc: std.mem.Allocator,
namespace: ?[]const u8,
qname: ?[]const u8,
doctype: ?*parser.DocumentType,
+ state: *SessionState,
) !*parser.Document {
+ const allocator = state.arena;
var cnamespace: ?[:0]const u8 = null;
if (namespace) |ns| {
- cnamespace = try alloc.dupeZ(u8, ns);
+ cnamespace = try allocator.dupeZ(u8, ns);
}
- defer if (cnamespace) |v| alloc.free(v);
+ defer if (cnamespace) |v| allocator.free(v);
var cqname: ?[:0]const u8 = null;
if (qname) |qn| {
- cqname = try alloc.dupeZ(u8, qn);
+ cqname = try allocator.dupeZ(u8, qn);
}
- defer if (cqname) |v| alloc.free(v);
+ defer if (cqname) |v| allocator.free(v);
return try parser.domImplementationCreateDocument(cnamespace, cqname, doctype);
}
@@ -89,17 +86,17 @@ pub const DOMImplementation = struct {
// Tests
// -----
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var getImplementation = [_]Case{
- .{ .src = "let impl = document.implementation", .ex = "undefined" },
- .{ .src = "impl.createHTMLDocument();", .ex = "[object HTMLDocument]" },
- .{ .src = "impl.createHTMLDocument('foo');", .ex = "[object HTMLDocument]" },
- .{ .src = "impl.createDocument(null, 'foo');", .ex = "[object Document]" },
- .{ .src = "impl.createDocumentType('foo', 'bar', 'baz')", .ex = "[object DocumentType]" },
- .{ .src = "impl.hasFeature()", .ex = "true" },
- };
- try checkCases(js_env, &getImplementation);
+const testing = @import("../../testing.zig");
+test "Browser.DOM.Implementation" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
+
+ try runner.testCases(&.{
+ .{ "let impl = document.implementation", "undefined" },
+ .{ "impl.createHTMLDocument();", "[object HTMLDocument]" },
+ .{ "impl.createHTMLDocument('foo');", "[object HTMLDocument]" },
+ .{ "impl.createDocument(null, 'foo');", "[object Document]" },
+ .{ "impl.createDocumentType('foo', 'bar', 'baz')", "[object DocumentType]" },
+ .{ "impl.hasFeature()", "true" },
+ }, .{});
}
diff --git a/src/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig
similarity index 68%
rename from src/dom/mutation_observer.zig
rename to src/browser/dom/mutation_observer.zig
index f5003686..0324399e 100644
--- a/src/dom/mutation_observer.zig
+++ b/src/browser/dom/mutation_observer.zig
@@ -18,14 +18,11 @@
const std = @import("std");
-const parser = @import("netsurf");
-
-const jsruntime = @import("jsruntime");
-const Callback = jsruntime.Callback;
-const CallbackResult = jsruntime.CallbackResult;
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
+const parser = @import("../netsurf.zig");
+const SessionState = @import("../env.zig").SessionState;
+const Env = @import("../env.zig").Env;
+const JsObject = @import("../env.zig").JsObject;
const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = .{
@@ -40,20 +37,18 @@ const log = std.log.scoped(.events);
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
pub const MutationObserver = struct {
- cbk: Callback,
+ cbk: Env.Callback,
observers: Observers,
- pub const mem_guarantied = true;
-
const Observer = struct {
node: *parser.Node,
options: MutationObserverInit,
};
const deinitFunc = struct {
- fn deinit(ctx: ?*anyopaque, alloc: std.mem.Allocator) void {
+ fn deinit(ctx: ?*anyopaque, allocator: std.mem.Allocator) void {
const o: *Observer = @ptrCast(@alignCast(ctx));
- alloc.destroy(o);
+ allocator.destroy(o);
}
}.deinit;
@@ -78,7 +73,7 @@ pub const MutationObserver = struct {
}
};
- pub fn constructor(cbk: Callback) !MutationObserver {
+ pub fn constructor(cbk: Env.Callback) !MutationObserver {
return MutationObserver{
.cbk = cbk,
.observers = .{},
@@ -90,22 +85,23 @@ pub const MutationObserver = struct {
return opt orelse .{};
}
- pub fn _observe(self: *MutationObserver, alloc: std.mem.Allocator, node: *parser.Node, options: ?MutationObserverInit) !void {
- const o = try alloc.create(Observer);
+ pub fn _observe(self: *MutationObserver, node: *parser.Node, options: ?MutationObserverInit, state: *SessionState) !void {
+ const arena = state.arena;
+ const o = try arena.create(Observer);
o.* = .{
.node = node,
.options = resolveOptions(options),
};
- errdefer alloc.destroy(o);
+ errdefer arena.destroy(o);
// register the new observer.
- try self.observers.append(alloc, o);
+ try self.observers.append(arena, o);
// register node's events.
if (o.options.childList or o.options.subtree) {
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
- alloc,
+ arena,
"DOMNodeInserted",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
@@ -113,7 +109,7 @@ pub const MutationObserver = struct {
);
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
- alloc,
+ arena,
"DOMNodeRemoved",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
@@ -123,7 +119,7 @@ pub const MutationObserver = struct {
if (o.options.attr()) {
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
- alloc,
+ arena,
"DOMAttrModified",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
@@ -133,7 +129,7 @@ pub const MutationObserver = struct {
if (o.options.cdata()) {
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
- alloc,
+ arena,
"DOMCharacterDataModified",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
@@ -143,7 +139,7 @@ pub const MutationObserver = struct {
if (o.options.subtree) {
try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
- alloc,
+ arena,
"DOMSubtreeModified",
EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
@@ -157,14 +153,17 @@ pub const MutationObserver = struct {
// TODO unregister listeners.
}
- pub fn deinit(self: *MutationObserver, alloc: std.mem.Allocator) void {
+ pub fn deinit(self: *MutationObserver, state: *SessionState) void {
+ const arena = state.arena;
// TODO unregister listeners.
- for (self.observers.items) |o| alloc.destroy(o);
- self.observers.deinit(alloc);
+ for (self.observers.items) |o| {
+ arena.destroy(o);
+ }
+ self.observers.deinit(arena);
}
// TODO
- pub fn _takeRecords(_: MutationObserver) ?[]const u8 {
+ pub fn _takeRecords(_: *const MutationObserver) ?[]const u8 {
return &[_]u8{};
}
};
@@ -173,15 +172,19 @@ pub const MutationObserver = struct {
pub const MutationRecords = struct {
first: ?MutationRecord = null,
- pub const mem_guarantied = true;
-
- pub fn get_length(self: *MutationRecords) u32 {
+ pub fn get_length(self: *const MutationRecords) u32 {
if (self.first == null) return 0;
return 1;
}
-
- pub fn postAttach(self: *MutationRecords, js_obj: jsruntime.JSObject) !void {
+ pub fn indexed_get(self: *const MutationRecords, i: u32, has_value: *bool) ?MutationRecord {
+ _ = i;
+ return self.first orelse {
+ has_value.* = false;
+ return null;
+ };
+ }
+ pub fn postAttach(self: *const MutationRecords, js_obj: JsObject) !void {
if (self.first) |mr| {
try js_obj.set("0", mr);
}
@@ -199,41 +202,39 @@ pub const MutationRecord = struct {
attributeNamespace: ?[]const u8 = null,
oldValue: ?[]const u8 = null,
- pub const mem_guarantied = true;
-
- pub fn get_type(self: MutationRecord) []const u8 {
+ pub fn get_type(self: *const MutationRecord) []const u8 {
return self.type;
}
- pub fn get_addedNodes(self: MutationRecord) NodeList {
+ pub fn get_addedNodes(self: *const MutationRecord) NodeList {
return self.addedNodes;
}
- pub fn get_removedNodes(self: MutationRecord) NodeList {
+ pub fn get_removedNodes(self: *const MutationRecord) NodeList {
return self.addedNodes;
}
- pub fn get_target(self: MutationRecord) *parser.Node {
+ pub fn get_target(self: *const MutationRecord) *parser.Node {
return self.target;
}
- pub fn get_attributeName(self: MutationRecord) ?[]const u8 {
+ pub fn get_attributeName(self: *const MutationRecord) ?[]const u8 {
return self.attributeName;
}
- pub fn get_attributeNamespace(self: MutationRecord) ?[]const u8 {
+ pub fn get_attributeNamespace(self: *const MutationRecord) ?[]const u8 {
return self.attributeNamespace;
}
- pub fn get_previousSibling(self: MutationRecord) ?*parser.Node {
+ pub fn get_previousSibling(self: *const MutationRecord) ?*parser.Node {
return self.previousSibling;
}
- pub fn get_nextSibling(self: MutationRecord) ?*parser.Node {
+ pub fn get_nextSibling(self: *const MutationRecord) ?*parser.Node {
return self.nextSibling;
}
- pub fn get_oldValue(self: MutationRecord) ?[]const u8 {
+ pub fn get_oldValue(self: *const MutationRecord) ?[]const u8 {
return self.oldValue;
}
};
@@ -283,7 +284,7 @@ const EventHandler = struct {
const muevt = parser.eventToMutationEvent(evt.?);
// TODO get the allocator by another way?
- const alloc = data.cbk.nat_ctx.alloc;
+ const alloc = data.cbk.executor.call_arena.allocator();
if (std.mem.eql(u8, t, "DOMAttrModified")) {
mrs.first = .{
@@ -340,66 +341,63 @@ const EventHandler = struct {
return;
}
- var res = CallbackResult.init(alloc);
- defer res.deinit();
-
// TODO pass MutationRecords and MutationObserver
- data.cbk.trycall(.{mrs}, &res) catch |e| log.err("mutation event handler error: {any}", .{e});
-
- // in case of function error, we log the result and the trace.
- if (!res.success) {
- log.info("mutation observer event handler error: {s}", .{res.result orelse "unknown"});
- log.debug("{s}", .{res.stack orelse "no stack trace"});
- }
+ var result: Env.Callback.Result = undefined;
+ data.cbk.tryCall(.{mrs}, &result) catch {
+ log.err("mutation observer callback error: {s}", .{result.exception});
+ log.debug("stack:\n{s}", .{result.stack orelse "???"});
+ };
}
}.handle;
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var constructor = [_]Case{
- .{ .src = "new MutationObserver(() => {}).observe(document, { childList: true });", .ex = "undefined" },
- };
- try checkCases(js_env, &constructor);
+const testing = @import("../../testing.zig");
+test "Browser.DOM.MutationObserver" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
- var attr = [_]Case{
- .{ .src =
- \\var nb = 0;
- \\var mrs;
- \\new MutationObserver((mu) => {
- \\ mrs = mu;
- \\ nb++;
- \\}).observe(document.firstElementChild, { attributes: true, attributeOldValue: true });
- \\document.firstElementChild.setAttribute("foo", "bar");
- \\// ignored b/c it's about another target.
- \\document.firstElementChild.firstChild.setAttribute("foo", "bar");
- \\nb;
- , .ex = "1" },
- .{ .src = "mrs[0].type", .ex = "attributes" },
- .{ .src = "mrs[0].target == document.firstElementChild", .ex = "true" },
- .{ .src = "mrs[0].target.getAttribute('foo')", .ex = "bar" },
- .{ .src = "mrs[0].attributeName", .ex = "foo" },
- .{ .src = "mrs[0].oldValue", .ex = "null" },
- };
- try checkCases(js_env, &attr);
+ try runner.testCases(&.{
+ .{ "new MutationObserver(() => {}).observe(document, { childList: true });", "undefined" },
+ }, .{});
- var cdata = [_]Case{
- .{ .src =
- \\var node = document.getElementById("para").firstChild;
- \\var nb2 = 0;
- \\var mrs2;
- \\new MutationObserver((mu) => {
- \\ mrs2 = mu;
- \\ nb2++;
- \\}).observe(node, { characterData: true, characterDataOldValue: true });
- \\node.data = "foo";
- \\nb2;
- , .ex = "1" },
- .{ .src = "mrs2[0].type", .ex = "characterData" },
- .{ .src = "mrs2[0].target == node", .ex = "true" },
- .{ .src = "mrs2[0].target.data", .ex = "foo" },
- .{ .src = "mrs2[0].oldValue", .ex = " And" },
- };
- try checkCases(js_env, &cdata);
+ try runner.testCases(&.{
+ .{
+ \\ var nb = 0;
+ \\ var mrs;
+ \\ new MutationObserver((mu) => {
+ \\ mrs = mu;
+ \\ nb++;
+ \\ }).observe(document.firstElementChild, { attributes: true, attributeOldValue: true });
+ \\ document.firstElementChild.setAttribute("foo", "bar");
+ \\ // ignored b/c it's about another target.
+ \\ document.firstElementChild.firstChild.setAttribute("foo", "bar");
+ \\ nb;
+ ,
+ "1",
+ },
+ .{ "mrs[0].type", "attributes" },
+ .{ "mrs[0].target == document.firstElementChild", "true" },
+ .{ "mrs[0].target.getAttribute('foo')", "bar" },
+ .{ "mrs[0].attributeName", "foo" },
+ .{ "mrs[0].oldValue", "null" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{
+ \\ var node = document.getElementById("para").firstChild;
+ \\ var nb2 = 0;
+ \\ var mrs2;
+ \\ new MutationObserver((mu) => {
+ \\ mrs2 = mu;
+ \\ nb2++;
+ \\ }).observe(node, { characterData: true, characterDataOldValue: true });
+ \\ node.data = "foo";
+ \\ nb2;
+ ,
+ "1",
+ },
+ .{ "mrs2[0].type", "characterData" },
+ .{ "mrs2[0].target == node", "true" },
+ .{ "mrs2[0].target.data", "foo" },
+ .{ "mrs2[0].oldValue", " And" },
+ }, .{});
}
diff --git a/src/dom/namednodemap.zig b/src/browser/dom/namednodemap.zig
similarity index 76%
rename from src/dom/namednodemap.zig
rename to src/browser/dom/namednodemap.zig
index 67840659..97a85b5c 100644
--- a/src/dom/namednodemap.zig
+++ b/src/browser/dom/namednodemap.zig
@@ -18,18 +18,13 @@
const std = @import("std");
-const parser = @import("netsurf");
-
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
+const parser = @import("../netsurf.zig");
const DOMException = @import("exceptions.zig").DOMException;
// WEB IDL https://dom.spec.whatwg.org/#namednodemap
pub const NamedNodeMap = struct {
pub const Self = parser.NamedNodeMap;
- pub const mem_guarantied = true;
pub const Exception = DOMException;
@@ -80,18 +75,18 @@ pub const NamedNodeMap = struct {
// Tests
// -----
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var setItem = [_]Case{
- .{ .src = "let a = document.getElementById('content').attributes", .ex = "undefined" },
- .{ .src = "a.length", .ex = "1" },
- .{ .src = "a.item(0)", .ex = "[object Attr]" },
- .{ .src = "a.item(1)", .ex = "null" },
- .{ .src = "a.getNamedItem('id')", .ex = "[object Attr]" },
- .{ .src = "a.getNamedItem('foo')", .ex = "null" },
- .{ .src = "a.setNamedItem(a.getNamedItem('id'))", .ex = "[object Attr]" },
- };
- try checkCases(js_env, &setItem);
+const testing = @import("../../testing.zig");
+test "Browser.DOM.NamedNodeMap" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
+
+ try runner.testCases(&.{
+ .{ "let a = document.getElementById('content').attributes", "undefined" },
+ .{ "a.length", "1" },
+ .{ "a.item(0)", "[object Attr]" },
+ .{ "a.item(1)", "null" },
+ .{ "a.getNamedItem('id')", "[object Attr]" },
+ .{ "a.getNamedItem('foo')", "null" },
+ .{ "a.setNamedItem(a.getNamedItem('id'))", "[object Attr]" },
+ }, .{});
}
diff --git a/src/dom/node.zig b/src/browser/dom/node.zig
similarity index 54%
rename from src/dom/node.zig
rename to src/browser/dom/node.zig
index 3e0a7561..a0771b16 100644
--- a/src/dom/node.zig
+++ b/src/browser/dom/node.zig
@@ -18,16 +18,10 @@
const std = @import("std");
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-const runScript = jsruntime.test_utils.runScript;
-const Variadic = jsruntime.Variadic;
-
-const generate = @import("../generate.zig");
-
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
+const generate = @import("../../runtime/generate.zig");
+const SessionState = @import("../env.zig").SessionState;
const EventTarget = @import("event_target.zig").EventTarget;
// DOM
@@ -66,7 +60,6 @@ pub const Union = generate.Union(Interfaces);
pub const Node = struct {
pub const Self = parser.Node;
pub const prototype = *EventTarget;
- pub const mem_guarantied = true;
pub fn toInterface(node: *parser.Node) !Union {
return switch (try parser.nodeType(node)) {
@@ -262,13 +255,14 @@ pub const Node = struct {
return try parser.nodeHasChildNodes(self);
}
- pub fn get_childNodes(self: *parser.Node, alloc: std.mem.Allocator) !NodeList {
+ pub fn get_childNodes(self: *parser.Node, state: *SessionState) !NodeList {
+ const allocator = state.arena;
var list = NodeList.init();
- errdefer list.deinit(alloc);
+ errdefer list.deinit(allocator);
var n = try parser.nodeFirstChild(self) orelse return list;
while (true) {
- try list.append(alloc, n);
+ try list.append(allocator, n);
n = try parser.nodeNextSibling(n) orelse return list;
}
}
@@ -327,11 +321,10 @@ pub const Node = struct {
// For now, it checks only if new nodes are not self.
// TODO implements the others contraints.
// see https://dom.spec.whatwg.org/#concept-node-tree
- pub fn hierarchy(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !bool {
- if (nodes == null) return true;
- if (nodes.?.slice.len == 0) return true;
+ pub fn hierarchy(self: *parser.Node, nodes: []const *parser.Node) !bool {
+ if (nodes.len == 0) return true;
- for (nodes.?.slice) |node| if (self == node) return false;
+ for (nodes) |node| if (self == node) return false;
return true;
}
@@ -339,22 +332,21 @@ pub const Node = struct {
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
- pub fn prepend(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void {
- if (nodes == null) return;
- if (nodes.?.slice.len == 0) return;
+ pub fn prepend(self: *parser.Node, nodes: []const *parser.Node) !void {
+ if (nodes.len == 0) return;
// check hierarchy
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
const first = try parser.nodeFirstChild(self);
if (first == null) {
- for (nodes.?.slice) |node| {
+ for (nodes) |node| {
_ = try parser.nodeAppendChild(self, node);
}
return;
}
- for (nodes.?.slice) |node| {
+ for (nodes) |node| {
_ = try parser.nodeInsertBefore(self, node, first.?);
}
}
@@ -362,14 +354,13 @@ pub const Node = struct {
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
- pub fn append(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void {
- if (nodes == null) return;
- if (nodes.?.slice.len == 0) return;
+ pub fn append(self: *parser.Node, nodes: []const *parser.Node) !void {
+ if (nodes.len == 0) return;
// check hierarchy
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
- for (nodes.?.slice) |node| {
+ for (nodes) |node| {
_ = try parser.nodeAppendChild(self, node);
}
}
@@ -377,9 +368,8 @@ pub const Node = struct {
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
- pub fn replaceChildren(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void {
- if (nodes == null) return;
- if (nodes.?.slice.len == 0) return;
+ pub fn replaceChildren(self: *parser.Node, nodes: []const *parser.Node) !void {
+ if (nodes.len == 0) return;
// check hierarchy
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
@@ -388,7 +378,7 @@ pub const Node = struct {
try removeChildren(self);
// add new children
- for (nodes.?.slice) |node| {
+ for (nodes) |node| {
_ = try parser.nodeAppendChild(self, node);
}
}
@@ -411,219 +401,192 @@ pub const Node = struct {
pub fn deinit(_: *parser.Node, _: std.mem.Allocator) void {}
};
-// Tests
-// -----
+const testing = @import("../../testing.zig");
+test "Browser.DOM.node" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
-pub fn testExecFn(
- alloc: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
+ {
+ var err_out: ?[]const u8 = null;
+ try runner.exec(
+ \\ function trimAndReplace(str) {
+ \\ str = str.replace(/(\r\n|\n|\r)/gm,'');
+ \\ str = str.replace(/\s+/g, ' ');
+ \\ str = str.trim();
+ \\ return str;
+ \\ }
+ , "trimAndReplace", &err_out);
+ }
- // helper functions
- const trim_and_replace =
- \\function trimAndReplace(str) {
- \\str = str.replace(/(\r\n|\n|\r)/gm,'');
- \\str = str.replace(/\s+/g, ' ');
- \\str = str.trim();
- \\return str;
- \\}
- ;
- try runScript(js_env, alloc, trim_and_replace, "proto_test");
+ try runner.testCases(&.{
+ .{ "document.body.compareDocumentPosition(document.firstChild); ", "10" },
+ .{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", "10" },
+ .{ "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "20" },
+ .{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", "0" },
+ .{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", "2" },
+ .{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "4" },
+ }, .{});
- var node_compare_document_position = [_]Case{
- .{ .src = "document.body.compareDocumentPosition(document.firstChild); ", .ex = "10" },
- .{ .src = "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", .ex = "10" },
- .{ .src = "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", .ex = "20" },
- .{ .src = "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", .ex = "0" },
- .{ .src = "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", .ex = "2" },
- .{ .src = "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", .ex = "4" },
- };
- try checkCases(js_env, &node_compare_document_position);
+ try runner.testCases(&.{
+ .{ "document.getElementById('content').getRootNode().__proto__.constructor.name", "HTMLDocument" },
+ }, .{});
- var get_root_node = [_]Case{
- .{ .src = "document.getElementById('content').getRootNode().__proto__.constructor.name", .ex = "HTMLDocument" },
- };
- try checkCases(js_env, &get_root_node);
-
- var first_child = [_]Case{
+ try runner.testCases(&.{
// for next test cases
- .{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "let link = document.getElementById('link')", .ex = "undefined" },
- .{ .src = "let first_child = content.firstChild.nextSibling", .ex = "undefined" }, // nextSibling because of line return \n
+ .{ "let content = document.getElementById('content')", "undefined" },
+ .{ "let link = document.getElementById('link')", "undefined" },
+ .{ "let first_child = content.firstChild.nextSibling", "undefined" }, // nextSibling because of line return \n
- .{ .src = "let body_first_child = document.body.firstChild", .ex = "undefined" },
- .{ .src = "body_first_child.localName", .ex = "div" },
- .{ .src = "body_first_child.__proto__.constructor.name", .ex = "HTMLDivElement" },
- .{ .src = "document.getElementById('para-empty').firstChild.firstChild", .ex = "null" },
- };
- try checkCases(js_env, &first_child);
+ .{ "let body_first_child = document.body.firstChild", "undefined" },
+ .{ "body_first_child.localName", "div" },
+ .{ "body_first_child.__proto__.constructor.name", "HTMLDivElement" },
+ .{ "document.getElementById('para-empty').firstChild.firstChild", "null" },
+ }, .{});
- var last_child = [_]Case{
- .{ .src = "let last_child = content.lastChild.previousSibling", .ex = "undefined" }, // previousSibling because of line return \n
- .{ .src = "last_child.__proto__.constructor.name", .ex = "Comment" },
- };
- try checkCases(js_env, &last_child);
+ try runner.testCases(&.{
+ .{ "let last_child = content.lastChild.previousSibling", "undefined" }, // previousSibling because of line return \n
+ .{ "last_child.__proto__.constructor.name", "Comment" },
+ }, .{});
- var next_sibling = [_]Case{
- .{ .src = "let next_sibling = link.nextSibling.nextSibling", .ex = "undefined" },
- .{ .src = "next_sibling.localName", .ex = "p" },
- .{ .src = "next_sibling.__proto__.constructor.name", .ex = "HTMLParagraphElement" },
- .{ .src = "content.nextSibling.nextSibling", .ex = "null" },
- };
- try checkCases(js_env, &next_sibling);
+ try runner.testCases(&.{
+ .{ "let next_sibling = link.nextSibling.nextSibling", "undefined" },
+ .{ "next_sibling.localName", "p" },
+ .{ "next_sibling.__proto__.constructor.name", "HTMLParagraphElement" },
+ .{ "content.nextSibling.nextSibling", "null" },
+ }, .{});
- var prev_sibling = [_]Case{
- .{ .src = "let prev_sibling = document.getElementById('para-empty').previousSibling.previousSibling", .ex = "undefined" },
- .{ .src = "prev_sibling.localName", .ex = "a" },
- .{ .src = "prev_sibling.__proto__.constructor.name", .ex = "HTMLAnchorElement" },
- .{ .src = "content.previousSibling", .ex = "null" },
- };
- try checkCases(js_env, &prev_sibling);
+ try runner.testCases(&.{
+ .{ "let prev_sibling = document.getElementById('para-empty').previousSibling.previousSibling", "undefined" },
+ .{ "prev_sibling.localName", "a" },
+ .{ "prev_sibling.__proto__.constructor.name", "HTMLAnchorElement" },
+ .{ "content.previousSibling", "null" },
+ }, .{});
- var parent = [_]Case{
- .{ .src = "let parent = document.getElementById('para').parentElement", .ex = "undefined" },
- .{ .src = "parent.localName", .ex = "div" },
- .{ .src = "parent.__proto__.constructor.name", .ex = "HTMLDivElement" },
- .{ .src = "let h = content.parentElement.parentElement", .ex = "undefined" },
- .{ .src = "h.parentElement", .ex = "null" },
- .{ .src = "h.parentNode.__proto__.constructor.name", .ex = "HTMLDocument" },
- };
- try checkCases(js_env, &parent);
+ try runner.testCases(&.{
+ .{ "let parent = document.getElementById('para').parentElement", "undefined" },
+ .{ "parent.localName", "div" },
+ .{ "parent.__proto__.constructor.name", "HTMLDivElement" },
+ .{ "let h = content.parentElement.parentElement", "undefined" },
+ .{ "h.parentElement", "null" },
+ .{ "h.parentNode.__proto__.constructor.name", "HTMLDocument" },
+ }, .{});
- var node_name = [_]Case{
- .{ .src = "first_child.nodeName === 'A'", .ex = "true" },
- .{ .src = "link.firstChild.nodeName === '#text'", .ex = "true" },
- .{ .src = "last_child.nodeName === '#comment'", .ex = "true" },
- .{ .src = "document.nodeName === '#document'", .ex = "true" },
- };
- try checkCases(js_env, &node_name);
+ try runner.testCases(&.{
+ .{ "first_child.nodeName === 'A'", "true" },
+ .{ "link.firstChild.nodeName === '#text'", "true" },
+ .{ "last_child.nodeName === '#comment'", "true" },
+ .{ "document.nodeName === '#document'", "true" },
+ }, .{});
- var node_type = [_]Case{
- .{ .src = "first_child.nodeType === 1", .ex = "true" },
- .{ .src = "link.firstChild.nodeType === 3", .ex = "true" },
- .{ .src = "last_child.nodeType === 8", .ex = "true" },
- .{ .src = "document.nodeType === 9", .ex = "true" },
- };
- try checkCases(js_env, &node_type);
+ try runner.testCases(&.{
+ .{ "first_child.nodeType === 1", "true" },
+ .{ "link.firstChild.nodeType === 3", "true" },
+ .{ "last_child.nodeType === 8", "true" },
+ .{ "document.nodeType === 9", "true" },
+ }, .{});
- var owner = [_]Case{
- .{ .src = "let owner = content.ownerDocument", .ex = "undefined" },
- .{ .src = "owner.__proto__.constructor.name", .ex = "HTMLDocument" },
- .{ .src = "document.ownerDocument", .ex = "null" },
- .{ .src = "let owner2 = document.createElement('div').ownerDocument", .ex = "undefined" },
- .{ .src = "owner2.__proto__.constructor.name", .ex = "HTMLDocument" },
- };
- try checkCases(js_env, &owner);
+ try runner.testCases(&.{
+ .{ "let owner = content.ownerDocument", "undefined" },
+ .{ "owner.__proto__.constructor.name", "HTMLDocument" },
+ .{ "document.ownerDocument", "null" },
+ .{ "let owner2 = document.createElement('div').ownerDocument", "undefined" },
+ .{ "owner2.__proto__.constructor.name", "HTMLDocument" },
+ }, .{});
- var connected = [_]Case{
- .{ .src = "content.isConnected", .ex = "true" },
- .{ .src = "document.isConnected", .ex = "true" },
- .{ .src = "document.createElement('div').isConnected", .ex = "false" },
- };
- try checkCases(js_env, &connected);
+ try runner.testCases(&.{
+ .{ "content.isConnected", "true" },
+ .{ "document.isConnected", "true" },
+ .{ "document.createElement('div').isConnected", "false" },
+ }, .{});
- var node_value = [_]Case{
- .{ .src = "last_child.nodeValue === 'comment'", .ex = "true" },
- .{ .src = "link.nodeValue === null", .ex = "true" },
- .{ .src = "let text = link.firstChild", .ex = "undefined" },
- .{ .src = "text.nodeValue === 'OK'", .ex = "true" },
- .{ .src = "text.nodeValue = 'OK modified'", .ex = "OK modified" },
- .{ .src = "text.nodeValue === 'OK modified'", .ex = "true" },
- .{ .src = "link.nodeValue = 'nothing'", .ex = "nothing" },
- };
- try checkCases(js_env, &node_value);
+ try runner.testCases(&.{
+ .{ "last_child.nodeValue === 'comment'", "true" },
+ .{ "link.nodeValue === null", "true" },
+ .{ "let text = link.firstChild", "undefined" },
+ .{ "text.nodeValue === 'OK'", "true" },
+ .{ "text.nodeValue = 'OK modified'", "OK modified" },
+ .{ "text.nodeValue === 'OK modified'", "true" },
+ .{ "link.nodeValue = 'nothing'", "nothing" },
+ }, .{});
- var node_text_content = [_]Case{
- .{ .src = "text.textContent === 'OK modified'", .ex = "true" },
- .{ .src = "trimAndReplace(content.textContent) === 'OK modified And'", .ex = "true" },
- .{ .src = "text.textContent = 'OK'", .ex = "OK" },
- .{ .src = "text.textContent", .ex = "OK" },
- .{ .src = "trimAndReplace(document.getElementById('para-empty').textContent)", .ex = "" },
- .{ .src = "document.getElementById('para-empty').textContent = 'OK'", .ex = "OK" },
- .{ .src = "document.getElementById('para-empty').firstChild.nodeName === '#text'", .ex = "true" },
- };
- try checkCases(js_env, &node_text_content);
+ try runner.testCases(&.{
+ .{ "text.textContent === 'OK modified'", "true" },
+ .{ "trimAndReplace(content.textContent) === 'OK modified And'", "true" },
+ .{ "text.textContent = 'OK'", "OK" },
+ .{ "text.textContent", "OK" },
+ .{ "trimAndReplace(document.getElementById('para-empty').textContent)", "" },
+ .{ "document.getElementById('para-empty').textContent = 'OK'", "OK" },
+ .{ "document.getElementById('para-empty').firstChild.nodeName === '#text'", "true" },
+ }, .{});
- var node_append_child = [_]Case{
- .{ .src = "let append = document.createElement('h1')", .ex = "undefined" },
- .{ .src = "content.appendChild(append).toString()", .ex = "[object HTMLHeadingElement]" },
- .{ .src = "content.lastChild.__proto__.constructor.name", .ex = "HTMLHeadingElement" },
- .{ .src = "content.appendChild(link).toString()", .ex = "[object HTMLAnchorElement]" },
- };
- try checkCases(js_env, &node_append_child);
+ try runner.testCases(&.{
+ .{ "let append = document.createElement('h1')", "undefined" },
+ .{ "content.appendChild(append).toString()", "[object HTMLHeadingElement]" },
+ .{ "content.lastChild.__proto__.constructor.name", "HTMLHeadingElement" },
+ .{ "content.appendChild(link).toString()", "[object HTMLAnchorElement]" },
+ }, .{});
- var node_clone = [_]Case{
- .{ .src = "let clone = link.cloneNode()", .ex = "undefined" },
- .{ .src = "clone.toString()", .ex = "[object HTMLAnchorElement]" },
- .{ .src = "clone.parentNode === null", .ex = "true" },
- .{ .src = "clone.firstChild === null", .ex = "true" },
- .{ .src = "let clone_deep = link.cloneNode(true)", .ex = "undefined" },
- .{ .src = "clone_deep.firstChild.nodeName === '#text'", .ex = "true" },
- };
- try checkCases(js_env, &node_clone);
+ try runner.testCases(&.{
+ .{ "let clone = link.cloneNode()", "undefined" },
+ .{ "clone.toString()", "[object HTMLAnchorElement]" },
+ .{ "clone.parentNode === null", "true" },
+ .{ "clone.firstChild === null", "true" },
+ .{ "let clone_deep = link.cloneNode(true)", "undefined" },
+ .{ "clone_deep.firstChild.nodeName === '#text'", "true" },
+ }, .{});
- var node_contains = [_]Case{
- .{ .src = "link.contains(text)", .ex = "true" },
- .{ .src = "text.contains(link)", .ex = "false" },
- };
- try checkCases(js_env, &node_contains);
+ try runner.testCases(&.{
+ .{ "link.contains(text)", "true" },
+ .{ "text.contains(link)", "false" },
+ }, .{});
- var node_has_child_nodes = [_]Case{
- .{ .src = "link.hasChildNodes()", .ex = "true" },
- .{ .src = "text.hasChildNodes()", .ex = "false" },
- };
- try checkCases(js_env, &node_has_child_nodes);
+ try runner.testCases(&.{
+ .{ "link.hasChildNodes()", "true" },
+ .{ "text.hasChildNodes()", "false" },
+ }, .{});
- var node_child_nodes = [_]Case{
- .{ .src = "link.childNodes.length", .ex = "1" },
- .{ .src = "text.childNodes.length", .ex = "0" },
- };
- try checkCases(js_env, &node_child_nodes);
+ try runner.testCases(&.{
+ .{ "link.childNodes.length", "1" },
+ .{ "text.childNodes.length", "0" },
+ }, .{});
- var node_insert_before = [_]Case{
- .{ .src = "let insertBefore = document.createElement('a')", .ex = "undefined" },
- .{ .src = "link.insertBefore(insertBefore, text) !== undefined", .ex = "true" },
- .{ .src = "link.firstChild.localName === 'a'", .ex = "true" },
- };
- try checkCases(js_env, &node_insert_before);
+ try runner.testCases(&.{
+ .{ "let insertBefore = document.createElement('a')", "undefined" },
+ .{ "link.insertBefore(insertBefore, text) !== undefined", "true" },
+ .{ "link.firstChild.localName === 'a'", "true" },
+ }, .{});
- var node_is_default_namespace = [_]Case{
+ try runner.testCases(&.{
// TODO: does not seems to work
- // .{ .src = "link.isDefaultNamespace('')", .ex = "true" },
- .{ .src = "link.isDefaultNamespace('false')", .ex = "false" },
- };
- try checkCases(js_env, &node_is_default_namespace);
+ // .{ "link.isDefaultNamespace('')", "true" },
+ .{ "link.isDefaultNamespace('false')", "false" },
+ }, .{});
- var node_is_equal_node = [_]Case{
- .{ .src = "let equal1 = document.createElement('a')", .ex = "undefined" },
- .{ .src = "let equal2 = document.createElement('a')", .ex = "undefined" },
- .{ .src = "equal1.textContent = 'is equal'", .ex = "is equal" },
- .{ .src = "equal2.textContent = 'is equal'", .ex = "is equal" },
+ try runner.testCases(&.{
+ .{ "let equal1 = document.createElement('a')", "undefined" },
+ .{ "let equal2 = document.createElement('a')", "undefined" },
+ .{ "equal1.textContent = 'is equal'", "is equal" },
+ .{ "equal2.textContent = 'is equal'", "is equal" },
// TODO: does not seems to work
- // .{ .src = "equal1.isEqualNode(equal2)", .ex = "true" },
- };
- try checkCases(js_env, &node_is_equal_node);
+ // .{ "equal1.isEqualNode(equal2)", "true" },
+ }, .{});
- var node_is_same_node = [_]Case{
- .{ .src = "document.body.isSameNode(document.body)", .ex = "true" },
- };
- try checkCases(js_env, &node_is_same_node);
+ try runner.testCases(&.{
+ .{ "document.body.isSameNode(document.body)", "true" },
+ }, .{});
- var node_normalize = [_]Case{
+ try runner.testCases(&.{
// TODO: no test
- .{ .src = "link.normalize()", .ex = "undefined" },
- };
- try checkCases(js_env, &node_normalize);
+ .{ "link.normalize()", "undefined" },
+ }, .{});
- var node_remove_child = [_]Case{
- .{ .src = "content.removeChild(append) !== undefined", .ex = "true" },
- .{ .src = "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", .ex = "true" },
- };
- try checkCases(js_env, &node_remove_child);
+ try runner.testCases(&.{
+ .{ "content.removeChild(append) !== undefined", "true" },
+ .{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" },
+ }, .{});
- var node_replace_child = [_]Case{
- .{ .src = "let replace = document.createElement('div')", .ex = "undefined" },
- .{ .src = "link.replaceChild(replace, insertBefore) !== undefined", .ex = "true" },
- };
- try checkCases(js_env, &node_replace_child);
+ try runner.testCases(&.{
+ .{ "let replace = document.createElement('div')", "undefined" },
+ .{ "link.replaceChild(replace, insertBefore) !== undefined", "true" },
+ }, .{});
}
diff --git a/src/dom/nodelist.zig b/src/browser/dom/nodelist.zig
similarity index 64%
rename from src/dom/nodelist.zig
rename to src/browser/dom/nodelist.zig
index 5dfc7031..2d27f4d0 100644
--- a/src/dom/nodelist.zig
+++ b/src/browser/dom/nodelist.zig
@@ -18,13 +18,11 @@
const std = @import("std");
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
-const jsruntime = @import("jsruntime");
-const Callback = jsruntime.Callback;
-const CallbackResult = jsruntime.CallbackResult;
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
+const JsObject = @import("../env.zig").JsObject;
+const Callback = @import("../env.zig").Callback;
+const SessionState = @import("../env.zig").SessionState;
const NodeUnion = @import("node.zig").Union;
const Node = @import("node.zig").Node;
@@ -41,8 +39,6 @@ pub const Interfaces = .{
};
pub const NodeListIterator = struct {
- pub const mem_guarantied = true;
-
coll: *NodeList,
index: u32 = 0,
@@ -69,8 +65,6 @@ pub const NodeListIterator = struct {
};
pub const NodeListEntriesIterator = struct {
- pub const mem_guarantied = true;
-
coll: *NodeList,
index: u32 = 0,
@@ -104,7 +98,6 @@ pub const NodeListEntriesIterator = struct {
// implementation allows only static nodelist.
// see https://dom.spec.whatwg.org/#old-style-collections
pub const NodeList = struct {
- pub const mem_guarantied = true;
pub const Exception = DOMException;
const NodesArrayList = std.ArrayListUnmanaged(*parser.Node);
@@ -130,7 +123,7 @@ pub const NodeList = struct {
return @intCast(self.nodes.items.len);
}
- pub fn _item(self: *NodeList, index: u32) !?NodeUnion {
+ pub fn _item(self: *const NodeList, index: u32) !?NodeUnion {
if (index >= self.nodes.items.len) {
return null;
}
@@ -139,17 +132,30 @@ pub const NodeList = struct {
return try Node.toInterface(n);
}
- pub fn _forEach(self: *NodeList, alloc: std.mem.Allocator, cbk: Callback) !void { // TODO handle thisArg
- var res = CallbackResult.init(alloc);
- defer res.deinit();
+ // This code works, but it's _MUCH_ slower than using postAttach. The benefit
+ // of this version, is that it's "live"..but we're talking many orders of
+ // magnitude slower.
+ //
+ // You can test it by commenting out `postAttach`, uncommenting this and
+ // running:
+ // zig build wpt -- tests/wpt/dom/nodes/NodeList-static-length-getter-tampered-indexOf-1.html
+ //
+ // I think this _is_ the right way to do it, but I must be doing something
+ // wrong to make it so slow.
+ // pub fn indexed_get(self: *const NodeList, index: u32, has_value: *bool) !?NodeUnion {
+ // return (try self._item(index)) orelse {
+ // has_value.* = false;
+ // return null;
+ // };
+ // }
+ pub fn _forEach(self: *NodeList, cbk: Callback) !void { // TODO handle thisArg
for (self.nodes.items, 0..) |n, i| {
const ii: u32 = @intCast(i);
- cbk.trycall(.{ n, ii, self }, &res) catch |e| {
- log.err("callback error: {s}", .{res.result orelse "unknown"});
- log.debug("{s}", .{res.stack orelse "no stack trace"});
-
- return e;
+ var result: Callback.Result = undefined;
+ cbk.tryCall(.{ n, ii, self }, &result) catch {
+ log.err("callback error: {s}", .{result.exception});
+ log.debug("stack:\n{s}", .{result.stack orelse "???"});
};
}
}
@@ -171,38 +177,32 @@ pub const NodeList = struct {
}
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
-
- pub fn postAttach(self: *NodeList, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void {
- const ln = self.get_length();
- var i: u32 = 0;
- while (i < ln) {
- defer i += 1;
- const k = try std.fmt.allocPrint(alloc, "{d}", .{i});
-
- const node = try self._item(i) orelse unreachable;
- try js_obj.set(k, node);
+ pub fn postAttach(self: *NodeList, js_obj: JsObject) !void {
+ const len = self.get_length();
+ for (0..len) |i| {
+ const node = try self._item(@intCast(i)) orelse unreachable;
+ try js_obj.setIndex(i, node);
}
}
};
-// Tests
-// -----
+const testing = @import("../../testing.zig");
+test "Browser.DOM.NodeList" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var childnodes = [_]Case{
- .{ .src = "let list = document.getElementById('content').childNodes", .ex = "undefined" },
- .{ .src = "list.length", .ex = "9" },
- .{ .src = "list[0].__proto__.constructor.name", .ex = "Text" },
- .{ .src =
- \\let i = 0;
- \\list.forEach(function (n, idx) {
- \\ i += idx;
- \\});
- \\i;
- , .ex = "36" },
- };
- try checkCases(js_env, &childnodes);
+ try runner.testCases(&.{
+ .{ "let list = document.getElementById('content').childNodes", "undefined" },
+ .{ "list.length", "9" },
+ .{ "list[0].__proto__.constructor.name", "Text" },
+ .{
+ \\ let i = 0;
+ \\ list.forEach(function (n, idx) {
+ \\ i += idx;
+ \\ });
+ \\ i;
+ ,
+ "36",
+ },
+ }, .{});
}
diff --git a/src/dom/processing_instruction.zig b/src/browser/dom/processing_instruction.zig
similarity index 71%
rename from src/dom/processing_instruction.zig
rename to src/browser/dom/processing_instruction.zig
index fc932ec5..d8960490 100644
--- a/src/dom/processing_instruction.zig
+++ b/src/browser/dom/processing_instruction.zig
@@ -18,11 +18,7 @@
const std = @import("std");
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
const Node = @import("node.zig").Node;
// https://dom.spec.whatwg.org/#processinginstruction
@@ -32,7 +28,6 @@ pub const ProcessingInstruction = struct {
// TODO for libdom processing instruction inherit from node.
// But the spec says it must inherit from CDATA.
pub const prototype = *Node;
- pub const mem_guarantied = true;
pub fn get_target(self: *parser.ProcessingInstruction) ![]const u8 {
// libdom stores the ProcessingInstruction target in the node's name.
@@ -52,18 +47,18 @@ pub const ProcessingInstruction = struct {
}
};
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var createProcessingInstruction = [_]Case{
- .{ .src = "let pi = document.createProcessingInstruction('foo', 'bar')", .ex = "undefined" },
- .{ .src = "pi.target", .ex = "foo" },
- .{ .src = "pi.data", .ex = "bar" },
- .{ .src = "pi.data = 'foo'", .ex = "foo" },
- .{ .src = "pi.data", .ex = "foo" },
+const testing = @import("../../testing.zig");
+test "Browser.DOM.ProcessingInstruction" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
- .{ .src = "let pi2 = pi.cloneNode()", .ex = "undefined" },
- };
- try checkCases(js_env, &createProcessingInstruction);
+ try runner.testCases(&.{
+ .{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" },
+ .{ "pi.target", "foo" },
+ .{ "pi.data", "bar" },
+ .{ "pi.data = 'foo'", "foo" },
+ .{ "pi.data", "foo" },
+
+ .{ "let pi2 = pi.cloneNode()", "undefined" },
+ }, .{});
}
diff --git a/src/dom/text.zig b/src/browser/dom/text.zig
similarity index 53%
rename from src/dom/text.zig
rename to src/browser/dom/text.zig
index a2d3f9cf..aa99deca 100644
--- a/src/dom/text.zig
+++ b/src/browser/dom/text.zig
@@ -18,17 +18,12 @@
const std = @import("std");
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
+const SessionState = @import("../env.zig").SessionState;
const CharacterData = @import("character_data.zig").CharacterData;
const CDATASection = @import("cdata_section.zig").CDATASection;
-const UserContext = @import("../user_context.zig").UserContext;
-
// Text interfaces
pub const Interfaces = .{
CDATASection,
@@ -37,11 +32,10 @@ pub const Interfaces = .{
pub const Text = struct {
pub const Self = parser.Text;
pub const prototype = *CharacterData;
- pub const mem_guarantied = true;
- pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Text {
+ pub fn constructor(data: ?[]const u8, state: *const SessionState) !*parser.Text {
return parser.documentCreateTextNode(
- parser.documentHTMLToDocument(userctx.document),
+ parser.documentHTMLToDocument(state.document.?),
data orelse "",
);
}
@@ -66,30 +60,28 @@ pub const Text = struct {
// Tests
// -----
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var constructor = [_]Case{
- .{ .src = "let t = new Text('foo')", .ex = "undefined" },
- .{ .src = "t.data", .ex = "foo" },
+const testing = @import("../../testing.zig");
+test "Browser.DOM.Text" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
- .{ .src = "let emptyt = new Text()", .ex = "undefined" },
- .{ .src = "emptyt.data", .ex = "" },
- };
- try checkCases(js_env, &constructor);
+ try runner.testCases(&.{
+ .{ "let t = new Text('foo')", "undefined" },
+ .{ "t.data", "foo" },
- var get_whole_text = [_]Case{
- .{ .src = "let text = document.getElementById('link').firstChild", .ex = "undefined" },
- .{ .src = "text.wholeText === 'OK'", .ex = "true" },
- };
- try checkCases(js_env, &get_whole_text);
+ .{ "let emptyt = new Text()", "undefined" },
+ .{ "emptyt.data", "" },
+ }, .{});
- var split_text = [_]Case{
- .{ .src = "text.data = 'OK modified'", .ex = "OK modified" },
- .{ .src = "let split = text.splitText('OK'.length)", .ex = "undefined" },
- .{ .src = "split.data === ' modified'", .ex = "true" },
- .{ .src = "text.data === 'OK'", .ex = "true" },
- };
- try checkCases(js_env, &split_text);
+ try runner.testCases(&.{
+ .{ "let text = document.getElementById('link').firstChild", "undefined" },
+ .{ "text.wholeText === 'OK'", "true" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "text.data = 'OK modified'", "OK modified" },
+ .{ "let split = text.splitText('OK'.length)", "undefined" },
+ .{ "split.data === ' modified'", "true" },
+ .{ "text.data === 'OK'", "true" },
+ }, .{});
}
diff --git a/src/dom/token_list.zig b/src/browser/dom/token_list.zig
similarity index 56%
rename from src/dom/token_list.zig
rename to src/browser/dom/token_list.zig
index 0ed75997..0472e81e 100644
--- a/src/dom/token_list.zig
+++ b/src/browser/dom/token_list.zig
@@ -18,12 +18,7 @@
const std = @import("std");
-const parser = @import("netsurf");
-
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-const Variadic = jsruntime.Variadic;
+const parser = @import("../netsurf.zig");
const DOMException = @import("exceptions.zig").DOMException;
@@ -31,7 +26,6 @@ const DOMException = @import("exceptions.zig").DOMException;
pub const DOMTokenList = struct {
pub const Self = parser.TokenList;
pub const Exception = DOMException;
- pub const mem_guarantied = true;
pub fn get_length(self: *parser.TokenList) !u32 {
return parser.tokenListGetLength(self);
@@ -45,16 +39,14 @@ pub const DOMTokenList = struct {
return parser.tokenListContains(self, token);
}
- pub fn _add(self: *parser.TokenList, tokens: ?Variadic([]const u8)) !void {
- if (tokens == null) return;
- for (tokens.?.slice) |token| {
+ pub fn _add(self: *parser.TokenList, tokens: []const []const u8) !void {
+ for (tokens) |token| {
try parser.tokenListAdd(self, token);
}
}
- pub fn _remove(self: *parser.TokenList, tokens: ?Variadic([]const u8)) !void {
- if (tokens == null) return;
- for (tokens.?.slice) |token| {
+ pub fn _remove(self: *parser.TokenList, tokens: []const []const u8) !void {
+ for (tokens) |token| {
try parser.tokenListRemove(self, token);
}
}
@@ -113,52 +105,49 @@ pub const DOMTokenList = struct {
// Tests
// -----
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var dynamiclist = [_]Case{
- .{ .src = "let gs = document.getElementById('para-empty')", .ex = "undefined" },
- .{ .src = "let cl = gs.classList", .ex = "undefined" },
- .{ .src = "gs.className", .ex = "ok empty" },
- .{ .src = "cl.value", .ex = "ok empty" },
- .{ .src = "cl.length", .ex = "2" },
- .{ .src = "gs.className = 'foo bar baz'", .ex = "foo bar baz" },
- .{ .src = "gs.className", .ex = "foo bar baz" },
- .{ .src = "cl.length", .ex = "3" },
- .{ .src = "gs.className = 'ok empty'", .ex = "ok empty" },
- .{ .src = "cl.length", .ex = "2" },
- };
- try checkCases(js_env, &dynamiclist);
+const testing = @import("../../testing.zig");
+test "Browser.DOM.TokenList" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
- var testcases = [_]Case{
- .{ .src = "let cl2 = gs.classList", .ex = "undefined" },
- .{ .src = "cl2.length", .ex = "2" },
- .{ .src = "cl2.item(0)", .ex = "ok" },
- .{ .src = "cl2.item(1)", .ex = "empty" },
- .{ .src = "cl2.contains('ok')", .ex = "true" },
- .{ .src = "cl2.contains('nok')", .ex = "false" },
- .{ .src = "cl2.add('foo', 'bar', 'baz')", .ex = "undefined" },
- .{ .src = "cl2.length", .ex = "5" },
- .{ .src = "cl2.remove('foo', 'bar', 'baz')", .ex = "undefined" },
- .{ .src = "cl2.length", .ex = "2" },
- };
- try checkCases(js_env, &testcases);
+ try runner.testCases(&.{
+ .{ "let gs = document.getElementById('para-empty')", "undefined" },
+ .{ "let cl = gs.classList", "undefined" },
+ .{ "gs.className", "ok empty" },
+ .{ "cl.value", "ok empty" },
+ .{ "cl.length", "2" },
+ .{ "gs.className = 'foo bar baz'", "foo bar baz" },
+ .{ "gs.className", "foo bar baz" },
+ .{ "cl.length", "3" },
+ .{ "gs.className = 'ok empty'", "ok empty" },
+ .{ "cl.length", "2" },
+ }, .{});
- var toogle = [_]Case{
- .{ .src = "let cl3 = gs.classList", .ex = "undefined" },
- .{ .src = "cl3.toggle('ok')", .ex = "false" },
- .{ .src = "cl3.toggle('ok')", .ex = "true" },
- .{ .src = "cl3.length", .ex = "2" },
- };
- try checkCases(js_env, &toogle);
+ try runner.testCases(&.{
+ .{ "let cl2 = gs.classList", "undefined" },
+ .{ "cl2.length", "2" },
+ .{ "cl2.item(0)", "ok" },
+ .{ "cl2.item(1)", "empty" },
+ .{ "cl2.contains('ok')", "true" },
+ .{ "cl2.contains('nok')", "false" },
+ .{ "cl2.add('foo', 'bar', 'baz')", "undefined" },
+ .{ "cl2.length", "5" },
+ .{ "cl2.remove('foo', 'bar', 'baz')", "undefined" },
+ .{ "cl2.length", "2" },
+ }, .{});
- var replace = [_]Case{
- .{ .src = "let cl4 = gs.classList", .ex = "undefined" },
- .{ .src = "cl4.replace('ok', 'nok')", .ex = "true" },
- .{ .src = "cl4.value", .ex = "empty nok" },
- .{ .src = "cl4.replace('nok', 'ok')", .ex = "true" },
- .{ .src = "cl4.value", .ex = "empty ok" },
- };
- try checkCases(js_env, &replace);
+ try runner.testCases(&.{
+ .{ "let cl3 = gs.classList", "undefined" },
+ .{ "cl3.toggle('ok')", "false" },
+ .{ "cl3.toggle('ok')", "true" },
+ .{ "cl3.length", "2" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "let cl4 = gs.classList", "undefined" },
+ .{ "cl4.replace('ok', 'nok')", "true" },
+ .{ "cl4.value", "empty nok" },
+ .{ "cl4.replace('nok', 'ok')", "true" },
+ .{ "cl4.value", "empty ok" },
+ }, .{});
}
diff --git a/src/dom/walker.zig b/src/browser/dom/walker.zig
similarity index 98%
rename from src/dom/walker.zig
rename to src/browser/dom/walker.zig
index 6f2c2fba..ad7ba5f7 100644
--- a/src/dom/walker.zig
+++ b/src/browser/dom/walker.zig
@@ -18,7 +18,7 @@
const std = @import("std");
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
pub const Walker = union(enum) {
walkerDepthFirst: WalkerDepthFirst,
diff --git a/src/browser/dump.zig b/src/browser/dump.zig
index 23fff552..4c4e7996 100644
--- a/src/browser/dump.zig
+++ b/src/browser/dump.zig
@@ -19,8 +19,8 @@
const std = @import("std");
const File = std.fs.File;
-const parser = @import("netsurf");
-const Walker = @import("../dom/walker.zig").WalkerChildren;
+const parser = @import("netsurf.zig");
+const Walker = @import("dom/walker.zig").WalkerChildren;
// writer must be a std.io.Writer
pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {
diff --git a/src/browser/env.zig b/src/browser/env.zig
new file mode 100644
index 00000000..0fd4370d
--- /dev/null
+++ b/src/browser/env.zig
@@ -0,0 +1,36 @@
+const std = @import("std");
+
+const parser = @import("netsurf.zig");
+const URL = @import("../url.zig").URL;
+const js = @import("../runtime/js.zig");
+const storage = @import("storage/storage.zig");
+const generate = @import("../runtime/generate.zig");
+const Loop = @import("../runtime/loop.zig").Loop;
+const HttpClient = @import("../http/client.zig").Client;
+const Renderer = @import("browser.zig").Renderer;
+
+const Interfaces = generate.Tuple(.{
+ @import("console/console.zig").Console,
+ @import("dom/dom.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("xmlserializer/xmlserializer.zig").Interfaces,
+});
+
+pub const JsObject = Env.JsObject;
+pub const Callback = Env.Callback;
+pub const Env = js.Env(*SessionState, Interfaces{});
+
+pub const SessionState = struct {
+ loop: *Loop,
+ url: *const URL,
+ renderer: *Renderer,
+ arena: std.mem.Allocator,
+ http_client: *HttpClient,
+ cookie_jar: *storage.CookieJar,
+ document: ?*parser.DocumentHTML,
+};
diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig
new file mode 100644
index 00000000..517b852d
--- /dev/null
+++ b/src/browser/events/event.zig
@@ -0,0 +1,245 @@
+// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+
+const parser = @import("../netsurf.zig");
+const Callback = @import("../env.zig").Callback;
+const generate = @import("../../runtime/generate.zig");
+
+const DOMException = @import("../dom/exceptions.zig").DOMException;
+const EventTarget = @import("../dom/event_target.zig").EventTarget;
+const EventTargetUnion = @import("../dom/event_target.zig").Union;
+
+const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
+
+const log = std.log.scoped(.events);
+
+// Event interfaces
+pub const Interfaces = .{
+ Event,
+ ProgressEvent,
+};
+
+pub const Union = generate.Union(Interfaces);
+
+// https://dom.spec.whatwg.org/#event
+pub const Event = struct {
+ pub const Self = parser.Event;
+ pub const Exception = DOMException;
+
+ pub const EventInit = parser.EventInit;
+
+ // JS
+ // --
+
+ pub const _CAPTURING_PHASE = 1;
+ pub const _AT_TARGET = 2;
+ pub const _BUBBLING_PHASE = 3;
+
+ pub fn toInterface(evt: *parser.Event) !Union {
+ return switch (try parser.eventGetInternalType(evt)) {
+ .event => .{ .Event = evt },
+ .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
+ };
+ }
+
+ pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event {
+ const event = try parser.eventCreate();
+ try parser.eventInit(event, eventType, opts orelse EventInit{});
+ return event;
+ }
+
+ // Getters
+
+ pub fn get_type(self: *parser.Event) ![]const u8 {
+ return try parser.eventType(self);
+ }
+
+ pub fn get_target(self: *parser.Event) !?EventTargetUnion {
+ const et = try parser.eventTarget(self);
+ if (et == null) return null;
+ return try EventTarget.toInterface(et.?);
+ }
+
+ pub fn get_currentTarget(self: *parser.Event) !?EventTargetUnion {
+ const et = try parser.eventCurrentTarget(self);
+ if (et == null) return null;
+ return try EventTarget.toInterface(et.?);
+ }
+
+ pub fn get_eventPhase(self: *parser.Event) !u8 {
+ return try parser.eventPhase(self);
+ }
+
+ pub fn get_bubbles(self: *parser.Event) !bool {
+ return try parser.eventBubbles(self);
+ }
+
+ pub fn get_cancelable(self: *parser.Event) !bool {
+ return try parser.eventCancelable(self);
+ }
+
+ pub fn get_defaultPrevented(self: *parser.Event) !bool {
+ return try parser.eventDefaultPrevented(self);
+ }
+
+ pub fn get_isTrusted(self: *parser.Event) !bool {
+ return try parser.eventIsTrusted(self);
+ }
+
+ pub fn get_timestamp(self: *parser.Event) !u32 {
+ return try parser.eventTimestamp(self);
+ }
+
+ // Methods
+
+ pub fn _initEvent(
+ self: *parser.Event,
+ eventType: []const u8,
+ bubbles: ?bool,
+ cancelable: ?bool,
+ ) !void {
+ const opts = EventInit{
+ .bubbles = bubbles orelse false,
+ .cancelable = cancelable orelse false,
+ };
+ return try parser.eventInit(self, eventType, opts);
+ }
+
+ pub fn _stopPropagation(self: *parser.Event) !void {
+ return try parser.eventStopPropagation(self);
+ }
+
+ pub fn _stopImmediatePropagation(self: *parser.Event) !void {
+ return try parser.eventStopImmediatePropagation(self);
+ }
+
+ pub fn _preventDefault(self: *parser.Event) !void {
+ return try parser.eventPreventDefault(self);
+ }
+};
+
+pub const EventHandler = struct {
+ fn handle(event: ?*parser.Event, data: parser.EventHandlerData) void {
+ var result: Callback.Result = undefined;
+ data.cbk.tryCall(.{if (event) |evt| Event.toInterface(evt) catch unreachable else null}, &result) catch {
+ log.err("event handler error: {s}", .{result.exception});
+ log.debug("stack:\n{s}", .{result.stack orelse "???"});
+ };
+ }
+}.handle;
+
+const testing = @import("../../testing.zig");
+test "Browser.Event" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
+
+ try runner.testCases(&.{
+ .{ "let content = document.getElementById('content')", "undefined" },
+ .{ "let para = document.getElementById('para')", "undefined" },
+ .{ "var nb = 0; var evt", "undefined" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{
+ \\ content.addEventListener('target', function(e) {
+ \\ evt = e; nb = nb + 1;
+ \\ e.preventDefault();
+ \\ })
+ ,
+ "undefined",
+ },
+ .{ "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", "false" },
+ .{ "nb", "1" },
+ .{ "evt.target === content", "true" },
+ .{ "evt.bubbles", "true" },
+ .{ "evt.cancelable", "true" },
+ .{ "evt.defaultPrevented", "true" },
+ .{ "evt.isTrusted", "true" },
+ .{ "evt.timestamp > 1704063600", "true" }, // 2024/01/01 00:00
+ // event.type, event.currentTarget, event.phase checked in EventTarget
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "nb = 0", "0" },
+ .{
+ \\ content.addEventListener('stop',function(e) {
+ \\ e.stopPropagation();
+ \\ nb = nb + 1;
+ \\ }, true)
+ ,
+ "undefined",
+ },
+ // the following event listener will not be invoked
+ .{
+ \\ para.addEventListener('stop',function(e) {
+ \\ nb = nb + 1;
+ \\ })
+ ,
+ "undefined",
+ },
+ .{ "para.dispatchEvent(new Event('stop'))", "true" },
+ .{ "nb", "1" }, // will be 2 if event was not stopped at content event listener
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "nb = 0", "0" },
+ .{
+ \\ content.addEventListener('immediate', function(e) {
+ \\ e.stopImmediatePropagation();
+ \\ nb = nb + 1;
+ \\ })
+ ,
+ "undefined",
+ },
+ // the following event listener will not be invoked
+ .{
+ \\ content.addEventListener('immediate', function(e) {
+ \\ nb = nb + 1;
+ \\ })
+ ,
+ "undefined",
+ },
+ .{ "content.dispatchEvent(new Event('immediate'))", "true" },
+ .{ "nb", "1" }, // will be 2 if event was not stopped at first content event listener
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "nb = 0", "0" },
+ .{
+ \\ content.addEventListener('legacy', function(e) {
+ \\ evt = e; nb = nb + 1;
+ \\ })
+ ,
+ "undefined",
+ },
+ .{ "let evtLegacy = document.createEvent('Event')", "undefined" },
+ .{ "evtLegacy.initEvent('legacy')", "undefined" },
+ .{ "content.dispatchEvent(evtLegacy)", "true" },
+ .{ "nb", "1" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", "undefined" },
+ .{ "document.addEventListener('count', cbk)", "undefined" },
+ .{ "document.removeEventListener('count', cbk)", "undefined" },
+ .{ "document.dispatchEvent(new Event('count'))", "true" },
+ .{ "nb", "0" },
+ }, .{});
+}
diff --git a/src/html/document.zig b/src/browser/html/document.zig
similarity index 57%
rename from src/html/document.zig
rename to src/browser/html/document.zig
index 19567078..d434ee21 100644
--- a/src/html/document.zig
+++ b/src/browser/html/document.zig
@@ -18,11 +18,8 @@
const std = @import("std");
-const parser = @import("netsurf");
-
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
+const parser = @import("../netsurf.zig");
+const SessionState = @import("../env.zig").SessionState;
const Node = @import("../dom/node.zig").Node;
const Document = @import("../dom/document.zig").Document;
@@ -32,15 +29,12 @@ const Location = @import("location.zig").Location;
const collection = @import("../dom/html_collection.zig");
const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
-
-const UserContext = @import("../user_context.zig").UserContext;
const Cookie = @import("../storage/cookie.zig").Cookie;
// WEB IDL https://html.spec.whatwg.org/#the-document-object
pub const HTMLDocument = struct {
pub const Self = parser.DocumentHTML;
pub const prototype = *Document;
- pub const mem_guarantied = true;
pub const sub_type = "node";
@@ -84,20 +78,18 @@ pub const HTMLDocument = struct {
}
}
- pub fn get_cookie(_: *parser.DocumentHTML, arena: std.mem.Allocator, userctx: UserContext) ![]const u8 {
+ pub fn get_cookie(_: *parser.DocumentHTML, state: *SessionState) ![]const u8 {
var buf: std.ArrayListUnmanaged(u8) = .{};
- try userctx.cookie_jar.forRequest(&userctx.url.uri, buf.writer(arena), .{ .navigation = true });
+ try state.cookie_jar.forRequest(&state.url.uri, buf.writer(state.arena), .{ .navigation = true });
return buf.items;
}
- pub fn set_cookie(_: *parser.DocumentHTML, userctx: UserContext, cookie_str: []const u8) ![]const u8 {
+ pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, state: *SessionState) ![]const u8 {
// we use the cookie jar's allocator to parse the cookie because it
// outlives the page's arena.
- const c = try Cookie.parse(userctx.cookie_jar.allocator, &userctx.url.uri, cookie_str);
+ const c = try Cookie.parse(state.cookie_jar.allocator, &state.url.uri, cookie_str);
errdefer c.deinit();
-
- try userctx.cookie_jar.add(c, std.time.timestamp());
-
+ try state.cookie_jar.add(c, std.time.timestamp());
return cookie_str;
}
@@ -110,44 +102,45 @@ pub const HTMLDocument = struct {
return v;
}
- pub fn _getElementsByName(self: *parser.DocumentHTML, alloc: std.mem.Allocator, name: []const u8) !NodeList {
+ pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, state: *SessionState) !NodeList {
+ const arena = state.arena;
var list = NodeList.init();
- errdefer list.deinit(alloc);
+ errdefer list.deinit(arena);
if (name.len == 0) return list;
const root = parser.documentHTMLToNode(self);
- var c = try collection.HTMLCollectionByName(alloc, root, name, false);
+ var c = try collection.HTMLCollectionByName(arena, root, name, false);
const ln = try c.get_length();
var i: u32 = 0;
while (i < ln) {
const n = try c.item(i) orelse break;
- try list.append(alloc, n);
+ try list.append(arena, n);
i += 1;
}
return list;
}
- pub fn get_images(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection {
- return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "img", false);
+ pub fn get_images(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
+ return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "img", false);
}
- pub fn get_embeds(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection {
- return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "embed", false);
+ pub fn get_embeds(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
+ return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "embed", false);
}
- pub fn get_plugins(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection {
- return get_embeds(self, alloc);
+ pub fn get_plugins(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
+ return get_embeds(self, state);
}
- pub fn get_forms(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection {
- return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "form", false);
+ pub fn get_forms(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
+ return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "form", false);
}
- pub fn get_scripts(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection {
- return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "script", false);
+ pub fn get_scripts(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
+ return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "script", false);
}
pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection {
@@ -218,62 +211,57 @@ pub const HTMLDocument = struct {
pub fn set_bgColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
return "";
}
-
- pub fn deinit(_: *parser.DocumentHTML, _: std.mem.Allocator) void {}
};
// Tests
// -----
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var constructor = [_]Case{
- .{ .src = "document.__proto__.constructor.name", .ex = "HTMLDocument" },
- .{ .src = "document.__proto__.__proto__.constructor.name", .ex = "Document" },
- .{ .src = "document.body.localName == 'body'", .ex = "true" },
- };
- try checkCases(js_env, &constructor);
+const testing = @import("../../testing.zig");
- var getters = [_]Case{
- .{ .src = "document.domain", .ex = "" },
- .{ .src = "document.referrer", .ex = "" },
- .{ .src = "document.title", .ex = "" },
- .{ .src = "document.body.localName", .ex = "body" },
- .{ .src = "document.head.localName", .ex = "head" },
- .{ .src = "document.images.length", .ex = "0" },
- .{ .src = "document.embeds.length", .ex = "0" },
- .{ .src = "document.plugins.length", .ex = "0" },
- .{ .src = "document.scripts.length", .ex = "0" },
- .{ .src = "document.forms.length", .ex = "0" },
- .{ .src = "document.links.length", .ex = "1" },
- .{ .src = "document.applets.length", .ex = "0" },
- .{ .src = "document.anchors.length", .ex = "0" },
- .{ .src = "document.all.length", .ex = "8" },
- .{ .src = "document.currentScript", .ex = "null" },
- };
- try checkCases(js_env, &getters);
+test "Browser.HTML.Document" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
- var titles = [_]Case{
- .{ .src = "document.title = 'foo'", .ex = "foo" },
- .{ .src = "document.title", .ex = "foo" },
- .{ .src = "document.title = ''", .ex = "" },
- };
- try checkCases(js_env, &titles);
+ try runner.testCases(&.{
+ .{ "document.__proto__.constructor.name", "HTMLDocument" },
+ .{ "document.__proto__.__proto__.constructor.name", "Document" },
+ .{ "document.body.localName == 'body'", "true" },
+ }, .{});
- var getElementsByName = [_]Case{
- .{ .src = "document.getElementById('link').setAttribute('name', 'foo')", .ex = "undefined" },
- .{ .src = "let list = document.getElementsByName('foo')", .ex = "undefined" },
- .{ .src = "list.length", .ex = "1" },
- };
- try checkCases(js_env, &getElementsByName);
+ try runner.testCases(&.{
+ .{ "document.domain", "" },
+ .{ "document.referrer", "" },
+ .{ "document.title", "" },
+ .{ "document.body.localName", "body" },
+ .{ "document.head.localName", "head" },
+ .{ "document.images.length", "0" },
+ .{ "document.embeds.length", "0" },
+ .{ "document.plugins.length", "0" },
+ .{ "document.scripts.length", "0" },
+ .{ "document.forms.length", "0" },
+ .{ "document.links.length", "1" },
+ .{ "document.applets.length", "0" },
+ .{ "document.anchors.length", "0" },
+ .{ "document.all.length", "8" },
+ .{ "document.currentScript", "null" },
+ }, .{});
- var cookie = [_]Case{
- .{ .src = "document.cookie", .ex = "" },
- .{ .src = "document.cookie = 'name=Oeschger; SameSite=None; Secure'", .ex = "name=Oeschger; SameSite=None; Secure" },
- .{ .src = "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", .ex = "favorite_food=tripe; SameSite=None; Secure" },
- .{ .src = "document.cookie", .ex = "name=Oeschger; favorite_food=tripe" },
- };
- try checkCases(js_env, &cookie);
+ try runner.testCases(&.{
+ .{ "document.title = 'foo'", "foo" },
+ .{ "document.title", "foo" },
+ .{ "document.title = ''", "" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "document.getElementById('link').setAttribute('name', 'foo')", "undefined" },
+ .{ "let list = document.getElementsByName('foo')", "undefined" },
+ .{ "list.length", "1" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "document.cookie", "" },
+ .{ "document.cookie = 'name=Oeschger; SameSite=None; Secure'", "name=Oeschger; SameSite=None; Secure" },
+ .{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" },
+ .{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
+ }, .{});
}
diff --git a/src/html/elements.zig b/src/browser/html/elements.zig
similarity index 69%
rename from src/html/elements.zig
rename to src/browser/html/elements.zig
index d0ff13a4..0cee0cca 100644
--- a/src/html/elements.zig
+++ b/src/browser/html/elements.zig
@@ -17,16 +17,13 @@
// along with this program. If not, see .
const std = @import("std");
-const parser = @import("netsurf");
-const generate = @import("../generate.zig");
+const parser = @import("../netsurf.zig");
+const generate = @import("../../runtime/generate.zig");
+const SessionState = @import("../env.zig").SessionState;
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-const Element = @import("../dom/element.zig").Element;
const URL = @import("../url/url.zig").URL;
const Node = @import("../dom/node.zig").Node;
+const Element = @import("../dom/element.zig").Element;
// HTMLElement interfaces
pub const Interfaces = .{
@@ -105,14 +102,11 @@ pub const Union = generate.Union(Interfaces);
// Abstract class
// --------------
-const CSSProperties = struct {
- pub const mem_guarantied = true;
-};
+const CSSProperties = struct {};
pub const HTMLElement = struct {
pub const Self = parser.ElementHTML;
pub const prototype = *Element;
- pub const mem_guarantied = true;
pub fn get_style(_: *parser.ElementHTML) CSSProperties {
return .{};
@@ -148,7 +142,6 @@ pub const HTMLElement = struct {
pub const HTMLMediaElement = struct {
pub const Self = parser.MediaElement;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
// HTML elements
@@ -157,14 +150,12 @@ pub const HTMLMediaElement = struct {
pub const HTMLUnknownElement = struct {
pub const Self = parser.Unknown;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
// https://html.spec.whatwg.org/#the-a-element
pub const HTMLAnchorElement = struct {
pub const Self = parser.Anchor;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
pub fn get_target(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetTarget(self);
@@ -174,7 +165,7 @@ pub const HTMLAnchorElement = struct {
return try parser.anchorSetTarget(self, href);
}
- pub fn get_download(_: *parser.Anchor) ![]const u8 {
+ pub fn get_download(_: *const parser.Anchor) ![]const u8 {
return ""; // TODO
}
@@ -218,47 +209,39 @@ pub const HTMLAnchorElement = struct {
return try parser.nodeSetTextContent(parser.anchorToNode(self), v);
}
- inline fn url(self: *parser.Anchor, alloc: std.mem.Allocator) !URL {
+ inline fn url(self: *parser.Anchor, state: *SessionState) !URL {
const href = try parser.anchorGetHref(self);
- return URL.constructor(alloc, href, null); // TODO inject base url
+ return URL.constructor(href, null, state); // TODO inject base url
}
// TODO return a disposable string
- pub fn get_origin(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
-
- return try u.get_origin(alloc);
+ pub fn get_origin(self: *parser.Anchor, state: *SessionState) ![]const u8 {
+ var u = try url(self, state);
+ return try u.get_origin(state);
}
// TODO return a disposable string
- pub fn get_protocol(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
-
- return u.get_protocol(alloc);
+ pub fn get_protocol(self: *parser.Anchor, state: *SessionState) ![]const u8 {
+ var u = try url(self, state);
+ return u.get_protocol(state);
}
- pub fn set_protocol(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
+ pub fn set_protocol(self: *parser.Anchor, v: []const u8, state: *SessionState) !void {
+ const arena = state.arena;
+ var u = try url(self, state);
u.uri.scheme = v;
- const href = try u.format(alloc);
- defer alloc.free(href);
-
+ const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
- pub fn get_host(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
-
- return try u.get_host(alloc);
+ pub fn get_host(self: *parser.Anchor, state: *SessionState) ![]const u8 {
+ var u = try url(self, state);
+ return try u.get_host(state);
}
- pub fn set_host(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
+ pub fn set_host(self: *parser.Anchor, v: []const u8, state: *SessionState) !void {
// search : separator
var p: ?u16 = null;
var h: []const u8 = undefined;
@@ -270,8 +253,8 @@ pub const HTMLAnchorElement = struct {
}
}
- var u = try url(self, alloc);
- defer u.deinit(alloc);
+ const arena = state.arena;
+ var u = try url(self, state);
if (p) |pp| {
u.uri.host = .{ .raw = h };
@@ -281,40 +264,33 @@ pub const HTMLAnchorElement = struct {
u.uri.port = null;
}
- const href = try u.format(alloc);
- defer alloc.free(href);
-
+ const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
- pub fn get_hostname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
-
- return try alloc.dupe(u8, u.get_hostname());
+ pub fn get_hostname(self: *parser.Anchor, state: *SessionState) ![]const u8 {
+ var u = try url(self, state);
+ return try state.arena.dupe(u8, u.get_hostname());
}
- pub fn set_hostname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
-
+ pub fn set_hostname(self: *parser.Anchor, v: []const u8, state: *SessionState) !void {
+ const arena = state.arena;
+ var u = try url(self, state);
u.uri.host = .{ .raw = v };
- const href = try u.format(alloc);
+ const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
- pub fn get_port(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
-
- return try u.get_port(alloc);
+ pub fn get_port(self: *parser.Anchor, state: *SessionState) ![]const u8 {
+ var u = try url(self, state);
+ return try u.get_port(state);
}
- pub fn set_port(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
+ pub fn set_port(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
+ const arena = state.arena;
+ var u = try url(self, state);
if (v != null and v.?.len > 0) {
u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
@@ -322,407 +298,340 @@ pub const HTMLAnchorElement = struct {
u.uri.port = null;
}
- const href = try u.format(alloc);
- defer alloc.free(href);
-
+ const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
- pub fn get_username(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
-
- return try alloc.dupe(u8, u.get_username());
+ pub fn get_username(self: *parser.Anchor, state: *SessionState) ![]const u8 {
+ var u = try url(self, state);
+ return try state.arena.dupe(u8, u.get_username());
}
- pub fn set_username(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
+ pub fn set_username(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
+ const arena = state.arena;
+ var u = try url(self, state);
if (v) |vv| {
u.uri.user = .{ .raw = vv };
} else {
u.uri.user = null;
}
- const href = try u.format(alloc);
- defer alloc.free(href);
+ const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
- pub fn get_password(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
-
- return try alloc.dupe(u8, u.get_password());
+ pub fn get_password(self: *parser.Anchor, state: *SessionState) ![]const u8 {
+ var u = try url(self, state);
+ return try state.arena.dupe(u8, u.get_password());
}
- pub fn set_password(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
+ pub fn set_password(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
+ const arena = state.arena;
+ var u = try url(self, state);
if (v) |vv| {
u.uri.password = .{ .raw = vv };
} else {
u.uri.password = null;
}
- const href = try u.format(alloc);
- defer alloc.free(href);
+ const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
- pub fn get_pathname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
-
- return try alloc.dupe(u8, u.get_pathname());
+ pub fn get_pathname(self: *parser.Anchor, state: *SessionState) ![]const u8 {
+ var u = try url(self, state);
+ return try state.arena.dupe(u8, u.get_pathname());
}
- pub fn set_pathname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
-
+ pub fn set_pathname(self: *parser.Anchor, v: []const u8, state: *SessionState) !void {
+ const arena = state.arena;
+ var u = try url(self, state);
u.uri.path = .{ .raw = v };
- const href = try u.format(alloc);
- defer alloc.free(href);
+ const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
- pub fn get_search(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
-
- return try u.get_search(alloc);
+ pub fn get_search(self: *parser.Anchor, state: *SessionState) ![]const u8 {
+ var u = try url(self, state);
+ return try u.get_search(state);
}
- pub fn set_search(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
+ pub fn set_search(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
+ const arena = state.arena;
+ var u = try url(self, state);
if (v) |vv| {
u.uri.query = .{ .raw = vv };
} else {
u.uri.query = null;
}
- const href = try u.format(alloc);
- defer alloc.free(href);
+ const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
- pub fn get_hash(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
-
- return try u.get_hash(alloc);
+ pub fn get_hash(self: *parser.Anchor, state: *SessionState) ![]const u8 {
+ var u = try url(self, state);
+ return try u.get_hash(state);
}
- pub fn set_hash(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
- var u = try url(self, alloc);
- defer u.deinit(alloc);
+ pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
+ const arena = state.arena;
+ var u = try url(self, state);
if (v) |vv| {
u.uri.fragment = .{ .raw = vv };
} else {
u.uri.fragment = null;
}
- const href = try u.format(alloc);
- defer alloc.free(href);
+ const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
-
- pub fn deinit(_: *parser.Anchor, _: std.mem.Allocator) void {}
};
pub const HTMLAppletElement = struct {
pub const Self = parser.Applet;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLAreaElement = struct {
pub const Self = parser.Area;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLAudioElement = struct {
pub const Self = parser.Audio;
pub const prototype = *HTMLMediaElement;
- pub const mem_guarantied = true;
};
pub const HTMLBRElement = struct {
pub const Self = parser.BR;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLBaseElement = struct {
pub const Self = parser.Base;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLBodyElement = struct {
pub const Self = parser.Body;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLButtonElement = struct {
pub const Self = parser.Button;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLCanvasElement = struct {
pub const Self = parser.Canvas;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLDListElement = struct {
pub const Self = parser.DList;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLDataElement = struct {
pub const Self = parser.Data;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLDataListElement = struct {
pub const Self = parser.DataList;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLDialogElement = struct {
pub const Self = parser.Dialog;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLDirectoryElement = struct {
pub const Self = parser.Directory;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLDivElement = struct {
pub const Self = parser.Div;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLEmbedElement = struct {
pub const Self = parser.Embed;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLFieldSetElement = struct {
pub const Self = parser.FieldSet;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLFontElement = struct {
pub const Self = parser.Font;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLFormElement = struct {
pub const Self = parser.Form;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLFrameElement = struct {
pub const Self = parser.Frame;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLFrameSetElement = struct {
pub const Self = parser.FrameSet;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLHRElement = struct {
pub const Self = parser.HR;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLHeadElement = struct {
pub const Self = parser.Head;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLHeadingElement = struct {
pub const Self = parser.Heading;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLHtmlElement = struct {
pub const Self = parser.Html;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLIFrameElement = struct {
pub const Self = parser.IFrame;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLImageElement = struct {
pub const Self = parser.Image;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLInputElement = struct {
pub const Self = parser.Input;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLLIElement = struct {
pub const Self = parser.LI;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLLabelElement = struct {
pub const Self = parser.Label;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLLegendElement = struct {
pub const Self = parser.Legend;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLLinkElement = struct {
pub const Self = parser.Link;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLMapElement = struct {
pub const Self = parser.Map;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLMetaElement = struct {
pub const Self = parser.Meta;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLMeterElement = struct {
pub const Self = parser.Meter;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLModElement = struct {
pub const Self = parser.Mod;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLOListElement = struct {
pub const Self = parser.OList;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLObjectElement = struct {
pub const Self = parser.Object;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLOptGroupElement = struct {
pub const Self = parser.OptGroup;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLOptionElement = struct {
pub const Self = parser.Option;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLOutputElement = struct {
pub const Self = parser.Output;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLParagraphElement = struct {
pub const Self = parser.Paragraph;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLParamElement = struct {
pub const Self = parser.Param;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLPictureElement = struct {
pub const Self = parser.Picture;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLPreElement = struct {
pub const Self = parser.Pre;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLProgressElement = struct {
pub const Self = parser.Progress;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLQuoteElement = struct {
pub const Self = parser.Quote;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
// https://html.spec.whatwg.org/#the-script-element
pub const HTMLScriptElement = struct {
pub const Self = parser.Script;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
pub fn get_src(self: *parser.Script) !?[]const u8 {
return try parser.elementGetAttribute(
@@ -837,103 +746,86 @@ pub const HTMLScriptElement = struct {
pub const HTMLSelectElement = struct {
pub const Self = parser.Select;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLSourceElement = struct {
pub const Self = parser.Source;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLSpanElement = struct {
pub const Self = parser.Span;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLStyleElement = struct {
pub const Self = parser.Style;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLTableElement = struct {
pub const Self = parser.Table;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLTableCaptionElement = struct {
pub const Self = parser.TableCaption;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLTableCellElement = struct {
pub const Self = parser.TableCell;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLTableColElement = struct {
pub const Self = parser.TableCol;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLTableRowElement = struct {
pub const Self = parser.TableRow;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLTableSectionElement = struct {
pub const Self = parser.TableSection;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLTemplateElement = struct {
pub const Self = parser.Template;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLTextAreaElement = struct {
pub const Self = parser.TextArea;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLTimeElement = struct {
pub const Self = parser.Time;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLTitleElement = struct {
pub const Self = parser.Title;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLTrackElement = struct {
pub const Self = parser.Track;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLUListElement = struct {
pub const Self = parser.UList;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub const HTMLVideoElement = struct {
pub const Self = parser.Video;
pub const prototype = *HTMLElement;
- pub const mem_guarantied = true;
};
pub fn toInterface(comptime T: type, e: *parser.Element) !T {
@@ -1010,89 +902,84 @@ pub fn toInterface(comptime T: type, e: *parser.Element) !T {
};
}
-// Tests
-// -----
+const testing = @import("../../testing.zig");
+test "Browser.HTML.Element" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var anchor = [_]Case{
- .{ .src = "let a = document.getElementById('link')", .ex = "undefined" },
- .{ .src = "a.target", .ex = "" },
- .{ .src = "a.target = '_blank'", .ex = "_blank" },
- .{ .src = "a.target", .ex = "_blank" },
- .{ .src = "a.target = ''", .ex = "" },
+ try runner.testCases(&.{
+ .{ "let a = document.getElementById('link')", "undefined" },
+ .{ "a.target", "" },
+ .{ "a.target = '_blank'", "_blank" },
+ .{ "a.target", "_blank" },
+ .{ "a.target = ''", "" },
- .{ .src = "a.href", .ex = "foo" },
- .{ .src = "a.href = 'https://lightpanda.io/'", .ex = "https://lightpanda.io/" },
- .{ .src = "a.href", .ex = "https://lightpanda.io/" },
+ .{ "a.href", "foo" },
+ .{ "a.href = 'https://lightpanda.io/'", "https://lightpanda.io/" },
+ .{ "a.href", "https://lightpanda.io/" },
- .{ .src = "a.origin", .ex = "https://lightpanda.io" },
+ .{ "a.origin", "https://lightpanda.io" },
- .{ .src = "a.host = 'lightpanda.io:443'", .ex = "lightpanda.io:443" },
- .{ .src = "a.host", .ex = "lightpanda.io:443" },
- .{ .src = "a.port", .ex = "443" },
- .{ .src = "a.hostname", .ex = "lightpanda.io" },
+ .{ "a.host = 'lightpanda.io:443'", "lightpanda.io:443" },
+ .{ "a.host", "lightpanda.io:443" },
+ .{ "a.port", "443" },
+ .{ "a.hostname", "lightpanda.io" },
- .{ .src = "a.host = 'lightpanda.io'", .ex = "lightpanda.io" },
- .{ .src = "a.host", .ex = "lightpanda.io" },
- .{ .src = "a.port", .ex = "" },
- .{ .src = "a.hostname", .ex = "lightpanda.io" },
+ .{ "a.host = 'lightpanda.io'", "lightpanda.io" },
+ .{ "a.host", "lightpanda.io" },
+ .{ "a.port", "" },
+ .{ "a.hostname", "lightpanda.io" },
- .{ .src = "a.host", .ex = "lightpanda.io" },
- .{ .src = "a.hostname", .ex = "lightpanda.io" },
- .{ .src = "a.hostname = 'foo.bar'", .ex = "foo.bar" },
- .{ .src = "a.href", .ex = "https://foo.bar/" },
+ .{ "a.host", "lightpanda.io" },
+ .{ "a.hostname", "lightpanda.io" },
+ .{ "a.hostname = 'foo.bar'", "foo.bar" },
+ .{ "a.href", "https://foo.bar/" },
- .{ .src = "a.search", .ex = "" },
- .{ .src = "a.search = 'q=bar'", .ex = "q=bar" },
- .{ .src = "a.search", .ex = "?q=bar" },
- .{ .src = "a.href", .ex = "https://foo.bar/?q=bar" },
+ .{ "a.search", "" },
+ .{ "a.search = 'q=bar'", "q=bar" },
+ .{ "a.search", "?q=bar" },
+ .{ "a.href", "https://foo.bar/?q=bar" },
- .{ .src = "a.hash", .ex = "" },
- .{ .src = "a.hash = 'frag'", .ex = "frag" },
- .{ .src = "a.hash", .ex = "#frag" },
- .{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" },
+ .{ "a.hash", "" },
+ .{ "a.hash = 'frag'", "frag" },
+ .{ "a.hash", "#frag" },
+ .{ "a.href", "https://foo.bar/?q=bar#frag" },
- .{ .src = "a.port", .ex = "" },
- .{ .src = "a.port = '443'", .ex = "443" },
- .{ .src = "a.host", .ex = "foo.bar:443" },
- .{ .src = "a.hostname", .ex = "foo.bar" },
- .{ .src = "a.href", .ex = "https://foo.bar:443/?q=bar#frag" },
- .{ .src = "a.port = null", .ex = "null" },
- .{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" },
+ .{ "a.port", "" },
+ .{ "a.port = '443'", "443" },
+ .{ "a.host", "foo.bar:443" },
+ .{ "a.hostname", "foo.bar" },
+ .{ "a.href", "https://foo.bar:443/?q=bar#frag" },
+ .{ "a.port = null", "null" },
+ .{ "a.href", "https://foo.bar/?q=bar#frag" },
- .{ .src = "a.href = 'foo'", .ex = "foo" },
+ .{ "a.href = 'foo'", "foo" },
- .{ .src = "a.type", .ex = "" },
- .{ .src = "a.type = 'text/html'", .ex = "text/html" },
- .{ .src = "a.type", .ex = "text/html" },
- .{ .src = "a.type = ''", .ex = "" },
+ .{ "a.type", "" },
+ .{ "a.type = 'text/html'", "text/html" },
+ .{ "a.type", "text/html" },
+ .{ "a.type = ''", "" },
- .{ .src = "a.text", .ex = "OK" },
- .{ .src = "a.text = 'foo'", .ex = "foo" },
- .{ .src = "a.text", .ex = "foo" },
- .{ .src = "a.text = 'OK'", .ex = "OK" },
- };
- try checkCases(js_env, &anchor);
+ .{ "a.text", "OK" },
+ .{ "a.text = 'foo'", "foo" },
+ .{ "a.text", "foo" },
+ .{ "a.text = 'OK'", "OK" },
+ }, .{});
- var script = [_]Case{
- .{ .src = "let script = document.createElement('script')", .ex = "undefined" },
- .{ .src = "script.src = 'foo.bar'", .ex = "foo.bar" },
+ try runner.testCases(&.{
+ .{ "let script = document.createElement('script')", "undefined" },
+ .{ "script.src = 'foo.bar'", "foo.bar" },
- .{ .src = "script.async = true", .ex = "true" },
- .{ .src = "script.async", .ex = "true" },
- .{ .src = "script.async = false", .ex = "false" },
- .{ .src = "script.async", .ex = "false" },
- };
- try checkCases(js_env, &script);
+ .{ "script.async = true", "true" },
+ .{ "script.async", "true" },
+ .{ "script.async = false", "false" },
+ .{ "script.async", "false" },
+ }, .{});
- var innertext = [_]Case{
- .{ .src = "const backup = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "document.getElementById('content').innerText = 'foo';", .ex = "foo" },
- .{ .src = "document.getElementById('content').innerText", .ex = "foo" },
- .{ .src = "document.getElementById('content').innerHTML = backup; true;", .ex = "true" },
- };
- try checkCases(js_env, &innertext);
+ try runner.testCases(&.{
+ .{ "const backup = document.getElementById('content')", "undefined" },
+ .{ "document.getElementById('content').innerText = 'foo';", "foo" },
+ .{ "document.getElementById('content').innerText", "foo" },
+ .{ "document.getElementById('content').innerHTML = backup; true;", "true" },
+ }, .{});
}
diff --git a/src/html/history.zig b/src/browser/html/history.zig
similarity index 70%
rename from src/html/history.zig
rename to src/browser/html/history.zig
index f3e96dc0..0bd07f9c 100644
--- a/src/html/history.zig
+++ b/src/browser/html/history.zig
@@ -19,15 +19,9 @@
const std = @import("std");
const builtin = @import("builtin");
-const jsruntime = @import("jsruntime");
-
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
pub const History = struct {
- pub const mem_guarantied = true;
-
const ScrollRestorationMode = enum {
auto,
manual,
@@ -98,31 +92,31 @@ pub const History = struct {
// Tests
// -----
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var history = [_]Case{
- .{ .src = "history.scrollRestoration", .ex = "auto" },
- .{ .src = "history.scrollRestoration = 'manual'", .ex = "manual" },
- .{ .src = "history.scrollRestoration = 'foo'", .ex = "foo" },
- .{ .src = "history.scrollRestoration", .ex = "manual" },
- .{ .src = "history.scrollRestoration = 'auto'", .ex = "auto" },
- .{ .src = "history.scrollRestoration", .ex = "auto" },
+const testing = @import("../../testing.zig");
+test "Browser.HTML.History" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
- .{ .src = "history.state", .ex = "null" },
+ try runner.testCases(&.{
+ .{ "history.scrollRestoration", "auto" },
+ .{ "history.scrollRestoration = 'manual'", "manual" },
+ .{ "history.scrollRestoration = 'foo'", "foo" },
+ .{ "history.scrollRestoration", "manual" },
+ .{ "history.scrollRestoration = 'auto'", "auto" },
+ .{ "history.scrollRestoration", "auto" },
- .{ .src = "history.pushState({}, null, '')", .ex = "undefined" },
+ .{ "history.state", "null" },
- .{ .src = "history.replaceState({}, null, '')", .ex = "undefined" },
+ .{ "history.pushState({}, null, '')", "undefined" },
- .{ .src = "history.go()", .ex = "undefined" },
- .{ .src = "history.go(1)", .ex = "undefined" },
- .{ .src = "history.go(-1)", .ex = "undefined" },
+ .{ "history.replaceState({}, null, '')", "undefined" },
- .{ .src = "history.forward()", .ex = "undefined" },
+ .{ "history.go()", "undefined" },
+ .{ "history.go(1)", "undefined" },
+ .{ "history.go(-1)", "undefined" },
- .{ .src = "history.back()", .ex = "undefined" },
- };
- try checkCases(js_env, &history);
+ .{ "history.forward()", "undefined" },
+
+ .{ "history.back()", "undefined" },
+ }, .{});
}
diff --git a/src/html/html.zig b/src/browser/html/html.zig
similarity index 96%
rename from src/html/html.zig
rename to src/browser/html/html.zig
index 9aae1de2..513e3fe5 100644
--- a/src/html/html.zig
+++ b/src/browser/html/html.zig
@@ -16,8 +16,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-const generate = @import("../generate.zig");
-
const HTMLDocument = @import("document.zig").HTMLDocument;
const HTMLElem = @import("elements.zig");
const Window = @import("window.zig").Window;
diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig
new file mode 100644
index 00000000..08f6aa4c
--- /dev/null
+++ b/src/browser/html/location.zig
@@ -0,0 +1,111 @@
+// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+
+const SessionState = @import("../env.zig").SessionState;
+
+const builtin = @import("builtin");
+
+const URL = @import("../url/url.zig").URL;
+
+// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface
+pub const Location = struct {
+ url: ?URL = null,
+
+ pub fn get_href(self: *Location, state: *SessionState) ![]const u8 {
+ if (self.url) |*u| return u.get_href(state);
+ return "";
+ }
+
+ pub fn get_protocol(self: *Location, state: *SessionState) ![]const u8 {
+ if (self.url) |*u| return u.get_protocol(state);
+ return "";
+ }
+
+ pub fn get_host(self: *Location, state: *SessionState) ![]const u8 {
+ if (self.url) |*u| return u.get_host(state);
+ return "";
+ }
+
+ pub fn get_hostname(self: *Location) []const u8 {
+ if (self.url) |*u| return u.get_hostname();
+ return "";
+ }
+
+ pub fn get_port(self: *Location, state: *SessionState) ![]const u8 {
+ if (self.url) |*u| return u.get_port(state);
+ return "";
+ }
+
+ pub fn get_pathname(self: *Location) []const u8 {
+ if (self.url) |*u| return u.get_pathname();
+ return "";
+ }
+
+ pub fn get_search(self: *Location, state: *SessionState) ![]const u8 {
+ if (self.url) |*u| return u.get_search(state);
+ return "";
+ }
+
+ pub fn get_hash(self: *Location, state: *SessionState) ![]const u8 {
+ if (self.url) |*u| return u.get_hash(state);
+ return "";
+ }
+
+ pub fn get_origin(self: *Location, state: *SessionState) ![]const u8 {
+ if (self.url) |*u| return u.get_origin(state);
+ return "";
+ }
+
+ // TODO
+ pub fn _assign(_: *Location, url: []const u8) !void {
+ _ = url;
+ }
+
+ // TODO
+ pub fn _replace(_: *Location, url: []const u8) !void {
+ _ = url;
+ }
+
+ // TODO
+ pub fn _reload(_: *Location) !void {}
+
+ pub fn _toString(self: *Location, state: *SessionState) ![]const u8 {
+ return try self.get_href(state);
+ }
+};
+
+const testing = @import("../../testing.zig");
+test "Browser.HTML.Location" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
+
+ try runner.testCases(&.{
+ .{ "location.href", "https://lightpanda.io/opensource-browser/" },
+ .{ "document.location.href", "https://lightpanda.io/opensource-browser/" },
+
+ .{ "location.host", "lightpanda.io" },
+ .{ "location.hostname", "lightpanda.io" },
+ .{ "location.origin", "https://lightpanda.io" },
+ .{ "location.pathname", "/opensource-browser/" },
+ .{ "location.hash", "" },
+ .{ "location.port", "" },
+ .{ "location.search", "" },
+ }, .{});
+}
diff --git a/src/html/navigator.zig b/src/browser/html/navigator.zig
similarity index 83%
rename from src/html/navigator.zig
rename to src/browser/html/navigator.zig
index aebb7b19..e09f6f1a 100644
--- a/src/html/navigator.zig
+++ b/src/browser/html/navigator.zig
@@ -19,15 +19,9 @@
const std = @import("std");
const builtin = @import("builtin");
-const jsruntime = @import("jsruntime");
-
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
// https://html.spec.whatwg.org/multipage/system-state.html#navigator
pub const Navigator = struct {
- pub const mem_guarantied = true;
-
agent: []const u8 = "Lightpanda/1.0",
version: []const u8 = "1.0",
vendor: []const u8 = "",
@@ -89,14 +83,14 @@ pub const Navigator = struct {
// Tests
// -----
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var navigator = [_]Case{
- .{ .src = "navigator.userAgent", .ex = "Lightpanda/1.0" },
- .{ .src = "navigator.appVersion", .ex = "1.0" },
- .{ .src = "navigator.language", .ex = "en-US" },
- };
- try checkCases(js_env, &navigator);
+const testing = @import("../../testing.zig");
+test "Browser.HTML.Navigator" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
+
+ try runner.testCases(&.{
+ .{ "navigator.userAgent", "Lightpanda/1.0" },
+ .{ "navigator.appVersion", "1.0" },
+ .{ "navigator.language", "en-US" },
+ }, .{});
}
diff --git a/src/html/window.zig b/src/browser/html/window.zig
similarity index 86%
rename from src/html/window.zig
rename to src/browser/html/window.zig
index 36b580ba..45aa7673 100644
--- a/src/html/window.zig
+++ b/src/browser/html/window.zig
@@ -18,17 +18,14 @@
const std = @import("std");
-const parser = @import("netsurf");
-const jsruntime = @import("jsruntime");
-const Callback = jsruntime.Callback;
-const CallbackArg = jsruntime.CallbackArg;
-const Loop = jsruntime.Loop;
+const parser = @import("../netsurf.zig");
+const Callback = @import("../env.zig").Callback;
+const SessionState = @import("../env.zig").SessionState;
-const URL = @import("../../../url.zig").URL;
-const EventTarget = @import("../dom/event_target.zig").EventTarget;
const Navigator = @import("navigator.zig").Navigator;
const History = @import("history.zig").History;
const Location = @import("location.zig").Location;
+const EventTarget = @import("../dom/event_target.zig").EventTarget;
const storage = @import("../storage/storage.zig");
@@ -36,14 +33,12 @@ const storage = @import("../storage/storage.zig");
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
pub const Window = struct {
pub const prototype = *EventTarget;
- pub const mem_guarantied = true;
- pub const global_type = true;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{},
document: ?*parser.DocumentHTML = null,
- target: []const u8,
+ target: []const u8 = "",
history: History = .{},
location: Location = .{},
storage_shelf: ?*storage.Shelf = null,
@@ -53,7 +48,7 @@ pub const Window = struct {
timeoutid: u32 = 0,
timeoutids: [512]u64 = undefined,
- navigator: Navigator,
+ navigator: Navigator = .{},
pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
return .{
@@ -121,11 +116,11 @@ pub const Window = struct {
}
// TODO handle callback arguments.
- pub fn _setTimeout(self: *Window, loop: *Loop, cbk: Callback, delay: ?u32) !u32 {
+ pub fn _setTimeout(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 {
if (self.timeoutid >= self.timeoutids.len) return error.TooMuchTimeout;
const ddelay: u63 = delay orelse 0;
- const id = try loop.timeout(ddelay * std.time.ns_per_ms, cbk);
+ const id = try state.loop.timeout(ddelay * std.time.ns_per_ms, cbk);
self.timeoutids[self.timeoutid] = id;
defer self.timeoutid += 1;
@@ -133,12 +128,12 @@ pub const Window = struct {
return self.timeoutid;
}
- pub fn _clearTimeout(self: *Window, loop: *Loop, id: u32) !void {
+ pub fn _clearTimeout(self: *Window, id: u32, state: *SessionState) !void {
// I do would prefer return an error in this case, but it seems some JS
// uses invalid id, in particular id 0.
// So we silently ignore invalid id for now.
if (id >= self.timeoutid) return;
- try loop.cancel(self.timeoutids[id], null);
+ try state.loop.cancel(self.timeoutids[id], null);
}
};
diff --git a/src/iterator/iterator.zig b/src/browser/iterator/iterator.zig
similarity index 97%
rename from src/iterator/iterator.zig
rename to src/browser/iterator/iterator.zig
index d582be11..aa248509 100644
--- a/src/iterator/iterator.zig
+++ b/src/browser/iterator/iterator.zig
@@ -5,8 +5,6 @@ pub const Interfaces = .{
};
pub const U32Iterator = struct {
- pub const mem_guarantied = true;
-
length: u32,
index: u32 = 0,
diff --git a/src/mimalloc/mimalloc.zig b/src/browser/mimalloc.zig
similarity index 100%
rename from src/mimalloc/mimalloc.zig
rename to src/browser/mimalloc.zig
diff --git a/src/browser/mime.zig b/src/browser/mime.zig
index 2f885d5f..33e14cba 100644
--- a/src/browser/mime.zig
+++ b/src/browser/mime.zig
@@ -23,7 +23,6 @@ pub const Mime = struct {
content_type: ContentType,
params: []const u8 = "",
charset: ?[]const u8 = null,
- arena: std.heap.ArenaAllocator,
pub const ContentTypeEnum = enum {
text_xml,
@@ -39,19 +38,15 @@ pub const Mime = struct {
other: struct { type: []const u8, sub_type: []const u8 },
};
- pub fn parse(allocator: Allocator, input: []const u8) !Mime {
+ pub fn parse(arena: Allocator, input: []const u8) !Mime {
if (input.len > 255) {
return error.TooBig;
}
-
- var arena = std.heap.ArenaAllocator.init(allocator);
- errdefer arena.deinit();
-
var trimmed = trim(input);
const content_type, const type_len = try parseContentType(trimmed);
if (type_len >= trimmed.len) {
- return .{ .arena = arena, .content_type = content_type };
+ return .{ .content_type = content_type };
}
const params = trimLeft(trimmed[type_len..]);
@@ -70,24 +65,19 @@ pub const Mime = struct {
switch (name.len) {
7 => if (isCaseEqual("charset", name)) {
- charset = try parseValue(arena.allocator(), value);
+ charset = try parseValue(arena, value);
},
else => {},
}
}
return .{
- .arena = arena,
.params = params,
.charset = charset,
.content_type = content_type,
};
}
- pub fn deinit(self: *Mime) void {
- self.arena.deinit();
- }
-
pub fn isHTML(self: *const Mime) bool {
return self.content_type == .text_html;
}
@@ -158,7 +148,7 @@ pub const Mime = struct {
break :blk v;
};
- fn parseValue(allocator: Allocator, value: []const u8) ![]const u8 {
+ fn parseValue(arena: Allocator, value: []const u8) ![]const u8 {
if (value[0] != '"') {
return value;
}
@@ -191,7 +181,7 @@ pub const Mime = struct {
}
value_pos = 1;
- const owned = try allocator.alloc(u8, unescaped_len);
+ const owned = try arena.alloc(u8, unescaped_len);
for (0..unescaped_len) |i| {
switch (value[value_pos]) {
'"' => break,
@@ -344,8 +334,9 @@ test "Mime: parse charset" {
test "Mime: isHTML" {
const isHTML = struct {
fn isHTML(expected: bool, input: []const u8) !void {
- var mime = try Mime.parse(testing.allocator, input);
- defer mime.deinit();
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ var mime = try Mime.parse(arena.allocator(), input);
try testing.expectEqual(expected, mime.isHTML());
}
}.isHTML;
@@ -364,8 +355,10 @@ const Expectation = struct {
};
fn expect(expected: Expectation, input: []const u8) !void {
- var actual = try Mime.parse(testing.allocator, input);
- defer actual.deinit();
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+
+ const actual = try Mime.parse(arena.allocator(), input);
try testing.expectEqual(
std.meta.activeTag(expected.content_type),
diff --git a/src/netsurf/netsurf.zig b/src/browser/netsurf.zig
similarity index 99%
rename from src/netsurf/netsurf.zig
rename to src/browser/netsurf.zig
index 92c0bbea..eef41155 100644
--- a/src/netsurf/netsurf.zig
+++ b/src/browser/netsurf.zig
@@ -27,9 +27,10 @@ const c = @cImport({
@cInclude("events/mouse_event.h");
});
-const mimalloc = @import("mimalloc");
+const mimalloc = @import("mimalloc.zig");
-const Callback = @import("jsruntime").Callback;
+const Callback = @import("env.zig").Callback;
+const SessionState = @import("env.zig").SessionState;
// init initializes netsurf lib.
// init starts a mimalloc heap arena for the netsurf session. The caller must
@@ -617,7 +618,7 @@ pub fn eventTargetHasListener(
defer c.dom_event_listener_unref(listener);
const ehd = EventHandlerDataInternal.fromListener(listener);
if (ehd) |d| {
- if (cbk_id == d.data.cbk.id()) {
+ if (cbk_id == d.data.cbk.id) {
return lst;
}
}
@@ -669,7 +670,7 @@ pub const EventHandlerData = struct {
// deinitFunc implements the data deinitialization.
deinitFunc: ?DeinitFunc = null,
- pub const DeinitFunc = *const fn (data: ?*anyopaque, alloc: std.mem.Allocator) void;
+ pub const DeinitFunc = *const fn (data: ?*anyopaque, allocator: std.mem.Allocator) void;
};
// EventHandlerDataInternal groups the EventHandlerFunc and the EventHandlerData.
@@ -687,8 +688,9 @@ const EventHandlerDataInternal = struct {
}
fn deinit(self: *EventHandlerDataInternal, alloc: std.mem.Allocator) void {
- if (self.data.deinitFunc) |d| d(self.data.data, alloc);
- self.data.cbk.deinit(alloc);
+ if (self.data.deinitFunc) |d| {
+ d(self.data.data, alloc);
+ }
alloc.destroy(self);
}
@@ -723,7 +725,7 @@ pub fn eventTargetAddEventListener(
// When a function is used as an event handler, its this parameter is bound
// to the DOM element on which the listener is placed.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#this_in_dom_event_handlers
- try ehd.data.cbk.setThisArg(et);
+ try ehd.data.cbk.setThis(et);
const ctx = @as(*anyopaque, @ptrCast(ehd));
var listener: ?*EventListener = undefined;
diff --git a/src/polyfill/fetch.js b/src/browser/polyfill/fetch.js
similarity index 100%
rename from src/polyfill/fetch.js
rename to src/browser/polyfill/fetch.js
diff --git a/src/browser/polyfill/fetch.zig b/src/browser/polyfill/fetch.zig
new file mode 100644
index 00000000..9581881e
--- /dev/null
+++ b/src/browser/polyfill/fetch.zig
@@ -0,0 +1,47 @@
+const std = @import("std");
+// fetch.js code comes from
+// https://github.com/JakeChampion/fetch/blob/main/fetch.js
+//
+// The original code source is available in MIT license.
+//
+// The script comes from the built version from npm.
+// You can get the package with the command:
+//
+// wget $(npm view whatwg-fetch dist.tarball)
+//
+// The source is the content of `package/dist/fetch.umd.js` file.
+pub const source = @embedFile("fetch.js");
+
+const testing = @import("../../testing.zig");
+test "Browser.fetch" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
+
+ try @import("polyfill.zig").load(testing.allocator, runner.executor);
+
+ try runner.testCases(&.{
+ .{
+ \\ var ok = false;
+ \\ const request = new Request("http://127.0.0.1:9582/loader");
+ \\ fetch(request).then((response) => { ok = response.ok; });
+ \\ false;
+ ,
+ "false",
+ },
+ // all events have been resolved.
+ .{ "ok", "true" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{
+ \\ var ok2 = false;
+ \\ const request2 = new Request("http://127.0.0.1:9582/loader");
+ \\ (async function () { resp = await fetch(request2); ok2 = resp.ok; }());
+ \\ false;
+ ,
+ "false",
+ },
+ // all events have been resolved.
+ .{ "ok2", "true" },
+ }, .{});
+}
diff --git a/src/polyfill/polyfill.zig b/src/browser/polyfill/polyfill.zig
similarity index 72%
rename from src/polyfill/polyfill.zig
rename to src/browser/polyfill/polyfill.zig
index 65bb0590..9e682ea2 100644
--- a/src/polyfill/polyfill.zig
+++ b/src/browser/polyfill/polyfill.zig
@@ -19,10 +19,8 @@
const std = @import("std");
const builtin = @import("builtin");
-const jsruntime = @import("jsruntime");
-const Env = jsruntime.Env;
-
-const fetch = @import("fetch.zig").fetch_polyfill;
+const Allocator = std.mem.Allocator;
+const Env = @import("../env.zig").Env;
const log = std.log.scoped(.polyfill);
@@ -33,23 +31,23 @@ const modules = [_]struct {
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
};
-pub fn load(alloc: std.mem.Allocator, env: *const Env) !void {
- var try_catch: jsruntime.TryCatch = undefined;
- try_catch.init(env);
+pub fn load(allocator: Allocator, executor: *Env.Executor) !void {
+ var try_catch: Env.TryCatch = undefined;
+ try_catch.init(executor);
defer try_catch.deinit();
for (modules) |m| {
- const res = env.exec(m.source, m.name) catch {
- if (try try_catch.err(alloc, env)) |msg| {
- defer alloc.free(msg);
+ const res = executor.exec(m.source, m.name) catch |err| {
+ if (try try_catch.err(allocator)) |msg| {
+ defer allocator.free(msg);
log.err("load {s}: {s}", .{ m.name, msg });
}
- return;
+ return err;
};
if (builtin.mode == .Debug) {
- const msg = try res.toString(alloc, env);
- defer alloc.free(msg);
+ const msg = try res.toString(allocator);
+ defer allocator.free(msg);
log.debug("load {s}: {s}", .{ m.name, msg });
}
}
diff --git a/src/storage/cookie.zig b/src/browser/storage/cookie.zig
similarity index 99%
rename from src/storage/cookie.zig
rename to src/browser/storage/cookie.zig
index e8da2869..dc483cf7 100644
--- a/src/storage/cookie.zig
+++ b/src/browser/storage/cookie.zig
@@ -3,9 +3,9 @@ const Uri = std.Uri;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
-const http = @import("../http/client.zig");
-const DateTime = @import("../datetime.zig").DateTime;
-const public_suffix_list = @import("../data/public_suffix_list.zig").lookup;
+const http = @import("../../http/client.zig");
+const DateTime = @import("../../datetime.zig").DateTime;
+const public_suffix_list = @import("../../data/public_suffix_list.zig").lookup;
const log = std.log.scoped(.cookie);
@@ -464,7 +464,7 @@ fn trimRight(str: []const u8) []const u8 {
return std.mem.trimLeft(u8, str, &std.ascii.whitespace);
}
-const testing = @import("../testing.zig");
+const testing = @import("../../testing.zig");
test "cookie: findSecondLevelDomain" {
const cases = [_]struct { []const u8, []const u8 }{
.{ "", "" },
diff --git a/src/storage/storage.zig b/src/browser/storage/storage.zig
similarity index 87%
rename from src/storage/storage.zig
rename to src/browser/storage/storage.zig
index 17fb8878..90a15584 100644
--- a/src/storage/storage.zig
+++ b/src/browser/storage/storage.zig
@@ -18,10 +18,7 @@
const std = @import("std");
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-const DOMError = @import("netsurf").DOMError;
+const DOMError = @import("../netsurf.zig").DOMError;
const log = std.log.scoped(.storage);
@@ -103,7 +100,6 @@ pub const Bucket = struct {
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface
pub const Bottle = struct {
- pub const mem_guarantied = true;
const Map = std.StringHashMapUnmanaged([]const u8);
// allocator is stored. we don't use the JS env allocator b/c the storage
@@ -216,27 +212,27 @@ pub const Bottle = struct {
// Tests
// -----
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var storage = [_]Case{
- .{ .src = "localStorage.length", .ex = "0" },
+const testing = @import("../../testing.zig");
+test "Browser.Storage.LocalStorage" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
- .{ .src = "localStorage.setItem('foo', 'bar')", .ex = "undefined" },
- .{ .src = "localStorage.length", .ex = "1" },
- .{ .src = "localStorage.getItem('foo')", .ex = "bar" },
- .{ .src = "localStorage.removeItem('foo')", .ex = "undefined" },
- .{ .src = "localStorage.length", .ex = "0" },
+ try runner.testCases(&.{
+ .{ "localStorage.length", "0" },
- // .{ .src = "localStorage['foo'] = 'bar'", .ex = "undefined" },
- // .{ .src = "localStorage['foo']", .ex = "bar" },
- // .{ .src = "localStorage.length", .ex = "1" },
+ .{ "localStorage.setItem('foo', 'bar')", "undefined" },
+ .{ "localStorage.length", "1" },
+ .{ "localStorage.getItem('foo')", "bar" },
+ .{ "localStorage.removeItem('foo')", "undefined" },
+ .{ "localStorage.length", "0" },
- .{ .src = "localStorage.clear()", .ex = "undefined" },
- .{ .src = "localStorage.length", .ex = "0" },
- };
- try checkCases(js_env, &storage);
+ // .{ "localStorage['foo'] = 'bar'", "undefined" },
+ // .{ "localStorage['foo']", "bar" },
+ // .{ "localStorage.length", "1" },
+
+ .{ "localStorage.clear()", "undefined" },
+ .{ "localStorage.length", "0" },
+ }, .{});
}
test "storage bottle" {
diff --git a/src/url/query.zig b/src/browser/url/query.zig
similarity index 99%
rename from src/url/query.zig
rename to src/browser/url/query.zig
index 14fec1b6..b8afa834 100644
--- a/src/url/query.zig
+++ b/src/browser/url/query.zig
@@ -18,8 +18,8 @@
const std = @import("std");
-const Reader = @import("../str/parser.zig").Reader;
-const asUint = @import("../str/parser.zig").asUint;
+const Reader = @import("../../str/parser.zig").Reader;
+const asUint = @import("../../str/parser.zig").asUint;
// Values is a map with string key of string values.
pub const Values = struct {
diff --git a/src/url/url.zig b/src/browser/url/url.zig
similarity index 50%
rename from src/url/url.zig
rename to src/browser/url/url.zig
index 2e28210d..7c7bf0a9 100644
--- a/src/url/url.zig
+++ b/src/browser/url/url.zig
@@ -17,10 +17,7 @@
// along with this program. If not, see .
const std = @import("std");
-
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
+const SessionState = @import("../env.zig").SessionState;
const query = @import("query.zig");
@@ -31,14 +28,14 @@ pub const Interfaces = .{
// https://url.spec.whatwg.org/#url
//
-// TODO we could avoid many of these getter string allocation in two differents
+// TODO we could avoid many of these getter string allocatoration in two differents
// way:
//
// 1. We can eventually get the slice of scheme *with* the following char in
// the underlying string. But I don't know if it's possible and how to do that.
// I mean, if the rawuri contains `https://foo.bar`, uri.scheme is a slice
// containing only `https`. I want `https:` so, in theory, I don't need to
-// allocate data, I should be able to retrieve the scheme + the following `:`
+// allocatorate data, I should be able to retrieve the scheme + the following `:`
// from rawuri.
//
// 2. The other way would bu to copy the `std.Uri` code to ahve a dedicated
@@ -47,9 +44,12 @@ pub const URL = struct {
uri: std.Uri,
search_params: URLSearchParams,
- pub const mem_guarantied = true;
-
- pub fn constructor(arena: std.mem.Allocator, url: []const u8, base: ?[]const u8) !URL {
+ pub fn constructor(
+ url: []const u8,
+ base: ?[]const u8,
+ state: *SessionState,
+ ) !URL {
+ const arena = state.arena;
const raw = try std.mem.concat(arena, u8, &[_][]const u8{ url, base orelse "" });
errdefer arena.free(raw);
@@ -60,24 +60,15 @@ pub const URL = struct {
pub fn init(arena: std.mem.Allocator, uri: std.Uri) !URL {
return .{
.uri = uri,
- .search_params = try URLSearchParams.constructor(
+ .search_params = try URLSearchParams.init(
arena,
uriComponentNullStr(uri.query),
),
};
}
- pub fn deinit(self: *URL, alloc: std.mem.Allocator) void {
- self.search_params.deinit(alloc);
- }
-
- // the caller must free the returned string.
- // TODO return a disposable string
- // https://github.com/lightpanda-io/jsruntime-lib/issues/195
- pub fn get_origin(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
- var buf = std.ArrayList(u8).init(alloc);
- defer buf.deinit();
-
+ pub fn get_origin(self: *URL, state: *SessionState) ![]const u8 {
+ var buf = std.ArrayList(u8).init(state.arena);
try self.uri.writeToStream(.{
.scheme = true,
.authentication = false,
@@ -86,31 +77,27 @@ pub const URL = struct {
.query = false,
.fragment = false,
}, buf.writer());
- return try buf.toOwnedSlice();
+ return buf.items;
}
// get_href returns the URL by writing all its components.
// The query is replaced by a dump of search params.
//
- // the caller must free the returned string.
- // TODO return a disposable string
- // https://github.com/lightpanda-io/jsruntime-lib/issues/195
- pub fn get_href(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
+ pub fn get_href(self: *URL, state: *SessionState) ![]const u8 {
+ const arena = state.arena;
// retrieve the query search from search_params.
const cur = self.uri.query;
defer self.uri.query = cur;
- var q = std.ArrayList(u8).init(alloc);
- defer q.deinit();
+ var q = std.ArrayList(u8).init(arena);
try self.search_params.values.encode(q.writer());
self.uri.query = .{ .percent_encoded = q.items };
- return try self.format(alloc);
+ return try self.toString(arena);
}
// format the url with all its components.
- pub fn format(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
- var buf = std.ArrayList(u8).init(alloc);
- defer buf.deinit();
+ pub fn toString(self: *URL, arena: std.mem.Allocator) ![]const u8 {
+ var buf = std.ArrayList(u8).init(arena);
try self.uri.writeToStream(.{
.scheme = true,
@@ -120,14 +107,11 @@ pub const URL = struct {
.query = uriComponentNullStr(self.uri.query).len > 0,
.fragment = uriComponentNullStr(self.uri.fragment).len > 0,
}, buf.writer());
- return try buf.toOwnedSlice();
+ return buf.items;
}
- // the caller must free the returned string.
- // TODO return a disposable string
- // https://github.com/lightpanda-io/jsruntime-lib/issues/195
- pub fn get_protocol(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
- return try std.mem.concat(alloc, u8, &[_][]const u8{ self.uri.scheme, ":" });
+ pub fn get_protocol(self: *URL, state: *SessionState) ![]const u8 {
+ return try std.mem.concat(state.arena, u8, &[_][]const u8{ self.uri.scheme, ":" });
}
pub fn get_username(self: *URL) []const u8 {
@@ -138,12 +122,8 @@ pub const URL = struct {
return uriComponentNullStr(self.uri.password);
}
- // the caller must free the returned string.
- // TODO return a disposable string
- // https://github.com/lightpanda-io/jsruntime-lib/issues/195
- pub fn get_host(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
- var buf = std.ArrayList(u8).init(alloc);
- defer buf.deinit();
+ pub fn get_host(self: *URL, state: *SessionState) ![]const u8 {
+ var buf = std.ArrayList(u8).init(state.arena);
try self.uri.writeToStream(.{
.scheme = false,
@@ -153,24 +133,20 @@ pub const URL = struct {
.query = false,
.fragment = false,
}, buf.writer());
- return try buf.toOwnedSlice();
+ return buf.items;
}
pub fn get_hostname(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.host);
}
- // the caller must free the returned string.
- // TODO return a disposable string
- // https://github.com/lightpanda-io/jsruntime-lib/issues/195
- pub fn get_port(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
- if (self.uri.port == null) return try alloc.dupe(u8, "");
-
- var buf = std.ArrayList(u8).init(alloc);
- defer buf.deinit();
+ pub fn get_port(self: *URL, state: *SessionState) ![]const u8 {
+ const arena = state.arena;
+ if (self.uri.port == null) return try arena.dupe(u8, "");
+ var buf = std.ArrayList(u8).init(arena);
try std.fmt.formatInt(self.uri.port.?, 10, .lower, .{}, buf.writer());
- return try buf.toOwnedSlice();
+ return buf.items;
}
pub fn get_pathname(self: *URL) []const u8 {
@@ -178,35 +154,30 @@ pub const URL = struct {
return uriComponentStr(self.uri.path);
}
- // the caller must free the returned string.
- // TODO return a disposable string
- // https://github.com/lightpanda-io/jsruntime-lib/issues/195
- pub fn get_search(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
- if (self.search_params.get_size() == 0) return try alloc.dupe(u8, "");
+ pub fn get_search(self: *URL, state: *SessionState) ![]const u8 {
+ const arena = state.arena;
+ if (self.search_params.get_size() == 0) return try arena.dupe(u8, "");
var buf: std.ArrayListUnmanaged(u8) = .{};
- defer buf.deinit(alloc);
- try buf.append(alloc, '?');
- try self.search_params.values.encode(buf.writer(alloc));
- return buf.toOwnedSlice(alloc);
+ try buf.append(arena, '?');
+ try self.search_params.values.encode(buf.writer(arena));
+ return buf.items;
}
- // the caller must free the returned string.
- // TODO return a disposable string
- // https://github.com/lightpanda-io/jsruntime-lib/issues/195
- pub fn get_hash(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
- if (self.uri.fragment == null) return try alloc.dupe(u8, "");
+ pub fn get_hash(self: *URL, state: *SessionState) ![]const u8 {
+ const arena = state.arena;
+ if (self.uri.fragment == null) return try arena.dupe(u8, "");
- return try std.mem.concat(alloc, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) });
+ return try std.mem.concat(arena, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) });
}
pub fn get_searchParams(self: *URL) *URLSearchParams {
return &self.search_params;
}
- pub fn _toJSON(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
- return try self.get_href(alloc);
+ pub fn _toJSON(self: *URL, state: *SessionState) ![]const u8 {
+ return try self.get_href(state);
}
};
@@ -230,16 +201,14 @@ fn uriComponentStr(c: std.Uri.Component) []const u8 {
pub const URLSearchParams = struct {
values: query.Values,
- pub const mem_guarantied = true;
-
- pub fn constructor(alloc: std.mem.Allocator, init: ?[]const u8) !URLSearchParams {
- return .{
- .values = try query.parseQuery(alloc, init orelse ""),
- };
+ pub fn constructor(qs: ?[]const u8, state: *SessionState) !URLSearchParams {
+ return init(state.arena, qs);
}
- pub fn deinit(self: *URLSearchParams, _: std.mem.Allocator) void {
- self.values.deinit();
+ pub fn init(arena: std.mem.Allocator, qs: ?[]const u8) !URLSearchParams {
+ return .{
+ .values = try query.parseQuery(arena, qs orelse ""),
+ };
}
pub fn get_size(self: *URLSearchParams) u32 {
@@ -269,47 +238,43 @@ pub const URLSearchParams = struct {
pub fn _sort(_: *URLSearchParams) void {}
};
-// Tests
-// -----
+const testing = @import("../../testing.zig");
+test "Browser.URL" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var url = [_]Case{
- .{ .src = "var url = new URL('https://foo.bar/path?query#fragment')", .ex = "undefined" },
- .{ .src = "url.origin", .ex = "https://foo.bar" },
- .{ .src = "url.href", .ex = "https://foo.bar/path?query#fragment" },
- .{ .src = "url.protocol", .ex = "https:" },
- .{ .src = "url.username", .ex = "" },
- .{ .src = "url.password", .ex = "" },
- .{ .src = "url.host", .ex = "foo.bar" },
- .{ .src = "url.hostname", .ex = "foo.bar" },
- .{ .src = "url.port", .ex = "" },
- .{ .src = "url.pathname", .ex = "/path" },
- .{ .src = "url.search", .ex = "?query" },
- .{ .src = "url.hash", .ex = "#fragment" },
- .{ .src = "url.searchParams.get('query')", .ex = "" },
- };
- try checkCases(js_env, &url);
+ try runner.testCases(&.{
+ .{ "var url = new URL('https://foo.bar/path?query#fragment')", "undefined" },
+ .{ "url.origin", "https://foo.bar" },
+ .{ "url.href", "https://foo.bar/path?query#fragment" },
+ .{ "url.protocol", "https:" },
+ .{ "url.username", "" },
+ .{ "url.password", "" },
+ .{ "url.host", "foo.bar" },
+ .{ "url.hostname", "foo.bar" },
+ .{ "url.port", "" },
+ .{ "url.pathname", "/path" },
+ .{ "url.search", "?query" },
+ .{ "url.hash", "#fragment" },
+ .{ "url.searchParams.get('query')", "" },
+ }, .{});
- var qs = [_]Case{
- .{ .src = "var url = new URL('https://foo.bar/path?a=~&b=%7E#fragment')", .ex = "undefined" },
- .{ .src = "url.searchParams.get('a')", .ex = "~" },
- .{ .src = "url.searchParams.get('b')", .ex = "~" },
- .{ .src = "url.searchParams.append('c', 'foo')", .ex = "undefined" },
- .{ .src = "url.searchParams.get('c')", .ex = "foo" },
- .{ .src = "url.searchParams.size", .ex = "3" },
+ try runner.testCases(&.{
+ .{ "var url = new URL('https://foo.bar/path?a=~&b=%7E#fragment')", "undefined" },
+ .{ "url.searchParams.get('a')", "~" },
+ .{ "url.searchParams.get('b')", "~" },
+ .{ "url.searchParams.append('c', 'foo')", "undefined" },
+ .{ "url.searchParams.get('c')", "foo" },
+ .{ "url.searchParams.size", "3" },
// search is dynamic
- .{ .src = "url.search", .ex = "?a=%7E&b=%7E&c=foo" },
+ .{ "url.search", "?a=%7E&b=%7E&c=foo" },
// href is dynamic
- .{ .src = "url.href", .ex = "https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment" },
+ .{ "url.href", "https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment" },
- .{ .src = "url.searchParams.delete('c', 'foo')", .ex = "undefined" },
- .{ .src = "url.searchParams.get('c')", .ex = "" },
- .{ .src = "url.searchParams.delete('a')", .ex = "undefined" },
- .{ .src = "url.searchParams.get('a')", .ex = "" },
- };
- try checkCases(js_env, &qs);
+ .{ "url.searchParams.delete('c', 'foo')", "undefined" },
+ .{ "url.searchParams.get('c')", "" },
+ .{ "url.searchParams.delete('a')", "undefined" },
+ .{ "url.searchParams.get('a')", "" },
+ }, .{});
}
diff --git a/src/xhr/event_target.zig b/src/browser/xhr/event_target.zig
similarity index 64%
rename from src/xhr/event_target.zig
rename to src/browser/xhr/event_target.zig
index 69bab2d5..02feb929 100644
--- a/src/xhr/event_target.zig
+++ b/src/browser/xhr/event_target.zig
@@ -18,19 +18,19 @@
const std = @import("std");
-const jsruntime = @import("jsruntime");
-const Callback = jsruntime.Callback;
+const Env = @import("../env.zig").Env;
+const Callback = Env.Callback;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler;
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
+const SessionState = @import("../env.zig").SessionState;
const log = std.log.scoped(.xhr);
pub const XMLHttpRequestEventTarget = struct {
pub const prototype = *EventTarget;
- pub const mem_guarantied = true;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{},
@@ -60,7 +60,7 @@ pub const XMLHttpRequestEventTarget = struct {
fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
const et = @as(*parser.EventTarget, @ptrCast(self));
// check if event target has already this listener
- const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id());
+ const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id);
if (lst == null) {
return;
}
@@ -88,39 +88,46 @@ pub const XMLHttpRequestEventTarget = struct {
return self.onloadend_cbk;
}
- pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
- if (self.onloadstart_cbk) |cbk| try self.unregister(alloc, "loadstart", cbk);
- try self.register(alloc, "loadstart", handler);
+ pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
+ const arena = state.arena;
+ if (self.onloadstart_cbk) |cbk| try self.unregister(arena, "loadstart", cbk);
+ try self.register(arena, "loadstart", handler);
self.onloadstart_cbk = handler;
}
- pub fn set_onprogress(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
- if (self.onprogress_cbk) |cbk| try self.unregister(alloc, "progress", cbk);
- try self.register(alloc, "progress", handler);
+ pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
+ const arena = state.arena;
+ if (self.onprogress_cbk) |cbk| try self.unregister(arena, "progress", cbk);
+ try self.register(arena, "progress", handler);
self.onprogress_cbk = handler;
}
- pub fn set_onabort(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
- if (self.onabort_cbk) |cbk| try self.unregister(alloc, "abort", cbk);
- try self.register(alloc, "abort", handler);
+ pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
+ const arena = state.arena;
+ if (self.onabort_cbk) |cbk| try self.unregister(arena, "abort", cbk);
+ try self.register(arena, "abort", handler);
self.onabort_cbk = handler;
}
- pub fn set_onload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
- if (self.onload_cbk) |cbk| try self.unregister(alloc, "load", cbk);
- try self.register(alloc, "load", handler);
+ pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
+ const arena = state.arena;
+ if (self.onload_cbk) |cbk| try self.unregister(arena, "load", cbk);
+ try self.register(arena, "load", handler);
self.onload_cbk = handler;
}
- pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
- if (self.ontimeout_cbk) |cbk| try self.unregister(alloc, "timeout", cbk);
- try self.register(alloc, "timeout", handler);
+ pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
+ const arena = state.arena;
+ if (self.ontimeout_cbk) |cbk| try self.unregister(arena, "timeout", cbk);
+ try self.register(arena, "timeout", handler);
self.ontimeout_cbk = handler;
}
- pub fn set_onloadend(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void {
- if (self.onloadend_cbk) |cbk| try self.unregister(alloc, "loadend", cbk);
- try self.register(alloc, "loadend", handler);
+ pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
+ const arena = state.arena;
+ if (self.onloadend_cbk) |cbk| try self.unregister(arena, "loadend", cbk);
+ try self.register(arena, "loadend", handler);
self.onloadend_cbk = handler;
}
- pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void {
- parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), alloc) catch |e| {
+ pub fn deinit(self: *XMLHttpRequestEventTarget, state: *SessionState) void {
+ const arena = state.arena;
+ parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), arena) catch |e| {
log.err("remove all listeners: {any}", .{e});
};
}
diff --git a/src/xhr/progress_event.zig b/src/browser/xhr/progress_event.zig
similarity index 63%
rename from src/xhr/progress_event.zig
rename to src/browser/xhr/progress_event.zig
index d985c76f..4fd8b8a7 100644
--- a/src/xhr/progress_event.zig
+++ b/src/browser/xhr/progress_event.zig
@@ -18,11 +18,7 @@
const std = @import("std");
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-const parser = @import("netsurf");
+const parser = @import("../netsurf.zig");
const Event = @import("../events/event.zig").Event;
const DOMException = @import("../dom/exceptions.zig").DOMException;
@@ -30,7 +26,6 @@ const DOMException = @import("../dom/exceptions.zig").DOMException;
pub const ProgressEvent = struct {
pub const prototype = *Event;
pub const Exception = DOMException;
- pub const mem_guarantied = true;
pub const EventInit = struct {
lengthComputable: bool = false,
@@ -59,32 +54,32 @@ pub const ProgressEvent = struct {
};
}
- pub fn get_lengthComputable(self: ProgressEvent) bool {
+ pub fn get_lengthComputable(self: *const ProgressEvent) bool {
return self.lengthComputable;
}
- pub fn get_loaded(self: ProgressEvent) u64 {
+ pub fn get_loaded(self: *const ProgressEvent) u64 {
return self.loaded;
}
- pub fn get_total(self: ProgressEvent) u64 {
+ pub fn get_total(self: *const ProgressEvent) u64 {
return self.total;
}
};
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var progress_event = [_]Case{
- .{ .src = "let pevt = new ProgressEvent('foo');", .ex = "undefined" },
- .{ .src = "pevt.loaded", .ex = "0" },
- .{ .src = "pevt instanceof ProgressEvent", .ex = "true" },
- .{ .src = "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", .ex = "undefined" },
- .{ .src = "document.addEventListener('foo', ccbk)", .ex = "undefined" },
- .{ .src = "document.dispatchEvent(pevt)", .ex = "true" },
- .{ .src = "eevt.type", .ex = "foo" },
- .{ .src = "eevt instanceof ProgressEvent", .ex = "true" },
- };
- try checkCases(js_env, &progress_event);
+const testing = @import("../../testing.zig");
+test "Browser.XHR.ProgressEvent" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
+
+ try runner.testCases(&.{
+ .{ "let pevt = new ProgressEvent('foo');", "undefined" },
+ .{ "pevt.loaded", "0" },
+ .{ "pevt instanceof ProgressEvent", "true" },
+ .{ "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", "undefined" },
+ .{ "document.addEventListener('foo', ccbk)", "undefined" },
+ .{ "document.dispatchEvent(pevt)", "true" },
+ .{ "eevt.type", "foo" },
+ .{ "eevt instanceof ProgressEvent", "true" },
+ }, .{});
}
diff --git a/src/xhr/xhr.zig b/src/browser/xhr/xhr.zig
similarity index 76%
rename from src/xhr/xhr.zig
rename to src/browser/xhr/xhr.zig
index 9c4e82b3..b0aa59d5 100644
--- a/src/xhr/xhr.zig
+++ b/src/browser/xhr/xhr.zig
@@ -17,27 +17,20 @@
// along with this program. If not, see .
const std = @import("std");
+const Allocator = std.mem.Allocator;
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-const DOMError = @import("netsurf").DOMError;
+const DOMError = @import("../netsurf.zig").DOMError;
const DOMException = @import("../dom/exceptions.zig").DOMException;
const ProgressEvent = @import("progress_event.zig").ProgressEvent;
const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEventTarget;
-const Mime = @import("../browser/mime.zig").Mime;
-
-const Loop = jsruntime.Loop;
-const URL = @import("../url.zig").URL;
-const http = @import("../http/client.zig");
-
-const parser = @import("netsurf");
-
+const URL = @import("../../url.zig").URL;
+const Mime = @import("../mime.zig").Mime;
+const parser = @import("../netsurf.zig");
+const http = @import("../../http/client.zig");
+const SessionState = @import("../env.zig").SessionState;
const CookieJar = @import("../storage/storage.zig").CookieJar;
-const UserContext = @import("../user_context.zig").UserContext;
const log = std.log.scoped(.xhr);
@@ -51,7 +44,6 @@ pub const Interfaces = .{
pub const XMLHttpRequestUpload = struct {
pub const prototype = *XMLHttpRequestEventTarget;
- pub const mem_guarantied = true;
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
};
@@ -83,7 +75,7 @@ pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) {
// Duplicate the body content.
// The caller owns the allocated string.
- fn dupe(self: XMLHttpRequestBodyInit, alloc: std.mem.Allocator) ![]const u8 {
+ fn dupe(self: XMLHttpRequestBodyInit, alloc: Allocator) ![]const u8 {
return switch (self) {
.Blob => error.NotImplemented,
.BufferSource => error.NotImplemented,
@@ -96,7 +88,7 @@ pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) {
pub const XMLHttpRequest = struct {
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
- alloc: std.mem.Allocator,
+ arena: Allocator,
client: *http.Client,
request: ?http.Request = null,
@@ -134,10 +126,6 @@ pub const XMLHttpRequest = struct {
response_type: ResponseType = .Empty,
response_headers: Headers,
- // used by zig client to parse response headers.
- // use 16KB for headers buffer size.
- response_header_buffer: [1024 * 16]u8 = undefined,
-
response_status: u16 = 0,
// TODO uncomment this field causes casting issue with
@@ -151,7 +139,6 @@ pub const XMLHttpRequest = struct {
send_flag: bool = false,
pub const prototype = *XMLHttpRequestEventTarget;
- pub const mem_guarantied = true;
const State = enum(u16) {
unsent = 0,
@@ -174,41 +161,41 @@ pub const XMLHttpRequest = struct {
const JSONValue = std.json.Value;
const Headers = struct {
- alloc: std.mem.Allocator,
list: List,
+ arena: Allocator,
const List = std.ArrayListUnmanaged(std.http.Header);
- fn init(alloc: std.mem.Allocator) Headers {
+ fn init(arena: Allocator) Headers {
return .{
- .alloc = alloc,
- .list = List{},
+ .arena = arena,
+ .list = .{},
};
}
fn deinit(self: *Headers) void {
self.free();
- self.list.deinit(self.alloc);
+ self.list.deinit(self.arena);
}
fn append(self: *Headers, k: []const u8, v: []const u8) !void {
// duplicate strings
- const kk = try self.alloc.dupe(u8, k);
- const vv = try self.alloc.dupe(u8, v);
- try self.list.append(self.alloc, .{ .name = kk, .value = vv });
+ const kk = try self.arena.dupe(u8, k);
+ const vv = try self.arena.dupe(u8, v);
+ try self.list.append(self.arena, .{ .name = kk, .value = vv });
}
// free all strings allocated.
fn free(self: *Headers) void {
for (self.list.items) |h| {
- self.alloc.free(h.name);
- self.alloc.free(h.value);
+ self.arena.free(h.name);
+ self.arena.free(h.value);
}
}
fn clearAndFree(self: *Headers) void {
self.free();
- self.list.clearAndFree(self.alloc);
+ self.list.clearAndFree(self.arena);
}
fn has(self: Headers, k: []const u8) bool {
@@ -236,8 +223,8 @@ pub const XMLHttpRequest = struct {
for (self.list.items, 0..) |h, i| {
if (std.ascii.eqlIgnoreCase(k, h.name)) {
const hh = self.list.swapRemove(i);
- self.alloc.free(hh.name);
- self.alloc.free(hh.value);
+ self.arena.free(hh.name);
+ self.arena.free(hh.value);
}
}
self.append(k, v);
@@ -286,17 +273,18 @@ pub const XMLHttpRequest = struct {
const min_delay: u64 = 50000000; // 50ms
- pub fn constructor(alloc: std.mem.Allocator, userctx: UserContext) !XMLHttpRequest {
+ pub fn constructor(session_state: *SessionState) !XMLHttpRequest {
+ const arena = session_state.arena;
return .{
- .alloc = alloc,
- .headers = Headers.init(alloc),
- .response_headers = Headers.init(alloc),
+ .arena = arena,
+ .headers = Headers.init(arena),
+ .response_headers = Headers.init(arena),
.method = undefined,
.state = .unsent,
.url = null,
- .origin_url = userctx.url,
- .client = userctx.http_client,
- .cookie_jar = userctx.cookie_jar,
+ .origin_url = session_state.url,
+ .client = session_state.http_client,
+ .cookie_jar = session_state.cookie_jar,
};
}
@@ -307,10 +295,7 @@ pub const XMLHttpRequest = struct {
self.response_obj = null;
self.response_type = .Empty;
- if (self.response_mime) |*mime| {
- mime.deinit();
- self.response_mime = null;
- }
+ self.response_mime = null;
// TODO should we clearRetainingCapacity instead?
self.headers.clearAndFree();
@@ -322,7 +307,7 @@ pub const XMLHttpRequest = struct {
self.priv_state = .new;
}
- pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
+ pub fn deinit(self: *XMLHttpRequest, alloc: Allocator) void {
self.reset();
self.headers.deinit();
self.response_headers.deinit();
@@ -362,7 +347,6 @@ pub const XMLHttpRequest = struct {
pub fn _open(
self: *XMLHttpRequest,
- alloc: std.mem.Allocator,
method: []const u8,
url: []const u8,
asyn: ?bool,
@@ -375,12 +359,13 @@ pub const XMLHttpRequest = struct {
// TODO If thisβs relevant global object is a Window object and its
// associated Document is not fully active, then throw an
// "InvalidStateError" DOMException.
-
- self.method = try validMethod(method);
-
self.reset();
- self.url = try self.origin_url.resolve(alloc, url);
+ self.method = try validMethod(method);
+ const arena = self.arena;
+
+ self.url = try self.origin_url.resolve(arena, url);
+
log.debug("open url ({s})", .{self.url.?});
self.sync = if (asyn) |b| !b else false;
@@ -466,7 +451,7 @@ pub const XMLHttpRequest = struct {
}
// TODO body can be either a XMLHttpRequestBodyInit or a document
- pub fn _send(self: *XMLHttpRequest, loop: *Loop, alloc: std.mem.Allocator, body: ?[]const u8) !void {
+ pub fn _send(self: *XMLHttpRequest, body: ?[]const u8, session_state: *SessionState) !void {
if (self.state != .opened) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState;
@@ -485,7 +470,7 @@ pub const XMLHttpRequest = struct {
{
var arr: std.ArrayListUnmanaged(u8) = .{};
- try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(alloc), .{
+ try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(session_state.arena), .{
.navigation = false,
.origin_uri = &self.origin_url.uri,
});
@@ -501,12 +486,12 @@ pub const XMLHttpRequest = struct {
// var used_body: ?XMLHttpRequestBodyInit = null;
if (body) |b| {
if (self.method != .GET and self.method != .HEAD) {
- request.body = try alloc.dupe(u8, b);
+ request.body = try session_state.arena.dupe(u8, b);
try request.addHeader("Content-Type", "text/plain; charset=UTF-8", .{});
}
}
- try request.sendAsync(loop, self, .{});
+ try request.sendAsync(session_state.loop, self, .{});
}
pub fn onHttpResponse(self: *XMLHttpRequest, progress_: anyerror!http.Progress) !void {
@@ -526,8 +511,15 @@ pub const XMLHttpRequest = struct {
}
// extract a mime type from headers.
- const ct = header.get("content-type") orelse "text/xml";
- self.response_mime = Mime.parse(self.alloc, ct) catch |e| return self.onErr(e);
+ {
+ var raw: []const u8 = "text/xml";
+ if (header.get("content-type")) |ct| {
+ raw = try self.arena.dupe(u8, ct);
+ }
+ self.response_mime = Mime.parse(self.arena, raw) catch |e| {
+ return self.onErr(e);
+ };
+ }
// TODO handle override mime type
self.state = .headers_received;
@@ -545,7 +537,7 @@ pub const XMLHttpRequest = struct {
}
if (progress.data) |data| {
- try self.response_bytes.appendSlice(self.alloc, data);
+ try self.response_bytes.appendSlice(self.arena, data);
}
const loaded = self.response_bytes.items.len;
@@ -636,7 +628,7 @@ pub const XMLHttpRequest = struct {
return url.raw;
}
- pub fn get_responseXML(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response {
+ pub fn get_responseXML(self: *XMLHttpRequest) !?Response {
if (self.response_type != .Empty and self.response_type != .Document) {
return DOMError.InvalidState;
}
@@ -652,7 +644,7 @@ pub const XMLHttpRequest = struct {
};
}
- self.setResponseObjDocument(alloc);
+ self.setResponseObjDocument();
if (self.response_obj) |obj| {
return switch (obj) {
@@ -665,7 +657,7 @@ pub const XMLHttpRequest = struct {
}
// https://xhr.spec.whatwg.org/#the-response-attribute
- pub fn get_response(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response {
+ pub fn get_response(self: *XMLHttpRequest) !?Response {
if (self.response_type == .Empty or self.response_type == .Text) {
if (self.state == .loading or self.state == .done) {
return .{ .Text = try self.get_responseText() };
@@ -703,7 +695,7 @@ pub const XMLHttpRequest = struct {
// Otherwise, if thisβs response type is "document", set a
// document response for this.
if (self.response_type == .Document) {
- self.setResponseObjDocument(alloc);
+ self.setResponseObjDocument();
}
if (self.response_type == .JSON) {
@@ -712,7 +704,7 @@ pub const XMLHttpRequest = struct {
// TODO Let jsonObject be the result of running parse JSON from bytes
// on thisβs received bytes. If that threw an exception, then return
// null.
- self.setResponseObjJSON(alloc);
+ self.setResponseObjJSON();
}
if (self.response_obj) |obj| {
@@ -731,19 +723,23 @@ pub const XMLHttpRequest = struct {
// If the par sing fails, a Failure is stored in response_obj.
// TODO parse XML.
// https://xhr.spec.whatwg.org/#response-object
- fn setResponseObjDocument(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
+ fn setResponseObjDocument(self: *XMLHttpRequest) void {
const response_mime = &self.response_mime.?;
const isHTML = response_mime.isHTML();
// TODO If finalMIME is not an HTML MIME type or an XML MIME type, then
// return.
- if (!isHTML) return;
-
- const ccharset = alloc.dupeZ(u8, response_mime.charset orelse "utf-8") catch {
- self.response_obj = .{ .Failure = true };
+ if (!isHTML) {
return;
- };
- defer alloc.free(ccharset);
+ }
+
+ var ccharset: [:0]const u8 = "utf-8";
+ if (response_mime.charset) |rc| {
+ ccharset = self.arena.dupeZ(u8, rc) catch {
+ self.response_obj = .{ .Failure = true };
+ return;
+ };
+ }
var fbs = std.io.fixedBufferStream(self.response_bytes.items);
const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch {
@@ -760,12 +756,12 @@ pub const XMLHttpRequest = struct {
}
// setResponseObjJSON parses the received bytes as a std.json.Value.
- fn setResponseObjJSON(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
+ fn setResponseObjJSON(self: *XMLHttpRequest) void {
// TODO should we use parseFromSliceLeaky if we expect the allocator is
// already an arena?
const p = std.json.parseFromSlice(
JSONValue,
- alloc,
+ self.arena,
self.response_bytes.items,
.{},
) catch |e| {
@@ -790,14 +786,12 @@ pub const XMLHttpRequest = struct {
// TODO change the return type to express the string ownership and let
// jsruntime free the string once copied to v8.
// see https://github.com/lightpanda-io/jsruntime-lib/issues/195
- pub fn _getAllResponseHeaders(self: *XMLHttpRequest, alloc: std.mem.Allocator) ![]const u8 {
+ pub fn _getAllResponseHeaders(self: *XMLHttpRequest) ![]const u8 {
if (self.response_headers.list.items.len == 0) return "";
self.response_headers.sort();
var buf: std.ArrayListUnmanaged(u8) = .{};
- errdefer buf.deinit(alloc);
-
- const w = buf.writer(alloc);
+ const w = buf.writer(self.arena);
for (self.response_headers.list.items) |entry| {
if (entry.value.len == 0) continue;
@@ -822,101 +816,96 @@ pub const XMLHttpRequest = struct {
}
};
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var send = [_]Case{
- .{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt = event; }", .ex = "undefined" },
- .{ .src = "const req = new XMLHttpRequest()", .ex = "undefined" },
+const testing = @import("../../testing.zig");
+test "Browser.XHR.XMLHttpRequest" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
- .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" },
- // Getter returning a callback crashes.
- // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/200
- // .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; evt = event; }" },
- //.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" },
+ try runner.testCases(&.{
+ .{ "var nb = 0; var evt = null; function cbk(event) { nb ++; evt = event; }", "undefined" },
+ .{ "const req = new XMLHttpRequest()", "undefined" },
- .{ .src = "req.open('GET', 'https://httpbin.io/html')", .ex = "undefined" },
- .{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" },
+ .{ "req.onload = cbk", "function cbk(event) { nb ++; evt = event; }" },
+
+ .{ "req.onload", "function cbk(event) { nb ++; evt = event; }" },
+ .{ "req.onload = cbk", "function cbk(event) { nb ++; evt = event; }" },
+
+ .{ "req.open('GET', 'https://127.0.0.1:9581/xhr')", "undefined" },
+ .{ "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", "undefined" },
// ensure open resets values
- .{ .src = "req.status", .ex = "0" },
- .{ .src = "req.statusText", .ex = "" },
- .{ .src = "req.getAllResponseHeaders()", .ex = "" },
- .{ .src = "req.getResponseHeader('Content-Type')", .ex = "null" },
- .{ .src = "req.responseText", .ex = "" },
+ .{ "req.status ", "0" },
+ .{ "req.statusText", "" },
+ .{ "req.getAllResponseHeaders()", "" },
+ .{ "req.getResponseHeader('Content-Type')", "null" },
+ .{ "req.responseText", "" },
- .{ .src = "req.send(); nb", .ex = "0" },
+ .{ "req.send(); nb", "0" },
+
+ // Each case executed waits for all loop callback calls.
+ // So the url has been retrieved.
+ .{ "nb", "1" },
+ .{ "evt.type", "load" },
+ .{ "evt.loaded > 0", "true" },
+ .{ "evt instanceof ProgressEvent", "true" },
+ .{ "req.status", "200" },
+ .{ "req.statusText", "OK" },
+ .{ "req.getResponseHeader('Content-Type')", "text/html; charset=utf-8" },
+ .{ "req.getAllResponseHeaders().length", "61" },
+ .{ "req.responseText.length", "100" },
+ .{ "req.response.length == req.responseText.length", "true" },
+ .{ "req.responseXML instanceof Document", "true" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "const req2 = new XMLHttpRequest()", "undefined" },
+ .{ "req2.open('GET', 'https://127.0.0.1:9581/xhr')", "undefined" },
+ .{ "req2.responseType = 'document'", "document" },
+
+ .{ "req2.send()", "undefined" },
// Each case executed waits for all loop callaback calls.
// So the url has been retrieved.
- .{ .src = "nb", .ex = "1" },
- .{ .src = "evt.type", .ex = "load" },
- .{ .src = "evt.loaded > 0", .ex = "true" },
- .{ .src = "evt instanceof ProgressEvent", .ex = "true" },
- .{ .src = "req.status", .ex = "200" },
- .{ .src = "req.statusText", .ex = "OK" },
- .{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=utf-8" },
- .{ .src = "req.getAllResponseHeaders().length > 64", .ex = "true" },
- .{ .src = "req.responseText.length > 64", .ex = "true" },
- .{ .src = "req.response.length == req.responseText.length", .ex = "true" },
- .{ .src = "req.responseXML instanceof Document", .ex = "true" },
- };
- try checkCases(js_env, &send);
+ .{ "req2.status", "200" },
+ .{ "req2.statusText", "OK" },
+ .{ "req2.response instanceof Document", "true" },
+ .{ "req2.responseXML instanceof Document", "true" },
+ }, .{});
- var document = [_]Case{
- .{ .src = "const req2 = new XMLHttpRequest()", .ex = "undefined" },
- .{ .src = "req2.open('GET', 'https://httpbin.io/html')", .ex = "undefined" },
- .{ .src = "req2.responseType = 'document'", .ex = "document" },
+ try runner.testCases(&.{
+ .{ "const req3 = new XMLHttpRequest()", "undefined" },
+ .{ "req3.open('GET', 'https://127.0.0.1:9581/xhr/json')", "undefined" },
+ .{ "req3.responseType = 'json'", "json" },
- .{ .src = "req2.send()", .ex = "undefined" },
+ .{ "req3.send()", "undefined" },
// Each case executed waits for all loop callaback calls.
// So the url has been retrieved.
- .{ .src = "req2.status", .ex = "200" },
- .{ .src = "req2.statusText", .ex = "OK" },
- .{ .src = "req2.response instanceof Document", .ex = "true" },
- .{ .src = "req2.responseXML instanceof Document", .ex = "true" },
- };
- try checkCases(js_env, &document);
+ .{ "req3.status", "200" },
+ .{ "req3.statusText", "OK" },
+ .{ "req3.response.over", "9000!!!" },
+ }, .{});
- var json = [_]Case{
- .{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
- .{ .src = "req3.open('GET', 'https://httpbin.io/json')", .ex = "undefined" },
- .{ .src = "req3.responseType = 'json'", .ex = "json" },
-
- .{ .src = "req3.send()", .ex = "undefined" },
+ try runner.testCases(&.{
+ .{ "const req4 = new XMLHttpRequest()", "undefined" },
+ .{ "req4.open('POST', 'https://127.0.0.1:9581/xhr')", "undefined" },
+ .{ "req4.send('foo')", "undefined" },
// Each case executed waits for all loop callaback calls.
// So the url has been retrieved.
- .{ .src = "req3.status", .ex = "200" },
- .{ .src = "req3.statusText", .ex = "OK" },
- .{ .src = "req3.response.slideshow.author", .ex = "Yours Truly" },
- };
- try checkCases(js_env, &json);
+ .{ "req4.status", "200" },
+ .{ "req4.statusText", "OK" },
+ .{ "req4.responseText.length > 64", "true" },
+ }, .{});
- var post = [_]Case{
- .{ .src = "const req4 = new XMLHttpRequest()", .ex = "undefined" },
- .{ .src = "req4.open('POST', 'https://httpbin.io/post')", .ex = "undefined" },
- .{ .src = "req4.send('foo')", .ex = "undefined" },
+ try runner.testCases(&.{
+ .{ "const req5 = new XMLHttpRequest()", "undefined" },
+ .{ "req5.open('GET', 'https://127.0.0.1:9581/xhr')", "undefined" },
+ .{ "var status = 0; req5.onload = function () { status = this.status };", "function () { status = this.status }" },
+ .{ "req5.send()", "undefined" },
// Each case executed waits for all loop callaback calls.
// So the url has been retrieved.
- .{ .src = "req4.status", .ex = "200" },
- .{ .src = "req4.statusText", .ex = "OK" },
- .{ .src = "req4.responseText.length > 64", .ex = "true" },
- };
- try checkCases(js_env, &post);
-
- var cbk = [_]Case{
- .{ .src = "const req5 = new XMLHttpRequest()", .ex = "undefined" },
- .{ .src = "req5.open('GET', 'https://httpbin.io/json')", .ex = "undefined" },
- .{ .src = "var status = 0; req5.onload = function () { status = this.status };", .ex = "function () { status = this.status }" },
- .{ .src = "req5.send()", .ex = "undefined" },
-
- // Each case executed waits for all loop callaback calls.
- // So the url has been retrieved.
- .{ .src = "status", .ex = "200" },
- };
- try checkCases(js_env, &cbk);
+ .{ "status", "200" },
+ }, .{});
}
diff --git a/src/xmlserializer/xmlserializer.zig b/src/browser/xmlserializer/xmlserializer.zig
similarity index 54%
rename from src/xmlserializer/xmlserializer.zig
rename to src/browser/xmlserializer/xmlserializer.zig
index a0153558..ea2a4697 100644
--- a/src/xmlserializer/xmlserializer.zig
+++ b/src/browser/xmlserializer/xmlserializer.zig
@@ -18,14 +18,11 @@
//
const std = @import("std");
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
+const SessionState = @import("../env.zig").SessionState;
-const DOMError = @import("netsurf").DOMError;
-
-const parser = @import("netsurf");
-const dump = @import("../browser/dump.zig");
+const dump = @import("../dump.zig");
+const parser = @import("../netsurf.zig");
+const DOMError = parser.DOMError;
pub const Interfaces = .{
XMLSerializer,
@@ -33,39 +30,28 @@ pub const Interfaces = .{
// https://w3c.github.io/DOM-Parsing/#dom-xmlserializer-constructor
pub const XMLSerializer = struct {
- pub const mem_guarantied = true;
-
pub fn constructor() !XMLSerializer {
return .{};
}
- pub fn deinit(_: *XMLSerializer, _: std.mem.Allocator) void {}
-
- pub fn _serializeToString(_: XMLSerializer, alloc: std.mem.Allocator, root: *parser.Node) ![]const u8 {
- var buf = std.ArrayList(u8).init(alloc);
- defer buf.deinit();
-
+ pub fn _serializeToString(_: *const XMLSerializer, root: *parser.Node, state: *SessionState) ![]const u8 {
+ var buf = std.ArrayList(u8).init(state.arena);
if (try parser.nodeType(root) == .document) {
try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), buf.writer());
} else {
try dump.writeNode(root, buf.writer());
}
- // TODO express the caller owned the slice.
- // https://github.com/lightpanda-io/jsruntime-lib/issues/195
- return try buf.toOwnedSlice();
+ return buf.items;
}
};
-// Tests
-// -----
+const testing = @import("../../testing.zig");
+test "Browser.XMLSerializer" {
+ var runner = try testing.jsRunner(testing.tracking_allocator, .{});
+ defer runner.deinit();
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var serializer = [_]Case{
- .{ .src = "const s = new XMLSerializer()", .ex = "undefined" },
- .{ .src = "s.serializeToString(document.getElementById('para'))", .ex = " And
" },
- };
- try checkCases(js_env, &serializer);
+ try runner.testCases(&.{
+ .{ "const s = new XMLSerializer()", "undefined" },
+ .{ "s.serializeToString(document.getElementById('para'))", " And
" },
+ }, .{});
}
diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig
index f609be49..a7a6b658 100644
--- a/src/cdp/Node.zig
+++ b/src/cdp/Node.zig
@@ -17,9 +17,10 @@
// along with this program. If not, see .
const std = @import("std");
-const parser = @import("netsurf");
const Allocator = std.mem.Allocator;
+const parser = @import("../browser/netsurf.zig");
+
pub const Id = u32;
const log = std.log.scoped(.cdp_node);
diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig
index ff00b5e9..146fc84e 100644
--- a/src/cdp/cdp.zig
+++ b/src/cdp/cdp.zig
@@ -72,13 +72,16 @@ pub fn CDPT(comptime TypeProvider: type) type {
pub const Browser = TypeProvider.Browser;
pub const Session = TypeProvider.Session;
- pub fn init(app: *App, client: TypeProvider.Client) Self {
+ pub fn init(app: *App, client: TypeProvider.Client) !Self {
const allocator = app.allocator;
+ const browser = try Browser.init(app);
+ errdefer browser.deinit();
+
return .{
.client = client,
+ .browser = browser,
.allocator = allocator,
.browser_context = null,
- .browser = Browser.init(app),
.message_arena = std.heap.ArenaAllocator.init(allocator),
.browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator),
};
diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig
index 80e6b96b..e85ee7fe 100644
--- a/src/cdp/domains/dom.zig
+++ b/src/cdp/domains/dom.zig
@@ -17,10 +17,10 @@
// along with this program. If not, see .
const std = @import("std");
-const parser = @import("netsurf");
const Node = @import("../Node.zig");
-const css = @import("../../dom/css.zig");
-const dom_node = @import("../../dom/node.zig");
+const css = @import("../../browser/dom/css.zig");
+const parser = @import("../../browser/netsurf.zig");
+const dom_node = @import("../../browser/dom/node.zig");
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
@@ -134,17 +134,20 @@ fn resolveNode(cmd: anytype) !void {
// node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
// So we use the Node.Union when retrieve the value from the environment
- const jsValue = try bc.session.env.findOrAddValue(try dom_node.Node.toInterface(node._node));
- const remoteObject = try bc.session.inspector.getRemoteObject(&bc.session.env, jsValue, params.objectGroup orelse "");
- defer remoteObject.deinit();
+ const remote_object = try bc.session.inspector.getRemoteObject(
+ bc.session.executor,
+ params.objectGroup orelse "",
+ try dom_node.Node.toInterface(node._node),
+ );
+ defer remote_object.deinit();
const arena = cmd.arena;
return cmd.sendResult(.{ .object = .{
- .type = try remoteObject.getType(arena),
- .subtype = try remoteObject.getSubtype(arena),
- .className = try remoteObject.getClassName(arena),
- .description = try remoteObject.getDescription(arena),
- .objectId = try remoteObject.getObjectId(arena),
+ .type = try remote_object.getType(arena),
+ .subtype = try remote_object.getSubtype(arena),
+ .className = try remote_object.getClassName(arena),
+ .description = try remote_object.getDescription(arena),
+ .objectId = try remote_object.getObjectId(arena),
} }, .{});
}
diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig
index c2c833e3..1f15caf6 100644
--- a/src/cdp/testing.zig
+++ b/src/cdp/testing.zig
@@ -23,9 +23,9 @@ const Allocator = std.mem.Allocator;
const Testing = @This();
const main = @import("cdp.zig");
-const parser = @import("netsurf");
const URL = @import("../url.zig").URL;
const App = @import("../app.zig").App;
+const parser = @import("../browser/netsurf.zig");
const base = @import("../testing.zig");
pub const allocator = base.allocator;
@@ -40,7 +40,7 @@ const Browser = struct {
session: ?*Session = null,
arena: std.heap.ArenaAllocator,
- pub fn init(app: *App) Browser {
+ pub fn init(app: *App) !Browser {
return .{
.arena = std.heap.ArenaAllocator.init(app.allocator),
};
@@ -61,8 +61,8 @@ const Browser = struct {
self.session.?.* = .{
.page = null,
.arena = arena,
- .env = Env{},
- .inspector = Inspector{},
+ .executor = .{},
+ .inspector = .{},
};
return self.session.?;
}
@@ -78,7 +78,7 @@ const Browser = struct {
const Session = struct {
page: ?Page = null,
arena: Allocator,
- env: Env,
+ executor: Executor,
inspector: Inspector,
pub fn currentPage(self: *Session) ?*Page {
@@ -107,19 +107,19 @@ const Session = struct {
}
};
-const Env = struct {
- pub fn findOrAddValue(self: *Env, value: anytype) !@TypeOf(value) { // ?
- _ = self;
- return value;
- }
-};
+const Executor = struct {};
const Inspector = struct {
- pub fn getRemoteObject(self: Inspector, env: *Env, jsValue: anytype, groupName: []const u8) !RemoteObject {
+ pub fn getRemoteObject(
+ self: *const Inspector,
+ executor: Executor,
+ group: []const u8,
+ value: anytype,
+ ) !RemoteObject {
_ = self;
- _ = env;
- _ = jsValue;
- _ = groupName;
+ _ = executor;
+ _ = group;
+ _ = value;
return RemoteObject{};
}
};
@@ -217,7 +217,7 @@ const TestContext = struct {
self.client = Client.init(self.arena.allocator());
// Don't use the arena here. We want to detect leaks in CDP.
// The arena is only for test-specific stuff
- self.cdp_ = TestCDP.init(self.app, &self.client.?);
+ self.cdp_ = try TestCDP.init(self.app, &self.client.?);
}
return &self.cdp_.?;
}
diff --git a/src/dom/document.zig b/src/dom/document.zig
deleted file mode 100644
index 0b8d0cd8..00000000
--- a/src/dom/document.zig
+++ /dev/null
@@ -1,468 +0,0 @@
-// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
-//
-// Francis Bouvier
-// Pierre Tachoire
-//
-// 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 .
-
-const std = @import("std");
-
-const parser = @import("netsurf");
-
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-const Variadic = jsruntime.Variadic;
-
-const Node = @import("node.zig").Node;
-const NodeList = @import("nodelist.zig").NodeList;
-const NodeUnion = @import("node.zig").Union;
-
-const collection = @import("html_collection.zig");
-const css = @import("css.zig");
-
-const Element = @import("element.zig").Element;
-const ElementUnion = @import("element.zig").Union;
-
-const DocumentType = @import("document_type.zig").DocumentType;
-const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
-const DOMImplementation = @import("implementation.zig").DOMImplementation;
-
-const UserContext = @import("../user_context.zig").UserContext;
-
-// WEB IDL https://dom.spec.whatwg.org/#document
-pub const Document = struct {
- pub const Self = parser.Document;
- pub const prototype = *Node;
- pub const mem_guarantied = true;
-
- pub fn constructor(userctx: UserContext) !*parser.DocumentHTML {
- const doc = try parser.documentCreateDocument(
- try parser.documentHTMLGetTitle(userctx.document),
- );
-
- // we have to work w/ document instead of html document.
- const ddoc = parser.documentHTMLToDocument(doc);
- const ccur = parser.documentHTMLToDocument(userctx.document);
- try parser.documentSetDocumentURI(ddoc, try parser.documentGetDocumentURI(ccur));
- try parser.documentSetInputEncoding(ddoc, try parser.documentGetInputEncoding(ccur));
-
- return doc;
- }
-
- // JS funcs
- // --------
- pub fn get_implementation(_: *parser.Document) DOMImplementation {
- return DOMImplementation{};
- }
-
- pub fn get_documentElement(self: *parser.Document) !?ElementUnion {
- const e = try parser.documentGetDocumentElement(self);
- if (e == null) return null;
- return try Element.toInterface(e.?);
- }
-
- pub fn get_documentURI(self: *parser.Document) ![]const u8 {
- return try parser.documentGetDocumentURI(self);
- }
-
- pub fn get_URL(self: *parser.Document) ![]const u8 {
- return try get_documentURI(self);
- }
-
- // TODO implement contentType
- pub fn get_contentType(self: *parser.Document) []const u8 {
- _ = self;
- return "text/html";
- }
-
- // TODO implement compactMode
- pub fn get_compatMode(self: *parser.Document) []const u8 {
- _ = self;
- return "CSS1Compat";
- }
-
- pub fn get_characterSet(self: *parser.Document) ![]const u8 {
- return try parser.documentGetInputEncoding(self);
- }
-
- // alias of get_characterSet
- pub fn get_charset(self: *parser.Document) ![]const u8 {
- return try get_characterSet(self);
- }
-
- // alias of get_characterSet
- pub fn get_inputEncoding(self: *parser.Document) ![]const u8 {
- return try get_characterSet(self);
- }
-
- pub fn get_doctype(self: *parser.Document) !?*parser.DocumentType {
- return try parser.documentGetDoctype(self);
- }
-
- pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !*parser.Event {
- // TODO: for now only "Event" constructor is supported
- // see table on https://dom.spec.whatwg.org/#dom-document-createevent $2
- if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) {
- return try parser.eventCreate();
- }
- return parser.DOMError.NotSupported;
- }
-
- pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion {
- const e = try parser.documentGetElementById(self, id) orelse return null;
- return try Element.toInterface(e);
- }
-
- pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
- const e = try parser.documentCreateElement(self, tag_name);
- return try Element.toInterface(e);
- }
-
- pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {
- const e = try parser.documentCreateElementNS(self, ns, tag_name);
- return try Element.toInterface(e);
- }
-
- // We can't simply use libdom dom_document_get_elements_by_tag_name here.
- // Indeed, netsurf implemented a previous dom spec when
- // getElementsByTagName returned a NodeList.
- // But since
- // https://github.com/whatwg/dom/commit/190700b7c12ecfd3b5ebdb359ab1d6ea9cbf7749
- // the spec changed to return an HTMLCollection instead.
- // That's why we reimplemented getElementsByTagName by using an
- // HTMLCollection in zig here.
- pub fn _getElementsByTagName(
- self: *parser.Document,
- alloc: std.mem.Allocator,
- tag_name: []const u8,
- ) !collection.HTMLCollection {
- return try collection.HTMLCollectionByTagName(alloc, parser.documentToNode(self), tag_name, true);
- }
-
- pub fn _getElementsByClassName(
- self: *parser.Document,
- alloc: std.mem.Allocator,
- classNames: []const u8,
- ) !collection.HTMLCollection {
- return try collection.HTMLCollectionByClassName(alloc, parser.documentToNode(self), classNames, true);
- }
-
- pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment {
- return try parser.documentCreateDocumentFragment(self);
- }
-
- pub fn _createTextNode(self: *parser.Document, data: []const u8) !*parser.Text {
- return try parser.documentCreateTextNode(self, data);
- }
-
- pub fn _createCDATASection(self: *parser.Document, data: []const u8) !*parser.CDATASection {
- return try parser.documentCreateCDATASection(self, data);
- }
-
- pub fn _createComment(self: *parser.Document, data: []const u8) !*parser.Comment {
- return try parser.documentCreateComment(self, data);
- }
-
- pub fn _createProcessingInstruction(self: *parser.Document, target: []const u8, data: []const u8) !*parser.ProcessingInstruction {
- return try parser.documentCreateProcessingInstruction(self, target, data);
- }
-
- pub fn _importNode(self: *parser.Document, node: *parser.Node, deep: ?bool) !NodeUnion {
- const n = try parser.documentImportNode(self, node, deep orelse false);
- return try Node.toInterface(n);
- }
-
- pub fn _adoptNode(self: *parser.Document, node: *parser.Node) !NodeUnion {
- const n = try parser.documentAdoptNode(self, node);
- return try Node.toInterface(n);
- }
-
- pub fn _createAttribute(self: *parser.Document, name: []const u8) !*parser.Attribute {
- return try parser.documentCreateAttribute(self, name);
- }
-
- pub fn _createAttributeNS(self: *parser.Document, ns: []const u8, qname: []const u8) !*parser.Attribute {
- return try parser.documentCreateAttributeNS(self, ns, qname);
- }
-
- // ParentNode
- // https://dom.spec.whatwg.org/#parentnode
- pub fn get_children(self: *parser.Document) !collection.HTMLCollection {
- return try collection.HTMLCollectionChildren(parser.documentToNode(self), false);
- }
-
- pub fn get_firstElementChild(self: *parser.Document) !?ElementUnion {
- const elt = try parser.documentGetDocumentElement(self) orelse return null;
- return try Element.toInterface(elt);
- }
-
- pub fn get_lastElementChild(self: *parser.Document) !?ElementUnion {
- const elt = try parser.documentGetDocumentElement(self) orelse return null;
- return try Element.toInterface(elt);
- }
-
- pub fn get_childElementCount(self: *parser.Document) !u32 {
- _ = try parser.documentGetDocumentElement(self) orelse return 0;
- return 1;
- }
-
- pub fn _querySelector(self: *parser.Document, alloc: std.mem.Allocator, selector: []const u8) !?ElementUnion {
- if (selector.len == 0) return null;
-
- const n = try css.querySelector(alloc, parser.documentToNode(self), selector);
-
- if (n == null) return null;
-
- return try Element.toInterface(parser.nodeToElement(n.?));
- }
-
- pub fn _querySelectorAll(self: *parser.Document, alloc: std.mem.Allocator, selector: []const u8) !NodeList {
- return css.querySelectorAll(alloc, parser.documentToNode(self), selector);
- }
-
- // TODO according with https://dom.spec.whatwg.org/#parentnode, the
- // function must accept either node or string.
- // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
- pub fn _prepend(self: *parser.Document, nodes: ?Variadic(*parser.Node)) !void {
- return Node.prepend(parser.documentToNode(self), nodes);
- }
-
- // TODO according with https://dom.spec.whatwg.org/#parentnode, the
- // function must accept either node or string.
- // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
- pub fn _append(self: *parser.Document, nodes: ?Variadic(*parser.Node)) !void {
- return Node.append(parser.documentToNode(self), nodes);
- }
-
- // TODO according with https://dom.spec.whatwg.org/#parentnode, the
- // function must accept either node or string.
- // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
- pub fn _replaceChildren(self: *parser.Document, nodes: ?Variadic(*parser.Node)) !void {
- return Node.replaceChildren(parser.documentToNode(self), nodes);
- }
-
- pub fn deinit(_: *parser.Document, _: std.mem.Allocator) void {}
-};
-
-// Tests
-// -----
-
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var constructor = [_]Case{
- .{ .src = "document.__proto__.__proto__.constructor.name", .ex = "Document" },
- .{ .src = "document.__proto__.__proto__.__proto__.constructor.name", .ex = "Node" },
- .{ .src = "document.__proto__.__proto__.__proto__.__proto__.constructor.name", .ex = "EventTarget" },
-
- .{ .src = "let newdoc = new Document()", .ex = "undefined" },
- .{ .src = "newdoc.documentElement", .ex = "null" },
- .{ .src = "newdoc.children.length", .ex = "0" },
- .{ .src = "newdoc.getElementsByTagName('*').length", .ex = "0" },
- .{ .src = "newdoc.getElementsByTagName('*').item(0)", .ex = "null" },
- .{ .src = "newdoc.inputEncoding === document.inputEncoding", .ex = "true" },
- .{ .src = "newdoc.documentURI === document.documentURI", .ex = "true" },
- .{ .src = "newdoc.URL === document.URL", .ex = "true" },
- .{ .src = "newdoc.compatMode === document.compatMode", .ex = "true" },
- .{ .src = "newdoc.characterSet === document.characterSet", .ex = "true" },
- .{ .src = "newdoc.charset === document.charset", .ex = "true" },
- .{ .src = "newdoc.contentType === document.contentType", .ex = "true" },
- };
- try checkCases(js_env, &constructor);
-
- var getElementById = [_]Case{
- .{ .src = "let getElementById = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "getElementById.constructor.name", .ex = "HTMLDivElement" },
- .{ .src = "getElementById.localName", .ex = "div" },
- };
- try checkCases(js_env, &getElementById);
-
- var getElementsByTagName = [_]Case{
- .{ .src = "let getElementsByTagName = document.getElementsByTagName('p')", .ex = "undefined" },
- .{ .src = "getElementsByTagName.length", .ex = "2" },
- .{ .src = "getElementsByTagName.item(0).localName", .ex = "p" },
- .{ .src = "getElementsByTagName.item(1).localName", .ex = "p" },
- .{ .src = "let getElementsByTagNameAll = document.getElementsByTagName('*')", .ex = "undefined" },
- .{ .src = "getElementsByTagNameAll.length", .ex = "8" },
- .{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" },
- .{ .src = "getElementsByTagNameAll.item(7).localName", .ex = "p" },
- .{ .src = "getElementsByTagNameAll.namedItem('para-empty-child').localName", .ex = "span" },
- };
- try checkCases(js_env, &getElementsByTagName);
-
- var getElementsByClassName = [_]Case{
- .{ .src = "let ok = document.getElementsByClassName('ok')", .ex = "undefined" },
- .{ .src = "ok.length", .ex = "2" },
- .{ .src = "let empty = document.getElementsByClassName('empty')", .ex = "undefined" },
- .{ .src = "empty.length", .ex = "1" },
- .{ .src = "let emptyok = document.getElementsByClassName('empty ok')", .ex = "undefined" },
- .{ .src = "emptyok.length", .ex = "1" },
- };
- try checkCases(js_env, &getElementsByClassName);
-
- var getDocumentElement = [_]Case{
- .{ .src = "let e = document.documentElement", .ex = "undefined" },
- .{ .src = "e.localName", .ex = "html" },
- };
- try checkCases(js_env, &getDocumentElement);
-
- var getCharacterSet = [_]Case{
- .{ .src = "document.characterSet", .ex = "UTF-8" },
- .{ .src = "document.charset", .ex = "UTF-8" },
- .{ .src = "document.inputEncoding", .ex = "UTF-8" },
- };
- try checkCases(js_env, &getCharacterSet);
-
- var getCompatMode = [_]Case{
- .{ .src = "document.compatMode", .ex = "CSS1Compat" },
- };
- try checkCases(js_env, &getCompatMode);
-
- var getContentType = [_]Case{
- .{ .src = "document.contentType", .ex = "text/html" },
- };
- try checkCases(js_env, &getContentType);
-
- var getDocumentURI = [_]Case{
- .{ .src = "document.documentURI", .ex = "about:blank" },
- .{ .src = "document.URL", .ex = "about:blank" },
- };
- try checkCases(js_env, &getDocumentURI);
-
- var getImplementation = [_]Case{
- .{ .src = "let impl = document.implementation", .ex = "undefined" },
- };
- try checkCases(js_env, &getImplementation);
-
- var new = [_]Case{
- .{ .src = "let d = new Document()", .ex = "undefined" },
- .{ .src = "d.characterSet", .ex = "UTF-8" },
- .{ .src = "d.URL", .ex = "about:blank" },
- .{ .src = "d.documentURI", .ex = "about:blank" },
- .{ .src = "d.compatMode", .ex = "CSS1Compat" },
- .{ .src = "d.contentType", .ex = "text/html" },
- };
- try checkCases(js_env, &new);
-
- var createDocumentFragment = [_]Case{
- .{ .src = "var v = document.createDocumentFragment()", .ex = "undefined" },
- .{ .src = "v.nodeName", .ex = "#document-fragment" },
- };
- try checkCases(js_env, &createDocumentFragment);
-
- var createTextNode = [_]Case{
- .{ .src = "var v = document.createTextNode('foo')", .ex = "undefined" },
- .{ .src = "v.nodeName", .ex = "#text" },
- };
- try checkCases(js_env, &createTextNode);
-
- var createCDATASection = [_]Case{
- .{ .src = "var v = document.createCDATASection('foo')", .ex = "undefined" },
- .{ .src = "v.nodeName", .ex = "#cdata-section" },
- };
- try checkCases(js_env, &createCDATASection);
-
- var createComment = [_]Case{
- .{ .src = "var v = document.createComment('foo')", .ex = "undefined" },
- .{ .src = "v.nodeName", .ex = "#comment" },
- .{ .src = "let v2 = v.cloneNode()", .ex = "undefined" },
- };
- try checkCases(js_env, &createComment);
-
- var createProcessingInstruction = [_]Case{
- .{ .src = "let pi = document.createProcessingInstruction('foo', 'bar')", .ex = "undefined" },
- .{ .src = "pi.target", .ex = "foo" },
- .{ .src = "let pi2 = pi.cloneNode()", .ex = "undefined" },
- };
- try checkCases(js_env, &createProcessingInstruction);
-
- var importNode = [_]Case{
- .{ .src = "let nimp = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "var v = document.importNode(nimp)", .ex = "undefined" },
- .{ .src = "v.nodeName", .ex = "DIV" },
- };
- try checkCases(js_env, &importNode);
-
- var createAttr = [_]Case{
- .{ .src = "var v = document.createAttribute('foo')", .ex = "undefined" },
- .{ .src = "v.nodeName", .ex = "foo" },
- };
- try checkCases(js_env, &createAttr);
-
- var parentNode = [_]Case{
- .{ .src = "document.children.length", .ex = "1" },
- .{ .src = "document.children.item(0).nodeName", .ex = "HTML" },
- .{ .src = "document.firstElementChild.nodeName", .ex = "HTML" },
- .{ .src = "document.lastElementChild.nodeName", .ex = "HTML" },
- .{ .src = "document.childElementCount", .ex = "1" },
-
- .{ .src = "let nd = new Document()", .ex = "undefined" },
- .{ .src = "nd.children.length", .ex = "0" },
- .{ .src = "nd.children.item(0)", .ex = "null" },
- .{ .src = "nd.firstElementChild", .ex = "null" },
- .{ .src = "nd.lastElementChild", .ex = "null" },
- .{ .src = "nd.childElementCount", .ex = "0" },
-
- .{ .src = "let emptydoc = document.createElement('html')", .ex = "undefined" },
- .{ .src = "emptydoc.prepend(document.createElement('html'))", .ex = "undefined" },
-
- .{ .src = "let emptydoc2 = document.createElement('html')", .ex = "undefined" },
- .{ .src = "emptydoc2.append(document.createElement('html'))", .ex = "undefined" },
- };
- try checkCases(js_env, &parentNode);
-
- var querySelector = [_]Case{
- .{ .src = "document.querySelector('')", .ex = "null" },
- .{ .src = "document.querySelector('*').nodeName", .ex = "HTML" },
- .{ .src = "document.querySelector('#content').id", .ex = "content" },
- .{ .src = "document.querySelector('#para').id", .ex = "para" },
- .{ .src = "document.querySelector('.ok').id", .ex = "link" },
- .{ .src = "document.querySelector('a ~ p').id", .ex = "para-empty" },
- .{ .src = "document.querySelector(':root').nodeName", .ex = "HTML" },
-
- .{ .src = "document.querySelectorAll('p').length", .ex = "2" },
- .{ .src =
- \\Array.from(document.querySelectorAll('#content > p#para-empty'))
- \\.map(row => row.querySelector('span').textContent)
- \\.length;
- , .ex = "1" },
- };
- try checkCases(js_env, &querySelector);
-
- // this test breaks the doc structure, keep it at the end of the test
- // suite.
- var adoptNode = [_]Case{
- .{ .src = "let nadop = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "var v = document.adoptNode(nadop)", .ex = "undefined" },
- .{ .src = "v.nodeName", .ex = "DIV" },
- };
- try checkCases(js_env, &adoptNode);
-
- const tags = comptime parser.Tag.all();
- var createElements: [(tags.len) * 2]Case = undefined;
- inline for (tags, 0..) |tag, i| {
- const tag_name = @tagName(tag);
- createElements[i * 2] = Case{
- .src = "var " ++ tag_name ++ "Elem = document.createElement('" ++ tag_name ++ "')",
- .ex = "undefined",
- };
- createElements[(i * 2) + 1] = Case{
- .src = tag_name ++ "Elem.localName",
- .ex = tag_name,
- };
- }
- try checkCases(js_env, &createElements);
-}
diff --git a/src/dom/event_target.zig b/src/dom/event_target.zig
deleted file mode 100644
index ca688993..00000000
--- a/src/dom/event_target.zig
+++ /dev/null
@@ -1,243 +0,0 @@
-// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
-//
-// Francis Bouvier
-// Pierre Tachoire
-//
-// 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 .
-
-const std = @import("std");
-
-const jsruntime = @import("jsruntime");
-const Callback = jsruntime.Callback;
-const JSObjectID = jsruntime.JSObjectID;
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-const parser = @import("netsurf");
-const EventHandler = @import("../events/event.zig").EventHandler;
-
-const DOMException = @import("exceptions.zig").DOMException;
-const Nod = @import("node.zig");
-
-// EventTarget interfaces
-pub const Union = Nod.Union;
-
-// EventTarget implementation
-pub const EventTarget = struct {
- pub const Self = parser.EventTarget;
- pub const Exception = DOMException;
- pub const mem_guarantied = true;
-
- pub fn toInterface(et: *parser.EventTarget) !Union {
- // NOTE: for now we state that all EventTarget are Nodes
- // TODO: handle other types (eg. Window)
- return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
- }
-
- // JS funcs
- // --------
-
- pub fn _addEventListener(
- self: *parser.EventTarget,
- alloc: std.mem.Allocator,
- eventType: []const u8,
- cbk: Callback,
- capture: ?bool,
- // TODO: hanle EventListenerOptions
- // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
- ) !void {
-
- // check if event target has already this listener
- const lst = try parser.eventTargetHasListener(
- self,
- eventType,
- capture orelse false,
- cbk.id(),
- );
- if (lst != null) {
- return;
- }
-
- try parser.eventTargetAddEventListener(
- self,
- alloc,
- eventType,
- EventHandler,
- .{ .cbk = cbk },
- capture orelse false,
- );
- }
-
- pub fn _removeEventListener(
- self: *parser.EventTarget,
- alloc: std.mem.Allocator,
- eventType: []const u8,
- cbk_id: JSObjectID,
- capture: ?bool,
- // TODO: hanle EventListenerOptions
- // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
- ) !void {
-
- // check if event target has already this listener
- const lst = try parser.eventTargetHasListener(
- self,
- eventType,
- capture orelse false,
- cbk_id.get(),
- );
- if (lst == null) {
- return;
- }
-
- // remove listener
- try parser.eventTargetRemoveEventListener(
- self,
- alloc,
- eventType,
- lst.?,
- capture orelse false,
- );
- }
-
- pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool {
- return try parser.eventTargetDispatchEvent(self, event);
- }
-
- pub fn deinit(self: *parser.EventTarget, alloc: std.mem.Allocator) void {
- parser.eventTargetRemoveAllEventListeners(self, alloc) catch unreachable;
- }
-};
-
-// Tests
-// -----
-
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var common = [_]Case{
- .{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "let para = document.getElementById('para')", .ex = "undefined" },
- // NOTE: as some event properties will change during the event dispatching phases
- // we need to copy thoses values in order to check them afterwards
- .{ .src =
- \\var nb = 0; var evt; var phase; var cur;
- \\function cbk(event) {
- \\evt = event;
- \\phase = event.eventPhase;
- \\cur = event.currentTarget;
- \\nb ++;
- \\}
- , .ex = "undefined" },
- };
- try checkCases(js_env, &common);
-
- var basic = [_]Case{
- .{ .src = "content.addEventListener('basic', cbk)", .ex = "undefined" },
- .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" },
- .{ .src = "nb", .ex = "1" },
- .{ .src = "evt instanceof Event", .ex = "true" },
- .{ .src = "evt.type", .ex = "basic" },
- .{ .src = "phase", .ex = "2" },
- .{ .src = "cur.getAttribute('id')", .ex = "content" },
- };
- try checkCases(js_env, &basic);
-
- var basic_child = [_]Case{
- .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" },
- .{ .src = "para.dispatchEvent(new Event('basic'))", .ex = "true" },
- .{ .src = "nb", .ex = "0" }, // handler is not called, no capture, not the target, no bubbling
- .{ .src = "evt === undefined", .ex = "true" },
- };
- try checkCases(js_env, &basic_child);
-
- var basic_twice = [_]Case{
- .{ .src = "nb = 0", .ex = "0" },
- .{ .src = "content.addEventListener('basic', cbk)", .ex = "undefined" },
- .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" },
- .{ .src = "nb", .ex = "1" },
- };
- try checkCases(js_env, &basic_twice);
-
- var basic_twice_capture = [_]Case{
- .{ .src = "nb = 0", .ex = "0" },
- .{ .src = "content.addEventListener('basic', cbk, true)", .ex = "undefined" },
- .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" },
- .{ .src = "nb", .ex = "2" },
- };
- try checkCases(js_env, &basic_twice_capture);
-
- var basic_remove = [_]Case{
- .{ .src = "nb = 0", .ex = "0" },
- .{ .src = "content.removeEventListener('basic', cbk)", .ex = "undefined" },
- .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" },
- .{ .src = "nb", .ex = "1" },
- };
- try checkCases(js_env, &basic_remove);
-
- var basic_capture_remove = [_]Case{
- .{ .src = "nb = 0", .ex = "0" },
- .{ .src = "content.removeEventListener('basic', cbk, true)", .ex = "undefined" },
- .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" },
- .{ .src = "nb", .ex = "0" },
- };
- try checkCases(js_env, &basic_capture_remove);
-
- var capture = [_]Case{
- .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" },
- .{ .src = "content.addEventListener('capture', cbk, true)", .ex = "undefined" },
- .{ .src = "content.dispatchEvent(new Event('capture'))", .ex = "true" },
- .{ .src = "nb", .ex = "1" },
- .{ .src = "evt instanceof Event", .ex = "true" },
- .{ .src = "evt.type", .ex = "capture" },
- .{ .src = "phase", .ex = "2" },
- .{ .src = "cur.getAttribute('id')", .ex = "content" },
- };
- try checkCases(js_env, &capture);
-
- var capture_child = [_]Case{
- .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" },
- .{ .src = "para.dispatchEvent(new Event('capture'))", .ex = "true" },
- .{ .src = "nb", .ex = "1" },
- .{ .src = "evt instanceof Event", .ex = "true" },
- .{ .src = "evt.type", .ex = "capture" },
- .{ .src = "phase", .ex = "1" },
- .{ .src = "cur.getAttribute('id')", .ex = "content" },
- };
- try checkCases(js_env, &capture_child);
-
- var bubbles = [_]Case{
- .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" },
- .{ .src = "content.addEventListener('bubbles', cbk)", .ex = "undefined" },
- .{ .src = "content.dispatchEvent(new Event('bubbles', {bubbles: true}))", .ex = "true" },
- .{ .src = "nb", .ex = "1" },
- .{ .src = "evt instanceof Event", .ex = "true" },
- .{ .src = "evt.type", .ex = "bubbles" },
- .{ .src = "evt.bubbles", .ex = "true" },
- .{ .src = "phase", .ex = "2" },
- .{ .src = "cur.getAttribute('id')", .ex = "content" },
- };
- try checkCases(js_env, &bubbles);
-
- var bubbles_child = [_]Case{
- .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" },
- .{ .src = "para.dispatchEvent(new Event('bubbles', {bubbles: true}))", .ex = "true" },
- .{ .src = "nb", .ex = "1" },
- .{ .src = "evt instanceof Event", .ex = "true" },
- .{ .src = "evt.type", .ex = "bubbles" },
- .{ .src = "phase", .ex = "3" },
- .{ .src = "cur.getAttribute('id')", .ex = "content" },
- };
- try checkCases(js_env, &bubbles_child);
-}
diff --git a/src/events/event.zig b/src/events/event.zig
deleted file mode 100644
index 35069ceb..00000000
--- a/src/events/event.zig
+++ /dev/null
@@ -1,263 +0,0 @@
-// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
-//
-// Francis Bouvier
-// Pierre Tachoire
-//
-// 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 .
-
-const std = @import("std");
-
-const generate = @import("../generate.zig");
-
-const jsruntime = @import("jsruntime");
-const Callback = jsruntime.Callback;
-const CallbackResult = jsruntime.CallbackResult;
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-const parser = @import("netsurf");
-
-const DOMException = @import("../dom/exceptions.zig").DOMException;
-const EventTarget = @import("../dom/event_target.zig").EventTarget;
-const EventTargetUnion = @import("../dom/event_target.zig").Union;
-
-const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
-
-const log = std.log.scoped(.events);
-
-// Event interfaces
-pub const Interfaces = .{
- Event,
- ProgressEvent,
-};
-
-pub const Union = generate.Union(Interfaces);
-
-// https://dom.spec.whatwg.org/#event
-pub const Event = struct {
- pub const Self = parser.Event;
- pub const Exception = DOMException;
- pub const mem_guarantied = true;
-
- pub const EventInit = parser.EventInit;
-
- // JS
- // --
-
- pub const _CAPTURING_PHASE = 1;
- pub const _AT_TARGET = 2;
- pub const _BUBBLING_PHASE = 3;
-
- pub fn toInterface(evt: *parser.Event) !Union {
- return switch (try parser.eventGetInternalType(evt)) {
- .event => .{ .Event = evt },
- .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
- };
- }
-
- pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event {
- const event = try parser.eventCreate();
- try parser.eventInit(event, eventType, opts orelse EventInit{});
- return event;
- }
-
- // Getters
-
- pub fn get_type(self: *parser.Event) ![]const u8 {
- return try parser.eventType(self);
- }
-
- pub fn get_target(self: *parser.Event) !?EventTargetUnion {
- const et = try parser.eventTarget(self);
- if (et == null) return null;
- return try EventTarget.toInterface(et.?);
- }
-
- pub fn get_currentTarget(self: *parser.Event) !?EventTargetUnion {
- const et = try parser.eventCurrentTarget(self);
- if (et == null) return null;
- return try EventTarget.toInterface(et.?);
- }
-
- pub fn get_eventPhase(self: *parser.Event) !u8 {
- return try parser.eventPhase(self);
- }
-
- pub fn get_bubbles(self: *parser.Event) !bool {
- return try parser.eventBubbles(self);
- }
-
- pub fn get_cancelable(self: *parser.Event) !bool {
- return try parser.eventCancelable(self);
- }
-
- pub fn get_defaultPrevented(self: *parser.Event) !bool {
- return try parser.eventDefaultPrevented(self);
- }
-
- pub fn get_isTrusted(self: *parser.Event) !bool {
- return try parser.eventIsTrusted(self);
- }
-
- pub fn get_timestamp(self: *parser.Event) !u32 {
- return try parser.eventTimestamp(self);
- }
-
- // Methods
-
- pub fn _initEvent(
- self: *parser.Event,
- eventType: []const u8,
- bubbles: ?bool,
- cancelable: ?bool,
- ) !void {
- const opts = EventInit{
- .bubbles = bubbles orelse false,
- .cancelable = cancelable orelse false,
- };
- return try parser.eventInit(self, eventType, opts);
- }
-
- pub fn _stopPropagation(self: *parser.Event) !void {
- return try parser.eventStopPropagation(self);
- }
-
- pub fn _stopImmediatePropagation(self: *parser.Event) !void {
- return try parser.eventStopImmediatePropagation(self);
- }
-
- pub fn _preventDefault(self: *parser.Event) !void {
- return try parser.eventPreventDefault(self);
- }
-};
-
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var common = [_]Case{
- .{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
- .{ .src = "let para = document.getElementById('para')", .ex = "undefined" },
- .{ .src = "var nb = 0; var evt", .ex = "undefined" },
- };
- try checkCases(js_env, &common);
-
- var basic = [_]Case{
- .{ .src =
- \\content.addEventListener('target',
- \\function(e) {
- \\evt = e; nb = nb + 1;
- \\e.preventDefault();
- \\})
- , .ex = "undefined" },
- .{ .src = "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", .ex = "false" },
- .{ .src = "nb", .ex = "1" },
- .{ .src = "evt.target === content", .ex = "true" },
- .{ .src = "evt.bubbles", .ex = "true" },
- .{ .src = "evt.cancelable", .ex = "true" },
- .{ .src = "evt.defaultPrevented", .ex = "true" },
- .{ .src = "evt.isTrusted", .ex = "true" },
- .{ .src = "evt.timestamp > 1704063600", .ex = "true" }, // 2024/01/01 00:00
- // event.type, event.currentTarget, event.phase checked in EventTarget
- };
- try checkCases(js_env, &basic);
-
- var stop = [_]Case{
- .{ .src = "nb = 0", .ex = "0" },
- .{ .src =
- \\content.addEventListener('stop',
- \\function(e) {
- \\e.stopPropagation();
- \\nb = nb + 1;
- \\}, true)
- , .ex = "undefined" },
- // the following event listener will not be invoked
- .{ .src =
- \\para.addEventListener('stop',
- \\function(e) {
- \\nb = nb + 1;
- \\})
- , .ex = "undefined" },
- .{ .src = "para.dispatchEvent(new Event('stop'))", .ex = "true" },
- .{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at content event listener
- };
- try checkCases(js_env, &stop);
-
- var stop_immediate = [_]Case{
- .{ .src = "nb = 0", .ex = "0" },
- .{ .src =
- \\content.addEventListener('immediate',
- \\function(e) {
- \\e.stopImmediatePropagation();
- \\nb = nb + 1;
- \\})
- , .ex = "undefined" },
- // the following event listener will not be invoked
- .{ .src =
- \\content.addEventListener('immediate',
- \\function(e) {
- \\nb = nb + 1;
- \\})
- , .ex = "undefined" },
- .{ .src = "content.dispatchEvent(new Event('immediate'))", .ex = "true" },
- .{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at first content event listener
- };
- try checkCases(js_env, &stop_immediate);
-
- var legacy = [_]Case{
- .{ .src = "nb = 0", .ex = "0" },
- .{ .src =
- \\content.addEventListener('legacy',
- \\function(e) {
- \\evt = e; nb = nb + 1;
- \\})
- , .ex = "undefined" },
- .{ .src = "let evtLegacy = document.createEvent('Event')", .ex = "undefined" },
- .{ .src = "evtLegacy.initEvent('legacy')", .ex = "undefined" },
- .{ .src = "content.dispatchEvent(evtLegacy)", .ex = "true" },
- .{ .src = "nb", .ex = "1" },
- };
- try checkCases(js_env, &legacy);
-
- var remove = [_]Case{
- .{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", .ex = "undefined" },
- .{ .src = "document.addEventListener('count', cbk)", .ex = "undefined" },
- .{ .src = "document.removeEventListener('count', cbk)", .ex = "undefined" },
- .{ .src = "document.dispatchEvent(new Event('count'))", .ex = "true" },
- .{ .src = "nb", .ex = "0" },
- };
- try checkCases(js_env, &remove);
-}
-
-pub const EventHandler = struct {
- fn handle(event: ?*parser.Event, data: parser.EventHandlerData) void {
- // TODO get the allocator by another way?
- var res = CallbackResult.init(data.cbk.nat_ctx.alloc);
- defer res.deinit();
-
- if (event) |evt| {
- data.cbk.trycall(.{
- Event.toInterface(evt) catch unreachable,
- }, &res) catch |e| log.err("event handler error: {any}", .{e});
- } else {
- data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error (null event): {any}", .{e});
- }
-
- // in case of function error, we log the result and the trace.
- if (!res.success) {
- log.info("event handler error try catch: {s}", .{res.result orelse "unknown"});
- log.debug("{s}", .{res.stack orelse "no stack trace"});
- }
- }
-}.handle;
diff --git a/src/html/location.zig b/src/html/location.zig
deleted file mode 100644
index 95a62dae..00000000
--- a/src/html/location.zig
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
-//
-// Francis Bouvier
-// Pierre Tachoire
-//
-// 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 .
-
-const std = @import("std");
-
-const builtin = @import("builtin");
-const jsruntime = @import("jsruntime");
-
-const URL = @import("../url/url.zig").URL;
-
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface
-pub const Location = struct {
- pub const mem_guarantied = true;
-
- url: ?URL = null,
-
- pub fn deinit(_: *Location, _: std.mem.Allocator) void {}
-
- pub fn get_href(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
- if (self.url) |*u| return u.get_href(alloc);
-
- return "";
- }
-
- pub fn get_protocol(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
- if (self.url) |*u| return u.get_protocol(alloc);
-
- return "";
- }
-
- pub fn get_host(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
- if (self.url) |*u| return u.get_host(alloc);
-
- return "";
- }
-
- pub fn get_hostname(self: *Location) []const u8 {
- if (self.url) |*u| return u.get_hostname();
-
- return "";
- }
-
- pub fn get_port(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
- if (self.url) |*u| return u.get_port(alloc);
-
- return "";
- }
-
- pub fn get_pathname(self: *Location) []const u8 {
- if (self.url) |*u| return u.get_pathname();
-
- return "";
- }
-
- pub fn get_search(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
- if (self.url) |*u| return u.get_search(alloc);
-
- return "";
- }
-
- pub fn get_hash(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
- if (self.url) |*u| return u.get_hash(alloc);
-
- return "";
- }
-
- pub fn get_origin(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
- if (self.url) |*u| return u.get_origin(alloc);
-
- return "";
- }
-
- // TODO
- pub fn _assign(_: *Location, url: []const u8) !void {
- _ = url;
- }
-
- // TODO
- pub fn _replace(_: *Location, url: []const u8) !void {
- _ = url;
- }
-
- // TODO
- pub fn _reload(_: *Location) !void {}
-
- pub fn _toString(self: *Location, alloc: std.mem.Allocator) ![]const u8 {
- return try self.get_href(alloc);
- }
-};
-
-// Tests
-// -----
-
-pub fn testExecFn(
- _: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- var location = [_]Case{
- .{ .src = "location.href", .ex = "https://lightpanda.io/opensource-browser/" },
- .{ .src = "document.location.href", .ex = "https://lightpanda.io/opensource-browser/" },
-
- .{ .src = "location.host", .ex = "lightpanda.io" },
- .{ .src = "location.hostname", .ex = "lightpanda.io" },
- .{ .src = "location.origin", .ex = "https://lightpanda.io" },
- .{ .src = "location.pathname", .ex = "/opensource-browser/" },
- .{ .src = "location.hash", .ex = "" },
- .{ .src = "location.port", .ex = "" },
- .{ .src = "location.search", .ex = "" },
- };
- try checkCases(js_env, &location);
-}
diff --git a/src/http/client.zig b/src/http/client.zig
index b6a4c4f4..936669cd 100644
--- a/src/http/client.zig
+++ b/src/http/client.zig
@@ -27,9 +27,8 @@ const MemoryPool = std.heap.MemoryPool;
const ArenaAllocator = std.heap.ArenaAllocator;
const tls = @import("tls");
-const jsruntime = @import("jsruntime");
-const IO = jsruntime.IO;
-const Loop = jsruntime.Loop;
+const IO = @import("../runtime/loop.zig").IO;
+const Loop = @import("../runtime/loop.zig").Loop;
const log = std.log.scoped(.http_client);
@@ -53,7 +52,7 @@ pub const Client = struct {
};
pub fn init(allocator: Allocator, max_concurrent: usize, opts: Opts) !Client {
- var root_ca = try tls.config.CertBundle.fromSystem(allocator);
+ var root_ca: tls.config.CertBundle = if (builtin.is_test) .{} else try tls.config.CertBundle.fromSystem(allocator);
errdefer root_ca.deinit(allocator);
const state_pool = try StatePool.init(allocator, max_concurrent);
@@ -69,7 +68,9 @@ pub const Client = struct {
pub fn deinit(self: *Client) void {
const allocator = self.allocator;
- self.root_ca.deinit(allocator);
+ if (builtin.is_test == false) {
+ self.root_ca.deinit(allocator);
+ }
self.state_pool.deinit(allocator);
}
@@ -1907,7 +1908,7 @@ test "HttpClient: sync GET redirect" {
}
test "HttpClient: async connect error" {
- var loop = try jsruntime.Loop.init(testing.allocator);
+ var loop = try Loop.init(testing.allocator);
defer loop.deinit();
const Handler = struct {
@@ -2193,7 +2194,7 @@ const TestResponse = struct {
};
const CaptureHandler = struct {
- loop: jsruntime.Loop,
+ loop: Loop,
reset: Thread.ResetEvent,
response: TestResponse,
@@ -2201,7 +2202,7 @@ const CaptureHandler = struct {
return .{
.reset = .{},
.response = TestResponse.init(),
- .loop = try jsruntime.Loop.init(testing.allocator),
+ .loop = try Loop.init(testing.allocator),
};
}
diff --git a/src/main.zig b/src/main.zig
index 80b5a8f6..2dfc339e 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -20,25 +20,19 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
-const jsruntime = @import("jsruntime");
-
-const App = @import("app.zig").App;
-const Browser = @import("browser/browser.zig").Browser;
const server = @import("server.zig");
+const App = @import("app.zig").App;
+const Platform = @import("runtime/js.zig").Platform;
+const Browser = @import("browser/browser.zig").Browser;
-const parser = @import("netsurf");
-const apiweb = @import("apiweb.zig");
-
-pub const Types = jsruntime.reflect(apiweb.Interfaces);
-pub const UserContext = apiweb.UserContext;
-pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
+const parser = @import("browser/netsurf.zig");
const version = @import("build_info").git_commit;
const log = std.log.scoped(.cli);
pub const std_options = std.Options{
// Set the log level to info
- .log_level = .debug,
+ .log_level = .info,
// Define logFn to override the std implementation
.logFn = logFn,
@@ -60,23 +54,34 @@ pub fn main() !void {
const args = try parseArgs(args_arena.allocator());
switch (args.mode) {
- .help => args.printUsageAndExit(args.mode.help),
+ .help => {
+ args.printUsageAndExit(args.mode.help);
+ return std.process.cleanExit();
+ },
.version => {
std.debug.print("{s}\n", .{version});
return std.process.cleanExit();
},
+ else => {},
+ }
+
+ const platform = Platform.init();
+ defer platform.deinit();
+
+ var app = try App.init(alloc, .{
+ .run_mode = args.mode,
+ .gc_hints = args.gcHints(),
+ .tls_verify_host = args.tlsVerifyHost(),
+ });
+ defer app.deinit();
+ app.telemetry.record(.{ .run = {} });
+
+ switch (args.mode) {
.serve => |opts| {
const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| {
log.err("address (host:port) {any}\n", .{err});
return args.printUsageAndExit(false);
};
- var app = try App.init(alloc, .{
- .run_mode = args.mode,
- .tls_verify_host = opts.tls_verify_host,
- });
- defer app.deinit();
-
- app.telemetry.record(.{ .run = {} });
const timeout = std.time.ns_per_s * @as(u64, opts.timeout);
server.run(app, address, timeout) catch |err| {
@@ -88,19 +93,8 @@ pub fn main() !void {
log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump });
const url = try @import("url.zig").URL.parse(opts.url, null);
- var app = try App.init(alloc, .{
- .run_mode = args.mode,
- .tls_verify_host = opts.tls_verify_host,
- });
- defer app.deinit();
- app.telemetry.record(.{ .run = {} });
-
- // vm
- const vm = jsruntime.VM.init();
- defer vm.deinit();
-
// browser
- var browser = Browser.init(app);
+ var browser = try Browser.init(app);
defer browser.deinit();
var session = try browser.newSession({});
@@ -126,6 +120,7 @@ pub fn main() !void {
try page.dump(std.io.getStdOut());
}
},
+ else => unreachable,
}
}
@@ -133,6 +128,21 @@ const Command = struct {
mode: Mode,
exec_name: []const u8,
+ fn gcHints(self: *const Command) bool {
+ return switch (self.mode) {
+ .serve => |opts| opts.gc_hints,
+ else => false,
+ };
+ }
+
+ fn tlsVerifyHost(self: *const Command) bool {
+ return switch (self.mode) {
+ .serve => |opts| opts.tls_verify_host,
+ .fetch => |opts| opts.tls_verify_host,
+ else => true,
+ };
+ }
+
const Mode = union(App.RunMode) {
help: bool, // false when being printed because of an error
fetch: Fetch,
@@ -144,6 +154,7 @@ const Command = struct {
host: []const u8,
port: u16,
timeout: u16,
+ gc_hints: bool,
tls_verify_host: bool,
};
@@ -187,6 +198,9 @@ const Command = struct {
\\--timeout Inactivity timeout in seconds before disconnecting clients
\\ Defaults to 3 (seconds)
\\
+ \\--gc_hints Encourage V8 to cleanup garbage for each new browser context.
+ \\ Defaults to false
+ \\
\\--insecure_disable_tls_host_verification
\\ Disables host verification on all HTTP requests.
\\ This is an advanced option which should only be
@@ -266,6 +280,11 @@ fn inferMode(opt: []const u8) ?App.RunMode {
if (std.mem.eql(u8, opt, "--timeout")) {
return .serve;
}
+
+ if (std.mem.eql(u8, opt, "--gc_hints")) {
+ return .serve;
+ }
+
return null;
}
@@ -276,6 +295,7 @@ fn parseServeArgs(
var host: []const u8 = "127.0.0.1";
var port: u16 = 9222;
var timeout: u16 = 3;
+ var gc_hints = false;
var tls_verify_host = true;
while (args.next()) |opt| {
@@ -319,6 +339,11 @@ fn parseServeArgs(
continue;
}
+ if (std.mem.eql(u8, "--gc_hints", opt)) {
+ gc_hints = true;
+ continue;
+ }
+
log.err("Unknown option to serve command: '{s}'", .{opt});
return error.UnkownOption;
}
@@ -327,6 +352,7 @@ fn parseServeArgs(
.host = host,
.port = port,
.timeout = timeout,
+ .gc_hints = gc_hints,
.tls_verify_host = tls_verify_host,
};
}
@@ -388,3 +414,196 @@ fn logFn(
// default std log function.
std.log.defaultLog(level, scope, format, args);
}
+
+test {
+ std.testing.refAllDecls(@This());
+}
+
+var test_wg: std.Thread.WaitGroup = .{};
+test "tests:beforeAll" {
+ try parser.init();
+ test_wg.startMany(3);
+ _ = Platform.init();
+
+ {
+ const address = try std.net.Address.parseIp("127.0.0.1", 9582);
+ const thread = try std.Thread.spawn(.{}, serveHTTP, .{address});
+ thread.detach();
+ }
+
+ {
+ const address = try std.net.Address.parseIp("127.0.0.1", 9581);
+ const thread = try std.Thread.spawn(.{}, serveHTTPS, .{address});
+ thread.detach();
+ }
+
+ {
+ const address = try std.net.Address.parseIp("127.0.0.1", 9583);
+ const thread = try std.Thread.spawn(.{}, serveCDP, .{address});
+ thread.detach();
+ }
+
+ // need to wait for the servers to be listening, else tests will fail because
+ // they aren't able to connect.
+ test_wg.wait();
+}
+
+test "tests:afterAll" {
+ parser.deinit();
+}
+
+fn serveHTTP(address: std.net.Address) !void {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ var listener = try address.listen(.{ .reuse_address = true });
+ defer listener.deinit();
+
+ test_wg.finish();
+
+ var read_buffer: [1024]u8 = undefined;
+ ACCEPT: while (true) {
+ defer _ = arena.reset(.{ .free_all = {} });
+ const aa = arena.allocator();
+
+ var conn = try listener.accept();
+ defer conn.stream.close();
+ var http_server = std.http.Server.init(conn, &read_buffer);
+
+ while (http_server.state == .ready) {
+ var request = http_server.receiveHead() catch |err| switch (err) {
+ error.HttpConnectionClosing => continue :ACCEPT,
+ else => {
+ std.debug.print("Test HTTP Server error: {}\n", .{err});
+ return err;
+ },
+ };
+
+ const path = request.head.target;
+ if (std.mem.eql(u8, path, "/loader")) {
+ try request.respond("Hello!", .{});
+ } else if (std.mem.eql(u8, path, "/http_client/simple")) {
+ try request.respond("", .{});
+ } else if (std.mem.eql(u8, path, "/http_client/redirect")) {
+ try request.respond("", .{
+ .status = .moved_permanently,
+ .extra_headers = &.{.{ .name = "LOCATION", .value = "../http_client/echo" }},
+ });
+ } else if (std.mem.eql(u8, path, "/http_client/redirect/secure")) {
+ try request.respond("", .{
+ .status = .moved_permanently,
+ .extra_headers = &.{.{ .name = "LOCATION", .value = "https://127.0.0.1:9581/http_client/body" }},
+ });
+ } else if (std.mem.eql(u8, path, "/http_client/echo")) {
+ var headers: std.ArrayListUnmanaged(std.http.Header) = .{};
+
+ var it = request.iterateHeaders();
+ while (it.next()) |hdr| {
+ try headers.append(aa, .{
+ .name = try std.fmt.allocPrint(aa, "_{s}", .{hdr.name}),
+ .value = hdr.value,
+ });
+ }
+
+ try request.respond("over 9000!", .{
+ .status = .created,
+ .extra_headers = headers.items,
+ });
+ }
+ }
+ }
+}
+
+// This is a lot of work for testing TLS, but the TLS (async) code is complicated
+// This "server" is written specifically to test the client. It assumes the client
+// isn't a jerk.
+fn serveHTTPS(address: std.net.Address) !void {
+ const tls = @import("tls");
+
+ var listener = try address.listen(.{ .reuse_address = true });
+ defer listener.deinit();
+
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+
+ test_wg.finish();
+
+ var seed: u64 = undefined;
+ std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable;
+ var r = std.Random.DefaultPrng.init(seed);
+ const rand = r.random();
+
+ var read_buffer: [1024]u8 = undefined;
+ while (true) {
+ // defer _ = arena.reset(.{ .retain_with_limit = 1024 });
+ // const aa = arena.allocator();
+
+ const stream = blk: {
+ const conn = try listener.accept();
+ break :blk conn.stream;
+ };
+ defer stream.close();
+
+ var conn = try tls.server(stream, .{ .auth = null });
+ defer conn.close() catch {};
+
+ var pos: usize = 0;
+ while (true) {
+ const n = try conn.read(read_buffer[pos..]);
+ if (n == 0) {
+ break;
+ }
+ pos += n;
+ const header_end = std.mem.indexOf(u8, read_buffer[0..pos], "\r\n\r\n") orelse {
+ continue;
+ };
+ var it = std.mem.splitScalar(u8, read_buffer[0..header_end], ' ');
+ _ = it.next() orelse unreachable; // method
+ const path = it.next() orelse unreachable;
+
+ var fragment = false;
+ var response: []const u8 = undefined;
+ if (std.mem.eql(u8, path, "/http_client/simple")) {
+ fragment = true;
+ response = "HTTP/1.1 200 \r\nContent-Length: 0\r\n\r\n";
+ } else if (std.mem.eql(u8, path, "/http_client/body")) {
+ fragment = true;
+ response = "HTTP/1.1 201 CREATED\r\nContent-Length: 20\r\n Another : HEaDer \r\n\r\n1234567890abcdefhijk";
+ } else if (std.mem.eql(u8, path, "/http_client/redirect/insecure")) {
+ fragment = true;
+ response = "HTTP/1.1 307 GOTO\r\nLocation: http://127.0.0.1:9582/http_client/redirect\r\n\r\n";
+ } else if (std.mem.eql(u8, path, "/xhr")) {
+ response = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 100\r\n\r\n" ++ ("1234567890" ** 10);
+ } else if (std.mem.eql(u8, path, "/xhr/json")) {
+ response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 18\r\n\r\n{\"over\":\"9000!!!\"}";
+ } else {
+ // should not have an unknown path
+ unreachable;
+ }
+
+ var unsent = response;
+ while (unsent.len > 0) {
+ const to_send = if (fragment) rand.intRangeAtMost(usize, 1, unsent.len) else unsent.len;
+ const sent = try conn.write(unsent[0..to_send]);
+ unsent = unsent[sent..];
+ std.time.sleep(std.time.ns_per_us * 5);
+ }
+ break;
+ }
+ }
+}
+
+fn serveCDP(address: std.net.Address) !void {
+ var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
+ var app = try App.init(gpa.allocator(), .{
+ .run_mode = .serve,
+ .tls_verify_host = false,
+ });
+ defer app.deinit();
+
+ test_wg.finish();
+ server.run(app, address, std.time.ns_per_s * 2) catch |err| {
+ std.debug.print("CDP server error: {}", .{err});
+ return err;
+ };
+}
diff --git a/src/main_tests.zig b/src/main_tests.zig
deleted file mode 100644
index 035e9d8f..00000000
--- a/src/main_tests.zig
+++ /dev/null
@@ -1,420 +0,0 @@
-// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
-//
-// Francis Bouvier
-// Pierre Tachoire
-//
-// 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 .
-
-const std = @import("std");
-const builtin = @import("builtin");
-
-const jsruntime = @import("jsruntime");
-const generate = @import("generate.zig");
-const pretty = @import("pretty");
-
-const parser = @import("netsurf");
-const apiweb = @import("apiweb.zig");
-const browser = @import("browser/browser.zig");
-const Window = @import("html/window.zig").Window;
-const xhr = @import("xhr/xhr.zig");
-const storage = @import("storage/storage.zig");
-const URL = @import("url.zig").URL;
-
-const documentTestExecFn = @import("dom/document.zig").testExecFn;
-const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn;
-const nodeTestExecFn = @import("dom/node.zig").testExecFn;
-const characterDataTestExecFn = @import("dom/character_data.zig").testExecFn;
-const textTestExecFn = @import("dom/text.zig").testExecFn;
-const elementTestExecFn = @import("dom/element.zig").testExecFn;
-const HTMLCollectionTestExecFn = @import("dom/html_collection.zig").testExecFn;
-const DOMExceptionTestExecFn = @import("dom/exceptions.zig").testExecFn;
-const DOMImplementationExecFn = @import("dom/implementation.zig").testExecFn;
-const NamedNodeMapExecFn = @import("dom/namednodemap.zig").testExecFn;
-const DOMTokenListExecFn = @import("dom/token_list.zig").testExecFn;
-const NodeListTestExecFn = @import("dom/nodelist.zig").testExecFn;
-const AttrTestExecFn = @import("dom/attribute.zig").testExecFn;
-const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn;
-const ProcessingInstructionTestExecFn = @import("dom/processing_instruction.zig").testExecFn;
-const CommentTestExecFn = @import("dom/comment.zig").testExecFn;
-const DocumentFragmentTestExecFn = @import("dom/document_fragment.zig").testExecFn;
-const EventTestExecFn = @import("events/event.zig").testExecFn;
-const XHRTestExecFn = xhr.testExecFn;
-const ProgressEventTestExecFn = @import("xhr/progress_event.zig").testExecFn;
-const StorageTestExecFn = storage.testExecFn;
-const URLTestExecFn = @import("url/url.zig").testExecFn;
-const HTMLElementTestExecFn = @import("html/elements.zig").testExecFn;
-const MutationObserverTestExecFn = @import("dom/mutation_observer.zig").testExecFn;
-
-pub const Types = jsruntime.reflect(apiweb.Interfaces);
-pub const UserContext = @import("user_context.zig").UserContext;
-pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
-
-var doc: *parser.DocumentHTML = undefined;
-
-fn testExecFn(
- alloc: std.mem.Allocator,
- js_env: *jsruntime.Env,
- comptime execFn: jsruntime.ContextExecFn,
-) anyerror!void {
- try parser.init();
- defer parser.deinit();
-
- // start JS env
- try js_env.start();
- defer js_env.stop();
-
- var storageShelf = storage.Shelf.init(alloc);
- defer storageShelf.deinit();
-
- // document
- const file = try std.fs.cwd().openFile("test.html", .{});
- defer file.close();
-
- doc = try parser.documentHTMLParse(file.reader(), "UTF-8");
- defer parser.documentHTMLClose(doc) catch |err| {
- std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
- };
-
- var http_client = try @import("http/client.zig").Client.init(alloc, 5, .{});
- defer http_client.deinit();
-
- // alias global as self and window
- var window = Window.create(null, null);
-
- const url = try URL.parse("https://lightpanda.io/opensource-browser/", null);
-
- var cookie_jar = storage.CookieJar.init(alloc);
- defer cookie_jar.deinit();
-
- var renderer = browser.Renderer.init(alloc);
- defer renderer.elements.deinit(alloc);
- defer renderer.positions.deinit(alloc);
-
- try js_env.setUserContext(.{
- .url = &url,
- .document = doc,
- .renderer = &renderer,
- .cookie_jar = &cookie_jar,
- .http_client = &http_client,
- });
-
- try window.replaceLocation(.{ .url = try url.toWebApi(alloc) });
-
- try window.replaceDocument(doc);
- window.setStorageShelf(&storageShelf);
-
- try js_env.bindGlobal(window);
-
- // run test
- try execFn(alloc, js_env);
-}
-
-fn testsAllExecFn(
- alloc: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- const testFns = [_]jsruntime.ContextExecFn{
- documentTestExecFn,
- HTMLDocumentTestExecFn,
- nodeTestExecFn,
- characterDataTestExecFn,
- textTestExecFn,
- elementTestExecFn,
- HTMLCollectionTestExecFn,
- DOMExceptionTestExecFn,
- DOMImplementationExecFn,
- NamedNodeMapExecFn,
- DOMTokenListExecFn,
- NodeListTestExecFn,
- AttrTestExecFn,
- CommentTestExecFn,
- DocumentFragmentTestExecFn,
- EventTargetTestExecFn,
- EventTestExecFn,
- XHRTestExecFn,
- ProgressEventTestExecFn,
- ProcessingInstructionTestExecFn,
- StorageTestExecFn,
- URLTestExecFn,
- HTMLElementTestExecFn,
- MutationObserverTestExecFn,
- @import("polyfill/fetch.zig").testExecFn,
- @import("html/navigator.zig").testExecFn,
- @import("html/history.zig").testExecFn,
- @import("html/location.zig").testExecFn,
- @import("xmlserializer/xmlserializer.zig").testExecFn,
- };
-
- inline for (testFns) |testFn| {
- try testExecFn(alloc, js_env, testFn);
- }
-}
-
-const usage =
- \\usage: test [options]
- \\ Run the tests. By default the command will run both js and unit tests.
- \\
- \\ -h, --help Print this help message and exit.
- \\ --browser run only browser js tests
- \\ --unit run only js unit tests
- \\ --json bench result is formatted in JSON.
- \\ only browser tests are benchmarked.
- \\
-;
-
-// Out list all the ouputs handled by benchmark result and written on stdout.
-const Out = enum {
- text,
- json,
-};
-
-// Which tests must be run.
-const Run = enum {
- all,
- browser,
- unit,
-};
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = gpa.deinit();
- const gpa_alloc = gpa.allocator();
-
- var args = try std.process.argsWithAllocator(gpa_alloc);
- defer args.deinit();
-
- // ignore the exec name.
- _ = args.next().?;
-
- var out: Out = .text;
- var run: Run = .all;
-
- while (args.next()) |arg| {
- if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
- try std.io.getStdErr().writer().print(usage, .{});
- std.posix.exit(0);
- }
- if (std.mem.eql(u8, "--json", arg)) {
- out = .json;
- continue;
- }
- if (std.mem.eql(u8, "--browser", arg)) {
- run = .browser;
- continue;
- }
- if (std.mem.eql(u8, "--unit", arg)) {
- run = .unit;
- continue;
- }
- }
-
- // run js tests
- if (run == .all or run == .browser) try run_js(out);
-
- // run standard unit tests.
- if (run == .all or run == .unit) {
- std.debug.print("\n", .{});
- for (builtin.test_functions) |test_fn| {
- if (std.mem.startsWith(u8, test_fn.name, "http.client.test")) {
- // covered by unit test, needs a dummy server started, which
- // main_test doesn't do.
- continue;
- }
-
- try parser.init();
- defer parser.deinit();
-
- std.testing.allocator_instance = .{};
- try test_fn.func();
-
- if (std.testing.allocator_instance.deinit() == .leak) {
- std.debug.print("======Memory Leak: {s}======\n", .{test_fn.name});
- } else {
- std.debug.print("{s}\tOK\n", .{test_fn.name});
- }
- }
- }
-}
-
-// Run js test and display the output depending of the output parameter.
-fn run_js(out: Out) !void {
- var bench_alloc = jsruntime.bench_allocator(std.testing.allocator);
-
- const start = try std.time.Instant.now();
-
- // run js exectuion tests
- try testJSRuntime(bench_alloc.allocator());
-
- const duration = std.time.Instant.since(try std.time.Instant.now(), start);
- const stats = bench_alloc.stats();
-
- // get and display the results
- if (out == .json) {
- const res = [_]struct {
- name: []const u8,
- bench: struct {
- duration: u64,
-
- alloc_nb: usize,
- realloc_nb: usize,
- alloc_size: usize,
- },
- }{
- .{ .name = "browser", .bench = .{
- .duration = duration,
- .alloc_nb = stats.alloc_nb,
- .realloc_nb = stats.realloc_nb,
- .alloc_size = stats.alloc_size,
- } },
- // TODO get libdom bench info.
- .{ .name = "libdom", .bench = .{
- .duration = duration,
- .alloc_nb = 0,
- .realloc_nb = 0,
- .alloc_size = 0,
- } },
- // TODO get v8 bench info.
- .{ .name = "v8", .bench = .{
- .duration = duration,
- .alloc_nb = 0,
- .realloc_nb = 0,
- .alloc_size = 0,
- } },
- // TODO get main bench info.
- .{ .name = "main", .bench = .{
- .duration = duration,
- .alloc_nb = 0,
- .realloc_nb = 0,
- .alloc_size = 0,
- } },
- };
-
- try std.json.stringify(res, .{ .whitespace = .indent_2 }, std.io.getStdOut().writer());
- return;
- }
-
- // display console result by default
- const dur = pretty.Measure{ .unit = "ms", .value = duration / ms };
- const size = pretty.Measure{ .unit = "kb", .value = stats.alloc_size / kb };
-
- const zerosize = pretty.Measure{ .unit = "kb", .value = 0 };
-
- // benchmark table
- const row_shape = .{ []const u8, pretty.Measure, u64, u64, pretty.Measure };
- const table = try pretty.GenerateTable(4, row_shape, pretty.TableConf{ .margin_left = " " });
- const header = .{ "FUNCTION", "DURATION", "ALLOCATIONS (nb)", "RE-ALLOCATIONS (nb)", "HEAP SIZE" };
- var t = table.init("Benchmark lightpanda π", header);
- try t.addRow(.{ "browser", dur, stats.alloc_nb, stats.realloc_nb, size });
- try t.addRow(.{ "libdom", dur, 0, 0, zerosize }); // TODO get libdom bench info.
- try t.addRow(.{ "v8", dur, 0, 0, zerosize }); // TODO get v8 bench info.
- try t.addRow(.{ "main", dur, 0, 0, zerosize }); // TODO get main bench info.
- try t.render(std.io.getStdOut().writer());
-}
-
-const kb = 1024;
-const ms = std.time.ns_per_ms;
-
-test {
- const dumpTest = @import("browser/dump.zig");
- std.testing.refAllDecls(dumpTest);
-
- const mimeTest = @import("browser/mime.zig");
- std.testing.refAllDecls(mimeTest);
-
- const cssTest = @import("css/css.zig");
- std.testing.refAllDecls(cssTest);
-
- const cssParserTest = @import("css/parser.zig");
- std.testing.refAllDecls(cssParserTest);
-
- const cssMatchTest = @import("css/match_test.zig");
- std.testing.refAllDecls(cssMatchTest);
-
- const cssLibdomTest = @import("css/libdom_test.zig");
- std.testing.refAllDecls(cssLibdomTest);
-
- const queryTest = @import("url/query.zig");
- std.testing.refAllDecls(queryTest);
-
- std.testing.refAllDecls(@import("generate.zig"));
-}
-
-fn testJSRuntime(alloc: std.mem.Allocator) !void {
- // create JS vm
- const vm = jsruntime.VM.init();
- defer vm.deinit();
-
- var arena_alloc = std.heap.ArenaAllocator.init(alloc);
- defer arena_alloc.deinit();
-
- try jsruntime.loadEnv(&arena_alloc, null, testsAllExecFn);
-}
-
-test "DocumentHTMLParseFromStr" {
- const file = try std.fs.cwd().openFile("test.html", .{});
- defer file.close();
-
- const str = try file.readToEndAlloc(std.testing.allocator, std.math.maxInt(u32));
- defer std.testing.allocator.free(str);
-
- doc = try parser.documentHTMLParseFromStr(str);
- parser.documentHTMLClose(doc) catch {};
-}
-
-// https://github.com/lightpanda-io/libdom/issues/4
-test "bug document html parsing #4" {
- const file = try std.fs.cwd().openFile("tests/html/bug-html-parsing-4.html", .{});
- defer file.close();
-
- doc = try parser.documentHTMLParse(file.reader(), "UTF-8");
- parser.documentHTMLClose(doc) catch {};
-}
-
-test "Window is a libdom event target" {
- var window = Window.create(null, null);
-
- const event = try parser.eventCreate();
- try parser.eventInit(event, "foo", .{});
-
- const et = parser.toEventTarget(Window, &window);
- _ = try parser.eventTargetDispatchEvent(et, event);
-}
-
-test "DocumentHTML is a libdom event target" {
- doc = try parser.documentHTMLParseFromStr("");
- parser.documentHTMLClose(doc) catch {};
-
- const event = try parser.eventCreate();
- try parser.eventInit(event, "foo", .{});
-
- const et = parser.toEventTarget(parser.DocumentHTML, doc);
- _ = try parser.eventTargetDispatchEvent(et, event);
-}
-
-test "XMLHttpRequest.validMethod" {
- // valid methods
- for ([_][]const u8{ "get", "GET", "head", "HEAD" }) |tc| {
- _ = try xhr.XMLHttpRequest.validMethod(tc);
- }
-
- // forbidden
- for ([_][]const u8{ "connect", "CONNECT" }) |tc| {
- try std.testing.expectError(parser.DOMError.Security, xhr.XMLHttpRequest.validMethod(tc));
- }
-
- // syntax
- for ([_][]const u8{ "foo", "BAR" }) |tc| {
- try std.testing.expectError(parser.DOMError.Syntax, xhr.XMLHttpRequest.validMethod(tc));
- }
-}
diff --git a/src/main_unit_tests.zig b/src/main_unit_tests.zig
deleted file mode 100644
index 23c2f9c6..00000000
--- a/src/main_unit_tests.zig
+++ /dev/null
@@ -1,224 +0,0 @@
-// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
-//
-// Francis Bouvier
-// Pierre Tachoire
-//
-// 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 .
-
-const std = @import("std");
-const tls = @import("tls");
-const parser = @import("netsurf");
-const builtin = @import("builtin");
-
-const Allocator = std.mem.Allocator;
-
-test {
- std.testing.refAllDecls(@import("url/query.zig"));
- std.testing.refAllDecls(@import("browser/dump.zig"));
- std.testing.refAllDecls(@import("browser/mime.zig"));
- std.testing.refAllDecls(@import("css/css.zig"));
- std.testing.refAllDecls(@import("css/libdom_test.zig"));
- std.testing.refAllDecls(@import("css/match_test.zig"));
- std.testing.refAllDecls(@import("css/parser.zig"));
- std.testing.refAllDecls(@import("generate.zig"));
- std.testing.refAllDecls(@import("storage/storage.zig"));
- std.testing.refAllDecls(@import("storage/cookie.zig"));
- std.testing.refAllDecls(@import("iterator/iterator.zig"));
- std.testing.refAllDecls(@import("server.zig"));
- std.testing.refAllDecls(@import("cdp/cdp.zig"));
- std.testing.refAllDecls(@import("log.zig"));
- std.testing.refAllDecls(@import("datetime.zig"));
- std.testing.refAllDecls(@import("telemetry/telemetry.zig"));
- std.testing.refAllDecls(@import("http/client.zig"));
-}
-
-var wg: std.Thread.WaitGroup = .{};
-var gpa = std.heap.GeneralPurposeAllocator(.{}){};
-test "tests:beforeAll" {
- try parser.init();
- wg.startMany(3);
-
- {
- const address = try std.net.Address.parseIp("127.0.0.1", 9582);
- const thread = try std.Thread.spawn(.{}, serveHTTP, .{address});
- thread.detach();
- }
-
- {
- const address = try std.net.Address.parseIp("127.0.0.1", 9581);
- const thread = try std.Thread.spawn(.{}, serveHTTPS, .{address});
- thread.detach();
- }
-
- {
- const address = try std.net.Address.parseIp("127.0.0.1", 9583);
- const thread = try std.Thread.spawn(.{}, serveCDP, .{address});
- thread.detach();
- }
-
- // need to wait for the servers to be listening, else tests will fail because
- // they aren't able to connect.
- wg.wait();
-}
-
-test "tests:afterAll" {
- parser.deinit();
-}
-
-fn serveHTTP(address: std.net.Address) !void {
- const allocator = gpa.allocator();
- var arena = std.heap.ArenaAllocator.init(allocator);
- defer arena.deinit();
-
- var listener = try address.listen(.{ .reuse_address = true });
- defer listener.deinit();
-
- wg.finish();
-
- var read_buffer: [1024]u8 = undefined;
- ACCEPT: while (true) {
- defer _ = arena.reset(.{ .retain_with_limit = 1024 });
- const aa = arena.allocator();
-
- var conn = try listener.accept();
- defer conn.stream.close();
- var server = std.http.Server.init(conn, &read_buffer);
-
- while (server.state == .ready) {
- var request = server.receiveHead() catch |err| switch (err) {
- error.HttpConnectionClosing => continue :ACCEPT,
- else => {
- std.debug.print("Test HTTP Server error: {}\n", .{err});
- return err;
- },
- };
-
- const path = request.head.target;
- if (std.mem.eql(u8, path, "/loader")) {
- try request.respond("Hello!", .{});
- } else if (std.mem.eql(u8, path, "/http_client/simple")) {
- try request.respond("", .{});
- } else if (std.mem.eql(u8, path, "/http_client/redirect")) {
- try request.respond("", .{
- .status = .moved_permanently,
- .extra_headers = &.{.{ .name = "LOCATION", .value = "../http_client/echo" }},
- });
- } else if (std.mem.eql(u8, path, "/http_client/redirect/secure")) {
- try request.respond("", .{
- .status = .moved_permanently,
- .extra_headers = &.{.{ .name = "LOCATION", .value = "https://127.0.0.1:9581/http_client/body" }},
- });
- } else if (std.mem.eql(u8, path, "/http_client/echo")) {
- var headers: std.ArrayListUnmanaged(std.http.Header) = .{};
-
- var it = request.iterateHeaders();
- while (it.next()) |hdr| {
- try headers.append(aa, .{
- .name = try std.fmt.allocPrint(aa, "_{s}", .{hdr.name}),
- .value = hdr.value,
- });
- }
-
- try request.respond("over 9000!", .{
- .status = .created,
- .extra_headers = headers.items,
- });
- }
- }
- }
-}
-
-// This is a lot of work for testing TLS, but the TLS (async) code is complicated
-// This "server" is written specifically to test the client. It assumes the client
-// isn't a jerk.
-fn serveHTTPS(address: std.net.Address) !void {
- const allocator = gpa.allocator();
-
- var listener = try address.listen(.{ .reuse_address = true });
- defer listener.deinit();
-
- var arena = std.heap.ArenaAllocator.init(allocator);
- defer arena.deinit();
-
- wg.finish();
-
- var seed: u64 = undefined;
- std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable;
- var r = std.Random.DefaultPrng.init(seed);
- const rand = r.random();
-
- var read_buffer: [1024]u8 = undefined;
- while (true) {
- // defer _ = arena.reset(.{ .retain_with_limit = 1024 });
- // const aa = arena.allocator();
-
- const stream = blk: {
- const conn = try listener.accept();
- break :blk conn.stream;
- };
- defer stream.close();
-
- var conn = try tls.server(stream, .{ .auth = null });
- defer conn.close() catch {};
-
- var pos: usize = 0;
- while (true) {
- const n = try conn.read(read_buffer[pos..]);
- if (n == 0) {
- break;
- }
- pos += n;
- const header_end = std.mem.indexOf(u8, read_buffer[0..pos], "\r\n\r\n") orelse {
- continue;
- };
- var it = std.mem.splitScalar(u8, read_buffer[0..header_end], ' ');
- _ = it.next() orelse unreachable; // method
- const path = it.next() orelse unreachable;
-
- var response: []const u8 = undefined;
- if (std.mem.eql(u8, path, "/http_client/simple")) {
- response = "HTTP/1.1 200 \r\nContent-Length: 0\r\n\r\n";
- } else if (std.mem.eql(u8, path, "/http_client/body")) {
- response = "HTTP/1.1 201 CREATED\r\nContent-Length: 20\r\n Another : HEaDer \r\n\r\n1234567890abcdefhijk";
- } else if (std.mem.eql(u8, path, "/http_client/redirect/insecure")) {
- response = "HTTP/1.1 307 GOTO\r\nLocation: http://127.0.0.1:9582/http_client/redirect\r\n\r\n";
- } else {
- // should not have an unknown path
- unreachable;
- }
-
- var unsent = response;
- while (unsent.len > 0) {
- const to_send = rand.intRangeAtMost(usize, 1, unsent.len);
- const sent = try conn.write(unsent[0..to_send]);
- unsent = unsent[sent..];
- std.time.sleep(std.time.ns_per_us * 5);
- }
- break;
- }
- }
-}
-
-fn serveCDP(address: std.net.Address) !void {
- const App = @import("app.zig").App;
- var app = try App.init(gpa.allocator(), .{ .run_mode = .serve });
- defer app.deinit();
-
- const server = @import("server.zig");
- wg.finish();
- server.run(app, address, std.time.ns_per_s * 2) catch |err| {
- std.debug.print("CDP server error: {}", .{err});
- return err;
- };
-}
diff --git a/src/main_wpt.zig b/src/main_wpt.zig
index 7cf2f077..eeabaf12 100644
--- a/src/main_wpt.zig
+++ b/src/main_wpt.zig
@@ -18,14 +18,10 @@
const std = @import("std");
-const jsruntime = @import("jsruntime");
-
-const Suite = @import("wpt/testcase.zig").Suite;
-const FileLoader = @import("wpt/fileloader.zig").FileLoader;
const wpt = @import("wpt/run.zig");
-
-const apiweb = @import("apiweb.zig");
-const HTMLElem = @import("html/elements.zig");
+const Suite = @import("wpt/testcase.zig").Suite;
+const Platform = @import("runtime/js.zig").Platform;
+const FileLoader = @import("wpt/fileloader.zig").FileLoader;
const wpt_dir = "tests/wpt";
@@ -47,10 +43,10 @@ const Out = enum {
text,
};
-pub const Types = jsruntime.reflect(apiweb.Interfaces);
-pub const GlobalType = apiweb.GlobalType;
-pub const UserContext = apiweb.UserContext;
-pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
+pub const std_options = std.Options{
+ // Set the log level to info
+ .log_level = .info,
+};
// TODO For now the WPT tests run is specific to WPT.
// It manually load js framwork libs, and run the first script w/ js content in
@@ -122,8 +118,8 @@ pub fn main() !void {
}
// initialize VM JS lib.
- const vm = jsruntime.VM.init();
- defer vm.deinit();
+ const platform = Platform.init();
+ defer platform.deinit();
// prepare libraries to load on each test case.
var loader = FileLoader.init(alloc, wpt_dir);
@@ -142,8 +138,9 @@ pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
- const res = wpt.run(&arena, wpt_dir, tc, &loader) catch |err| {
- const suite = try Suite.init(alloc, tc, false, @errorName(err));
+ var msg_out: ?[]const u8 = null;
+ const res = wpt.run(arena.allocator(), wpt_dir, tc, &loader, &msg_out) catch |err| {
+ const suite = try Suite.init(alloc, tc, false, if (msg_out) |msg| msg else @errorName(err));
try results.append(suite);
if (out == .text) {
@@ -152,9 +149,8 @@ pub fn main() !void {
failures += 1;
continue;
};
- defer res.deinit(arena.allocator());
- const suite = try Suite.init(alloc, tc, res.ok, res.msg orelse "");
+ const suite = try Suite.init(alloc, tc, true, res);
try results.append(suite);
if (out == .json) {
diff --git a/src/polyfill/fetch.zig b/src/polyfill/fetch.zig
deleted file mode 100644
index 5cf8b99d..00000000
--- a/src/polyfill/fetch.zig
+++ /dev/null
@@ -1,55 +0,0 @@
-const std = @import("std");
-const jsruntime = @import("jsruntime");
-const Case = jsruntime.test_utils.Case;
-const checkCases = jsruntime.test_utils.checkCases;
-
-// fetch.js code comes from
-// https://github.com/JakeChampion/fetch/blob/main/fetch.js
-//
-// The original code source is available in MIT license.
-//
-// The script comes from the built version from npm.
-// You can get the package with the command:
-//
-// wget $(npm view whatwg-fetch dist.tarball)
-//
-// The source is the content of `package/dist/fetch.umd.js` file.
-pub const source = @embedFile("fetch.js");
-
-pub fn testExecFn(
- alloc: std.mem.Allocator,
- js_env: *jsruntime.Env,
-) anyerror!void {
- try @import("polyfill.zig").load(alloc, js_env);
-
- var fetch = [_]Case{
- .{
- .src =
- \\var ok = false;
- \\const request = new Request("https://httpbin.io/json");
- \\fetch(request)
- \\ .then((response) => { ok = response.ok; });
- \\false;
- ,
- .ex = "false",
- },
- // all events have been resolved.
- .{ .src = "ok", .ex = "true" },
- };
- try checkCases(js_env, &fetch);
-
- var fetch2 = [_]Case{
- .{
- .src =
- \\var ok2 = false;
- \\const request2 = new Request("https://httpbin.io/json");
- \\(async function () { resp = await fetch(request2); ok2 = resp.ok; }());
- \\false;
- ,
- .ex = "false",
- },
- // all events have been resolved.
- .{ .src = "ok2", .ex = "true" },
- };
- try checkCases(js_env, &fetch2);
-}
diff --git a/src/generate.zig b/src/runtime/generate.zig
similarity index 96%
rename from src/generate.zig
rename to src/runtime/generate.zig
index 9b5c164e..1e1c5193 100644
--- a/src/generate.zig
+++ b/src/runtime/generate.zig
@@ -25,7 +25,7 @@ const Type = std.builtin.Type;
// -----
// Generate a flatten tagged Union from a Tuple
-pub fn Union(interfaces: anytype) type {
+pub fn Union(comptime interfaces: anytype) type {
// @setEvalBranchQuota(10000);
const tuple = Tuple(interfaces){};
const fields = std.meta.fields(@TypeOf(tuple));
@@ -93,7 +93,7 @@ pub fn Union(interfaces: anytype) type {
// Flattens and depuplicates a list of nested tuples. For example
// input: {A, B, {C, B, D}, {A, E}}
// output {A, B, C, D, E}
-pub fn Tuple(args: anytype) type {
+pub fn Tuple(comptime args: anytype) type {
@setEvalBranchQuota(100000);
const count = countInterfaces(args, 0);
@@ -188,7 +188,7 @@ test "generate.Union" {
const value = Union(.{ Astruct, Bstruct, .{Cstruct} });
const ti = @typeInfo(value).@"union";
try std.testing.expectEqual(3, ti.fields.len);
- try std.testing.expectEqualStrings("*generate.test.generate.Union.Astruct.Other", @typeName(ti.fields[0].type));
+ try std.testing.expectEqualStrings("*runtime.generate.test.generate.Union.Astruct.Other", @typeName(ti.fields[0].type));
try std.testing.expectEqualStrings(ti.fields[0].name, "Astruct");
try std.testing.expectEqual(Bstruct, ti.fields[1].type);
try std.testing.expectEqualStrings(ti.fields[1].name, "Bstruct");
diff --git a/src/runtime/js.zig b/src/runtime/js.zig
new file mode 100644
index 00000000..329a8728
--- /dev/null
+++ b/src/runtime/js.zig
@@ -0,0 +1,2303 @@
+// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+const builtin = @import("builtin");
+const v8 = @import("v8");
+
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+
+const log = std.log.scoped(.js);
+
+// Global, should only be initialized once.
+pub const Platform = struct {
+ inner: v8.Platform,
+
+ pub fn init() Platform {
+ 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();
+ }
+};
+
+// 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 Executors, which actually execute JavaScript.
+// The `S` parameter is arbitrary state. When we start an Executor, 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.
+pub fn Env(comptime S: type, comptime types: anytype) type {
+ const Types = @typeInfo(@TypeOf(types)).@"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`? `TypeLookup` is a struct
+ // that looks like:
+ //
+ // const TypeLookup = 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));
+ //
+ const TypeLookup = comptime 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
+ // TypeLookup. 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 = @field(types, s.name);
+ 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) }));
+ }
+
+ const R = Receiver(@field(types, s.name));
+ fields[i] = .{
+ .name = @typeName(R),
+ .type = usize,
+ .is_comptime = true,
+ .alignment = @alignOf(usize),
+ .default_value_ptr = @ptrCast(&i),
+ };
+ }
+ break :blk @Type(.{ .@"struct" = .{
+ .layout = .auto,
+ .decls = &.{},
+ .is_tuple = false,
+ .fields = &fields,
+ } });
+ };
+
+ // 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.
+ const PROTOTYPE_TABLE = comptime blk: {
+ var table: [Types.len]u16 = undefined;
+ const TYPE_LOOKUP = TypeLookup{};
+ for (Types, 0..) |s, i| {
+ var prototype_index = i;
+ const Struct = @field(types, s.name);
+ if (@hasDecl(Struct, "prototype")) {
+ const TI = @typeInfo(Struct.prototype);
+ const proto_name = @typeName(Receiver(TI.pointer.child));
+ prototype_index = @field(TYPE_LOOKUP, proto_name);
+ }
+ table[i] = prototype_index;
+ }
+ break :blk table;
+ };
+
+ return struct {
+ allocator: Allocator,
+
+ // the global isolate
+ isolate: v8.Isolate,
+
+ // this is the global scope that all our classes are defined in
+ global_scope: v8.HandleScope,
+
+ // 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,
+
+ // Sessions are cheap, we mostly do this so we can get a stable pointer
+ executor_pool: std.heap.MemoryPool(Executor),
+
+ // Send a lowMemoryNotification whenever we stop an executor
+ gc_hints: bool,
+
+ const Self = @This();
+
+ const State = S;
+ const TYPE_LOOKUP = TypeLookup{};
+
+ const Opts = struct {
+ gc_hints: bool = false,
+ };
+
+ pub fn init(allocator: Allocator, opts: Opts) !*Self {
+ var params = v8.initCreateParams();
+ params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
+ errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
+
+ var isolate = v8.Isolate.init(¶ms);
+ errdefer isolate.deinit();
+
+ isolate.enter();
+ errdefer isolate.exit();
+
+ var global_scope: v8.HandleScope = undefined;
+ v8.HandleScope.init(&global_scope, isolate);
+ errdefer global_scope.deinit();
+
+ const env = try allocator.create(Self);
+ errdefer allocator.destroy(env);
+
+ env.* = .{
+ .isolate = isolate,
+ .templates = undefined,
+ .allocator = allocator,
+ .isolate_params = params,
+ .gc_hints = opts.gc_hints,
+ .global_scope = global_scope,
+ .prototype_lookup = undefined,
+ .executor_pool = std.heap.MemoryPool(Executor).init(allocator),
+ };
+
+ // 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(TYPE_LOOKUP, type_name)
+ const templates = &env.templates;
+ inline for (Types, 0..) |s, i| {
+ templates[i] = env.generateClass(@field(types, s.name));
+ }
+
+ // Above, we've created all our our FunctionTemplates. Now that we
+ // have them all, we can hook up the prototypes.
+ inline for (Types, 0..) |s, i| {
+ const Struct = @field(types, s.name);
+ if (@hasDecl(Struct, "prototype")) {
+ const TI = @typeInfo(Struct.prototype);
+ const proto_name = @typeName(Receiver(TI.pointer.child));
+ if (@hasField(TypeLookup, 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 TYPE_LOOKUP.
+ // Just like we said above, given a type, we can get its
+ // template index.
+
+ const proto_index = @field(TYPE_LOOKUP, proto_name);
+ templates[i].inherit(templates[proto_index]);
+ }
+ }
+
+ return env;
+ }
+
+ pub fn deinit(self: *Self) void {
+ self.global_scope.deinit();
+ self.isolate.exit();
+ self.isolate.deinit();
+ self.executor_pool.deinit();
+ v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
+ self.allocator.destroy(self);
+ }
+
+ pub fn runMicrotasks(self: *const Self) void {
+ self.isolate.performMicrotasksCheckpoint();
+ }
+
+ pub fn startExecutor(self: *Self, comptime Global: type, state: State, module_loader: anytype) !*Executor {
+ const isolate = self.isolate;
+ const templates = &self.templates;
+
+ // Acts like an Arena. Most things V8 has to allocate from this point
+ // on will be tied to this handle_scope - which we deinit in
+ // stopExecutor
+ var handle_scope: v8.HandleScope = undefined;
+ v8.HandleScope.init(&handle_scope, isolate);
+
+ // The global FunctionTemplate (i.e. Window).
+ const globals = v8.FunctionTemplate.initDefault(isolate);
+
+ const global_template = globals.getInstanceTemplate();
+ global_template.setInternalFieldCount(1);
+ self.attachClass(Global, globals);
+
+ // 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 = @field(types, s.name);
+ const class_name = v8.String.initUtf8(isolate, comptime 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
+ // 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 = Receiver(@typeInfo(Global.prototype).pointer.child);
+ const proto_name = @typeName(proto_type);
+ const proto_index = @field(TYPE_LOOKUP, proto_name);
+ globals.inherit(templates[proto_index]);
+ }
+
+ const context = v8.Context.init(isolate, global_template, null);
+ context.enter();
+ errdefer 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 = @field(types, s.name);
+
+ if (@hasDecl(Struct, "prototype")) {
+ const proto_type = Receiver(@typeInfo(Struct.prototype).pointer.child);
+ const proto_name = @typeName(proto_type);
+ if (@hasField(TypeLookup, proto_name) == false) {
+ @compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name);
+ }
+
+ const proto_index = @field(TYPE_LOOKUP, proto_name);
+ const proto_obj = templates[proto_index].getFunction(context).toObject();
+
+ const self_obj = templates[i].getFunction(context).toObject();
+ _ = self_obj.setPrototype(context, proto_obj);
+ }
+ }
+
+ const executor = try self.executor_pool.create();
+ errdefer self.executor_pool.destroy(executor);
+
+ {
+ // Given a context, we can get our executor.
+ // (we store a pointer to our executor in the context's
+ // embeddeder data)
+ const data = isolate.initBigIntU64(@intCast(@intFromPtr(executor)));
+ context.setEmbedderData(1, data);
+ }
+
+ const allocator = self.allocator;
+
+ executor.* = .{
+ .state = state,
+ .context = context,
+ .isolate = isolate,
+ .templates = templates,
+ .handle_scope = handle_scope,
+ .call_arena = ArenaAllocator.init(allocator),
+ .scope_arena = ArenaAllocator.init(allocator),
+ .module_loader = .{
+ .ptr = @ptrCast(module_loader),
+ .func = @TypeOf(module_loader.*).fetchModuleSource,
+ },
+ };
+
+ errdefer self.stopExecutor(executor);
+
+ // 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 = @field(types, s.name);
+ if (@hasDecl(Struct, "ErrorSet")) {
+ const script = comptime classNameForStruct(Struct) ++ ".prototype.__proto__ = Error.prototype";
+ _ = try executor.exec(script, "errorSubclass");
+ }
+ }
+
+ return executor;
+ }
+
+ // In startExecutor we started a V8.Context. Here, we're going to
+ // deinit it. But V8 doesn't immediately free memory associated with
+ // a Context, it's managed by the garbage collector. So, when the
+ // `gc_hints` option is enabled, we'll use the `lowMemoryNotification`
+ // call on the isolate to encourage v8 to free the context.
+ pub fn stopExecutor(self: *Self, executor: *Executor) void {
+ executor.deinit();
+ self.executor_pool.destroy(executor);
+ if (self.gc_hints) {
+ self.isolate.lowMemoryNotification();
+ }
+ }
+
+ // 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(self: *Self, comptime Struct: type) v8.FunctionTemplate {
+ const template = self.generateConstructor(Struct);
+ self.attachClass(Struct, 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 startExecutor)
+ fn attachClass(self: *Self, comptime Struct: type, 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" => self.generateMethod(Struct, name, template_proto),
+ else => self.generateAttribute(Struct, name, template, template_proto),
+ }
+ } else if (comptime std.mem.startsWith(u8, name, "get_")) {
+ self.generateProperty(Struct, name[4..], template_proto);
+ }
+ }
+
+ 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 key = v8.Symbol.getToStringTag(self.isolate).toName();
+ template_proto.setGetter(key, struct {
+ fn stringTag(_: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void {
+ const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
+ const class_name = v8.String.initUtf8(info.getIsolate(), comptime classNameForStruct(Struct));
+ info.getReturnValue().set(class_name);
+ }
+ }.stringTag);
+ }
+
+ self.generateIndexer(Struct, template_proto);
+ self.generateNamedIndexer(Struct, template_proto);
+ }
+
+ // 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(self: *Self, comptime Struct: type) v8.FunctionTemplate {
+ const template = v8.FunctionTemplate.initCallback(self.isolate, struct {
+ fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
+ const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
+ var caller = Caller(Self).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 isolate = caller.isolate;
+ const js_exception = isolate.throwException(createException(isolate, "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 = NamedFunction(Struct, Struct.constructor, "constructor"){};
+ caller.constructor(named_function, info) catch |err| {
+ caller.handleError(named_function, err, info);
+ };
+ }
+ }.callback);
+
+ if (comptime isEmpty(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(self.isolate, comptime classNameForStruct(Struct));
+ template.setClassName(class_name);
+ return template;
+ }
+
+ fn generateMethod(self: *Self, comptime Struct: type, comptime name: []const u8, 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(self.isolate).toName();
+ } else {
+ js_name = v8.String.initUtf8(self.isolate, name[1..]).toName();
+ }
+ const function_template = v8.FunctionTemplate.initCallback(self.isolate, struct {
+ fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
+ const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
+ var caller = Caller(Self).init(info);
+ defer caller.deinit();
+
+ const named_function = NamedFunction(Struct, @field(Struct, name), name){};
+ caller.method(named_function, info) catch |err| {
+ caller.handleError(named_function, err, info);
+ };
+ }
+ }.callback);
+ template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
+ }
+
+ fn generateAttribute(self: *Self, comptime Struct: type, comptime name: []const u8, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void {
+ const zig_value = @field(Struct, name);
+ const js_value = simpleZigValueToJs(self.isolate, zig_value, true);
+
+ const js_name = v8.String.initUtf8(self.isolate, name[1..]).toName();
+
+ // apply it both to the type itself
+ template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
+
+ // andto instances of the type
+ template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
+ }
+
+ fn generateProperty(self: *Self, comptime Struct: type, comptime name: []const u8, template_proto: v8.ObjectTemplate) void {
+ const getter = @field(Struct, "get_" ++ name);
+ const param_count = @typeInfo(@TypeOf(getter)).@"fn".params.len;
+
+ var js_name: v8.Name = undefined;
+ if (comptime std.mem.eql(u8, name, "symbol_toStringTag")) {
+ if (param_count != 0) {
+ @compileError(@typeName(Struct) ++ ".get_symbol_toStringTag() cannot take any parameters");
+ }
+ js_name = v8.Symbol.getToStringTag(self.isolate).toName();
+ } else {
+ js_name = v8.String.initUtf8(self.isolate, name).toName();
+ }
+
+ const getter_callback = struct {
+ fn callback(_: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void {
+ const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
+ var caller = Caller(Self).init(info);
+ defer caller.deinit();
+
+ const named_function = NamedFunction(Struct, getter, "get_" ++ name){};
+ caller.getter(named_function, info) catch |err| {
+ caller.handleError(named_function, err, info);
+ };
+ }
+ }.callback;
+
+ const setter_name = "set_" ++ name;
+ if (@hasDecl(Struct, setter_name) == false) {
+ template_proto.setGetter(js_name, getter_callback);
+ return;
+ }
+
+ const setter = @field(Struct, setter_name);
+ const setter_callback = struct {
+ fn callback(_: ?*const v8.C_Name, raw_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void {
+ const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
+ var caller = Caller(Self).init(info);
+ defer caller.deinit();
+
+ const js_value = v8.Value{ .handle = raw_value.? };
+ const named_function = NamedFunction(Struct, setter, "set_" ++ name){};
+ caller.setter(named_function, js_value, info) catch |err| {
+ caller.handleError(named_function, err, info);
+ };
+ }
+ }.callback;
+ template_proto.setGetterAndSetter(js_name, getter_callback, setter_callback);
+ }
+
+ fn generateIndexer(_: *Self, 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) void {
+ const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
+ var caller = Caller(Self).init(info);
+ defer caller.deinit();
+
+ const named_function = NamedFunction(Struct, Struct.indexed_get, "indexed_get"){};
+ caller.getIndex(named_function, idx, info) catch |err| {
+ caller.handleError(named_function, err, info);
+ };
+ }
+ }.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 ont he 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(_: *Self, comptime Struct: type, template_proto: v8.ObjectTemplate) void {
+ if (@hasDecl(Struct, "named_get") == false) {
+ return;
+ }
+ const configuration = v8.NamedPropertyHandlerConfiguration{
+ .getter = struct {
+ fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void {
+ const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
+ var caller = Caller(Self).init(info);
+ defer caller.deinit();
+
+ const named_function = NamedFunction(Struct, Struct.named_get, "named_get"){};
+ caller.getNamedIndex(named_function, .{ .handle = c_name.? }, info) catch |err| {
+ caller.handleError(named_function, err, info);
+ };
+ }
+ }.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 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 ont he 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.setNamedProperty(configuration, null);
+ }
+
+ // Turns a Zig value into a JS one.
+ fn zigValueToJs(
+ templates: []v8.FunctionTemplate,
+ isolate: v8.Isolate,
+ context: v8.Context,
+ value: anytype,
+ ) anyerror!v8.Value {
+ // Check if it's a "simple" type. This is extracted so that it can be
+ // reused by other parts of the code. "simple" types only require an
+ // isolate to create (specifically, they don't our templates array)
+ if (simpleZigValueToJs(isolate, value, false)) |js_value| {
+ return js_value;
+ }
+
+ const T = @TypeOf(value);
+ switch (@typeInfo(T)) {
+ .void, .bool, .int, .comptime_int, .float, .comptime_float, .array => {
+ // Need to do this to keep the compiler happy
+ // simpleZigValueToJs handles all of these cases.
+ unreachable;
+ },
+ .pointer => |ptr| switch (ptr.size) {
+ .one => {
+ const type_name = @typeName(ptr.child);
+ if (@hasField(TypeLookup, type_name)) {
+ const template = templates[@field(TYPE_LOOKUP, type_name)];
+ const js_obj = try Executor.mapZigInstanceToJs(context, template, value);
+ return js_obj.toValue();
+ }
+
+ const one_info = @typeInfo(ptr.child);
+ if (one_info == .array and one_info.array.child == u8) {
+ // Need to do this to keep the compiler happy
+ // If this was the case, simpleZigValueToJs would
+ // have handled it
+ unreachable;
+ }
+ },
+ .slice => {
+ if (ptr.child == u8) {
+ // Need to do this to keep the compiler happy
+ // If this was the case, simpleZigValueToJs would
+ // have handled it
+ unreachable;
+ }
+ var js_arr = v8.Array.init(isolate, @intCast(value.len));
+ var js_obj = js_arr.castTo(v8.Object);
+
+ for (value, 0..) |v, i| {
+ const js_val = try zigValueToJs(templates, isolate, context, v);
+ if (js_obj.setValueAtIndex(context, @intCast(i), js_val) == false) {
+ return error.FailedToCreateArray;
+ }
+ }
+ return js_obj.toValue();
+ },
+ else => {},
+ },
+ .@"struct" => |s| {
+ const type_name = @typeName(T);
+ if (@hasField(TypeLookup, type_name)) {
+ const template = templates[@field(TYPE_LOOKUP, type_name)];
+ const js_obj = try Executor.mapZigInstanceToJs(context, template, value);
+ return js_obj.toValue();
+ }
+
+ if (T == Callback) {
+ // we're returnig a callback
+ return value.func.toValue();
+ }
+
+ // return the struct as a JS object
+ const js_obj = v8.Object.init(isolate);
+ inline for (s.fields) |f| {
+ const js_val = try zigValueToJs(templates, isolate, context, @field(value, f.name));
+ const key = v8.String.initUtf8(isolate, f.name);
+ if (!js_obj.setValue(context, key, js_val)) {
+ return error.CreateObjectFailure;
+ }
+ }
+ return js_obj.toValue();
+ },
+ .@"union" => |un| {
+ if (T == std.json.Value) {
+ return zigJsonToJs(isolate, context, value);
+ }
+ if (un.tag_type) |UnionTagType| {
+ inline for (un.fields) |field| {
+ if (value == @field(UnionTagType, field.name)) {
+ return zigValueToJs(templates, isolate, context, @field(value, field.name));
+ }
+ }
+ unreachable;
+ }
+ @compileError("Cannot use untagged union: " ++ @typeName(T));
+ },
+ .optional => {
+ if (value) |v| {
+ return zigValueToJs(templates, isolate, context, v);
+ }
+ return v8.initNull(isolate).toValue();
+ },
+ .error_union => return zigValueToJs(templates, isolate, context, value catch |err| return err),
+ else => {},
+ }
+ @compileLog(@typeInfo(T));
+ @compileError("A function returns an unsupported type: " ++ @typeName(T));
+ }
+
+ const PersistentObject = v8.Persistent(v8.Object);
+ const PersistentFunction = v8.Persistent(v8.Function);
+
+ // This is capable of executing JavaScript.
+ pub const Executor = struct {
+ state: State,
+ isolate: v8.Isolate,
+
+ handle_scope: v8.HandleScope,
+
+ // @intFromPtr of our Executor is stored in this context, so given
+ // a context, we can always get the Executor back.
+ context: v8.Context,
+
+ // Arena whose lifetime is for a single getter/setter/function/etc.
+ // Largely used to get strings out of V8, like a stack trace from
+ // a TryCatch. The allocator will be owned by the Scope, but the
+ // arena itself is owned by the Executor so that we can re-use it
+ // from scope to scope.
+ call_arena: ArenaAllocator,
+
+ // Arena whose lifetime is for a single page load, aka a Scope. Where
+ // the call_arena lives for a single function call, the scope_arena
+ // lives for the lifetime of the entire page. The allocator will be
+ // owned by the Scope, but the arena itself is owned by the Executor
+ // so that we can re-use it from scope to scope.
+ scope_arena: ArenaAllocator,
+
+ // When we need to load a resource (i.e. an external script), we call
+ // this function to get the source. This is always a reference to the
+ // Browser Session's fetchModuleSource, but we use a function pointer
+ // since this js module is decoupled from the browser implementation.
+ module_loader: ModuleLoader,
+
+ // A Scope maps to a Browser's Page. Here though, it's only a
+ // mechanism to organization page-specific memory. The Executor
+ // does all the work, but having all page-specific data structures
+ // grouped together helps keep things clean.
+ scope: ?Scope = null,
+
+ // refernces the Env.template array
+ templates: []v8.FunctionTemplate,
+
+ const ModuleLoader = struct { ptr: *anyopaque, func: *const fn (ptr: *anyopaque, specifier: []const u8) anyerror![]const u8 };
+
+ // no init, must be initialized via env.startExecutor()
+
+ // not public, must be destroyed via env.stopExecutor()
+ fn deinit(self: *Executor) void {
+ if (self.scope) |*s| {
+ s.deinit();
+ }
+ self.context.exit();
+ self.handle_scope.deinit();
+ self.call_arena.deinit();
+ self.scope_arena.deinit();
+ }
+
+ // Executes the src
+ pub fn exec(self: *Executor, src: []const u8, name: ?[]const u8) !Value {
+ const isolate = self.isolate;
+ const context = self.context;
+
+ var origin: ?v8.ScriptOrigin = null;
+ if (name) |n| {
+ const scr_name = v8.String.initUtf8(isolate, n);
+ origin = v8.ScriptOrigin.initDefault(isolate, scr_name.toValue());
+ }
+ const scr_js = v8.String.initUtf8(isolate, src);
+ const scr = v8.Script.compile(context, scr_js, origin) catch {
+ return error.CompilationError;
+ };
+
+ const value = scr.run(context) catch {
+ return error.ExecutionError;
+ };
+
+ return self.createValue(value);
+ }
+
+ // compile and eval a JS module
+ // It doesn't wait for callbacks execution
+ pub fn module(self: *Executor, src: []const u8, name: []const u8) !Value {
+ const context = self.context;
+ const m = try self.compileModule(src, name);
+
+ // instantiate
+ // TODO handle ResolveModuleCallback parameters to load module's
+ // dependencies.
+ const ok = m.instantiate(context, resolveModuleCallback) catch {
+ return error.ExecutionError;
+ };
+
+ if (!ok) {
+ return error.ModuleInstantiationError;
+ }
+
+ // evaluate
+ const value = m.evaluate(context) catch return error.ExecutionError;
+ return self.createValue(value);
+ }
+
+ fn compileModule(self: *Executor, src: []const u8, name: []const u8) !v8.Module {
+ const isolate = self.isolate;
+
+ // compile
+ const script_name = v8.String.initUtf8(isolate, name);
+ const script_source = v8.String.initUtf8(isolate, src);
+
+ const origin = v8.ScriptOrigin.init(
+ self.isolate,
+ script_name.toValue(),
+ 0, // resource_line_offset
+ 0, // resource_column_offset
+ false, // resource_is_shared_cross_origin
+ -1, // script_id
+ null, // source_map_url
+ false, // resource_is_opaque
+ false, // is_wasm
+ true, // is_module
+ null, // host_defined_options
+ );
+
+ var script_comp_source: v8.ScriptCompilerSource = undefined;
+ v8.ScriptCompilerSource.init(&script_comp_source, script_source, origin, null);
+ defer script_comp_source.deinit();
+
+ return v8.ScriptCompiler.compileModule(
+ isolate,
+ &script_comp_source,
+ .kNoCompileOptions,
+ .kNoCacheNoReason,
+ ) catch return error.CompilationError;
+ }
+
+ // Our scope maps to a "browser.Page".
+ // 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 "scope_arena" which allows us to have
+ // all page related memory easily managed.
+ pub fn startScope(self: *Executor, global: anytype) !void {
+ std.debug.assert(self.scope == null);
+
+ var handle_scope: v8.HandleScope = undefined;
+ v8.HandleScope.init(&handle_scope, self.isolate);
+ self.scope = Scope{
+ .handle_scope = handle_scope,
+ .arena = self.scope_arena.allocator(),
+ .call_arena = self.scope_arena.allocator(),
+ };
+ _ = try self._mapZigInstanceToJs(self.context.getGlobal(), global);
+ }
+
+ pub fn endScope(self: *Executor) void {
+ self.scope.?.deinit();
+ self.scope = null;
+ _ = self.scope_arena.reset(.{ .retain_with_limit = 1024 * 64 });
+ }
+
+ // Wrap a v8.Value, largely so that we can provide a convenient
+ // toString function
+ fn createValue(self: *const Executor, value: v8.Value) Value {
+ return .{
+ .value = value,
+ .executor = self,
+ };
+ }
+
+ fn zigValueToJs(self: *const Executor, value: anytype) !v8.Value {
+ return Self.zigValueToJs(self.templates, self.isolate, self.context, value);
+ }
+
+ // See _mapZigInstanceToJs, this is wrapper that can be called
+ // without an Executor. This is possible because we store our
+ // executor in the EmbedderData of the v8.Context. So, as long as
+ // we have a v8.Context, we can get the executor.
+ fn mapZigInstanceToJs(context: v8.Context, js_obj_or_template: anytype, value: anytype) !PersistentObject {
+ const executor: *Executor = @ptrFromInt(context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
+ return executor._mapZigInstanceToJs(js_obj_or_template, value);
+ }
+
+ // To turn a Zig instance into a v8 object, we need to do a number of things.
+ // First, if it's a struct, we need to put it on the heap
+ // Second, if we've already returned this instance, we should return
+ // the same object. Hence, our executor maintains a map of Zig objects
+ // to v8.PersistentObject (the "identity_map").
+ // Finally, if this is the first time we've seen this instance, we need to:
+ // 1 - get the FunctionTemplate (from our templates slice)
+ // 2 - Create the TaggedAnyOpaque so that, if needed, we can do the reverse
+ // (i.e. js -> zig)
+ // 3 - Create a v8.PersistentObject (because Zig owns this object, not v8)
+ // 4 - Store our TaggedAnyOpaque into the persistent object
+ // 5 - Update our identity_map (so that, if we return this same instance again,
+ // we can just grab it from the identity_map)
+ fn _mapZigInstanceToJs(self: *Executor, js_obj_or_template: anytype, value: anytype) !PersistentObject {
+ const scope = &self.scope.?;
+ const context = self.context;
+ const scope_arena = scope.arena;
+
+ const T = @TypeOf(value);
+ switch (@typeInfo(T)) {
+ .@"struct" => {
+ // Struct, has to be placed on the heap
+ const heap = try scope_arena.create(T);
+ heap.* = value;
+ return self._mapZigInstanceToJs(js_obj_or_template, heap);
+ },
+ .pointer => |ptr| {
+ const gop = try scope.identity_map.getOrPut(scope_arena, @intFromPtr(value));
+ if (gop.found_existing) {
+ // we've seen this instance before, return the same
+ // PersistentObject.
+ return gop.value_ptr.*;
+ }
+
+ // Sometimes we're creating a new v8.Object, like when
+ // we're returning a value from a function. In those cases
+ // we have the FunctionTemplate, and we can get an object
+ // by calling initInstance its InstanceTemplate.
+ // Sometimes though we already have the v8.Objct to bind to
+ // for example, when we're executing a constructor, v8 has
+ // already created the "this" object.
+ const js_obj = switch (@TypeOf(js_obj_or_template)) {
+ v8.Object => js_obj_or_template,
+ v8.FunctionTemplate => js_obj_or_template.getInstanceTemplate().initInstance(context),
+ else => @compileError("mapZigInstanceToJs requires a v8.Object (constructors) or v8.FunctionTemplate, got: " ++ @typeName(@TypeOf(js_obj_or_template))),
+ };
+
+ const isolate = self.isolate;
+
+ if (isEmpty(ptr.child) == false) {
+ // The TAO contains the pointer ot our Zig instance as
+ // well as any meta data we'll need to use it later.
+ // See the TaggedAnyOpaque struct for more details.
+ const tao = try scope_arena.create(TaggedAnyOpaque);
+ tao.* = .{
+ .ptr = value,
+ .index = @field(TYPE_LOOKUP, @typeName(ptr.child)),
+ .sub_type = if (@hasDecl(ptr.child, "sub_type")) ptr.child.sub_type else null,
+ .offset = if (@typeInfo(ptr.child) != .@"opaque" and @hasField(ptr.child, "proto")) @offsetOf(ptr.child, "proto") else -1,
+ };
+
+ js_obj.setInternalField(0, v8.External.init(isolate, tao));
+ } else {
+ // If the struct is empty, we don't need to do all
+ // the TOA stuff and setting the internal data.
+ // When we try to map this from JS->Zig, in
+ // typeTaggedAnyOpaque, we'll also know there that
+ // the type is empty and can create an empty instance.
+ }
+
+ // Do not move this _AFTER_ the postAttach code.
+ // postAttach is likely to call back into this function
+ // mutating our identity_map, and making the gop pointers
+ // invalid.
+ const js_persistent = PersistentObject.init(isolate, js_obj);
+ gop.value_ptr.* = js_persistent;
+
+ if (@hasDecl(ptr.child, "postAttach")) {
+ const obj_wrap = JsObject{ .js_obj = js_obj, .executor = self };
+ switch (@typeInfo(@TypeOf(ptr.child.postAttach)).@"fn".params.len) {
+ 2 => try value.postAttach(obj_wrap),
+ 3 => try value.postAttach(self.state, obj_wrap),
+ else => @compileError(@typeName(ptr.child) ++ ".postAttach must take 2 or 3 parameters"),
+ }
+ }
+
+ return js_persistent;
+ },
+ else => @compileError("Expected a struct or pointer, got " ++ @typeName(T) ++ " (constructors must return struct or pointers)"),
+ }
+ }
+
+ // Callback from V8, asking us to load a module. The "specifier" is
+ // the src of the module to load.
+ fn resolveModuleCallback(
+ c_context: ?*const v8.C_Context,
+ c_specifier: ?*const v8.C_String,
+ import_attributes: ?*const v8.C_FixedArray,
+ referrer: ?*const v8.C_Module,
+ ) callconv(.C) ?*const v8.C_Module {
+ _ = import_attributes;
+ _ = referrer;
+
+ std.debug.assert(c_context != null);
+ const context = v8.Context{ .handle = c_context.? };
+
+ const self: *Executor = @ptrFromInt(context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
+
+ var buf: [1024]u8 = undefined;
+ var fba = std.heap.FixedBufferAllocator.init(&buf);
+
+ // build the specifier value.
+ const specifier = valueToString(
+ fba.allocator(),
+ .{ .handle = c_specifier.? },
+ self.isolate,
+ context,
+ ) catch |e| {
+ log.err("resolveModuleCallback: get ref str: {any}", .{e});
+ return null;
+ };
+
+ // not currently needed
+ // const referrer_module = if (referrer) |ref| v8.Module{ .handle = ref } else null;
+ const module_loader = self.module_loader;
+ const source = module_loader.func(module_loader.ptr, specifier) catch |err| {
+ log.err("fetchModuleSource for '{s}' fetch error: {}", .{ specifier, err });
+ return null;
+ };
+
+ const m = self.compileModule(source, specifier) catch |err| {
+ log.err("fetchModuleSource for '{s}' compile error: {}", .{ specifier, err });
+ return null;
+ };
+ return m.handle;
+ }
+ };
+
+ // Loosely maps to a Browser Page. Executor does all the work, this just
+ // contains all the data structures / memory we need for a page. It helps
+ // to keep things organized. I.e. we have a single nullable,
+ // scope: ?Scope = null
+ // in executor, rather than having one for each of these.
+ pub const Scope = struct {
+ arena: Allocator,
+ call_arena: Allocator,
+ handle_scope: v8.HandleScope,
+ callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .{},
+ identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{},
+
+ fn deinit(self: *Scope) void {
+ var it = self.identity_map.valueIterator();
+ while (it.next()) |p| {
+ p.deinit();
+ }
+ for (self.callbacks.items) |*cb| {
+ cb.deinit();
+ }
+ self.handle_scope.deinit();
+ }
+
+ fn trackCallback(self: *Scope, pf: PersistentFunction) !void {
+ return self.callbacks.append(self.arena, pf);
+ }
+ };
+
+ pub const Callback = struct {
+ id: usize,
+ executor: *Executor,
+ this: ?v8.Object = null,
+ func: PersistentFunction,
+
+ // We use this when mapping a JS value to a Zig object. We can't
+ // Say we have a Zig function that takes a Callback, we can't just
+ // check param.type == Callback, because Callback is a generic.
+ // So, as a quick hack, we can determine if the Zig type is a
+ // callback by checking @hasDecl(T, "_CALLBACK_ID_KLUDGE")
+ const _CALLBACK_ID_KLUDGE = true;
+
+ pub const Result = struct {
+ stack: ?[]const u8,
+ exception: []const u8,
+ };
+
+ pub fn setThis(self: *Callback, value: anytype) !void {
+ const persistent_object = self.executor.scope.?.identity_map.get(@intFromPtr(value)) orelse {
+ return error.InvalidThisForCallback;
+ };
+ self.this = persistent_object.castToObject();
+ }
+
+ pub fn call(self: *const Callback, args: anytype) !void {
+ return self.callWithThis(self.this orelse self.executor.context.getGlobal(), args);
+ }
+
+ pub fn tryCall(self: *const Callback, args: anytype, result: *Result) !void {
+ var try_catch: TryCatch = undefined;
+ try_catch.init(self.executor);
+ defer try_catch.deinit();
+
+ self.call(args) catch |err| {
+ if (try_catch.hasCaught()) {
+ const allocator = self.executor.scope.?.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;
+ };
+ }
+
+ fn callWithThis(self: *const @This(), js_this: v8.Object, args: anytype) !void {
+ const executor = self.executor;
+
+ const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
+ const fields = @typeInfo(@TypeOf(aargs)).@"struct".fields;
+ var js_args: [fields.len]v8.Value = undefined;
+ inline for (fields, 0..) |f, i| {
+ js_args[i] = try executor.zigValueToJs(@field(aargs, f.name));
+ }
+
+ const result = self.func.castToFunction().call(executor.context, js_this, &js_args);
+ if (result == null) {
+ return error.JSExecCallback;
+ }
+ }
+
+ // debug/helper to print the source of the JS callback
+ fn printFunc(self: *const @This()) !void {
+ const executor = self.executor;
+ const value = self.func.castToFunction().toValue();
+ const src = try valueToString(executor.call_arena.allocator(), value, executor.isolate, executor.context);
+ std.debug.print("{s}\n", .{src});
+ }
+ };
+
+ pub const JsObject = struct {
+ js_obj: v8.Object,
+ executor: *Executor,
+
+ // If a Zig struct wants the Object parameter, it'll declare a
+ // function like:
+ // fn _length(self: *const NodeList, js_obj: Env.Object) usize
+ //
+ // When we're trying to call this function, we can't just do
+ // if (params[i].type.? == Object)
+ // Because there is _no_ object, there's only an Env.Object, where
+ // Env is a generic.
+ // We could probably figure out a way to do this, but simply checking
+ // for this declaration is _a lot_ easier.
+ const _JSOBJECT_ID_KLUDGE = true;
+
+ pub fn setIndex(self: JsObject, index: usize, value: anytype) !void {
+ const key = switch (index) {
+ inline 0...1000 => |i| std.fmt.comptimePrint("{d}", .{i}),
+ else => try std.fmt.allocPrint(self.executor.scope_arena.allocator(), "{d}", .{index}),
+ };
+ return self.set(key, value);
+ }
+
+ pub fn set(self: JsObject, key: []const u8, value: anytype) !void {
+ const executor = self.executor;
+
+ const js_key = v8.String.initUtf8(executor.isolate, key);
+ const js_value = try executor.zigValueToJs(value);
+ if (!self.js_obj.setValue(executor.context, js_key, js_value)) {
+ return error.FailedToSet;
+ }
+ }
+ };
+
+ pub const TryCatch = struct {
+ inner: v8.TryCatch,
+ executor: *const Executor,
+
+ pub fn init(self: *TryCatch, executor: *const Executor) void {
+ self.executor = executor;
+ self.inner.init(executor.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;
+ const executor = self.executor;
+ return try valueToString(allocator, msg, executor.isolate, executor.context);
+ }
+
+ // the caller needs to deinit the string returned
+ pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
+ const executor = self.executor;
+ const s = self.inner.getStackTrace(executor.context) orelse return null;
+ return try valueToString(allocator, s, executor.isolate, executor.context);
+ }
+
+ // a shorthand method to return either the entire stack message
+ // or just the exception message
+ // - in Debug mode return the stack if available
+ // - otherwhise return the exception if available
+ // the caller needs to deinit the string returned
+ pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 {
+ if (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();
+ }
+ };
+
+ pub const Inspector = struct {
+ isolate: v8.Isolate,
+ inner: *v8.Inspector,
+ session: v8.InspectorSession,
+
+ // We expect allocator to be an arena
+ pub fn init(allocator: Allocator, executor: *const Executor, 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) @constCast(@ptrCast(&{})) else ctx;
+
+ const isolate = executor.isolate;
+ 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 {
+ self.session.dispatchProtocolMessage(self.isolate, msg);
+ }
+
+ pub fn contextCreated(
+ self: *const Inspector,
+ executor: *const Executor,
+ name: []const u8,
+ origin: []const u8,
+ aux_data: ?[]const u8,
+ ) void {
+ self.inner.contextCreated(executor.context, name, origin, aux_data);
+ }
+
+ // Retrieves the RemoteObject for a given value.
+ // The value is loaded through the Executor'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 scope ends.
+ pub fn getRemoteObject(
+ self: *const Inspector,
+ executor: *const Executor,
+ group: []const u8,
+ value: anytype,
+ ) !RemoteObject {
+ const js_value = try zigValueToJs(
+ executor.templates,
+ executor.isolate,
+ executor.context,
+ value,
+ );
+
+ // We do not want to expose this as a parameter for now
+ const generate_preview = false;
+ return self.session.wrapObject(
+ executor.isolate,
+ executor.context,
+ js_value,
+ group,
+ generate_preview,
+ );
+ }
+ };
+
+ pub const RemoteObject = v8.RemoteObject;
+
+ pub const Value = struct {
+ value: v8.Value,
+ executor: *const Executor,
+
+ // the caller needs to deinit the string returned
+ pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
+ const executor = self.executor;
+ return valueToString(allocator, self.value, executor.isolate, executor.context);
+ }
+ };
+
+ // Reverses the mapZigInstanceToJs, making sure that our TaggedAnyOpaque
+ // contains a ptr to the correct type.
+ fn typeTaggedAnyOpaque(comptime named_function: anytype, comptime R: type, js_obj: v8.Object) !R {
+ const ti = @typeInfo(R);
+ if (ti != .pointer) {
+ @compileError(std.fmt.comptimePrint(
+ "{s} has a non-pointer Zig parameter type: {s}",
+ .{ named_function.full_name, @typeName(R) },
+ ));
+ }
+
+ const T = ti.pointer.child;
+ if (comptime isEmpty(T)) {
+ // Empty structs aren't stored as TOAs and there's no data
+ // stored in the JSObject's IntenrnalField. Why bother when
+ // we can just return an empty struct here?
+ return @constCast(@as(*const T, &.{}));
+ }
+
+ const type_name = @typeName(T);
+ if (@hasField(TypeLookup, type_name) == false) {
+ @compileError(std.fmt.comptimePrint(
+ "{s} has an unknown Zig type: {s}",
+ .{ named_function.full_name, @typeName(R) },
+ ));
+ }
+
+ const op = js_obj.getInternalField(0).castTo(v8.External).get();
+ const toa: *TaggedAnyOpaque = @alignCast(@ptrCast(op));
+ const expected_type_index = @field(TYPE_LOOKUP, type_name);
+
+ var type_index = toa.index;
+ if (type_index == expected_type_index) {
+ return @alignCast(@ptrCast(toa.ptr));
+ }
+
+ // search through the prototype tree
+ while (true) {
+ const prototype_index = PROTOTYPE_TABLE[type_index];
+ if (prototype_index == expected_type_index) {
+ // -1 is a sentinel value used for non-composition prototype
+ // This is used with netsurf and we just unsafely cast one
+ // type to another
+ const offset = toa.offset;
+ if (offset == -1) {
+ return @alignCast(@ptrCast(toa.ptr));
+ }
+
+ // A non-negative offset means we're using composition prototype
+ // (i.e. our struct has a "proto" field). the offset
+ // reresents the byte offset of the field. We can use that
+ // + the toa.ptr to get the field
+ return @ptrFromInt(@intFromPtr(toa.ptr) + @as(usize, @intCast(offset)));
+ }
+ if (prototype_index == type_index) {
+ return error.InvalidArgument;
+ }
+ type_index = prototype_index;
+ }
+ }
+ };
+}
+
+fn isEmpty(comptime T: type) bool {
+ return @typeInfo(T) != .@"opaque" and @sizeOf(T) == 0;
+}
+
+// Responsible for calling Zig functions from JS invokations. This could
+// probably just contained in Executor, but having this specific logic, which
+// is somewhat repetitive between constructors, functions, getters, etc contained
+// here does feel like it makes it clenaer.
+fn Caller(comptime E: type) type {
+ const State = E.State;
+ const TYPE_LOOKUP = E.TYPE_LOOKUP;
+ const TypeLookup = @TypeOf(TYPE_LOOKUP);
+
+ return struct {
+ context: v8.Context,
+ isolate: v8.Isolate,
+ executor: *E.Executor,
+ call_allocator: Allocator,
+
+ const Self = @This();
+
+ // info is a v8.PropertyCallbackInfo or a v8.FunctionCallback
+ // All we really want from it is the isolate.
+ // executor = Isolate -> getCurrentContext -> getEmbedderData()
+ fn init(info: anytype) Self {
+ const isolate = info.getIsolate();
+ const context = isolate.getCurrentContext();
+ const executor: *E.Executor = @ptrFromInt(context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
+
+ return .{
+ .isolate = isolate,
+ .context = context,
+ .executor = executor,
+ .call_allocator = executor.scope.?.call_arena,
+ };
+ }
+
+ fn deinit(self: *Self) void {
+ _ = self;
+ // _ = self.executor.call_arena.reset(.{ .retain_with_limit = 4096 });
+ }
+
+ fn constructor(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void {
+ const S = named_function.S;
+ const args = try self.getArgs(named_function, 0, info);
+ const res = @call(.auto, S.constructor, args);
+
+ const ReturnType = @typeInfo(@TypeOf(S.constructor)).@"fn".return_type orelse {
+ @compileError(@typeName(S) ++ " 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 E.Executor.mapZigInstanceToJs(self.context, this, non_error_res);
+ } else {
+ _ = try E.Executor.mapZigInstanceToJs(self.context, this, res);
+ }
+ info.getReturnValue().set(this);
+ }
+
+ fn method(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void {
+ const S = named_function.S;
+ comptime assertSelfReceiver(named_function);
+
+ var args = try self.getArgs(named_function, 1, info);
+ const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis());
+
+ // inject 'self' as the first parameter
+ @field(args, "0") = zig_instance;
+
+ const res = @call(.auto, named_function.func, args);
+ info.getReturnValue().set(try self.zigValueToJs(res));
+ }
+
+ fn getter(self: *Self, comptime named_function: anytype, info: v8.PropertyCallbackInfo) !void {
+ const S = named_function.S;
+ const Getter = @TypeOf(named_function.func);
+ if (@typeInfo(Getter).@"fn".return_type == null) {
+ @compileError(@typeName(S) ++ " has a getter without a return type: " ++ @typeName(Getter));
+ }
+
+ var args: ParamterTypes(Getter) = undefined;
+ const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
+ switch (arg_fields.len) {
+ 0 => {}, // getters _can_ be parameterless
+ 1, 2 => {
+ const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis());
+ comptime assertSelfReceiver(named_function);
+ @field(args, "0") = zig_instance;
+ if (comptime arg_fields.len == 2) {
+ comptime assertIsStateArg(named_function, 1);
+ @field(args, "1") = self.executor.state;
+ }
+ },
+ else => @compileError(named_function.full_name + " has too many parmaters: " ++ @typeName(named_function.func)),
+ }
+ const res = @call(.auto, named_function.func, args);
+ info.getReturnValue().set(try self.zigValueToJs(res));
+ }
+
+ fn setter(self: *Self, comptime named_function: anytype, js_value: v8.Value, info: v8.PropertyCallbackInfo) !void {
+ const S = named_function.S;
+ comptime assertSelfReceiver(named_function);
+
+ const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis());
+
+ const Setter = @TypeOf(named_function.func);
+ var args: ParamterTypes(Setter) = undefined;
+ const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
+ switch (arg_fields.len) {
+ 0 => unreachable, // assertSelfReceiver make sure of this
+ 1 => @compileError(named_function.full_name ++ " only has 1 parameter"),
+ 2, 3 => {
+ @field(args, "0") = zig_instance;
+ @field(args, "1") = try self.jsValueToZig(named_function, arg_fields[1].type, js_value);
+ if (comptime arg_fields.len == 3) {
+ comptime assertIsStateArg(named_function, 2);
+ @field(args, "2") = self.executor.state;
+ }
+ },
+ else => @compileError(named_function.full_name ++ " setter with more than 3 parameters, why?"),
+ }
+
+ if (@typeInfo(Setter).@"fn".return_type) |return_type| {
+ if (@typeInfo(return_type) == .error_union) {
+ _ = try @call(.auto, named_function.func, args);
+ return;
+ }
+ }
+ _ = @call(.auto, named_function.func, args);
+ }
+
+ fn getIndex(self: *Self, comptime named_function: anytype, idx: u32, info: v8.PropertyCallbackInfo) !void {
+ const S = named_function.S;
+ const IndexedGet = @TypeOf(named_function.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 E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis());
+ comptime assertSelfReceiver(named_function);
+ @field(args, "0") = zig_instance;
+ @field(args, "1") = idx;
+ @field(args, "2") = &has_value;
+ if (comptime arg_fields.len == 4) {
+ comptime assertIsStateArg(named_function, 3);
+ @field(args, "3") = self.executor.state;
+ }
+ },
+ else => @compileError(named_function.full_name ++ " has too many parmaters"),
+ }
+
+ const res = @call(.auto, S.indexed_get, args);
+ if (has_value == false) {
+ // for an indexed parameter, say nodes[10000], we should return
+ // undefined, not null, if the index is out of rante
+ info.getReturnValue().set(try self.zigValueToJs({}));
+ } else {
+ info.getReturnValue().set(try self.zigValueToJs(res));
+ }
+ }
+
+ fn getNamedIndex(self: *Self, comptime named_function: anytype, name: v8.Name, info: v8.PropertyCallbackInfo) !void {
+ const S = named_function.S;
+ const NamedGet = @TypeOf(named_function.func);
+ if (@typeInfo(NamedGet).@"fn".return_type == null) {
+ @compileError(named_function.full_name ++ " must have a return type");
+ }
+
+ var has_value = true;
+ var args: ParamterTypes(NamedGet) = 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 E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis());
+ comptime assertSelfReceiver(named_function);
+ @field(args, "0") = zig_instance;
+ @field(args, "1") = try self.nameToString(name);
+ @field(args, "2") = &has_value;
+ if (comptime arg_fields.len == 4) {
+ comptime assertIsStateArg(named_function, 3);
+ @field(args, "3") = self.executor.state;
+ }
+ },
+ else => @compileError(named_function.full_name ++ " has too many parmaters"),
+ }
+
+ const res = @call(.auto, S.named_get, args);
+ if (has_value == false) {
+ // for an indexed parameter, say nodes[10000], we should return
+ // undefined, not null, if the index is out of rante
+ info.getReturnValue().set(try self.zigValueToJs({}));
+ } else {
+ info.getReturnValue().set(try self.zigValueToJs(res));
+ }
+ }
+
+ fn nameToString(self: *Self, name: v8.Name) ![]const u8 {
+ return valueToString(self.call_allocator, .{ .handle = name.handle }, self.isolate, self.context);
+ }
+
+ fn assertSelfReceiver(comptime named_function: anytype) void {
+ const params = @typeInfo(@TypeOf(named_function.func)).@"fn".params;
+ if (params.len == 0) {
+ @compileError(named_function.full_name ++ " must have a self parameter");
+ }
+ const R = Receiver(named_function.S);
+
+ const first_param = params[0].type.?;
+ if (first_param != *R and first_param != *const R) {
+ @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) }));
+ }
+ }
+
+ fn assertIsStateArg(comptime named_function: anytype, index: comptime_int) void {
+ const F = @TypeOf(named_function.func);
+ const params = @typeInfo(F).@"fn".params;
+
+ const param = params[index].type.?;
+ if (param != State) {
+ @compileError(std.fmt.comptimePrint("The {d} parameter to {s} must be a {s}. Got: {s}", .{ index, named_function.full_name, @typeName(State), @typeName(param) }));
+ }
+ }
+
+ fn handleError(self: *Self, comptime named_function: anytype, err: anyerror, info: anytype) void {
+ const isolate = self.isolate;
+ var js_err: ?v8.Value = switch (err) {
+ error.InvalidArgument => createTypeException(isolate, "invalid argument"),
+ error.OutOfMemory => createException(isolate, "out of memory"),
+ else => blk: {
+ const return_type = @typeInfo(@TypeOf(named_function.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 Exception = comptime getCustomException(named_function.S) orelse break :blk null;
+ if (function_error_set == Exception or isErrorSetException(Exception, err)) {
+ const custom_exception = Exception.init(self.call_allocator, 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 createException(isolate, "out of memory"),
+ }
+ };
+ // ughh..how to handle an error here?
+ break :blk self.zigValueToJs(custom_exception) catch createException(isolate, "internal error");
+ }
+ // this error isn't part of a custom exception
+ break :blk null;
+ },
+ };
+
+ if (js_err == null) {
+ js_err = 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 Exception: type, err: anytype) bool {
+ const Entry = std.meta.Tuple(&.{ []const u8, void });
+ const error_set = @typeInfo(Exception.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 State.
+ //
+ // 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 Self, comptime named_function: anytype, comptime offset: usize, info: anytype) !ParamterTypes(@TypeOf(named_function.func)) {
+ const F = @TypeOf(named_function.func);
+ 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 State, set it, and exclude it
+ // from our params slice, because we don't want to bind it to
+ // a JS argument
+ if (comptime isState(params[params.len - 1].type.?)) {
+ @field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = self.executor.state;
+ break :blk params[0 .. params.len - 1];
+ }
+
+ // If the last parameter is a JsObject, set it, and exclude it
+ // from our params slice, because we don't want to bind it to
+ // a JS argument
+ if (comptime isJsObject(params[params.len - 1].type.?)) {
+ @field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{
+ .handle = info.getThis(),
+ .executor = self.executor,
+ };
+
+ // AND the 2nd last parameter is state
+ if (params.len > 1 and comptime isState(params[params.len - 2].type.?)) {
+ @field(args, std.fmt.comptimePrint("{d}", .{params.len - 2 + offset})) = self.executor.state;
+ break :blk params[0 .. params.len - 2];
+ }
+
+ break :blk params[0 .. params.len - 1];
+ }
+
+ // we have neither a State 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 paremeter
+ // 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 slice_type != u8) {
+ is_variadic = true;
+ if (js_parameter_count == 0) {
+ @field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
+ } else {
+ const arr = try self.call_allocator.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 self.jsValueToZig(named_function, slice_type, js_value);
+ }
+ @field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
+ }
+ }
+ }
+ }
+
+ 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 isState(param.type.?)) {
+ @compileError("State must be the last parameter (or 2nd last if there's a JsObject): " ++ named_function.full_name);
+ } else if (comptime isJsObject(param.type.?)) {
+ @compileError("JsObject 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)) = self.jsValueToZig(named_function, param.type.?, js_value) catch {
+ return error.InvalidArgument;
+ };
+ }
+ }
+
+ return args;
+ }
+
+ fn jsValueToZig(self: *const Self, comptime named_function: anytype, comptime T: type, js_value: v8.Value) !T {
+ switch (@typeInfo(T)) {
+ .optional => |o| {
+ if (js_value.isNull() or js_value.isUndefined()) {
+ return null;
+ }
+ return try self.jsValueToZig(named_function, o.child, js_value);
+ },
+ .float => |f| switch (f.bits) {
+ 0...32 => return js_value.toF32(self.context),
+ 33...64 => return js_value.toF64(self.context),
+ else => {},
+ },
+ .int => return jsIntToZig(T, js_value, self.context),
+ .bool => return js_value.toBool(self.isolate),
+ .pointer => |ptr| switch (ptr.size) {
+ .one => {
+ if (!js_value.isObject()) {
+ return error.InvalidArgument;
+ }
+ if (@hasField(TypeLookup, @typeName(ptr.child))) {
+ const obj = js_value.castTo(v8.Object);
+ if (obj.internalFieldCount() == 0) {
+ return error.InvalidArgument;
+ }
+ return E.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), obj);
+ }
+ },
+ .slice => {
+ if (ptr.child == u8) {
+ return valueToString(self.call_allocator, js_value, self.isolate, self.context);
+ }
+
+ // TODO: TypedArray
+ // if (js_value.isArrayBufferView()) {
+ // const abv = js_value.castTo(v8.ArrayBufferView);
+ // const ab = abv.getBuffer();
+ // const bs = v8.BackingStore.sharedPtrGet(&ab.getBackingStore());
+ // const data = bs.getData();
+ // var arr = @as([*]i32, @alignCast(@ptrCast(data)))[0..2];
+ // std.debug.print("{d} {d} {d}\n", .{arr[0], arr[1], bs.getByteLength()});
+ // arr[1] = 3333;
+ // return &.{};
+ // }
+
+ if (!js_value.isArray()) {
+ return error.InvalidArgument;
+ }
+
+ const context = self.context;
+ const js_arr = js_value.castTo(v8.Array);
+ const js_obj = js_arr.castTo(v8.Object);
+
+ // Newer version of V8 appear to have an optimized way
+ // to do this (V8::Array has an iterate method on it)
+ const arr = try self.call_allocator.alloc(ptr.child, js_arr.length());
+ for (arr, 0..) |*a, i| {
+ a.* = try self.jsValueToZig(named_function, ptr.child, try js_obj.getAtIndex(context, @intCast(i)));
+ }
+ return arr;
+ },
+ else => {},
+ },
+ .@"struct" => |s| {
+ if (@hasDecl(T, "_CALLBACK_ID_KLUDGE")) {
+ if (!js_value.isFunction()) {
+ return error.InvalidArgument;
+ }
+
+ const executor = self.executor;
+ const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function));
+ try executor.scope.?.trackCallback(func);
+
+ return .{
+ .func = func,
+ .executor = executor,
+ .id = js_value.castTo(v8.Object).getIdentityHash(),
+ };
+ }
+
+ if (!js_value.isObject()) {
+ return error.InvalidArgument;
+ }
+ const context = self.context;
+ const isolate = self.isolate;
+ const js_obj = js_value.castTo(v8.Object);
+
+ var value: T = undefined;
+ inline for (s.fields) |field| {
+ const name = field.name;
+ const key = v8.String.initUtf8(isolate, name);
+ if (js_obj.has(context, key.toValue())) {
+ @field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(context, key));
+ } else if (@typeInfo(field.type) == .optional) {
+ @field(value, name) = null;
+ } else {
+ if (field.defaultValue()) |dflt| {
+ @field(value, name) = dflt;
+ } else {
+ return error.JSWrongObject;
+ }
+ }
+ }
+ return value;
+ },
+ else => {},
+ }
+
+ @compileError(std.fmt.comptimePrint("{s} has an unsupported parameter type: {s}", .{ named_function.full_name, @typeName(T) }));
+ }
+
+ fn jsIntToZig(comptime T: type, js_value: v8.Value, context: v8.Context) !T {
+ const n = @typeInfo(T).int;
+ switch (n.signedness) {
+ .signed => switch (n.bits) {
+ 8 => return jsSignedIntToZig(i8, -128, 127, try js_value.toI32(context)),
+ 16 => return jsSignedIntToZig(i16, -32_768, 32_767, try js_value.toI32(context)),
+ 32 => return jsSignedIntToZig(i32, -2_147_483_648, 2_147_483_647, try js_value.toI32(context)),
+ 64 => {
+ if (js_value.isBigInt()) {
+ const v = js_value.castTo(v8.BigInt);
+ return v.getInt64();
+ }
+ return jsSignedIntToZig(i64, -2_147_483_648, 2_147_483_647, try js_value.toI32(context));
+ },
+ else => {},
+ },
+ .unsigned => switch (n.bits) {
+ 8 => return jsUnsignedIntToZig(u8, 255, try js_value.toU32(context)),
+ 16 => return jsUnsignedIntToZig(u16, 65_535, try js_value.toU32(context)),
+ 32 => return jsUnsignedIntToZig(u32, 4_294_967_295, try js_value.toU32(context)),
+ 64 => {
+ if (js_value.isBigInt()) {
+ const v = js_value.castTo(v8.BigInt);
+ return v.getUint64();
+ }
+ return jsUnsignedIntToZig(u64, 4_294_967_295, try js_value.toU32(context));
+ },
+ else => {},
+ },
+ }
+ @compileError("Only i8, i16, i32, i64, u8, u16, u32 and u64 are supported");
+ }
+
+ fn jsSignedIntToZig(comptime T: type, comptime min: comptime_int, max: comptime_int, maybe: i32) !T {
+ if (maybe >= min and maybe <= max) {
+ return @intCast(maybe);
+ }
+ return error.InvalidArgument;
+ }
+
+ fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T {
+ if (maybe <= max) {
+ return @intCast(maybe);
+ }
+ return error.InvalidArgument;
+ }
+
+ fn zigValueToJs(self: *const Self, value: anytype) !v8.Value {
+ return self.executor.zigValueToJs(value);
+ }
+
+ fn isState(comptime T: type) bool {
+ const ti = @typeInfo(State);
+ const Const_State = if (ti == .pointer) *const ti.pointer.child else State;
+ return T == State or T == Const_State;
+ }
+
+ fn isJsObject(comptime T: type) bool {
+ return @typeInfo(T) == .@"struct" and @hasDecl(T, "_JSOBJECT_ID_KLUDGE");
+ }
+ };
+}
+
+// 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)
+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(),
+ .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();
+ },
+ .@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail),
+ else => {},
+ }
+ if (fail) {
+ @compileError("Unsupported Zig type " ++ @typeName(@TypeOf(value)));
+ }
+ return null;
+}
+
+pub fn zigJsonToJs(isolate: v8.Isolate, context: v8.Context, value: std.json.Value) !v8.Value {
+ switch (value) {
+ .bool => |v| return simpleZigValueToJs(isolate, v, true),
+ .float => |v| return simpleZigValueToJs(isolate, v, true),
+ .integer => |v| return simpleZigValueToJs(isolate, v, true),
+ .string => |v| return simpleZigValueToJs(isolate, v, true),
+ .null => return isolate.initNull().toValue(),
+
+ // TODO handle number_string.
+ // It is used to represent too big numbers.
+ .number_string => return error.TODO,
+
+ .array => |v| {
+ const a = v8.Array.init(isolate, @intCast(v.items.len));
+ const obj = a.castTo(v8.Object);
+ for (v.items, 0..) |array_value, i| {
+ const js_val = try zigJsonToJs(isolate, context, array_value);
+ if (!obj.setValueAtIndex(context, @intCast(i), js_val)) {
+ return error.JSObjectSetValue;
+ }
+ }
+ return obj.toValue();
+ },
+ .object => |v| {
+ var obj = v8.Object.init(isolate);
+ var it = v.iterator();
+ while (it.next()) |kv| {
+ const js_key = v8.String.initUtf8(isolate, kv.key_ptr.*);
+ const js_val = try zigJsonToJs(isolate, context, kv.value_ptr.*);
+ if (!obj.setValue(context, js_key, js_val)) {
+ return error.JSObjectSetValue;
+ }
+ }
+ return obj.toValue();
+ },
+ }
+}
+
+// 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 std.fmt.comptimePrint("{d}", .{i});
+}
+
+fn createException(isolate: v8.Isolate, msg: []const u8) v8.Value {
+ return v8.Exception.initError(v8.String.initUtf8(isolate, msg));
+}
+
+fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value {
+ return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg));
+}
+
+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).
+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,
+
+ // If this type has composition-based prototype, represents the byte-offset
+ // from ptr where the `proto` field is located. The value -1 represents
+ // unsafe prototype where we can just cast ptr to the destination type
+ // (this is used extensively with netsurf)
+ offset: i32,
+
+ // 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.
+ sub_type: ?[*c]const u8,
+};
+
+fn valueToString(allocator: Allocator, value: v8.Value, isolate: v8.Isolate, context: v8.Context) ![]u8 {
+ const str = try value.toString(context);
+ const len = str.lenUtf8(isolate);
+ const buf = try allocator.alloc(u8, len);
+ const n = str.writeUtf8(isolate, buf);
+ std.debug.assert(n == len);
+ return buf;
+}
+
+const NoopInspector = struct {
+ pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
+ pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
+};
+
+// 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
+fn Receiver(comptime S: type) type {
+ return if (@hasDecl(S, "Self")) S.Self else S;
+}
+
+// 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 capture it early and mostly pass around this NamedFunction instance
+// whenever we're trying to bind a function/getter/setter/etc so that we always
+// have the main data (struct + function) along with the meta data for displaying
+// better errors.
+fn NamedFunction(comptime S: type, comptime function: anytype, comptime name: []const u8) type {
+ const full_name = @typeName(S) ++ "." ++ name;
+ const js_name = if (name[0] == '_') name[1..] else name;
+ return struct {
+ S: type = S,
+ full_name: []const u8 = full_name,
+ func: @TypeOf(function) = function,
+ js_name: []const u8 = js_name,
+ };
+}
+
+// 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 = getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
+ return if (external_entry.sub_type) |st| 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,
+ context: *const v8.C_Context,
+ c_value: *const v8.C_Value,
+) callconv(.C) [*c]const u8 {
+ _ = 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 = getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
+ return if (external_entry.sub_type == null) null else "";
+}
+
+fn getTaggedAnyOpaque(value: v8.Value) ?*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 @alignCast(@ptrCast(external_data));
+}
+
+test {
+ std.testing.refAllDecls(@import("test_primitive_types.zig"));
+ std.testing.refAllDecls(@import("test_complex_types.zig"));
+ std.testing.refAllDecls(@import("test_object_types.zig"));
+}
diff --git a/src/runtime/loop.zig b/src/runtime/loop.zig
new file mode 100644
index 00000000..797b371e
--- /dev/null
+++ b/src/runtime/loop.zig
@@ -0,0 +1,469 @@
+// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+const builtin = @import("builtin");
+const MemoryPool = std.heap.MemoryPool;
+
+pub const IO = @import("tigerbeetle-io").IO;
+
+const JSCallback = @import("../browser/env.zig").Env.Callback;
+
+const log = std.log.scoped(.loop);
+
+// SingleThreaded I/O Loop based on Tigerbeetle io_uring loop.
+// On Linux it's using io_uring.
+// On MacOS and Windows it's using kqueue/IOCP with a ring design.
+// This is a thread-unsafe version without any lock on shared resources,
+// use it only on a single thread.
+// The loop provides I/O APIs based on callbacks.
+// I/O APIs based on async/await might be added in the future.
+pub const Loop = struct {
+ alloc: std.mem.Allocator, // TODO: unmanaged version ?
+ io: IO,
+
+ // both events_nb are used to track how many callbacks are to be called.
+ // We use these counters to wait until all the events are finished.
+ js_events_nb: usize,
+ zig_events_nb: usize,
+
+ cbk_error: bool = false,
+
+ // js_ctx_id is incremented each time the loop is reset for JS.
+ // All JS callbacks store an initial js_ctx_id and compare before execution.
+ // If a ctx is outdated, the callback is ignored.
+ // This is a weak way to cancel all future JS callbacks.
+ js_ctx_id: u32 = 0,
+
+ // zig_ctx_id is incremented each time the loop is reset for Zig.
+ // All Zig callbacks store an initial zig_ctx_id and compare before execution.
+ // If a ctx is outdated, the callback is ignored.
+ // This is a weak way to cancel all future Zig callbacks.
+ zig_ctx_id: u32 = 0,
+
+ // The MacOS event loop doesn't support cancellation. We use this to track
+ // cancellation ids and, on the timeout callback, we can can check here
+ // to see if it's been cancelled.
+ cancelled: std.AutoHashMapUnmanaged(usize, void),
+
+ cancel_pool: MemoryPool(ContextCancel),
+ timeout_pool: MemoryPool(ContextTimeout),
+ event_callback_pool: MemoryPool(EventCallbackContext),
+
+ const Self = @This();
+ pub const Completion = IO.Completion;
+
+ pub const ConnectError = IO.ConnectError;
+ pub const RecvError = IO.RecvError;
+ pub const SendError = IO.SendError;
+
+ pub fn init(alloc: std.mem.Allocator) !Self {
+ return Self{
+ .alloc = alloc,
+ .cancelled = .{},
+ .io = try IO.init(32, 0),
+ .js_events_nb = 0,
+ .zig_events_nb = 0,
+ .cancel_pool = MemoryPool(ContextCancel).init(alloc),
+ .timeout_pool = MemoryPool(ContextTimeout).init(alloc),
+ .event_callback_pool = MemoryPool(EventCallbackContext).init(alloc),
+ };
+ }
+
+ pub fn deinit(self: *Self) void {
+ // first disable callbacks for existing events.
+ // We don't want a callback re-create a setTimeout, it could create an
+ // infinite loop on wait for events.
+ self.resetJS();
+ self.resetZig();
+
+ // run tail events. We do run the tail events to ensure all the
+ // contexts are correcly free.
+ while (self.eventsNb(.js) > 0 or self.eventsNb(.zig) > 0) {
+ self.io.run_for_ns(10 * std.time.ns_per_ms) catch |err| {
+ log.err("deinit run tail events: {any}", .{err});
+ break;
+ };
+ }
+ if (comptime CANCEL_SUPPORTED) {
+ self.io.cancel_all();
+ }
+ self.io.deinit();
+ self.cancel_pool.deinit();
+ self.timeout_pool.deinit();
+ self.event_callback_pool.deinit();
+ self.cancelled.deinit(self.alloc);
+ }
+
+ // Retrieve all registred I/O events completed by OS kernel,
+ // and execute sequentially their callbacks.
+ // Stops when there is no more I/O events registered on the loop.
+ // Note that I/O events callbacks might register more I/O events
+ // on the go when they are executed (ie. nested I/O events).
+ pub fn run(self: *Self) !void {
+ while (self.eventsNb(.js) > 0) {
+ try self.io.run_for_ns(10 * std.time.ns_per_ms);
+ // at each iteration we might have new events registred by previous callbacks
+ }
+ // TODO: return instead immediatly on the first JS callback error
+ // and let the caller decide what to do next
+ // (typically retrieve the exception through the TryCatch and
+ // continue the execution of callbacks with a new call to loop.run)
+ if (self.cbk_error) {
+ return error.JSExecCallback;
+ }
+ }
+
+ const Event = enum { js, zig };
+
+ fn eventsPtr(self: *Self, comptime event: Event) *usize {
+ return switch (event) {
+ .zig => &self.zig_events_nb,
+ .js => &self.js_events_nb,
+ };
+ }
+
+ // Register events atomically
+ // - add 1 event and return previous value
+ fn addEvent(self: *Self, comptime event: Event) void {
+ _ = @atomicRmw(usize, self.eventsPtr(event), .Add, 1, .acq_rel);
+ }
+ // - remove 1 event and return previous value
+ fn removeEvent(self: *Self, comptime event: Event) void {
+ _ = @atomicRmw(usize, self.eventsPtr(event), .Sub, 1, .acq_rel);
+ }
+ // - get the number of current events
+ fn eventsNb(self: *Self, comptime event: Event) usize {
+ return @atomicLoad(usize, self.eventsPtr(event), .seq_cst);
+ }
+
+ // JS callbacks APIs
+ // -----------------
+
+ // Timeout
+
+ const ContextTimeout = struct {
+ loop: *Self,
+ js_cbk: ?JSCallback,
+ js_ctx_id: u32,
+ };
+
+ fn timeoutCallback(
+ ctx: *ContextTimeout,
+ completion: *IO.Completion,
+ result: IO.TimeoutError!void,
+ ) void {
+ const loop = ctx.loop;
+ defer {
+ loop.removeEvent(.js);
+ loop.timeout_pool.destroy(ctx);
+ loop.alloc.destroy(completion);
+ }
+
+ if (comptime CANCEL_SUPPORTED == false) {
+ if (loop.cancelled.remove(@intFromPtr(completion))) {
+ return;
+ }
+ }
+
+ // If the loop's context id has changed, don't call the js callback
+ // function. The callback's memory has already be cleaned and the
+ // events nb reset.
+ if (ctx.js_ctx_id != loop.js_ctx_id) return;
+
+ // TODO: return the error to the callback
+ result catch |err| {
+ switch (err) {
+ error.Canceled => {},
+ else => log.err("timeout callback: {any}", .{err}),
+ }
+ return;
+ };
+
+ // js callback
+ if (ctx.js_cbk) |*js_cbk| {
+ js_cbk.call(null) catch {
+ loop.cbk_error = true;
+ };
+ }
+ }
+
+ pub fn timeout(self: *Self, nanoseconds: u63, js_cbk: ?JSCallback) !usize {
+ const completion = try self.alloc.create(Completion);
+ errdefer self.alloc.destroy(completion);
+ completion.* = undefined;
+
+ const ctx = try self.timeout_pool.create();
+ errdefer self.timeout_pool.destroy(ctx);
+ ctx.* = ContextTimeout{
+ .loop = self,
+ .js_cbk = js_cbk,
+ .js_ctx_id = self.js_ctx_id,
+ };
+
+ self.addEvent(.js);
+ self.io.timeout(*ContextTimeout, ctx, timeoutCallback, completion, nanoseconds);
+ return @intFromPtr(completion);
+ }
+
+ const ContextCancel = struct {
+ loop: *Self,
+ js_cbk: ?JSCallback,
+ js_ctx_id: u32,
+ };
+
+ fn cancelCallback(
+ ctx: *ContextCancel,
+ completion: *IO.Completion,
+ result: IO.CancelOneError!void,
+ ) void {
+ const loop = ctx.loop;
+
+ defer {
+ loop.removeEvent(.js);
+ loop.cancel_pool.destroy(ctx);
+ loop.alloc.destroy(completion);
+ }
+
+ // If the loop's context id has changed, don't call the js callback
+ // function. The callback's memory has already be cleaned and the
+ // events nb reset.
+ if (ctx.js_ctx_id != loop.js_ctx_id) return;
+
+ // TODO: return the error to the callback
+ result catch |err| {
+ switch (err) {
+ error.NotFound => log.debug("cancel callback: {any}", .{err}),
+ else => log.err("cancel callback: {any}", .{err}),
+ }
+ return;
+ };
+
+ // js callback
+ if (ctx.js_cbk) |*js_cbk| {
+ js_cbk.call(null) catch {
+ loop.cbk_error = true;
+ };
+ }
+ }
+
+ pub fn cancel(self: *Self, id: usize, js_cbk: ?JSCallback) !void {
+ const alloc = self.alloc;
+ if (comptime CANCEL_SUPPORTED == false) {
+ try self.cancelled.put(alloc, id, {});
+ if (js_cbk) |cbk| {
+ cbk.call(null) catch {
+ self.cbk_error = true;
+ };
+ }
+ return;
+ }
+ const comp_cancel: *IO.Completion = @ptrFromInt(id);
+
+ const completion = try alloc.create(Completion);
+ errdefer alloc.destroy(completion);
+ completion.* = undefined;
+
+ const ctx = self.alloc.create(ContextCancel) catch unreachable;
+ ctx.* = ContextCancel{
+ .loop = self,
+ .js_cbk = js_cbk,
+ .js_ctx_id = self.js_ctx_id,
+ };
+
+ self.addEvent(.js);
+ self.io.cancel_one(*ContextCancel, ctx, cancelCallback, completion, comp_cancel);
+ }
+
+ // Reset all existing JS callbacks.
+ // The existing events will happen and their memory will be cleanup but the
+ // corresponding callbacks will not be called.
+ pub fn resetJS(self: *Self) void {
+ self.js_ctx_id += 1;
+ self.cancelled.clearRetainingCapacity();
+ }
+
+ // Reset all existing Zig callbacks.
+ // The existing events will happen and their memory will be cleanup but the
+ // corresponding callbacks will not be called.
+ pub fn resetZig(self: *Self) void {
+ self.zig_ctx_id += 1;
+ }
+
+ // IO callbacks APIs
+ // -----------------
+
+ // Connect
+
+ pub fn connect(
+ self: *Self,
+ comptime Ctx: type,
+ ctx: *Ctx,
+ completion: *Completion,
+ comptime cbk: fn (ctx: *Ctx, _: *Completion, res: ConnectError!void) void,
+ socket: std.posix.socket_t,
+ address: std.net.Address,
+ ) !void {
+ const onConnect = struct {
+ fn onConnect(callback: *EventCallbackContext, completion_: *Completion, res: ConnectError!void) void {
+ defer callback.loop.event_callback_pool.destroy(callback);
+ callback.loop.removeEvent(.js);
+ cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res);
+ }
+ }.onConnect;
+
+ const callback = try self.event_callback_pool.create();
+ errdefer self.event_callback_pool.destroy(callback);
+ callback.* = .{ .loop = self, .ctx = ctx };
+
+ self.addEvent(.js);
+ self.io.connect(*EventCallbackContext, callback, onConnect, completion, socket, address);
+ }
+
+ // Send
+
+ pub fn send(
+ self: *Self,
+ comptime Ctx: type,
+ ctx: *Ctx,
+ completion: *Completion,
+ comptime cbk: fn (ctx: *Ctx, completion: *Completion, res: SendError!usize) void,
+ socket: std.posix.socket_t,
+ buf: []const u8,
+ ) !void {
+ const onSend = struct {
+ fn onSend(callback: *EventCallbackContext, completion_: *Completion, res: SendError!usize) void {
+ defer callback.loop.event_callback_pool.destroy(callback);
+ callback.loop.removeEvent(.js);
+ cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res);
+ }
+ }.onSend;
+
+ const callback = try self.event_callback_pool.create();
+ errdefer self.event_callback_pool.destroy(callback);
+ callback.* = .{ .loop = self, .ctx = ctx };
+
+ self.addEvent(.js);
+ self.io.send(*EventCallbackContext, callback, onSend, completion, socket, buf);
+ }
+
+ // Recv
+
+ pub fn recv(
+ self: *Self,
+ comptime Ctx: type,
+ ctx: *Ctx,
+ completion: *Completion,
+ comptime cbk: fn (ctx: *Ctx, completion: *Completion, res: RecvError!usize) void,
+ socket: std.posix.socket_t,
+ buf: []u8,
+ ) !void {
+ const onRecv = struct {
+ fn onRecv(callback: *EventCallbackContext, completion_: *Completion, res: RecvError!usize) void {
+ defer callback.loop.event_callback_pool.destroy(callback);
+ callback.loop.removeEvent(.js);
+ cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res);
+ }
+ }.onRecv;
+
+ const callback = try self.event_callback_pool.create();
+ errdefer self.event_callback_pool.destroy(callback);
+ callback.* = .{ .loop = self, .ctx = ctx };
+
+ self.addEvent(.js);
+ self.io.recv(*EventCallbackContext, callback, onRecv, completion, socket, buf);
+ }
+
+ // Zig timeout
+
+ const ContextZigTimeout = struct {
+ loop: *Self,
+ zig_ctx_id: u32,
+
+ context: *anyopaque,
+ callback: *const fn (
+ context: ?*anyopaque,
+ ) void,
+ };
+
+ fn zigTimeoutCallback(
+ ctx: *ContextZigTimeout,
+ completion: *IO.Completion,
+ result: IO.TimeoutError!void,
+ ) void {
+ const loop = ctx.loop;
+ defer {
+ loop.removeEvent(.zig);
+ loop.alloc.destroy(ctx);
+ loop.alloc.destroy(completion);
+ }
+
+ // If the loop's context id has changed, don't call the js callback
+ // function. The callback's memory has already be cleaned and the
+ // events nb reset.
+ if (ctx.zig_ctx_id != loop.zig_ctx_id) return;
+
+ result catch |err| {
+ switch (err) {
+ error.Canceled => {},
+ else => log.err("zig timeout callback: {any}", .{err}),
+ }
+ return;
+ };
+
+ // callback
+ ctx.callback(ctx.context);
+ }
+
+ // zigTimeout performs a timeout but the callback is a zig function.
+ pub fn zigTimeout(
+ self: *Self,
+ nanoseconds: u63,
+ comptime Context: type,
+ context: Context,
+ comptime callback: fn (context: Context) void,
+ ) void {
+ const completion = self.alloc.create(IO.Completion) catch unreachable;
+ completion.* = undefined;
+ const ctxtimeout = self.alloc.create(ContextZigTimeout) catch unreachable;
+ ctxtimeout.* = ContextZigTimeout{
+ .loop = self,
+ .zig_ctx_id = self.zig_ctx_id,
+ .context = context,
+ .callback = struct {
+ fn wrapper(ctx: ?*anyopaque) void {
+ callback(@ptrCast(@alignCast(ctx)));
+ }
+ }.wrapper,
+ };
+
+ self.addEvent(.zig);
+ self.io.timeout(*ContextZigTimeout, ctxtimeout, zigTimeoutCallback, completion, nanoseconds);
+ }
+};
+
+const EventCallbackContext = struct {
+ ctx: *anyopaque,
+ loop: *Loop,
+};
+
+const CANCEL_SUPPORTED = switch (builtin.target.os.tag) {
+ .linux => true,
+ .macos, .tvos, .watchos, .ios => false,
+ else => @compileError("IO is not supported for platform"),
+};
diff --git a/src/runtime/test_complex_types.zig b/src/runtime/test_complex_types.zig
new file mode 100644
index 00000000..9794ad2b
--- /dev/null
+++ b/src/runtime/test_complex_types.zig
@@ -0,0 +1,260 @@
+// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const MyList = struct {
+ items: []u8,
+
+ pub fn constructor(elem1: u8, elem2: u8, elem3: u8, state: State) MyList {
+ var items = state.arena.alloc(u8, 3) catch unreachable;
+ items[0] = elem1;
+ items[1] = elem2;
+ items[2] = elem3;
+ return .{ .items = items };
+ }
+
+ pub fn _first(self: *const MyList) u8 {
+ return self.items[0];
+ }
+
+ pub fn _symbol_iterator(self: *const MyList) IterableU8 {
+ return IterableU8.init(self.items);
+ }
+};
+
+const MyVariadic = struct {
+ member: u8,
+
+ pub fn constructor() MyVariadic {
+ return .{ .member = 0 };
+ }
+
+ pub fn _len(_: *const MyVariadic, variadic: []bool) u64 {
+ return @as(u64, variadic.len);
+ }
+
+ pub fn _first(_: *const MyVariadic, _: []const u8, variadic: []bool) bool {
+ return variadic[0];
+ }
+
+ pub fn _last(_: *const MyVariadic, variadic: []bool) bool {
+ return variadic[variadic.len - 1];
+ }
+
+ pub fn _empty(_: *const MyVariadic, _: []bool) bool {
+ return true;
+ }
+
+ pub fn _myListLen(_: *const MyVariadic, variadic: []*const MyList) u8 {
+ return @as(u8, @intCast(variadic.len));
+ }
+
+ pub fn _myListFirst(_: *const MyVariadic, variadic: []*const MyList) ?u8 {
+ if (variadic.len == 0) return null;
+ return variadic[0]._first();
+ }
+};
+
+const MyErrorUnion = struct {
+ pub fn constructor(is_err: bool) !MyErrorUnion {
+ if (is_err) return error.MyError;
+ return .{};
+ }
+
+ pub fn get_withoutError(_: *const MyErrorUnion) !u8 {
+ return 0;
+ }
+
+ pub fn get_withError(_: *const MyErrorUnion) !u8 {
+ return error.MyError;
+ }
+
+ pub fn set_withoutError(_: *const MyErrorUnion, _: bool) !void {}
+
+ pub fn set_withError(_: *const MyErrorUnion, _: bool) !void {
+ return error.MyError;
+ }
+
+ pub fn _funcWithoutError(_: *const MyErrorUnion) !void {}
+
+ pub fn _funcWithError(_: *const MyErrorUnion) !void {
+ return error.MyError;
+ }
+};
+
+pub const MyException = struct {
+ err: ErrorSet,
+
+ const errorNames = [_][]const u8{
+ "MyCustomError",
+ };
+ const errorMsgs = [_][]const u8{
+ "Some custom message.",
+ };
+ fn errorStrings(comptime i: usize) []const u8 {
+ return errorNames[0] ++ ": " ++ errorMsgs[i];
+ }
+
+ // interface definition
+
+ pub const ErrorSet = error{
+ MyCustomError,
+ };
+
+ pub fn init(_: Allocator, err: anyerror, _: []const u8) !MyException {
+ return .{ .err = @as(ErrorSet, @errorCast(err)) };
+ }
+
+ pub fn get_name(self: *const MyException) []const u8 {
+ return switch (self.err) {
+ ErrorSet.MyCustomError => errorNames[0],
+ };
+ }
+
+ pub fn get_message(self: *const MyException) []const u8 {
+ return switch (self.err) {
+ ErrorSet.MyCustomError => errorMsgs[0],
+ };
+ }
+
+ pub fn _toString(self: *const MyException) []const u8 {
+ return switch (self.err) {
+ ErrorSet.MyCustomError => errorStrings(0),
+ };
+ }
+};
+
+const MyTypeWithException = struct {
+ pub const Exception = MyException;
+
+ pub fn constructor() MyTypeWithException {
+ return .{};
+ }
+
+ pub fn _withoutError(_: *const MyTypeWithException) MyException.ErrorSet!void {}
+
+ pub fn _withError(_: *const MyTypeWithException) MyException.ErrorSet!void {
+ return MyException.ErrorSet.MyCustomError;
+ }
+
+ pub fn _superSetError(_: *const MyTypeWithException) !void {
+ return MyException.ErrorSet.MyCustomError;
+ }
+
+ pub fn _outOfMemory(_: *const MyTypeWithException) !void {
+ return error.OutOfMemory;
+ }
+};
+
+const IterableU8 = Iterable(u8);
+
+pub fn Iterable(comptime T: type) type {
+ return struct {
+ const Self = @This();
+
+ items: []T,
+ index: usize = 0,
+
+ pub fn init(items: []T) Self {
+ return .{ .items = items };
+ }
+
+ pub const Return = struct {
+ value: ?T,
+ done: bool,
+ };
+
+ pub fn _next(self: *Self) Return {
+ if (self.items.len > self.index) {
+ const val = self.items[self.index];
+ self.index += 1;
+ return .{ .value = val, .done = false };
+ } else {
+ return .{ .value = null, .done = true };
+ }
+ }
+ };
+}
+
+const State = struct {
+ arena: Allocator,
+};
+
+const testing = @import("testing.zig");
+test "JS: complex types" {
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+
+ var runner = try testing.Runner(State, void, .{
+ MyList,
+ IterableU8,
+ MyVariadic,
+ MyErrorUnion,
+ MyException,
+ MyTypeWithException,
+ }).init(.{ .arena = arena.allocator() }, {});
+
+ defer runner.deinit();
+
+ try runner.testCases(&.{
+ .{ "let myList = new MyList(1, 2, 3);", "undefined" },
+ .{ "myList.first();", "1" },
+ .{ "let iter = myList[Symbol.iterator]();", "undefined" },
+ .{ "iter.next().value;", "1" },
+ .{ "iter.next().value;", "2" },
+ .{ "iter.next().value;", "3" },
+ .{ "iter.next().done;", "true" },
+ .{ "let arr = Array.from(myList);", "undefined" },
+ .{ "arr.length;", "3" },
+ .{ "arr[0];", "1" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "let myVariadic = new MyVariadic();", "undefined" },
+ .{ "myVariadic.len(true, false, true)", "3" },
+ .{ "myVariadic.first('a_str', true, false, true, false)", "true" },
+ .{ "myVariadic.last(true, false)", "false" },
+ .{ "myVariadic.empty()", "true" },
+ .{ "myVariadic.myListLen(myList)", "1" },
+ .{ "myVariadic.myListFirst(myList)", "1" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "var myErrorCstr = ''; try {new MyErrorUnion(true)} catch (error) {myErrorCstr = error}; myErrorCstr", "Error: MyError" },
+ .{ "let myErrorUnion = new MyErrorUnion(false);", "undefined" },
+ .{ "myErrorUnion.withoutError", "0" },
+ .{ "var myErrorGetter = ''; try {myErrorUnion.withError} catch (error) {myErrorGetter = error}; myErrorGetter", "Error: MyError" },
+ .{ "myErrorUnion.withoutError = true", "true" },
+ .{ "var myErrorSetter = ''; try {myErrorUnion.withError = true} catch (error) {myErrorSetter = error}; myErrorSetter", "Error: MyError" },
+ .{ "myErrorUnion.funcWithoutError()", "undefined" },
+ .{ "var myErrorFunc = ''; try {myErrorUnion.funcWithError()} catch (error) {myErrorFunc = error}; myErrorFunc", "Error: MyError" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "MyException.prototype.__proto__ === Error.prototype", "true" },
+ .{ "let myTypeWithException = new MyTypeWithException();", "undefined" },
+ .{ "myTypeWithException.withoutError()", "undefined" },
+ .{ "var myCustomError = ''; try {myTypeWithException.withError()} catch (error) {myCustomError = error}", "MyCustomError: Some custom message." },
+ .{ "myCustomError instanceof MyException", "true" },
+ .{ "myCustomError instanceof Error", "true" },
+ .{ "var mySuperError = ''; try {myTypeWithException.superSetError()} catch (error) {mySuperError = error}", "MyCustomError: Some custom message." },
+ .{ "var oomError = ''; try {myTypeWithException.outOfMemory()} catch (error) {oomError = error}; oomError", "Error: out of memory" },
+ }, .{});
+}
diff --git a/src/runtime/test_object_types.zig b/src/runtime/test_object_types.zig
new file mode 100644
index 00000000..b3594dc0
--- /dev/null
+++ b/src/runtime/test_object_types.zig
@@ -0,0 +1,123 @@
+// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+pub const Other = struct {
+ val: u8,
+
+ fn init(val: u8) Other {
+ return .{ .val = val };
+ }
+
+ pub fn _val(self: *const Other) u8 {
+ return self.val;
+ }
+};
+
+pub const OtherUnion = union(enum) {
+ Other: Other,
+ Bool: bool,
+};
+
+pub const MyObject = struct {
+ val: bool,
+
+ pub fn constructor(do_set: bool) MyObject {
+ return .{
+ .val = do_set,
+ };
+ }
+
+ pub fn named_get(_: *const MyObject, name: []const u8, has_value: *bool) ?OtherUnion {
+ if (std.mem.eql(u8, name, "a")) {
+ has_value.* = true;
+ return .{ .Other = .{ .val = 4 } };
+ }
+ if (std.mem.eql(u8, name, "c")) {
+ has_value.* = true;
+ return .{ .Bool = true };
+ }
+ has_value.* = false;
+ return null;
+ }
+
+ pub fn get_val(self: *const MyObject) bool {
+ return self.val;
+ }
+
+ pub fn set_val(self: *MyObject, val: bool) void {
+ self.val = val;
+ }
+};
+
+pub const MyAPI = struct {
+ pub fn constructor() MyAPI {
+ return .{};
+ }
+
+ pub fn _obj(_: *const MyAPI) !MyObject {
+ return MyObject.constructor(true);
+ }
+};
+
+const State = struct {
+ arena: Allocator,
+};
+
+const testing = @import("testing.zig");
+test "JS: object types" {
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+
+ var runner = try testing.Runner(State, void, .{
+ Other,
+ MyObject,
+ MyAPI,
+ }).init(.{ .arena = arena.allocator() }, {});
+
+ defer runner.deinit();
+
+ // v8 has 5 default "own" properties
+ const own_base = "5";
+
+ try runner.testCases(&.{
+ .{ "Object.getOwnPropertyNames(MyObject).length;", own_base },
+ .{ "let myObj = new MyObject(true);", "undefined" },
+ // check object property
+ .{ "myObj.a.val()", "4" },
+ .{ "myObj.b", "undefined" },
+ .{ "Object.getOwnPropertyNames(myObj).length;", "0" },
+
+ // check if setter (pointer) still works
+ .{ "myObj.val", "true" },
+ .{ "myObj.val = false", "false" },
+ .{ "myObj.val", "false" },
+
+ .{ "let myObj2 = new MyObject(false);", "undefined" },
+ .{ "myObj2.c", "true" },
+ }, .{});
+
+ try runner.testCases(&.{
+ .{ "let myAPI = new MyAPI();", "undefined" },
+ .{ "let myObjIndirect = myAPI.obj();", "undefined" },
+ // check object property
+ .{ "myObjIndirect.a.val()", "4" },
+ }, .{});
+}
diff --git a/src/runtime/test_primitive_types.zig b/src/runtime/test_primitive_types.zig
new file mode 100644
index 00000000..022a8c91
--- /dev/null
+++ b/src/runtime/test_primitive_types.zig
@@ -0,0 +1,176 @@
+// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+
+// TODO: use functions instead of "fake" struct once we handle function API generation
+const Primitives = struct {
+ pub fn constructor() Primitives {
+ return .{};
+ }
+
+ // List of bytes (string)
+ pub fn _checkString(_: *const Primitives, v: []u8) []u8 {
+ return v;
+ }
+
+ // Integers signed
+
+ pub fn _checkI32(_: *const Primitives, v: i32) i32 {
+ return v;
+ }
+
+ pub fn _checkI64(_: *const Primitives, v: i64) i64 {
+ return v;
+ }
+
+ // Integers unsigned
+
+ pub fn _checkU32(_: *const Primitives, v: u32) u32 {
+ return v;
+ }
+
+ pub fn _checkU64(_: *const Primitives, v: u64) u64 {
+ return v;
+ }
+
+ // Floats
+
+ pub fn _checkF32(_: *const Primitives, v: f32) f32 {
+ return v;
+ }
+
+ pub fn _checkF64(_: *const Primitives, v: f64) f64 {
+ return v;
+ }
+
+ // Bool
+ pub fn _checkBool(_: *const Primitives, v: bool) bool {
+ return v;
+ }
+
+ // Undefined
+ // TODO: there is a bug with this function
+ // void paramater does not work => avoid for now
+ // pub fn _checkUndefined(_: *const Primitives, v: void) void {
+ // return v;
+ // }
+
+ // Null
+ pub fn _checkNullEmpty(_: *const Primitives, v: ?u32) bool {
+ return (v == null);
+ }
+ pub fn _checkNullNotEmpty(_: *const Primitives, v: ?u32) bool {
+ return (v != null);
+ }
+
+ // Optionals
+ pub fn _checkOptional(_: *const Primitives, _: ?u8, v: u8, _: ?u8, _: ?u8) u8 {
+ return v;
+ }
+ pub fn _checkNonOptional(_: *const Primitives, v: u8) u8 {
+ std.debug.print("x: {d}\n", .{v});
+ return v;
+ }
+ pub fn _checkOptionalReturn(_: *const Primitives) ?bool {
+ return true;
+ }
+ pub fn _checkOptionalReturnNull(_: *const Primitives) ?bool {
+ return null;
+ }
+ pub fn _checkOptionalReturnString(_: *const Primitives) ?[]const u8 {
+ return "ok";
+ }
+};
+
+const testing = @import("testing.zig");
+test "JS: primitive types" {
+ var runner = try testing.Runner(void, void, .{Primitives}).init({}, {});
+ defer runner.deinit();
+
+ // constructor
+ try runner.testCases(&.{
+ .{ "let p = new Primitives();", "undefined" },
+ }, .{});
+
+ // JS <> Native translation of primitive types
+ try runner.testCases(&.{
+ .{ "p.checkString('ok ascii') === 'ok ascii';", "true" },
+ .{ "p.checkString('ok emoji π') === 'ok emoji π';", "true" },
+ .{ "p.checkString('ok chinese ιΏ') === 'ok chinese ιΏ';", "true" },
+
+ // String (JS liberal cases)
+ .{ "p.checkString(1) === '1';", "true" },
+ .{ "p.checkString(null) === 'null';", "true" },
+ .{ "p.checkString(undefined) === 'undefined';", "true" },
+
+ // Integers
+
+ // signed
+ .{ "const min_i32 = -2147483648", "undefined" },
+ .{ "p.checkI32(min_i32) === min_i32;", "true" },
+ .{ "p.checkI32(min_i32-1) === min_i32-1;", "false" },
+ .{ "try { p.checkI32(9007199254740995n) } catch(e) { e instanceof TypeError; }", "true" },
+
+ // unsigned
+ .{ "const max_u32 = 4294967295", "undefined" },
+ .{ "p.checkU32(max_u32) === max_u32;", "true" },
+ .{ "p.checkU32(max_u32+1) === max_u32+1;", "false" },
+
+ // int64 (with BigInt)
+ .{ "const big_int = 9007199254740995n", "undefined" },
+ .{ "p.checkI64(big_int) === big_int", "true" },
+ .{ "p.checkU64(big_int) === big_int;", "true" },
+ .{ "p.checkI64(0) === 0;", "true" },
+ .{ "p.checkI64(-1) === -1;", "true" },
+ .{ "p.checkU64(0) === 0;", "true" },
+
+ // Floats
+ // use round 2 decimals for float to ensure equality
+ .{ "const r = function(x) {return Math.round(x * 100) / 100};", "undefined" },
+ .{ "const double = 10.02;", "undefined" },
+ .{ "r(p.checkF32(double)) === double;", "true" },
+ .{ "r(p.checkF64(double)) === double;", "true" },
+
+ // Bool
+ .{ "p.checkBool(true);", "true" },
+ .{ "p.checkBool(false);", "false" },
+ .{ "p.checkBool(0);", "false" },
+ .{ "p.checkBool(1);", "true" },
+
+ // Bool (JS liberal cases)
+ .{ "p.checkBool(null);", "false" },
+ .{ "p.checkBool(undefined);", "false" },
+
+ // Undefined
+ // see TODO on Primitives.checkUndefined
+ // .{ "p.checkUndefined(undefined) === undefined;", "true" },
+
+ // Null
+ .{ "p.checkNullEmpty(null);", "true" },
+ .{ "p.checkNullEmpty(undefined);", "true" },
+ .{ "p.checkNullNotEmpty(1);", "true" },
+
+ // Optional
+ .{ "p.checkOptional(null, 3);", "3" },
+ .{ "p.checkNonOptional();", "TypeError" },
+ .{ "p.checkOptionalReturn() === true;", "true" },
+ .{ "p.checkOptionalReturnNull() === null;", "true" },
+ .{ "p.checkOptionalReturnString() === 'ok';", "true" },
+ }, .{});
+}
diff --git a/src/runtime/testing.zig b/src/runtime/testing.zig
new file mode 100644
index 00000000..2a09b2e8
--- /dev/null
+++ b/src/runtime/testing.zig
@@ -0,0 +1,104 @@
+// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+const js = @import("js.zig");
+const generate = @import("generate.zig");
+
+pub const allocator = std.testing.allocator;
+
+// Very similar to the JSRunner in src/testing.zig, but it isn't tied to the
+// browser.Env or the browser.SessionState
+pub fn Runner(comptime State: type, comptime Global: type, comptime types: anytype) type {
+ const AdjustedTypes = if (Global == void) generate.Tuple(.{ types, DefaultGlobal }) else types;
+ const Env = js.Env(State, AdjustedTypes{});
+
+ return struct {
+ env: *Env,
+ executor: *Env.Executor,
+
+ const Self = @This();
+
+ pub fn init(state: State, global: Global) !*Self {
+ const runner = try allocator.create(Self);
+ errdefer allocator.destroy(runner);
+
+ runner.env = try Env.init(allocator, .{});
+ errdefer runner.env.deinit();
+
+ const G = if (Global == void) DefaultGlobal else Global;
+
+ runner.executor = try runner.env.startExecutor(G, state, runner);
+ errdefer runner.env.stopExecutor(runner.executor);
+
+ try runner.executor.startScope(if (Global == void) &default_global else global);
+ return runner;
+ }
+
+ pub fn deinit(self: *Self) void {
+ self.executor.endScope();
+ self.env.stopExecutor(self.executor);
+ self.env.deinit();
+ allocator.destroy(self);
+ }
+
+ const RunOpts = struct {};
+ pub const Case = std.meta.Tuple(&.{ []const u8, []const u8 });
+ pub fn testCases(self: *Self, cases: []const Case, _: RunOpts) !void {
+ for (cases, 0..) |case, i| {
+ var try_catch: Env.TryCatch = undefined;
+ try_catch.init(self.executor);
+ defer try_catch.deinit();
+
+ const value = self.executor.exec(case.@"0", null) catch |err| {
+ if (try try_catch.err(allocator)) |msg| {
+ defer allocator.free(msg);
+ if (isExpectedTypeError(case.@"1", msg)) {
+ continue;
+ }
+ std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" });
+ }
+ return err;
+ };
+
+ const actual = try value.toString(allocator);
+ defer allocator.free(actual);
+ if (std.mem.eql(u8, case.@"1", actual) == false) {
+ std.debug.print("Expected:\n{s}\n\nGot:\n{s}\n\nCase: {d}\n{s}\n", .{ case.@"1", actual, i + 1, case.@"0" });
+ return error.UnexpectedResult;
+ }
+ }
+ }
+
+ pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
+ _ = ctx;
+ _ = specifier;
+ return error.DummyModuleLoader;
+ }
+ };
+}
+
+fn isExpectedTypeError(expected: []const u8, msg: []const u8) bool {
+ if (!std.mem.eql(u8, expected, "TypeError")) {
+ return false;
+ }
+ return std.mem.startsWith(u8, msg, "TypeError: ");
+}
+
+var default_global = DefaultGlobal{};
+const DefaultGlobal = struct {};
diff --git a/src/server.zig b/src/server.zig
index a071b441..471c2cf2 100644
--- a/src/server.zig
+++ b/src/server.zig
@@ -25,14 +25,15 @@ const posix = std.posix;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
-const jsruntime = @import("jsruntime");
-const Completion = jsruntime.IO.Completion;
-const AcceptError = jsruntime.IO.AcceptError;
-const RecvError = jsruntime.IO.RecvError;
-const SendError = jsruntime.IO.SendError;
-const CloseError = jsruntime.IO.CloseError;
-const CancelError = jsruntime.IO.CancelOneError;
-const TimeoutError = jsruntime.IO.TimeoutError;
+const IO = @import("runtime/loop.zig").IO;
+const Completion = IO.Completion;
+const AcceptError = IO.AcceptError;
+const RecvError = IO.RecvError;
+const SendError = IO.SendError;
+const CloseError = IO.CloseError;
+const CancelError = IO.CancelOneError;
+const TimeoutError = IO.TimeoutError;
+const Loop = @import("runtime/loop.zig").Loop;
const App = @import("app.zig").App;
const CDP = @import("cdp/cdp.zig").CDP;
@@ -51,7 +52,7 @@ const MAX_MESSAGE_SIZE = 256 * 1024 + 14;
const Server = struct {
app: *App,
allocator: Allocator,
- loop: *jsruntime.Loop,
+ loop: *Loop,
// internal fields
listener: posix.socket_t,
@@ -453,7 +454,7 @@ pub const Client = struct {
};
self.mode = .websocket;
- self.cdp = CDP.init(self.server.app, self);
+ self.cdp = try CDP.init(self.server.app, self);
return self.send(arena, response);
}
@@ -1023,10 +1024,6 @@ pub fn run(
try posix.bind(listener, &address.any, address.getOsSockLen());
try posix.listen(listener, 1);
- // create v8 vm
- const vm = jsruntime.VM.init();
- defer vm.deinit();
-
var loop = app.loop;
const allocator = app.allocator;
const json_version_response = try buildJSONVersionResponse(allocator, address);
@@ -1451,7 +1448,7 @@ const MockCDP = struct {
allocator: Allocator = testing.allocator,
- fn init(_: Allocator, client: anytype, loop: *jsruntime.Loop) MockCDP {
+ fn init(_: Allocator, client: anytype, loop: *Loop) MockCDP {
_ = loop;
_ = client;
return .{};
diff --git a/src/test_runner.zig b/src/test_runner.zig
index f43b5735..c7e2ad0d 100644
--- a/src/test_runner.zig
+++ b/src/test_runner.zig
@@ -26,10 +26,6 @@ const BORDER = "=" ** 80;
// use in custom panic handler
var current_test: ?[]const u8 = null;
-const jsruntime = @import("jsruntime");
-pub const Types = jsruntime.reflect(@import("generate.zig").Tuple(.{}){});
-pub const UserContext = @import("user_context.zig").UserContext;
-
pub const std_options = std.Options{
.log_level = .warn,
@@ -38,6 +34,9 @@ pub const std_options = std.Options{
.side_channels_mitigations = .none,
};
+pub var js_runner_duration: usize = 0;
+pub var tracking_allocator = TrackingAllocator.init(std.testing.allocator);
+
pub fn main() !void {
var mem: [8192]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&mem);
@@ -50,6 +49,19 @@ pub fn main() !void {
var slowest = SlowTracker.init(allocator, 5);
defer slowest.deinit();
+ var args = try std.process.argsWithAllocator(allocator);
+ defer args.deinit();
+
+ // ignore the exec name.
+ _ = args.next();
+ var json_stats = false;
+ while (args.next()) |arg| {
+ if (std.mem.eql(u8, "--json", arg)) {
+ json_stats = true;
+ continue;
+ }
+ }
+
var pass: usize = 0;
var fail: usize = 0;
var skip: usize = 0;
@@ -159,6 +171,38 @@ pub fn main() !void {
printer.fmt("\n", .{});
try slowest.display(printer);
printer.fmt("\n", .{});
+
+ // TODO: at the very least, `browser` should return real stats
+ if (json_stats) {
+ const stats = tracking_allocator.stats();
+ try std.json.stringify(&.{
+ .{ .name = "browser", .bench = .{
+ .duration = js_runner_duration,
+ .alloc_nb = stats.allocation_count,
+ .realloc_nb = stats.reallocation_count,
+ .alloc_size = stats.allocated_bytes,
+ } },
+ .{ .name = "libdom", .bench = .{
+ .duration = js_runner_duration,
+ .alloc_nb = 0,
+ .realloc_nb = 0,
+ .alloc_size = 0,
+ } },
+ .{ .name = "v8", .bench = .{
+ .duration = js_runner_duration,
+ .alloc_nb = 0,
+ .realloc_nb = 0,
+ .alloc_size = 0,
+ } },
+ .{ .name = "main", .bench = .{
+ .duration = js_runner_duration,
+ .alloc_nb = 0,
+ .realloc_nb = 0,
+ .alloc_size = 0,
+ } },
+ }, .{ .whitespace = .indent_2 }, std.io.getStdOut().writer());
+ }
+
std.posix.exit(if (fail == 0) 0 else 1);
}
@@ -335,3 +379,90 @@ fn isSetup(t: std.builtin.TestFn) bool {
fn isTeardown(t: std.builtin.TestFn) bool {
return std.mem.endsWith(u8, t.name, "tests:afterAll");
}
+
+pub const TrackingAllocator = struct {
+ parent_allocator: Allocator,
+ free_count: usize = 0,
+ allocated_bytes: usize = 0,
+ allocation_count: usize = 0,
+ reallocation_count: usize = 0,
+
+ const Stats = struct {
+ allocated_bytes: usize,
+ allocation_count: usize,
+ reallocation_count: usize,
+ };
+
+ fn init(parent_allocator: Allocator) TrackingAllocator {
+ return .{
+ .parent_allocator = parent_allocator,
+ };
+ }
+
+ pub fn stats(self: *const TrackingAllocator) Stats {
+ return .{
+ .allocated_bytes = self.allocated_bytes,
+ .allocation_count = self.allocation_count,
+ .reallocation_count = self.reallocation_count,
+ };
+ }
+
+ pub fn allocator(self: *TrackingAllocator) Allocator {
+ return .{ .ptr = self, .vtable = &.{
+ .alloc = alloc,
+ .resize = resize,
+ .free = free,
+ .remap = remap,
+ } };
+ }
+
+ fn alloc(
+ ctx: *anyopaque,
+ len: usize,
+ alignment: std.mem.Alignment,
+ return_address: usize,
+ ) ?[*]u8 {
+ const self: *TrackingAllocator = @ptrCast(@alignCast(ctx));
+ const result = self.parent_allocator.rawAlloc(len, alignment, return_address);
+ self.allocation_count += 1;
+ self.allocated_bytes += len;
+ return result;
+ }
+
+ fn resize(
+ ctx: *anyopaque,
+ old_mem: []u8,
+ alignment: std.mem.Alignment,
+ new_len: usize,
+ ra: usize,
+ ) bool {
+ const self: *TrackingAllocator = @ptrCast(@alignCast(ctx));
+ const result = self.parent_allocator.rawResize(old_mem, alignment, new_len, ra);
+ self.reallocation_count += 1; // TODO: only if result is not null?
+ return result;
+ }
+
+ fn free(
+ ctx: *anyopaque,
+ old_mem: []u8,
+ alignment: std.mem.Alignment,
+ ra: usize,
+ ) void {
+ const self: *TrackingAllocator = @ptrCast(@alignCast(ctx));
+ self.parent_allocator.rawFree(old_mem, alignment, ra);
+ self.free_count += 1;
+ }
+
+ fn remap(
+ ctx: *anyopaque,
+ memory: []u8,
+ alignment: std.mem.Alignment,
+ new_len: usize,
+ ret_addr: usize,
+ ) ?[*]u8 {
+ const self: *TrackingAllocator = @ptrCast(@alignCast(ctx));
+ const result = self.parent_allocator.rawRemap(memory, alignment, new_len, ret_addr);
+ self.reallocation_count += 1; // TODO: only if result is not null?
+ return result;
+ }
+};
diff --git a/src/testing.zig b/src/testing.zig
index 633d9087..9dbb8f79 100644
--- a/src/testing.zig
+++ b/src/testing.zig
@@ -17,15 +17,15 @@
// along with this program. If not, see .
const std = @import("std");
+const Allocator = std.mem.Allocator;
-const parser = @import("netsurf");
pub const allocator = std.testing.allocator;
pub const expectError = std.testing.expectError;
pub const expectString = std.testing.expectEqualStrings;
pub const expectEqualSlices = std.testing.expectEqualSlices;
const App = @import("app.zig").App;
-const Allocator = std.mem.Allocator;
+const parser = @import("browser/netsurf.zig");
// Merged std.testing.expectEqual and std.testing.expectString
// can be useful when testing fields of an anytype an you don't know
@@ -217,13 +217,13 @@ pub const Document = struct {
}
pub fn querySelectorAll(self: *Document, selector: []const u8) ![]const *parser.Node {
- const css = @import("dom/css.zig");
+ const css = @import("browser/dom/css.zig");
const node_list = try css.querySelectorAll(self.arena.allocator(), self.asNode(), selector);
return node_list.nodes.items;
}
pub fn querySelector(self: *Document, selector: []const u8) !?*parser.Node {
- const css = @import("dom/css.zig");
+ const css = @import("browser/dom/css.zig");
return css.querySelector(self.arena.allocator(), self.asNode(), selector);
}
@@ -350,3 +350,163 @@ fn isJsonValue(a: std.json.Value, b: std.json.Value) bool {
},
}
}
+
+pub const tracking_allocator = @import("root").tracking_allocator.allocator();
+pub const JsRunner = struct {
+ const URL = @import("url.zig").URL;
+ const Env = @import("browser/env.zig").Env;
+ const Loop = @import("runtime/loop.zig").Loop;
+ const HttpClient = @import("http/client.zig").Client;
+ const storage = @import("browser/storage/storage.zig");
+ const Window = @import("browser/html/window.zig").Window;
+ const Renderer = @import("browser/browser.zig").Renderer;
+ const SessionState = @import("browser/env.zig").SessionState;
+
+ url: URL,
+ env: *Env,
+ loop: Loop,
+ window: Window,
+ state: SessionState,
+ arena: Allocator,
+ renderer: Renderer,
+ http_client: HttpClient,
+ executor: *Env.Executor,
+ storage_shelf: storage.Shelf,
+ cookie_jar: storage.CookieJar,
+
+ fn init(parent_allocator: Allocator, opts: RunnerOpts) !*JsRunner {
+ parser.deinit();
+ try parser.init();
+
+ const aa = try parent_allocator.create(std.heap.ArenaAllocator);
+ aa.* = std.heap.ArenaAllocator.init(parent_allocator);
+ errdefer aa.deinit();
+
+ const arena = aa.allocator();
+ const runner = try arena.create(JsRunner);
+ runner.arena = arena;
+
+ runner.env = try Env.init(arena, .{});
+ errdefer runner.env.deinit();
+
+ runner.url = try URL.parse("https://lightpanda.io/opensource-browser/", null);
+
+ runner.renderer = Renderer.init(arena);
+ runner.cookie_jar = storage.CookieJar.init(arena);
+ runner.loop = try Loop.init(arena);
+ errdefer runner.loop.deinit();
+
+ var html = std.io.fixedBufferStream(opts.html);
+ const document = try parser.documentHTMLParse(html.reader(), "UTF-8");
+
+ runner.state = .{
+ .arena = arena,
+ .loop = &runner.loop,
+ .document = document,
+ .url = &runner.url,
+ .renderer = &runner.renderer,
+ .cookie_jar = &runner.cookie_jar,
+ .http_client = &runner.http_client,
+ };
+
+ runner.window = .{};
+ try runner.window.replaceDocument(document);
+ try runner.window.replaceLocation(.{
+ .url = try runner.url.toWebApi(arena),
+ });
+
+ runner.storage_shelf = storage.Shelf.init(arena);
+ runner.window.setStorageShelf(&runner.storage_shelf);
+
+ runner.http_client = try HttpClient.init(arena, 1, .{
+ .tls_verify_host = false,
+ });
+
+ runner.executor = try runner.env.startExecutor(Window, &runner.state, runner);
+ errdefer runner.env.stopExecutor(runner.executor);
+
+ try runner.executor.startScope(&runner.window);
+ return runner;
+ }
+
+ pub fn deinit(self: *JsRunner) void {
+ self.loop.deinit();
+ self.executor.endScope();
+ self.env.deinit();
+ self.http_client.deinit();
+ self.storage_shelf.deinit();
+
+ const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(self.arena.ptr));
+ arena.deinit();
+ arena.child_allocator.destroy(arena);
+ }
+
+ const RunOpts = struct {};
+ pub const Case = std.meta.Tuple(&.{ []const u8, []const u8 });
+ pub fn testCases(self: *JsRunner, cases: []const Case, _: RunOpts) !void {
+ const start = try std.time.Instant.now();
+
+ for (cases, 0..) |case, i| {
+ var try_catch: Env.TryCatch = undefined;
+ try_catch.init(self.executor);
+ defer try_catch.deinit();
+
+ const value = self.executor.exec(case.@"0", null) catch |err| {
+ if (try try_catch.err(self.arena)) |msg| {
+ std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" });
+ }
+ return err;
+ };
+ try self.loop.run();
+ @import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start);
+
+ const actual = try value.toString(self.arena);
+ if (std.mem.eql(u8, case.@"1", actual) == false) {
+ std.debug.print("Expected:\n{s}\n\nGot:\n{s}\n\nCase: {d}\n{s}\n", .{ case.@"1", actual, i + 1, case.@"0" });
+ return error.UnexpectedResult;
+ }
+ }
+ }
+
+ pub fn exec(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !void {
+ _ = try self.eval(src, name, err_msg);
+ }
+
+ pub fn eval(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !Env.Value {
+ var try_catch: Env.TryCatch = undefined;
+ try_catch.init(self.executor);
+ defer try_catch.deinit();
+
+ return self.executor.exec(src, name) catch |err| {
+ if (try try_catch.err(self.arena)) |msg| {
+ err_msg.* = msg;
+ std.debug.print("Error running script: {s}\n", .{msg});
+ }
+ return err;
+ };
+ }
+
+ pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
+ _ = ctx;
+ _ = specifier;
+ return error.DummyModuleLoader;
+ }
+};
+
+const RunnerOpts = struct {
+ html: []const u8 =
+ \\
+ \\
OK
+ \\
+ \\
+ \\
+ \\
And
+ \\
+ \\
+ \\
+ ,
+};
+
+pub fn jsRunner(alloc: Allocator, opts: RunnerOpts) !*JsRunner {
+ return JsRunner.init(alloc, opts);
+}
diff --git a/src/url.zig b/src/url.zig
index 8c4d4368..a5baef08 100644
--- a/src/url.zig
+++ b/src/url.zig
@@ -2,7 +2,7 @@ const std = @import("std");
const Uri = std.Uri;
const Allocator = std.mem.Allocator;
-const WebApiURL = @import("url/url.zig").URL;
+const WebApiURL = @import("browser/url/url.zig").URL;
pub const URL = struct {
uri: Uri,
diff --git a/src/user_context.zig b/src/user_context.zig
deleted file mode 100644
index 21505cae..00000000
--- a/src/user_context.zig
+++ /dev/null
@@ -1,14 +0,0 @@
-const std = @import("std");
-const parser = @import("netsurf");
-const URL = @import("url.zig").URL;
-const storage = @import("storage/storage.zig");
-const Client = @import("http/client.zig").Client;
-const Renderer = @import("browser/browser.zig").Renderer;
-
-pub const UserContext = struct {
- url: *const URL,
- http_client: *Client,
- document: *parser.DocumentHTML,
- cookie_jar: *storage.CookieJar,
- renderer: *Renderer,
-};
diff --git a/src/wpt/run.zig b/src/wpt/run.zig
index 90486415..6823aaf9 100644
--- a/src/wpt/run.zig
+++ b/src/wpt/run.zig
@@ -18,112 +18,55 @@
const std = @import("std");
const fspath = std.fs.path;
+const Allocator = std.mem.Allocator;
+const Env = @import("../browser/env.zig").Env;
const FileLoader = @import("fileloader.zig").FileLoader;
+const Window = @import("../browser/html/window.zig").Window;
-const parser = @import("netsurf");
-
-const jsruntime = @import("jsruntime");
-const Loop = jsruntime.Loop;
-const Env = jsruntime.Env;
-const URL = @import("../url.zig").URL;
-const browser = @import("../browser/browser.zig");
-const Window = @import("../html/window.zig").Window;
-const storage = @import("../storage/storage.zig");
-const HttpClient = @import("../http/client.zig").Client;
-
-const Types = @import("../main_wpt.zig").Types;
-const UserContext = @import("../main_wpt.zig").UserContext;
-
-const polyfill = @import("../polyfill/polyfill.zig");
+const parser = @import("../browser/netsurf.zig");
+const polyfill = @import("../browser/polyfill/polyfill.zig");
// runWPT parses the given HTML file, starts a js env and run the first script
// tags containing javascript sources.
// It loads first the js libs files.
-pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const u8, loader: *FileLoader) !Res {
- const alloc = arena.allocator();
- try parser.init();
- defer parser.deinit();
-
+pub fn run(arena: Allocator, comptime dir: []const u8, f: []const u8, loader: *FileLoader, err_msg: *?[]const u8) ![]const u8 {
// document
- const file = try std.fs.cwd().openFile(f, .{});
- defer file.close();
-
- const html_doc = try parser.documentHTMLParse(file.reader(), "UTF-8");
+ const html = blk: {
+ const file = try std.fs.cwd().openFile(f, .{});
+ defer file.close();
+ break :blk try file.readToEndAlloc(arena, 128 * 1024);
+ };
const dirname = fspath.dirname(f[dir.len..]) orelse unreachable;
- // create JS env
- var loop = try Loop.init(alloc);
- defer loop.deinit();
-
- var http_client = try HttpClient.init(alloc, 2, .{});
- defer http_client.deinit();
-
- var cookie_jar = storage.CookieJar.init(alloc);
- defer cookie_jar.deinit();
-
- var renderer = browser.Renderer.init(alloc);
- defer renderer.elements.deinit(alloc);
- defer renderer.positions.deinit(alloc);
-
- const url = try URL.parse("https://lightpanda.io", null);
-
- var js_env: Env = undefined;
- Env.init(&js_env, alloc, &loop, UserContext{
- .url = &url,
- .document = html_doc,
- .cookie_jar = &cookie_jar,
- .http_client = &http_client,
- .renderer = &renderer,
+ var runner = try @import("../testing.zig").jsRunner(arena, .{
+ .html = html,
});
- defer js_env.deinit();
-
- var storageShelf = storage.Shelf.init(alloc);
- defer storageShelf.deinit();
-
- // load user-defined types in JS env
- var js_types: [Types.len]usize = undefined;
- try js_env.load(&js_types);
-
- // start JS env
- try js_env.start();
- defer js_env.stop();
-
- // load polyfills
- try polyfill.load(alloc, &js_env);
+ defer runner.deinit();
+ try polyfill.load(arena, runner.executor);
// display console logs
defer {
- const res = evalJS(&js_env, alloc, "console.join('\\n');", "console") catch unreachable;
- defer res.deinit(alloc);
-
- if (res.msg != null and res.msg.?.len > 0) {
- std.debug.print("-- CONSOLE LOG\n{s}\n--\n", .{res.msg.?});
+ const res = runner.eval("console.join('\\n');", "console", err_msg) catch unreachable;
+ const log = res.toString(arena) catch unreachable;
+ if (log.len > 0) {
+ std.debug.print("-- CONSOLE LOG\n{s}\n--\n", .{log});
}
}
- // setup global env vars.
- var window = Window.create(null, null);
- try window.replaceDocument(html_doc);
- window.setStorageShelf(&storageShelf);
- try js_env.bindGlobal(&window);
+ try runner.exec(
+ \\ console = [];
+ \\ console.log = function () {
+ \\ console.push(...arguments);
+ \\ };
+ \\ console.debug = function () {
+ \\ console.push("debug", ...arguments);
+ \\ };
+ , "init", err_msg);
- const init =
- \\console = [];
- \\console.log = function () {
- \\ console.push(...arguments);
- \\};
- \\console.debug = function () {
- \\ console.push("debug", ...arguments);
- \\};
- ;
- var res = try evalJS(&js_env, alloc, init, "init");
- if (!res.ok) return res;
- res.deinit(alloc);
-
- // loop hover the scripts.
- const doc = parser.documentHTMLToDocument(html_doc);
+ // loop over the scripts.
+ const doc = parser.documentHTMLToDocument(runner.state.document.?);
const scripts = try parser.documentGetElementsByTagName(doc, "script");
const slen = try parser.nodeListLength(scripts);
for (0..slen) |i| {
@@ -134,19 +77,14 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
var path = src;
if (!std.mem.startsWith(u8, src, "/")) {
// no need to free path, thanks to the arena.
- path = try fspath.join(alloc, &.{ "/", dirname, path });
+ path = try fspath.join(arena, &.{ "/", dirname, path });
}
-
- res = try evalJS(&js_env, alloc, try loader.get(path), src);
- if (!res.ok) return res;
- res.deinit(alloc);
+ try runner.exec(try loader.get(path), src, err_msg);
}
// If the script as a source text, execute it.
const src = try parser.nodeTextContent(s) orelse continue;
- res = try evalJS(&js_env, alloc, src, "");
- if (!res.ok) return res;
- res.deinit(alloc);
+ try runner.exec(src, null, err_msg);
}
// Mark tests as ready to run.
@@ -155,57 +93,29 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
try parser.eventInit(loadevt, "load", .{});
_ = try parser.eventTargetDispatchEvent(
- parser.toEventTarget(Window, &window),
+ parser.toEventTarget(@TypeOf(runner.window), &runner.window),
loadevt,
);
// wait for all async executions
- var try_catch: jsruntime.TryCatch = undefined;
- try_catch.init(&js_env);
- defer try_catch.deinit();
- js_env.wait() catch {
- return .{
- .ok = false,
- .msg = try try_catch.err(alloc, &js_env),
+ {
+ var try_catch: Env.TryCatch = undefined;
+ try_catch.init(runner.executor);
+ defer try_catch.deinit();
+ runner.loop.run() catch |err| {
+ if (try try_catch.err(arena)) |msg| {
+ err_msg.* = msg;
+ }
+ return err;
};
- };
+ }
// Check the final test status.
- res = try evalJS(&js_env, alloc, "report.status;", "teststatus");
- if (!res.ok) return res;
- res.deinit(alloc);
+ try runner.exec("report.status", "teststatus", err_msg);
// return the detailed result.
- return try evalJS(&js_env, alloc, "report.log", "teststatus");
-}
-
-pub const Res = struct {
- ok: bool,
- msg: ?[]const u8,
-
- pub fn deinit(res: Res, alloc: std.mem.Allocator) void {
- if (res.msg) |msg| {
- alloc.free(msg);
- }
- }
-};
-
-fn evalJS(env: *const jsruntime.Env, alloc: std.mem.Allocator, script: []const u8, name: ?[]const u8) !Res {
- var try_catch: jsruntime.TryCatch = undefined;
- try_catch.init(env);
- defer try_catch.deinit();
-
- const v = env.exec(script, name) catch {
- return .{
- .ok = false,
- .msg = try try_catch.err(alloc, env),
- };
- };
-
- return .{
- .ok = true,
- .msg = try v.toString(alloc, env),
- };
+ const res = try runner.eval("report.log", "report", err_msg);
+ return res.toString(arena);
}
// browse the path to find the tests list.
diff --git a/vendor/zig-js-runtime b/vendor/zig-js-runtime
deleted file mode 160000
index 9b87782f..00000000
--- a/vendor/zig-js-runtime
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 9b87782f1edc0a3c4541f771d4ff443820fa38ac