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

View File

@@ -31,7 +31,7 @@ jobs:
arch: ${{env.ARCH}}
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -63,7 +63,7 @@ jobs:
arch: ${{env.ARCH}}
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -95,7 +95,7 @@ jobs:
arch: ${{env.ARCH}}
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -127,7 +127,7 @@ jobs:
arch: ${{env.ARCH}}
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}

View File

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

View File

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

View File

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

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ zig-out
/vendor/netsurf/out
/vendor/libiconv/
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"]
path = vendor/netsurf/libwapcaplet
url = https://github.com/lightpanda-io/libwapcaplet.git/

View File

@@ -61,8 +61,8 @@ RUN make install-libiconv && \
# download and install v8
RUN curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
mkdir -p vendor/zig-js-runtime/vendor/v8/${ARCH}-linux/release && \
mv libc_v8.a vendor/zig-js-runtime/vendor/v8/${ARCH}-linux/release/libc_v8.a
mkdir -p v8/build/${ARCH}-linux/release/ninja/obj/zig/ && \
mv libc_v8.a v8/build/${ARCH}-linux/release/ninja/obj/zig/libc_v8.a
# build release
RUN make build

View File

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

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
[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html).
**zig-js-runtime**
**v8**
Our own Zig/Javascript runtime, which includes the v8 Javascript engine.
This build task is very long and cpu consuming, as you will build v8 from sources.
First, get the tools necessary for building V8, as well as the V8 source code:
```
make install-zig-js-runtime
make get-v8
```
For dev env, use `make install-zig-js-runtime-dev`.
Next, build v8. This build task is very long and cpu consuming, as you will build v8 from sources.
```
make build-v8
```
For dev env, use `make build-v8-dev`.
## Test

317
build.zig
View File

@@ -17,16 +17,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const jsruntime_path = "vendor/zig-js-runtime/";
const jsruntime = @import("vendor/zig-js-runtime/build.zig");
const jsruntime_pkgs = jsruntime.packages(jsruntime_path);
/// Do not rename this constant. It is scanned by some scripts to determine
/// which zig version to install.
const recommended_zig_version = jsruntime.recommended_zig_version;
const recommended_zig_version = "0.14.0";
pub fn build(b: *std.Build) !void {
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
@@ -42,193 +37,190 @@ pub fn build(b: *std.Build) !void {
},
}
var opts = b.addOptions();
opts.addOption(
[]const u8,
"git_commit",
b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
);
const target = b.standardTargetOptions(.{});
const mode = b.standardOptimizeOption(.{});
const optimize = b.standardOptimizeOption(.{});
const options = jsruntime.buildOptions(b);
// browser
// -------
// compile and install
const exe = b.addExecutable(.{
.name = "lightpanda",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = mode,
});
try common(b, exe, options);
{
var opt = b.addOptions();
opt.addOption(
[]const u8,
"git_commit",
b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
);
exe.root_module.addImport("build_info", opt.createModule());
}
b.installArtifact(exe);
// browser
// -------
// run
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
// compile and install
const exe = b.addExecutable(.{
.name = "lightpanda",
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/main.zig"),
});
try common(b, opts, exe);
b.installArtifact(exe);
// run
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
}
// step
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
// step
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
// shell
// -----
// compile and install
const shell = b.addExecutable(.{
.name = "lightpanda-shell",
.root_source_file = b.path("src/main_shell.zig"),
.target = target,
.optimize = mode,
});
try common(b, shell, options);
try jsruntime_pkgs.add_shell(shell);
// run
const shell_cmd = b.addRunArtifact(shell);
if (b.args) |args| {
shell_cmd.addArgs(args);
{
// get v8
// -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
const get_v8 = b.addRunArtifact(v8.artifact("get-v8"));
const get_step = b.step("get-v8", "Get v8");
get_step.dependOn(&get_v8.step);
}
// step
const shell_step = b.step("shell", "Run JS shell");
shell_step.dependOn(&shell_cmd.step);
// test
// ----
// compile
const tests = b.addTest(.{
.root_source_file = b.path("src/main_tests.zig"),
.test_runner = .{ .path = b.path("src/main_tests.zig"), .mode = .simple },
.target = target,
.optimize = mode,
});
try common(b, tests, options);
// add jsruntime pretty deps
tests.root_module.addAnonymousImport("pretty", .{
.root_source_file = b.path("vendor/zig-js-runtime/src/pretty.zig"),
});
const run_tests = b.addRunArtifact(tests);
if (b.args) |args| {
run_tests.addArgs(args);
{
// build v8
// -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
const build_v8 = b.addRunArtifact(v8.artifact("build-v8"));
const build_step = b.step("build-v8", "Build v8");
build_step.dependOn(&build_v8.step);
}
// step
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_tests.step);
{
// tests
// ----
// unittest
// ----
// compile
const tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
.target = target,
.optimize = optimize,
});
try common(b, opts, tests);
// compile
const unit_tests = b.addTest(.{
.root_source_file = b.path("src/main_unit_tests.zig"),
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
.target = target,
.optimize = mode,
});
try common(b, unit_tests, options);
const run_tests = b.addRunArtifact(tests);
if (b.args) |args| {
run_tests.addArgs(args);
}
const run_unit_tests = b.addRunArtifact(unit_tests);
if (b.args) |args| {
run_unit_tests.addArgs(args);
// step
const tests_step = b.step("test", "Run unit tests");
tests_step.dependOn(&run_tests.step);
}
// step
const unit_test_step = b.step("unittest", "Run unit tests");
unit_test_step.dependOn(&run_unit_tests.step);
{
// wpt
// -----
// wpt
// -----
// compile and install
const wpt = b.addExecutable(.{
.name = "lightpanda-wpt",
.root_source_file = b.path("src/main_wpt.zig"),
.target = target,
.optimize = optimize,
});
try common(b, opts, wpt);
// compile and install
const wpt = b.addExecutable(.{
.name = "lightpanda-wpt",
.root_source_file = b.path("src/main_wpt.zig"),
.target = target,
.optimize = mode,
});
try common(b, wpt, options);
// run
const wpt_cmd = b.addRunArtifact(wpt);
if (b.args) |args| {
wpt_cmd.addArgs(args);
// run
const wpt_cmd = b.addRunArtifact(wpt);
if (b.args) |args| {
wpt_cmd.addArgs(args);
}
// step
const wpt_step = b.step("wpt", "WPT tests");
wpt_step.dependOn(&wpt_cmd.step);
}
// step
const wpt_step = b.step("wpt", "WPT tests");
wpt_step.dependOn(&wpt_cmd.step);
}
fn common(
b: *std.Build,
step: *std.Build.Step.Compile,
options: jsruntime.Options,
) !void {
const target = step.root_module.resolved_target.?;
const optimize = step.root_module.optimize.?;
fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Compile) !void {
const mod = step.root_module;
const target = mod.resolved_target.?;
const optimize = mod.optimize.?;
const dep_opts = .{ .target = target, .optimize = optimize };
const jsruntimemod = try jsruntime_pkgs.module(
b,
options,
step.root_module.optimize.?,
target,
try moduleNetSurf(b, step, target);
mod.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
mod.addImport("tigerbeetle-io", b.dependency("tigerbeetle_io", .{}).module("tigerbeetle_io"));
{
// v8
const v8_opts = b.addOptions();
v8_opts.addOption(bool, "inspector_subtype", false);
const v8_mod = b.dependency("v8", dep_opts).module("v8");
v8_mod.addOptions("default_exports", v8_opts);
mod.addImport("v8", v8_mod);
}
const mode_str: []const u8 = if (mod.optimize.? == .Debug) "debug" else "release";
// FIXME: we are tied to native v8 builds, currently:
// - aarch64-macos
// - x86_64-linux
const os = target.result.os.tag;
const arch = target.result.cpu.arch;
switch (os) {
.macos => {},
.linux => {
// TODO: why do we need it? It should be linked already when we built v8
mod.link_libcpp = true;
},
else => return error.OsNotSupported,
}
const lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/build/{s}-{s}/{s}/ninja/obj/zig/libc_v8.a",
.{ @tagName(arch), @tagName(os), mode_str },
);
step.root_module.addImport("jsruntime", jsruntimemod);
const netsurf = try moduleNetSurf(b, target);
netsurf.addImport("jsruntime", jsruntimemod);
step.root_module.addImport("netsurf", netsurf);
step.root_module.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
mod.addObjectFile(mod.owner.path(lib_path));
mod.addImport("build_info", opts.createModule());
}
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
const mod = b.addModule("netsurf", .{
.root_source_file = b.path("src/netsurf/netsurf.zig"),
.target = target,
});
fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build.ResolvedTarget) !void {
const os = target.result.os.tag;
const arch = target.result.cpu.arch;
// iconv
const libiconv_lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
b.allocator,
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
.{ @tagName(os), @tagName(arch) },
);
const libiconv_include_path = try std.fmt.allocPrint(
mod.owner.allocator,
b.allocator,
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
.{ @tagName(os), @tagName(arch) },
);
mod.addObjectFile(b.path(libiconv_lib_path));
mod.addIncludePath(b.path(libiconv_include_path));
step.addObjectFile(b.path(libiconv_lib_path));
step.addIncludePath(b.path(libiconv_include_path));
// mimalloc
mod.addImport("mimalloc", (try moduleMimalloc(b, target)));
{
// mimalloc
const mimalloc = "vendor/mimalloc";
const lib_path = try std.fmt.allocPrint(
b.allocator,
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
.{ @tagName(os), @tagName(arch) },
);
step.addObjectFile(b.path(lib_path));
step.addIncludePath(b.path(mimalloc ++ "/include"));
}
// netsurf libs
const ns = "vendor/netsurf";
const ns_include_path = try std.fmt.allocPrint(
mod.owner.allocator,
b.allocator,
ns ++ "/out/{s}-{s}/include",
.{ @tagName(os), @tagName(arch) },
);
mod.addIncludePath(b.path(ns_include_path));
step.addIncludePath(b.path(ns_include_path));
const libs: [4][]const u8 = .{
"libdom",
@@ -238,34 +230,11 @@ fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Mo
};
inline for (libs) |lib| {
const ns_lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
b.allocator,
ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a",
.{ @tagName(os), @tagName(arch) },
);
mod.addObjectFile(b.path(ns_lib_path));
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
step.addObjectFile(b.path(ns_lib_path));
step.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
}
return mod;
}
fn moduleMimalloc(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
const mod = b.addModule("mimalloc", .{
.root_source_file = b.path("src/mimalloc/mimalloc.zig"),
.target = target,
});
const os = target.result.os.tag;
const arch = target.result.cpu.arch;
const mimalloc = "vendor/mimalloc";
const lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
.{ @tagName(os), @tagName(arch) },
);
mod.addObjectFile(b.path(lib_path));
mod.addIncludePath(b.path(mimalloc ++ "/include"));
return mod;
}

View File

@@ -6,7 +6,17 @@
.dependencies = .{
.tls = .{
.url = "https://github.com/ianic/tls.zig/archive/b29a8b45fc59fc2d202769c4f54509bb9e17d0a2.tar.gz",
.hash = "1220e6fd39920dd6e28b2bc06688787a39430f8856f0597cd77c44ca868c6c54fb86",
.hash = "tls-0.1.0-ER2e0uAxBQDm_TmSDdbiiyvAZoh4ejlDD4hW8Fl813xE",
},
.tigerbeetle_io = .{
.url = "https://github.com/lightpanda-io/tigerbeetle-io/archive/61d9652f1a957b7f4db723ea6aa0ce9635e840ce.tar.gz",
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
},
.v8 = .{
.url = "https://github.com/karlseguin/zig-v8-fork/archive/e5f1c0c9f1ed147617427f22cdaf11df4ab60b79.tar.gz",
.hash = "v8-0.0.0-xddH61vYIACI2pT1t-dUbXm18cHAKy-KWT_Qft4sBwam",
},
//.v8 = .{ .path = "../zig-v8-fork" },
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
},
}

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

View File

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

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 parser = @import("netsurf");
const parser = @import("../netsurf.zig");
// Node implementation with Netsurf Libdom C lib.
pub const Node = struct {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("../netsurf.zig");
const SessionState = @import("../env.zig").SessionState;
const Node = @import("node.zig").Node;
const UserContext = @import("../user_context.zig").UserContext;
// WEB IDL https://dom.spec.whatwg.org/#documentfragment
pub const DocumentFragment = struct {
pub const Self = parser.DocumentFragment;
pub const prototype = *Node;
pub const mem_guarantied = true;
pub fn constructor(userctx: UserContext) !*parser.DocumentFragment {
pub fn constructor(state: *const SessionState) !*parser.DocumentFragment {
return parser.documentCreateDocumentFragment(
parser.documentHTMLToDocument(userctx.document),
parser.documentHTMLToDocument(state.document.?),
);
}
};
// Tests
// -----
const testing = @import("../../testing.zig");
test "Browser.DOM.DocumentFragment" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var constructor = [_]Case{
.{ .src = "const dc = new DocumentFragment()", .ex = "undefined" },
.{ .src = "dc.constructor.name", .ex = "DocumentFragment" },
};
try checkCases(js_env, &constructor);
try runner.testCases(&.{
.{ "const dc = new DocumentFragment()", "undefined" },
.{ "dc.constructor.name", "DocumentFragment" },
}, .{});
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,9 +27,10 @@ const c = @cImport({
@cInclude("events/mouse_event.h");
});
const mimalloc = @import("mimalloc");
const mimalloc = @import("mimalloc.zig");
const Callback = @import("jsruntime").Callback;
const Callback = @import("env.zig").Callback;
const SessionState = @import("env.zig").SessionState;
// init initializes netsurf lib.
// init starts a mimalloc heap arena for the netsurf session. The caller must
@@ -617,7 +618,7 @@ pub fn eventTargetHasListener(
defer c.dom_event_listener_unref(listener);
const ehd = EventHandlerDataInternal.fromListener(listener);
if (ehd) |d| {
if (cbk_id == d.data.cbk.id()) {
if (cbk_id == d.data.cbk.id) {
return lst;
}
}
@@ -669,7 +670,7 @@ pub const EventHandlerData = struct {
// deinitFunc implements the data deinitialization.
deinitFunc: ?DeinitFunc = null,
pub const DeinitFunc = *const fn (data: ?*anyopaque, alloc: std.mem.Allocator) void;
pub const DeinitFunc = *const fn (data: ?*anyopaque, allocator: std.mem.Allocator) void;
};
// EventHandlerDataInternal groups the EventHandlerFunc and the EventHandlerData.
@@ -687,8 +688,9 @@ const EventHandlerDataInternal = struct {
}
fn deinit(self: *EventHandlerDataInternal, alloc: std.mem.Allocator) void {
if (self.data.deinitFunc) |d| d(self.data.data, alloc);
self.data.cbk.deinit(alloc);
if (self.data.deinitFunc) |d| {
d(self.data.data, alloc);
}
alloc.destroy(self);
}
@@ -723,7 +725,7 @@ pub fn eventTargetAddEventListener(
// When a function is used as an event handler, its this parameter is bound
// to the DOM element on which the listener is placed.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#this_in_dom_event_handlers
try ehd.data.cbk.setThisArg(et);
try ehd.data.cbk.setThis(et);
const ctx = @as(*anyopaque, @ptrCast(ehd));
var listener: ?*EventListener = undefined;

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -72,13 +72,16 @@ pub fn CDPT(comptime TypeProvider: type) type {
pub const Browser = TypeProvider.Browser;
pub const Session = TypeProvider.Session;
pub fn init(app: *App, client: TypeProvider.Client) Self {
pub fn init(app: *App, client: TypeProvider.Client) !Self {
const allocator = app.allocator;
const browser = try Browser.init(app);
errdefer browser.deinit();
return .{
.client = client,
.browser = browser,
.allocator = allocator,
.browser_context = null,
.browser = Browser.init(app),
.message_arena = std.heap.ArenaAllocator.init(allocator),
.browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator),
};

View File

@@ -17,10 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("netsurf");
const Node = @import("../Node.zig");
const css = @import("../../dom/css.zig");
const dom_node = @import("../../dom/node.zig");
const css = @import("../../browser/dom/css.zig");
const parser = @import("../../browser/netsurf.zig");
const dom_node = @import("../../browser/dom/node.zig");
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
@@ -134,17 +134,20 @@ fn resolveNode(cmd: anytype) !void {
// node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
// So we use the Node.Union when retrieve the value from the environment
const jsValue = try bc.session.env.findOrAddValue(try dom_node.Node.toInterface(node._node));
const remoteObject = try bc.session.inspector.getRemoteObject(&bc.session.env, jsValue, params.objectGroup orelse "");
defer remoteObject.deinit();
const remote_object = try bc.session.inspector.getRemoteObject(
bc.session.executor,
params.objectGroup orelse "",
try dom_node.Node.toInterface(node._node),
);
defer remote_object.deinit();
const arena = cmd.arena;
return cmd.sendResult(.{ .object = .{
.type = try remoteObject.getType(arena),
.subtype = try remoteObject.getSubtype(arena),
.className = try remoteObject.getClassName(arena),
.description = try remoteObject.getDescription(arena),
.objectId = try remoteObject.getObjectId(arena),
.type = try remote_object.getType(arena),
.subtype = try remote_object.getSubtype(arena),
.className = try remote_object.getClassName(arena),
.description = try remote_object.getDescription(arena),
.objectId = try remote_object.getObjectId(arena),
} }, .{});
}

View File

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

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

View File

@@ -20,25 +20,19 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const jsruntime = @import("jsruntime");
const App = @import("app.zig").App;
const Browser = @import("browser/browser.zig").Browser;
const server = @import("server.zig");
const App = @import("app.zig").App;
const Platform = @import("runtime/js.zig").Platform;
const Browser = @import("browser/browser.zig").Browser;
const parser = @import("netsurf");
const apiweb = @import("apiweb.zig");
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
const parser = @import("browser/netsurf.zig");
const version = @import("build_info").git_commit;
const log = std.log.scoped(.cli);
pub const std_options = std.Options{
// Set the log level to info
.log_level = .debug,
.log_level = .info,
// Define logFn to override the std implementation
.logFn = logFn,
@@ -60,23 +54,34 @@ pub fn main() !void {
const args = try parseArgs(args_arena.allocator());
switch (args.mode) {
.help => args.printUsageAndExit(args.mode.help),
.help => {
args.printUsageAndExit(args.mode.help);
return std.process.cleanExit();
},
.version => {
std.debug.print("{s}\n", .{version});
return std.process.cleanExit();
},
else => {},
}
const platform = Platform.init();
defer platform.deinit();
var app = try App.init(alloc, .{
.run_mode = args.mode,
.gc_hints = args.gcHints(),
.tls_verify_host = args.tlsVerifyHost(),
});
defer app.deinit();
app.telemetry.record(.{ .run = {} });
switch (args.mode) {
.serve => |opts| {
const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| {
log.err("address (host:port) {any}\n", .{err});
return args.printUsageAndExit(false);
};
var app = try App.init(alloc, .{
.run_mode = args.mode,
.tls_verify_host = opts.tls_verify_host,
});
defer app.deinit();
app.telemetry.record(.{ .run = {} });
const timeout = std.time.ns_per_s * @as(u64, opts.timeout);
server.run(app, address, timeout) catch |err| {
@@ -88,19 +93,8 @@ pub fn main() !void {
log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump });
const url = try @import("url.zig").URL.parse(opts.url, null);
var app = try App.init(alloc, .{
.run_mode = args.mode,
.tls_verify_host = opts.tls_verify_host,
});
defer app.deinit();
app.telemetry.record(.{ .run = {} });
// vm
const vm = jsruntime.VM.init();
defer vm.deinit();
// browser
var browser = Browser.init(app);
var browser = try Browser.init(app);
defer browser.deinit();
var session = try browser.newSession({});
@@ -126,6 +120,7 @@ pub fn main() !void {
try page.dump(std.io.getStdOut());
}
},
else => unreachable,
}
}
@@ -133,6 +128,21 @@ const Command = struct {
mode: Mode,
exec_name: []const u8,
fn gcHints(self: *const Command) bool {
return switch (self.mode) {
.serve => |opts| opts.gc_hints,
else => false,
};
}
fn tlsVerifyHost(self: *const Command) bool {
return switch (self.mode) {
.serve => |opts| opts.tls_verify_host,
.fetch => |opts| opts.tls_verify_host,
else => true,
};
}
const Mode = union(App.RunMode) {
help: bool, // false when being printed because of an error
fetch: Fetch,
@@ -144,6 +154,7 @@ const Command = struct {
host: []const u8,
port: u16,
timeout: u16,
gc_hints: bool,
tls_verify_host: bool,
};
@@ -187,6 +198,9 @@ const Command = struct {
\\--timeout Inactivity timeout in seconds before disconnecting clients
\\ Defaults to 3 (seconds)
\\
\\--gc_hints Encourage V8 to cleanup garbage for each new browser context.
\\ Defaults to false
\\
\\--insecure_disable_tls_host_verification
\\ Disables host verification on all HTTP requests.
\\ This is an advanced option which should only be
@@ -266,6 +280,11 @@ fn inferMode(opt: []const u8) ?App.RunMode {
if (std.mem.eql(u8, opt, "--timeout")) {
return .serve;
}
if (std.mem.eql(u8, opt, "--gc_hints")) {
return .serve;
}
return null;
}
@@ -276,6 +295,7 @@ fn parseServeArgs(
var host: []const u8 = "127.0.0.1";
var port: u16 = 9222;
var timeout: u16 = 3;
var gc_hints = false;
var tls_verify_host = true;
while (args.next()) |opt| {
@@ -319,6 +339,11 @@ fn parseServeArgs(
continue;
}
if (std.mem.eql(u8, "--gc_hints", opt)) {
gc_hints = true;
continue;
}
log.err("Unknown option to serve command: '{s}'", .{opt});
return error.UnkownOption;
}
@@ -327,6 +352,7 @@ fn parseServeArgs(
.host = host,
.port = port,
.timeout = timeout,
.gc_hints = gc_hints,
.tls_verify_host = tls_verify_host,
};
}
@@ -388,3 +414,196 @@ fn logFn(
// default std log function.
std.log.defaultLog(level, scope, format, args);
}
test {
std.testing.refAllDecls(@This());
}
var test_wg: std.Thread.WaitGroup = .{};
test "tests:beforeAll" {
try parser.init();
test_wg.startMany(3);
_ = Platform.init();
{
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
const thread = try std.Thread.spawn(.{}, serveHTTP, .{address});
thread.detach();
}
{
const address = try std.net.Address.parseIp("127.0.0.1", 9581);
const thread = try std.Thread.spawn(.{}, serveHTTPS, .{address});
thread.detach();
}
{
const address = try std.net.Address.parseIp("127.0.0.1", 9583);
const thread = try std.Thread.spawn(.{}, serveCDP, .{address});
thread.detach();
}
// need to wait for the servers to be listening, else tests will fail because
// they aren't able to connect.
test_wg.wait();
}
test "tests:afterAll" {
parser.deinit();
}
fn serveHTTP(address: std.net.Address) !void {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var listener = try address.listen(.{ .reuse_address = true });
defer listener.deinit();
test_wg.finish();
var read_buffer: [1024]u8 = undefined;
ACCEPT: while (true) {
defer _ = arena.reset(.{ .free_all = {} });
const aa = arena.allocator();
var conn = try listener.accept();
defer conn.stream.close();
var http_server = std.http.Server.init(conn, &read_buffer);
while (http_server.state == .ready) {
var request = http_server.receiveHead() catch |err| switch (err) {
error.HttpConnectionClosing => continue :ACCEPT,
else => {
std.debug.print("Test HTTP Server error: {}\n", .{err});
return err;
},
};
const path = request.head.target;
if (std.mem.eql(u8, path, "/loader")) {
try request.respond("Hello!", .{});
} else if (std.mem.eql(u8, path, "/http_client/simple")) {
try request.respond("", .{});
} else if (std.mem.eql(u8, path, "/http_client/redirect")) {
try request.respond("", .{
.status = .moved_permanently,
.extra_headers = &.{.{ .name = "LOCATION", .value = "../http_client/echo" }},
});
} else if (std.mem.eql(u8, path, "/http_client/redirect/secure")) {
try request.respond("", .{
.status = .moved_permanently,
.extra_headers = &.{.{ .name = "LOCATION", .value = "https://127.0.0.1:9581/http_client/body" }},
});
} else if (std.mem.eql(u8, path, "/http_client/echo")) {
var headers: std.ArrayListUnmanaged(std.http.Header) = .{};
var it = request.iterateHeaders();
while (it.next()) |hdr| {
try headers.append(aa, .{
.name = try std.fmt.allocPrint(aa, "_{s}", .{hdr.name}),
.value = hdr.value,
});
}
try request.respond("over 9000!", .{
.status = .created,
.extra_headers = headers.items,
});
}
}
}
}
// This is a lot of work for testing TLS, but the TLS (async) code is complicated
// This "server" is written specifically to test the client. It assumes the client
// isn't a jerk.
fn serveHTTPS(address: std.net.Address) !void {
const tls = @import("tls");
var listener = try address.listen(.{ .reuse_address = true });
defer listener.deinit();
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
test_wg.finish();
var seed: u64 = undefined;
std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable;
var r = std.Random.DefaultPrng.init(seed);
const rand = r.random();
var read_buffer: [1024]u8 = undefined;
while (true) {
// defer _ = arena.reset(.{ .retain_with_limit = 1024 });
// const aa = arena.allocator();
const stream = blk: {
const conn = try listener.accept();
break :blk conn.stream;
};
defer stream.close();
var conn = try tls.server(stream, .{ .auth = null });
defer conn.close() catch {};
var pos: usize = 0;
while (true) {
const n = try conn.read(read_buffer[pos..]);
if (n == 0) {
break;
}
pos += n;
const header_end = std.mem.indexOf(u8, read_buffer[0..pos], "\r\n\r\n") orelse {
continue;
};
var it = std.mem.splitScalar(u8, read_buffer[0..header_end], ' ');
_ = it.next() orelse unreachable; // method
const path = it.next() orelse unreachable;
var fragment = false;
var response: []const u8 = undefined;
if (std.mem.eql(u8, path, "/http_client/simple")) {
fragment = true;
response = "HTTP/1.1 200 \r\nContent-Length: 0\r\n\r\n";
} else if (std.mem.eql(u8, path, "/http_client/body")) {
fragment = true;
response = "HTTP/1.1 201 CREATED\r\nContent-Length: 20\r\n Another : HEaDer \r\n\r\n1234567890abcdefhijk";
} else if (std.mem.eql(u8, path, "/http_client/redirect/insecure")) {
fragment = true;
response = "HTTP/1.1 307 GOTO\r\nLocation: http://127.0.0.1:9582/http_client/redirect\r\n\r\n";
} else if (std.mem.eql(u8, path, "/xhr")) {
response = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 100\r\n\r\n" ++ ("1234567890" ** 10);
} else if (std.mem.eql(u8, path, "/xhr/json")) {
response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 18\r\n\r\n{\"over\":\"9000!!!\"}";
} else {
// should not have an unknown path
unreachable;
}
var unsent = response;
while (unsent.len > 0) {
const to_send = if (fragment) rand.intRangeAtMost(usize, 1, unsent.len) else unsent.len;
const sent = try conn.write(unsent[0..to_send]);
unsent = unsent[sent..];
std.time.sleep(std.time.ns_per_us * 5);
}
break;
}
}
}
fn serveCDP(address: std.net.Address) !void {
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var app = try App.init(gpa.allocator(), .{
.run_mode = .serve,
.tls_verify_host = false,
});
defer app.deinit();
test_wg.finish();
server.run(app, address, std.time.ns_per_s * 2) catch |err| {
std.debug.print("CDP server error: {}", .{err});
return err;
};
}

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

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

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

View File

@@ -26,10 +26,6 @@ const BORDER = "=" ** 80;
// use in custom panic handler
var current_test: ?[]const u8 = null;
const jsruntime = @import("jsruntime");
pub const Types = jsruntime.reflect(@import("generate.zig").Tuple(.{}){});
pub const UserContext = @import("user_context.zig").UserContext;
pub const std_options = std.Options{
.log_level = .warn,
@@ -38,6 +34,9 @@ pub const std_options = std.Options{
.side_channels_mitigations = .none,
};
pub var js_runner_duration: usize = 0;
pub var tracking_allocator = TrackingAllocator.init(std.testing.allocator);
pub fn main() !void {
var mem: [8192]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&mem);
@@ -50,6 +49,19 @@ pub fn main() !void {
var slowest = SlowTracker.init(allocator, 5);
defer slowest.deinit();
var args = try std.process.argsWithAllocator(allocator);
defer args.deinit();
// ignore the exec name.
_ = args.next();
var json_stats = false;
while (args.next()) |arg| {
if (std.mem.eql(u8, "--json", arg)) {
json_stats = true;
continue;
}
}
var pass: usize = 0;
var fail: usize = 0;
var skip: usize = 0;
@@ -159,6 +171,38 @@ pub fn main() !void {
printer.fmt("\n", .{});
try slowest.display(printer);
printer.fmt("\n", .{});
// TODO: at the very least, `browser` should return real stats
if (json_stats) {
const stats = tracking_allocator.stats();
try std.json.stringify(&.{
.{ .name = "browser", .bench = .{
.duration = js_runner_duration,
.alloc_nb = stats.allocation_count,
.realloc_nb = stats.reallocation_count,
.alloc_size = stats.allocated_bytes,
} },
.{ .name = "libdom", .bench = .{
.duration = js_runner_duration,
.alloc_nb = 0,
.realloc_nb = 0,
.alloc_size = 0,
} },
.{ .name = "v8", .bench = .{
.duration = js_runner_duration,
.alloc_nb = 0,
.realloc_nb = 0,
.alloc_size = 0,
} },
.{ .name = "main", .bench = .{
.duration = js_runner_duration,
.alloc_nb = 0,
.realloc_nb = 0,
.alloc_size = 0,
} },
}, .{ .whitespace = .indent_2 }, std.io.getStdOut().writer());
}
std.posix.exit(if (fail == 0) 0 else 1);
}
@@ -335,3 +379,90 @@ fn isSetup(t: std.builtin.TestFn) bool {
fn isTeardown(t: std.builtin.TestFn) bool {
return std.mem.endsWith(u8, t.name, "tests:afterAll");
}
pub const TrackingAllocator = struct {
parent_allocator: Allocator,
free_count: usize = 0,
allocated_bytes: usize = 0,
allocation_count: usize = 0,
reallocation_count: usize = 0,
const Stats = struct {
allocated_bytes: usize,
allocation_count: usize,
reallocation_count: usize,
};
fn init(parent_allocator: Allocator) TrackingAllocator {
return .{
.parent_allocator = parent_allocator,
};
}
pub fn stats(self: *const TrackingAllocator) Stats {
return .{
.allocated_bytes = self.allocated_bytes,
.allocation_count = self.allocation_count,
.reallocation_count = self.reallocation_count,
};
}
pub fn allocator(self: *TrackingAllocator) Allocator {
return .{ .ptr = self, .vtable = &.{
.alloc = alloc,
.resize = resize,
.free = free,
.remap = remap,
} };
}
fn alloc(
ctx: *anyopaque,
len: usize,
alignment: std.mem.Alignment,
return_address: usize,
) ?[*]u8 {
const self: *TrackingAllocator = @ptrCast(@alignCast(ctx));
const result = self.parent_allocator.rawAlloc(len, alignment, return_address);
self.allocation_count += 1;
self.allocated_bytes += len;
return result;
}
fn resize(
ctx: *anyopaque,
old_mem: []u8,
alignment: std.mem.Alignment,
new_len: usize,
ra: usize,
) bool {
const self: *TrackingAllocator = @ptrCast(@alignCast(ctx));
const result = self.parent_allocator.rawResize(old_mem, alignment, new_len, ra);
self.reallocation_count += 1; // TODO: only if result is not null?
return result;
}
fn free(
ctx: *anyopaque,
old_mem: []u8,
alignment: std.mem.Alignment,
ra: usize,
) void {
const self: *TrackingAllocator = @ptrCast(@alignCast(ctx));
self.parent_allocator.rawFree(old_mem, alignment, ra);
self.free_count += 1;
}
fn remap(
ctx: *anyopaque,
memory: []u8,
alignment: std.mem.Alignment,
new_len: usize,
ret_addr: usize,
) ?[*]u8 {
const self: *TrackingAllocator = @ptrCast(@alignCast(ctx));
const result = self.parent_allocator.rawRemap(memory, alignment, new_len, ret_addr);
self.reallocation_count += 1; // TODO: only if result is not null?
return result;
}
};

View File

@@ -17,15 +17,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const parser = @import("netsurf");
pub const allocator = std.testing.allocator;
pub const expectError = std.testing.expectError;
pub const expectString = std.testing.expectEqualStrings;
pub const expectEqualSlices = std.testing.expectEqualSlices;
const App = @import("app.zig").App;
const Allocator = std.mem.Allocator;
const parser = @import("browser/netsurf.zig");
// Merged std.testing.expectEqual and std.testing.expectString
// can be useful when testing fields of an anytype an you don't know
@@ -217,13 +217,13 @@ pub const Document = struct {
}
pub fn querySelectorAll(self: *Document, selector: []const u8) ![]const *parser.Node {
const css = @import("dom/css.zig");
const css = @import("browser/dom/css.zig");
const node_list = try css.querySelectorAll(self.arena.allocator(), self.asNode(), selector);
return node_list.nodes.items;
}
pub fn querySelector(self: *Document, selector: []const u8) !?*parser.Node {
const css = @import("dom/css.zig");
const css = @import("browser/dom/css.zig");
return css.querySelector(self.arena.allocator(), self.asNode(), selector);
}
@@ -350,3 +350,163 @@ fn isJsonValue(a: std.json.Value, b: std.json.Value) bool {
},
}
}
pub const tracking_allocator = @import("root").tracking_allocator.allocator();
pub const JsRunner = struct {
const URL = @import("url.zig").URL;
const Env = @import("browser/env.zig").Env;
const Loop = @import("runtime/loop.zig").Loop;
const HttpClient = @import("http/client.zig").Client;
const storage = @import("browser/storage/storage.zig");
const Window = @import("browser/html/window.zig").Window;
const Renderer = @import("browser/browser.zig").Renderer;
const SessionState = @import("browser/env.zig").SessionState;
url: URL,
env: *Env,
loop: Loop,
window: Window,
state: SessionState,
arena: Allocator,
renderer: Renderer,
http_client: HttpClient,
executor: *Env.Executor,
storage_shelf: storage.Shelf,
cookie_jar: storage.CookieJar,
fn init(parent_allocator: Allocator, opts: RunnerOpts) !*JsRunner {
parser.deinit();
try parser.init();
const aa = try parent_allocator.create(std.heap.ArenaAllocator);
aa.* = std.heap.ArenaAllocator.init(parent_allocator);
errdefer aa.deinit();
const arena = aa.allocator();
const runner = try arena.create(JsRunner);
runner.arena = arena;
runner.env = try Env.init(arena, .{});
errdefer runner.env.deinit();
runner.url = try URL.parse("https://lightpanda.io/opensource-browser/", null);
runner.renderer = Renderer.init(arena);
runner.cookie_jar = storage.CookieJar.init(arena);
runner.loop = try Loop.init(arena);
errdefer runner.loop.deinit();
var html = std.io.fixedBufferStream(opts.html);
const document = try parser.documentHTMLParse(html.reader(), "UTF-8");
runner.state = .{
.arena = arena,
.loop = &runner.loop,
.document = document,
.url = &runner.url,
.renderer = &runner.renderer,
.cookie_jar = &runner.cookie_jar,
.http_client = &runner.http_client,
};
runner.window = .{};
try runner.window.replaceDocument(document);
try runner.window.replaceLocation(.{
.url = try runner.url.toWebApi(arena),
});
runner.storage_shelf = storage.Shelf.init(arena);
runner.window.setStorageShelf(&runner.storage_shelf);
runner.http_client = try HttpClient.init(arena, 1, .{
.tls_verify_host = false,
});
runner.executor = try runner.env.startExecutor(Window, &runner.state, runner);
errdefer runner.env.stopExecutor(runner.executor);
try runner.executor.startScope(&runner.window);
return runner;
}
pub fn deinit(self: *JsRunner) void {
self.loop.deinit();
self.executor.endScope();
self.env.deinit();
self.http_client.deinit();
self.storage_shelf.deinit();
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(self.arena.ptr));
arena.deinit();
arena.child_allocator.destroy(arena);
}
const RunOpts = struct {};
pub const Case = std.meta.Tuple(&.{ []const u8, []const u8 });
pub fn testCases(self: *JsRunner, cases: []const Case, _: RunOpts) !void {
const start = try std.time.Instant.now();
for (cases, 0..) |case, i| {
var try_catch: Env.TryCatch = undefined;
try_catch.init(self.executor);
defer try_catch.deinit();
const value = self.executor.exec(case.@"0", null) catch |err| {
if (try try_catch.err(self.arena)) |msg| {
std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" });
}
return err;
};
try self.loop.run();
@import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start);
const actual = try value.toString(self.arena);
if (std.mem.eql(u8, case.@"1", actual) == false) {
std.debug.print("Expected:\n{s}\n\nGot:\n{s}\n\nCase: {d}\n{s}\n", .{ case.@"1", actual, i + 1, case.@"0" });
return error.UnexpectedResult;
}
}
}
pub fn exec(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !void {
_ = try self.eval(src, name, err_msg);
}
pub fn eval(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !Env.Value {
var try_catch: Env.TryCatch = undefined;
try_catch.init(self.executor);
defer try_catch.deinit();
return self.executor.exec(src, name) catch |err| {
if (try try_catch.err(self.arena)) |msg| {
err_msg.* = msg;
std.debug.print("Error running script: {s}\n", .{msg});
}
return err;
};
}
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 {
_ = ctx;
_ = specifier;
return error.DummyModuleLoader;
}
};
const RunnerOpts = struct {
html: []const u8 =
\\ <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 Allocator = std.mem.Allocator;
const WebApiURL = @import("url/url.zig").URL;
const WebApiURL = @import("browser/url/url.zig").URL;
pub const URL = struct {
uri: Uri,

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

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