Merge pull request #506 from lightpanda-io/jsruntime

replace zig-js-runtime
This commit is contained in:
Pierre Tachoire
2025-04-17 13:09:44 +02:00
committed by GitHub
98 changed files with 6939 additions and 4197 deletions

View File

@@ -59,11 +59,11 @@ runs:
- name: install v8 - name: install v8
shell: bash shell: bash
run: | run: |
mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug mkdir -p v8/build/${{inputs.arch}}-${{inputs.os}}/debug/ninja/obj/zig/
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug/libc_v8.a 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 mkdir -p v8/build/${{inputs.arch}}-${{inputs.os}}/release/ninja/obj/zig/
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release/libc_v8.a ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/build/${{inputs.arch}}-${{inputs.os}}/release/ninja/obj/zig/libc_v8.a
- name: libiconv - name: libiconv
shell: bash shell: bash

View File

@@ -31,7 +31,7 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
- name: zig build - 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 - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -63,7 +63,7 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
- name: zig build - 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 - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -95,7 +95,7 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
- name: zig build - 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 - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -127,7 +127,7 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
- name: zig build - 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 - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}

View File

@@ -47,7 +47,7 @@ jobs:
- uses: ./.github/actions/install - uses: ./.github/actions/install
- name: zig build release - name: zig build release
run: zig build -Doptimize=ReleaseSafe -Dengine=v8 run: zig build -Doptimize=ReleaseSafe
- name: upload artifact - name: upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -86,7 +86,7 @@ jobs:
- name: run puppeteer - name: run puppeteer
run: | run: |
python3 -m http.server 1234 -d ./public & echo $! > PYTHON.pid 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 RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
kill `cat LPD.pid` `cat PYTHON.pid` kill `cat LPD.pid` `cat PYTHON.pid`

View File

@@ -55,7 +55,7 @@ jobs:
- uses: ./.github/actions/install - 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. # For now WPT tests doesn't pass at all.
# We accept then to continue the job on failure. # We accept then to continue the job on failure.
@@ -80,7 +80,7 @@ jobs:
- uses: ./.github/actions/install - uses: ./.github/actions/install
- name: json output - name: json output
run: zig build wpt -Dengine=v8 -- --safe --json > wpt.json run: zig build wpt -- --safe --json > wpt.json
- name: write commit - name: write commit
run: | run: |

View File

@@ -56,7 +56,7 @@ jobs:
- uses: ./.github/actions/install - uses: ./.github/actions/install
- name: zig build debug - name: zig build debug
run: zig build -Dengine=v8 run: zig build
- name: upload artifact - name: upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -102,11 +102,8 @@ jobs:
- uses: ./.github/actions/install - uses: ./.github/actions/install
- name: zig build unittest
run: zig build unittest -freference-trace --summary all
- name: zig build test - name: zig build test
run: zig build test -Dengine=v8 -- --json > bench.json run: zig build test -- --json > bench.json
- name: write commit - name: write commit
run: | run: |

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ zig-out
/vendor/netsurf/out /vendor/netsurf/out
/vendor/libiconv/ /vendor/libiconv/
lightpanda.id lightpanda.id
/v8/

4
.gitmodules vendored
View File

@@ -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"] [submodule "vendor/netsurf/libwapcaplet"]
path = vendor/netsurf/libwapcaplet path = vendor/netsurf/libwapcaplet
url = https://github.com/lightpanda-io/libwapcaplet.git/ url = https://github.com/lightpanda-io/libwapcaplet.git/

View File

@@ -61,8 +61,8 @@ RUN make install-libiconv && \
# download and install v8 # 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 && \ 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 && \ mkdir -p v8/build/${ARCH}-linux/release/ninja/obj/zig/ && \
mv libc_v8.a vendor/zig-js-runtime/vendor/v8/${ARCH}-linux/release/libc_v8.a mv libc_v8.a v8/build/${ARCH}-linux/release/ninja/obj/zig/libc_v8.a
# build release # build release
RUN make build RUN make build

View File

@@ -3,7 +3,7 @@
ZIG := zig ZIG := zig
BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# option test filter make unittest F="server" # option test filter make test F="server"
F= F=
# OS and ARCH # OS and ARCH
@@ -47,7 +47,7 @@ help:
# $(ZIG) commands # $(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) 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 in release-safe mode
build: build:
@printf "\e[36mBuilding (release safe)...\e[0m\n" @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" @printf "\e[33mBuild OK\e[0m\n"
## Build in debug mode ## Build in debug mode
build-dev: build-dev:
@printf "\e[36mBuilding (debug)...\e[0m\n" @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" @printf "\e[33mBuild OK\e[0m\n"
## Run the server in debug mode ## Run the server in debug mode
@@ -79,39 +79,47 @@ run: build
## Run a JS shell in debug mode ## Run a JS shell in debug mode
shell: shell:
@printf "\e[36mBuilding shell...\e[0m\n" @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 ## Run WPT tests
wpt: wpt:
@printf "\e[36mBuilding wpt...\e[0m\n" @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: wpt-summary:
@printf "\e[36mBuilding wpt...\e[0m\n" @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
test: test:
@printf "\e[36mTesting...\e[0m\n" @TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
@$(ZIG) build test -Dengine=v8 || (printf "\e[33mTest ERROR\e[0m\n"; exit 1;)
@printf "\e[33mTest OK\e[0m\n"
unittest: ## v8
@TEST_FILTER='${F}' $(ZIG) build unittest -freference-trace --summary all get-v8:
@printf "\e[36mGetting v8 source...\e[0m\n"
@$(ZIG) build get-v8
build-v8-dev:
@printf "\e[36mBuilding v8 (dev)...\e[0m\n"
@$(ZIG) build build-v8
build-v8:
@printf "\e[36mBuilding v8...\e[0m\n"
@$(ZIG) build -Doptimize=ReleaseSafe build-v8
# Install and build required dependencies commands # Install and build required dependencies commands
# ------------ # ------------
.PHONY: install-submodule .PHONY: install-submodule
.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-netsurf install-netsurf clean-netsurf test-netsurf install-netsurf-dev
.PHONY: install-mimalloc install-mimalloc-dev clean-mimalloc .PHONY: install-mimalloc install-mimalloc-dev clean-mimalloc
.PHONY: install-dev install .PHONY: install-dev install
## Install and build dependencies for release ## 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 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: _install-netsurf
install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
@@ -194,14 +202,6 @@ ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
make clean make clean
endif 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: data:
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig

View File

@@ -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 env var `MIMALLOC_SHOW_STATS=1`. See
[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html). [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. First, get the tools necessary for building V8, as well as the V8 source code:
This build task is very long and cpu consuming, as you will build v8 from sources.
``` ```
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 ## Test

317
build.zig
View File

@@ -17,16 +17,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); 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 /// Do not rename this constant. It is scanned by some scripts to determine
/// which zig version to install. /// which zig version to install.
const recommended_zig_version = jsruntime.recommended_zig_version; const recommended_zig_version = "0.14.0";
pub fn build(b: *std.Build) !void { pub fn build(b: *std.Build) !void {
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) { switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
@@ -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 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(); // browser
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);
// run // compile and install
const run_cmd = b.addRunArtifact(exe); const exe = b.addExecutable(.{
if (b.args) |args| { .name = "lightpanda",
run_cmd.addArgs(args); .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"); // get v8
run_step.dependOn(&run_cmd.step); // -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
// shell const get_v8 = b.addRunArtifact(v8.artifact("get-v8"));
// ----- const get_step = b.step("get-v8", "Get v8");
get_step.dependOn(&get_v8.step);
// 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);
} }
// step {
const shell_step = b.step("shell", "Run JS shell"); // build v8
shell_step.dependOn(&shell_cmd.step); // -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
// test const build_v8 = b.addRunArtifact(v8.artifact("build-v8"));
// ---- const build_step = b.step("build-v8", "Build v8");
build_step.dependOn(&build_v8.step);
// 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);
} }
// step {
const test_step = b.step("test", "Run unit tests"); // tests
test_step.dependOn(&run_tests.step); // ----
// 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 run_tests = b.addRunArtifact(tests);
const unit_tests = b.addTest(.{ if (b.args) |args| {
.root_source_file = b.path("src/main_unit_tests.zig"), run_tests.addArgs(args);
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple }, }
.target = target,
.optimize = mode,
});
try common(b, unit_tests, options);
const run_unit_tests = b.addRunArtifact(unit_tests); // step
if (b.args) |args| { const tests_step = b.step("test", "Run unit tests");
run_unit_tests.addArgs(args); tests_step.dependOn(&run_tests.step);
} }
// step {
const unit_test_step = b.step("unittest", "Run unit tests"); // wpt
unit_test_step.dependOn(&run_unit_tests.step); // -----
// 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 // run
const wpt = b.addExecutable(.{ const wpt_cmd = b.addRunArtifact(wpt);
.name = "lightpanda-wpt", if (b.args) |args| {
.root_source_file = b.path("src/main_wpt.zig"), wpt_cmd.addArgs(args);
.target = target, }
.optimize = mode, // step
}); const wpt_step = b.step("wpt", "WPT tests");
try common(b, wpt, options); wpt_step.dependOn(&wpt_cmd.step);
// 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);
} }
fn common( fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Compile) !void {
b: *std.Build, const mod = step.root_module;
step: *std.Build.Step.Compile, const target = mod.resolved_target.?;
options: jsruntime.Options, const optimize = mod.optimize.?;
) !void {
const target = step.root_module.resolved_target.?;
const optimize = step.root_module.optimize.?;
const dep_opts = .{ .target = target, .optimize = optimize }; const dep_opts = .{ .target = target, .optimize = optimize };
const jsruntimemod = try jsruntime_pkgs.module( try moduleNetSurf(b, step, target);
b, mod.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
options, mod.addImport("tigerbeetle-io", b.dependency("tigerbeetle_io", .{}).module("tigerbeetle_io"));
step.root_module.optimize.?,
target, {
// 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); mod.addObjectFile(mod.owner.path(lib_path));
mod.addImport("build_info", opts.createModule());
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"));
} }
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module { fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build.ResolvedTarget) !void {
const mod = b.addModule("netsurf", .{
.root_source_file = b.path("src/netsurf/netsurf.zig"),
.target = target,
});
const os = target.result.os.tag; const os = target.result.os.tag;
const arch = target.result.cpu.arch; const arch = target.result.cpu.arch;
// iconv // iconv
const libiconv_lib_path = try std.fmt.allocPrint( const libiconv_lib_path = try std.fmt.allocPrint(
mod.owner.allocator, b.allocator,
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a", "vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
.{ @tagName(os), @tagName(arch) }, .{ @tagName(os), @tagName(arch) },
); );
const libiconv_include_path = try std.fmt.allocPrint( const libiconv_include_path = try std.fmt.allocPrint(
mod.owner.allocator, b.allocator,
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a", "vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
.{ @tagName(os), @tagName(arch) }, .{ @tagName(os), @tagName(arch) },
); );
mod.addObjectFile(b.path(libiconv_lib_path)); step.addObjectFile(b.path(libiconv_lib_path));
mod.addIncludePath(b.path(libiconv_include_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 // netsurf libs
const ns = "vendor/netsurf"; const ns = "vendor/netsurf";
const ns_include_path = try std.fmt.allocPrint( const ns_include_path = try std.fmt.allocPrint(
mod.owner.allocator, b.allocator,
ns ++ "/out/{s}-{s}/include", ns ++ "/out/{s}-{s}/include",
.{ @tagName(os), @tagName(arch) }, .{ @tagName(os), @tagName(arch) },
); );
mod.addIncludePath(b.path(ns_include_path)); step.addIncludePath(b.path(ns_include_path));
const libs: [4][]const u8 = .{ const libs: [4][]const u8 = .{
"libdom", "libdom",
@@ -238,34 +230,11 @@ fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Mo
}; };
inline for (libs) |lib| { inline for (libs) |lib| {
const ns_lib_path = try std.fmt.allocPrint( const ns_lib_path = try std.fmt.allocPrint(
mod.owner.allocator, b.allocator,
ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a", ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a",
.{ @tagName(os), @tagName(arch) }, .{ @tagName(os), @tagName(arch) },
); );
mod.addObjectFile(b.path(ns_lib_path)); step.addObjectFile(b.path(ns_lib_path));
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src")); 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;
} }

View File

@@ -6,7 +6,17 @@
.dependencies = .{ .dependencies = .{
.tls = .{ .tls = .{
.url = "https://github.com/ianic/tls.zig/archive/b29a8b45fc59fc2d202769c4f54509bb9e17d0a2.tar.gz", .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" },
}, },
} }

View File

@@ -1,47 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const 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;

View File

@@ -1,7 +1,8 @@
const std = @import("std"); const std = @import("std");
const Loop = @import("jsruntime").Loop;
const Allocator = std.mem.Allocator; 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 HttpClient = @import("http/client.zig").Client;
const Telemetry = @import("telemetry/telemetry.zig").Telemetry; const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
@@ -11,6 +12,7 @@ const log = std.log.scoped(.app);
// might need. // might need.
pub const App = struct { pub const App = struct {
loop: *Loop, loop: *Loop,
config: Config,
allocator: Allocator, allocator: Allocator,
telemetry: Telemetry, telemetry: Telemetry,
http_client: HttpClient, http_client: HttpClient,
@@ -24,8 +26,9 @@ pub const App = struct {
}; };
pub const Config = struct { pub const Config = struct {
tls_verify_host: bool = true,
run_mode: RunMode, run_mode: RunMode,
gc_hints: bool = false,
tls_verify_host: bool = true,
}; };
pub fn init(allocator: Allocator, config: Config) !*App { pub fn init(allocator: Allocator, config: Config) !*App {
@@ -48,6 +51,7 @@ pub const App = struct {
.http_client = try HttpClient.init(allocator, 5, .{ .http_client = try HttpClient.init(allocator, 5, .{
.tls_verify_host = config.tls_verify_host, .tls_verify_host = config.tls_verify_host,
}), }),
.config = config,
}; };
app.telemetry = Telemetry.init(app, config.run_mode); app.telemetry = Telemetry.init(app, config.run_mode);

View File

@@ -21,31 +21,26 @@ const builtin = @import("builtin");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Types = @import("root").Types;
const parser = @import("netsurf");
const Dump = @import("dump.zig"); const Dump = @import("dump.zig");
const Mime = @import("mime.zig").Mime; const Mime = @import("mime.zig").Mime;
const parser = @import("netsurf.zig");
const jsruntime = @import("jsruntime"); const Window = @import("html/window.zig").Window;
const Loop = jsruntime.Loop; const Walker = @import("dom/walker.zig").WalkerDepthFirst;
const Env = jsruntime.Env;
const Module = jsruntime.Module;
const Env = @import("env.zig").Env;
const App = @import("../app.zig").App; 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 URL = @import("../url.zig").URL;
const storage = @import("../storage/storage.zig");
const Notification = @import("../notification.zig").Notification;
const http = @import("../http/client.zig"); 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); const log = std.log.scoped(.browser);
@@ -56,6 +51,7 @@ pub const user_agent = "Lightpanda/1.0";
// A browser contains only one session. // A browser contains only one session.
// TODO allow multiple sessions per browser. // TODO allow multiple sessions per browser.
pub const Browser = struct { pub const Browser = struct {
env: *Env,
app: *App, app: *App,
session: ?*Session, session: ?*Session,
allocator: Allocator, allocator: Allocator,
@@ -65,10 +61,17 @@ pub const Browser = struct {
const SessionPool = std.heap.MemoryPool(Session); const SessionPool = std.heap.MemoryPool(Session);
pub fn init(app: *App) Browser { pub fn init(app: *App) !Browser {
const allocator = app.allocator; const allocator = app.allocator;
const env = try Env.init(allocator, .{
.gc_hints = app.config.gc_hints,
});
errdefer env.deinit();
return .{ return .{
.app = app, .app = app,
.env = env,
.session = null, .session = null,
.allocator = allocator, .allocator = allocator,
.http_client = &app.http_client, .http_client = &app.http_client,
@@ -79,6 +82,7 @@ pub const Browser = struct {
pub fn deinit(self: *Browser) void { pub fn deinit(self: *Browser) void {
self.closeSession(); self.closeSession();
self.env.deinit();
self.session_pool.deinit(); self.session_pool.deinit();
self.page_arena.deinit(); self.page_arena.deinit();
} }
@@ -101,10 +105,7 @@ pub const Browser = struct {
} }
pub fn runMicrotasks(self: *const Browser) void { pub fn runMicrotasks(self: *const Browser) void {
// if no session exists, there is nothing to do. return self.env.runMicrotasks();
if (self.session == null) return;
return self.session.?.env.runMicrotasks();
} }
}; };
@@ -113,8 +114,11 @@ pub const Browser = struct {
// You can create successively multiple pages for a session, but you must // You can create successively multiple pages for a session, but you must
// deinit a page before running another one. // deinit a page before running another one.
pub const Session = struct { pub const Session = struct {
app: *App, state: SessionState,
executor: *Env.Executor,
inspector: Env.Inspector,
app: *App,
browser: *Browser, browser: *Browser,
// The arena is used only to bound the js env init b/c it leaks memory. // 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. // all others Session deps use directly self.alloc and not the arena.
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
env: Env,
inspector: jsruntime.Inspector,
window: Window, window: Window,
// TODO move the shed/jar to the browser? // TODO move the shed/jar to the browser?
@@ -136,8 +137,6 @@ pub const Session = struct {
page: ?Page = null, page: ?Page = null,
http_client: *http.Client, http_client: *http.Client,
jstypes: [Types.len]usize = undefined,
// recipient of notification, passed as the first parameter to notify // recipient of notification, passed as the first parameter to notify
notify_ctx: *anyopaque, notify_ctx: *anyopaque,
notify_func: *const fn (ctx: *anyopaque, notification: *const Notification) anyerror!void, notify_func: *const fn (ctx: *anyopaque, notification: *const Notification) anyerror!void,
@@ -159,48 +158,59 @@ pub const Session = struct {
const allocator = app.allocator; const allocator = app.allocator;
self.* = .{ self.* = .{
.app = app, .app = app,
.env = undefined,
.browser = browser, .browser = browser,
.notify_ctx = any_ctx, .notify_ctx = any_ctx,
.inspector = undefined, .inspector = undefined,
.notify_func = ContextStruct.notify, .notify_func = ContextStruct.notify,
.http_client = browser.http_client, .http_client = browser.http_client,
.executor = undefined,
.storage_shed = storage.Shed.init(allocator), .storage_shed = storage.Shed.init(allocator),
.arena = std.heap.ArenaAllocator.init(allocator), .arena = std.heap.ArenaAllocator.init(allocator),
.cookie_jar = storage.CookieJar.init(allocator), .cookie_jar = storage.CookieJar.init(allocator),
.window = Window.create(null, .{ .agent = user_agent }), .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(); self.executor = try browser.env.startExecutor(Window, &self.state, self);
Env.init(&self.env, arena, app.loop, null); errdefer browser.env.stopExecutor(self.executor);
errdefer self.env.deinit(); self.inspector = try Env.Inspector.init(self.arena.allocator(), self.executor, ctx);
try self.env.load(&self.jstypes);
// const ctx_opaque = @as(*anyopaque, @ptrCast(ctx)); self.microtaskLoop();
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);
} }
fn deinit(self: *Session) void { fn deinit(self: *Session) void {
self.app.loop.resetZig();
if (self.page != null) { if (self.page != null) {
self.removePage(); self.removePage();
} }
self.env.deinit(); self.inspector.deinit();
self.arena.deinit(); self.arena.deinit();
self.cookie_jar.deinit(); self.cookie_jar.deinit();
self.storage_shed.deinit(); self.storage_shed.deinit();
self.browser.env.stopExecutor(self.executor);
} }
fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module { fn microtaskLoop(self: *Session) void {
_ = referrer; 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 self: *Session = @ptrCast(@alignCast(ctx));
const page = &(self.page orelse return error.NoPage); 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 // Use the page_arena for this, which has a more appropriate lifetime
// and which has more retained memory between sessions and pages. // and which has more retained memory between sessions and pages.
const arena = self.browser.page_arena.allocator(); const arena = self.browser.page_arena.allocator();
const body = try page.fetchData( return try page.fetchData(
arena, arena,
specifier, specifier,
if (page.current_script) |s| s.src else null, if (page.current_script) |s| s.src else null,
); );
return self.env.compileModule(body, specifier);
} }
pub fn callInspector(self: *Session, msg: []const u8) void { pub fn callInspector(self: *const Session, msg: []const u8) void {
self.inspector.send(self.env, msg); self.inspector.send(msg);
} }
// NOTE: the caller is not the owner of the returned value, // NOTE: the caller is not the owner of the returned value,
@@ -232,19 +241,14 @@ pub const Session = struct {
const page = &self.page.?; const page = &self.page.?;
// start JS env // start JS env
log.debug("start js env", .{}); log.debug("start new js scope", .{});
try self.env.start(); self.state.arena = self.browser.page_arena.allocator();
errdefer self.state.arena = undefined;
if (comptime builtin.is_test == false) { try self.executor.startScope(&self.window);
// 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);
}
// load polyfills // 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.executor);
try polyfill.load(self.arena.allocator(), &self.env);
// inspector // inspector
self.contextCreated(page, aux_data); self.contextCreated(page, aux_data);
@@ -254,11 +258,10 @@ pub const Session = struct {
pub fn removePage(self: *Session) void { pub fn removePage(self: *Session) void {
std.debug.assert(self.page != null); std.debug.assert(self.page != null);
// Reset all existing callbacks. // Reset all existing callbacks.
self.app.loop.resetJS(); self.app.loop.resetJS();
self.executor.endScope();
self.env.stop();
// TODO unload document: https://html.spec.whatwg.org/#unloading-documents // TODO unload document: https://html.spec.whatwg.org/#unloading-documents
self.window.replaceLocation(.{ .url = null }) catch |e| { self.window.replaceLocation(.{ .url = null }) catch |e| {
@@ -267,6 +270,7 @@ pub const Session = struct {
// clear netsurf memory arena. // clear netsurf memory arena.
parser.deinit(); parser.deinit();
self.state.arena = undefined;
self.page = null; self.page = null;
} }
@@ -277,7 +281,7 @@ pub const Session = struct {
fn contextCreated(self: *Session, page: *Page, aux_data: ?[]const u8) void { fn contextCreated(self: *Session, page: *Page, aux_data: ?[]const u8) void {
log.debug("inspector context created", .{}); 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 { fn notify(self: *const Session, notification: *const Notification) void {
@@ -332,19 +336,16 @@ pub const Page = struct {
pub fn wait(self: *Page) !void { pub fn wait(self: *Page) !void {
// try catch // try catch
var try_catch: jsruntime.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(&self.session.env); try_catch.init(self.session.executor);
defer try_catch.deinit(); defer try_catch.deinit();
self.session.env.wait() catch |err| { self.session.app.loop.run() catch |err| {
// the js env could not be started if the document wasn't an HTML. if (try try_catch.err(self.arena)) |msg| {
if (err == error.EnvNotStarted) return;
const arena = self.arena;
if (try try_catch.err(arena, &self.session.env)) |msg| {
defer arena.free(msg);
log.info("wait error: {s}", .{msg}); log.info("wait error: {s}", .{msg});
return; return;
} else {
log.info("wait error: {any}", .{err});
} }
}; };
log.debug("wait: OK", .{}); log.debug("wait: OK", .{});
@@ -397,7 +398,7 @@ pub const Page = struct {
try session.cookie_jar.populateFromResponse(&url.uri, &header); try session.cookie_jar.populateFromResponse(&url.uri, &header);
// TODO handle fragment in url. // 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 }); log.info("GET {any} {d}", .{ url, header.status });
@@ -414,7 +415,6 @@ pub const Page = struct {
log.debug("header content-type: {s}", .{ct}); log.debug("header content-type: {s}", .{ct});
var mime = try Mime.parse(arena, ct); var mime = try Mime.parse(arena, ct);
defer mime.deinit();
if (mime.isHTML()) { if (mime.isHTML()) {
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8", aux_data); 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 // https://html.spec.whatwg.org/#reporting-document-loading-status
// inject the URL to the document including the fragment. // 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; const session = self.session;
// TODO set the referrer to the document. // TODO set the referrer to the document.
@@ -499,14 +499,13 @@ pub const Page = struct {
// inspector // inspector
session.contextCreated(self, aux_data); session.contextCreated(self, aux_data);
// replace the user context document with the new one. {
try session.env.setUserContext(.{ // update the sessions state
.url = @ptrCast(&self.url.?), const state = &session.state;
.document = html_doc, state.url = &self.url.?;
.renderer = @ptrCast(&self.renderer), state.document = html_doc;
.cookie_jar = @ptrCast(&self.session.cookie_jar), state.renderer = &self.renderer;
.http_client = @ptrCast(self.session.http_client), }
});
// browse the DOM tree to retrieve scripts // browse the DOM tree to retrieve scripts
// TODO execute the synchronous scripts during the HTL parsing. // TODO execute the synchronous scripts during the HTL parsing.
@@ -643,7 +642,7 @@ pub const Page = struct {
// TODO handle charset attribute // TODO handle charset attribute
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element)); const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
if (opt_text) |text| { if (opt_text) |text| {
try s.eval(self.arena, &self.session.env, text); try s.eval(self.arena, self.session, text);
return; return;
} }
@@ -711,7 +710,7 @@ pub const Page = struct {
fn fetchScript(self: *const Page, s: *const Script) !void { fn fetchScript(self: *const Page, s: *const Script) !void {
const arena = self.arena; const arena = self.arena;
const body = try self.fetchData(arena, s.src, null); 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 { 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; return .unknown;
} }
fn eval(self: Script, arena: Allocator, env: *const Env, body: []const u8) !void { fn eval(self: Script, arena: Allocator, session: *Session, body: []const u8) !void {
var try_catch: jsruntime.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(env); try_catch.init(session.executor);
defer try_catch.deinit(); defer try_catch.deinit();
const res = switch (self.kind) { const res = switch (self.kind) {
.unknown => return error.UnknownScript, .unknown => return error.UnknownScript,
.javascript => env.exec(body, self.src), .javascript => session.executor.exec(body, self.src),
.module => env.module(body, self.src), .module => session.executor.module(body, self.src),
} catch { } 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 }); log.info("eval script {s}: {s}", .{ self.src, msg });
} }
return FetchError.JsErr; return FetchError.JsErr;
}; };
if (builtin.mode == .Debug) { 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 }); log.debug("eval script {s}: {s}", .{ self.src, msg });
} }
} }
@@ -810,7 +809,7 @@ const FlatRenderer = struct {
// given an index, get the element // given an index, get the element
elements: std.ArrayListUnmanaged(u64), 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 // we expect allocator to be an arena
pub fn init(allocator: Allocator) FlatRenderer { pub fn init(allocator: Allocator) FlatRenderer {

View File

@@ -0,0 +1,28 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = 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});
}
};

View File

@@ -18,7 +18,7 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
// Node implementation with Netsurf Libdom C lib. // Node implementation with Netsurf Libdom C lib.
pub const Node = struct { pub const Node = struct {

View File

@@ -19,7 +19,7 @@
const std = @import("std"); const std = @import("std");
const css = @import("css.zig"); const css = @import("css.zig");
const Node = @import("libdom.zig").Node; const Node = @import("libdom.zig").Node;
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const Matcher = struct { const Matcher = struct {
const Nodes = std.ArrayList(Node); const Nodes = std.ArrayList(Node);

View File

@@ -18,11 +18,7 @@
const std = @import("std"); const std = @import("std");
const jsruntime = @import("jsruntime"); const parser = @import("../netsurf.zig");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("netsurf");
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
const DOMException = @import("exceptions.zig").DOMException; const DOMException = @import("exceptions.zig").DOMException;
@@ -31,7 +27,6 @@ const DOMException = @import("exceptions.zig").DOMException;
pub const Attr = struct { pub const Attr = struct {
pub const Self = parser.Attribute; pub const Self = parser.Attribute;
pub const prototype = *Node; pub const prototype = *Node;
pub const mem_guarantied = true;
pub fn get_namespaceURI(self: *parser.Attribute) !?[]const u8 { pub fn get_namespaceURI(self: *parser.Attribute) !?[]const u8 {
return try parser.nodeGetNamespace(parser.attributeToNode(self)); return try parser.nodeGetNamespace(parser.attributeToNode(self));
@@ -70,34 +65,33 @@ pub const Attr = struct {
// Tests // Tests
// ----- // -----
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.DOM.Attribute" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
var getters = [_]Case{
.{ .src = "let a = document.createAttributeNS('foo', 'bar')", .ex = "undefined" }, try runner.testCases(&.{
.{ .src = "a.namespaceURI", .ex = "foo" }, .{ "let a = document.createAttributeNS('foo', 'bar')", "undefined" },
.{ .src = "a.prefix", .ex = "null" }, .{ "a.namespaceURI", "foo" },
.{ .src = "a.localName", .ex = "bar" }, .{ "a.prefix", "null" },
.{ .src = "a.name", .ex = "bar" }, .{ "a.localName", "bar" },
.{ .src = "a.value", .ex = "" }, .{ "a.name", "bar" },
.{ "a.value", "" },
// TODO: libdom has a bug here: the created attr has no parent, it // TODO: libdom has a bug here: the created attr has no parent, it
// causes a panic w/ libdom when setting the value. // causes a panic w/ libdom when setting the value.
//.{ .src = "a.value = 'nok'", .ex = "nok" }, //.{ "a.value = 'nok'", "nok" },
.{ .src = "a.ownerElement", .ex = "null" }, .{ "a.ownerElement", "null" },
}; }, .{});
try checkCases(js_env, &getters);
var attr = [_]Case{ try runner.testCases(&.{
.{ .src = "let b = document.getElementById('link').getAttributeNode('class')", .ex = "undefined" }, .{ "let b = document.getElementById('link').getAttributeNode('class')", "undefined" },
.{ .src = "b.name", .ex = "class" }, .{ "b.name", "class" },
.{ .src = "b.value", .ex = "ok" }, .{ "b.value", "ok" },
.{ .src = "b.value = 'nok'", .ex = "nok" }, .{ "b.value = 'nok'", "nok" },
.{ .src = "b.value", .ex = "nok" }, .{ "b.value", "nok" },
.{ .src = "b.value = null", .ex = "null" }, .{ "b.value = null", "null" },
.{ .src = "b.value", .ex = "null" }, .{ "b.value", "null" },
.{ .src = "b.value = 'ok'", .ex = "ok" }, .{ "b.value = 'ok'", "ok" },
.{ .src = "b.ownerElement.id", .ex = "link" }, .{ "b.ownerElement.id", "link" },
}; }, .{});
try checkCases(js_env, &attr);
} }

View File

@@ -18,7 +18,7 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const Text = @import("text.zig").Text; const Text = @import("text.zig").Text;
@@ -26,5 +26,4 @@ const Text = @import("text.zig").Text;
pub const CDATASection = struct { pub const CDATASection = struct {
pub const Self = parser.CDATASection; pub const Self = parser.CDATASection;
pub const prototype = *Text; pub const prototype = *Text;
pub const mem_guarantied = true;
}; };

View File

@@ -18,11 +18,7 @@
const std = @import("std"); const std = @import("std");
const jsruntime = @import("jsruntime"); const parser = @import("../netsurf.zig");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("netsurf");
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
const Comment = @import("comment.zig").Comment; const Comment = @import("comment.zig").Comment;
@@ -42,7 +38,6 @@ pub const Interfaces = .{
pub const CharacterData = struct { pub const CharacterData = struct {
pub const Self = parser.CharacterData; pub const Self = parser.CharacterData;
pub const prototype = *Node; pub const prototype = *Node;
pub const mem_guarantied = true;
// JS funcs // JS funcs
// -------- // --------
@@ -106,74 +101,65 @@ pub const CharacterData = struct {
// Tests // Tests
// ----- // -----
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.DOM.CharacterData" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
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);
var set_data = [_]Case{ try runner.testCases(&.{
.{ .src = "cdata.data = 'OK modified'", .ex = "OK modified" }, .{ "let link = document.getElementById('link')", "undefined" },
.{ .src = "cdata.data === 'OK modified'", .ex = "true" }, .{ "let cdata = link.firstChild", "undefined" },
.{ .src = "cdata.data = 'OK'", .ex = "OK" }, .{ "cdata.data", "OK" },
}; }, .{});
try checkCases(js_env, &set_data);
var get_length = [_]Case{ try runner.testCases(&.{
.{ .src = "cdata.length === 2", .ex = "true" }, .{ "cdata.data = 'OK modified'", "OK modified" },
}; .{ "cdata.data === 'OK modified'", "true" },
try checkCases(js_env, &get_length); .{ "cdata.data = 'OK'", "OK" },
}, .{});
var get_next_elem_sibling = [_]Case{ try runner.testCases(&.{
.{ .src = "cdata.nextElementSibling === null", .ex = "true" }, .{ "cdata.length === 2", "true" },
}, .{});
try runner.testCases(&.{
.{ "cdata.nextElementSibling === null", "true" },
// create a next element // create a next element
.{ .src = "let next = document.createElement('a')", .ex = "undefined" }, .{ "let next = document.createElement('a')", "undefined" },
.{ .src = "link.appendChild(next, cdata) !== undefined", .ex = "true" }, .{ "link.appendChild(next, cdata) !== undefined", "true" },
.{ .src = "cdata.nextElementSibling.localName === 'a' ", .ex = "true" }, .{ "cdata.nextElementSibling.localName === 'a' ", "true" },
}; }, .{});
try checkCases(js_env, &get_next_elem_sibling);
var get_prev_elem_sibling = [_]Case{ try runner.testCases(&.{
.{ .src = "cdata.previousElementSibling === null", .ex = "true" }, .{ "cdata.previousElementSibling === null", "true" },
// create a prev element // create a prev element
.{ .src = "let prev = document.createElement('div')", .ex = "undefined" }, .{ "let prev = document.createElement('div')", "undefined" },
.{ .src = "link.insertBefore(prev, cdata) !== undefined", .ex = "true" }, .{ "link.insertBefore(prev, cdata) !== undefined", "true" },
.{ .src = "cdata.previousElementSibling.localName === 'div' ", .ex = "true" }, .{ "cdata.previousElementSibling.localName === 'div' ", "true" },
}; }, .{});
try checkCases(js_env, &get_prev_elem_sibling);
var append_data = [_]Case{ try runner.testCases(&.{
.{ .src = "cdata.appendData(' modified')", .ex = "undefined" }, .{ "cdata.appendData(' modified')", "undefined" },
.{ .src = "cdata.data === 'OK modified' ", .ex = "true" }, .{ "cdata.data === 'OK modified' ", "true" },
}; }, .{});
try checkCases(js_env, &append_data);
var delete_data = [_]Case{ try runner.testCases(&.{
.{ .src = "cdata.deleteData('OK'.length, ' modified'.length)", .ex = "undefined" }, .{ "cdata.deleteData('OK'.length, ' modified'.length)", "undefined" },
.{ .src = "cdata.data == 'OK'", .ex = "true" }, .{ "cdata.data == 'OK'", "true" },
}; }, .{});
try checkCases(js_env, &delete_data);
var insert_data = [_]Case{ try runner.testCases(&.{
.{ .src = "cdata.insertData('OK'.length-1, 'modified')", .ex = "undefined" }, .{ "cdata.insertData('OK'.length-1, 'modified')", "undefined" },
.{ .src = "cdata.data == 'OmodifiedK'", .ex = "true" }, .{ "cdata.data == 'OmodifiedK'", "true" },
}; }, .{});
try checkCases(js_env, &insert_data);
var replace_data = [_]Case{ try runner.testCases(&.{
.{ .src = "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", .ex = "undefined" }, .{ "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", "undefined" },
.{ .src = "cdata.data == 'OreplacedK'", .ex = "true" }, .{ "cdata.data == 'OreplacedK'", "true" },
}; }, .{});
try checkCases(js_env, &replace_data);
var substring_data = [_]Case{ try runner.testCases(&.{
.{ .src = "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", .ex = "true" }, .{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
.{ .src = "cdata.substringData('OK'.length-1, 0) == ''", .ex = "true" }, .{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
}; }, .{});
try checkCases(js_env, &substring_data);
} }

View File

@@ -17,25 +17,20 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const CharacterData = @import("character_data.zig").CharacterData; 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 // https://dom.spec.whatwg.org/#interface-comment
pub const Comment = struct { pub const Comment = struct {
pub const Self = parser.Comment; pub const Self = parser.Comment;
pub const prototype = *CharacterData; 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( return parser.documentCreateComment(
parser.documentHTMLToDocument(userctx.document), parser.documentHTMLToDocument(state.document.?),
data orelse "", data orelse "",
); );
} }
@@ -44,16 +39,16 @@ pub const Comment = struct {
// Tests // Tests
// ----- // -----
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.DOM.Comment" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
var constructor = [_]Case{
.{ .src = "let comment = new Comment('foo')", .ex = "undefined" },
.{ .src = "comment.data", .ex = "foo" },
.{ .src = "let emptycomment = new Comment()", .ex = "undefined" }, try runner.testCases(&.{
.{ .src = "emptycomment.data", .ex = "" }, .{ "let comment = new Comment('foo')", "undefined" },
}; .{ "comment.data", "foo" },
try checkCases(js_env, &constructor);
.{ "let emptycomment = new Comment()", "undefined" },
.{ "emptycomment.data", "" },
}, .{});
} }

View File

@@ -18,7 +18,7 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const css = @import("../css/css.zig"); const css = @import("../css/css.zig");
const Node = @import("../css/libdom.zig").Node; const Node = @import("../css/libdom.zig").Node;

View File

@@ -0,0 +1,444 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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, .{});
}

View File

@@ -18,39 +18,30 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const SessionState = @import("../env.zig").SessionState;
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
const UserContext = @import("../user_context.zig").UserContext;
// WEB IDL https://dom.spec.whatwg.org/#documentfragment // WEB IDL https://dom.spec.whatwg.org/#documentfragment
pub const DocumentFragment = struct { pub const DocumentFragment = struct {
pub const Self = parser.DocumentFragment; pub const Self = parser.DocumentFragment;
pub const prototype = *Node; 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( 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( try runner.testCases(&.{
_: std.mem.Allocator, .{ "const dc = new DocumentFragment()", "undefined" },
js_env: *jsruntime.Env, .{ "dc.constructor.name", "DocumentFragment" },
) anyerror!void { }, .{});
var constructor = [_]Case{
.{ .src = "const dc = new DocumentFragment()", .ex = "undefined" },
.{ .src = "dc.constructor.name", .ex = "DocumentFragment" },
};
try checkCases(js_env, &constructor);
} }

View File

@@ -18,7 +18,7 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
@@ -26,7 +26,6 @@ const Node = @import("node.zig").Node;
pub const DocumentType = struct { pub const DocumentType = struct {
pub const Self = parser.DocumentType; pub const Self = parser.DocumentType;
pub const prototype = *Node; pub const prototype = *Node;
pub const mem_guarantied = true;
pub fn get_name(self: *parser.DocumentType) ![]const u8 { pub fn get_name(self: *parser.DocumentType) ![]const u8 {
return try parser.documentTypeGetName(self); return try parser.documentTypeGetName(self);

View File

@@ -22,7 +22,7 @@ const DOMImplementation = @import("implementation.zig").DOMImplementation;
const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap; const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
const DOMTokenList = @import("token_list.zig").DOMTokenList; const DOMTokenList = @import("token_list.zig").DOMTokenList;
const NodeList = @import("nodelist.zig"); const NodeList = @import("nodelist.zig");
const Nod = @import("node.zig"); const Node = @import("node.zig");
const MutationObserver = @import("mutation_observer.zig"); const MutationObserver = @import("mutation_observer.zig");
pub const Interfaces = .{ pub const Interfaces = .{
@@ -32,7 +32,7 @@ pub const Interfaces = .{
NamedNodeMap, NamedNodeMap,
DOMTokenList, DOMTokenList,
NodeList.Interfaces, NodeList.Interfaces,
Nod.Node, Node.Node,
Nod.Interfaces, Node.Interfaces,
MutationObserver.Interfaces, MutationObserver.Interfaces,
}; };

View File

@@ -18,22 +18,17 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const SessionState = @import("../env.zig").SessionState;
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const Variadic = jsruntime.Variadic;
const collection = @import("html_collection.zig"); const collection = @import("html_collection.zig");
const dump = @import("../browser/dump.zig"); const dump = @import("../dump.zig");
const css = @import("css.zig"); const css = @import("css.zig");
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
const Walker = @import("walker.zig").WalkerDepthFirst; const Walker = @import("walker.zig").WalkerDepthFirst;
const NodeList = @import("nodelist.zig").NodeList; const NodeList = @import("nodelist.zig").NodeList;
const HTMLElem = @import("../html/elements.zig"); const HTMLElem = @import("../html/elements.zig");
const UserContext = @import("../user_context.zig").UserContext;
pub const Union = @import("../html/elements.zig").Union; pub const Union = @import("../html/elements.zig").Union;
const DOMException = @import("exceptions.zig").DOMException; const DOMException = @import("exceptions.zig").DOMException;
@@ -42,7 +37,6 @@ const DOMException = @import("exceptions.zig").DOMException;
pub const Element = struct { pub const Element = struct {
pub const Self = parser.Element; pub const Self = parser.Element;
pub const prototype = *Node; pub const prototype = *Node;
pub const mem_guarantied = true;
pub const DOMRect = struct { pub const DOMRect = struct {
x: f64, x: f64,
@@ -106,8 +100,8 @@ pub const Element = struct {
return try parser.nodeGetAttributes(parser.elementToNode(self)); return try parser.nodeGetAttributes(parser.elementToNode(self));
} }
pub fn get_innerHTML(self: *parser.Element, alloc: std.mem.Allocator) ![]const u8 { pub fn get_innerHTML(self: *parser.Element, state: *SessionState) ![]const u8 {
var buf = std.ArrayList(u8).init(alloc); var buf = std.ArrayList(u8).init(state.arena);
defer buf.deinit(); defer buf.deinit();
try dump.writeChildren(parser.elementToNode(self), buf.writer()); try dump.writeChildren(parser.elementToNode(self), buf.writer());
@@ -116,8 +110,8 @@ pub const Element = struct {
return buf.toOwnedSlice(); return buf.toOwnedSlice();
} }
pub fn get_outerHTML(self: *parser.Element, alloc: std.mem.Allocator) ![]const u8 { pub fn get_outerHTML(self: *parser.Element, state: *SessionState) ![]const u8 {
var buf = std.ArrayList(u8).init(alloc); var buf = std.ArrayList(u8).init(state.arena);
defer buf.deinit(); defer buf.deinit();
try dump.writeNode(parser.elementToNode(self), buf.writer()); try dump.writeNode(parser.elementToNode(self), buf.writer());
@@ -232,11 +226,11 @@ pub const Element = struct {
pub fn _getElementsByTagName( pub fn _getElementsByTagName(
self: *parser.Element, self: *parser.Element,
alloc: std.mem.Allocator,
tag_name: []const u8, tag_name: []const u8,
state: *SessionState,
) !collection.HTMLCollection { ) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName( return try collection.HTMLCollectionByTagName(
alloc, state.arena,
parser.elementToNode(self), parser.elementToNode(self),
tag_name, tag_name,
false, false,
@@ -245,11 +239,11 @@ pub const Element = struct {
pub fn _getElementsByClassName( pub fn _getElementsByClassName(
self: *parser.Element, self: *parser.Element,
alloc: std.mem.Allocator,
classNames: []const u8, classNames: []const u8,
state: *SessionState,
) !collection.HTMLCollection { ) !collection.HTMLCollection {
return try collection.HTMLCollectionByClassName( return try collection.HTMLCollectionByClassName(
alloc, state.arena,
parser.elementToNode(self), parser.elementToNode(self),
classNames, classNames,
false, 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; 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; if (n == null) return null;
return try toInterface(parser.nodeToElement(n.?)); return try toInterface(parser.nodeToElement(n.?));
} }
pub fn _querySelectorAll(self: *parser.Element, alloc: std.mem.Allocator, selector: []const u8) !NodeList { pub fn _querySelectorAll(self: *parser.Element, selector: []const u8, state: *SessionState) !NodeList {
return css.querySelectorAll(alloc, parser.elementToNode(self), selector); return css.querySelectorAll(state.arena, parser.elementToNode(self), selector);
} }
// TODO according with https://dom.spec.whatwg.org/#parentnode, the // TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string. // function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 // 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); return Node.prepend(parser.elementToNode(self), nodes);
} }
// TODO according with https://dom.spec.whatwg.org/#parentnode, the // TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string. // function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 // 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); return Node.append(parser.elementToNode(self), nodes);
} }
// TODO according with https://dom.spec.whatwg.org/#parentnode, the // TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string. // function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 // 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); return Node.replaceChildren(parser.elementToNode(self), nodes);
} }
pub fn _getBoundingClientRect(self: *parser.Element, user_context: UserContext) !DOMRect { pub fn _getBoundingClientRect(self: *parser.Element, state: *SessionState) !DOMRect {
return user_context.renderer.getRect(self); return state.renderer.getRect(self);
} }
pub fn get_clientWidth(_: *parser.Element, user_context: UserContext) u32 { pub fn get_clientWidth(_: *parser.Element, state: *SessionState) u32 {
return user_context.renderer.width(); return state.renderer.width();
} }
pub fn get_clientHeight(_: *parser.Element, user_context: UserContext) u32 { pub fn get_clientHeight(_: *parser.Element, state: *SessionState) u32 {
return user_context.renderer.height(); return state.renderer.height();
} }
pub fn deinit(_: *parser.Element, _: std.mem.Allocator) void {} pub fn deinit(_: *parser.Element, _: std.mem.Allocator) void {}
@@ -365,172 +359,161 @@ pub const Element = struct {
// Tests // Tests
// ----- // -----
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.DOM.Element" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
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);
var gettersetters = [_]Case{ try runner.testCases(&.{
.{ .src = "let gs = document.getElementById('content')", .ex = "undefined" }, .{ "let g = document.getElementById('content')", "undefined" },
.{ .src = "gs.id", .ex = "content" }, .{ "g.namespaceURI", "http://www.w3.org/1999/xhtml" },
.{ .src = "gs.id = 'foo'", .ex = "foo" }, .{ "g.prefix", "null" },
.{ .src = "gs.id", .ex = "foo" }, .{ "g.localName", "div" },
.{ .src = "gs.id = 'content'", .ex = "content" }, .{ "g.tagName", "DIV" },
.{ .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);
var attribute = [_]Case{ try runner.testCases(&.{
.{ .src = "let a = document.getElementById('content')", .ex = "undefined" }, .{ "let gs = document.getElementById('content')", "undefined" },
.{ .src = "a.hasAttributes()", .ex = "true" }, .{ "gs.id", "content" },
.{ .src = "a.attributes.length", .ex = "1" }, .{ "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" }, .{ "a.getAttribute('id')", "content" },
.{ .src = "a.getAttribute('foo')", .ex = "null" },
.{ .src = "a.setAttribute('foo', 'bar')", .ex = "undefined" }, .{ "a.hasAttribute('foo')", "false" },
.{ .src = "a.hasAttribute('foo')", .ex = "true" }, .{ "a.getAttribute('foo')", "null" },
.{ .src = "a.getAttribute('foo')", .ex = "bar" },
.{ .src = "a.setAttribute('foo', 'baz')", .ex = "undefined" }, .{ "a.setAttribute('foo', 'bar')", "undefined" },
.{ .src = "a.hasAttribute('foo')", .ex = "true" }, .{ "a.hasAttribute('foo')", "true" },
.{ .src = "a.getAttribute('foo')", .ex = "baz" }, .{ "a.getAttribute('foo')", "bar" },
.{ .src = "a.removeAttribute('foo')", .ex = "undefined" }, .{ "a.setAttribute('foo', 'baz')", "undefined" },
.{ .src = "a.hasAttribute('foo')", .ex = "false" }, .{ "a.hasAttribute('foo')", "true" },
.{ .src = "a.getAttribute('foo')", .ex = "null" }, .{ "a.getAttribute('foo')", "baz" },
};
try checkCases(js_env, &attribute);
var toggleAttr = [_]Case{ .{ "a.removeAttribute('foo')", "undefined" },
.{ .src = "let b = document.getElementById('content')", .ex = "undefined" }, .{ "a.hasAttribute('foo')", "false" },
.{ .src = "b.toggleAttribute('foo')", .ex = "true" }, .{ "a.getAttribute('foo')", "null" },
.{ .src = "b.hasAttribute('foo')", .ex = "true" }, }, .{});
.{ .src = "b.getAttribute('foo')", .ex = "" },
.{ .src = "b.toggleAttribute('foo')", .ex = "false" }, try runner.testCases(&.{
.{ .src = "b.hasAttribute('foo')", .ex = "false" }, .{ "let b = document.getElementById('content')", "undefined" },
}; .{ "b.toggleAttribute('foo')", "true" },
try checkCases(js_env, &toggleAttr); .{ "b.hasAttribute('foo')", "true" },
.{ "b.getAttribute('foo')", "" },
var parentNode = [_]Case{ .{ "b.toggleAttribute('foo')", "false" },
.{ .src = "let c = document.getElementById('content')", .ex = "undefined" }, .{ "b.hasAttribute('foo')", "false" },
.{ .src = "c.children.length", .ex = "3" }, }, .{});
.{ .src = "c.firstElementChild.nodeName", .ex = "A" },
.{ .src = "c.lastElementChild.nodeName", .ex = "P" },
.{ .src = "c.childElementCount", .ex = "3" },
.{ .src = "c.prepend(document.createTextNode('foo'))", .ex = "undefined" }, try runner.testCases(&.{
.{ .src = "c.append(document.createTextNode('bar'))", .ex = "undefined" }, .{ "let c = document.getElementById('content')", "undefined" },
}; .{ "c.children.length", "3" },
try checkCases(js_env, &parentNode); .{ "c.firstElementChild.nodeName", "A" },
.{ "c.lastElementChild.nodeName", "P" },
.{ "c.childElementCount", "3" },
var elementSibling = [_]Case{ .{ "c.prepend(document.createTextNode('foo'))", "undefined" },
.{ .src = "let d = document.getElementById('para')", .ex = "undefined" }, .{ "c.append(document.createTextNode('bar'))", "undefined" },
.{ .src = "d.previousElementSibling.nodeName", .ex = "P" }, }, .{});
.{ .src = "d.nextElementSibling", .ex = "null" },
};
try checkCases(js_env, &elementSibling);
var querySelector = [_]Case{ try runner.testCases(&.{
.{ .src = "let e = document.getElementById('content')", .ex = "undefined" }, .{ "let d = document.getElementById('para')", "undefined" },
.{ .src = "e.querySelector('foo')", .ex = "null" }, .{ "d.previousElementSibling.nodeName", "P" },
.{ .src = "e.querySelector('#foo')", .ex = "null" }, .{ "d.nextElementSibling", "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" },
.{ .src = "e.querySelectorAll('foo').length", .ex = "0" }, try runner.testCases(&.{
.{ .src = "e.querySelectorAll('#foo').length", .ex = "0" }, .{ "let e = document.getElementById('content')", "undefined" },
.{ .src = "e.querySelectorAll('#link').length", .ex = "1" }, .{ "e.querySelector('foo')", "null" },
.{ .src = "e.querySelectorAll('#link').item(0).id", .ex = "link" }, .{ "e.querySelector('#foo')", "null" },
.{ .src = "e.querySelectorAll('#para').length", .ex = "1" }, .{ "e.querySelector('#link').id", "link" },
.{ .src = "e.querySelectorAll('#para').item(0).id", .ex = "para" }, .{ "e.querySelector('#para').id", "para" },
.{ .src = "e.querySelectorAll('*').length", .ex = "4" }, .{ "e.querySelector('*').id", "link" },
.{ .src = "e.querySelectorAll('p').length", .ex = "2" }, .{ "e.querySelector('')", "null" },
.{ .src = "e.querySelectorAll('.ok').item(0).id", .ex = "link" }, .{ "e.querySelector('*').id", "link" },
}; .{ "e.querySelector('#content')", "null" },
try checkCases(js_env, &querySelector); .{ "e.querySelector('#para').id", "para" },
.{ "e.querySelector('.ok').id", "link" },
.{ "e.querySelector('a ~ p').id", "para-empty" },
var attrNode = [_]Case{ .{ "e.querySelectorAll('foo').length", "0" },
.{ .src = "let f = document.getElementById('content')", .ex = "undefined" }, .{ "e.querySelectorAll('#foo').length", "0" },
.{ .src = "let ff = document.createAttribute('foo')", .ex = "undefined" }, .{ "e.querySelectorAll('#link').length", "1" },
.{ .src = "f.setAttributeNode(ff)", .ex = "null" }, .{ "e.querySelectorAll('#link').item(0).id", "link" },
.{ .src = "f.getAttributeNode('foo').name", .ex = "foo" }, .{ "e.querySelectorAll('#para').length", "1" },
.{ .src = "f.removeAttributeNode(ff).name", .ex = "foo" }, .{ "e.querySelectorAll('#para').item(0).id", "para" },
.{ .src = "f.getAttributeNode('bar')", .ex = "null" }, .{ "e.querySelectorAll('*').length", "4" },
}; .{ "e.querySelectorAll('p').length", "2" },
try checkCases(js_env, &attrNode); .{ "e.querySelectorAll('.ok').item(0).id", "link" },
}, .{});
var innerHTML = [_]Case{ try runner.testCases(&.{
.{ .src = "document.getElementById('para').innerHTML", .ex = " And" }, .{ "let f = document.getElementById('content')", "undefined" },
.{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "<span id=\"para-empty-child\"></span>" }, .{ "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" }, try runner.testCases(&.{
.{ .src = "const prev = h.innerHTML", .ex = "undefined" }, .{ "document.getElementById('para').innerHTML", " And" },
.{ .src = "h.innerHTML = '<p id=\"hello\">hello world</p>'", .ex = "<p id=\"hello\">hello world</p>" }, .{ "document.getElementById('para-empty').innerHTML.trim()", "<span id=\"para-empty-child\"></span>" },
.{ .src = "h.innerHTML", .ex = "<p id=\"hello\">hello world</p>" },
.{ .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 = "<span id=\"para-empty-child\"></span>" },
};
try checkCases(js_env, &innerHTML);
var outerHTML = [_]Case{ .{ "let h = document.getElementById('para-empty')", "undefined" },
.{ .src = "document.getElementById('para').outerHTML", .ex = "<p id=\"para\"> And</p>" }, .{ "const prev = h.innerHTML", "undefined" },
}; .{ "h.innerHTML = '<p id=\"hello\">hello world</p>'", "<p id=\"hello\">hello world</p>" },
.{ "h.innerHTML", "<p id=\"hello\">hello world</p>" },
.{ "h.firstChild.nodeName", "P" },
.{ "h.firstChild.id", "hello" },
.{ "h.firstChild.textContent", "hello world" },
.{ "h.innerHTML = prev; true", "true" },
.{ "document.getElementById('para-empty').innerHTML.trim()", "<span id=\"para-empty-child\"></span>" },
}, .{});
var getBoundingClientRect = [_]Case{ try runner.testCases(&.{
.{ .src = "document.getElementById('para').clientWidth", .ex = "0" }, .{ "document.getElementById('para').outerHTML", "<p id=\"para\"> And</p>" },
.{ .src = "document.getElementById('para').clientHeight", .ex = "1" }, }, .{});
.{ .src = "let r1 = document.getElementById('para').getBoundingClientRect()", .ex = "undefined" }, try runner.testCases(&.{
.{ .src = "r1.x", .ex = "1" }, .{ "document.getElementById('para').clientWidth", "0" },
.{ .src = "r1.y", .ex = "0" }, .{ "document.getElementById('para').clientHeight", "1" },
.{ .src = "r1.width", .ex = "1" },
.{ .src = "r1.height", .ex = "1" },
.{ .src = "let r2 = document.getElementById('content').getBoundingClientRect()", .ex = "undefined" }, .{ "let r1 = document.getElementById('para').getBoundingClientRect()", "undefined" },
.{ .src = "r2.x", .ex = "2" }, .{ "r1.x", "1" },
.{ .src = "r2.y", .ex = "0" }, .{ "r1.y", "0" },
.{ .src = "r2.width", .ex = "1" }, .{ "r1.width", "1" },
.{ .src = "r2.height", .ex = "1" }, .{ "r1.height", "1" },
.{ .src = "let r3 = document.getElementById('para').getBoundingClientRect()", .ex = "undefined" }, .{ "let r2 = document.getElementById('content').getBoundingClientRect()", "undefined" },
.{ .src = "r3.x", .ex = "1" }, .{ "r2.x", "2" },
.{ .src = "r3.y", .ex = "0" }, .{ "r2.y", "0" },
.{ .src = "r3.width", .ex = "1" }, .{ "r2.width", "1" },
.{ .src = "r3.height", .ex = "1" }, .{ "r2.height", "1" },
.{ .src = "document.getElementById('para').clientWidth", .ex = "2" }, .{ "let r3 = document.getElementById('para').getBoundingClientRect()", "undefined" },
.{ .src = "document.getElementById('para').clientHeight", .ex = "1" }, .{ "r3.x", "1" },
}; .{ "r3.y", "0" },
try checkCases(js_env, &getBoundingClientRect); .{ "r3.width", "1" },
.{ "r3.height", "1" },
try checkCases(js_env, &outerHTML); .{ "document.getElementById('para').clientWidth", "2" },
.{ "document.getElementById('para').clientHeight", "1" },
}, .{});
} }

View File

@@ -0,0 +1,226 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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" },
}, .{});
}

View File

@@ -19,19 +19,13 @@
const std = @import("std"); const std = @import("std");
const allocPrint = std.fmt.allocPrint; const allocPrint = std.fmt.allocPrint;
const jsruntime = @import("jsruntime"); const parser = @import("../netsurf.zig");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("netsurf");
// https://webidl.spec.whatwg.org/#idl-DOMException // https://webidl.spec.whatwg.org/#idl-DOMException
pub const DOMException = struct { pub const DOMException = struct {
err: parser.DOMError, err: parser.DOMError,
str: []const u8, str: []const u8,
pub const mem_guarantied = true;
pub const ErrorSet = parser.DOMError; pub const ErrorSet = parser.DOMError;
// static attributes // static attributes
@@ -62,7 +56,7 @@ pub const DOMException = struct {
pub const _DATA_CLONE_ERR = 25; pub const _DATA_CLONE_ERR = 25;
// TODO: deinit // 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 errCast = @as(parser.DOMError, @errorCast(err));
const errName = DOMException.name(errCast); const errName = DOMException.name(errCast);
const str = switch (errCast) { const str = switch (errCast) {
@@ -120,7 +114,7 @@ pub const DOMException = struct {
// JS properties and methods // JS properties and methods
pub fn get_code(self: DOMException) u8 { pub fn get_code(self: *const DOMException) u8 {
return switch (self.err) { return switch (self.err) {
error.IndexSize => 1, error.IndexSize => 1,
error.StringSize => 2, 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); 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); const errName = DOMException.name(self.err);
return self.str[errName.len + 2 ..]; return self.str[errName.len + 2 ..];
} }
pub fn _toString(self: DOMException) []const u8 { pub fn _toString(self: *const DOMException) []const u8 {
return self.str; 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."; const err = "Failed to execute 'appendChild' on 'Node': The new child element contains the parent.";
var cases = [_]Case{ try runner.testCases(&.{
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, .{ "let content = document.getElementById('content')", "undefined" },
.{ .src = "let link = document.getElementById('link')", .ex = "undefined" }, .{ "let link = document.getElementById('link')", "undefined" },
// HierarchyRequestError // HierarchyRequestError
.{ .src = "var HierarchyRequestError; try {link.appendChild(content)} catch (error) {HierarchyRequestError = error} HierarchyRequestError.name", .ex = "HierarchyRequestError" }, .{
.{ .src = "HierarchyRequestError.code", .ex = "3" }, \\ var he;
.{ .src = "HierarchyRequestError.message", .ex = err }, \\ try { link.appendChild(content) } catch (error) { he = error}
.{ .src = "HierarchyRequestError.toString()", .ex = "HierarchyRequestError: " ++ err }, \\ he.name
.{ .src = "HierarchyRequestError instanceof DOMException", .ex = "true" }, ,
.{ .src = "HierarchyRequestError instanceof Error", .ex = "true" }, "HierarchyRequestError",
}; },
try checkCases(js_env, &cases); .{ "he.code", "3" },
.{ "he.message", err },
.{ "he.toString()", "HierarchyRequestError: " ++ err },
.{ "he instanceof DOMException", "true" },
.{ "he instanceof Error", "true" },
}, .{});
} }

View File

@@ -18,17 +18,14 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const utils = @import("utils.z"); const utils = @import("utils.z");
const Element = @import("element.zig").Element; const Element = @import("element.zig").Element;
const Union = @import("element.zig").Union; const Union = @import("element.zig").Union;
const JsObject = @import("../env.zig").JsObject;
const Walker = @import("walker.zig").Walker; const Walker = @import("walker.zig").Walker;
const WalkerDepthFirst = @import("walker.zig").WalkerDepthFirst; const WalkerDepthFirst = @import("walker.zig").WalkerDepthFirst;
const WalkerChildren = @import("walker.zig").WalkerChildren; const WalkerChildren = @import("walker.zig").WalkerChildren;
@@ -279,8 +276,6 @@ pub fn HTMLCollectionByAnchors(
} }
pub const HTMLCollectionIterator = struct { pub const HTMLCollectionIterator = struct {
pub const mem_guarantied = true;
coll: *HTMLCollection, coll: *HTMLCollection,
index: u32 = 0, index: u32 = 0,
@@ -311,8 +306,6 @@ pub const HTMLCollectionIterator = struct {
// dom_html_collection expects a comparison function callback as arguement. // dom_html_collection expects a comparison function callback as arguement.
// But we wanted a dynamically comparison here, according to the match tagname. // But we wanted a dynamically comparison here, according to the match tagname.
pub const HTMLCollection = struct { pub const HTMLCollection = struct {
pub const mem_guarantied = true;
matcher: Matcher, matcher: Matcher,
walker: Walker, walker: Walker,
@@ -327,10 +320,6 @@ pub const HTMLCollection = struct {
cur_idx: ?u32 = undefined, cur_idx: ?u32 = undefined,
cur_node: ?*parser.Node = 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. // start returns the first node to walk on.
fn start(self: HTMLCollection) !?*parser.Node { fn start(self: HTMLCollection) !?*parser.Node {
if (self.root == null) return null; if (self.root == null) return null;
@@ -412,7 +401,7 @@ pub const HTMLCollection = struct {
return try Element.toInterface(e); 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 (self.root == null) return null;
if (name.len == 0) return null; if (name.len == 0) return null;
@@ -454,81 +443,67 @@ pub const HTMLCollection = struct {
return null; return null;
} }
pub fn postAttach(self: *HTMLCollection, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void { pub fn postAttach(self: *HTMLCollection, js_obj: JsObject) !void {
const ln = try self.get_length(); const len = try self.get_length();
var i: u32 = 0; for (0..len) |i| {
while (i < ln) { const node = try self.item(@intCast(i)) orelse unreachable;
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;
const e = @as(*parser.Element, @ptrCast(node)); 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| { if (try item_name(e)) |name| {
try js_obj.set(name, e); 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( try runner.testCases(&.{
_: std.mem.Allocator, .{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
js_env: *jsruntime.Env, .{ "getElementsByTagName.length", "2" },
) anyerror!void { .{ "let getElementsByTagNameCI = document.getElementsByTagName('P')", "undefined" },
var getElementsByTagName = [_]Case{ .{ "getElementsByTagNameCI.length", "2" },
.{ .src = "let getElementsByTagName = document.getElementsByTagName('p')", .ex = "undefined" }, .{ "getElementsByTagName.item(0).localName", "p" },
.{ .src = "getElementsByTagName.length", .ex = "2" }, .{ "getElementsByTagName.item(1).localName", "p" },
.{ .src = "let getElementsByTagNameCI = document.getElementsByTagName('P')", .ex = "undefined" }, .{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
.{ .src = "getElementsByTagNameCI.length", .ex = "2" }, .{ "getElementsByTagNameAll.length", "8" },
.{ .src = "getElementsByTagName.item(0).localName", .ex = "p" }, .{ "getElementsByTagNameAll.item(0).localName", "html" },
.{ .src = "getElementsByTagName.item(1).localName", .ex = "p" }, .{ "getElementsByTagNameAll.item(0).localName", "html" },
.{ .src = "let getElementsByTagNameAll = document.getElementsByTagName('*')", .ex = "undefined" }, .{ "getElementsByTagNameAll.item(1).localName", "head" },
.{ .src = "getElementsByTagNameAll.length", .ex = "8" }, .{ "getElementsByTagNameAll.item(0).localName", "html" },
.{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" }, .{ "getElementsByTagNameAll.item(2).localName", "body" },
.{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" }, .{ "getElementsByTagNameAll.item(3).localName", "div" },
.{ .src = "getElementsByTagNameAll.item(1).localName", .ex = "head" }, .{ "getElementsByTagNameAll.item(7).localName", "p" },
.{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" }, .{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
.{ .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" },
// array like // array like
.{ .src = "getElementsByTagNameAll[0].localName", .ex = "html" }, .{ "getElementsByTagNameAll[0].localName", "html" },
.{ .src = "getElementsByTagNameAll[7].localName", .ex = "p" }, .{ "getElementsByTagNameAll[7].localName", "p" },
.{ .src = "getElementsByTagNameAll[8]", .ex = "undefined" }, .{ "getElementsByTagNameAll[8]", "undefined" },
.{ .src = "getElementsByTagNameAll['para-empty-child'].localName", .ex = "span" }, .{ "getElementsByTagNameAll['para-empty-child'].localName", "span" },
.{ .src = "getElementsByTagNameAll['foo']", .ex = "undefined" }, .{ "getElementsByTagNameAll['foo']", "undefined" },
.{ .src = "document.getElementById('content').getElementsByTagName('*').length", .ex = "4" }, .{ "document.getElementById('content').getElementsByTagName('*').length", "4" },
.{ .src = "document.getElementById('content').getElementsByTagName('p').length", .ex = "2" }, .{ "document.getElementById('content').getElementsByTagName('p').length", "2" },
.{ .src = "document.getElementById('content').getElementsByTagName('div').length", .ex = "0" }, .{ "document.getElementById('content').getElementsByTagName('div').length", "0" },
.{ .src = "document.children.length", .ex = "1" }, .{ "document.children.length", "1" },
.{ .src = "document.getElementById('content').children.length", .ex = "3" }, .{ "document.getElementById('content').children.length", "3" },
// check liveness // check liveness
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, .{ "let content = document.getElementById('content')", "undefined" },
.{ .src = "let pe = document.getElementById('para-empty')", .ex = "undefined" }, .{ "let pe = document.getElementById('para-empty')", "undefined" },
.{ .src = "let p = document.createElement('p')", .ex = "undefined" }, .{ "let p = document.createElement('p')", "undefined" },
.{ .src = "p.textContent = 'OK live'", .ex = "OK live" }, .{ "p.textContent = 'OK live'", "OK live" },
.{ .src = "getElementsByTagName.item(1).textContent", .ex = " And" }, .{ "getElementsByTagName.item(1).textContent", " And" },
.{ .src = "content.appendChild(p) != undefined", .ex = "true" }, .{ "content.appendChild(p) != undefined", "true" },
.{ .src = "getElementsByTagName.length", .ex = "3" }, .{ "getElementsByTagName.length", "3" },
.{ .src = "getElementsByTagName.item(2).textContent", .ex = "OK live" }, .{ "getElementsByTagName.item(2).textContent", "OK live" },
.{ .src = "content.insertBefore(p, pe) != undefined", .ex = "true" }, .{ "content.insertBefore(p, pe) != undefined", "true" },
.{ .src = "getElementsByTagName.item(0).textContent", .ex = "OK live" }, .{ "getElementsByTagName.item(0).textContent", "OK live" },
}; }, .{});
try checkCases(js_env, &getElementsByTagName);
} }

View File

@@ -18,11 +18,8 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const SessionState = @import("../env.zig").SessionState;
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const Document = @import("document.zig").Document; const Document = @import("document.zig").Document;
const DocumentType = @import("document_type.zig").DocumentType; 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 // WEB IDL https://dom.spec.whatwg.org/#domimplementation
pub const DOMImplementation = struct { pub const DOMImplementation = struct {
pub const mem_guarantied = true;
pub const Exception = DOMException; pub const Exception = DOMException;
pub fn _createDocumentType( pub fn _createDocumentType(
_: *DOMImplementation, _: *DOMImplementation,
alloc: std.mem.Allocator,
qname: []const u8, qname: []const u8,
publicId: []const u8, publicId: []const u8,
systemId: []const u8, systemId: []const u8,
state: *SessionState,
) !*parser.DocumentType { ) !*parser.DocumentType {
const cqname = try alloc.dupeZ(u8, qname); const allocator = state.arena;
defer alloc.free(cqname); const cqname = try allocator.dupeZ(u8, qname);
defer allocator.free(cqname);
const cpublicId = try alloc.dupeZ(u8, publicId); const cpublicId = try allocator.dupeZ(u8, publicId);
defer alloc.free(cpublicId); defer allocator.free(cpublicId);
const csystemId = try alloc.dupeZ(u8, systemId); const csystemId = try allocator.dupeZ(u8, systemId);
defer alloc.free(csystemId); defer allocator.free(csystemId);
return try parser.domImplementationCreateDocumentType(cqname, cpublicId, csystemId); return try parser.domImplementationCreateDocumentType(cqname, cpublicId, csystemId);
} }
pub fn _createDocument( pub fn _createDocument(
_: *DOMImplementation, _: *DOMImplementation,
alloc: std.mem.Allocator,
namespace: ?[]const u8, namespace: ?[]const u8,
qname: ?[]const u8, qname: ?[]const u8,
doctype: ?*parser.DocumentType, doctype: ?*parser.DocumentType,
state: *SessionState,
) !*parser.Document { ) !*parser.Document {
const allocator = state.arena;
var cnamespace: ?[:0]const u8 = null; var cnamespace: ?[:0]const u8 = null;
if (namespace) |ns| { 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; var cqname: ?[:0]const u8 = null;
if (qname) |qn| { 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); return try parser.domImplementationCreateDocument(cnamespace, cqname, doctype);
} }
@@ -89,17 +86,17 @@ pub const DOMImplementation = struct {
// Tests // Tests
// ----- // -----
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.DOM.Implementation" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
var getImplementation = [_]Case{
.{ .src = "let impl = document.implementation", .ex = "undefined" }, try runner.testCases(&.{
.{ .src = "impl.createHTMLDocument();", .ex = "[object HTMLDocument]" }, .{ "let impl = document.implementation", "undefined" },
.{ .src = "impl.createHTMLDocument('foo');", .ex = "[object HTMLDocument]" }, .{ "impl.createHTMLDocument();", "[object HTMLDocument]" },
.{ .src = "impl.createDocument(null, 'foo');", .ex = "[object Document]" }, .{ "impl.createHTMLDocument('foo');", "[object HTMLDocument]" },
.{ .src = "impl.createDocumentType('foo', 'bar', 'baz')", .ex = "[object DocumentType]" }, .{ "impl.createDocument(null, 'foo');", "[object Document]" },
.{ .src = "impl.hasFeature()", .ex = "true" }, .{ "impl.createDocumentType('foo', 'bar', 'baz')", "[object DocumentType]" },
}; .{ "impl.hasFeature()", "true" },
try checkCases(js_env, &getImplementation); }, .{});
} }

View File

@@ -18,14 +18,11 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const SessionState = @import("../env.zig").SessionState;
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 Env = @import("../env.zig").Env;
const JsObject = @import("../env.zig").JsObject;
const NodeList = @import("nodelist.zig").NodeList; const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = .{ pub const Interfaces = .{
@@ -40,20 +37,18 @@ const log = std.log.scoped(.events);
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver // WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
pub const MutationObserver = struct { pub const MutationObserver = struct {
cbk: Callback, cbk: Env.Callback,
observers: Observers, observers: Observers,
pub const mem_guarantied = true;
const Observer = struct { const Observer = struct {
node: *parser.Node, node: *parser.Node,
options: MutationObserverInit, options: MutationObserverInit,
}; };
const deinitFunc = struct { 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)); const o: *Observer = @ptrCast(@alignCast(ctx));
alloc.destroy(o); allocator.destroy(o);
} }
}.deinit; }.deinit;
@@ -78,7 +73,7 @@ pub const MutationObserver = struct {
} }
}; };
pub fn constructor(cbk: Callback) !MutationObserver { pub fn constructor(cbk: Env.Callback) !MutationObserver {
return MutationObserver{ return MutationObserver{
.cbk = cbk, .cbk = cbk,
.observers = .{}, .observers = .{},
@@ -90,22 +85,23 @@ pub const MutationObserver = struct {
return opt orelse .{}; return opt orelse .{};
} }
pub fn _observe(self: *MutationObserver, alloc: std.mem.Allocator, node: *parser.Node, options: ?MutationObserverInit) !void { pub fn _observe(self: *MutationObserver, node: *parser.Node, options: ?MutationObserverInit, state: *SessionState) !void {
const o = try alloc.create(Observer); const arena = state.arena;
const o = try arena.create(Observer);
o.* = .{ o.* = .{
.node = node, .node = node,
.options = resolveOptions(options), .options = resolveOptions(options),
}; };
errdefer alloc.destroy(o); errdefer arena.destroy(o);
// register the new observer. // register the new observer.
try self.observers.append(alloc, o); try self.observers.append(arena, o);
// register node's events. // register node's events.
if (o.options.childList or o.options.subtree) { if (o.options.childList or o.options.subtree) {
try parser.eventTargetAddEventListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
alloc, arena,
"DOMNodeInserted", "DOMNodeInserted",
EventHandler, EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc }, .{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
@@ -113,7 +109,7 @@ pub const MutationObserver = struct {
); );
try parser.eventTargetAddEventListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
alloc, arena,
"DOMNodeRemoved", "DOMNodeRemoved",
EventHandler, EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc }, .{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
@@ -123,7 +119,7 @@ pub const MutationObserver = struct {
if (o.options.attr()) { if (o.options.attr()) {
try parser.eventTargetAddEventListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
alloc, arena,
"DOMAttrModified", "DOMAttrModified",
EventHandler, EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc }, .{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
@@ -133,7 +129,7 @@ pub const MutationObserver = struct {
if (o.options.cdata()) { if (o.options.cdata()) {
try parser.eventTargetAddEventListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
alloc, arena,
"DOMCharacterDataModified", "DOMCharacterDataModified",
EventHandler, EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc }, .{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
@@ -143,7 +139,7 @@ pub const MutationObserver = struct {
if (o.options.subtree) { if (o.options.subtree) {
try parser.eventTargetAddEventListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
alloc, arena,
"DOMSubtreeModified", "DOMSubtreeModified",
EventHandler, EventHandler,
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc }, .{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
@@ -157,14 +153,17 @@ pub const MutationObserver = struct {
// TODO unregister listeners. // 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. // TODO unregister listeners.
for (self.observers.items) |o| alloc.destroy(o); for (self.observers.items) |o| {
self.observers.deinit(alloc); arena.destroy(o);
}
self.observers.deinit(arena);
} }
// TODO // TODO
pub fn _takeRecords(_: MutationObserver) ?[]const u8 { pub fn _takeRecords(_: *const MutationObserver) ?[]const u8 {
return &[_]u8{}; return &[_]u8{};
} }
}; };
@@ -173,15 +172,19 @@ pub const MutationObserver = struct {
pub const MutationRecords = struct { pub const MutationRecords = struct {
first: ?MutationRecord = null, first: ?MutationRecord = null,
pub const mem_guarantied = true; pub fn get_length(self: *const MutationRecords) u32 {
pub fn get_length(self: *MutationRecords) u32 {
if (self.first == null) return 0; if (self.first == null) return 0;
return 1; return 1;
} }
pub fn indexed_get(self: *const MutationRecords, i: u32, has_value: *bool) ?MutationRecord {
pub fn postAttach(self: *MutationRecords, js_obj: jsruntime.JSObject) !void { _ = i;
return self.first orelse {
has_value.* = false;
return null;
};
}
pub fn postAttach(self: *const MutationRecords, js_obj: JsObject) !void {
if (self.first) |mr| { if (self.first) |mr| {
try js_obj.set("0", mr); try js_obj.set("0", mr);
} }
@@ -199,41 +202,39 @@ pub const MutationRecord = struct {
attributeNamespace: ?[]const u8 = null, attributeNamespace: ?[]const u8 = null,
oldValue: ?[]const u8 = null, oldValue: ?[]const u8 = null,
pub const mem_guarantied = true; pub fn get_type(self: *const MutationRecord) []const u8 {
pub fn get_type(self: MutationRecord) []const u8 {
return self.type; return self.type;
} }
pub fn get_addedNodes(self: MutationRecord) NodeList { pub fn get_addedNodes(self: *const MutationRecord) NodeList {
return self.addedNodes; return self.addedNodes;
} }
pub fn get_removedNodes(self: MutationRecord) NodeList { pub fn get_removedNodes(self: *const MutationRecord) NodeList {
return self.addedNodes; return self.addedNodes;
} }
pub fn get_target(self: MutationRecord) *parser.Node { pub fn get_target(self: *const MutationRecord) *parser.Node {
return self.target; return self.target;
} }
pub fn get_attributeName(self: MutationRecord) ?[]const u8 { pub fn get_attributeName(self: *const MutationRecord) ?[]const u8 {
return self.attributeName; return self.attributeName;
} }
pub fn get_attributeNamespace(self: MutationRecord) ?[]const u8 { pub fn get_attributeNamespace(self: *const MutationRecord) ?[]const u8 {
return self.attributeNamespace; return self.attributeNamespace;
} }
pub fn get_previousSibling(self: MutationRecord) ?*parser.Node { pub fn get_previousSibling(self: *const MutationRecord) ?*parser.Node {
return self.previousSibling; return self.previousSibling;
} }
pub fn get_nextSibling(self: MutationRecord) ?*parser.Node { pub fn get_nextSibling(self: *const MutationRecord) ?*parser.Node {
return self.nextSibling; return self.nextSibling;
} }
pub fn get_oldValue(self: MutationRecord) ?[]const u8 { pub fn get_oldValue(self: *const MutationRecord) ?[]const u8 {
return self.oldValue; return self.oldValue;
} }
}; };
@@ -283,7 +284,7 @@ const EventHandler = struct {
const muevt = parser.eventToMutationEvent(evt.?); const muevt = parser.eventToMutationEvent(evt.?);
// TODO get the allocator by another way? // 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")) { if (std.mem.eql(u8, t, "DOMAttrModified")) {
mrs.first = .{ mrs.first = .{
@@ -340,66 +341,63 @@ const EventHandler = struct {
return; return;
} }
var res = CallbackResult.init(alloc);
defer res.deinit();
// TODO pass MutationRecords and MutationObserver // TODO pass MutationRecords and MutationObserver
data.cbk.trycall(.{mrs}, &res) catch |e| log.err("mutation event handler error: {any}", .{e}); var result: Env.Callback.Result = undefined;
data.cbk.tryCall(.{mrs}, &result) catch {
// in case of function error, we log the result and the trace. log.err("mutation observer callback error: {s}", .{result.exception});
if (!res.success) { log.debug("stack:\n{s}", .{result.stack orelse "???"});
log.info("mutation observer event handler error: {s}", .{res.result orelse "unknown"}); };
log.debug("{s}", .{res.stack orelse "no stack trace"});
}
} }
}.handle; }.handle;
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.DOM.MutationObserver" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
var constructor = [_]Case{
.{ .src = "new MutationObserver(() => {}).observe(document, { childList: true });", .ex = "undefined" },
};
try checkCases(js_env, &constructor);
var attr = [_]Case{ try runner.testCases(&.{
.{ .src = .{ "new MutationObserver(() => {}).observe(document, { childList: true });", "undefined" },
\\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);
var cdata = [_]Case{ try runner.testCases(&.{
.{ .src = .{
\\var node = document.getElementById("para").firstChild; \\ var nb = 0;
\\var nb2 = 0; \\ var mrs;
\\var mrs2; \\ new MutationObserver((mu) => {
\\new MutationObserver((mu) => { \\ mrs = mu;
\\ mrs2 = mu; \\ nb++;
\\ nb2++; \\ }).observe(document.firstElementChild, { attributes: true, attributeOldValue: true });
\\}).observe(node, { characterData: true, characterDataOldValue: true }); \\ document.firstElementChild.setAttribute("foo", "bar");
\\node.data = "foo"; \\ // ignored b/c it's about another target.
\\nb2; \\ document.firstElementChild.firstChild.setAttribute("foo", "bar");
, .ex = "1" }, \\ nb;
.{ .src = "mrs2[0].type", .ex = "characterData" }, ,
.{ .src = "mrs2[0].target == node", .ex = "true" }, "1",
.{ .src = "mrs2[0].target.data", .ex = "foo" }, },
.{ .src = "mrs2[0].oldValue", .ex = " And" }, .{ "mrs[0].type", "attributes" },
}; .{ "mrs[0].target == document.firstElementChild", "true" },
try checkCases(js_env, &cdata); .{ "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" },
}, .{});
} }

View File

@@ -18,18 +18,13 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const DOMException = @import("exceptions.zig").DOMException; const DOMException = @import("exceptions.zig").DOMException;
// WEB IDL https://dom.spec.whatwg.org/#namednodemap // WEB IDL https://dom.spec.whatwg.org/#namednodemap
pub const NamedNodeMap = struct { pub const NamedNodeMap = struct {
pub const Self = parser.NamedNodeMap; pub const Self = parser.NamedNodeMap;
pub const mem_guarantied = true;
pub const Exception = DOMException; pub const Exception = DOMException;
@@ -80,18 +75,18 @@ pub const NamedNodeMap = struct {
// Tests // Tests
// ----- // -----
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.DOM.NamedNodeMap" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
var setItem = [_]Case{
.{ .src = "let a = document.getElementById('content').attributes", .ex = "undefined" }, try runner.testCases(&.{
.{ .src = "a.length", .ex = "1" }, .{ "let a = document.getElementById('content').attributes", "undefined" },
.{ .src = "a.item(0)", .ex = "[object Attr]" }, .{ "a.length", "1" },
.{ .src = "a.item(1)", .ex = "null" }, .{ "a.item(0)", "[object Attr]" },
.{ .src = "a.getNamedItem('id')", .ex = "[object Attr]" }, .{ "a.item(1)", "null" },
.{ .src = "a.getNamedItem('foo')", .ex = "null" }, .{ "a.getNamedItem('id')", "[object Attr]" },
.{ .src = "a.setNamedItem(a.getNamedItem('id'))", .ex = "[object Attr]" }, .{ "a.getNamedItem('foo')", "null" },
}; .{ "a.setNamedItem(a.getNamedItem('id'))", "[object Attr]" },
try checkCases(js_env, &setItem); }, .{});
} }

View File

@@ -18,16 +18,10 @@
const std = @import("std"); const std = @import("std");
const jsruntime = @import("jsruntime"); const parser = @import("../netsurf.zig");
const Case = jsruntime.test_utils.Case; const generate = @import("../../runtime/generate.zig");
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 SessionState = @import("../env.zig").SessionState;
const EventTarget = @import("event_target.zig").EventTarget; const EventTarget = @import("event_target.zig").EventTarget;
// DOM // DOM
@@ -66,7 +60,6 @@ pub const Union = generate.Union(Interfaces);
pub const Node = struct { pub const Node = struct {
pub const Self = parser.Node; pub const Self = parser.Node;
pub const prototype = *EventTarget; pub const prototype = *EventTarget;
pub const mem_guarantied = true;
pub fn toInterface(node: *parser.Node) !Union { pub fn toInterface(node: *parser.Node) !Union {
return switch (try parser.nodeType(node)) { return switch (try parser.nodeType(node)) {
@@ -262,13 +255,14 @@ pub const Node = struct {
return try parser.nodeHasChildNodes(self); 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(); var list = NodeList.init();
errdefer list.deinit(alloc); errdefer list.deinit(allocator);
var n = try parser.nodeFirstChild(self) orelse return list; var n = try parser.nodeFirstChild(self) orelse return list;
while (true) { while (true) {
try list.append(alloc, n); try list.append(allocator, n);
n = try parser.nodeNextSibling(n) orelse return list; 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. // For now, it checks only if new nodes are not self.
// TODO implements the others contraints. // TODO implements the others contraints.
// see https://dom.spec.whatwg.org/#concept-node-tree // see https://dom.spec.whatwg.org/#concept-node-tree
pub fn hierarchy(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !bool { pub fn hierarchy(self: *parser.Node, nodes: []const *parser.Node) !bool {
if (nodes == null) return true; if (nodes.len == 0) return true;
if (nodes.?.slice.len == 0) return true;
for (nodes.?.slice) |node| if (self == node) return false; for (nodes) |node| if (self == node) return false;
return true; return true;
} }
@@ -339,22 +332,21 @@ pub const Node = struct {
// TODO according with https://dom.spec.whatwg.org/#parentnode, the // TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string. // function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
pub fn prepend(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void { pub fn prepend(self: *parser.Node, nodes: []const *parser.Node) !void {
if (nodes == null) return; if (nodes.len == 0) return;
if (nodes.?.slice.len == 0) return;
// check hierarchy // check hierarchy
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest; if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
const first = try parser.nodeFirstChild(self); const first = try parser.nodeFirstChild(self);
if (first == null) { if (first == null) {
for (nodes.?.slice) |node| { for (nodes) |node| {
_ = try parser.nodeAppendChild(self, node); _ = try parser.nodeAppendChild(self, node);
} }
return; return;
} }
for (nodes.?.slice) |node| { for (nodes) |node| {
_ = try parser.nodeInsertBefore(self, node, first.?); _ = try parser.nodeInsertBefore(self, node, first.?);
} }
} }
@@ -362,14 +354,13 @@ pub const Node = struct {
// TODO according with https://dom.spec.whatwg.org/#parentnode, the // TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string. // function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
pub fn append(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void { pub fn append(self: *parser.Node, nodes: []const *parser.Node) !void {
if (nodes == null) return; if (nodes.len == 0) return;
if (nodes.?.slice.len == 0) return;
// check hierarchy // check hierarchy
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest; if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
for (nodes.?.slice) |node| { for (nodes) |node| {
_ = try parser.nodeAppendChild(self, node); _ = try parser.nodeAppendChild(self, node);
} }
} }
@@ -377,9 +368,8 @@ pub const Node = struct {
// TODO according with https://dom.spec.whatwg.org/#parentnode, the // TODO according with https://dom.spec.whatwg.org/#parentnode, the
// function must accept either node or string. // function must accept either node or string.
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
pub fn replaceChildren(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void { pub fn replaceChildren(self: *parser.Node, nodes: []const *parser.Node) !void {
if (nodes == null) return; if (nodes.len == 0) return;
if (nodes.?.slice.len == 0) return;
// check hierarchy // check hierarchy
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest; if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
@@ -388,7 +378,7 @@ pub const Node = struct {
try removeChildren(self); try removeChildren(self);
// add new children // add new children
for (nodes.?.slice) |node| { for (nodes) |node| {
_ = try parser.nodeAppendChild(self, node); _ = try parser.nodeAppendChild(self, node);
} }
} }
@@ -411,219 +401,192 @@ pub const Node = struct {
pub fn deinit(_: *parser.Node, _: std.mem.Allocator) void {} 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, var err_out: ?[]const u8 = null;
js_env: *jsruntime.Env, try runner.exec(
) anyerror!void { \\ 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 try runner.testCases(&.{
const trim_and_replace = .{ "document.body.compareDocumentPosition(document.firstChild); ", "10" },
\\function trimAndReplace(str) { .{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", "10" },
\\str = str.replace(/(\r\n|\n|\r)/gm,''); .{ "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "20" },
\\str = str.replace(/\s+/g, ' '); .{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", "0" },
\\str = str.trim(); .{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", "2" },
\\return str; .{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "4" },
\\} }, .{});
;
try runScript(js_env, alloc, trim_and_replace, "proto_test");
var node_compare_document_position = [_]Case{ try runner.testCases(&.{
.{ .src = "document.body.compareDocumentPosition(document.firstChild); ", .ex = "10" }, .{ "document.getElementById('content').getRootNode().__proto__.constructor.name", "HTMLDocument" },
.{ .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);
var get_root_node = [_]Case{ try runner.testCases(&.{
.{ .src = "document.getElementById('content').getRootNode().__proto__.constructor.name", .ex = "HTMLDocument" },
};
try checkCases(js_env, &get_root_node);
var first_child = [_]Case{
// for next test cases // for next test cases
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, .{ "let content = document.getElementById('content')", "undefined" },
.{ .src = "let link = document.getElementById('link')", .ex = "undefined" }, .{ "let link = document.getElementById('link')", "undefined" },
.{ .src = "let first_child = content.firstChild.nextSibling", .ex = "undefined" }, // nextSibling because of line return \n .{ "let first_child = content.firstChild.nextSibling", "undefined" }, // nextSibling because of line return \n
.{ .src = "let body_first_child = document.body.firstChild", .ex = "undefined" }, .{ "let body_first_child = document.body.firstChild", "undefined" },
.{ .src = "body_first_child.localName", .ex = "div" }, .{ "body_first_child.localName", "div" },
.{ .src = "body_first_child.__proto__.constructor.name", .ex = "HTMLDivElement" }, .{ "body_first_child.__proto__.constructor.name", "HTMLDivElement" },
.{ .src = "document.getElementById('para-empty').firstChild.firstChild", .ex = "null" }, .{ "document.getElementById('para-empty').firstChild.firstChild", "null" },
}; }, .{});
try checkCases(js_env, &first_child);
var last_child = [_]Case{ try runner.testCases(&.{
.{ .src = "let last_child = content.lastChild.previousSibling", .ex = "undefined" }, // previousSibling because of line return \n .{ "let last_child = content.lastChild.previousSibling", "undefined" }, // previousSibling because of line return \n
.{ .src = "last_child.__proto__.constructor.name", .ex = "Comment" }, .{ "last_child.__proto__.constructor.name", "Comment" },
}; }, .{});
try checkCases(js_env, &last_child);
var next_sibling = [_]Case{ try runner.testCases(&.{
.{ .src = "let next_sibling = link.nextSibling.nextSibling", .ex = "undefined" }, .{ "let next_sibling = link.nextSibling.nextSibling", "undefined" },
.{ .src = "next_sibling.localName", .ex = "p" }, .{ "next_sibling.localName", "p" },
.{ .src = "next_sibling.__proto__.constructor.name", .ex = "HTMLParagraphElement" }, .{ "next_sibling.__proto__.constructor.name", "HTMLParagraphElement" },
.{ .src = "content.nextSibling.nextSibling", .ex = "null" }, .{ "content.nextSibling.nextSibling", "null" },
}; }, .{});
try checkCases(js_env, &next_sibling);
var prev_sibling = [_]Case{ try runner.testCases(&.{
.{ .src = "let prev_sibling = document.getElementById('para-empty').previousSibling.previousSibling", .ex = "undefined" }, .{ "let prev_sibling = document.getElementById('para-empty').previousSibling.previousSibling", "undefined" },
.{ .src = "prev_sibling.localName", .ex = "a" }, .{ "prev_sibling.localName", "a" },
.{ .src = "prev_sibling.__proto__.constructor.name", .ex = "HTMLAnchorElement" }, .{ "prev_sibling.__proto__.constructor.name", "HTMLAnchorElement" },
.{ .src = "content.previousSibling", .ex = "null" }, .{ "content.previousSibling", "null" },
}; }, .{});
try checkCases(js_env, &prev_sibling);
var parent = [_]Case{ try runner.testCases(&.{
.{ .src = "let parent = document.getElementById('para').parentElement", .ex = "undefined" }, .{ "let parent = document.getElementById('para').parentElement", "undefined" },
.{ .src = "parent.localName", .ex = "div" }, .{ "parent.localName", "div" },
.{ .src = "parent.__proto__.constructor.name", .ex = "HTMLDivElement" }, .{ "parent.__proto__.constructor.name", "HTMLDivElement" },
.{ .src = "let h = content.parentElement.parentElement", .ex = "undefined" }, .{ "let h = content.parentElement.parentElement", "undefined" },
.{ .src = "h.parentElement", .ex = "null" }, .{ "h.parentElement", "null" },
.{ .src = "h.parentNode.__proto__.constructor.name", .ex = "HTMLDocument" }, .{ "h.parentNode.__proto__.constructor.name", "HTMLDocument" },
}; }, .{});
try checkCases(js_env, &parent);
var node_name = [_]Case{ try runner.testCases(&.{
.{ .src = "first_child.nodeName === 'A'", .ex = "true" }, .{ "first_child.nodeName === 'A'", "true" },
.{ .src = "link.firstChild.nodeName === '#text'", .ex = "true" }, .{ "link.firstChild.nodeName === '#text'", "true" },
.{ .src = "last_child.nodeName === '#comment'", .ex = "true" }, .{ "last_child.nodeName === '#comment'", "true" },
.{ .src = "document.nodeName === '#document'", .ex = "true" }, .{ "document.nodeName === '#document'", "true" },
}; }, .{});
try checkCases(js_env, &node_name);
var node_type = [_]Case{ try runner.testCases(&.{
.{ .src = "first_child.nodeType === 1", .ex = "true" }, .{ "first_child.nodeType === 1", "true" },
.{ .src = "link.firstChild.nodeType === 3", .ex = "true" }, .{ "link.firstChild.nodeType === 3", "true" },
.{ .src = "last_child.nodeType === 8", .ex = "true" }, .{ "last_child.nodeType === 8", "true" },
.{ .src = "document.nodeType === 9", .ex = "true" }, .{ "document.nodeType === 9", "true" },
}; }, .{});
try checkCases(js_env, &node_type);
var owner = [_]Case{ try runner.testCases(&.{
.{ .src = "let owner = content.ownerDocument", .ex = "undefined" }, .{ "let owner = content.ownerDocument", "undefined" },
.{ .src = "owner.__proto__.constructor.name", .ex = "HTMLDocument" }, .{ "owner.__proto__.constructor.name", "HTMLDocument" },
.{ .src = "document.ownerDocument", .ex = "null" }, .{ "document.ownerDocument", "null" },
.{ .src = "let owner2 = document.createElement('div').ownerDocument", .ex = "undefined" }, .{ "let owner2 = document.createElement('div').ownerDocument", "undefined" },
.{ .src = "owner2.__proto__.constructor.name", .ex = "HTMLDocument" }, .{ "owner2.__proto__.constructor.name", "HTMLDocument" },
}; }, .{});
try checkCases(js_env, &owner);
var connected = [_]Case{ try runner.testCases(&.{
.{ .src = "content.isConnected", .ex = "true" }, .{ "content.isConnected", "true" },
.{ .src = "document.isConnected", .ex = "true" }, .{ "document.isConnected", "true" },
.{ .src = "document.createElement('div').isConnected", .ex = "false" }, .{ "document.createElement('div').isConnected", "false" },
}; }, .{});
try checkCases(js_env, &connected);
var node_value = [_]Case{ try runner.testCases(&.{
.{ .src = "last_child.nodeValue === 'comment'", .ex = "true" }, .{ "last_child.nodeValue === 'comment'", "true" },
.{ .src = "link.nodeValue === null", .ex = "true" }, .{ "link.nodeValue === null", "true" },
.{ .src = "let text = link.firstChild", .ex = "undefined" }, .{ "let text = link.firstChild", "undefined" },
.{ .src = "text.nodeValue === 'OK'", .ex = "true" }, .{ "text.nodeValue === 'OK'", "true" },
.{ .src = "text.nodeValue = 'OK modified'", .ex = "OK modified" }, .{ "text.nodeValue = 'OK modified'", "OK modified" },
.{ .src = "text.nodeValue === 'OK modified'", .ex = "true" }, .{ "text.nodeValue === 'OK modified'", "true" },
.{ .src = "link.nodeValue = 'nothing'", .ex = "nothing" }, .{ "link.nodeValue = 'nothing'", "nothing" },
}; }, .{});
try checkCases(js_env, &node_value);
var node_text_content = [_]Case{ try runner.testCases(&.{
.{ .src = "text.textContent === 'OK modified'", .ex = "true" }, .{ "text.textContent === 'OK modified'", "true" },
.{ .src = "trimAndReplace(content.textContent) === 'OK modified And'", .ex = "true" }, .{ "trimAndReplace(content.textContent) === 'OK modified And'", "true" },
.{ .src = "text.textContent = 'OK'", .ex = "OK" }, .{ "text.textContent = 'OK'", "OK" },
.{ .src = "text.textContent", .ex = "OK" }, .{ "text.textContent", "OK" },
.{ .src = "trimAndReplace(document.getElementById('para-empty').textContent)", .ex = "" }, .{ "trimAndReplace(document.getElementById('para-empty').textContent)", "" },
.{ .src = "document.getElementById('para-empty').textContent = 'OK'", .ex = "OK" }, .{ "document.getElementById('para-empty').textContent = 'OK'", "OK" },
.{ .src = "document.getElementById('para-empty').firstChild.nodeName === '#text'", .ex = "true" }, .{ "document.getElementById('para-empty').firstChild.nodeName === '#text'", "true" },
}; }, .{});
try checkCases(js_env, &node_text_content);
var node_append_child = [_]Case{ try runner.testCases(&.{
.{ .src = "let append = document.createElement('h1')", .ex = "undefined" }, .{ "let append = document.createElement('h1')", "undefined" },
.{ .src = "content.appendChild(append).toString()", .ex = "[object HTMLHeadingElement]" }, .{ "content.appendChild(append).toString()", "[object HTMLHeadingElement]" },
.{ .src = "content.lastChild.__proto__.constructor.name", .ex = "HTMLHeadingElement" }, .{ "content.lastChild.__proto__.constructor.name", "HTMLHeadingElement" },
.{ .src = "content.appendChild(link).toString()", .ex = "[object HTMLAnchorElement]" }, .{ "content.appendChild(link).toString()", "[object HTMLAnchorElement]" },
}; }, .{});
try checkCases(js_env, &node_append_child);
var node_clone = [_]Case{ try runner.testCases(&.{
.{ .src = "let clone = link.cloneNode()", .ex = "undefined" }, .{ "let clone = link.cloneNode()", "undefined" },
.{ .src = "clone.toString()", .ex = "[object HTMLAnchorElement]" }, .{ "clone.toString()", "[object HTMLAnchorElement]" },
.{ .src = "clone.parentNode === null", .ex = "true" }, .{ "clone.parentNode === null", "true" },
.{ .src = "clone.firstChild === null", .ex = "true" }, .{ "clone.firstChild === null", "true" },
.{ .src = "let clone_deep = link.cloneNode(true)", .ex = "undefined" }, .{ "let clone_deep = link.cloneNode(true)", "undefined" },
.{ .src = "clone_deep.firstChild.nodeName === '#text'", .ex = "true" }, .{ "clone_deep.firstChild.nodeName === '#text'", "true" },
}; }, .{});
try checkCases(js_env, &node_clone);
var node_contains = [_]Case{ try runner.testCases(&.{
.{ .src = "link.contains(text)", .ex = "true" }, .{ "link.contains(text)", "true" },
.{ .src = "text.contains(link)", .ex = "false" }, .{ "text.contains(link)", "false" },
}; }, .{});
try checkCases(js_env, &node_contains);
var node_has_child_nodes = [_]Case{ try runner.testCases(&.{
.{ .src = "link.hasChildNodes()", .ex = "true" }, .{ "link.hasChildNodes()", "true" },
.{ .src = "text.hasChildNodes()", .ex = "false" }, .{ "text.hasChildNodes()", "false" },
}; }, .{});
try checkCases(js_env, &node_has_child_nodes);
var node_child_nodes = [_]Case{ try runner.testCases(&.{
.{ .src = "link.childNodes.length", .ex = "1" }, .{ "link.childNodes.length", "1" },
.{ .src = "text.childNodes.length", .ex = "0" }, .{ "text.childNodes.length", "0" },
}; }, .{});
try checkCases(js_env, &node_child_nodes);
var node_insert_before = [_]Case{ try runner.testCases(&.{
.{ .src = "let insertBefore = document.createElement('a')", .ex = "undefined" }, .{ "let insertBefore = document.createElement('a')", "undefined" },
.{ .src = "link.insertBefore(insertBefore, text) !== undefined", .ex = "true" }, .{ "link.insertBefore(insertBefore, text) !== undefined", "true" },
.{ .src = "link.firstChild.localName === 'a'", .ex = "true" }, .{ "link.firstChild.localName === 'a'", "true" },
}; }, .{});
try checkCases(js_env, &node_insert_before);
var node_is_default_namespace = [_]Case{ try runner.testCases(&.{
// TODO: does not seems to work // TODO: does not seems to work
// .{ .src = "link.isDefaultNamespace('')", .ex = "true" }, // .{ "link.isDefaultNamespace('')", "true" },
.{ .src = "link.isDefaultNamespace('false')", .ex = "false" }, .{ "link.isDefaultNamespace('false')", "false" },
}; }, .{});
try checkCases(js_env, &node_is_default_namespace);
var node_is_equal_node = [_]Case{ try runner.testCases(&.{
.{ .src = "let equal1 = document.createElement('a')", .ex = "undefined" }, .{ "let equal1 = document.createElement('a')", "undefined" },
.{ .src = "let equal2 = document.createElement('a')", .ex = "undefined" }, .{ "let equal2 = document.createElement('a')", "undefined" },
.{ .src = "equal1.textContent = 'is equal'", .ex = "is equal" }, .{ "equal1.textContent = 'is equal'", "is equal" },
.{ .src = "equal2.textContent = 'is equal'", .ex = "is equal" }, .{ "equal2.textContent = 'is equal'", "is equal" },
// TODO: does not seems to work // TODO: does not seems to work
// .{ .src = "equal1.isEqualNode(equal2)", .ex = "true" }, // .{ "equal1.isEqualNode(equal2)", "true" },
}; }, .{});
try checkCases(js_env, &node_is_equal_node);
var node_is_same_node = [_]Case{ try runner.testCases(&.{
.{ .src = "document.body.isSameNode(document.body)", .ex = "true" }, .{ "document.body.isSameNode(document.body)", "true" },
}; }, .{});
try checkCases(js_env, &node_is_same_node);
var node_normalize = [_]Case{ try runner.testCases(&.{
// TODO: no test // TODO: no test
.{ .src = "link.normalize()", .ex = "undefined" }, .{ "link.normalize()", "undefined" },
}; }, .{});
try checkCases(js_env, &node_normalize);
var node_remove_child = [_]Case{ try runner.testCases(&.{
.{ .src = "content.removeChild(append) !== undefined", .ex = "true" }, .{ "content.removeChild(append) !== undefined", "true" },
.{ .src = "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", .ex = "true" }, .{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" },
}; }, .{});
try checkCases(js_env, &node_remove_child);
var node_replace_child = [_]Case{ try runner.testCases(&.{
.{ .src = "let replace = document.createElement('div')", .ex = "undefined" }, .{ "let replace = document.createElement('div')", "undefined" },
.{ .src = "link.replaceChild(replace, insertBefore) !== undefined", .ex = "true" }, .{ "link.replaceChild(replace, insertBefore) !== undefined", "true" },
}; }, .{});
try checkCases(js_env, &node_replace_child);
} }

View File

@@ -18,13 +18,11 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const jsruntime = @import("jsruntime"); const JsObject = @import("../env.zig").JsObject;
const Callback = jsruntime.Callback; const Callback = @import("../env.zig").Callback;
const CallbackResult = jsruntime.CallbackResult; const SessionState = @import("../env.zig").SessionState;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const NodeUnion = @import("node.zig").Union; const NodeUnion = @import("node.zig").Union;
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
@@ -41,8 +39,6 @@ pub const Interfaces = .{
}; };
pub const NodeListIterator = struct { pub const NodeListIterator = struct {
pub const mem_guarantied = true;
coll: *NodeList, coll: *NodeList,
index: u32 = 0, index: u32 = 0,
@@ -69,8 +65,6 @@ pub const NodeListIterator = struct {
}; };
pub const NodeListEntriesIterator = struct { pub const NodeListEntriesIterator = struct {
pub const mem_guarantied = true;
coll: *NodeList, coll: *NodeList,
index: u32 = 0, index: u32 = 0,
@@ -104,7 +98,6 @@ pub const NodeListEntriesIterator = struct {
// implementation allows only static nodelist. // implementation allows only static nodelist.
// see https://dom.spec.whatwg.org/#old-style-collections // see https://dom.spec.whatwg.org/#old-style-collections
pub const NodeList = struct { pub const NodeList = struct {
pub const mem_guarantied = true;
pub const Exception = DOMException; pub const Exception = DOMException;
const NodesArrayList = std.ArrayListUnmanaged(*parser.Node); const NodesArrayList = std.ArrayListUnmanaged(*parser.Node);
@@ -130,7 +123,7 @@ pub const NodeList = struct {
return @intCast(self.nodes.items.len); 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) { if (index >= self.nodes.items.len) {
return null; return null;
} }
@@ -139,17 +132,30 @@ pub const NodeList = struct {
return try Node.toInterface(n); return try Node.toInterface(n);
} }
pub fn _forEach(self: *NodeList, alloc: std.mem.Allocator, cbk: Callback) !void { // TODO handle thisArg // This code works, but it's _MUCH_ slower than using postAttach. The benefit
var res = CallbackResult.init(alloc); // of this version, is that it's "live"..but we're talking many orders of
defer res.deinit(); // 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| { for (self.nodes.items, 0..) |n, i| {
const ii: u32 = @intCast(i); const ii: u32 = @intCast(i);
cbk.trycall(.{ n, ii, self }, &res) catch |e| { var result: Callback.Result = undefined;
log.err("callback error: {s}", .{res.result orelse "unknown"}); cbk.tryCall(.{ n, ii, self }, &result) catch {
log.debug("{s}", .{res.stack orelse "no stack trace"}); log.err("callback error: {s}", .{result.exception});
log.debug("stack:\n{s}", .{result.stack orelse "???"});
return e;
}; };
} }
} }
@@ -171,38 +177,32 @@ pub const NodeList = struct {
} }
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries // TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
pub fn postAttach(self: *NodeList, js_obj: JsObject) !void {
pub fn postAttach(self: *NodeList, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void { const len = self.get_length();
const ln = self.get_length(); for (0..len) |i| {
var i: u32 = 0; const node = try self._item(@intCast(i)) orelse unreachable;
while (i < ln) { try js_obj.setIndex(i, node);
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);
} }
} }
}; };
// Tests const testing = @import("../../testing.zig");
// ----- test "Browser.DOM.NodeList" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
pub fn testExecFn( try runner.testCases(&.{
_: std.mem.Allocator, .{ "let list = document.getElementById('content').childNodes", "undefined" },
js_env: *jsruntime.Env, .{ "list.length", "9" },
) anyerror!void { .{ "list[0].__proto__.constructor.name", "Text" },
var childnodes = [_]Case{ .{
.{ .src = "let list = document.getElementById('content').childNodes", .ex = "undefined" }, \\ let i = 0;
.{ .src = "list.length", .ex = "9" }, \\ list.forEach(function (n, idx) {
.{ .src = "list[0].__proto__.constructor.name", .ex = "Text" }, \\ i += idx;
.{ .src = \\ });
\\let i = 0; \\ i;
\\list.forEach(function (n, idx) { ,
\\ i += idx; "36",
\\}); },
\\i; }, .{});
, .ex = "36" },
};
try checkCases(js_env, &childnodes);
} }

View File

@@ -18,11 +18,7 @@
const std = @import("std"); const std = @import("std");
const jsruntime = @import("jsruntime"); const parser = @import("../netsurf.zig");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("netsurf");
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
// https://dom.spec.whatwg.org/#processinginstruction // https://dom.spec.whatwg.org/#processinginstruction
@@ -32,7 +28,6 @@ pub const ProcessingInstruction = struct {
// TODO for libdom processing instruction inherit from node. // TODO for libdom processing instruction inherit from node.
// But the spec says it must inherit from CDATA. // But the spec says it must inherit from CDATA.
pub const prototype = *Node; pub const prototype = *Node;
pub const mem_guarantied = true;
pub fn get_target(self: *parser.ProcessingInstruction) ![]const u8 { pub fn get_target(self: *parser.ProcessingInstruction) ![]const u8 {
// libdom stores the ProcessingInstruction target in the node's name. // libdom stores the ProcessingInstruction target in the node's name.
@@ -52,18 +47,18 @@ pub const ProcessingInstruction = struct {
} }
}; };
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.DOM.ProcessingInstruction" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
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" },
.{ .src = "let pi2 = pi.cloneNode()", .ex = "undefined" }, try runner.testCases(&.{
}; .{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" },
try checkCases(js_env, &createProcessingInstruction); .{ "pi.target", "foo" },
.{ "pi.data", "bar" },
.{ "pi.data = 'foo'", "foo" },
.{ "pi.data", "foo" },
.{ "let pi2 = pi.cloneNode()", "undefined" },
}, .{});
} }

View File

@@ -18,17 +18,12 @@
const std = @import("std"); const std = @import("std");
const jsruntime = @import("jsruntime"); const parser = @import("../netsurf.zig");
const Case = jsruntime.test_utils.Case; const SessionState = @import("../env.zig").SessionState;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("netsurf");
const CharacterData = @import("character_data.zig").CharacterData; const CharacterData = @import("character_data.zig").CharacterData;
const CDATASection = @import("cdata_section.zig").CDATASection; const CDATASection = @import("cdata_section.zig").CDATASection;
const UserContext = @import("../user_context.zig").UserContext;
// Text interfaces // Text interfaces
pub const Interfaces = .{ pub const Interfaces = .{
CDATASection, CDATASection,
@@ -37,11 +32,10 @@ pub const Interfaces = .{
pub const Text = struct { pub const Text = struct {
pub const Self = parser.Text; pub const Self = parser.Text;
pub const prototype = *CharacterData; 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( return parser.documentCreateTextNode(
parser.documentHTMLToDocument(userctx.document), parser.documentHTMLToDocument(state.document.?),
data orelse "", data orelse "",
); );
} }
@@ -66,30 +60,28 @@ pub const Text = struct {
// Tests // Tests
// ----- // -----
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.DOM.Text" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
var constructor = [_]Case{
.{ .src = "let t = new Text('foo')", .ex = "undefined" },
.{ .src = "t.data", .ex = "foo" },
.{ .src = "let emptyt = new Text()", .ex = "undefined" }, try runner.testCases(&.{
.{ .src = "emptyt.data", .ex = "" }, .{ "let t = new Text('foo')", "undefined" },
}; .{ "t.data", "foo" },
try checkCases(js_env, &constructor);
var get_whole_text = [_]Case{ .{ "let emptyt = new Text()", "undefined" },
.{ .src = "let text = document.getElementById('link').firstChild", .ex = "undefined" }, .{ "emptyt.data", "" },
.{ .src = "text.wholeText === 'OK'", .ex = "true" }, }, .{});
};
try checkCases(js_env, &get_whole_text);
var split_text = [_]Case{ try runner.testCases(&.{
.{ .src = "text.data = 'OK modified'", .ex = "OK modified" }, .{ "let text = document.getElementById('link').firstChild", "undefined" },
.{ .src = "let split = text.splitText('OK'.length)", .ex = "undefined" }, .{ "text.wholeText === 'OK'", "true" },
.{ .src = "split.data === ' modified'", .ex = "true" }, }, .{});
.{ .src = "text.data === 'OK'", .ex = "true" },
}; try runner.testCases(&.{
try checkCases(js_env, &split_text); .{ "text.data = 'OK modified'", "OK modified" },
.{ "let split = text.splitText('OK'.length)", "undefined" },
.{ "split.data === ' modified'", "true" },
.{ "text.data === 'OK'", "true" },
}, .{});
} }

View File

@@ -18,12 +18,7 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const Variadic = jsruntime.Variadic;
const DOMException = @import("exceptions.zig").DOMException; const DOMException = @import("exceptions.zig").DOMException;
@@ -31,7 +26,6 @@ const DOMException = @import("exceptions.zig").DOMException;
pub const DOMTokenList = struct { pub const DOMTokenList = struct {
pub const Self = parser.TokenList; pub const Self = parser.TokenList;
pub const Exception = DOMException; pub const Exception = DOMException;
pub const mem_guarantied = true;
pub fn get_length(self: *parser.TokenList) !u32 { pub fn get_length(self: *parser.TokenList) !u32 {
return parser.tokenListGetLength(self); return parser.tokenListGetLength(self);
@@ -45,16 +39,14 @@ pub const DOMTokenList = struct {
return parser.tokenListContains(self, token); return parser.tokenListContains(self, token);
} }
pub fn _add(self: *parser.TokenList, tokens: ?Variadic([]const u8)) !void { pub fn _add(self: *parser.TokenList, tokens: []const []const u8) !void {
if (tokens == null) return; for (tokens) |token| {
for (tokens.?.slice) |token| {
try parser.tokenListAdd(self, token); try parser.tokenListAdd(self, token);
} }
} }
pub fn _remove(self: *parser.TokenList, tokens: ?Variadic([]const u8)) !void { pub fn _remove(self: *parser.TokenList, tokens: []const []const u8) !void {
if (tokens == null) return; for (tokens) |token| {
for (tokens.?.slice) |token| {
try parser.tokenListRemove(self, token); try parser.tokenListRemove(self, token);
} }
} }
@@ -113,52 +105,49 @@ pub const DOMTokenList = struct {
// Tests // Tests
// ----- // -----
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.DOM.TokenList" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
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);
var testcases = [_]Case{ try runner.testCases(&.{
.{ .src = "let cl2 = gs.classList", .ex = "undefined" }, .{ "let gs = document.getElementById('para-empty')", "undefined" },
.{ .src = "cl2.length", .ex = "2" }, .{ "let cl = gs.classList", "undefined" },
.{ .src = "cl2.item(0)", .ex = "ok" }, .{ "gs.className", "ok empty" },
.{ .src = "cl2.item(1)", .ex = "empty" }, .{ "cl.value", "ok empty" },
.{ .src = "cl2.contains('ok')", .ex = "true" }, .{ "cl.length", "2" },
.{ .src = "cl2.contains('nok')", .ex = "false" }, .{ "gs.className = 'foo bar baz'", "foo bar baz" },
.{ .src = "cl2.add('foo', 'bar', 'baz')", .ex = "undefined" }, .{ "gs.className", "foo bar baz" },
.{ .src = "cl2.length", .ex = "5" }, .{ "cl.length", "3" },
.{ .src = "cl2.remove('foo', 'bar', 'baz')", .ex = "undefined" }, .{ "gs.className = 'ok empty'", "ok empty" },
.{ .src = "cl2.length", .ex = "2" }, .{ "cl.length", "2" },
}; }, .{});
try checkCases(js_env, &testcases);
var toogle = [_]Case{ try runner.testCases(&.{
.{ .src = "let cl3 = gs.classList", .ex = "undefined" }, .{ "let cl2 = gs.classList", "undefined" },
.{ .src = "cl3.toggle('ok')", .ex = "false" }, .{ "cl2.length", "2" },
.{ .src = "cl3.toggle('ok')", .ex = "true" }, .{ "cl2.item(0)", "ok" },
.{ .src = "cl3.length", .ex = "2" }, .{ "cl2.item(1)", "empty" },
}; .{ "cl2.contains('ok')", "true" },
try checkCases(js_env, &toogle); .{ "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{ try runner.testCases(&.{
.{ .src = "let cl4 = gs.classList", .ex = "undefined" }, .{ "let cl3 = gs.classList", "undefined" },
.{ .src = "cl4.replace('ok', 'nok')", .ex = "true" }, .{ "cl3.toggle('ok')", "false" },
.{ .src = "cl4.value", .ex = "empty nok" }, .{ "cl3.toggle('ok')", "true" },
.{ .src = "cl4.replace('nok', 'ok')", .ex = "true" }, .{ "cl3.length", "2" },
.{ .src = "cl4.value", .ex = "empty ok" }, }, .{});
};
try checkCases(js_env, &replace); 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" },
}, .{});
} }

View File

@@ -18,7 +18,7 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
pub const Walker = union(enum) { pub const Walker = union(enum) {
walkerDepthFirst: WalkerDepthFirst, walkerDepthFirst: WalkerDepthFirst,

View File

@@ -19,8 +19,8 @@
const std = @import("std"); const std = @import("std");
const File = std.fs.File; const File = std.fs.File;
const parser = @import("netsurf"); const parser = @import("netsurf.zig");
const Walker = @import("../dom/walker.zig").WalkerChildren; const Walker = @import("dom/walker.zig").WalkerChildren;
// writer must be a std.io.Writer // writer must be a std.io.Writer
pub fn writeHTML(doc: *parser.Document, writer: anytype) !void { pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {

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

@@ -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,
};

View File

@@ -0,0 +1,245 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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" },
}, .{});
}

View File

@@ -18,11 +18,8 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const SessionState = @import("../env.zig").SessionState;
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const Node = @import("../dom/node.zig").Node; const Node = @import("../dom/node.zig").Node;
const Document = @import("../dom/document.zig").Document; 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 collection = @import("../dom/html_collection.zig");
const Walker = @import("../dom/walker.zig").WalkerDepthFirst; const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
const UserContext = @import("../user_context.zig").UserContext;
const Cookie = @import("../storage/cookie.zig").Cookie; const Cookie = @import("../storage/cookie.zig").Cookie;
// WEB IDL https://html.spec.whatwg.org/#the-document-object // WEB IDL https://html.spec.whatwg.org/#the-document-object
pub const HTMLDocument = struct { pub const HTMLDocument = struct {
pub const Self = parser.DocumentHTML; pub const Self = parser.DocumentHTML;
pub const prototype = *Document; pub const prototype = *Document;
pub const mem_guarantied = true;
pub const sub_type = "node"; 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) = .{}; 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; 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 // we use the cookie jar's allocator to parse the cookie because it
// outlives the page's arena. // 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(); errdefer c.deinit();
try state.cookie_jar.add(c, std.time.timestamp());
try userctx.cookie_jar.add(c, std.time.timestamp());
return cookie_str; return cookie_str;
} }
@@ -110,44 +102,45 @@ pub const HTMLDocument = struct {
return v; 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(); var list = NodeList.init();
errdefer list.deinit(alloc); errdefer list.deinit(arena);
if (name.len == 0) return list; if (name.len == 0) return list;
const root = parser.documentHTMLToNode(self); 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(); const ln = try c.get_length();
var i: u32 = 0; var i: u32 = 0;
while (i < ln) { while (i < ln) {
const n = try c.item(i) orelse break; const n = try c.item(i) orelse break;
try list.append(alloc, n); try list.append(arena, n);
i += 1; i += 1;
} }
return list; return list;
} }
pub fn get_images(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection { pub fn get_images(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "img", false); return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "img", false);
} }
pub fn get_embeds(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection { pub fn get_embeds(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "embed", false); return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "embed", false);
} }
pub fn get_plugins(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection { pub fn get_plugins(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
return get_embeds(self, alloc); return get_embeds(self, state);
} }
pub fn get_forms(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection { pub fn get_forms(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "form", false); return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "form", false);
} }
pub fn get_scripts(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection { pub fn get_scripts(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "script", false); return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "script", false);
} }
pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection { 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 { pub fn set_bgColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
return ""; return "";
} }
pub fn deinit(_: *parser.DocumentHTML, _: std.mem.Allocator) void {}
}; };
// Tests // Tests
// ----- // -----
pub fn testExecFn( const testing = @import("../../testing.zig");
_: 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);
var getters = [_]Case{ test "Browser.HTML.Document" {
.{ .src = "document.domain", .ex = "" }, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
.{ .src = "document.referrer", .ex = "" }, defer runner.deinit();
.{ .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);
var titles = [_]Case{ try runner.testCases(&.{
.{ .src = "document.title = 'foo'", .ex = "foo" }, .{ "document.__proto__.constructor.name", "HTMLDocument" },
.{ .src = "document.title", .ex = "foo" }, .{ "document.__proto__.__proto__.constructor.name", "Document" },
.{ .src = "document.title = ''", .ex = "" }, .{ "document.body.localName == 'body'", "true" },
}; }, .{});
try checkCases(js_env, &titles);
var getElementsByName = [_]Case{ try runner.testCases(&.{
.{ .src = "document.getElementById('link').setAttribute('name', 'foo')", .ex = "undefined" }, .{ "document.domain", "" },
.{ .src = "let list = document.getElementsByName('foo')", .ex = "undefined" }, .{ "document.referrer", "" },
.{ .src = "list.length", .ex = "1" }, .{ "document.title", "" },
}; .{ "document.body.localName", "body" },
try checkCases(js_env, &getElementsByName); .{ "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{ try runner.testCases(&.{
.{ .src = "document.cookie", .ex = "" }, .{ "document.title = 'foo'", "foo" },
.{ .src = "document.cookie = 'name=Oeschger; SameSite=None; Secure'", .ex = "name=Oeschger; SameSite=None; Secure" }, .{ "document.title", "foo" },
.{ .src = "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", .ex = "favorite_food=tripe; SameSite=None; Secure" }, .{ "document.title = ''", "" },
.{ .src = "document.cookie", .ex = "name=Oeschger; favorite_food=tripe" }, }, .{});
};
try checkCases(js_env, &cookie); 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" },
}, .{});
} }

View File

@@ -17,16 +17,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const generate = @import("../generate.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 URL = @import("../url/url.zig").URL;
const Node = @import("../dom/node.zig").Node; const Node = @import("../dom/node.zig").Node;
const Element = @import("../dom/element.zig").Element;
// HTMLElement interfaces // HTMLElement interfaces
pub const Interfaces = .{ pub const Interfaces = .{
@@ -105,14 +102,11 @@ pub const Union = generate.Union(Interfaces);
// Abstract class // Abstract class
// -------------- // --------------
const CSSProperties = struct { const CSSProperties = struct {};
pub const mem_guarantied = true;
};
pub const HTMLElement = struct { pub const HTMLElement = struct {
pub const Self = parser.ElementHTML; pub const Self = parser.ElementHTML;
pub const prototype = *Element; pub const prototype = *Element;
pub const mem_guarantied = true;
pub fn get_style(_: *parser.ElementHTML) CSSProperties { pub fn get_style(_: *parser.ElementHTML) CSSProperties {
return .{}; return .{};
@@ -148,7 +142,6 @@ pub const HTMLElement = struct {
pub const HTMLMediaElement = struct { pub const HTMLMediaElement = struct {
pub const Self = parser.MediaElement; pub const Self = parser.MediaElement;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
// HTML elements // HTML elements
@@ -157,14 +150,12 @@ pub const HTMLMediaElement = struct {
pub const HTMLUnknownElement = struct { pub const HTMLUnknownElement = struct {
pub const Self = parser.Unknown; pub const Self = parser.Unknown;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
// https://html.spec.whatwg.org/#the-a-element // https://html.spec.whatwg.org/#the-a-element
pub const HTMLAnchorElement = struct { pub const HTMLAnchorElement = struct {
pub const Self = parser.Anchor; pub const Self = parser.Anchor;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
pub fn get_target(self: *parser.Anchor) ![]const u8 { pub fn get_target(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetTarget(self); return try parser.anchorGetTarget(self);
@@ -174,7 +165,7 @@ pub const HTMLAnchorElement = struct {
return try parser.anchorSetTarget(self, href); 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 return ""; // TODO
} }
@@ -218,47 +209,39 @@ pub const HTMLAnchorElement = struct {
return try parser.nodeSetTextContent(parser.anchorToNode(self), v); 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); 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 // TODO return a disposable string
pub fn get_origin(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { pub fn get_origin(self: *parser.Anchor, state: *SessionState) ![]const u8 {
var u = try url(self, alloc); var u = try url(self, state);
defer u.deinit(alloc); return try u.get_origin(state);
return try u.get_origin(alloc);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_protocol(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { pub fn get_protocol(self: *parser.Anchor, state: *SessionState) ![]const u8 {
var u = try url(self, alloc); var u = try url(self, state);
defer u.deinit(alloc); return u.get_protocol(state);
return u.get_protocol(alloc);
} }
pub fn set_protocol(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void { pub fn set_protocol(self: *parser.Anchor, v: []const u8, state: *SessionState) !void {
var u = try url(self, alloc); const arena = state.arena;
defer u.deinit(alloc); var u = try url(self, state);
u.uri.scheme = v; u.uri.scheme = v;
const href = try u.format(alloc); const href = try u.toString(arena);
defer alloc.free(href);
try parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_host(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { pub fn get_host(self: *parser.Anchor, state: *SessionState) ![]const u8 {
var u = try url(self, alloc); var u = try url(self, state);
defer u.deinit(alloc); return try u.get_host(state);
return try u.get_host(alloc);
} }
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 // search : separator
var p: ?u16 = null; var p: ?u16 = null;
var h: []const u8 = undefined; var h: []const u8 = undefined;
@@ -270,8 +253,8 @@ pub const HTMLAnchorElement = struct {
} }
} }
var u = try url(self, alloc); const arena = state.arena;
defer u.deinit(alloc); var u = try url(self, state);
if (p) |pp| { if (p) |pp| {
u.uri.host = .{ .raw = h }; u.uri.host = .{ .raw = h };
@@ -281,40 +264,33 @@ pub const HTMLAnchorElement = struct {
u.uri.port = null; u.uri.port = null;
} }
const href = try u.format(alloc); const href = try u.toString(arena);
defer alloc.free(href);
try parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_hostname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { pub fn get_hostname(self: *parser.Anchor, state: *SessionState) ![]const u8 {
var u = try url(self, alloc); var u = try url(self, state);
defer u.deinit(alloc); return try state.arena.dupe(u8, u.get_hostname());
return try alloc.dupe(u8, u.get_hostname());
} }
pub fn set_hostname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void { pub fn set_hostname(self: *parser.Anchor, v: []const u8, state: *SessionState) !void {
var u = try url(self, alloc); const arena = state.arena;
defer u.deinit(alloc); var u = try url(self, state);
u.uri.host = .{ .raw = v }; u.uri.host = .{ .raw = v };
const href = try u.format(alloc); const href = try u.toString(arena);
try parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_port(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { pub fn get_port(self: *parser.Anchor, state: *SessionState) ![]const u8 {
var u = try url(self, alloc); var u = try url(self, state);
defer u.deinit(alloc); return try u.get_port(state);
return try u.get_port(alloc);
} }
pub fn set_port(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void { pub fn set_port(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
var u = try url(self, alloc); const arena = state.arena;
defer u.deinit(alloc); var u = try url(self, state);
if (v != null and v.?.len > 0) { if (v != null and v.?.len > 0) {
u.uri.port = try std.fmt.parseInt(u16, v.?, 10); u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
@@ -322,407 +298,340 @@ pub const HTMLAnchorElement = struct {
u.uri.port = null; u.uri.port = null;
} }
const href = try u.format(alloc); const href = try u.toString(arena);
defer alloc.free(href);
try parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_username(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { pub fn get_username(self: *parser.Anchor, state: *SessionState) ![]const u8 {
var u = try url(self, alloc); var u = try url(self, state);
defer u.deinit(alloc); return try state.arena.dupe(u8, u.get_username());
return try alloc.dupe(u8, u.get_username());
} }
pub fn set_username(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void { pub fn set_username(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
var u = try url(self, alloc); const arena = state.arena;
defer u.deinit(alloc); var u = try url(self, state);
if (v) |vv| { if (v) |vv| {
u.uri.user = .{ .raw = vv }; u.uri.user = .{ .raw = vv };
} else { } else {
u.uri.user = null; u.uri.user = null;
} }
const href = try u.format(alloc); const href = try u.toString(arena);
defer alloc.free(href);
try parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_password(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { pub fn get_password(self: *parser.Anchor, state: *SessionState) ![]const u8 {
var u = try url(self, alloc); var u = try url(self, state);
defer u.deinit(alloc); return try state.arena.dupe(u8, u.get_password());
return try alloc.dupe(u8, u.get_password());
} }
pub fn set_password(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void { pub fn set_password(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
var u = try url(self, alloc); const arena = state.arena;
defer u.deinit(alloc); var u = try url(self, state);
if (v) |vv| { if (v) |vv| {
u.uri.password = .{ .raw = vv }; u.uri.password = .{ .raw = vv };
} else { } else {
u.uri.password = null; u.uri.password = null;
} }
const href = try u.format(alloc); const href = try u.toString(arena);
defer alloc.free(href);
try parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_pathname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { pub fn get_pathname(self: *parser.Anchor, state: *SessionState) ![]const u8 {
var u = try url(self, alloc); var u = try url(self, state);
defer u.deinit(alloc); return try state.arena.dupe(u8, u.get_pathname());
return try alloc.dupe(u8, u.get_pathname());
} }
pub fn set_pathname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void { pub fn set_pathname(self: *parser.Anchor, v: []const u8, state: *SessionState) !void {
var u = try url(self, alloc); const arena = state.arena;
defer u.deinit(alloc); var u = try url(self, state);
u.uri.path = .{ .raw = v }; u.uri.path = .{ .raw = v };
const href = try u.format(alloc); const href = try u.toString(arena);
defer alloc.free(href);
try parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_search(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { pub fn get_search(self: *parser.Anchor, state: *SessionState) ![]const u8 {
var u = try url(self, alloc); var u = try url(self, state);
defer u.deinit(alloc); return try u.get_search(state);
return try u.get_search(alloc);
} }
pub fn set_search(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void { pub fn set_search(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
var u = try url(self, alloc); const arena = state.arena;
defer u.deinit(alloc); var u = try url(self, state);
if (v) |vv| { if (v) |vv| {
u.uri.query = .{ .raw = vv }; u.uri.query = .{ .raw = vv };
} else { } else {
u.uri.query = null; u.uri.query = null;
} }
const href = try u.format(alloc); const href = try u.toString(arena);
defer alloc.free(href);
try parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_hash(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { pub fn get_hash(self: *parser.Anchor, state: *SessionState) ![]const u8 {
var u = try url(self, alloc); var u = try url(self, state);
defer u.deinit(alloc); return try u.get_hash(state);
return try u.get_hash(alloc);
} }
pub fn set_hash(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void { pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void {
var u = try url(self, alloc); const arena = state.arena;
defer u.deinit(alloc); var u = try url(self, state);
if (v) |vv| { if (v) |vv| {
u.uri.fragment = .{ .raw = vv }; u.uri.fragment = .{ .raw = vv };
} else { } else {
u.uri.fragment = null; u.uri.fragment = null;
} }
const href = try u.format(alloc); const href = try u.toString(arena);
defer alloc.free(href);
try parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
pub fn deinit(_: *parser.Anchor, _: std.mem.Allocator) void {}
}; };
pub const HTMLAppletElement = struct { pub const HTMLAppletElement = struct {
pub const Self = parser.Applet; pub const Self = parser.Applet;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLAreaElement = struct { pub const HTMLAreaElement = struct {
pub const Self = parser.Area; pub const Self = parser.Area;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLAudioElement = struct { pub const HTMLAudioElement = struct {
pub const Self = parser.Audio; pub const Self = parser.Audio;
pub const prototype = *HTMLMediaElement; pub const prototype = *HTMLMediaElement;
pub const mem_guarantied = true;
}; };
pub const HTMLBRElement = struct { pub const HTMLBRElement = struct {
pub const Self = parser.BR; pub const Self = parser.BR;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLBaseElement = struct { pub const HTMLBaseElement = struct {
pub const Self = parser.Base; pub const Self = parser.Base;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLBodyElement = struct { pub const HTMLBodyElement = struct {
pub const Self = parser.Body; pub const Self = parser.Body;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLButtonElement = struct { pub const HTMLButtonElement = struct {
pub const Self = parser.Button; pub const Self = parser.Button;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLCanvasElement = struct { pub const HTMLCanvasElement = struct {
pub const Self = parser.Canvas; pub const Self = parser.Canvas;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLDListElement = struct { pub const HTMLDListElement = struct {
pub const Self = parser.DList; pub const Self = parser.DList;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLDataElement = struct { pub const HTMLDataElement = struct {
pub const Self = parser.Data; pub const Self = parser.Data;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLDataListElement = struct { pub const HTMLDataListElement = struct {
pub const Self = parser.DataList; pub const Self = parser.DataList;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLDialogElement = struct { pub const HTMLDialogElement = struct {
pub const Self = parser.Dialog; pub const Self = parser.Dialog;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLDirectoryElement = struct { pub const HTMLDirectoryElement = struct {
pub const Self = parser.Directory; pub const Self = parser.Directory;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLDivElement = struct { pub const HTMLDivElement = struct {
pub const Self = parser.Div; pub const Self = parser.Div;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLEmbedElement = struct { pub const HTMLEmbedElement = struct {
pub const Self = parser.Embed; pub const Self = parser.Embed;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLFieldSetElement = struct { pub const HTMLFieldSetElement = struct {
pub const Self = parser.FieldSet; pub const Self = parser.FieldSet;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLFontElement = struct { pub const HTMLFontElement = struct {
pub const Self = parser.Font; pub const Self = parser.Font;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLFormElement = struct { pub const HTMLFormElement = struct {
pub const Self = parser.Form; pub const Self = parser.Form;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLFrameElement = struct { pub const HTMLFrameElement = struct {
pub const Self = parser.Frame; pub const Self = parser.Frame;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLFrameSetElement = struct { pub const HTMLFrameSetElement = struct {
pub const Self = parser.FrameSet; pub const Self = parser.FrameSet;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLHRElement = struct { pub const HTMLHRElement = struct {
pub const Self = parser.HR; pub const Self = parser.HR;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLHeadElement = struct { pub const HTMLHeadElement = struct {
pub const Self = parser.Head; pub const Self = parser.Head;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLHeadingElement = struct { pub const HTMLHeadingElement = struct {
pub const Self = parser.Heading; pub const Self = parser.Heading;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLHtmlElement = struct { pub const HTMLHtmlElement = struct {
pub const Self = parser.Html; pub const Self = parser.Html;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLIFrameElement = struct { pub const HTMLIFrameElement = struct {
pub const Self = parser.IFrame; pub const Self = parser.IFrame;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLImageElement = struct { pub const HTMLImageElement = struct {
pub const Self = parser.Image; pub const Self = parser.Image;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLInputElement = struct { pub const HTMLInputElement = struct {
pub const Self = parser.Input; pub const Self = parser.Input;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLLIElement = struct { pub const HTMLLIElement = struct {
pub const Self = parser.LI; pub const Self = parser.LI;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLLabelElement = struct { pub const HTMLLabelElement = struct {
pub const Self = parser.Label; pub const Self = parser.Label;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLLegendElement = struct { pub const HTMLLegendElement = struct {
pub const Self = parser.Legend; pub const Self = parser.Legend;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLLinkElement = struct { pub const HTMLLinkElement = struct {
pub const Self = parser.Link; pub const Self = parser.Link;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLMapElement = struct { pub const HTMLMapElement = struct {
pub const Self = parser.Map; pub const Self = parser.Map;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLMetaElement = struct { pub const HTMLMetaElement = struct {
pub const Self = parser.Meta; pub const Self = parser.Meta;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLMeterElement = struct { pub const HTMLMeterElement = struct {
pub const Self = parser.Meter; pub const Self = parser.Meter;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLModElement = struct { pub const HTMLModElement = struct {
pub const Self = parser.Mod; pub const Self = parser.Mod;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLOListElement = struct { pub const HTMLOListElement = struct {
pub const Self = parser.OList; pub const Self = parser.OList;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLObjectElement = struct { pub const HTMLObjectElement = struct {
pub const Self = parser.Object; pub const Self = parser.Object;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLOptGroupElement = struct { pub const HTMLOptGroupElement = struct {
pub const Self = parser.OptGroup; pub const Self = parser.OptGroup;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLOptionElement = struct { pub const HTMLOptionElement = struct {
pub const Self = parser.Option; pub const Self = parser.Option;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLOutputElement = struct { pub const HTMLOutputElement = struct {
pub const Self = parser.Output; pub const Self = parser.Output;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLParagraphElement = struct { pub const HTMLParagraphElement = struct {
pub const Self = parser.Paragraph; pub const Self = parser.Paragraph;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLParamElement = struct { pub const HTMLParamElement = struct {
pub const Self = parser.Param; pub const Self = parser.Param;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLPictureElement = struct { pub const HTMLPictureElement = struct {
pub const Self = parser.Picture; pub const Self = parser.Picture;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLPreElement = struct { pub const HTMLPreElement = struct {
pub const Self = parser.Pre; pub const Self = parser.Pre;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLProgressElement = struct { pub const HTMLProgressElement = struct {
pub const Self = parser.Progress; pub const Self = parser.Progress;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLQuoteElement = struct { pub const HTMLQuoteElement = struct {
pub const Self = parser.Quote; pub const Self = parser.Quote;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
// https://html.spec.whatwg.org/#the-script-element // https://html.spec.whatwg.org/#the-script-element
pub const HTMLScriptElement = struct { pub const HTMLScriptElement = struct {
pub const Self = parser.Script; pub const Self = parser.Script;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
pub fn get_src(self: *parser.Script) !?[]const u8 { pub fn get_src(self: *parser.Script) !?[]const u8 {
return try parser.elementGetAttribute( return try parser.elementGetAttribute(
@@ -837,103 +746,86 @@ pub const HTMLScriptElement = struct {
pub const HTMLSelectElement = struct { pub const HTMLSelectElement = struct {
pub const Self = parser.Select; pub const Self = parser.Select;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLSourceElement = struct { pub const HTMLSourceElement = struct {
pub const Self = parser.Source; pub const Self = parser.Source;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLSpanElement = struct { pub const HTMLSpanElement = struct {
pub const Self = parser.Span; pub const Self = parser.Span;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLStyleElement = struct { pub const HTMLStyleElement = struct {
pub const Self = parser.Style; pub const Self = parser.Style;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLTableElement = struct { pub const HTMLTableElement = struct {
pub const Self = parser.Table; pub const Self = parser.Table;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLTableCaptionElement = struct { pub const HTMLTableCaptionElement = struct {
pub const Self = parser.TableCaption; pub const Self = parser.TableCaption;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLTableCellElement = struct { pub const HTMLTableCellElement = struct {
pub const Self = parser.TableCell; pub const Self = parser.TableCell;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLTableColElement = struct { pub const HTMLTableColElement = struct {
pub const Self = parser.TableCol; pub const Self = parser.TableCol;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLTableRowElement = struct { pub const HTMLTableRowElement = struct {
pub const Self = parser.TableRow; pub const Self = parser.TableRow;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLTableSectionElement = struct { pub const HTMLTableSectionElement = struct {
pub const Self = parser.TableSection; pub const Self = parser.TableSection;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLTemplateElement = struct { pub const HTMLTemplateElement = struct {
pub const Self = parser.Template; pub const Self = parser.Template;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLTextAreaElement = struct { pub const HTMLTextAreaElement = struct {
pub const Self = parser.TextArea; pub const Self = parser.TextArea;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLTimeElement = struct { pub const HTMLTimeElement = struct {
pub const Self = parser.Time; pub const Self = parser.Time;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLTitleElement = struct { pub const HTMLTitleElement = struct {
pub const Self = parser.Title; pub const Self = parser.Title;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLTrackElement = struct { pub const HTMLTrackElement = struct {
pub const Self = parser.Track; pub const Self = parser.Track;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLUListElement = struct { pub const HTMLUListElement = struct {
pub const Self = parser.UList; pub const Self = parser.UList;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub const HTMLVideoElement = struct { pub const HTMLVideoElement = struct {
pub const Self = parser.Video; pub const Self = parser.Video;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const mem_guarantied = true;
}; };
pub fn toInterface(comptime T: type, e: *parser.Element) !T { 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( try runner.testCases(&.{
_: std.mem.Allocator, .{ "let a = document.getElementById('link')", "undefined" },
js_env: *jsruntime.Env, .{ "a.target", "" },
) anyerror!void { .{ "a.target = '_blank'", "_blank" },
var anchor = [_]Case{ .{ "a.target", "_blank" },
.{ .src = "let a = document.getElementById('link')", .ex = "undefined" }, .{ "a.target = ''", "" },
.{ .src = "a.target", .ex = "" },
.{ .src = "a.target = '_blank'", .ex = "_blank" },
.{ .src = "a.target", .ex = "_blank" },
.{ .src = "a.target = ''", .ex = "" },
.{ .src = "a.href", .ex = "foo" }, .{ "a.href", "foo" },
.{ .src = "a.href = 'https://lightpanda.io/'", .ex = "https://lightpanda.io/" }, .{ "a.href = 'https://lightpanda.io/'", "https://lightpanda.io/" },
.{ .src = "a.href", .ex = "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" }, .{ "a.host = 'lightpanda.io:443'", "lightpanda.io:443" },
.{ .src = "a.host", .ex = "lightpanda.io:443" }, .{ "a.host", "lightpanda.io:443" },
.{ .src = "a.port", .ex = "443" }, .{ "a.port", "443" },
.{ .src = "a.hostname", .ex = "lightpanda.io" }, .{ "a.hostname", "lightpanda.io" },
.{ .src = "a.host = 'lightpanda.io'", .ex = "lightpanda.io" }, .{ "a.host = 'lightpanda.io'", "lightpanda.io" },
.{ .src = "a.host", .ex = "lightpanda.io" }, .{ "a.host", "lightpanda.io" },
.{ .src = "a.port", .ex = "" }, .{ "a.port", "" },
.{ .src = "a.hostname", .ex = "lightpanda.io" }, .{ "a.hostname", "lightpanda.io" },
.{ .src = "a.host", .ex = "lightpanda.io" }, .{ "a.host", "lightpanda.io" },
.{ .src = "a.hostname", .ex = "lightpanda.io" }, .{ "a.hostname", "lightpanda.io" },
.{ .src = "a.hostname = 'foo.bar'", .ex = "foo.bar" }, .{ "a.hostname = 'foo.bar'", "foo.bar" },
.{ .src = "a.href", .ex = "https://foo.bar/" }, .{ "a.href", "https://foo.bar/" },
.{ .src = "a.search", .ex = "" }, .{ "a.search", "" },
.{ .src = "a.search = 'q=bar'", .ex = "q=bar" }, .{ "a.search = 'q=bar'", "q=bar" },
.{ .src = "a.search", .ex = "?q=bar" }, .{ "a.search", "?q=bar" },
.{ .src = "a.href", .ex = "https://foo.bar/?q=bar" }, .{ "a.href", "https://foo.bar/?q=bar" },
.{ .src = "a.hash", .ex = "" }, .{ "a.hash", "" },
.{ .src = "a.hash = 'frag'", .ex = "frag" }, .{ "a.hash = 'frag'", "frag" },
.{ .src = "a.hash", .ex = "#frag" }, .{ "a.hash", "#frag" },
.{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" }, .{ "a.href", "https://foo.bar/?q=bar#frag" },
.{ .src = "a.port", .ex = "" }, .{ "a.port", "" },
.{ .src = "a.port = '443'", .ex = "443" }, .{ "a.port = '443'", "443" },
.{ .src = "a.host", .ex = "foo.bar:443" }, .{ "a.host", "foo.bar:443" },
.{ .src = "a.hostname", .ex = "foo.bar" }, .{ "a.hostname", "foo.bar" },
.{ .src = "a.href", .ex = "https://foo.bar:443/?q=bar#frag" }, .{ "a.href", "https://foo.bar:443/?q=bar#frag" },
.{ .src = "a.port = null", .ex = "null" }, .{ "a.port = null", "null" },
.{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" }, .{ "a.href", "https://foo.bar/?q=bar#frag" },
.{ .src = "a.href = 'foo'", .ex = "foo" }, .{ "a.href = 'foo'", "foo" },
.{ .src = "a.type", .ex = "" }, .{ "a.type", "" },
.{ .src = "a.type = 'text/html'", .ex = "text/html" }, .{ "a.type = 'text/html'", "text/html" },
.{ .src = "a.type", .ex = "text/html" }, .{ "a.type", "text/html" },
.{ .src = "a.type = ''", .ex = "" }, .{ "a.type = ''", "" },
.{ .src = "a.text", .ex = "OK" }, .{ "a.text", "OK" },
.{ .src = "a.text = 'foo'", .ex = "foo" }, .{ "a.text = 'foo'", "foo" },
.{ .src = "a.text", .ex = "foo" }, .{ "a.text", "foo" },
.{ .src = "a.text = 'OK'", .ex = "OK" }, .{ "a.text = 'OK'", "OK" },
}; }, .{});
try checkCases(js_env, &anchor);
var script = [_]Case{ try runner.testCases(&.{
.{ .src = "let script = document.createElement('script')", .ex = "undefined" }, .{ "let script = document.createElement('script')", "undefined" },
.{ .src = "script.src = 'foo.bar'", .ex = "foo.bar" }, .{ "script.src = 'foo.bar'", "foo.bar" },
.{ .src = "script.async = true", .ex = "true" }, .{ "script.async = true", "true" },
.{ .src = "script.async", .ex = "true" }, .{ "script.async", "true" },
.{ .src = "script.async = false", .ex = "false" }, .{ "script.async = false", "false" },
.{ .src = "script.async", .ex = "false" }, .{ "script.async", "false" },
}; }, .{});
try checkCases(js_env, &script);
var innertext = [_]Case{ try runner.testCases(&.{
.{ .src = "const backup = document.getElementById('content')", .ex = "undefined" }, .{ "const backup = document.getElementById('content')", "undefined" },
.{ .src = "document.getElementById('content').innerText = 'foo';", .ex = "foo" }, .{ "document.getElementById('content').innerText = 'foo';", "foo" },
.{ .src = "document.getElementById('content').innerText", .ex = "foo" }, .{ "document.getElementById('content').innerText", "foo" },
.{ .src = "document.getElementById('content').innerHTML = backup; true;", .ex = "true" }, .{ "document.getElementById('content').innerHTML = backup; true;", "true" },
}; }, .{});
try checkCases(js_env, &innertext);
} }

View File

@@ -19,15 +19,9 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); 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 // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
pub const History = struct { pub const History = struct {
pub const mem_guarantied = true;
const ScrollRestorationMode = enum { const ScrollRestorationMode = enum {
auto, auto,
manual, manual,
@@ -98,31 +92,31 @@ pub const History = struct {
// Tests // Tests
// ----- // -----
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.HTML.History" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
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" },
.{ .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" }, .{ "history.replaceState({}, null, '')", "undefined" },
.{ .src = "history.go(1)", .ex = "undefined" },
.{ .src = "history.go(-1)", .ex = "undefined" },
.{ .src = "history.forward()", .ex = "undefined" }, .{ "history.go()", "undefined" },
.{ "history.go(1)", "undefined" },
.{ "history.go(-1)", "undefined" },
.{ .src = "history.back()", .ex = "undefined" }, .{ "history.forward()", "undefined" },
};
try checkCases(js_env, &history); .{ "history.back()", "undefined" },
}, .{});
} }

View File

@@ -16,8 +16,6 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const generate = @import("../generate.zig");
const HTMLDocument = @import("document.zig").HTMLDocument; const HTMLDocument = @import("document.zig").HTMLDocument;
const HTMLElem = @import("elements.zig"); const HTMLElem = @import("elements.zig");
const Window = @import("window.zig").Window; const Window = @import("window.zig").Window;

View File

@@ -0,0 +1,111 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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", "" },
}, .{});
}

View File

@@ -19,15 +19,9 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); 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 // https://html.spec.whatwg.org/multipage/system-state.html#navigator
pub const Navigator = struct { pub const Navigator = struct {
pub const mem_guarantied = true;
agent: []const u8 = "Lightpanda/1.0", agent: []const u8 = "Lightpanda/1.0",
version: []const u8 = "1.0", version: []const u8 = "1.0",
vendor: []const u8 = "", vendor: []const u8 = "",
@@ -89,14 +83,14 @@ pub const Navigator = struct {
// Tests // Tests
// ----- // -----
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.HTML.Navigator" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
var navigator = [_]Case{
.{ .src = "navigator.userAgent", .ex = "Lightpanda/1.0" }, try runner.testCases(&.{
.{ .src = "navigator.appVersion", .ex = "1.0" }, .{ "navigator.userAgent", "Lightpanda/1.0" },
.{ .src = "navigator.language", .ex = "en-US" }, .{ "navigator.appVersion", "1.0" },
}; .{ "navigator.language", "en-US" },
try checkCases(js_env, &navigator); }, .{});
} }

View File

@@ -18,17 +18,14 @@
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const jsruntime = @import("jsruntime"); const Callback = @import("../env.zig").Callback;
const Callback = jsruntime.Callback; const SessionState = @import("../env.zig").SessionState;
const CallbackArg = jsruntime.CallbackArg;
const Loop = jsruntime.Loop;
const URL = @import("../../../url.zig").URL;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const Navigator = @import("navigator.zig").Navigator; const Navigator = @import("navigator.zig").Navigator;
const History = @import("history.zig").History; const History = @import("history.zig").History;
const Location = @import("location.zig").Location; const Location = @import("location.zig").Location;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const storage = @import("../storage/storage.zig"); 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 // https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
pub const Window = struct { pub const Window = struct {
pub const prototype = *EventTarget; pub const prototype = *EventTarget;
pub const mem_guarantied = true;
pub const global_type = true;
// Extend libdom event target for pure zig struct. // Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{}, base: parser.EventTargetTBase = parser.EventTargetTBase{},
document: ?*parser.DocumentHTML = null, document: ?*parser.DocumentHTML = null,
target: []const u8, target: []const u8 = "",
history: History = .{}, history: History = .{},
location: Location = .{}, location: Location = .{},
storage_shelf: ?*storage.Shelf = null, storage_shelf: ?*storage.Shelf = null,
@@ -53,7 +48,7 @@ pub const Window = struct {
timeoutid: u32 = 0, timeoutid: u32 = 0,
timeoutids: [512]u64 = undefined, timeoutids: [512]u64 = undefined,
navigator: Navigator, navigator: Navigator = .{},
pub fn create(target: ?[]const u8, navigator: ?Navigator) Window { pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
return .{ return .{
@@ -121,11 +116,11 @@ pub const Window = struct {
} }
// TODO handle callback arguments. // 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; if (self.timeoutid >= self.timeoutids.len) return error.TooMuchTimeout;
const ddelay: u63 = delay orelse 0; 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; self.timeoutids[self.timeoutid] = id;
defer self.timeoutid += 1; defer self.timeoutid += 1;
@@ -133,12 +128,12 @@ pub const Window = struct {
return self.timeoutid; 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 // I do would prefer return an error in this case, but it seems some JS
// uses invalid id, in particular id 0. // uses invalid id, in particular id 0.
// So we silently ignore invalid id for now. // So we silently ignore invalid id for now.
if (id >= self.timeoutid) return; if (id >= self.timeoutid) return;
try loop.cancel(self.timeoutids[id], null); try state.loop.cancel(self.timeoutids[id], null);
} }
}; };

View File

@@ -5,8 +5,6 @@ pub const Interfaces = .{
}; };
pub const U32Iterator = struct { pub const U32Iterator = struct {
pub const mem_guarantied = true;
length: u32, length: u32,
index: u32 = 0, index: u32 = 0,

View File

@@ -23,7 +23,6 @@ pub const Mime = struct {
content_type: ContentType, content_type: ContentType,
params: []const u8 = "", params: []const u8 = "",
charset: ?[]const u8 = null, charset: ?[]const u8 = null,
arena: std.heap.ArenaAllocator,
pub const ContentTypeEnum = enum { pub const ContentTypeEnum = enum {
text_xml, text_xml,
@@ -39,19 +38,15 @@ pub const Mime = struct {
other: struct { type: []const u8, sub_type: []const u8 }, 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) { if (input.len > 255) {
return error.TooBig; return error.TooBig;
} }
var arena = std.heap.ArenaAllocator.init(allocator);
errdefer arena.deinit();
var trimmed = trim(input); var trimmed = trim(input);
const content_type, const type_len = try parseContentType(trimmed); const content_type, const type_len = try parseContentType(trimmed);
if (type_len >= trimmed.len) { if (type_len >= trimmed.len) {
return .{ .arena = arena, .content_type = content_type }; return .{ .content_type = content_type };
} }
const params = trimLeft(trimmed[type_len..]); const params = trimLeft(trimmed[type_len..]);
@@ -70,24 +65,19 @@ pub const Mime = struct {
switch (name.len) { switch (name.len) {
7 => if (isCaseEqual("charset", name)) { 7 => if (isCaseEqual("charset", name)) {
charset = try parseValue(arena.allocator(), value); charset = try parseValue(arena, value);
}, },
else => {}, else => {},
} }
} }
return .{ return .{
.arena = arena,
.params = params, .params = params,
.charset = charset, .charset = charset,
.content_type = content_type, .content_type = content_type,
}; };
} }
pub fn deinit(self: *Mime) void {
self.arena.deinit();
}
pub fn isHTML(self: *const Mime) bool { pub fn isHTML(self: *const Mime) bool {
return self.content_type == .text_html; return self.content_type == .text_html;
} }
@@ -158,7 +148,7 @@ pub const Mime = struct {
break :blk v; break :blk v;
}; };
fn parseValue(allocator: Allocator, value: []const u8) ![]const u8 { fn parseValue(arena: Allocator, value: []const u8) ![]const u8 {
if (value[0] != '"') { if (value[0] != '"') {
return value; return value;
} }
@@ -191,7 +181,7 @@ pub const Mime = struct {
} }
value_pos = 1; value_pos = 1;
const owned = try allocator.alloc(u8, unescaped_len); const owned = try arena.alloc(u8, unescaped_len);
for (0..unescaped_len) |i| { for (0..unescaped_len) |i| {
switch (value[value_pos]) { switch (value[value_pos]) {
'"' => break, '"' => break,
@@ -344,8 +334,9 @@ test "Mime: parse charset" {
test "Mime: isHTML" { test "Mime: isHTML" {
const isHTML = struct { const isHTML = struct {
fn isHTML(expected: bool, input: []const u8) !void { fn isHTML(expected: bool, input: []const u8) !void {
var mime = try Mime.parse(testing.allocator, input); var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer mime.deinit(); defer arena.deinit();
var mime = try Mime.parse(arena.allocator(), input);
try testing.expectEqual(expected, mime.isHTML()); try testing.expectEqual(expected, mime.isHTML());
} }
}.isHTML; }.isHTML;
@@ -364,8 +355,10 @@ const Expectation = struct {
}; };
fn expect(expected: Expectation, input: []const u8) !void { fn expect(expected: Expectation, input: []const u8) !void {
var actual = try Mime.parse(testing.allocator, input); var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer actual.deinit(); defer arena.deinit();
const actual = try Mime.parse(arena.allocator(), input);
try testing.expectEqual( try testing.expectEqual(
std.meta.activeTag(expected.content_type), std.meta.activeTag(expected.content_type),

View File

@@ -27,9 +27,10 @@ const c = @cImport({
@cInclude("events/mouse_event.h"); @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 initializes netsurf lib.
// init starts a mimalloc heap arena for the netsurf session. The caller must // 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); defer c.dom_event_listener_unref(listener);
const ehd = EventHandlerDataInternal.fromListener(listener); const ehd = EventHandlerDataInternal.fromListener(listener);
if (ehd) |d| { if (ehd) |d| {
if (cbk_id == d.data.cbk.id()) { if (cbk_id == d.data.cbk.id) {
return lst; return lst;
} }
} }
@@ -669,7 +670,7 @@ pub const EventHandlerData = struct {
// deinitFunc implements the data deinitialization. // deinitFunc implements the data deinitialization.
deinitFunc: ?DeinitFunc = null, 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. // EventHandlerDataInternal groups the EventHandlerFunc and the EventHandlerData.
@@ -687,8 +688,9 @@ const EventHandlerDataInternal = struct {
} }
fn deinit(self: *EventHandlerDataInternal, alloc: std.mem.Allocator) void { fn deinit(self: *EventHandlerDataInternal, alloc: std.mem.Allocator) void {
if (self.data.deinitFunc) |d| d(self.data.data, alloc); if (self.data.deinitFunc) |d| {
self.data.cbk.deinit(alloc); d(self.data.data, alloc);
}
alloc.destroy(self); alloc.destroy(self);
} }
@@ -723,7 +725,7 @@ pub fn eventTargetAddEventListener(
// When a function is used as an event handler, its this parameter is bound // When a function is used as an event handler, its this parameter is bound
// to the DOM element on which the listener is placed. // 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 // 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)); const ctx = @as(*anyopaque, @ptrCast(ehd));
var listener: ?*EventListener = undefined; var listener: ?*EventListener = undefined;

View File

@@ -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" },
}, .{});
}

View File

@@ -19,10 +19,8 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const jsruntime = @import("jsruntime"); const Allocator = std.mem.Allocator;
const Env = jsruntime.Env; const Env = @import("../env.zig").Env;
const fetch = @import("fetch.zig").fetch_polyfill;
const log = std.log.scoped(.polyfill); const log = std.log.scoped(.polyfill);
@@ -33,23 +31,23 @@ const modules = [_]struct {
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source }, .{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
}; };
pub fn load(alloc: std.mem.Allocator, env: *const Env) !void { pub fn load(allocator: Allocator, executor: *Env.Executor) !void {
var try_catch: jsruntime.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(env); try_catch.init(executor);
defer try_catch.deinit(); defer try_catch.deinit();
for (modules) |m| { for (modules) |m| {
const res = env.exec(m.source, m.name) catch { const res = executor.exec(m.source, m.name) catch |err| {
if (try try_catch.err(alloc, env)) |msg| { if (try try_catch.err(allocator)) |msg| {
defer alloc.free(msg); defer allocator.free(msg);
log.err("load {s}: {s}", .{ m.name, msg }); log.err("load {s}: {s}", .{ m.name, msg });
} }
return; return err;
}; };
if (builtin.mode == .Debug) { if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, env); const msg = try res.toString(allocator);
defer alloc.free(msg); defer allocator.free(msg);
log.debug("load {s}: {s}", .{ m.name, msg }); log.debug("load {s}: {s}", .{ m.name, msg });
} }
} }

View File

@@ -3,9 +3,9 @@ const Uri = std.Uri;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const http = @import("../http/client.zig"); const http = @import("../../http/client.zig");
const DateTime = @import("../datetime.zig").DateTime; const DateTime = @import("../../datetime.zig").DateTime;
const public_suffix_list = @import("../data/public_suffix_list.zig").lookup; const public_suffix_list = @import("../../data/public_suffix_list.zig").lookup;
const log = std.log.scoped(.cookie); 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); return std.mem.trimLeft(u8, str, &std.ascii.whitespace);
} }
const testing = @import("../testing.zig"); const testing = @import("../../testing.zig");
test "cookie: findSecondLevelDomain" { test "cookie: findSecondLevelDomain" {
const cases = [_]struct { []const u8, []const u8 }{ const cases = [_]struct { []const u8, []const u8 }{
.{ "", "" }, .{ "", "" },

View File

@@ -18,10 +18,7 @@
const std = @import("std"); const std = @import("std");
const jsruntime = @import("jsruntime"); const DOMError = @import("../netsurf.zig").DOMError;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const DOMError = @import("netsurf").DOMError;
const log = std.log.scoped(.storage); 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 // https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface
pub const Bottle = struct { pub const Bottle = struct {
pub const mem_guarantied = true;
const Map = std.StringHashMapUnmanaged([]const u8); const Map = std.StringHashMapUnmanaged([]const u8);
// allocator is stored. we don't use the JS env allocator b/c the storage // allocator is stored. we don't use the JS env allocator b/c the storage
@@ -216,27 +212,27 @@ pub const Bottle = struct {
// Tests // Tests
// ----- // -----
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.Storage.LocalStorage" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
var storage = [_]Case{
.{ .src = "localStorage.length", .ex = "0" },
.{ .src = "localStorage.setItem('foo', 'bar')", .ex = "undefined" }, try runner.testCases(&.{
.{ .src = "localStorage.length", .ex = "1" }, .{ "localStorage.length", "0" },
.{ .src = "localStorage.getItem('foo')", .ex = "bar" },
.{ .src = "localStorage.removeItem('foo')", .ex = "undefined" },
.{ .src = "localStorage.length", .ex = "0" },
// .{ .src = "localStorage['foo'] = 'bar'", .ex = "undefined" }, .{ "localStorage.setItem('foo', 'bar')", "undefined" },
// .{ .src = "localStorage['foo']", .ex = "bar" }, .{ "localStorage.length", "1" },
// .{ .src = "localStorage.length", .ex = "1" }, .{ "localStorage.getItem('foo')", "bar" },
.{ "localStorage.removeItem('foo')", "undefined" },
.{ "localStorage.length", "0" },
.{ .src = "localStorage.clear()", .ex = "undefined" }, // .{ "localStorage['foo'] = 'bar'", "undefined" },
.{ .src = "localStorage.length", .ex = "0" }, // .{ "localStorage['foo']", "bar" },
}; // .{ "localStorage.length", "1" },
try checkCases(js_env, &storage);
.{ "localStorage.clear()", "undefined" },
.{ "localStorage.length", "0" },
}, .{});
} }
test "storage bottle" { test "storage bottle" {

View File

@@ -18,8 +18,8 @@
const std = @import("std"); const std = @import("std");
const Reader = @import("../str/parser.zig").Reader; const Reader = @import("../../str/parser.zig").Reader;
const asUint = @import("../str/parser.zig").asUint; const asUint = @import("../../str/parser.zig").asUint;
// Values is a map with string key of string values. // Values is a map with string key of string values.
pub const Values = struct { pub const Values = struct {

View File

@@ -17,10 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const SessionState = @import("../env.zig").SessionState;
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const query = @import("query.zig"); const query = @import("query.zig");
@@ -31,14 +28,14 @@ pub const Interfaces = .{
// https://url.spec.whatwg.org/#url // 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: // way:
// //
// 1. We can eventually get the slice of scheme *with* the following char in // 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. // 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 // 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 // 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. // from rawuri.
// //
// 2. The other way would bu to copy the `std.Uri` code to ahve a dedicated // 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, uri: std.Uri,
search_params: URLSearchParams, search_params: URLSearchParams,
pub const mem_guarantied = true; pub fn constructor(
url: []const u8,
pub fn constructor(arena: std.mem.Allocator, url: []const u8, base: ?[]const u8) !URL { base: ?[]const u8,
state: *SessionState,
) !URL {
const arena = state.arena;
const raw = try std.mem.concat(arena, u8, &[_][]const u8{ url, base orelse "" }); const raw = try std.mem.concat(arena, u8, &[_][]const u8{ url, base orelse "" });
errdefer arena.free(raw); errdefer arena.free(raw);
@@ -60,24 +60,15 @@ pub const URL = struct {
pub fn init(arena: std.mem.Allocator, uri: std.Uri) !URL { pub fn init(arena: std.mem.Allocator, uri: std.Uri) !URL {
return .{ return .{
.uri = uri, .uri = uri,
.search_params = try URLSearchParams.constructor( .search_params = try URLSearchParams.init(
arena, arena,
uriComponentNullStr(uri.query), uriComponentNullStr(uri.query),
), ),
}; };
} }
pub fn deinit(self: *URL, alloc: std.mem.Allocator) void { pub fn get_origin(self: *URL, state: *SessionState) ![]const u8 {
self.search_params.deinit(alloc); var buf = std.ArrayList(u8).init(state.arena);
}
// 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();
try self.uri.writeToStream(.{ try self.uri.writeToStream(.{
.scheme = true, .scheme = true,
.authentication = false, .authentication = false,
@@ -86,31 +77,27 @@ pub const URL = struct {
.query = false, .query = false,
.fragment = false, .fragment = false,
}, buf.writer()); }, buf.writer());
return try buf.toOwnedSlice(); return buf.items;
} }
// get_href returns the URL by writing all its components. // get_href returns the URL by writing all its components.
// The query is replaced by a dump of search params. // The query is replaced by a dump of search params.
// //
// the caller must free the returned string. pub fn get_href(self: *URL, state: *SessionState) ![]const u8 {
// TODO return a disposable string const arena = state.arena;
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
pub fn get_href(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
// retrieve the query search from search_params. // retrieve the query search from search_params.
const cur = self.uri.query; const cur = self.uri.query;
defer self.uri.query = cur; defer self.uri.query = cur;
var q = std.ArrayList(u8).init(alloc); var q = std.ArrayList(u8).init(arena);
defer q.deinit();
try self.search_params.values.encode(q.writer()); try self.search_params.values.encode(q.writer());
self.uri.query = .{ .percent_encoded = q.items }; self.uri.query = .{ .percent_encoded = q.items };
return try self.format(alloc); return try self.toString(arena);
} }
// format the url with all its components. // format the url with all its components.
pub fn format(self: *URL, alloc: std.mem.Allocator) ![]const u8 { pub fn toString(self: *URL, arena: std.mem.Allocator) ![]const u8 {
var buf = std.ArrayList(u8).init(alloc); var buf = std.ArrayList(u8).init(arena);
defer buf.deinit();
try self.uri.writeToStream(.{ try self.uri.writeToStream(.{
.scheme = true, .scheme = true,
@@ -120,14 +107,11 @@ pub const URL = struct {
.query = uriComponentNullStr(self.uri.query).len > 0, .query = uriComponentNullStr(self.uri.query).len > 0,
.fragment = uriComponentNullStr(self.uri.fragment).len > 0, .fragment = uriComponentNullStr(self.uri.fragment).len > 0,
}, buf.writer()); }, buf.writer());
return try buf.toOwnedSlice(); return buf.items;
} }
// the caller must free the returned string. pub fn get_protocol(self: *URL, state: *SessionState) ![]const u8 {
// TODO return a disposable string return try std.mem.concat(state.arena, u8, &[_][]const u8{ self.uri.scheme, ":" });
// 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_username(self: *URL) []const u8 { pub fn get_username(self: *URL) []const u8 {
@@ -138,12 +122,8 @@ pub const URL = struct {
return uriComponentNullStr(self.uri.password); return uriComponentNullStr(self.uri.password);
} }
// the caller must free the returned string. pub fn get_host(self: *URL, state: *SessionState) ![]const u8 {
// TODO return a disposable string var buf = std.ArrayList(u8).init(state.arena);
// 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();
try self.uri.writeToStream(.{ try self.uri.writeToStream(.{
.scheme = false, .scheme = false,
@@ -153,24 +133,20 @@ pub const URL = struct {
.query = false, .query = false,
.fragment = false, .fragment = false,
}, buf.writer()); }, buf.writer());
return try buf.toOwnedSlice(); return buf.items;
} }
pub fn get_hostname(self: *URL) []const u8 { pub fn get_hostname(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.host); return uriComponentNullStr(self.uri.host);
} }
// the caller must free the returned string. pub fn get_port(self: *URL, state: *SessionState) ![]const u8 {
// TODO return a disposable string const arena = state.arena;
// https://github.com/lightpanda-io/jsruntime-lib/issues/195 if (self.uri.port == null) return try arena.dupe(u8, "");
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();
var buf = std.ArrayList(u8).init(arena);
try std.fmt.formatInt(self.uri.port.?, 10, .lower, .{}, buf.writer()); 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 { pub fn get_pathname(self: *URL) []const u8 {
@@ -178,35 +154,30 @@ pub const URL = struct {
return uriComponentStr(self.uri.path); return uriComponentStr(self.uri.path);
} }
// the caller must free the returned string. pub fn get_search(self: *URL, state: *SessionState) ![]const u8 {
// TODO return a disposable string const arena = state.arena;
// https://github.com/lightpanda-io/jsruntime-lib/issues/195 if (self.search_params.get_size() == 0) return try arena.dupe(u8, "");
pub fn get_search(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
if (self.search_params.get_size() == 0) return try alloc.dupe(u8, "");
var buf: std.ArrayListUnmanaged(u8) = .{}; var buf: std.ArrayListUnmanaged(u8) = .{};
defer buf.deinit(alloc);
try buf.append(alloc, '?'); try buf.append(arena, '?');
try self.search_params.values.encode(buf.writer(alloc)); try self.search_params.values.encode(buf.writer(arena));
return buf.toOwnedSlice(alloc); return buf.items;
} }
// the caller must free the returned string. pub fn get_hash(self: *URL, state: *SessionState) ![]const u8 {
// TODO return a disposable string const arena = state.arena;
// https://github.com/lightpanda-io/jsruntime-lib/issues/195 if (self.uri.fragment == null) return try arena.dupe(u8, "");
pub fn get_hash(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
if (self.uri.fragment == null) return try alloc.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 { pub fn get_searchParams(self: *URL) *URLSearchParams {
return &self.search_params; return &self.search_params;
} }
pub fn _toJSON(self: *URL, alloc: std.mem.Allocator) ![]const u8 { pub fn _toJSON(self: *URL, state: *SessionState) ![]const u8 {
return try self.get_href(alloc); return try self.get_href(state);
} }
}; };
@@ -230,16 +201,14 @@ fn uriComponentStr(c: std.Uri.Component) []const u8 {
pub const URLSearchParams = struct { pub const URLSearchParams = struct {
values: query.Values, values: query.Values,
pub const mem_guarantied = true; pub fn constructor(qs: ?[]const u8, state: *SessionState) !URLSearchParams {
return init(state.arena, qs);
pub fn constructor(alloc: std.mem.Allocator, init: ?[]const u8) !URLSearchParams {
return .{
.values = try query.parseQuery(alloc, init orelse ""),
};
} }
pub fn deinit(self: *URLSearchParams, _: std.mem.Allocator) void { pub fn init(arena: std.mem.Allocator, qs: ?[]const u8) !URLSearchParams {
self.values.deinit(); return .{
.values = try query.parseQuery(arena, qs orelse ""),
};
} }
pub fn get_size(self: *URLSearchParams) u32 { pub fn get_size(self: *URLSearchParams) u32 {
@@ -269,47 +238,43 @@ pub const URLSearchParams = struct {
pub fn _sort(_: *URLSearchParams) void {} 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( try runner.testCases(&.{
_: std.mem.Allocator, .{ "var url = new URL('https://foo.bar/path?query#fragment')", "undefined" },
js_env: *jsruntime.Env, .{ "url.origin", "https://foo.bar" },
) anyerror!void { .{ "url.href", "https://foo.bar/path?query#fragment" },
var url = [_]Case{ .{ "url.protocol", "https:" },
.{ .src = "var url = new URL('https://foo.bar/path?query#fragment')", .ex = "undefined" }, .{ "url.username", "" },
.{ .src = "url.origin", .ex = "https://foo.bar" }, .{ "url.password", "" },
.{ .src = "url.href", .ex = "https://foo.bar/path?query#fragment" }, .{ "url.host", "foo.bar" },
.{ .src = "url.protocol", .ex = "https:" }, .{ "url.hostname", "foo.bar" },
.{ .src = "url.username", .ex = "" }, .{ "url.port", "" },
.{ .src = "url.password", .ex = "" }, .{ "url.pathname", "/path" },
.{ .src = "url.host", .ex = "foo.bar" }, .{ "url.search", "?query" },
.{ .src = "url.hostname", .ex = "foo.bar" }, .{ "url.hash", "#fragment" },
.{ .src = "url.port", .ex = "" }, .{ "url.searchParams.get('query')", "" },
.{ .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);
var qs = [_]Case{ try runner.testCases(&.{
.{ .src = "var url = new URL('https://foo.bar/path?a=~&b=%7E#fragment')", .ex = "undefined" }, .{ "var url = new URL('https://foo.bar/path?a=~&b=%7E#fragment')", "undefined" },
.{ .src = "url.searchParams.get('a')", .ex = "~" }, .{ "url.searchParams.get('a')", "~" },
.{ .src = "url.searchParams.get('b')", .ex = "~" }, .{ "url.searchParams.get('b')", "~" },
.{ .src = "url.searchParams.append('c', 'foo')", .ex = "undefined" }, .{ "url.searchParams.append('c', 'foo')", "undefined" },
.{ .src = "url.searchParams.get('c')", .ex = "foo" }, .{ "url.searchParams.get('c')", "foo" },
.{ .src = "url.searchParams.size", .ex = "3" }, .{ "url.searchParams.size", "3" },
// search is dynamic // search is dynamic
.{ .src = "url.search", .ex = "?a=%7E&b=%7E&c=foo" }, .{ "url.search", "?a=%7E&b=%7E&c=foo" },
// href is dynamic // 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" }, .{ "url.searchParams.delete('c', 'foo')", "undefined" },
.{ .src = "url.searchParams.get('c')", .ex = "" }, .{ "url.searchParams.get('c')", "" },
.{ .src = "url.searchParams.delete('a')", .ex = "undefined" }, .{ "url.searchParams.delete('a')", "undefined" },
.{ .src = "url.searchParams.get('a')", .ex = "" }, .{ "url.searchParams.get('a')", "" },
}; }, .{});
try checkCases(js_env, &qs);
} }

View File

@@ -18,19 +18,19 @@
const std = @import("std"); const std = @import("std");
const jsruntime = @import("jsruntime"); const Env = @import("../env.zig").Env;
const Callback = jsruntime.Callback; const Callback = Env.Callback;
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler; const EventHandler = @import("../events/event.zig").EventHandler;
const parser = @import("netsurf"); const parser = @import("../netsurf.zig");
const SessionState = @import("../env.zig").SessionState;
const log = std.log.scoped(.xhr); const log = std.log.scoped(.xhr);
pub const XMLHttpRequestEventTarget = struct { pub const XMLHttpRequestEventTarget = struct {
pub const prototype = *EventTarget; pub const prototype = *EventTarget;
pub const mem_guarantied = true;
// Extend libdom event target for pure zig struct. // Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{}, 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 { fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
const et = @as(*parser.EventTarget, @ptrCast(self)); const et = @as(*parser.EventTarget, @ptrCast(self));
// check if event target has already this listener // 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) { if (lst == null) {
return; return;
} }
@@ -88,39 +88,46 @@ pub const XMLHttpRequestEventTarget = struct {
return self.onloadend_cbk; return self.onloadend_cbk;
} }
pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
if (self.onloadstart_cbk) |cbk| try self.unregister(alloc, "loadstart", cbk); const arena = state.arena;
try self.register(alloc, "loadstart", handler); if (self.onloadstart_cbk) |cbk| try self.unregister(arena, "loadstart", cbk);
try self.register(arena, "loadstart", handler);
self.onloadstart_cbk = handler; self.onloadstart_cbk = handler;
} }
pub fn set_onprogress(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
if (self.onprogress_cbk) |cbk| try self.unregister(alloc, "progress", cbk); const arena = state.arena;
try self.register(alloc, "progress", handler); if (self.onprogress_cbk) |cbk| try self.unregister(arena, "progress", cbk);
try self.register(arena, "progress", handler);
self.onprogress_cbk = handler; self.onprogress_cbk = handler;
} }
pub fn set_onabort(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
if (self.onabort_cbk) |cbk| try self.unregister(alloc, "abort", cbk); const arena = state.arena;
try self.register(alloc, "abort", handler); if (self.onabort_cbk) |cbk| try self.unregister(arena, "abort", cbk);
try self.register(arena, "abort", handler);
self.onabort_cbk = handler; self.onabort_cbk = handler;
} }
pub fn set_onload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
if (self.onload_cbk) |cbk| try self.unregister(alloc, "load", cbk); const arena = state.arena;
try self.register(alloc, "load", handler); if (self.onload_cbk) |cbk| try self.unregister(arena, "load", cbk);
try self.register(arena, "load", handler);
self.onload_cbk = handler; self.onload_cbk = handler;
} }
pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
if (self.ontimeout_cbk) |cbk| try self.unregister(alloc, "timeout", cbk); const arena = state.arena;
try self.register(alloc, "timeout", handler); if (self.ontimeout_cbk) |cbk| try self.unregister(arena, "timeout", cbk);
try self.register(arena, "timeout", handler);
self.ontimeout_cbk = handler; self.ontimeout_cbk = handler;
} }
pub fn set_onloadend(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
if (self.onloadend_cbk) |cbk| try self.unregister(alloc, "loadend", cbk); const arena = state.arena;
try self.register(alloc, "loadend", handler); if (self.onloadend_cbk) |cbk| try self.unregister(arena, "loadend", cbk);
try self.register(arena, "loadend", handler);
self.onloadend_cbk = handler; self.onloadend_cbk = handler;
} }
pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void { pub fn deinit(self: *XMLHttpRequestEventTarget, state: *SessionState) void {
parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), alloc) catch |e| { const arena = state.arena;
parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), arena) catch |e| {
log.err("remove all listeners: {any}", .{e}); log.err("remove all listeners: {any}", .{e});
}; };
} }

View File

@@ -18,11 +18,7 @@
const std = @import("std"); const std = @import("std");
const jsruntime = @import("jsruntime"); const parser = @import("../netsurf.zig");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("netsurf");
const Event = @import("../events/event.zig").Event; const Event = @import("../events/event.zig").Event;
const DOMException = @import("../dom/exceptions.zig").DOMException; const DOMException = @import("../dom/exceptions.zig").DOMException;
@@ -30,7 +26,6 @@ const DOMException = @import("../dom/exceptions.zig").DOMException;
pub const ProgressEvent = struct { pub const ProgressEvent = struct {
pub const prototype = *Event; pub const prototype = *Event;
pub const Exception = DOMException; pub const Exception = DOMException;
pub const mem_guarantied = true;
pub const EventInit = struct { pub const EventInit = struct {
lengthComputable: bool = false, 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; return self.lengthComputable;
} }
pub fn get_loaded(self: ProgressEvent) u64 { pub fn get_loaded(self: *const ProgressEvent) u64 {
return self.loaded; return self.loaded;
} }
pub fn get_total(self: ProgressEvent) u64 { pub fn get_total(self: *const ProgressEvent) u64 {
return self.total; return self.total;
} }
}; };
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.XHR.ProgressEvent" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
var progress_event = [_]Case{
.{ .src = "let pevt = new ProgressEvent('foo');", .ex = "undefined" }, try runner.testCases(&.{
.{ .src = "pevt.loaded", .ex = "0" }, .{ "let pevt = new ProgressEvent('foo');", "undefined" },
.{ .src = "pevt instanceof ProgressEvent", .ex = "true" }, .{ "pevt.loaded", "0" },
.{ .src = "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", .ex = "undefined" }, .{ "pevt instanceof ProgressEvent", "true" },
.{ .src = "document.addEventListener('foo', ccbk)", .ex = "undefined" }, .{ "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", "undefined" },
.{ .src = "document.dispatchEvent(pevt)", .ex = "true" }, .{ "document.addEventListener('foo', ccbk)", "undefined" },
.{ .src = "eevt.type", .ex = "foo" }, .{ "document.dispatchEvent(pevt)", "true" },
.{ .src = "eevt instanceof ProgressEvent", .ex = "true" }, .{ "eevt.type", "foo" },
}; .{ "eevt instanceof ProgressEvent", "true" },
try checkCases(js_env, &progress_event); }, .{});
} }

View File

@@ -17,27 +17,20 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const jsruntime = @import("jsruntime"); const DOMError = @import("../netsurf.zig").DOMError;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const DOMError = @import("netsurf").DOMError;
const DOMException = @import("../dom/exceptions.zig").DOMException; const DOMException = @import("../dom/exceptions.zig").DOMException;
const ProgressEvent = @import("progress_event.zig").ProgressEvent; const ProgressEvent = @import("progress_event.zig").ProgressEvent;
const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEventTarget; const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEventTarget;
const Mime = @import("../browser/mime.zig").Mime; const URL = @import("../../url.zig").URL;
const Mime = @import("../mime.zig").Mime;
const Loop = jsruntime.Loop; const parser = @import("../netsurf.zig");
const URL = @import("../url.zig").URL; const http = @import("../../http/client.zig");
const http = @import("../http/client.zig"); const SessionState = @import("../env.zig").SessionState;
const parser = @import("netsurf");
const CookieJar = @import("../storage/storage.zig").CookieJar; const CookieJar = @import("../storage/storage.zig").CookieJar;
const UserContext = @import("../user_context.zig").UserContext;
const log = std.log.scoped(.xhr); const log = std.log.scoped(.xhr);
@@ -51,7 +44,6 @@ pub const Interfaces = .{
pub const XMLHttpRequestUpload = struct { pub const XMLHttpRequestUpload = struct {
pub const prototype = *XMLHttpRequestEventTarget; pub const prototype = *XMLHttpRequestEventTarget;
pub const mem_guarantied = true;
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
}; };
@@ -83,7 +75,7 @@ pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) {
// Duplicate the body content. // Duplicate the body content.
// The caller owns the allocated string. // 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) { return switch (self) {
.Blob => error.NotImplemented, .Blob => error.NotImplemented,
.BufferSource => error.NotImplemented, .BufferSource => error.NotImplemented,
@@ -96,7 +88,7 @@ pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) {
pub const XMLHttpRequest = struct { pub const XMLHttpRequest = struct {
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
alloc: std.mem.Allocator, arena: Allocator,
client: *http.Client, client: *http.Client,
request: ?http.Request = null, request: ?http.Request = null,
@@ -134,10 +126,6 @@ pub const XMLHttpRequest = struct {
response_type: ResponseType = .Empty, response_type: ResponseType = .Empty,
response_headers: Headers, 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, response_status: u16 = 0,
// TODO uncomment this field causes casting issue with // TODO uncomment this field causes casting issue with
@@ -151,7 +139,6 @@ pub const XMLHttpRequest = struct {
send_flag: bool = false, send_flag: bool = false,
pub const prototype = *XMLHttpRequestEventTarget; pub const prototype = *XMLHttpRequestEventTarget;
pub const mem_guarantied = true;
const State = enum(u16) { const State = enum(u16) {
unsent = 0, unsent = 0,
@@ -174,41 +161,41 @@ pub const XMLHttpRequest = struct {
const JSONValue = std.json.Value; const JSONValue = std.json.Value;
const Headers = struct { const Headers = struct {
alloc: std.mem.Allocator,
list: List, list: List,
arena: Allocator,
const List = std.ArrayListUnmanaged(std.http.Header); const List = std.ArrayListUnmanaged(std.http.Header);
fn init(alloc: std.mem.Allocator) Headers { fn init(arena: Allocator) Headers {
return .{ return .{
.alloc = alloc, .arena = arena,
.list = List{}, .list = .{},
}; };
} }
fn deinit(self: *Headers) void { fn deinit(self: *Headers) void {
self.free(); self.free();
self.list.deinit(self.alloc); self.list.deinit(self.arena);
} }
fn append(self: *Headers, k: []const u8, v: []const u8) !void { fn append(self: *Headers, k: []const u8, v: []const u8) !void {
// duplicate strings // duplicate strings
const kk = try self.alloc.dupe(u8, k); const kk = try self.arena.dupe(u8, k);
const vv = try self.alloc.dupe(u8, v); const vv = try self.arena.dupe(u8, v);
try self.list.append(self.alloc, .{ .name = kk, .value = vv }); try self.list.append(self.arena, .{ .name = kk, .value = vv });
} }
// free all strings allocated. // free all strings allocated.
fn free(self: *Headers) void { fn free(self: *Headers) void {
for (self.list.items) |h| { for (self.list.items) |h| {
self.alloc.free(h.name); self.arena.free(h.name);
self.alloc.free(h.value); self.arena.free(h.value);
} }
} }
fn clearAndFree(self: *Headers) void { fn clearAndFree(self: *Headers) void {
self.free(); self.free();
self.list.clearAndFree(self.alloc); self.list.clearAndFree(self.arena);
} }
fn has(self: Headers, k: []const u8) bool { fn has(self: Headers, k: []const u8) bool {
@@ -236,8 +223,8 @@ pub const XMLHttpRequest = struct {
for (self.list.items, 0..) |h, i| { for (self.list.items, 0..) |h, i| {
if (std.ascii.eqlIgnoreCase(k, h.name)) { if (std.ascii.eqlIgnoreCase(k, h.name)) {
const hh = self.list.swapRemove(i); const hh = self.list.swapRemove(i);
self.alloc.free(hh.name); self.arena.free(hh.name);
self.alloc.free(hh.value); self.arena.free(hh.value);
} }
} }
self.append(k, v); self.append(k, v);
@@ -286,17 +273,18 @@ pub const XMLHttpRequest = struct {
const min_delay: u64 = 50000000; // 50ms 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 .{ return .{
.alloc = alloc, .arena = arena,
.headers = Headers.init(alloc), .headers = Headers.init(arena),
.response_headers = Headers.init(alloc), .response_headers = Headers.init(arena),
.method = undefined, .method = undefined,
.state = .unsent, .state = .unsent,
.url = null, .url = null,
.origin_url = userctx.url, .origin_url = session_state.url,
.client = userctx.http_client, .client = session_state.http_client,
.cookie_jar = userctx.cookie_jar, .cookie_jar = session_state.cookie_jar,
}; };
} }
@@ -307,10 +295,7 @@ pub const XMLHttpRequest = struct {
self.response_obj = null; self.response_obj = null;
self.response_type = .Empty; self.response_type = .Empty;
if (self.response_mime) |*mime| { self.response_mime = null;
mime.deinit();
self.response_mime = null;
}
// TODO should we clearRetainingCapacity instead? // TODO should we clearRetainingCapacity instead?
self.headers.clearAndFree(); self.headers.clearAndFree();
@@ -322,7 +307,7 @@ pub const XMLHttpRequest = struct {
self.priv_state = .new; 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.reset();
self.headers.deinit(); self.headers.deinit();
self.response_headers.deinit(); self.response_headers.deinit();
@@ -362,7 +347,6 @@ pub const XMLHttpRequest = struct {
pub fn _open( pub fn _open(
self: *XMLHttpRequest, self: *XMLHttpRequest,
alloc: std.mem.Allocator,
method: []const u8, method: []const u8,
url: []const u8, url: []const u8,
asyn: ?bool, asyn: ?bool,
@@ -375,12 +359,13 @@ pub const XMLHttpRequest = struct {
// TODO If thiss relevant global object is a Window object and its // TODO If thiss relevant global object is a Window object and its
// associated Document is not fully active, then throw an // associated Document is not fully active, then throw an
// "InvalidStateError" DOMException. // "InvalidStateError" DOMException.
self.method = try validMethod(method);
self.reset(); 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.?}); log.debug("open url ({s})", .{self.url.?});
self.sync = if (asyn) |b| !b else false; 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 // 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.state != .opened) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState; if (self.send_flag) return DOMError.InvalidState;
@@ -485,7 +470,7 @@ pub const XMLHttpRequest = struct {
{ {
var arr: std.ArrayListUnmanaged(u8) = .{}; 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, .navigation = false,
.origin_uri = &self.origin_url.uri, .origin_uri = &self.origin_url.uri,
}); });
@@ -501,12 +486,12 @@ pub const XMLHttpRequest = struct {
// var used_body: ?XMLHttpRequestBodyInit = null; // var used_body: ?XMLHttpRequestBodyInit = null;
if (body) |b| { if (body) |b| {
if (self.method != .GET and self.method != .HEAD) { 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.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 { pub fn onHttpResponse(self: *XMLHttpRequest, progress_: anyerror!http.Progress) !void {
@@ -526,8 +511,15 @@ pub const XMLHttpRequest = struct {
} }
// extract a mime type from headers. // 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 // TODO handle override mime type
self.state = .headers_received; self.state = .headers_received;
@@ -545,7 +537,7 @@ pub const XMLHttpRequest = struct {
} }
if (progress.data) |data| { 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; const loaded = self.response_bytes.items.len;
@@ -636,7 +628,7 @@ pub const XMLHttpRequest = struct {
return url.raw; 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) { if (self.response_type != .Empty and self.response_type != .Document) {
return DOMError.InvalidState; return DOMError.InvalidState;
} }
@@ -652,7 +644,7 @@ pub const XMLHttpRequest = struct {
}; };
} }
self.setResponseObjDocument(alloc); self.setResponseObjDocument();
if (self.response_obj) |obj| { if (self.response_obj) |obj| {
return switch (obj) { return switch (obj) {
@@ -665,7 +657,7 @@ pub const XMLHttpRequest = struct {
} }
// https://xhr.spec.whatwg.org/#the-response-attribute // 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.response_type == .Empty or self.response_type == .Text) {
if (self.state == .loading or self.state == .done) { if (self.state == .loading or self.state == .done) {
return .{ .Text = try self.get_responseText() }; return .{ .Text = try self.get_responseText() };
@@ -703,7 +695,7 @@ pub const XMLHttpRequest = struct {
// Otherwise, if thiss response type is "document", set a // Otherwise, if thiss response type is "document", set a
// document response for this. // document response for this.
if (self.response_type == .Document) { if (self.response_type == .Document) {
self.setResponseObjDocument(alloc); self.setResponseObjDocument();
} }
if (self.response_type == .JSON) { 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 // TODO Let jsonObject be the result of running parse JSON from bytes
// on thiss received bytes. If that threw an exception, then return // on thiss received bytes. If that threw an exception, then return
// null. // null.
self.setResponseObjJSON(alloc); self.setResponseObjJSON();
} }
if (self.response_obj) |obj| { 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. // If the par sing fails, a Failure is stored in response_obj.
// TODO parse XML. // TODO parse XML.
// https://xhr.spec.whatwg.org/#response-object // 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 response_mime = &self.response_mime.?;
const isHTML = response_mime.isHTML(); const isHTML = response_mime.isHTML();
// TODO If finalMIME is not an HTML MIME type or an XML MIME type, then // TODO If finalMIME is not an HTML MIME type or an XML MIME type, then
// return. // return.
if (!isHTML) return; if (!isHTML) {
const ccharset = alloc.dupeZ(u8, response_mime.charset orelse "utf-8") catch {
self.response_obj = .{ .Failure = true };
return; 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); var fbs = std.io.fixedBufferStream(self.response_bytes.items);
const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch { 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. // 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 // TODO should we use parseFromSliceLeaky if we expect the allocator is
// already an arena? // already an arena?
const p = std.json.parseFromSlice( const p = std.json.parseFromSlice(
JSONValue, JSONValue,
alloc, self.arena,
self.response_bytes.items, self.response_bytes.items,
.{}, .{},
) catch |e| { ) catch |e| {
@@ -790,14 +786,12 @@ pub const XMLHttpRequest = struct {
// TODO change the return type to express the string ownership and let // TODO change the return type to express the string ownership and let
// jsruntime free the string once copied to v8. // jsruntime free the string once copied to v8.
// see https://github.com/lightpanda-io/jsruntime-lib/issues/195 // 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 ""; if (self.response_headers.list.items.len == 0) return "";
self.response_headers.sort(); self.response_headers.sort();
var buf: std.ArrayListUnmanaged(u8) = .{}; var buf: std.ArrayListUnmanaged(u8) = .{};
errdefer buf.deinit(alloc); const w = buf.writer(self.arena);
const w = buf.writer(alloc);
for (self.response_headers.list.items) |entry| { for (self.response_headers.list.items) |entry| {
if (entry.value.len == 0) continue; if (entry.value.len == 0) continue;
@@ -822,101 +816,96 @@ pub const XMLHttpRequest = struct {
} }
}; };
pub fn testExecFn( const testing = @import("../../testing.zig");
_: std.mem.Allocator, test "Browser.XHR.XMLHttpRequest" {
js_env: *jsruntime.Env, var runner = try testing.jsRunner(testing.tracking_allocator, .{});
) anyerror!void { defer runner.deinit();
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" },
.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" }, try runner.testCases(&.{
// Getter returning a callback crashes. .{ "var nb = 0; var evt = null; function cbk(event) { nb ++; evt = event; }", "undefined" },
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/200 .{ "const req = new XMLHttpRequest()", "undefined" },
// .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; evt = event; }" },
//.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" },
.{ .src = "req.open('GET', 'https://httpbin.io/html')", .ex = "undefined" }, .{ "req.onload = cbk", "function cbk(event) { nb ++; evt = event; }" },
.{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" },
.{ "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 // ensure open resets values
.{ .src = "req.status", .ex = "0" }, .{ "req.status ", "0" },
.{ .src = "req.statusText", .ex = "" }, .{ "req.statusText", "" },
.{ .src = "req.getAllResponseHeaders()", .ex = "" }, .{ "req.getAllResponseHeaders()", "" },
.{ .src = "req.getResponseHeader('Content-Type')", .ex = "null" }, .{ "req.getResponseHeader('Content-Type')", "null" },
.{ .src = "req.responseText", .ex = "" }, .{ "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. // Each case executed waits for all loop callaback calls.
// So the url has been retrieved. // So the url has been retrieved.
.{ .src = "nb", .ex = "1" }, .{ "req2.status", "200" },
.{ .src = "evt.type", .ex = "load" }, .{ "req2.statusText", "OK" },
.{ .src = "evt.loaded > 0", .ex = "true" }, .{ "req2.response instanceof Document", "true" },
.{ .src = "evt instanceof ProgressEvent", .ex = "true" }, .{ "req2.responseXML instanceof Document", "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);
var document = [_]Case{ try runner.testCases(&.{
.{ .src = "const req2 = new XMLHttpRequest()", .ex = "undefined" }, .{ "const req3 = new XMLHttpRequest()", "undefined" },
.{ .src = "req2.open('GET', 'https://httpbin.io/html')", .ex = "undefined" }, .{ "req3.open('GET', 'https://127.0.0.1:9581/xhr/json')", "undefined" },
.{ .src = "req2.responseType = 'document'", .ex = "document" }, .{ "req3.responseType = 'json'", "json" },
.{ .src = "req2.send()", .ex = "undefined" }, .{ "req3.send()", "undefined" },
// Each case executed waits for all loop callaback calls. // Each case executed waits for all loop callaback calls.
// So the url has been retrieved. // So the url has been retrieved.
.{ .src = "req2.status", .ex = "200" }, .{ "req3.status", "200" },
.{ .src = "req2.statusText", .ex = "OK" }, .{ "req3.statusText", "OK" },
.{ .src = "req2.response instanceof Document", .ex = "true" }, .{ "req3.response.over", "9000!!!" },
.{ .src = "req2.responseXML instanceof Document", .ex = "true" }, }, .{});
};
try checkCases(js_env, &document);
var json = [_]Case{ try runner.testCases(&.{
.{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" }, .{ "const req4 = new XMLHttpRequest()", "undefined" },
.{ .src = "req3.open('GET', 'https://httpbin.io/json')", .ex = "undefined" }, .{ "req4.open('POST', 'https://127.0.0.1:9581/xhr')", "undefined" },
.{ .src = "req3.responseType = 'json'", .ex = "json" }, .{ "req4.send('foo')", "undefined" },
.{ .src = "req3.send()", .ex = "undefined" },
// Each case executed waits for all loop callaback calls. // Each case executed waits for all loop callaback calls.
// So the url has been retrieved. // So the url has been retrieved.
.{ .src = "req3.status", .ex = "200" }, .{ "req4.status", "200" },
.{ .src = "req3.statusText", .ex = "OK" }, .{ "req4.statusText", "OK" },
.{ .src = "req3.response.slideshow.author", .ex = "Yours Truly" }, .{ "req4.responseText.length > 64", "true" },
}; }, .{});
try checkCases(js_env, &json);
var post = [_]Case{ try runner.testCases(&.{
.{ .src = "const req4 = new XMLHttpRequest()", .ex = "undefined" }, .{ "const req5 = new XMLHttpRequest()", "undefined" },
.{ .src = "req4.open('POST', 'https://httpbin.io/post')", .ex = "undefined" }, .{ "req5.open('GET', 'https://127.0.0.1:9581/xhr')", "undefined" },
.{ .src = "req4.send('foo')", .ex = "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. // Each case executed waits for all loop callaback calls.
// So the url has been retrieved. // So the url has been retrieved.
.{ .src = "req4.status", .ex = "200" }, .{ "status", "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);
} }

View File

@@ -18,14 +18,11 @@
// //
const std = @import("std"); const std = @import("std");
const jsruntime = @import("jsruntime"); const SessionState = @import("../env.zig").SessionState;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const DOMError = @import("netsurf").DOMError; const dump = @import("../dump.zig");
const parser = @import("../netsurf.zig");
const parser = @import("netsurf"); const DOMError = parser.DOMError;
const dump = @import("../browser/dump.zig");
pub const Interfaces = .{ pub const Interfaces = .{
XMLSerializer, XMLSerializer,
@@ -33,39 +30,28 @@ pub const Interfaces = .{
// https://w3c.github.io/DOM-Parsing/#dom-xmlserializer-constructor // https://w3c.github.io/DOM-Parsing/#dom-xmlserializer-constructor
pub const XMLSerializer = struct { pub const XMLSerializer = struct {
pub const mem_guarantied = true;
pub fn constructor() !XMLSerializer { pub fn constructor() !XMLSerializer {
return .{}; return .{};
} }
pub fn deinit(_: *XMLSerializer, _: std.mem.Allocator) void {} pub fn _serializeToString(_: *const XMLSerializer, root: *parser.Node, state: *SessionState) ![]const u8 {
var buf = std.ArrayList(u8).init(state.arena);
pub fn _serializeToString(_: XMLSerializer, alloc: std.mem.Allocator, root: *parser.Node) ![]const u8 {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
if (try parser.nodeType(root) == .document) { if (try parser.nodeType(root) == .document) {
try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), buf.writer()); try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), buf.writer());
} else { } else {
try dump.writeNode(root, buf.writer()); try dump.writeNode(root, buf.writer());
} }
// TODO express the caller owned the slice. return buf.items;
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
return try buf.toOwnedSlice();
} }
}; };
// Tests const testing = @import("../../testing.zig");
// ----- test "Browser.XMLSerializer" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
pub fn testExecFn( try runner.testCases(&.{
_: std.mem.Allocator, .{ "const s = new XMLSerializer()", "undefined" },
js_env: *jsruntime.Env, .{ "s.serializeToString(document.getElementById('para'))", "<p id=\"para\"> And</p>" },
) anyerror!void { }, .{});
var serializer = [_]Case{
.{ .src = "const s = new XMLSerializer()", .ex = "undefined" },
.{ .src = "s.serializeToString(document.getElementById('para'))", .ex = "<p id=\"para\"> And</p>" },
};
try checkCases(js_env, &serializer);
} }

View File

@@ -17,9 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const parser = @import("../browser/netsurf.zig");
pub const Id = u32; pub const Id = u32;
const log = std.log.scoped(.cdp_node); const log = std.log.scoped(.cdp_node);

View File

@@ -72,13 +72,16 @@ pub fn CDPT(comptime TypeProvider: type) type {
pub const Browser = TypeProvider.Browser; pub const Browser = TypeProvider.Browser;
pub const Session = TypeProvider.Session; 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 allocator = app.allocator;
const browser = try Browser.init(app);
errdefer browser.deinit();
return .{ return .{
.client = client, .client = client,
.browser = browser,
.allocator = allocator, .allocator = allocator,
.browser_context = null, .browser_context = null,
.browser = Browser.init(app),
.message_arena = std.heap.ArenaAllocator.init(allocator), .message_arena = std.heap.ArenaAllocator.init(allocator),
.browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator), .browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator),
}; };

View File

@@ -17,10 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const parser = @import("netsurf");
const Node = @import("../Node.zig"); const Node = @import("../Node.zig");
const css = @import("../../dom/css.zig"); const css = @import("../../browser/dom/css.zig");
const dom_node = @import("../../dom/node.zig"); const parser = @import("../../browser/netsurf.zig");
const dom_node = @import("../../browser/dom/node.zig");
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { 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 // 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 // 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 remote_object = try bc.session.inspector.getRemoteObject(
const remoteObject = try bc.session.inspector.getRemoteObject(&bc.session.env, jsValue, params.objectGroup orelse ""); bc.session.executor,
defer remoteObject.deinit(); params.objectGroup orelse "",
try dom_node.Node.toInterface(node._node),
);
defer remote_object.deinit();
const arena = cmd.arena; const arena = cmd.arena;
return cmd.sendResult(.{ .object = .{ return cmd.sendResult(.{ .object = .{
.type = try remoteObject.getType(arena), .type = try remote_object.getType(arena),
.subtype = try remoteObject.getSubtype(arena), .subtype = try remote_object.getSubtype(arena),
.className = try remoteObject.getClassName(arena), .className = try remote_object.getClassName(arena),
.description = try remoteObject.getDescription(arena), .description = try remote_object.getDescription(arena),
.objectId = try remoteObject.getObjectId(arena), .objectId = try remote_object.getObjectId(arena),
} }, .{}); } }, .{});
} }

View File

@@ -23,9 +23,9 @@ const Allocator = std.mem.Allocator;
const Testing = @This(); const Testing = @This();
const main = @import("cdp.zig"); const main = @import("cdp.zig");
const parser = @import("netsurf");
const URL = @import("../url.zig").URL; const URL = @import("../url.zig").URL;
const App = @import("../app.zig").App; const App = @import("../app.zig").App;
const parser = @import("../browser/netsurf.zig");
const base = @import("../testing.zig"); const base = @import("../testing.zig");
pub const allocator = base.allocator; pub const allocator = base.allocator;
@@ -40,7 +40,7 @@ const Browser = struct {
session: ?*Session = null, session: ?*Session = null,
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
pub fn init(app: *App) Browser { pub fn init(app: *App) !Browser {
return .{ return .{
.arena = std.heap.ArenaAllocator.init(app.allocator), .arena = std.heap.ArenaAllocator.init(app.allocator),
}; };
@@ -61,8 +61,8 @@ const Browser = struct {
self.session.?.* = .{ self.session.?.* = .{
.page = null, .page = null,
.arena = arena, .arena = arena,
.env = Env{}, .executor = .{},
.inspector = Inspector{}, .inspector = .{},
}; };
return self.session.?; return self.session.?;
} }
@@ -78,7 +78,7 @@ const Browser = struct {
const Session = struct { const Session = struct {
page: ?Page = null, page: ?Page = null,
arena: Allocator, arena: Allocator,
env: Env, executor: Executor,
inspector: Inspector, inspector: Inspector,
pub fn currentPage(self: *Session) ?*Page { pub fn currentPage(self: *Session) ?*Page {
@@ -107,19 +107,19 @@ const Session = struct {
} }
}; };
const Env = struct { const Executor = struct {};
pub fn findOrAddValue(self: *Env, value: anytype) !@TypeOf(value) { // ?
_ = self;
return value;
}
};
const Inspector = 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; _ = self;
_ = env; _ = executor;
_ = jsValue; _ = group;
_ = groupName; _ = value;
return RemoteObject{}; return RemoteObject{};
} }
}; };
@@ -217,7 +217,7 @@ const TestContext = struct {
self.client = Client.init(self.arena.allocator()); self.client = Client.init(self.arena.allocator());
// Don't use the arena here. We want to detect leaks in CDP. // Don't use the arena here. We want to detect leaks in CDP.
// The arena is only for test-specific stuff // 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_.?; return &self.cdp_.?;
} }

View File

@@ -1,468 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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);
}

View File

@@ -1,243 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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);
}

View File

@@ -1,263 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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;

View File

@@ -1,129 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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);
}

View File

@@ -27,9 +27,8 @@ const MemoryPool = std.heap.MemoryPool;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const tls = @import("tls"); const tls = @import("tls");
const jsruntime = @import("jsruntime"); const IO = @import("../runtime/loop.zig").IO;
const IO = jsruntime.IO; const Loop = @import("../runtime/loop.zig").Loop;
const Loop = jsruntime.Loop;
const log = std.log.scoped(.http_client); 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 { 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); errdefer root_ca.deinit(allocator);
const state_pool = try StatePool.init(allocator, max_concurrent); const state_pool = try StatePool.init(allocator, max_concurrent);
@@ -69,7 +68,9 @@ pub const Client = struct {
pub fn deinit(self: *Client) void { pub fn deinit(self: *Client) void {
const allocator = self.allocator; const allocator = self.allocator;
self.root_ca.deinit(allocator); if (builtin.is_test == false) {
self.root_ca.deinit(allocator);
}
self.state_pool.deinit(allocator); self.state_pool.deinit(allocator);
} }
@@ -1907,7 +1908,7 @@ test "HttpClient: sync GET redirect" {
} }
test "HttpClient: async connect error" { test "HttpClient: async connect error" {
var loop = try jsruntime.Loop.init(testing.allocator); var loop = try Loop.init(testing.allocator);
defer loop.deinit(); defer loop.deinit();
const Handler = struct { const Handler = struct {
@@ -2193,7 +2194,7 @@ const TestResponse = struct {
}; };
const CaptureHandler = struct { const CaptureHandler = struct {
loop: jsruntime.Loop, loop: Loop,
reset: Thread.ResetEvent, reset: Thread.ResetEvent,
response: TestResponse, response: TestResponse,
@@ -2201,7 +2202,7 @@ const CaptureHandler = struct {
return .{ return .{
.reset = .{}, .reset = .{},
.response = TestResponse.init(), .response = TestResponse.init(),
.loop = try jsruntime.Loop.init(testing.allocator), .loop = try Loop.init(testing.allocator),
}; };
} }

View File

@@ -20,25 +20,19 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const Allocator = std.mem.Allocator; 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 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 parser = @import("browser/netsurf.zig");
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 version = @import("build_info").git_commit; const version = @import("build_info").git_commit;
const log = std.log.scoped(.cli); const log = std.log.scoped(.cli);
pub const std_options = std.Options{ pub const std_options = std.Options{
// Set the log level to info // Set the log level to info
.log_level = .debug, .log_level = .info,
// Define logFn to override the std implementation // Define logFn to override the std implementation
.logFn = logFn, .logFn = logFn,
@@ -60,23 +54,34 @@ pub fn main() !void {
const args = try parseArgs(args_arena.allocator()); const args = try parseArgs(args_arena.allocator());
switch (args.mode) { switch (args.mode) {
.help => args.printUsageAndExit(args.mode.help), .help => {
args.printUsageAndExit(args.mode.help);
return std.process.cleanExit();
},
.version => { .version => {
std.debug.print("{s}\n", .{version}); std.debug.print("{s}\n", .{version});
return std.process.cleanExit(); 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| { .serve => |opts| {
const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| { const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| {
log.err("address (host:port) {any}\n", .{err}); log.err("address (host:port) {any}\n", .{err});
return args.printUsageAndExit(false); 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); const timeout = std.time.ns_per_s * @as(u64, opts.timeout);
server.run(app, address, timeout) catch |err| { 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 }); log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump });
const url = try @import("url.zig").URL.parse(opts.url, null); 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 // browser
var browser = Browser.init(app); var browser = try Browser.init(app);
defer browser.deinit(); defer browser.deinit();
var session = try browser.newSession({}); var session = try browser.newSession({});
@@ -126,6 +120,7 @@ pub fn main() !void {
try page.dump(std.io.getStdOut()); try page.dump(std.io.getStdOut());
} }
}, },
else => unreachable,
} }
} }
@@ -133,6 +128,21 @@ const Command = struct {
mode: Mode, mode: Mode,
exec_name: []const u8, 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) { const Mode = union(App.RunMode) {
help: bool, // false when being printed because of an error help: bool, // false when being printed because of an error
fetch: Fetch, fetch: Fetch,
@@ -144,6 +154,7 @@ const Command = struct {
host: []const u8, host: []const u8,
port: u16, port: u16,
timeout: u16, timeout: u16,
gc_hints: bool,
tls_verify_host: bool, tls_verify_host: bool,
}; };
@@ -187,6 +198,9 @@ const Command = struct {
\\--timeout Inactivity timeout in seconds before disconnecting clients \\--timeout Inactivity timeout in seconds before disconnecting clients
\\ Defaults to 3 (seconds) \\ Defaults to 3 (seconds)
\\ \\
\\--gc_hints Encourage V8 to cleanup garbage for each new browser context.
\\ Defaults to false
\\
\\--insecure_disable_tls_host_verification \\--insecure_disable_tls_host_verification
\\ Disables host verification on all HTTP requests. \\ Disables host verification on all HTTP requests.
\\ This is an advanced option which should only be \\ 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")) { if (std.mem.eql(u8, opt, "--timeout")) {
return .serve; return .serve;
} }
if (std.mem.eql(u8, opt, "--gc_hints")) {
return .serve;
}
return null; return null;
} }
@@ -276,6 +295,7 @@ fn parseServeArgs(
var host: []const u8 = "127.0.0.1"; var host: []const u8 = "127.0.0.1";
var port: u16 = 9222; var port: u16 = 9222;
var timeout: u16 = 3; var timeout: u16 = 3;
var gc_hints = false;
var tls_verify_host = true; var tls_verify_host = true;
while (args.next()) |opt| { while (args.next()) |opt| {
@@ -319,6 +339,11 @@ fn parseServeArgs(
continue; continue;
} }
if (std.mem.eql(u8, "--gc_hints", opt)) {
gc_hints = true;
continue;
}
log.err("Unknown option to serve command: '{s}'", .{opt}); log.err("Unknown option to serve command: '{s}'", .{opt});
return error.UnkownOption; return error.UnkownOption;
} }
@@ -327,6 +352,7 @@ fn parseServeArgs(
.host = host, .host = host,
.port = port, .port = port,
.timeout = timeout, .timeout = timeout,
.gc_hints = gc_hints,
.tls_verify_host = tls_verify_host, .tls_verify_host = tls_verify_host,
}; };
} }
@@ -388,3 +414,196 @@ fn logFn(
// default std log function. // default std log function.
std.log.defaultLog(level, scope, format, args); 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;
};
}

View File

@@ -1,420 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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("<body></body>");
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));
}
}

View File

@@ -1,224 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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;
};
}

View File

@@ -18,14 +18,10 @@
const std = @import("std"); 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 wpt = @import("wpt/run.zig");
const Suite = @import("wpt/testcase.zig").Suite;
const apiweb = @import("apiweb.zig"); const Platform = @import("runtime/js.zig").Platform;
const HTMLElem = @import("html/elements.zig"); const FileLoader = @import("wpt/fileloader.zig").FileLoader;
const wpt_dir = "tests/wpt"; const wpt_dir = "tests/wpt";
@@ -47,10 +43,10 @@ const Out = enum {
text, text,
}; };
pub const Types = jsruntime.reflect(apiweb.Interfaces); pub const std_options = std.Options{
pub const GlobalType = apiweb.GlobalType; // Set the log level to info
pub const UserContext = apiweb.UserContext; .log_level = .info,
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop); };
// TODO For now the WPT tests run is specific to WPT. // 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 // 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. // initialize VM JS lib.
const vm = jsruntime.VM.init(); const platform = Platform.init();
defer vm.deinit(); defer platform.deinit();
// prepare libraries to load on each test case. // prepare libraries to load on each test case.
var loader = FileLoader.init(alloc, wpt_dir); var loader = FileLoader.init(alloc, wpt_dir);
@@ -142,8 +138,9 @@ pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(alloc); var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit(); defer arena.deinit();
const res = wpt.run(&arena, wpt_dir, tc, &loader) catch |err| { var msg_out: ?[]const u8 = null;
const suite = try Suite.init(alloc, tc, false, @errorName(err)); 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); try results.append(suite);
if (out == .text) { if (out == .text) {
@@ -152,9 +149,8 @@ pub fn main() !void {
failures += 1; failures += 1;
continue; 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); try results.append(suite);
if (out == .json) { if (out == .json) {

View File

@@ -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);
}

View File

@@ -25,7 +25,7 @@ const Type = std.builtin.Type;
// ----- // -----
// Generate a flatten tagged Union from a Tuple // Generate a flatten tagged Union from a Tuple
pub fn Union(interfaces: anytype) type { pub fn Union(comptime interfaces: anytype) type {
// @setEvalBranchQuota(10000); // @setEvalBranchQuota(10000);
const tuple = Tuple(interfaces){}; const tuple = Tuple(interfaces){};
const fields = std.meta.fields(@TypeOf(tuple)); 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 // Flattens and depuplicates a list of nested tuples. For example
// input: {A, B, {C, B, D}, {A, E}} // input: {A, B, {C, B, D}, {A, E}}
// output {A, B, C, D, E} // output {A, B, C, D, E}
pub fn Tuple(args: anytype) type { pub fn Tuple(comptime args: anytype) type {
@setEvalBranchQuota(100000); @setEvalBranchQuota(100000);
const count = countInterfaces(args, 0); const count = countInterfaces(args, 0);
@@ -188,7 +188,7 @@ test "generate.Union" {
const value = Union(.{ Astruct, Bstruct, .{Cstruct} }); const value = Union(.{ Astruct, Bstruct, .{Cstruct} });
const ti = @typeInfo(value).@"union"; const ti = @typeInfo(value).@"union";
try std.testing.expectEqual(3, ti.fields.len); 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.expectEqualStrings(ti.fields[0].name, "Astruct");
try std.testing.expectEqual(Bstruct, ti.fields[1].type); try std.testing.expectEqual(Bstruct, ti.fields[1].type);
try std.testing.expectEqualStrings(ti.fields[1].name, "Bstruct"); try std.testing.expectEqualStrings(ti.fields[1].name, "Bstruct");

2303
src/runtime/js.zig Normal file

File diff suppressed because it is too large Load Diff

469
src/runtime/loop.zig Normal file
View File

@@ -0,0 +1,469 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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"),
};

View File

@@ -0,0 +1,260 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const MyList = struct {
items: []u8,
pub fn constructor(elem1: u8, elem2: u8, elem3: u8, state: State) MyList {
var items = state.arena.alloc(u8, 3) catch unreachable;
items[0] = elem1;
items[1] = elem2;
items[2] = elem3;
return .{ .items = items };
}
pub fn _first(self: *const MyList) u8 {
return self.items[0];
}
pub fn _symbol_iterator(self: *const MyList) IterableU8 {
return IterableU8.init(self.items);
}
};
const MyVariadic = struct {
member: u8,
pub fn constructor() MyVariadic {
return .{ .member = 0 };
}
pub fn _len(_: *const MyVariadic, variadic: []bool) u64 {
return @as(u64, variadic.len);
}
pub fn _first(_: *const MyVariadic, _: []const u8, variadic: []bool) bool {
return variadic[0];
}
pub fn _last(_: *const MyVariadic, variadic: []bool) bool {
return variadic[variadic.len - 1];
}
pub fn _empty(_: *const MyVariadic, _: []bool) bool {
return true;
}
pub fn _myListLen(_: *const MyVariadic, variadic: []*const MyList) u8 {
return @as(u8, @intCast(variadic.len));
}
pub fn _myListFirst(_: *const MyVariadic, variadic: []*const MyList) ?u8 {
if (variadic.len == 0) return null;
return variadic[0]._first();
}
};
const MyErrorUnion = struct {
pub fn constructor(is_err: bool) !MyErrorUnion {
if (is_err) return error.MyError;
return .{};
}
pub fn get_withoutError(_: *const MyErrorUnion) !u8 {
return 0;
}
pub fn get_withError(_: *const MyErrorUnion) !u8 {
return error.MyError;
}
pub fn set_withoutError(_: *const MyErrorUnion, _: bool) !void {}
pub fn set_withError(_: *const MyErrorUnion, _: bool) !void {
return error.MyError;
}
pub fn _funcWithoutError(_: *const MyErrorUnion) !void {}
pub fn _funcWithError(_: *const MyErrorUnion) !void {
return error.MyError;
}
};
pub const MyException = struct {
err: ErrorSet,
const errorNames = [_][]const u8{
"MyCustomError",
};
const errorMsgs = [_][]const u8{
"Some custom message.",
};
fn errorStrings(comptime i: usize) []const u8 {
return errorNames[0] ++ ": " ++ errorMsgs[i];
}
// interface definition
pub const ErrorSet = error{
MyCustomError,
};
pub fn init(_: Allocator, err: anyerror, _: []const u8) !MyException {
return .{ .err = @as(ErrorSet, @errorCast(err)) };
}
pub fn get_name(self: *const MyException) []const u8 {
return switch (self.err) {
ErrorSet.MyCustomError => errorNames[0],
};
}
pub fn get_message(self: *const MyException) []const u8 {
return switch (self.err) {
ErrorSet.MyCustomError => errorMsgs[0],
};
}
pub fn _toString(self: *const MyException) []const u8 {
return switch (self.err) {
ErrorSet.MyCustomError => errorStrings(0),
};
}
};
const MyTypeWithException = struct {
pub const Exception = MyException;
pub fn constructor() MyTypeWithException {
return .{};
}
pub fn _withoutError(_: *const MyTypeWithException) MyException.ErrorSet!void {}
pub fn _withError(_: *const MyTypeWithException) MyException.ErrorSet!void {
return MyException.ErrorSet.MyCustomError;
}
pub fn _superSetError(_: *const MyTypeWithException) !void {
return MyException.ErrorSet.MyCustomError;
}
pub fn _outOfMemory(_: *const MyTypeWithException) !void {
return error.OutOfMemory;
}
};
const 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" },
}, .{});
}

View File

@@ -0,0 +1,123 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
pub const Other = struct {
val: u8,
fn init(val: u8) Other {
return .{ .val = val };
}
pub fn _val(self: *const Other) u8 {
return self.val;
}
};
pub const OtherUnion = union(enum) {
Other: Other,
Bool: bool,
};
pub const MyObject = struct {
val: bool,
pub fn constructor(do_set: bool) MyObject {
return .{
.val = do_set,
};
}
pub fn named_get(_: *const MyObject, name: []const u8, has_value: *bool) ?OtherUnion {
if (std.mem.eql(u8, name, "a")) {
has_value.* = true;
return .{ .Other = .{ .val = 4 } };
}
if (std.mem.eql(u8, name, "c")) {
has_value.* = true;
return .{ .Bool = true };
}
has_value.* = false;
return null;
}
pub fn get_val(self: *const MyObject) bool {
return self.val;
}
pub fn set_val(self: *MyObject, val: bool) void {
self.val = val;
}
};
pub const MyAPI = struct {
pub fn constructor() MyAPI {
return .{};
}
pub fn _obj(_: *const MyAPI) !MyObject {
return MyObject.constructor(true);
}
};
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" },
}, .{});
}

View File

@@ -0,0 +1,176 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
// 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" },
}, .{});
}

104
src/runtime/testing.zig Normal file
View File

@@ -0,0 +1,104 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("js.zig");
const 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 {};

View File

@@ -25,14 +25,15 @@ const posix = std.posix;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const jsruntime = @import("jsruntime"); const IO = @import("runtime/loop.zig").IO;
const Completion = jsruntime.IO.Completion; const Completion = IO.Completion;
const AcceptError = jsruntime.IO.AcceptError; const AcceptError = IO.AcceptError;
const RecvError = jsruntime.IO.RecvError; const RecvError = IO.RecvError;
const SendError = jsruntime.IO.SendError; const SendError = IO.SendError;
const CloseError = jsruntime.IO.CloseError; const CloseError = IO.CloseError;
const CancelError = jsruntime.IO.CancelOneError; const CancelError = IO.CancelOneError;
const TimeoutError = jsruntime.IO.TimeoutError; const TimeoutError = IO.TimeoutError;
const Loop = @import("runtime/loop.zig").Loop;
const App = @import("app.zig").App; const App = @import("app.zig").App;
const CDP = @import("cdp/cdp.zig").CDP; const CDP = @import("cdp/cdp.zig").CDP;
@@ -51,7 +52,7 @@ const MAX_MESSAGE_SIZE = 256 * 1024 + 14;
const Server = struct { const Server = struct {
app: *App, app: *App,
allocator: Allocator, allocator: Allocator,
loop: *jsruntime.Loop, loop: *Loop,
// internal fields // internal fields
listener: posix.socket_t, listener: posix.socket_t,
@@ -453,7 +454,7 @@ pub const Client = struct {
}; };
self.mode = .websocket; 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); return self.send(arena, response);
} }
@@ -1023,10 +1024,6 @@ pub fn run(
try posix.bind(listener, &address.any, address.getOsSockLen()); try posix.bind(listener, &address.any, address.getOsSockLen());
try posix.listen(listener, 1); try posix.listen(listener, 1);
// create v8 vm
const vm = jsruntime.VM.init();
defer vm.deinit();
var loop = app.loop; var loop = app.loop;
const allocator = app.allocator; const allocator = app.allocator;
const json_version_response = try buildJSONVersionResponse(allocator, address); const json_version_response = try buildJSONVersionResponse(allocator, address);
@@ -1451,7 +1448,7 @@ const MockCDP = struct {
allocator: Allocator = testing.allocator, allocator: Allocator = testing.allocator,
fn init(_: Allocator, client: anytype, loop: *jsruntime.Loop) MockCDP { fn init(_: Allocator, client: anytype, loop: *Loop) MockCDP {
_ = loop; _ = loop;
_ = client; _ = client;
return .{}; return .{};

View File

@@ -26,10 +26,6 @@ const BORDER = "=" ** 80;
// use in custom panic handler // use in custom panic handler
var current_test: ?[]const u8 = null; 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{ pub const std_options = std.Options{
.log_level = .warn, .log_level = .warn,
@@ -38,6 +34,9 @@ pub const std_options = std.Options{
.side_channels_mitigations = .none, .side_channels_mitigations = .none,
}; };
pub var js_runner_duration: usize = 0;
pub var tracking_allocator = TrackingAllocator.init(std.testing.allocator);
pub fn main() !void { pub fn main() !void {
var mem: [8192]u8 = undefined; var mem: [8192]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&mem); var fba = std.heap.FixedBufferAllocator.init(&mem);
@@ -50,6 +49,19 @@ pub fn main() !void {
var slowest = SlowTracker.init(allocator, 5); var slowest = SlowTracker.init(allocator, 5);
defer slowest.deinit(); 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 pass: usize = 0;
var fail: usize = 0; var fail: usize = 0;
var skip: usize = 0; var skip: usize = 0;
@@ -159,6 +171,38 @@ pub fn main() !void {
printer.fmt("\n", .{}); printer.fmt("\n", .{});
try slowest.display(printer); try slowest.display(printer);
printer.fmt("\n", .{}); 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); 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 { fn isTeardown(t: std.builtin.TestFn) bool {
return std.mem.endsWith(u8, t.name, "tests:afterAll"); 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;
}
};

View File

@@ -17,15 +17,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const parser = @import("netsurf");
pub const allocator = std.testing.allocator; pub const allocator = std.testing.allocator;
pub const expectError = std.testing.expectError; pub const expectError = std.testing.expectError;
pub const expectString = std.testing.expectEqualStrings; pub const expectString = std.testing.expectEqualStrings;
pub const expectEqualSlices = std.testing.expectEqualSlices; pub const expectEqualSlices = std.testing.expectEqualSlices;
const App = @import("app.zig").App; 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 // Merged std.testing.expectEqual and std.testing.expectString
// can be useful when testing fields of an anytype an you don't know // 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 { 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); const node_list = try css.querySelectorAll(self.arena.allocator(), self.asNode(), selector);
return node_list.nodes.items; return node_list.nodes.items;
} }
pub fn querySelector(self: *Document, selector: []const u8) !?*parser.Node { 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); 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 =
\\ <div id="content">
\\ <a id="link" href="foo" class="ok">OK</a>
\\ <p id="para-empty" class="ok empty">
\\ <span id="para-empty-child"></span>
\\ </p>
\\ <p id="para"> And</p>
\\ <!--comment-->
\\ </div>
\\
,
};
pub fn jsRunner(alloc: Allocator, opts: RunnerOpts) !*JsRunner {
return JsRunner.init(alloc, opts);
}

View File

@@ -2,7 +2,7 @@ const std = @import("std");
const Uri = std.Uri; const Uri = std.Uri;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const WebApiURL = @import("url/url.zig").URL; const WebApiURL = @import("browser/url/url.zig").URL;
pub const URL = struct { pub const URL = struct {
uri: Uri, uri: Uri,

View File

@@ -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,
};

View File

@@ -18,112 +18,55 @@
const std = @import("std"); const std = @import("std");
const fspath = std.fs.path; const fspath = std.fs.path;
const Allocator = std.mem.Allocator;
const Env = @import("../browser/env.zig").Env;
const FileLoader = @import("fileloader.zig").FileLoader; const FileLoader = @import("fileloader.zig").FileLoader;
const Window = @import("../browser/html/window.zig").Window;
const parser = @import("netsurf"); const parser = @import("../browser/netsurf.zig");
const polyfill = @import("../browser/polyfill/polyfill.zig");
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");
// runWPT parses the given HTML file, starts a js env and run the first script // runWPT parses the given HTML file, starts a js env and run the first script
// tags containing javascript sources. // tags containing javascript sources.
// It loads first the js libs files. // It loads first the js libs files.
pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const u8, loader: *FileLoader) !Res { pub fn run(arena: Allocator, comptime dir: []const u8, f: []const u8, loader: *FileLoader, err_msg: *?[]const u8) ![]const u8 {
const alloc = arena.allocator();
try parser.init();
defer parser.deinit();
// document // document
const file = try std.fs.cwd().openFile(f, .{}); const html = blk: {
defer file.close(); const file = try std.fs.cwd().openFile(f, .{});
defer file.close();
const html_doc = try parser.documentHTMLParse(file.reader(), "UTF-8"); break :blk try file.readToEndAlloc(arena, 128 * 1024);
};
const dirname = fspath.dirname(f[dir.len..]) orelse unreachable; const dirname = fspath.dirname(f[dir.len..]) orelse unreachable;
// create JS env var runner = try @import("../testing.zig").jsRunner(arena, .{
var loop = try Loop.init(alloc); .html = html,
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,
}); });
defer js_env.deinit(); defer runner.deinit();
try polyfill.load(arena, runner.executor);
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);
// display console logs // display console logs
defer { defer {
const res = evalJS(&js_env, alloc, "console.join('\\n');", "console") catch unreachable; const res = runner.eval("console.join('\\n');", "console", err_msg) catch unreachable;
defer res.deinit(alloc); const log = res.toString(arena) catch unreachable;
if (log.len > 0) {
if (res.msg != null and res.msg.?.len > 0) { std.debug.print("-- CONSOLE LOG\n{s}\n--\n", .{log});
std.debug.print("-- CONSOLE LOG\n{s}\n--\n", .{res.msg.?});
} }
} }
// setup global env vars. try runner.exec(
var window = Window.create(null, null); \\ console = [];
try window.replaceDocument(html_doc); \\ console.log = function () {
window.setStorageShelf(&storageShelf); \\ console.push(...arguments);
try js_env.bindGlobal(&window); \\ };
\\ console.debug = function () {
\\ console.push("debug", ...arguments);
\\ };
, "init", err_msg);
const init = // loop over the scripts.
\\console = []; const doc = parser.documentHTMLToDocument(runner.state.document.?);
\\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);
const scripts = try parser.documentGetElementsByTagName(doc, "script"); const scripts = try parser.documentGetElementsByTagName(doc, "script");
const slen = try parser.nodeListLength(scripts); const slen = try parser.nodeListLength(scripts);
for (0..slen) |i| { for (0..slen) |i| {
@@ -134,19 +77,14 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
var path = src; var path = src;
if (!std.mem.startsWith(u8, src, "/")) { if (!std.mem.startsWith(u8, src, "/")) {
// no need to free path, thanks to the arena. // no need to free path, thanks to the arena.
path = try fspath.join(alloc, &.{ "/", dirname, path }); path = try fspath.join(arena, &.{ "/", dirname, path });
} }
try runner.exec(try loader.get(path), src, err_msg);
res = try evalJS(&js_env, alloc, try loader.get(path), src);
if (!res.ok) return res;
res.deinit(alloc);
} }
// If the script as a source text, execute it. // If the script as a source text, execute it.
const src = try parser.nodeTextContent(s) orelse continue; const src = try parser.nodeTextContent(s) orelse continue;
res = try evalJS(&js_env, alloc, src, ""); try runner.exec(src, null, err_msg);
if (!res.ok) return res;
res.deinit(alloc);
} }
// Mark tests as ready to run. // 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.eventInit(loadevt, "load", .{});
_ = try parser.eventTargetDispatchEvent( _ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, &window), parser.toEventTarget(@TypeOf(runner.window), &runner.window),
loadevt, loadevt,
); );
// wait for all async executions // wait for all async executions
var try_catch: jsruntime.TryCatch = undefined; {
try_catch.init(&js_env); var try_catch: Env.TryCatch = undefined;
defer try_catch.deinit(); try_catch.init(runner.executor);
js_env.wait() catch { defer try_catch.deinit();
return .{ runner.loop.run() catch |err| {
.ok = false, if (try try_catch.err(arena)) |msg| {
.msg = try try_catch.err(alloc, &js_env), err_msg.* = msg;
}
return err;
}; };
}; }
// Check the final test status. // Check the final test status.
res = try evalJS(&js_env, alloc, "report.status;", "teststatus"); try runner.exec("report.status", "teststatus", err_msg);
if (!res.ok) return res;
res.deinit(alloc);
// return the detailed result. // return the detailed result.
return try evalJS(&js_env, alloc, "report.log", "teststatus"); const res = try runner.eval("report.log", "report", err_msg);
} return res.toString(arena);
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),
};
} }
// browse the path to find the tests list. // browse the path to find the tests list.

Submodule vendor/zig-js-runtime deleted from 9b87782f1e