diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 2b5fa598..bfe8fa40 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -59,11 +59,11 @@ runs: - name: install v8 shell: bash run: | - mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug - ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug/libc_v8.a + mkdir -p v8/build/${{inputs.arch}}-${{inputs.os}}/debug/ninja/obj/zig/ + ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/build/${{inputs.arch}}-${{inputs.os}}/debug/ninja/obj/zig/libc_v8.a - mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release - ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release/libc_v8.a + mkdir -p v8/build/${{inputs.arch}}-${{inputs.os}}/release/ninja/obj/zig/ + ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/build/${{inputs.arch}}-${{inputs.os}}/release/ninja/obj/zig/libc_v8.a - name: libiconv shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9010b503..36493448 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: arch: ${{env.ARCH}} - name: zig build - run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) + run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) - name: Rename binary run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} @@ -63,7 +63,7 @@ jobs: arch: ${{env.ARCH}} - name: zig build - run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) + run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) - name: Rename binary run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} @@ -95,7 +95,7 @@ jobs: arch: ${{env.ARCH}} - name: zig build - run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) + run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) - name: Rename binary run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} @@ -127,7 +127,7 @@ jobs: arch: ${{env.ARCH}} - name: zig build - run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) + run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) - name: Rename binary run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 53427e09..732c6fa6 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -47,7 +47,7 @@ jobs: - uses: ./.github/actions/install - name: zig build release - run: zig build -Doptimize=ReleaseSafe -Dengine=v8 + run: zig build -Doptimize=ReleaseSafe - name: upload artifact uses: actions/upload-artifact@v4 @@ -86,7 +86,7 @@ jobs: - name: run puppeteer run: | python3 -m http.server 1234 -d ./public & echo $! > PYTHON.pid - ./lightpanda serve & echo $! > LPD.pid + ./lightpanda serve --gc_hints & echo $! > LPD.pid RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1 cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM kill `cat LPD.pid` `cat PYTHON.pid` diff --git a/.github/workflows/wpt.yml b/.github/workflows/wpt.yml index a3416928..349c94c1 100644 --- a/.github/workflows/wpt.yml +++ b/.github/workflows/wpt.yml @@ -55,7 +55,7 @@ jobs: - uses: ./.github/actions/install - - run: zig build wpt -Dengine=v8 -- --safe --summary + - run: zig build wpt -- --safe --summary # For now WPT tests doesn't pass at all. # We accept then to continue the job on failure. @@ -80,7 +80,7 @@ jobs: - uses: ./.github/actions/install - name: json output - run: zig build wpt -Dengine=v8 -- --safe --json > wpt.json + run: zig build wpt -- --safe --json > wpt.json - name: write commit run: | diff --git a/.github/workflows/zig-test.yml b/.github/workflows/zig-test.yml index c462eb0c..bbe8afae 100644 --- a/.github/workflows/zig-test.yml +++ b/.github/workflows/zig-test.yml @@ -56,7 +56,7 @@ jobs: - uses: ./.github/actions/install - name: zig build debug - run: zig build -Dengine=v8 + run: zig build - name: upload artifact uses: actions/upload-artifact@v4 @@ -102,11 +102,8 @@ jobs: - uses: ./.github/actions/install - - name: zig build unittest - run: zig build unittest -freference-trace --summary all - - name: zig build test - run: zig build test -Dengine=v8 -- --json > bench.json + run: zig build test -- --json > bench.json - name: write commit run: | diff --git a/.gitignore b/.gitignore index 4c5a0f9f..ad9ae7b4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ zig-out /vendor/netsurf/out /vendor/libiconv/ lightpanda.id +/v8/ diff --git a/.gitmodules b/.gitmodules index 9ee19adb..f025f0bd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ -[submodule "vendor/zig-js-runtime"] - path = vendor/zig-js-runtime - url = https://github.com/lightpanda-io/zig-js-runtime.git/ - branch = zig-0.14 [submodule "vendor/netsurf/libwapcaplet"] path = vendor/netsurf/libwapcaplet url = https://github.com/lightpanda-io/libwapcaplet.git/ diff --git a/Dockerfile b/Dockerfile index 7c0095d6..539dfdc6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,8 +61,8 @@ RUN make install-libiconv && \ # download and install v8 RUN curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \ - mkdir -p vendor/zig-js-runtime/vendor/v8/${ARCH}-linux/release && \ - mv libc_v8.a vendor/zig-js-runtime/vendor/v8/${ARCH}-linux/release/libc_v8.a + mkdir -p v8/build/${ARCH}-linux/release/ninja/obj/zig/ && \ + mv libc_v8.a v8/build/${ARCH}-linux/release/ninja/obj/zig/libc_v8.a # build release RUN make build diff --git a/Makefile b/Makefile index e9bc5716..309bbb42 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ ZIG := zig BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) -# option test filter make unittest F="server" +# option test filter make test F="server" F= # OS and ARCH @@ -47,7 +47,7 @@ help: # $(ZIG) commands # ------------ -.PHONY: build build-dev run run-release shell test bench download-zig wpt unittest data +.PHONY: build build-dev run run-release shell test bench download-zig wpt data get-v8 build-v8 build-v8-dev zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2) @@ -62,13 +62,13 @@ download-zig: ## Build in release-safe mode build: @printf "\e[36mBuilding (release safe)...\e[0m\n" - $(ZIG) build -Doptimize=ReleaseSafe -Dengine=v8 -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) + $(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) @printf "\e[33mBuild OK\e[0m\n" ## Build in debug mode build-dev: @printf "\e[36mBuilding (debug)...\e[0m\n" - @$(ZIG) build -Dengine=v8 -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) + @$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) @printf "\e[33mBuild OK\e[0m\n" ## Run the server in debug mode @@ -79,39 +79,47 @@ run: build ## Run a JS shell in debug mode shell: @printf "\e[36mBuilding shell...\e[0m\n" - @$(ZIG) build shell -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) + @$(ZIG) build shell || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) ## Run WPT tests wpt: @printf "\e[36mBuilding wpt...\e[0m\n" - @$(ZIG) build wpt -Dengine=v8 -- --safe $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) + @$(ZIG) build wpt -- --safe $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) wpt-summary: @printf "\e[36mBuilding wpt...\e[0m\n" - @$(ZIG) build wpt -Dengine=v8 -- --safe --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) + @$(ZIG) build wpt -- --safe --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) ## Test test: - @printf "\e[36mTesting...\e[0m\n" - @$(ZIG) build test -Dengine=v8 || (printf "\e[33mTest ERROR\e[0m\n"; exit 1;) - @printf "\e[33mTest OK\e[0m\n" + @TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all -unittest: - @TEST_FILTER='${F}' $(ZIG) build unittest -freference-trace --summary all +## v8 +get-v8: + @printf "\e[36mGetting v8 source...\e[0m\n" + @$(ZIG) build get-v8 + +build-v8-dev: + @printf "\e[36mBuilding v8 (dev)...\e[0m\n" + @$(ZIG) build build-v8 + +build-v8: + @printf "\e[36mBuilding v8...\e[0m\n" + @$(ZIG) build -Doptimize=ReleaseSafe build-v8 # Install and build required dependencies commands # ------------ .PHONY: install-submodule -.PHONY: install-zig-js-runtime install-zig-js-runtime-dev install-libiconv +.PHONY: install-libiconv .PHONY: _install-netsurf install-netsurf clean-netsurf test-netsurf install-netsurf-dev .PHONY: install-mimalloc install-mimalloc-dev clean-mimalloc .PHONY: install-dev install ## Install and build dependencies for release -install: install-submodule install-zig-js-runtime install-libiconv install-netsurf install-mimalloc +install: install-submodule install-libiconv install-netsurf install-mimalloc ## Install and build dependencies for dev -install-dev: install-submodule install-zig-js-runtime-dev install-libiconv install-netsurf-dev install-mimalloc-dev +install-dev: install-submodule install-libiconv install-netsurf-dev install-mimalloc-dev install-netsurf-dev: _install-netsurf install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG @@ -194,14 +202,6 @@ ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","") make clean endif -install-zig-js-runtime-dev: - @cd vendor/zig-js-runtime && \ - make install-dev - -install-zig-js-runtime: - @cd vendor/zig-js-runtime && \ - make install - data: cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig diff --git a/README.md b/README.md index eadf865e..5587c815 100644 --- a/README.md +++ b/README.md @@ -221,17 +221,21 @@ Note: when Mimalloc is built in dev mode, you can dump memory stats with the env var `MIMALLOC_SHOW_STATS=1`. See [https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html). -**zig-js-runtime** +**v8** -Our own Zig/Javascript runtime, which includes the v8 Javascript engine. - -This build task is very long and cpu consuming, as you will build v8 from sources. +First, get the tools necessary for building V8, as well as the V8 source code: ``` -make install-zig-js-runtime +make get-v8 ``` -For dev env, use `make install-zig-js-runtime-dev`. +Next, build v8. This build task is very long and cpu consuming, as you will build v8 from sources. + +``` +make build-v8 +``` + +For dev env, use `make build-v8-dev`. ## Test diff --git a/build.zig b/build.zig index c26988c6..160e3c3f 100644 --- a/build.zig +++ b/build.zig @@ -17,16 +17,11 @@ // along with this program. If not, see . const std = @import("std"); - const builtin = @import("builtin"); -const jsruntime_path = "vendor/zig-js-runtime/"; -const jsruntime = @import("vendor/zig-js-runtime/build.zig"); -const jsruntime_pkgs = jsruntime.packages(jsruntime_path); - /// Do not rename this constant. It is scanned by some scripts to determine /// which zig version to install. -const recommended_zig_version = jsruntime.recommended_zig_version; +const recommended_zig_version = "0.14.0"; pub fn build(b: *std.Build) !void { switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) { @@ -42,193 +37,190 @@ pub fn build(b: *std.Build) !void { }, } + var opts = b.addOptions(); + opts.addOption( + []const u8, + "git_commit", + b.option([]const u8, "git_commit", "Current git commit") orelse "dev", + ); + const target = b.standardTargetOptions(.{}); - const mode = b.standardOptimizeOption(.{}); + const optimize = b.standardOptimizeOption(.{}); - const options = jsruntime.buildOptions(b); - - // browser - // ------- - - // compile and install - const exe = b.addExecutable(.{ - .name = "lightpanda", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = mode, - }); - try common(b, exe, options); { - var opt = b.addOptions(); - opt.addOption( - []const u8, - "git_commit", - b.option([]const u8, "git_commit", "Current git commit") orelse "dev", - ); - exe.root_module.addImport("build_info", opt.createModule()); - } - b.installArtifact(exe); + // browser + // ------- - // run - const run_cmd = b.addRunArtifact(exe); - if (b.args) |args| { - run_cmd.addArgs(args); + // compile and install + const exe = b.addExecutable(.{ + .name = "lightpanda", + .target = target, + .optimize = optimize, + .root_source_file = b.path("src/main.zig"), + }); + + try common(b, opts, exe); + b.installArtifact(exe); + + // run + const run_cmd = b.addRunArtifact(exe); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // step + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); } - // step - const run_step = b.step("run", "Run the app"); - run_step.dependOn(&run_cmd.step); - - // shell - // ----- - - // compile and install - const shell = b.addExecutable(.{ - .name = "lightpanda-shell", - .root_source_file = b.path("src/main_shell.zig"), - .target = target, - .optimize = mode, - }); - try common(b, shell, options); - try jsruntime_pkgs.add_shell(shell); - - // run - const shell_cmd = b.addRunArtifact(shell); - if (b.args) |args| { - shell_cmd.addArgs(args); + { + // get v8 + // ------- + const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize }); + const get_v8 = b.addRunArtifact(v8.artifact("get-v8")); + const get_step = b.step("get-v8", "Get v8"); + get_step.dependOn(&get_v8.step); } - // step - const shell_step = b.step("shell", "Run JS shell"); - shell_step.dependOn(&shell_cmd.step); - - // test - // ---- - - // compile - const tests = b.addTest(.{ - .root_source_file = b.path("src/main_tests.zig"), - .test_runner = .{ .path = b.path("src/main_tests.zig"), .mode = .simple }, - .target = target, - .optimize = mode, - }); - try common(b, tests, options); - - // add jsruntime pretty deps - tests.root_module.addAnonymousImport("pretty", .{ - .root_source_file = b.path("vendor/zig-js-runtime/src/pretty.zig"), - }); - - const run_tests = b.addRunArtifact(tests); - if (b.args) |args| { - run_tests.addArgs(args); + { + // build v8 + // ------- + const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize }); + const build_v8 = b.addRunArtifact(v8.artifact("build-v8")); + const build_step = b.step("build-v8", "Build v8"); + build_step.dependOn(&build_v8.step); } - // step - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_tests.step); + { + // tests + // ---- - // unittest - // ---- + // compile + const tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple }, + .target = target, + .optimize = optimize, + }); + try common(b, opts, tests); - // compile - const unit_tests = b.addTest(.{ - .root_source_file = b.path("src/main_unit_tests.zig"), - .test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple }, - .target = target, - .optimize = mode, - }); - try common(b, unit_tests, options); + const run_tests = b.addRunArtifact(tests); + if (b.args) |args| { + run_tests.addArgs(args); + } - const run_unit_tests = b.addRunArtifact(unit_tests); - if (b.args) |args| { - run_unit_tests.addArgs(args); + // step + const tests_step = b.step("test", "Run unit tests"); + tests_step.dependOn(&run_tests.step); } - // step - const unit_test_step = b.step("unittest", "Run unit tests"); - unit_test_step.dependOn(&run_unit_tests.step); + { + // wpt + // ----- - // wpt - // ----- + // compile and install + const wpt = b.addExecutable(.{ + .name = "lightpanda-wpt", + .root_source_file = b.path("src/main_wpt.zig"), + .target = target, + .optimize = optimize, + }); + try common(b, opts, wpt); - // compile and install - const wpt = b.addExecutable(.{ - .name = "lightpanda-wpt", - .root_source_file = b.path("src/main_wpt.zig"), - .target = target, - .optimize = mode, - }); - try common(b, wpt, options); - - // run - const wpt_cmd = b.addRunArtifact(wpt); - if (b.args) |args| { - wpt_cmd.addArgs(args); + // run + const wpt_cmd = b.addRunArtifact(wpt); + if (b.args) |args| { + wpt_cmd.addArgs(args); + } + // step + const wpt_step = b.step("wpt", "WPT tests"); + wpt_step.dependOn(&wpt_cmd.step); } - // step - const wpt_step = b.step("wpt", "WPT tests"); - wpt_step.dependOn(&wpt_cmd.step); } -fn common( - b: *std.Build, - step: *std.Build.Step.Compile, - options: jsruntime.Options, -) !void { - const target = step.root_module.resolved_target.?; - const optimize = step.root_module.optimize.?; +fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Compile) !void { + const mod = step.root_module; + const target = mod.resolved_target.?; + const optimize = mod.optimize.?; const dep_opts = .{ .target = target, .optimize = optimize }; - const jsruntimemod = try jsruntime_pkgs.module( - b, - options, - step.root_module.optimize.?, - target, + try moduleNetSurf(b, step, target); + mod.addImport("tls", b.dependency("tls", dep_opts).module("tls")); + mod.addImport("tigerbeetle-io", b.dependency("tigerbeetle_io", .{}).module("tigerbeetle_io")); + + { + // v8 + const v8_opts = b.addOptions(); + v8_opts.addOption(bool, "inspector_subtype", false); + + const v8_mod = b.dependency("v8", dep_opts).module("v8"); + v8_mod.addOptions("default_exports", v8_opts); + mod.addImport("v8", v8_mod); + } + + const mode_str: []const u8 = if (mod.optimize.? == .Debug) "debug" else "release"; + + // FIXME: we are tied to native v8 builds, currently: + // - aarch64-macos + // - x86_64-linux + const os = target.result.os.tag; + const arch = target.result.cpu.arch; + switch (os) { + .macos => {}, + .linux => { + // TODO: why do we need it? It should be linked already when we built v8 + mod.link_libcpp = true; + }, + else => return error.OsNotSupported, + } + + const lib_path = try std.fmt.allocPrint( + mod.owner.allocator, + "v8/build/{s}-{s}/{s}/ninja/obj/zig/libc_v8.a", + .{ @tagName(arch), @tagName(os), mode_str }, ); - step.root_module.addImport("jsruntime", jsruntimemod); - - const netsurf = try moduleNetSurf(b, target); - netsurf.addImport("jsruntime", jsruntimemod); - step.root_module.addImport("netsurf", netsurf); - - step.root_module.addImport("tls", b.dependency("tls", dep_opts).module("tls")); + mod.addObjectFile(mod.owner.path(lib_path)); + mod.addImport("build_info", opts.createModule()); } -fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module { - const mod = b.addModule("netsurf", .{ - .root_source_file = b.path("src/netsurf/netsurf.zig"), - .target = target, - }); - +fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build.ResolvedTarget) !void { const os = target.result.os.tag; const arch = target.result.cpu.arch; // iconv const libiconv_lib_path = try std.fmt.allocPrint( - mod.owner.allocator, + b.allocator, "vendor/libiconv/out/{s}-{s}/lib/libiconv.a", .{ @tagName(os), @tagName(arch) }, ); const libiconv_include_path = try std.fmt.allocPrint( - mod.owner.allocator, + b.allocator, "vendor/libiconv/out/{s}-{s}/lib/libiconv.a", .{ @tagName(os), @tagName(arch) }, ); - mod.addObjectFile(b.path(libiconv_lib_path)); - mod.addIncludePath(b.path(libiconv_include_path)); + step.addObjectFile(b.path(libiconv_lib_path)); + step.addIncludePath(b.path(libiconv_include_path)); - // mimalloc - mod.addImport("mimalloc", (try moduleMimalloc(b, target))); + { + // mimalloc + const mimalloc = "vendor/mimalloc"; + const lib_path = try std.fmt.allocPrint( + b.allocator, + mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a", + .{ @tagName(os), @tagName(arch) }, + ); + step.addObjectFile(b.path(lib_path)); + step.addIncludePath(b.path(mimalloc ++ "/include")); + } // netsurf libs const ns = "vendor/netsurf"; const ns_include_path = try std.fmt.allocPrint( - mod.owner.allocator, + b.allocator, ns ++ "/out/{s}-{s}/include", .{ @tagName(os), @tagName(arch) }, ); - mod.addIncludePath(b.path(ns_include_path)); + step.addIncludePath(b.path(ns_include_path)); const libs: [4][]const u8 = .{ "libdom", @@ -238,34 +230,11 @@ fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Mo }; inline for (libs) |lib| { const ns_lib_path = try std.fmt.allocPrint( - mod.owner.allocator, + b.allocator, ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a", .{ @tagName(os), @tagName(arch) }, ); - mod.addObjectFile(b.path(ns_lib_path)); - mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src")); + step.addObjectFile(b.path(ns_lib_path)); + step.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src")); } - - return mod; -} - -fn moduleMimalloc(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module { - const mod = b.addModule("mimalloc", .{ - .root_source_file = b.path("src/mimalloc/mimalloc.zig"), - .target = target, - }); - - const os = target.result.os.tag; - const arch = target.result.cpu.arch; - - const mimalloc = "vendor/mimalloc"; - const lib_path = try std.fmt.allocPrint( - mod.owner.allocator, - mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a", - .{ @tagName(os), @tagName(arch) }, - ); - mod.addObjectFile(b.path(lib_path)); - mod.addIncludePath(b.path(mimalloc ++ "/include")); - - return mod; } diff --git a/build.zig.zon b/build.zig.zon index 7a3981e4..757b2e28 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,7 +6,17 @@ .dependencies = .{ .tls = .{ .url = "https://github.com/ianic/tls.zig/archive/b29a8b45fc59fc2d202769c4f54509bb9e17d0a2.tar.gz", - .hash = "1220e6fd39920dd6e28b2bc06688787a39430f8856f0597cd77c44ca868c6c54fb86", + .hash = "tls-0.1.0-ER2e0uAxBQDm_TmSDdbiiyvAZoh4ejlDD4hW8Fl813xE", }, + .tigerbeetle_io = .{ + .url = "https://github.com/lightpanda-io/tigerbeetle-io/archive/61d9652f1a957b7f4db723ea6aa0ce9635e840ce.tar.gz", + .hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd", + }, + .v8 = .{ + .url = "https://github.com/karlseguin/zig-v8-fork/archive/e5f1c0c9f1ed147617427f22cdaf11df4ab60b79.tar.gz", + .hash = "v8-0.0.0-xddH61vYIACI2pT1t-dUbXm18cHAKy-KWT_Qft4sBwam", + }, + //.v8 = .{ .path = "../zig-v8-fork" }, + //.tigerbeetle_io = .{ .path = "../tigerbeetle-io" }, }, } diff --git a/src/apiweb.zig b/src/apiweb.zig deleted file mode 100644 index 8df1bd6d..00000000 --- a/src/apiweb.zig +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -const generate = @import("generate.zig"); - -const Console = @import("jsruntime").Console; - -const DOM = @import("dom/dom.zig"); -const HTML = @import("html/html.zig"); -const Events = @import("events/event.zig"); -const XHR = @import("xhr/xhr.zig"); -const Storage = @import("storage/storage.zig"); -const URL = @import("url/url.zig"); -const Iterators = @import("iterator/iterator.zig"); -const XMLSerializer = @import("xmlserializer/xmlserializer.zig"); - -pub const HTMLDocument = @import("html/document.zig").HTMLDocument; - -// Interfaces -pub const Interfaces = generate.Tuple(.{ - Console, - DOM.Interfaces, - Events.Interfaces, - HTML.Interfaces, - XHR.Interfaces, - Storage.Interfaces, - URL.Interfaces, - Iterators.Interfaces, - XMLSerializer.Interfaces, -}){}; - -pub const UserContext = @import("user_context.zig").UserContext; diff --git a/src/app.zig b/src/app.zig index 5c90b546..6bb883f9 100644 --- a/src/app.zig +++ b/src/app.zig @@ -1,7 +1,8 @@ const std = @import("std"); - -const Loop = @import("jsruntime").Loop; const Allocator = std.mem.Allocator; + +const js = @import("runtime/js.zig"); +const Loop = @import("runtime/loop.zig").Loop; const HttpClient = @import("http/client.zig").Client; const Telemetry = @import("telemetry/telemetry.zig").Telemetry; @@ -11,6 +12,7 @@ const log = std.log.scoped(.app); // might need. pub const App = struct { loop: *Loop, + config: Config, allocator: Allocator, telemetry: Telemetry, http_client: HttpClient, @@ -24,8 +26,9 @@ pub const App = struct { }; pub const Config = struct { - tls_verify_host: bool = true, run_mode: RunMode, + gc_hints: bool = false, + tls_verify_host: bool = true, }; pub fn init(allocator: Allocator, config: Config) !*App { @@ -48,6 +51,7 @@ pub const App = struct { .http_client = try HttpClient.init(allocator, 5, .{ .tls_verify_host = config.tls_verify_host, }), + .config = config, }; app.telemetry = Telemetry.init(app, config.run_mode); diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 8181e350..679fdc52 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -21,31 +21,26 @@ const builtin = @import("builtin"); const Allocator = std.mem.Allocator; -const Types = @import("root").Types; - -const parser = @import("netsurf"); const Dump = @import("dump.zig"); const Mime = @import("mime.zig").Mime; +const parser = @import("netsurf.zig"); -const jsruntime = @import("jsruntime"); -const Loop = jsruntime.Loop; -const Env = jsruntime.Env; -const Module = jsruntime.Module; +const Window = @import("html/window.zig").Window; +const Walker = @import("dom/walker.zig").WalkerDepthFirst; +const Env = @import("env.zig").Env; const App = @import("../app.zig").App; -const apiweb = @import("../apiweb.zig"); - -const Window = @import("../html/window.zig").Window; -const Walker = @import("../dom/walker.zig").WalkerDepthFirst; const URL = @import("../url.zig").URL; -const storage = @import("../storage/storage.zig"); -const Notification = @import("../notification.zig").Notification; const http = @import("../http/client.zig"); -const UserContext = @import("../user_context.zig").UserContext; +const storage = @import("storage/storage.zig"); +const Loop = @import("../runtime/loop.zig").Loop; +const SessionState = @import("env.zig").SessionState; +const HttpClient = @import("../http/client.zig").Client; +const Notification = @import("../notification.zig").Notification; -const polyfill = @import("../polyfill/polyfill.zig"); +const polyfill = @import("polyfill/polyfill.zig"); const log = std.log.scoped(.browser); @@ -56,6 +51,7 @@ pub const user_agent = "Lightpanda/1.0"; // A browser contains only one session. // TODO allow multiple sessions per browser. pub const Browser = struct { + env: *Env, app: *App, session: ?*Session, allocator: Allocator, @@ -65,10 +61,17 @@ pub const Browser = struct { const SessionPool = std.heap.MemoryPool(Session); - pub fn init(app: *App) Browser { + pub fn init(app: *App) !Browser { const allocator = app.allocator; + + const env = try Env.init(allocator, .{ + .gc_hints = app.config.gc_hints, + }); + errdefer env.deinit(); + return .{ .app = app, + .env = env, .session = null, .allocator = allocator, .http_client = &app.http_client, @@ -79,6 +82,7 @@ pub const Browser = struct { pub fn deinit(self: *Browser) void { self.closeSession(); + self.env.deinit(); self.session_pool.deinit(); self.page_arena.deinit(); } @@ -101,10 +105,7 @@ pub const Browser = struct { } pub fn runMicrotasks(self: *const Browser) void { - // if no session exists, there is nothing to do. - if (self.session == null) return; - - return self.session.?.env.runMicrotasks(); + return self.env.runMicrotasks(); } }; @@ -113,8 +114,11 @@ pub const Browser = struct { // You can create successively multiple pages for a session, but you must // deinit a page before running another one. pub const Session = struct { - app: *App, + state: SessionState, + executor: *Env.Executor, + inspector: Env.Inspector, + app: *App, browser: *Browser, // The arena is used only to bound the js env init b/c it leaks memory. @@ -124,9 +128,6 @@ pub const Session = struct { // all others Session deps use directly self.alloc and not the arena. arena: std.heap.ArenaAllocator, - env: Env, - inspector: jsruntime.Inspector, - window: Window, // TODO move the shed/jar to the browser? @@ -136,8 +137,6 @@ pub const Session = struct { page: ?Page = null, http_client: *http.Client, - jstypes: [Types.len]usize = undefined, - // recipient of notification, passed as the first parameter to notify notify_ctx: *anyopaque, notify_func: *const fn (ctx: *anyopaque, notification: *const Notification) anyerror!void, @@ -159,48 +158,59 @@ pub const Session = struct { const allocator = app.allocator; self.* = .{ .app = app, - .env = undefined, .browser = browser, .notify_ctx = any_ctx, .inspector = undefined, .notify_func = ContextStruct.notify, .http_client = browser.http_client, + .executor = undefined, .storage_shed = storage.Shed.init(allocator), .arena = std.heap.ArenaAllocator.init(allocator), .cookie_jar = storage.CookieJar.init(allocator), .window = Window.create(null, .{ .agent = user_agent }), + .state = .{ + .loop = app.loop, + .document = null, + .http_client = browser.http_client, + + // we'll set this immediately after + .cookie_jar = undefined, + + // nothing should be used on the state until we have a page + // at which point we'll set these fields + .renderer = undefined, + .url = undefined, + .arena = undefined, + }, }; + self.state.cookie_jar = &self.cookie_jar; + errdefer self.arena.deinit(); - const arena = self.arena.allocator(); - Env.init(&self.env, arena, app.loop, null); - errdefer self.env.deinit(); - try self.env.load(&self.jstypes); + self.executor = try browser.env.startExecutor(Window, &self.state, self); + errdefer browser.env.stopExecutor(self.executor); + self.inspector = try Env.Inspector.init(self.arena.allocator(), self.executor, ctx); - // const ctx_opaque = @as(*anyopaque, @ptrCast(ctx)); - self.inspector = try jsruntime.Inspector.init( - arena, - &self.env, - any_ctx, - ContextStruct.onInspectorResponse, - ContextStruct.onInspectorEvent, - ); - self.env.setInspector(self.inspector); - try self.env.setModuleLoadFn(self, Session.fetchModule); + self.microtaskLoop(); } fn deinit(self: *Session) void { + self.app.loop.resetZig(); if (self.page != null) { self.removePage(); } - self.env.deinit(); + self.inspector.deinit(); self.arena.deinit(); self.cookie_jar.deinit(); self.storage_shed.deinit(); + self.browser.env.stopExecutor(self.executor); } - fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module { - _ = referrer; + fn microtaskLoop(self: *Session) void { + self.browser.runMicrotasks(); + self.app.loop.zigTimeout(1 * std.time.ns_per_ms, *Session, self, microtaskLoop); + } + pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 { const self: *Session = @ptrCast(@alignCast(ctx)); const page = &(self.page orelse return error.NoPage); @@ -209,16 +219,15 @@ pub const Session = struct { // Use the page_arena for this, which has a more appropriate lifetime // and which has more retained memory between sessions and pages. const arena = self.browser.page_arena.allocator(); - const body = try page.fetchData( + return try page.fetchData( arena, specifier, if (page.current_script) |s| s.src else null, ); - return self.env.compileModule(body, specifier); } - pub fn callInspector(self: *Session, msg: []const u8) void { - self.inspector.send(self.env, msg); + pub fn callInspector(self: *const Session, msg: []const u8) void { + self.inspector.send(msg); } // NOTE: the caller is not the owner of the returned value, @@ -232,19 +241,14 @@ pub const Session = struct { const page = &self.page.?; // start JS env - log.debug("start js env", .{}); - try self.env.start(); + log.debug("start new js scope", .{}); + self.state.arena = self.browser.page_arena.allocator(); + errdefer self.state.arena = undefined; - if (comptime builtin.is_test == false) { - // By not loading this during tests, we aren't required to load - // all of the interfaces into zig-js-runtime. - log.debug("setup global env", .{}); - try self.env.bindGlobal(&self.window); - } + try self.executor.startScope(&self.window); // load polyfills - // TODO: change to 'env' when https://github.com/lightpanda-io/zig-js-runtime/pull/285 lands - try polyfill.load(self.arena.allocator(), &self.env); + try polyfill.load(self.arena.allocator(), self.executor); // inspector self.contextCreated(page, aux_data); @@ -254,11 +258,10 @@ pub const Session = struct { pub fn removePage(self: *Session) void { std.debug.assert(self.page != null); - // Reset all existing callbacks. self.app.loop.resetJS(); + self.executor.endScope(); - self.env.stop(); // TODO unload document: https://html.spec.whatwg.org/#unloading-documents self.window.replaceLocation(.{ .url = null }) catch |e| { @@ -267,6 +270,7 @@ pub const Session = struct { // clear netsurf memory arena. parser.deinit(); + self.state.arena = undefined; self.page = null; } @@ -277,7 +281,7 @@ pub const Session = struct { fn contextCreated(self: *Session, page: *Page, aux_data: ?[]const u8) void { log.debug("inspector context created", .{}); - self.inspector.contextCreated(&self.env, "", (page.origin() catch "://") orelse "://", aux_data); + self.inspector.contextCreated(self.executor, "", (page.origin() catch "://") orelse "://", aux_data); } fn notify(self: *const Session, notification: *const Notification) void { @@ -332,19 +336,16 @@ pub const Page = struct { pub fn wait(self: *Page) !void { // try catch - var try_catch: jsruntime.TryCatch = undefined; - try_catch.init(&self.session.env); + var try_catch: Env.TryCatch = undefined; + try_catch.init(self.session.executor); defer try_catch.deinit(); - self.session.env.wait() catch |err| { - // the js env could not be started if the document wasn't an HTML. - if (err == error.EnvNotStarted) return; - - const arena = self.arena; - if (try try_catch.err(arena, &self.session.env)) |msg| { - defer arena.free(msg); + self.session.app.loop.run() catch |err| { + if (try try_catch.err(self.arena)) |msg| { log.info("wait error: {s}", .{msg}); return; + } else { + log.info("wait error: {any}", .{err}); } }; log.debug("wait: OK", .{}); @@ -397,7 +398,7 @@ pub const Page = struct { try session.cookie_jar.populateFromResponse(&url.uri, &header); // TODO handle fragment in url. - try self.session.window.replaceLocation(.{ .url = try url.toWebApi(arena) }); + try session.window.replaceLocation(.{ .url = try url.toWebApi(arena) }); log.info("GET {any} {d}", .{ url, header.status }); @@ -414,7 +415,6 @@ pub const Page = struct { log.debug("header content-type: {s}", .{ct}); var mime = try Mime.parse(arena, ct); - defer mime.deinit(); if (mime.isHTML()) { try self.loadHTMLDoc(&response, mime.charset orelse "utf-8", aux_data); @@ -485,7 +485,7 @@ pub const Page = struct { // https://html.spec.whatwg.org/#reporting-document-loading-status // inject the URL to the document including the fragment. - try parser.documentSetDocumentURI(doc, if (self.url) |*url| url.raw else "about:blank"); + try parser.documentSetDocumentURI(doc, self.url.?.raw); const session = self.session; // TODO set the referrer to the document. @@ -499,14 +499,13 @@ pub const Page = struct { // inspector session.contextCreated(self, aux_data); - // replace the user context document with the new one. - try session.env.setUserContext(.{ - .url = @ptrCast(&self.url.?), - .document = html_doc, - .renderer = @ptrCast(&self.renderer), - .cookie_jar = @ptrCast(&self.session.cookie_jar), - .http_client = @ptrCast(self.session.http_client), - }); + { + // update the sessions state + const state = &session.state; + state.url = &self.url.?; + state.document = html_doc; + state.renderer = &self.renderer; + } // browse the DOM tree to retrieve scripts // TODO execute the synchronous scripts during the HTL parsing. @@ -643,7 +642,7 @@ pub const Page = struct { // TODO handle charset attribute const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element)); if (opt_text) |text| { - try s.eval(self.arena, &self.session.env, text); + try s.eval(self.arena, self.session, text); return; } @@ -711,7 +710,7 @@ pub const Page = struct { fn fetchScript(self: *const Page, s: *const Script) !void { const arena = self.arena; const body = try self.fetchData(arena, s.src, null); - try s.eval(arena, &self.session.env, body); + try s.eval(arena, self.session, body); } fn newHTTPRequest(self: *const Page, method: http.Request.Method, url: *const URL, opts: storage.cookie.LookupOpts) !http.Request { @@ -769,24 +768,24 @@ pub const Page = struct { return .unknown; } - fn eval(self: Script, arena: Allocator, env: *const Env, body: []const u8) !void { - var try_catch: jsruntime.TryCatch = undefined; - try_catch.init(env); + fn eval(self: Script, arena: Allocator, session: *Session, body: []const u8) !void { + var try_catch: Env.TryCatch = undefined; + try_catch.init(session.executor); defer try_catch.deinit(); const res = switch (self.kind) { .unknown => return error.UnknownScript, - .javascript => env.exec(body, self.src), - .module => env.module(body, self.src), + .javascript => session.executor.exec(body, self.src), + .module => session.executor.module(body, self.src), } catch { - if (try try_catch.err(arena, env)) |msg| { + if (try try_catch.err(arena)) |msg| { log.info("eval script {s}: {s}", .{ self.src, msg }); } return FetchError.JsErr; }; if (builtin.mode == .Debug) { - const msg = try res.toString(arena, env); + const msg = try res.toString(arena); log.debug("eval script {s}: {s}", .{ self.src, msg }); } } @@ -810,7 +809,7 @@ const FlatRenderer = struct { // given an index, get the element elements: std.ArrayListUnmanaged(u64), - const Element = @import("../dom/element.zig").Element; + const Element = @import("dom/element.zig").Element; // we expect allocator to be an arena pub fn init(allocator: Allocator) FlatRenderer { diff --git a/src/browser/console/console.zig b/src/browser/console/console.zig new file mode 100644 index 00000000..59f56594 --- /dev/null +++ b/src/browser/console/console.zig @@ -0,0 +1,28 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const log = std.log.scoped(.console); + +pub const Console = struct { + // TODO: configurable writer + + pub fn _log(_: *const Console, str: []const u8) void { + log.debug("{s}\n", .{str}); + } +}; diff --git a/src/css/README.md b/src/browser/css/README.md similarity index 100% rename from src/css/README.md rename to src/browser/css/README.md diff --git a/src/css/css.zig b/src/browser/css/css.zig similarity index 100% rename from src/css/css.zig rename to src/browser/css/css.zig diff --git a/src/css/libdom.zig b/src/browser/css/libdom.zig similarity index 98% rename from src/css/libdom.zig rename to src/browser/css/libdom.zig index 21333726..44307c63 100644 --- a/src/css/libdom.zig +++ b/src/browser/css/libdom.zig @@ -18,7 +18,7 @@ const std = @import("std"); -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); // Node implementation with Netsurf Libdom C lib. pub const Node = struct { diff --git a/src/css/libdom_test.zig b/src/browser/css/libdom_test.zig similarity index 99% rename from src/css/libdom_test.zig rename to src/browser/css/libdom_test.zig index c0cdbb3f..4cd267e0 100644 --- a/src/css/libdom_test.zig +++ b/src/browser/css/libdom_test.zig @@ -19,7 +19,7 @@ const std = @import("std"); const css = @import("css.zig"); const Node = @import("libdom.zig").Node; -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); const Matcher = struct { const Nodes = std.ArrayList(Node); diff --git a/src/css/match_test.zig b/src/browser/css/match_test.zig similarity index 100% rename from src/css/match_test.zig rename to src/browser/css/match_test.zig diff --git a/src/css/parser.zig b/src/browser/css/parser.zig similarity index 100% rename from src/css/parser.zig rename to src/browser/css/parser.zig diff --git a/src/css/selector.zig b/src/browser/css/selector.zig similarity index 100% rename from src/css/selector.zig rename to src/browser/css/selector.zig diff --git a/src/dom/attribute.zig b/src/browser/dom/attribute.zig similarity index 62% rename from src/dom/attribute.zig rename to src/browser/dom/attribute.zig index d85f87a0..7153a509 100644 --- a/src/dom/attribute.zig +++ b/src/browser/dom/attribute.zig @@ -18,11 +18,7 @@ const std = @import("std"); -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); const Node = @import("node.zig").Node; const DOMException = @import("exceptions.zig").DOMException; @@ -31,7 +27,6 @@ const DOMException = @import("exceptions.zig").DOMException; pub const Attr = struct { pub const Self = parser.Attribute; pub const prototype = *Node; - pub const mem_guarantied = true; pub fn get_namespaceURI(self: *parser.Attribute) !?[]const u8 { return try parser.nodeGetNamespace(parser.attributeToNode(self)); @@ -70,34 +65,33 @@ pub const Attr = struct { // Tests // ----- -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var getters = [_]Case{ - .{ .src = "let a = document.createAttributeNS('foo', 'bar')", .ex = "undefined" }, - .{ .src = "a.namespaceURI", .ex = "foo" }, - .{ .src = "a.prefix", .ex = "null" }, - .{ .src = "a.localName", .ex = "bar" }, - .{ .src = "a.name", .ex = "bar" }, - .{ .src = "a.value", .ex = "" }, +const testing = @import("../../testing.zig"); +test "Browser.DOM.Attribute" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "let a = document.createAttributeNS('foo', 'bar')", "undefined" }, + .{ "a.namespaceURI", "foo" }, + .{ "a.prefix", "null" }, + .{ "a.localName", "bar" }, + .{ "a.name", "bar" }, + .{ "a.value", "" }, // TODO: libdom has a bug here: the created attr has no parent, it // causes a panic w/ libdom when setting the value. - //.{ .src = "a.value = 'nok'", .ex = "nok" }, - .{ .src = "a.ownerElement", .ex = "null" }, - }; - try checkCases(js_env, &getters); + //.{ "a.value = 'nok'", "nok" }, + .{ "a.ownerElement", "null" }, + }, .{}); - var attr = [_]Case{ - .{ .src = "let b = document.getElementById('link').getAttributeNode('class')", .ex = "undefined" }, - .{ .src = "b.name", .ex = "class" }, - .{ .src = "b.value", .ex = "ok" }, - .{ .src = "b.value = 'nok'", .ex = "nok" }, - .{ .src = "b.value", .ex = "nok" }, - .{ .src = "b.value = null", .ex = "null" }, - .{ .src = "b.value", .ex = "null" }, - .{ .src = "b.value = 'ok'", .ex = "ok" }, - .{ .src = "b.ownerElement.id", .ex = "link" }, - }; - try checkCases(js_env, &attr); + try runner.testCases(&.{ + .{ "let b = document.getElementById('link').getAttributeNode('class')", "undefined" }, + .{ "b.name", "class" }, + .{ "b.value", "ok" }, + .{ "b.value = 'nok'", "nok" }, + .{ "b.value", "nok" }, + .{ "b.value = null", "null" }, + .{ "b.value", "null" }, + .{ "b.value = 'ok'", "ok" }, + .{ "b.ownerElement.id", "link" }, + }, .{}); } diff --git a/src/dom/cdata_section.zig b/src/browser/dom/cdata_section.zig similarity index 93% rename from src/dom/cdata_section.zig rename to src/browser/dom/cdata_section.zig index c8ff6107..51017b2e 100644 --- a/src/dom/cdata_section.zig +++ b/src/browser/dom/cdata_section.zig @@ -18,7 +18,7 @@ const std = @import("std"); -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); const Text = @import("text.zig").Text; @@ -26,5 +26,4 @@ const Text = @import("text.zig").Text; pub const CDATASection = struct { pub const Self = parser.CDATASection; pub const prototype = *Text; - pub const mem_guarantied = true; }; diff --git a/src/dom/character_data.zig b/src/browser/dom/character_data.zig similarity index 53% rename from src/dom/character_data.zig rename to src/browser/dom/character_data.zig index 372395c2..fd4d4bec 100644 --- a/src/dom/character_data.zig +++ b/src/browser/dom/character_data.zig @@ -18,11 +18,7 @@ const std = @import("std"); -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); const Node = @import("node.zig").Node; const Comment = @import("comment.zig").Comment; @@ -42,7 +38,6 @@ pub const Interfaces = .{ pub const CharacterData = struct { pub const Self = parser.CharacterData; pub const prototype = *Node; - pub const mem_guarantied = true; // JS funcs // -------- @@ -106,74 +101,65 @@ pub const CharacterData = struct { // Tests // ----- -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var get_data = [_]Case{ - .{ .src = "let link = document.getElementById('link')", .ex = "undefined" }, - .{ .src = "let cdata = link.firstChild", .ex = "undefined" }, - .{ .src = "cdata.data", .ex = "OK" }, - }; - try checkCases(js_env, &get_data); +const testing = @import("../../testing.zig"); +test "Browser.DOM.CharacterData" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); - var set_data = [_]Case{ - .{ .src = "cdata.data = 'OK modified'", .ex = "OK modified" }, - .{ .src = "cdata.data === 'OK modified'", .ex = "true" }, - .{ .src = "cdata.data = 'OK'", .ex = "OK" }, - }; - try checkCases(js_env, &set_data); + try runner.testCases(&.{ + .{ "let link = document.getElementById('link')", "undefined" }, + .{ "let cdata = link.firstChild", "undefined" }, + .{ "cdata.data", "OK" }, + }, .{}); - var get_length = [_]Case{ - .{ .src = "cdata.length === 2", .ex = "true" }, - }; - try checkCases(js_env, &get_length); + try runner.testCases(&.{ + .{ "cdata.data = 'OK modified'", "OK modified" }, + .{ "cdata.data === 'OK modified'", "true" }, + .{ "cdata.data = 'OK'", "OK" }, + }, .{}); - var get_next_elem_sibling = [_]Case{ - .{ .src = "cdata.nextElementSibling === null", .ex = "true" }, + try runner.testCases(&.{ + .{ "cdata.length === 2", "true" }, + }, .{}); + + try runner.testCases(&.{ + .{ "cdata.nextElementSibling === null", "true" }, // create a next element - .{ .src = "let next = document.createElement('a')", .ex = "undefined" }, - .{ .src = "link.appendChild(next, cdata) !== undefined", .ex = "true" }, - .{ .src = "cdata.nextElementSibling.localName === 'a' ", .ex = "true" }, - }; - try checkCases(js_env, &get_next_elem_sibling); + .{ "let next = document.createElement('a')", "undefined" }, + .{ "link.appendChild(next, cdata) !== undefined", "true" }, + .{ "cdata.nextElementSibling.localName === 'a' ", "true" }, + }, .{}); - var get_prev_elem_sibling = [_]Case{ - .{ .src = "cdata.previousElementSibling === null", .ex = "true" }, + try runner.testCases(&.{ + .{ "cdata.previousElementSibling === null", "true" }, // create a prev element - .{ .src = "let prev = document.createElement('div')", .ex = "undefined" }, - .{ .src = "link.insertBefore(prev, cdata) !== undefined", .ex = "true" }, - .{ .src = "cdata.previousElementSibling.localName === 'div' ", .ex = "true" }, - }; - try checkCases(js_env, &get_prev_elem_sibling); + .{ "let prev = document.createElement('div')", "undefined" }, + .{ "link.insertBefore(prev, cdata) !== undefined", "true" }, + .{ "cdata.previousElementSibling.localName === 'div' ", "true" }, + }, .{}); - var append_data = [_]Case{ - .{ .src = "cdata.appendData(' modified')", .ex = "undefined" }, - .{ .src = "cdata.data === 'OK modified' ", .ex = "true" }, - }; - try checkCases(js_env, &append_data); + try runner.testCases(&.{ + .{ "cdata.appendData(' modified')", "undefined" }, + .{ "cdata.data === 'OK modified' ", "true" }, + }, .{}); - var delete_data = [_]Case{ - .{ .src = "cdata.deleteData('OK'.length, ' modified'.length)", .ex = "undefined" }, - .{ .src = "cdata.data == 'OK'", .ex = "true" }, - }; - try checkCases(js_env, &delete_data); + try runner.testCases(&.{ + .{ "cdata.deleteData('OK'.length, ' modified'.length)", "undefined" }, + .{ "cdata.data == 'OK'", "true" }, + }, .{}); - var insert_data = [_]Case{ - .{ .src = "cdata.insertData('OK'.length-1, 'modified')", .ex = "undefined" }, - .{ .src = "cdata.data == 'OmodifiedK'", .ex = "true" }, - }; - try checkCases(js_env, &insert_data); + try runner.testCases(&.{ + .{ "cdata.insertData('OK'.length-1, 'modified')", "undefined" }, + .{ "cdata.data == 'OmodifiedK'", "true" }, + }, .{}); - var replace_data = [_]Case{ - .{ .src = "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", .ex = "undefined" }, - .{ .src = "cdata.data == 'OreplacedK'", .ex = "true" }, - }; - try checkCases(js_env, &replace_data); + try runner.testCases(&.{ + .{ "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", "undefined" }, + .{ "cdata.data == 'OreplacedK'", "true" }, + }, .{}); - var substring_data = [_]Case{ - .{ .src = "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", .ex = "true" }, - .{ .src = "cdata.substringData('OK'.length-1, 0) == ''", .ex = "true" }, - }; - try checkCases(js_env, &substring_data); + try runner.testCases(&.{ + .{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" }, + .{ "cdata.substringData('OK'.length-1, 0) == ''", "true" }, + }, .{}); } diff --git a/src/dom/comment.zig b/src/browser/dom/comment.zig similarity index 58% rename from src/dom/comment.zig rename to src/browser/dom/comment.zig index fe4111bc..25734253 100644 --- a/src/dom/comment.zig +++ b/src/browser/dom/comment.zig @@ -17,25 +17,20 @@ // along with this program. If not, see . const std = @import("std"); -const parser = @import("netsurf"); - -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; +const parser = @import("../netsurf.zig"); const CharacterData = @import("character_data.zig").CharacterData; -const UserContext = @import("../user_context.zig").UserContext; +const SessionState = @import("../env.zig").SessionState; // https://dom.spec.whatwg.org/#interface-comment pub const Comment = struct { pub const Self = parser.Comment; pub const prototype = *CharacterData; - pub const mem_guarantied = true; - pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Comment { + pub fn constructor(data: ?[]const u8, state: *const SessionState) !*parser.Comment { return parser.documentCreateComment( - parser.documentHTMLToDocument(userctx.document), + parser.documentHTMLToDocument(state.document.?), data orelse "", ); } @@ -44,16 +39,16 @@ pub const Comment = struct { // Tests // ----- -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var constructor = [_]Case{ - .{ .src = "let comment = new Comment('foo')", .ex = "undefined" }, - .{ .src = "comment.data", .ex = "foo" }, +const testing = @import("../../testing.zig"); +test "Browser.DOM.Comment" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); - .{ .src = "let emptycomment = new Comment()", .ex = "undefined" }, - .{ .src = "emptycomment.data", .ex = "" }, - }; - try checkCases(js_env, &constructor); + try runner.testCases(&.{ + .{ "let comment = new Comment('foo')", "undefined" }, + .{ "comment.data", "foo" }, + + .{ "let emptycomment = new Comment()", "undefined" }, + .{ "emptycomment.data", "" }, + }, .{}); } diff --git a/src/dom/css.zig b/src/browser/dom/css.zig similarity index 98% rename from src/dom/css.zig rename to src/browser/dom/css.zig index 0432e83e..50c262e4 100644 --- a/src/dom/css.zig +++ b/src/browser/dom/css.zig @@ -18,7 +18,7 @@ const std = @import("std"); -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); const css = @import("../css/css.zig"); const Node = @import("../css/libdom.zig").Node; diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig new file mode 100644 index 00000000..1ac56df4 --- /dev/null +++ b/src/browser/dom/document.zig @@ -0,0 +1,444 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const parser = @import("../netsurf.zig"); +const SessionState = @import("../env.zig").SessionState; + +const Node = @import("node.zig").Node; +const NodeList = @import("nodelist.zig").NodeList; +const NodeUnion = @import("node.zig").Union; + +const collection = @import("html_collection.zig"); +const css = @import("css.zig"); + +const Element = @import("element.zig").Element; +const ElementUnion = @import("element.zig").Union; + +const DocumentType = @import("document_type.zig").DocumentType; +const DocumentFragment = @import("document_fragment.zig").DocumentFragment; +const DOMImplementation = @import("implementation.zig").DOMImplementation; + +// WEB IDL https://dom.spec.whatwg.org/#document +pub const Document = struct { + pub const Self = parser.Document; + pub const prototype = *Node; + + pub fn constructor(state: *const SessionState) !*parser.DocumentHTML { + const doc = try parser.documentCreateDocument( + try parser.documentHTMLGetTitle(state.document.?), + ); + + // we have to work w/ document instead of html document. + const ddoc = parser.documentHTMLToDocument(doc); + const ccur = parser.documentHTMLToDocument(state.document.?); + try parser.documentSetDocumentURI(ddoc, try parser.documentGetDocumentURI(ccur)); + try parser.documentSetInputEncoding(ddoc, try parser.documentGetInputEncoding(ccur)); + + return doc; + } + + // JS funcs + // -------- + pub fn get_implementation(_: *parser.Document) DOMImplementation { + return DOMImplementation{}; + } + + pub fn get_documentElement(self: *parser.Document) !?ElementUnion { + const e = try parser.documentGetDocumentElement(self); + if (e == null) return null; + return try Element.toInterface(e.?); + } + + pub fn get_documentURI(self: *parser.Document) ![]const u8 { + return try parser.documentGetDocumentURI(self); + } + + pub fn get_URL(self: *parser.Document) ![]const u8 { + return try get_documentURI(self); + } + + // TODO implement contentType + pub fn get_contentType(self: *parser.Document) []const u8 { + _ = self; + return "text/html"; + } + + // TODO implement compactMode + pub fn get_compatMode(self: *parser.Document) []const u8 { + _ = self; + return "CSS1Compat"; + } + + pub fn get_characterSet(self: *parser.Document) ![]const u8 { + return try parser.documentGetInputEncoding(self); + } + + // alias of get_characterSet + pub fn get_charset(self: *parser.Document) ![]const u8 { + return try get_characterSet(self); + } + + // alias of get_characterSet + pub fn get_inputEncoding(self: *parser.Document) ![]const u8 { + return try get_characterSet(self); + } + + pub fn get_doctype(self: *parser.Document) !?*parser.DocumentType { + return try parser.documentGetDoctype(self); + } + + pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !*parser.Event { + // TODO: for now only "Event" constructor is supported + // see table on https://dom.spec.whatwg.org/#dom-document-createevent $2 + if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) { + return try parser.eventCreate(); + } + return parser.DOMError.NotSupported; + } + + pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion { + const e = try parser.documentGetElementById(self, id) orelse return null; + return try Element.toInterface(e); + } + + pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion { + const e = try parser.documentCreateElement(self, tag_name); + return try Element.toInterface(e); + } + + pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion { + const e = try parser.documentCreateElementNS(self, ns, tag_name); + return try Element.toInterface(e); + } + + // We can't simply use libdom dom_document_get_elements_by_tag_name here. + // Indeed, netsurf implemented a previous dom spec when + // getElementsByTagName returned a NodeList. + // But since + // https://github.com/whatwg/dom/commit/190700b7c12ecfd3b5ebdb359ab1d6ea9cbf7749 + // the spec changed to return an HTMLCollection instead. + // That's why we reimplemented getElementsByTagName by using an + // HTMLCollection in zig here. + pub fn _getElementsByTagName( + self: *parser.Document, + tag_name: []const u8, + state: *SessionState, + ) !collection.HTMLCollection { + return try collection.HTMLCollectionByTagName(state.arena, parser.documentToNode(self), tag_name, true); + } + + pub fn _getElementsByClassName( + self: *parser.Document, + classNames: []const u8, + state: *SessionState, + ) !collection.HTMLCollection { + const allocator = state.arena; + return try collection.HTMLCollectionByClassName(allocator, parser.documentToNode(self), classNames, true); + } + + pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment { + return try parser.documentCreateDocumentFragment(self); + } + + pub fn _createTextNode(self: *parser.Document, data: []const u8) !*parser.Text { + return try parser.documentCreateTextNode(self, data); + } + + pub fn _createCDATASection(self: *parser.Document, data: []const u8) !*parser.CDATASection { + return try parser.documentCreateCDATASection(self, data); + } + + pub fn _createComment(self: *parser.Document, data: []const u8) !*parser.Comment { + return try parser.documentCreateComment(self, data); + } + + pub fn _createProcessingInstruction(self: *parser.Document, target: []const u8, data: []const u8) !*parser.ProcessingInstruction { + return try parser.documentCreateProcessingInstruction(self, target, data); + } + + pub fn _importNode(self: *parser.Document, node: *parser.Node, deep: ?bool) !NodeUnion { + const n = try parser.documentImportNode(self, node, deep orelse false); + return try Node.toInterface(n); + } + + pub fn _adoptNode(self: *parser.Document, node: *parser.Node) !NodeUnion { + const n = try parser.documentAdoptNode(self, node); + return try Node.toInterface(n); + } + + pub fn _createAttribute(self: *parser.Document, name: []const u8) !*parser.Attribute { + return try parser.documentCreateAttribute(self, name); + } + + pub fn _createAttributeNS(self: *parser.Document, ns: []const u8, qname: []const u8) !*parser.Attribute { + return try parser.documentCreateAttributeNS(self, ns, qname); + } + + // ParentNode + // https://dom.spec.whatwg.org/#parentnode + pub fn get_children(self: *parser.Document) !collection.HTMLCollection { + return try collection.HTMLCollectionChildren(parser.documentToNode(self), false); + } + + pub fn get_firstElementChild(self: *parser.Document) !?ElementUnion { + const elt = try parser.documentGetDocumentElement(self) orelse return null; + return try Element.toInterface(elt); + } + + pub fn get_lastElementChild(self: *parser.Document) !?ElementUnion { + const elt = try parser.documentGetDocumentElement(self) orelse return null; + return try Element.toInterface(elt); + } + + pub fn get_childElementCount(self: *parser.Document) !u32 { + _ = try parser.documentGetDocumentElement(self) orelse return 0; + return 1; + } + + pub fn _querySelector(self: *parser.Document, selector: []const u8, state: *SessionState) !?ElementUnion { + if (selector.len == 0) return null; + + const allocator = state.arena; + const n = try css.querySelector(allocator, parser.documentToNode(self), selector); + + if (n == null) return null; + + return try Element.toInterface(parser.nodeToElement(n.?)); + } + + pub fn _querySelectorAll(self: *parser.Document, selector: []const u8, state: *SessionState) !NodeList { + const allocator = state.arena; + return css.querySelectorAll(allocator, parser.documentToNode(self), selector); + } + + // TODO according with https://dom.spec.whatwg.org/#parentnode, the + // function must accept either node or string. + // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 + pub fn _prepend(self: *parser.Document, nodes: []const *parser.Node) !void { + return Node.prepend(parser.documentToNode(self), nodes); + } + + // TODO according with https://dom.spec.whatwg.org/#parentnode, the + // function must accept either node or string. + // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 + pub fn _append(self: *parser.Document, nodes: []const *parser.Node) !void { + return Node.append(parser.documentToNode(self), nodes); + } + + // TODO according with https://dom.spec.whatwg.org/#parentnode, the + // function must accept either node or string. + // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 + pub fn _replaceChildren(self: *parser.Document, nodes: []const *parser.Node) !void { + return Node.replaceChildren(parser.documentToNode(self), nodes); + } + + pub fn deinit(_: *parser.Document, _: std.mem.Allocator) void {} +}; + +const testing = @import("../../testing.zig"); +test "Browser.DOM.Document" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "document.__proto__.__proto__.constructor.name", "Document" }, + .{ "document.__proto__.__proto__.__proto__.constructor.name", "Node" }, + .{ "document.__proto__.__proto__.__proto__.__proto__.constructor.name", "EventTarget" }, + + .{ "let newdoc = new Document()", "undefined" }, + .{ "newdoc.documentElement", "null" }, + .{ "newdoc.children.length", "0" }, + .{ "newdoc.getElementsByTagName('*').length", "0" }, + .{ "newdoc.getElementsByTagName('*').item(0)", "null" }, + .{ "newdoc.inputEncoding === document.inputEncoding", "true" }, + .{ "newdoc.documentURI === document.documentURI", "true" }, + .{ "newdoc.URL === document.URL", "true" }, + .{ "newdoc.compatMode === document.compatMode", "true" }, + .{ "newdoc.characterSet === document.characterSet", "true" }, + .{ "newdoc.charset === document.charset", "true" }, + .{ "newdoc.contentType === document.contentType", "true" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let getElementById = document.getElementById('content')", "undefined" }, + .{ "getElementById.constructor.name", "HTMLDivElement" }, + .{ "getElementById.localName", "div" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" }, + .{ "getElementsByTagName.length", "2" }, + .{ "getElementsByTagName.item(0).localName", "p" }, + .{ "getElementsByTagName.item(1).localName", "p" }, + .{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" }, + .{ "getElementsByTagNameAll.length", "8" }, + .{ "getElementsByTagNameAll.item(0).localName", "html" }, + .{ "getElementsByTagNameAll.item(7).localName", "p" }, + .{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let ok = document.getElementsByClassName('ok')", "undefined" }, + .{ "ok.length", "2" }, + .{ "let empty = document.getElementsByClassName('empty')", "undefined" }, + .{ "empty.length", "1" }, + .{ "let emptyok = document.getElementsByClassName('empty ok')", "undefined" }, + .{ "emptyok.length", "1" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let e = document.documentElement", "undefined" }, + .{ "e.localName", "html" }, + }, .{}); + + try runner.testCases(&.{ + .{ "document.characterSet", "UTF-8" }, + .{ "document.charset", "UTF-8" }, + .{ "document.inputEncoding", "UTF-8" }, + }, .{}); + + try runner.testCases(&.{ + .{ "document.compatMode", "CSS1Compat" }, + }, .{}); + + try runner.testCases(&.{ + .{ "document.contentType", "text/html" }, + }, .{}); + + try runner.testCases(&.{ + .{ "document.documentURI", "about:blank" }, + .{ "document.URL", "about:blank" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let impl = document.implementation", "undefined" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let d = new Document()", "undefined" }, + .{ "d.characterSet", "UTF-8" }, + .{ "d.URL", "about:blank" }, + .{ "d.documentURI", "about:blank" }, + .{ "d.compatMode", "CSS1Compat" }, + .{ "d.contentType", "text/html" }, + }, .{}); + + try runner.testCases(&.{ + .{ "var v = document.createDocumentFragment()", "undefined" }, + .{ "v.nodeName", "#document-fragment" }, + }, .{}); + + try runner.testCases(&.{ + .{ "var v = document.createTextNode('foo')", "undefined" }, + .{ "v.nodeName", "#text" }, + }, .{}); + + try runner.testCases(&.{ + .{ "var v = document.createCDATASection('foo')", "undefined" }, + .{ "v.nodeName", "#cdata-section" }, + }, .{}); + + try runner.testCases(&.{ + .{ "var v = document.createComment('foo')", "undefined" }, + .{ "v.nodeName", "#comment" }, + .{ "let v2 = v.cloneNode()", "undefined" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" }, + .{ "pi.target", "foo" }, + .{ "let pi2 = pi.cloneNode()", "undefined" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let nimp = document.getElementById('content')", "undefined" }, + .{ "var v = document.importNode(nimp)", "undefined" }, + .{ "v.nodeName", "DIV" }, + }, .{}); + + try runner.testCases(&.{ + .{ "var v = document.createAttribute('foo')", "undefined" }, + .{ "v.nodeName", "foo" }, + }, .{}); + + try runner.testCases(&.{ + .{ "document.children.length", "1" }, + .{ "document.children.item(0).nodeName", "HTML" }, + .{ "document.firstElementChild.nodeName", "HTML" }, + .{ "document.lastElementChild.nodeName", "HTML" }, + .{ "document.childElementCount", "1" }, + + .{ "let nd = new Document()", "undefined" }, + .{ "nd.children.length", "0" }, + .{ "nd.children.item(0)", "null" }, + .{ "nd.firstElementChild", "null" }, + .{ "nd.lastElementChild", "null" }, + .{ "nd.childElementCount", "0" }, + + .{ "let emptydoc = document.createElement('html')", "undefined" }, + .{ "emptydoc.prepend(document.createElement('html'))", "undefined" }, + + .{ "let emptydoc2 = document.createElement('html')", "undefined" }, + .{ "emptydoc2.append(document.createElement('html'))", "undefined" }, + }, .{}); + + try runner.testCases(&.{ + .{ "document.querySelector('')", "null" }, + .{ "document.querySelector('*').nodeName", "HTML" }, + .{ "document.querySelector('#content').id", "content" }, + .{ "document.querySelector('#para').id", "para" }, + .{ "document.querySelector('.ok').id", "link" }, + .{ "document.querySelector('a ~ p').id", "para-empty" }, + .{ "document.querySelector(':root').nodeName", "HTML" }, + + .{ "document.querySelectorAll('p').length", "2" }, + .{ + \\ Array.from(document.querySelectorAll('#content > p#para-empty')) + \\ .map(row => row.querySelector('span').textContent) + \\ .length; + , + "1", + }, + }, .{}); + + // this test breaks the doc structure, keep it at the end of the test + // suite. + try runner.testCases(&.{ + .{ "let nadop = document.getElementById('content')", "undefined" }, + .{ "var v = document.adoptNode(nadop)", "undefined" }, + .{ "v.nodeName", "DIV" }, + }, .{}); + + const Case = testing.JsRunner.Case; + const tags = comptime parser.Tag.all(); + var createElements: [(tags.len) * 2]Case = undefined; + inline for (tags, 0..) |tag, i| { + const tag_name = @tagName(tag); + createElements[i * 2] = Case{ + "var " ++ tag_name ++ "Elem = document.createElement('" ++ tag_name ++ "')", + "undefined", + }; + createElements[(i * 2) + 1] = Case{ + tag_name ++ "Elem.localName", + tag_name, + }; + } + try runner.testCases(&createElements, .{}); +} diff --git a/src/dom/document_fragment.zig b/src/browser/dom/document_fragment.zig similarity index 60% rename from src/dom/document_fragment.zig rename to src/browser/dom/document_fragment.zig index 574e8eb1..2ae1a8a5 100644 --- a/src/dom/document_fragment.zig +++ b/src/browser/dom/document_fragment.zig @@ -18,39 +18,30 @@ const std = @import("std"); -const parser = @import("netsurf"); - -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; +const parser = @import("../netsurf.zig"); +const SessionState = @import("../env.zig").SessionState; const Node = @import("node.zig").Node; -const UserContext = @import("../user_context.zig").UserContext; - // WEB IDL https://dom.spec.whatwg.org/#documentfragment pub const DocumentFragment = struct { pub const Self = parser.DocumentFragment; pub const prototype = *Node; - pub const mem_guarantied = true; - pub fn constructor(userctx: UserContext) !*parser.DocumentFragment { + pub fn constructor(state: *const SessionState) !*parser.DocumentFragment { return parser.documentCreateDocumentFragment( - parser.documentHTMLToDocument(userctx.document), + parser.documentHTMLToDocument(state.document.?), ); } }; -// Tests -// ----- +const testing = @import("../../testing.zig"); +test "Browser.DOM.DocumentFragment" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var constructor = [_]Case{ - .{ .src = "const dc = new DocumentFragment()", .ex = "undefined" }, - .{ .src = "dc.constructor.name", .ex = "DocumentFragment" }, - }; - try checkCases(js_env, &constructor); + try runner.testCases(&.{ + .{ "const dc = new DocumentFragment()", "undefined" }, + .{ "dc.constructor.name", "DocumentFragment" }, + }, .{}); } diff --git a/src/dom/document_type.zig b/src/browser/dom/document_type.zig similarity index 95% rename from src/dom/document_type.zig rename to src/browser/dom/document_type.zig index cd40a732..0749b8bc 100644 --- a/src/dom/document_type.zig +++ b/src/browser/dom/document_type.zig @@ -18,7 +18,7 @@ const std = @import("std"); -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); const Node = @import("node.zig").Node; @@ -26,7 +26,6 @@ const Node = @import("node.zig").Node; pub const DocumentType = struct { pub const Self = parser.DocumentType; pub const prototype = *Node; - pub const mem_guarantied = true; pub fn get_name(self: *parser.DocumentType) ![]const u8 { return try parser.documentTypeGetName(self); diff --git a/src/dom/dom.zig b/src/browser/dom/dom.zig similarity index 95% rename from src/dom/dom.zig rename to src/browser/dom/dom.zig index 76a4a185..07e57c06 100644 --- a/src/dom/dom.zig +++ b/src/browser/dom/dom.zig @@ -22,7 +22,7 @@ const DOMImplementation = @import("implementation.zig").DOMImplementation; const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap; const DOMTokenList = @import("token_list.zig").DOMTokenList; const NodeList = @import("nodelist.zig"); -const Nod = @import("node.zig"); +const Node = @import("node.zig"); const MutationObserver = @import("mutation_observer.zig"); pub const Interfaces = .{ @@ -32,7 +32,7 @@ pub const Interfaces = .{ NamedNodeMap, DOMTokenList, NodeList.Interfaces, - Nod.Node, - Nod.Interfaces, + Node.Node, + Node.Interfaces, MutationObserver.Interfaces, }; diff --git a/src/dom/element.zig b/src/browser/dom/element.zig similarity index 54% rename from src/dom/element.zig rename to src/browser/dom/element.zig index 0453a283..ffd937e9 100644 --- a/src/dom/element.zig +++ b/src/browser/dom/element.zig @@ -18,22 +18,17 @@ const std = @import("std"); -const parser = @import("netsurf"); - -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; -const Variadic = jsruntime.Variadic; +const parser = @import("../netsurf.zig"); +const SessionState = @import("../env.zig").SessionState; const collection = @import("html_collection.zig"); -const dump = @import("../browser/dump.zig"); +const dump = @import("../dump.zig"); const css = @import("css.zig"); const Node = @import("node.zig").Node; const Walker = @import("walker.zig").WalkerDepthFirst; const NodeList = @import("nodelist.zig").NodeList; const HTMLElem = @import("../html/elements.zig"); -const UserContext = @import("../user_context.zig").UserContext; pub const Union = @import("../html/elements.zig").Union; const DOMException = @import("exceptions.zig").DOMException; @@ -42,7 +37,6 @@ const DOMException = @import("exceptions.zig").DOMException; pub const Element = struct { pub const Self = parser.Element; pub const prototype = *Node; - pub const mem_guarantied = true; pub const DOMRect = struct { x: f64, @@ -106,8 +100,8 @@ pub const Element = struct { return try parser.nodeGetAttributes(parser.elementToNode(self)); } - pub fn get_innerHTML(self: *parser.Element, alloc: std.mem.Allocator) ![]const u8 { - var buf = std.ArrayList(u8).init(alloc); + pub fn get_innerHTML(self: *parser.Element, state: *SessionState) ![]const u8 { + var buf = std.ArrayList(u8).init(state.arena); defer buf.deinit(); try dump.writeChildren(parser.elementToNode(self), buf.writer()); @@ -116,8 +110,8 @@ pub const Element = struct { return buf.toOwnedSlice(); } - pub fn get_outerHTML(self: *parser.Element, alloc: std.mem.Allocator) ![]const u8 { - var buf = std.ArrayList(u8).init(alloc); + pub fn get_outerHTML(self: *parser.Element, state: *SessionState) ![]const u8 { + var buf = std.ArrayList(u8).init(state.arena); defer buf.deinit(); try dump.writeNode(parser.elementToNode(self), buf.writer()); @@ -232,11 +226,11 @@ pub const Element = struct { pub fn _getElementsByTagName( self: *parser.Element, - alloc: std.mem.Allocator, tag_name: []const u8, + state: *SessionState, ) !collection.HTMLCollection { return try collection.HTMLCollectionByTagName( - alloc, + state.arena, parser.elementToNode(self), tag_name, false, @@ -245,11 +239,11 @@ pub const Element = struct { pub fn _getElementsByClassName( self: *parser.Element, - alloc: std.mem.Allocator, classNames: []const u8, + state: *SessionState, ) !collection.HTMLCollection { return try collection.HTMLCollectionByClassName( - alloc, + state.arena, parser.elementToNode(self), classNames, false, @@ -312,51 +306,51 @@ pub const Element = struct { } } - pub fn _querySelector(self: *parser.Element, alloc: std.mem.Allocator, selector: []const u8) !?Union { + pub fn _querySelector(self: *parser.Element, selector: []const u8, state: *SessionState) !?Union { if (selector.len == 0) return null; - const n = try css.querySelector(alloc, parser.elementToNode(self), selector); + const n = try css.querySelector(state.arena, parser.elementToNode(self), selector); if (n == null) return null; return try toInterface(parser.nodeToElement(n.?)); } - pub fn _querySelectorAll(self: *parser.Element, alloc: std.mem.Allocator, selector: []const u8) !NodeList { - return css.querySelectorAll(alloc, parser.elementToNode(self), selector); + pub fn _querySelectorAll(self: *parser.Element, selector: []const u8, state: *SessionState) !NodeList { + return css.querySelectorAll(state.arena, parser.elementToNode(self), selector); } // TODO according with https://dom.spec.whatwg.org/#parentnode, the // function must accept either node or string. // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn _prepend(self: *parser.Element, nodes: ?Variadic(*parser.Node)) !void { + pub fn _prepend(self: *parser.Element, nodes: []const *parser.Node) !void { return Node.prepend(parser.elementToNode(self), nodes); } // TODO according with https://dom.spec.whatwg.org/#parentnode, the // function must accept either node or string. // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn _append(self: *parser.Element, nodes: ?Variadic(*parser.Node)) !void { + pub fn _append(self: *parser.Element, nodes: []const *parser.Node) !void { return Node.append(parser.elementToNode(self), nodes); } // TODO according with https://dom.spec.whatwg.org/#parentnode, the // function must accept either node or string. // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn _replaceChildren(self: *parser.Element, nodes: ?Variadic(*parser.Node)) !void { + pub fn _replaceChildren(self: *parser.Element, nodes: []const *parser.Node) !void { return Node.replaceChildren(parser.elementToNode(self), nodes); } - pub fn _getBoundingClientRect(self: *parser.Element, user_context: UserContext) !DOMRect { - return user_context.renderer.getRect(self); + pub fn _getBoundingClientRect(self: *parser.Element, state: *SessionState) !DOMRect { + return state.renderer.getRect(self); } - pub fn get_clientWidth(_: *parser.Element, user_context: UserContext) u32 { - return user_context.renderer.width(); + pub fn get_clientWidth(_: *parser.Element, state: *SessionState) u32 { + return state.renderer.width(); } - pub fn get_clientHeight(_: *parser.Element, user_context: UserContext) u32 { - return user_context.renderer.height(); + pub fn get_clientHeight(_: *parser.Element, state: *SessionState) u32 { + return state.renderer.height(); } pub fn deinit(_: *parser.Element, _: std.mem.Allocator) void {} @@ -365,172 +359,161 @@ pub const Element = struct { // Tests // ----- -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var getters = [_]Case{ - .{ .src = "let g = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "g.namespaceURI", .ex = "http://www.w3.org/1999/xhtml" }, - .{ .src = "g.prefix", .ex = "null" }, - .{ .src = "g.localName", .ex = "div" }, - .{ .src = "g.tagName", .ex = "DIV" }, - }; - try checkCases(js_env, &getters); +const testing = @import("../../testing.zig"); +test "Browser.DOM.Element" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); - var gettersetters = [_]Case{ - .{ .src = "let gs = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "gs.id", .ex = "content" }, - .{ .src = "gs.id = 'foo'", .ex = "foo" }, - .{ .src = "gs.id", .ex = "foo" }, - .{ .src = "gs.id = 'content'", .ex = "content" }, - .{ .src = "gs.className", .ex = "" }, - .{ .src = "let gs2 = document.getElementById('para-empty')", .ex = "undefined" }, - .{ .src = "gs2.className", .ex = "ok empty" }, - .{ .src = "gs2.className = 'foo bar baz'", .ex = "foo bar baz" }, - .{ .src = "gs2.className", .ex = "foo bar baz" }, - .{ .src = "gs2.className = 'ok empty'", .ex = "ok empty" }, - .{ .src = "let cl = gs2.classList", .ex = "undefined" }, - .{ .src = "cl.length", .ex = "2" }, - }; - try checkCases(js_env, &gettersetters); + try runner.testCases(&.{ + .{ "let g = document.getElementById('content')", "undefined" }, + .{ "g.namespaceURI", "http://www.w3.org/1999/xhtml" }, + .{ "g.prefix", "null" }, + .{ "g.localName", "div" }, + .{ "g.tagName", "DIV" }, + }, .{}); - var attribute = [_]Case{ - .{ .src = "let a = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "a.hasAttributes()", .ex = "true" }, - .{ .src = "a.attributes.length", .ex = "1" }, + try runner.testCases(&.{ + .{ "let gs = document.getElementById('content')", "undefined" }, + .{ "gs.id", "content" }, + .{ "gs.id = 'foo'", "foo" }, + .{ "gs.id", "foo" }, + .{ "gs.id = 'content'", "content" }, + .{ "gs.className", "" }, + .{ "let gs2 = document.getElementById('para-empty')", "undefined" }, + .{ "gs2.className", "ok empty" }, + .{ "gs2.className = 'foo bar baz'", "foo bar baz" }, + .{ "gs2.className", "foo bar baz" }, + .{ "gs2.className = 'ok empty'", "ok empty" }, + .{ "let cl = gs2.classList", "undefined" }, + .{ "cl.length", "2" }, + }, .{}); - .{ .src = "a.getAttribute('id')", .ex = "content" }, + try runner.testCases(&.{ + .{ "let a = document.getElementById('content')", "undefined" }, + .{ "a.hasAttributes()", "true" }, + .{ "a.attributes.length", "1" }, - .{ .src = "a.hasAttribute('foo')", .ex = "false" }, - .{ .src = "a.getAttribute('foo')", .ex = "null" }, + .{ "a.getAttribute('id')", "content" }, - .{ .src = "a.setAttribute('foo', 'bar')", .ex = "undefined" }, - .{ .src = "a.hasAttribute('foo')", .ex = "true" }, - .{ .src = "a.getAttribute('foo')", .ex = "bar" }, + .{ "a.hasAttribute('foo')", "false" }, + .{ "a.getAttribute('foo')", "null" }, - .{ .src = "a.setAttribute('foo', 'baz')", .ex = "undefined" }, - .{ .src = "a.hasAttribute('foo')", .ex = "true" }, - .{ .src = "a.getAttribute('foo')", .ex = "baz" }, + .{ "a.setAttribute('foo', 'bar')", "undefined" }, + .{ "a.hasAttribute('foo')", "true" }, + .{ "a.getAttribute('foo')", "bar" }, - .{ .src = "a.removeAttribute('foo')", .ex = "undefined" }, - .{ .src = "a.hasAttribute('foo')", .ex = "false" }, - .{ .src = "a.getAttribute('foo')", .ex = "null" }, - }; - try checkCases(js_env, &attribute); + .{ "a.setAttribute('foo', 'baz')", "undefined" }, + .{ "a.hasAttribute('foo')", "true" }, + .{ "a.getAttribute('foo')", "baz" }, - var toggleAttr = [_]Case{ - .{ .src = "let b = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "b.toggleAttribute('foo')", .ex = "true" }, - .{ .src = "b.hasAttribute('foo')", .ex = "true" }, - .{ .src = "b.getAttribute('foo')", .ex = "" }, + .{ "a.removeAttribute('foo')", "undefined" }, + .{ "a.hasAttribute('foo')", "false" }, + .{ "a.getAttribute('foo')", "null" }, + }, .{}); - .{ .src = "b.toggleAttribute('foo')", .ex = "false" }, - .{ .src = "b.hasAttribute('foo')", .ex = "false" }, - }; - try checkCases(js_env, &toggleAttr); + try runner.testCases(&.{ + .{ "let b = document.getElementById('content')", "undefined" }, + .{ "b.toggleAttribute('foo')", "true" }, + .{ "b.hasAttribute('foo')", "true" }, + .{ "b.getAttribute('foo')", "" }, - var parentNode = [_]Case{ - .{ .src = "let c = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "c.children.length", .ex = "3" }, - .{ .src = "c.firstElementChild.nodeName", .ex = "A" }, - .{ .src = "c.lastElementChild.nodeName", .ex = "P" }, - .{ .src = "c.childElementCount", .ex = "3" }, + .{ "b.toggleAttribute('foo')", "false" }, + .{ "b.hasAttribute('foo')", "false" }, + }, .{}); - .{ .src = "c.prepend(document.createTextNode('foo'))", .ex = "undefined" }, - .{ .src = "c.append(document.createTextNode('bar'))", .ex = "undefined" }, - }; - try checkCases(js_env, &parentNode); + try runner.testCases(&.{ + .{ "let c = document.getElementById('content')", "undefined" }, + .{ "c.children.length", "3" }, + .{ "c.firstElementChild.nodeName", "A" }, + .{ "c.lastElementChild.nodeName", "P" }, + .{ "c.childElementCount", "3" }, - var elementSibling = [_]Case{ - .{ .src = "let d = document.getElementById('para')", .ex = "undefined" }, - .{ .src = "d.previousElementSibling.nodeName", .ex = "P" }, - .{ .src = "d.nextElementSibling", .ex = "null" }, - }; - try checkCases(js_env, &elementSibling); + .{ "c.prepend(document.createTextNode('foo'))", "undefined" }, + .{ "c.append(document.createTextNode('bar'))", "undefined" }, + }, .{}); - var querySelector = [_]Case{ - .{ .src = "let e = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "e.querySelector('foo')", .ex = "null" }, - .{ .src = "e.querySelector('#foo')", .ex = "null" }, - .{ .src = "e.querySelector('#link').id", .ex = "link" }, - .{ .src = "e.querySelector('#para').id", .ex = "para" }, - .{ .src = "e.querySelector('*').id", .ex = "link" }, - .{ .src = "e.querySelector('')", .ex = "null" }, - .{ .src = "e.querySelector('*').id", .ex = "link" }, - .{ .src = "e.querySelector('#content')", .ex = "null" }, - .{ .src = "e.querySelector('#para').id", .ex = "para" }, - .{ .src = "e.querySelector('.ok').id", .ex = "link" }, - .{ .src = "e.querySelector('a ~ p').id", .ex = "para-empty" }, + try runner.testCases(&.{ + .{ "let d = document.getElementById('para')", "undefined" }, + .{ "d.previousElementSibling.nodeName", "P" }, + .{ "d.nextElementSibling", "null" }, + }, .{}); - .{ .src = "e.querySelectorAll('foo').length", .ex = "0" }, - .{ .src = "e.querySelectorAll('#foo').length", .ex = "0" }, - .{ .src = "e.querySelectorAll('#link').length", .ex = "1" }, - .{ .src = "e.querySelectorAll('#link').item(0).id", .ex = "link" }, - .{ .src = "e.querySelectorAll('#para').length", .ex = "1" }, - .{ .src = "e.querySelectorAll('#para').item(0).id", .ex = "para" }, - .{ .src = "e.querySelectorAll('*').length", .ex = "4" }, - .{ .src = "e.querySelectorAll('p').length", .ex = "2" }, - .{ .src = "e.querySelectorAll('.ok').item(0).id", .ex = "link" }, - }; - try checkCases(js_env, &querySelector); + try runner.testCases(&.{ + .{ "let e = document.getElementById('content')", "undefined" }, + .{ "e.querySelector('foo')", "null" }, + .{ "e.querySelector('#foo')", "null" }, + .{ "e.querySelector('#link').id", "link" }, + .{ "e.querySelector('#para').id", "para" }, + .{ "e.querySelector('*').id", "link" }, + .{ "e.querySelector('')", "null" }, + .{ "e.querySelector('*').id", "link" }, + .{ "e.querySelector('#content')", "null" }, + .{ "e.querySelector('#para').id", "para" }, + .{ "e.querySelector('.ok').id", "link" }, + .{ "e.querySelector('a ~ p').id", "para-empty" }, - var attrNode = [_]Case{ - .{ .src = "let f = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "let ff = document.createAttribute('foo')", .ex = "undefined" }, - .{ .src = "f.setAttributeNode(ff)", .ex = "null" }, - .{ .src = "f.getAttributeNode('foo').name", .ex = "foo" }, - .{ .src = "f.removeAttributeNode(ff).name", .ex = "foo" }, - .{ .src = "f.getAttributeNode('bar')", .ex = "null" }, - }; - try checkCases(js_env, &attrNode); + .{ "e.querySelectorAll('foo').length", "0" }, + .{ "e.querySelectorAll('#foo').length", "0" }, + .{ "e.querySelectorAll('#link').length", "1" }, + .{ "e.querySelectorAll('#link').item(0).id", "link" }, + .{ "e.querySelectorAll('#para').length", "1" }, + .{ "e.querySelectorAll('#para').item(0).id", "para" }, + .{ "e.querySelectorAll('*').length", "4" }, + .{ "e.querySelectorAll('p').length", "2" }, + .{ "e.querySelectorAll('.ok').item(0).id", "link" }, + }, .{}); - var innerHTML = [_]Case{ - .{ .src = "document.getElementById('para').innerHTML", .ex = " And" }, - .{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "" }, + try runner.testCases(&.{ + .{ "let f = document.getElementById('content')", "undefined" }, + .{ "let ff = document.createAttribute('foo')", "undefined" }, + .{ "f.setAttributeNode(ff)", "null" }, + .{ "f.getAttributeNode('foo').name", "foo" }, + .{ "f.removeAttributeNode(ff).name", "foo" }, + .{ "f.getAttributeNode('bar')", "null" }, + }, .{}); - .{ .src = "let h = document.getElementById('para-empty')", .ex = "undefined" }, - .{ .src = "const prev = h.innerHTML", .ex = "undefined" }, - .{ .src = "h.innerHTML = '

hello world

'", .ex = "

hello world

" }, - .{ .src = "h.innerHTML", .ex = "

hello world

" }, - .{ .src = "h.firstChild.nodeName", .ex = "P" }, - .{ .src = "h.firstChild.id", .ex = "hello" }, - .{ .src = "h.firstChild.textContent", .ex = "hello world" }, - .{ .src = "h.innerHTML = prev; true", .ex = "true" }, - .{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "" }, - }; - try checkCases(js_env, &innerHTML); + try runner.testCases(&.{ + .{ "document.getElementById('para').innerHTML", " And" }, + .{ "document.getElementById('para-empty').innerHTML.trim()", "" }, - var outerHTML = [_]Case{ - .{ .src = "document.getElementById('para').outerHTML", .ex = "

And

" }, - }; + .{ "let h = document.getElementById('para-empty')", "undefined" }, + .{ "const prev = h.innerHTML", "undefined" }, + .{ "h.innerHTML = '

hello world

'", "

hello world

" }, + .{ "h.innerHTML", "

hello world

" }, + .{ "h.firstChild.nodeName", "P" }, + .{ "h.firstChild.id", "hello" }, + .{ "h.firstChild.textContent", "hello world" }, + .{ "h.innerHTML = prev; true", "true" }, + .{ "document.getElementById('para-empty').innerHTML.trim()", "" }, + }, .{}); - var getBoundingClientRect = [_]Case{ - .{ .src = "document.getElementById('para').clientWidth", .ex = "0" }, - .{ .src = "document.getElementById('para').clientHeight", .ex = "1" }, + try runner.testCases(&.{ + .{ "document.getElementById('para').outerHTML", "

And

" }, + }, .{}); - .{ .src = "let r1 = document.getElementById('para').getBoundingClientRect()", .ex = "undefined" }, - .{ .src = "r1.x", .ex = "1" }, - .{ .src = "r1.y", .ex = "0" }, - .{ .src = "r1.width", .ex = "1" }, - .{ .src = "r1.height", .ex = "1" }, + try runner.testCases(&.{ + .{ "document.getElementById('para').clientWidth", "0" }, + .{ "document.getElementById('para').clientHeight", "1" }, - .{ .src = "let r2 = document.getElementById('content').getBoundingClientRect()", .ex = "undefined" }, - .{ .src = "r2.x", .ex = "2" }, - .{ .src = "r2.y", .ex = "0" }, - .{ .src = "r2.width", .ex = "1" }, - .{ .src = "r2.height", .ex = "1" }, + .{ "let r1 = document.getElementById('para').getBoundingClientRect()", "undefined" }, + .{ "r1.x", "1" }, + .{ "r1.y", "0" }, + .{ "r1.width", "1" }, + .{ "r1.height", "1" }, - .{ .src = "let r3 = document.getElementById('para').getBoundingClientRect()", .ex = "undefined" }, - .{ .src = "r3.x", .ex = "1" }, - .{ .src = "r3.y", .ex = "0" }, - .{ .src = "r3.width", .ex = "1" }, - .{ .src = "r3.height", .ex = "1" }, + .{ "let r2 = document.getElementById('content').getBoundingClientRect()", "undefined" }, + .{ "r2.x", "2" }, + .{ "r2.y", "0" }, + .{ "r2.width", "1" }, + .{ "r2.height", "1" }, - .{ .src = "document.getElementById('para').clientWidth", .ex = "2" }, - .{ .src = "document.getElementById('para').clientHeight", .ex = "1" }, - }; - try checkCases(js_env, &getBoundingClientRect); + .{ "let r3 = document.getElementById('para').getBoundingClientRect()", "undefined" }, + .{ "r3.x", "1" }, + .{ "r3.y", "0" }, + .{ "r3.width", "1" }, + .{ "r3.height", "1" }, - try checkCases(js_env, &outerHTML); + .{ "document.getElementById('para').clientWidth", "2" }, + .{ "document.getElementById('para').clientHeight", "1" }, + }, .{}); } diff --git a/src/browser/dom/event_target.zig b/src/browser/dom/event_target.zig new file mode 100644 index 00000000..959a35ca --- /dev/null +++ b/src/browser/dom/event_target.zig @@ -0,0 +1,226 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const Env = @import("../env.zig").Env; +const parser = @import("../netsurf.zig"); +const SessionState = @import("../env.zig").SessionState; + +const EventHandler = @import("../events/event.zig").EventHandler; + +const DOMException = @import("exceptions.zig").DOMException; +const Nod = @import("node.zig"); + +// EventTarget interfaces +pub const Union = Nod.Union; + +// EventTarget implementation +pub const EventTarget = struct { + pub const Self = parser.EventTarget; + pub const Exception = DOMException; + + pub fn toInterface(et: *parser.EventTarget) !Union { + // NOTE: for now we state that all EventTarget are Nodes + // TODO: handle other types (eg. Window) + return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))); + } + + // JS funcs + // -------- + + pub fn _addEventListener( + self: *parser.EventTarget, + eventType: []const u8, + cbk: Env.Callback, + capture: ?bool, + state: *SessionState, + // TODO: hanle EventListenerOptions + // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114 + ) !void { + // check if event target has already this listener + const lst = try parser.eventTargetHasListener( + self, + eventType, + capture orelse false, + cbk.id, + ); + if (lst != null) { + return; + } + + try parser.eventTargetAddEventListener( + self, + state.arena, + eventType, + EventHandler, + .{ .cbk = cbk }, + capture orelse false, + ); + } + + pub fn _removeEventListener( + self: *parser.EventTarget, + eventType: []const u8, + cbk: Env.Callback, + capture: ?bool, + state: *SessionState, + // TODO: hanle EventListenerOptions + // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114 + ) !void { + // check if event target has already this listener + const lst = try parser.eventTargetHasListener( + self, + eventType, + capture orelse false, + cbk.id, + ); + if (lst == null) { + return; + } + + // remove listener + try parser.eventTargetRemoveEventListener( + self, + state.arena, + eventType, + lst.?, + capture orelse false, + ); + } + + pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool { + return try parser.eventTargetDispatchEvent(self, event); + } + + pub fn deinit(self: *parser.EventTarget, state: *SessionState) void { + parser.eventTargetRemoveAllEventListeners(self, state.arena) catch unreachable; + } +}; + +const testing = @import("../../testing.zig"); +test "Browser.DOM.EventTarget" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "let content = document.getElementById('content')", "undefined" }, + .{ "let para = document.getElementById('para')", "undefined" }, + // NOTE: as some event properties will change during the event dispatching phases + // we need to copy thoses values in order to check them afterwards + .{ + \\ var nb = 0; var evt; var phase; var cur; + \\ function cbk(event) { + \\ evt = event; + \\ phase = event.eventPhase; + \\ cur = event.currentTarget; + \\ nb ++; + \\ } + , + "undefined", + }, + }, .{}); + + try runner.testCases(&.{ + .{ "content.addEventListener('basic', cbk)", "undefined" }, + .{ "content.dispatchEvent(new Event('basic'))", "true" }, + .{ "nb", "1" }, + .{ "evt instanceof Event", "true" }, + .{ "evt.type", "basic" }, + .{ "phase", "2" }, + .{ "cur.getAttribute('id')", "content" }, + }, .{}); + + try runner.testCases(&.{ + .{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" }, + .{ "para.dispatchEvent(new Event('basic'))", "true" }, + .{ "nb", "0" }, // handler is not called, no capture, not the target, no bubbling + .{ "evt === undefined", "true" }, + }, .{}); + + try runner.testCases(&.{ + .{ "nb = 0", "0" }, + .{ "content.addEventListener('basic', cbk)", "undefined" }, + .{ "content.dispatchEvent(new Event('basic'))", "true" }, + .{ "nb", "1" }, + }, .{}); + + try runner.testCases(&.{ + .{ "nb = 0", "0" }, + .{ "content.addEventListener('basic', cbk, true)", "undefined" }, + .{ "content.dispatchEvent(new Event('basic'))", "true" }, + .{ "nb", "2" }, + }, .{}); + + try runner.testCases(&.{ + .{ "nb = 0", "0" }, + .{ "content.removeEventListener('basic', cbk)", "undefined" }, + .{ "content.dispatchEvent(new Event('basic'))", "true" }, + .{ "nb", "1" }, + }, .{}); + + try runner.testCases(&.{ + .{ "nb = 0", "0" }, + .{ "content.removeEventListener('basic', cbk, true)", "undefined" }, + .{ "content.dispatchEvent(new Event('basic'))", "true" }, + .{ "nb", "0" }, + }, .{}); + + try runner.testCases(&.{ + .{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" }, + .{ "content.addEventListener('capture', cbk, true)", "undefined" }, + .{ "content.dispatchEvent(new Event('capture'))", "true" }, + .{ "nb", "1" }, + .{ "evt instanceof Event", "true" }, + .{ "evt.type", "capture" }, + .{ "phase", "2" }, + .{ "cur.getAttribute('id')", "content" }, + }, .{}); + + try runner.testCases(&.{ + .{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" }, + .{ "para.dispatchEvent(new Event('capture'))", "true" }, + .{ "nb", "1" }, + .{ "evt instanceof Event", "true" }, + .{ "evt.type", "capture" }, + .{ "phase", "1" }, + .{ "cur.getAttribute('id')", "content" }, + }, .{}); + + try runner.testCases(&.{ + .{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" }, + .{ "content.addEventListener('bubbles', cbk)", "undefined" }, + .{ "content.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" }, + .{ "nb", "1" }, + .{ "evt instanceof Event", "true" }, + .{ "evt.type", "bubbles" }, + .{ "evt.bubbles", "true" }, + .{ "phase", "2" }, + .{ "cur.getAttribute('id')", "content" }, + }, .{}); + + try runner.testCases(&.{ + .{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" }, + .{ "para.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" }, + .{ "nb", "1" }, + .{ "evt instanceof Event", "true" }, + .{ "evt.type", "bubbles" }, + .{ "phase", "3" }, + .{ "cur.getAttribute('id')", "content" }, + }, .{}); +} diff --git a/src/dom/exceptions.zig b/src/browser/dom/exceptions.zig similarity index 81% rename from src/dom/exceptions.zig rename to src/browser/dom/exceptions.zig index 209f4345..441fe92c 100644 --- a/src/dom/exceptions.zig +++ b/src/browser/dom/exceptions.zig @@ -19,19 +19,13 @@ const std = @import("std"); const allocPrint = std.fmt.allocPrint; -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); // https://webidl.spec.whatwg.org/#idl-DOMException pub const DOMException = struct { err: parser.DOMError, str: []const u8, - pub const mem_guarantied = true; - pub const ErrorSet = parser.DOMError; // static attributes @@ -62,7 +56,7 @@ pub const DOMException = struct { pub const _DATA_CLONE_ERR = 25; // TODO: deinit - pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) anyerror!DOMException { + pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) !DOMException { const errCast = @as(parser.DOMError, @errorCast(err)); const errName = DOMException.name(errCast); const str = switch (errCast) { @@ -120,7 +114,7 @@ pub const DOMException = struct { // JS properties and methods - pub fn get_code(self: DOMException) u8 { + pub fn get_code(self: *const DOMException) u8 { return switch (self.err) { error.IndexSize => 1, error.StringSize => 2, @@ -157,38 +151,41 @@ pub const DOMException = struct { }; } - pub fn get_name(self: DOMException) []const u8 { + pub fn get_name(self: *const DOMException) []const u8 { return DOMException.name(self.err); } - pub fn get_message(self: DOMException) []const u8 { + pub fn get_message(self: *const DOMException) []const u8 { const errName = DOMException.name(self.err); return self.str[errName.len + 2 ..]; } - pub fn _toString(self: DOMException) []const u8 { + pub fn _toString(self: *const DOMException) []const u8 { return self.str; } }; -// Tests -// ----- +const testing = @import("../../testing.zig"); +test "Browser.DOM.Exception" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { const err = "Failed to execute 'appendChild' on 'Node': The new child element contains the parent."; - var cases = [_]Case{ - .{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "let link = document.getElementById('link')", .ex = "undefined" }, + try runner.testCases(&.{ + .{ "let content = document.getElementById('content')", "undefined" }, + .{ "let link = document.getElementById('link')", "undefined" }, // HierarchyRequestError - .{ .src = "var HierarchyRequestError; try {link.appendChild(content)} catch (error) {HierarchyRequestError = error} HierarchyRequestError.name", .ex = "HierarchyRequestError" }, - .{ .src = "HierarchyRequestError.code", .ex = "3" }, - .{ .src = "HierarchyRequestError.message", .ex = err }, - .{ .src = "HierarchyRequestError.toString()", .ex = "HierarchyRequestError: " ++ err }, - .{ .src = "HierarchyRequestError instanceof DOMException", .ex = "true" }, - .{ .src = "HierarchyRequestError instanceof Error", .ex = "true" }, - }; - try checkCases(js_env, &cases); + .{ + \\ var he; + \\ try { link.appendChild(content) } catch (error) { he = error} + \\ he.name + , + "HierarchyRequestError", + }, + .{ "he.code", "3" }, + .{ "he.message", err }, + .{ "he.toString()", "HierarchyRequestError: " ++ err }, + .{ "he instanceof DOMException", "true" }, + .{ "he instanceof Error", "true" }, + }, .{}); } diff --git a/src/dom/html_collection.zig b/src/browser/dom/html_collection.zig similarity index 76% rename from src/dom/html_collection.zig rename to src/browser/dom/html_collection.zig index 298d6b72..3a7aa8fa 100644 --- a/src/dom/html_collection.zig +++ b/src/browser/dom/html_collection.zig @@ -18,17 +18,14 @@ const std = @import("std"); -const parser = @import("netsurf"); - -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; -const generate = @import("../generate.zig"); +const parser = @import("../netsurf.zig"); const utils = @import("utils.z"); const Element = @import("element.zig").Element; const Union = @import("element.zig").Union; +const JsObject = @import("../env.zig").JsObject; + const Walker = @import("walker.zig").Walker; const WalkerDepthFirst = @import("walker.zig").WalkerDepthFirst; const WalkerChildren = @import("walker.zig").WalkerChildren; @@ -279,8 +276,6 @@ pub fn HTMLCollectionByAnchors( } pub const HTMLCollectionIterator = struct { - pub const mem_guarantied = true; - coll: *HTMLCollection, index: u32 = 0, @@ -311,8 +306,6 @@ pub const HTMLCollectionIterator = struct { // dom_html_collection expects a comparison function callback as arguement. // But we wanted a dynamically comparison here, according to the match tagname. pub const HTMLCollection = struct { - pub const mem_guarantied = true; - matcher: Matcher, walker: Walker, @@ -327,10 +320,6 @@ pub const HTMLCollection = struct { cur_idx: ?u32 = undefined, cur_node: ?*parser.Node = undefined, - // array_like_keys is used to keep reference to array like interface implementation. - // the collection generates keys string which must be free on deinit. - array_like_keys: std.ArrayListUnmanaged([]u8) = .{}, - // start returns the first node to walk on. fn start(self: HTMLCollection) !?*parser.Node { if (self.root == null) return null; @@ -412,7 +401,7 @@ pub const HTMLCollection = struct { return try Element.toInterface(e); } - pub fn _namedItem(self: *HTMLCollection, name: []const u8) !?Union { + pub fn _namedItem(self: *const HTMLCollection, name: []const u8) !?Union { if (self.root == null) return null; if (name.len == 0) return null; @@ -454,81 +443,67 @@ pub const HTMLCollection = struct { return null; } - pub fn postAttach(self: *HTMLCollection, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void { - const ln = try self.get_length(); - var i: u32 = 0; - while (i < ln) { - defer i += 1; - const k = try std.fmt.allocPrint(alloc, "{d}", .{i}); - try self.array_like_keys.append(alloc, k); - - const node = try self.item(i) orelse unreachable; + pub fn postAttach(self: *HTMLCollection, js_obj: JsObject) !void { + const len = try self.get_length(); + for (0..len) |i| { + const node = try self.item(@intCast(i)) orelse unreachable; const e = @as(*parser.Element, @ptrCast(node)); - try js_obj.set(k, e); + try js_obj.setIndex(@intCast(i), e); if (try item_name(e)) |name| { try js_obj.set(name, e); } } } - - pub fn deinit(self: *HTMLCollection, alloc: std.mem.Allocator) void { - for (self.array_like_keys_) |k| alloc.free(k); - self.array_like_keys.deinit(alloc); - self.matcher.deinit(alloc); - } }; -// Tests -// ----- +const testing = @import("../../testing.zig"); +test "Browser.DOM.HTMLCollection" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var getElementsByTagName = [_]Case{ - .{ .src = "let getElementsByTagName = document.getElementsByTagName('p')", .ex = "undefined" }, - .{ .src = "getElementsByTagName.length", .ex = "2" }, - .{ .src = "let getElementsByTagNameCI = document.getElementsByTagName('P')", .ex = "undefined" }, - .{ .src = "getElementsByTagNameCI.length", .ex = "2" }, - .{ .src = "getElementsByTagName.item(0).localName", .ex = "p" }, - .{ .src = "getElementsByTagName.item(1).localName", .ex = "p" }, - .{ .src = "let getElementsByTagNameAll = document.getElementsByTagName('*')", .ex = "undefined" }, - .{ .src = "getElementsByTagNameAll.length", .ex = "8" }, - .{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" }, - .{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" }, - .{ .src = "getElementsByTagNameAll.item(1).localName", .ex = "head" }, - .{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" }, - .{ .src = "getElementsByTagNameAll.item(2).localName", .ex = "body" }, - .{ .src = "getElementsByTagNameAll.item(3).localName", .ex = "div" }, - .{ .src = "getElementsByTagNameAll.item(7).localName", .ex = "p" }, - .{ .src = "getElementsByTagNameAll.namedItem('para-empty-child').localName", .ex = "span" }, + try runner.testCases(&.{ + .{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" }, + .{ "getElementsByTagName.length", "2" }, + .{ "let getElementsByTagNameCI = document.getElementsByTagName('P')", "undefined" }, + .{ "getElementsByTagNameCI.length", "2" }, + .{ "getElementsByTagName.item(0).localName", "p" }, + .{ "getElementsByTagName.item(1).localName", "p" }, + .{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" }, + .{ "getElementsByTagNameAll.length", "8" }, + .{ "getElementsByTagNameAll.item(0).localName", "html" }, + .{ "getElementsByTagNameAll.item(0).localName", "html" }, + .{ "getElementsByTagNameAll.item(1).localName", "head" }, + .{ "getElementsByTagNameAll.item(0).localName", "html" }, + .{ "getElementsByTagNameAll.item(2).localName", "body" }, + .{ "getElementsByTagNameAll.item(3).localName", "div" }, + .{ "getElementsByTagNameAll.item(7).localName", "p" }, + .{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" }, // array like - .{ .src = "getElementsByTagNameAll[0].localName", .ex = "html" }, - .{ .src = "getElementsByTagNameAll[7].localName", .ex = "p" }, - .{ .src = "getElementsByTagNameAll[8]", .ex = "undefined" }, - .{ .src = "getElementsByTagNameAll['para-empty-child'].localName", .ex = "span" }, - .{ .src = "getElementsByTagNameAll['foo']", .ex = "undefined" }, + .{ "getElementsByTagNameAll[0].localName", "html" }, + .{ "getElementsByTagNameAll[7].localName", "p" }, + .{ "getElementsByTagNameAll[8]", "undefined" }, + .{ "getElementsByTagNameAll['para-empty-child'].localName", "span" }, + .{ "getElementsByTagNameAll['foo']", "undefined" }, - .{ .src = "document.getElementById('content').getElementsByTagName('*').length", .ex = "4" }, - .{ .src = "document.getElementById('content').getElementsByTagName('p').length", .ex = "2" }, - .{ .src = "document.getElementById('content').getElementsByTagName('div').length", .ex = "0" }, + .{ "document.getElementById('content').getElementsByTagName('*').length", "4" }, + .{ "document.getElementById('content').getElementsByTagName('p').length", "2" }, + .{ "document.getElementById('content').getElementsByTagName('div').length", "0" }, - .{ .src = "document.children.length", .ex = "1" }, - .{ .src = "document.getElementById('content').children.length", .ex = "3" }, + .{ "document.children.length", "1" }, + .{ "document.getElementById('content').children.length", "3" }, // check liveness - .{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "let pe = document.getElementById('para-empty')", .ex = "undefined" }, - .{ .src = "let p = document.createElement('p')", .ex = "undefined" }, - .{ .src = "p.textContent = 'OK live'", .ex = "OK live" }, - .{ .src = "getElementsByTagName.item(1).textContent", .ex = " And" }, - .{ .src = "content.appendChild(p) != undefined", .ex = "true" }, - .{ .src = "getElementsByTagName.length", .ex = "3" }, - .{ .src = "getElementsByTagName.item(2).textContent", .ex = "OK live" }, - .{ .src = "content.insertBefore(p, pe) != undefined", .ex = "true" }, - .{ .src = "getElementsByTagName.item(0).textContent", .ex = "OK live" }, - }; - try checkCases(js_env, &getElementsByTagName); + .{ "let content = document.getElementById('content')", "undefined" }, + .{ "let pe = document.getElementById('para-empty')", "undefined" }, + .{ "let p = document.createElement('p')", "undefined" }, + .{ "p.textContent = 'OK live'", "OK live" }, + .{ "getElementsByTagName.item(1).textContent", " And" }, + .{ "content.appendChild(p) != undefined", "true" }, + .{ "getElementsByTagName.length", "3" }, + .{ "getElementsByTagName.item(2).textContent", "OK live" }, + .{ "content.insertBefore(p, pe) != undefined", "true" }, + .{ "getElementsByTagName.item(0).textContent", "OK live" }, + }, .{}); } diff --git a/src/dom/implementation.zig b/src/browser/dom/implementation.zig similarity index 61% rename from src/dom/implementation.zig rename to src/browser/dom/implementation.zig index ec90014f..b97b37e5 100644 --- a/src/dom/implementation.zig +++ b/src/browser/dom/implementation.zig @@ -18,11 +18,8 @@ const std = @import("std"); -const parser = @import("netsurf"); - -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; +const parser = @import("../netsurf.zig"); +const SessionState = @import("../env.zig").SessionState; const Document = @import("document.zig").Document; const DocumentType = @import("document_type.zig").DocumentType; @@ -30,47 +27,47 @@ const DOMException = @import("exceptions.zig").DOMException; // WEB IDL https://dom.spec.whatwg.org/#domimplementation pub const DOMImplementation = struct { - pub const mem_guarantied = true; - pub const Exception = DOMException; pub fn _createDocumentType( _: *DOMImplementation, - alloc: std.mem.Allocator, qname: []const u8, publicId: []const u8, systemId: []const u8, + state: *SessionState, ) !*parser.DocumentType { - const cqname = try alloc.dupeZ(u8, qname); - defer alloc.free(cqname); + const allocator = state.arena; + const cqname = try allocator.dupeZ(u8, qname); + defer allocator.free(cqname); - const cpublicId = try alloc.dupeZ(u8, publicId); - defer alloc.free(cpublicId); + const cpublicId = try allocator.dupeZ(u8, publicId); + defer allocator.free(cpublicId); - const csystemId = try alloc.dupeZ(u8, systemId); - defer alloc.free(csystemId); + const csystemId = try allocator.dupeZ(u8, systemId); + defer allocator.free(csystemId); return try parser.domImplementationCreateDocumentType(cqname, cpublicId, csystemId); } pub fn _createDocument( _: *DOMImplementation, - alloc: std.mem.Allocator, namespace: ?[]const u8, qname: ?[]const u8, doctype: ?*parser.DocumentType, + state: *SessionState, ) !*parser.Document { + const allocator = state.arena; var cnamespace: ?[:0]const u8 = null; if (namespace) |ns| { - cnamespace = try alloc.dupeZ(u8, ns); + cnamespace = try allocator.dupeZ(u8, ns); } - defer if (cnamespace) |v| alloc.free(v); + defer if (cnamespace) |v| allocator.free(v); var cqname: ?[:0]const u8 = null; if (qname) |qn| { - cqname = try alloc.dupeZ(u8, qn); + cqname = try allocator.dupeZ(u8, qn); } - defer if (cqname) |v| alloc.free(v); + defer if (cqname) |v| allocator.free(v); return try parser.domImplementationCreateDocument(cnamespace, cqname, doctype); } @@ -89,17 +86,17 @@ pub const DOMImplementation = struct { // Tests // ----- -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var getImplementation = [_]Case{ - .{ .src = "let impl = document.implementation", .ex = "undefined" }, - .{ .src = "impl.createHTMLDocument();", .ex = "[object HTMLDocument]" }, - .{ .src = "impl.createHTMLDocument('foo');", .ex = "[object HTMLDocument]" }, - .{ .src = "impl.createDocument(null, 'foo');", .ex = "[object Document]" }, - .{ .src = "impl.createDocumentType('foo', 'bar', 'baz')", .ex = "[object DocumentType]" }, - .{ .src = "impl.hasFeature()", .ex = "true" }, - }; - try checkCases(js_env, &getImplementation); +const testing = @import("../../testing.zig"); +test "Browser.DOM.Implementation" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "let impl = document.implementation", "undefined" }, + .{ "impl.createHTMLDocument();", "[object HTMLDocument]" }, + .{ "impl.createHTMLDocument('foo');", "[object HTMLDocument]" }, + .{ "impl.createDocument(null, 'foo');", "[object Document]" }, + .{ "impl.createDocumentType('foo', 'bar', 'baz')", "[object DocumentType]" }, + .{ "impl.hasFeature()", "true" }, + }, .{}); } diff --git a/src/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig similarity index 68% rename from src/dom/mutation_observer.zig rename to src/browser/dom/mutation_observer.zig index f5003686..0324399e 100644 --- a/src/dom/mutation_observer.zig +++ b/src/browser/dom/mutation_observer.zig @@ -18,14 +18,11 @@ const std = @import("std"); -const parser = @import("netsurf"); - -const jsruntime = @import("jsruntime"); -const Callback = jsruntime.Callback; -const CallbackResult = jsruntime.CallbackResult; -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; +const parser = @import("../netsurf.zig"); +const SessionState = @import("../env.zig").SessionState; +const Env = @import("../env.zig").Env; +const JsObject = @import("../env.zig").JsObject; const NodeList = @import("nodelist.zig").NodeList; pub const Interfaces = .{ @@ -40,20 +37,18 @@ const log = std.log.scoped(.events); // WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver pub const MutationObserver = struct { - cbk: Callback, + cbk: Env.Callback, observers: Observers, - pub const mem_guarantied = true; - const Observer = struct { node: *parser.Node, options: MutationObserverInit, }; const deinitFunc = struct { - fn deinit(ctx: ?*anyopaque, alloc: std.mem.Allocator) void { + fn deinit(ctx: ?*anyopaque, allocator: std.mem.Allocator) void { const o: *Observer = @ptrCast(@alignCast(ctx)); - alloc.destroy(o); + allocator.destroy(o); } }.deinit; @@ -78,7 +73,7 @@ pub const MutationObserver = struct { } }; - pub fn constructor(cbk: Callback) !MutationObserver { + pub fn constructor(cbk: Env.Callback) !MutationObserver { return MutationObserver{ .cbk = cbk, .observers = .{}, @@ -90,22 +85,23 @@ pub const MutationObserver = struct { return opt orelse .{}; } - pub fn _observe(self: *MutationObserver, alloc: std.mem.Allocator, node: *parser.Node, options: ?MutationObserverInit) !void { - const o = try alloc.create(Observer); + pub fn _observe(self: *MutationObserver, node: *parser.Node, options: ?MutationObserverInit, state: *SessionState) !void { + const arena = state.arena; + const o = try arena.create(Observer); o.* = .{ .node = node, .options = resolveOptions(options), }; - errdefer alloc.destroy(o); + errdefer arena.destroy(o); // register the new observer. - try self.observers.append(alloc, o); + try self.observers.append(arena, o); // register node's events. if (o.options.childList or o.options.subtree) { try parser.eventTargetAddEventListener( parser.toEventTarget(parser.Node, node), - alloc, + arena, "DOMNodeInserted", EventHandler, .{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc }, @@ -113,7 +109,7 @@ pub const MutationObserver = struct { ); try parser.eventTargetAddEventListener( parser.toEventTarget(parser.Node, node), - alloc, + arena, "DOMNodeRemoved", EventHandler, .{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc }, @@ -123,7 +119,7 @@ pub const MutationObserver = struct { if (o.options.attr()) { try parser.eventTargetAddEventListener( parser.toEventTarget(parser.Node, node), - alloc, + arena, "DOMAttrModified", EventHandler, .{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc }, @@ -133,7 +129,7 @@ pub const MutationObserver = struct { if (o.options.cdata()) { try parser.eventTargetAddEventListener( parser.toEventTarget(parser.Node, node), - alloc, + arena, "DOMCharacterDataModified", EventHandler, .{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc }, @@ -143,7 +139,7 @@ pub const MutationObserver = struct { if (o.options.subtree) { try parser.eventTargetAddEventListener( parser.toEventTarget(parser.Node, node), - alloc, + arena, "DOMSubtreeModified", EventHandler, .{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc }, @@ -157,14 +153,17 @@ pub const MutationObserver = struct { // TODO unregister listeners. } - pub fn deinit(self: *MutationObserver, alloc: std.mem.Allocator) void { + pub fn deinit(self: *MutationObserver, state: *SessionState) void { + const arena = state.arena; // TODO unregister listeners. - for (self.observers.items) |o| alloc.destroy(o); - self.observers.deinit(alloc); + for (self.observers.items) |o| { + arena.destroy(o); + } + self.observers.deinit(arena); } // TODO - pub fn _takeRecords(_: MutationObserver) ?[]const u8 { + pub fn _takeRecords(_: *const MutationObserver) ?[]const u8 { return &[_]u8{}; } }; @@ -173,15 +172,19 @@ pub const MutationObserver = struct { pub const MutationRecords = struct { first: ?MutationRecord = null, - pub const mem_guarantied = true; - - pub fn get_length(self: *MutationRecords) u32 { + pub fn get_length(self: *const MutationRecords) u32 { if (self.first == null) return 0; return 1; } - - pub fn postAttach(self: *MutationRecords, js_obj: jsruntime.JSObject) !void { + pub fn indexed_get(self: *const MutationRecords, i: u32, has_value: *bool) ?MutationRecord { + _ = i; + return self.first orelse { + has_value.* = false; + return null; + }; + } + pub fn postAttach(self: *const MutationRecords, js_obj: JsObject) !void { if (self.first) |mr| { try js_obj.set("0", mr); } @@ -199,41 +202,39 @@ pub const MutationRecord = struct { attributeNamespace: ?[]const u8 = null, oldValue: ?[]const u8 = null, - pub const mem_guarantied = true; - - pub fn get_type(self: MutationRecord) []const u8 { + pub fn get_type(self: *const MutationRecord) []const u8 { return self.type; } - pub fn get_addedNodes(self: MutationRecord) NodeList { + pub fn get_addedNodes(self: *const MutationRecord) NodeList { return self.addedNodes; } - pub fn get_removedNodes(self: MutationRecord) NodeList { + pub fn get_removedNodes(self: *const MutationRecord) NodeList { return self.addedNodes; } - pub fn get_target(self: MutationRecord) *parser.Node { + pub fn get_target(self: *const MutationRecord) *parser.Node { return self.target; } - pub fn get_attributeName(self: MutationRecord) ?[]const u8 { + pub fn get_attributeName(self: *const MutationRecord) ?[]const u8 { return self.attributeName; } - pub fn get_attributeNamespace(self: MutationRecord) ?[]const u8 { + pub fn get_attributeNamespace(self: *const MutationRecord) ?[]const u8 { return self.attributeNamespace; } - pub fn get_previousSibling(self: MutationRecord) ?*parser.Node { + pub fn get_previousSibling(self: *const MutationRecord) ?*parser.Node { return self.previousSibling; } - pub fn get_nextSibling(self: MutationRecord) ?*parser.Node { + pub fn get_nextSibling(self: *const MutationRecord) ?*parser.Node { return self.nextSibling; } - pub fn get_oldValue(self: MutationRecord) ?[]const u8 { + pub fn get_oldValue(self: *const MutationRecord) ?[]const u8 { return self.oldValue; } }; @@ -283,7 +284,7 @@ const EventHandler = struct { const muevt = parser.eventToMutationEvent(evt.?); // TODO get the allocator by another way? - const alloc = data.cbk.nat_ctx.alloc; + const alloc = data.cbk.executor.call_arena.allocator(); if (std.mem.eql(u8, t, "DOMAttrModified")) { mrs.first = .{ @@ -340,66 +341,63 @@ const EventHandler = struct { return; } - var res = CallbackResult.init(alloc); - defer res.deinit(); - // TODO pass MutationRecords and MutationObserver - data.cbk.trycall(.{mrs}, &res) catch |e| log.err("mutation event handler error: {any}", .{e}); - - // in case of function error, we log the result and the trace. - if (!res.success) { - log.info("mutation observer event handler error: {s}", .{res.result orelse "unknown"}); - log.debug("{s}", .{res.stack orelse "no stack trace"}); - } + var result: Env.Callback.Result = undefined; + data.cbk.tryCall(.{mrs}, &result) catch { + log.err("mutation observer callback error: {s}", .{result.exception}); + log.debug("stack:\n{s}", .{result.stack orelse "???"}); + }; } }.handle; -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var constructor = [_]Case{ - .{ .src = "new MutationObserver(() => {}).observe(document, { childList: true });", .ex = "undefined" }, - }; - try checkCases(js_env, &constructor); +const testing = @import("../../testing.zig"); +test "Browser.DOM.MutationObserver" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); - var attr = [_]Case{ - .{ .src = - \\var nb = 0; - \\var mrs; - \\new MutationObserver((mu) => { - \\ mrs = mu; - \\ nb++; - \\}).observe(document.firstElementChild, { attributes: true, attributeOldValue: true }); - \\document.firstElementChild.setAttribute("foo", "bar"); - \\// ignored b/c it's about another target. - \\document.firstElementChild.firstChild.setAttribute("foo", "bar"); - \\nb; - , .ex = "1" }, - .{ .src = "mrs[0].type", .ex = "attributes" }, - .{ .src = "mrs[0].target == document.firstElementChild", .ex = "true" }, - .{ .src = "mrs[0].target.getAttribute('foo')", .ex = "bar" }, - .{ .src = "mrs[0].attributeName", .ex = "foo" }, - .{ .src = "mrs[0].oldValue", .ex = "null" }, - }; - try checkCases(js_env, &attr); + try runner.testCases(&.{ + .{ "new MutationObserver(() => {}).observe(document, { childList: true });", "undefined" }, + }, .{}); - var cdata = [_]Case{ - .{ .src = - \\var node = document.getElementById("para").firstChild; - \\var nb2 = 0; - \\var mrs2; - \\new MutationObserver((mu) => { - \\ mrs2 = mu; - \\ nb2++; - \\}).observe(node, { characterData: true, characterDataOldValue: true }); - \\node.data = "foo"; - \\nb2; - , .ex = "1" }, - .{ .src = "mrs2[0].type", .ex = "characterData" }, - .{ .src = "mrs2[0].target == node", .ex = "true" }, - .{ .src = "mrs2[0].target.data", .ex = "foo" }, - .{ .src = "mrs2[0].oldValue", .ex = " And" }, - }; - try checkCases(js_env, &cdata); + try runner.testCases(&.{ + .{ + \\ var nb = 0; + \\ var mrs; + \\ new MutationObserver((mu) => { + \\ mrs = mu; + \\ nb++; + \\ }).observe(document.firstElementChild, { attributes: true, attributeOldValue: true }); + \\ document.firstElementChild.setAttribute("foo", "bar"); + \\ // ignored b/c it's about another target. + \\ document.firstElementChild.firstChild.setAttribute("foo", "bar"); + \\ nb; + , + "1", + }, + .{ "mrs[0].type", "attributes" }, + .{ "mrs[0].target == document.firstElementChild", "true" }, + .{ "mrs[0].target.getAttribute('foo')", "bar" }, + .{ "mrs[0].attributeName", "foo" }, + .{ "mrs[0].oldValue", "null" }, + }, .{}); + + try runner.testCases(&.{ + .{ + \\ var node = document.getElementById("para").firstChild; + \\ var nb2 = 0; + \\ var mrs2; + \\ new MutationObserver((mu) => { + \\ mrs2 = mu; + \\ nb2++; + \\ }).observe(node, { characterData: true, characterDataOldValue: true }); + \\ node.data = "foo"; + \\ nb2; + , + "1", + }, + .{ "mrs2[0].type", "characterData" }, + .{ "mrs2[0].target == node", "true" }, + .{ "mrs2[0].target.data", "foo" }, + .{ "mrs2[0].oldValue", " And" }, + }, .{}); } diff --git a/src/dom/namednodemap.zig b/src/browser/dom/namednodemap.zig similarity index 76% rename from src/dom/namednodemap.zig rename to src/browser/dom/namednodemap.zig index 67840659..97a85b5c 100644 --- a/src/dom/namednodemap.zig +++ b/src/browser/dom/namednodemap.zig @@ -18,18 +18,13 @@ const std = @import("std"); -const parser = @import("netsurf"); - -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; +const parser = @import("../netsurf.zig"); const DOMException = @import("exceptions.zig").DOMException; // WEB IDL https://dom.spec.whatwg.org/#namednodemap pub const NamedNodeMap = struct { pub const Self = parser.NamedNodeMap; - pub const mem_guarantied = true; pub const Exception = DOMException; @@ -80,18 +75,18 @@ pub const NamedNodeMap = struct { // Tests // ----- -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var setItem = [_]Case{ - .{ .src = "let a = document.getElementById('content').attributes", .ex = "undefined" }, - .{ .src = "a.length", .ex = "1" }, - .{ .src = "a.item(0)", .ex = "[object Attr]" }, - .{ .src = "a.item(1)", .ex = "null" }, - .{ .src = "a.getNamedItem('id')", .ex = "[object Attr]" }, - .{ .src = "a.getNamedItem('foo')", .ex = "null" }, - .{ .src = "a.setNamedItem(a.getNamedItem('id'))", .ex = "[object Attr]" }, - }; - try checkCases(js_env, &setItem); +const testing = @import("../../testing.zig"); +test "Browser.DOM.NamedNodeMap" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "let a = document.getElementById('content').attributes", "undefined" }, + .{ "a.length", "1" }, + .{ "a.item(0)", "[object Attr]" }, + .{ "a.item(1)", "null" }, + .{ "a.getNamedItem('id')", "[object Attr]" }, + .{ "a.getNamedItem('foo')", "null" }, + .{ "a.setNamedItem(a.getNamedItem('id'))", "[object Attr]" }, + }, .{}); } diff --git a/src/dom/node.zig b/src/browser/dom/node.zig similarity index 54% rename from src/dom/node.zig rename to src/browser/dom/node.zig index 3e0a7561..a0771b16 100644 --- a/src/dom/node.zig +++ b/src/browser/dom/node.zig @@ -18,16 +18,10 @@ const std = @import("std"); -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; -const runScript = jsruntime.test_utils.runScript; -const Variadic = jsruntime.Variadic; - -const generate = @import("../generate.zig"); - -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); +const generate = @import("../../runtime/generate.zig"); +const SessionState = @import("../env.zig").SessionState; const EventTarget = @import("event_target.zig").EventTarget; // DOM @@ -66,7 +60,6 @@ pub const Union = generate.Union(Interfaces); pub const Node = struct { pub const Self = parser.Node; pub const prototype = *EventTarget; - pub const mem_guarantied = true; pub fn toInterface(node: *parser.Node) !Union { return switch (try parser.nodeType(node)) { @@ -262,13 +255,14 @@ pub const Node = struct { return try parser.nodeHasChildNodes(self); } - pub fn get_childNodes(self: *parser.Node, alloc: std.mem.Allocator) !NodeList { + pub fn get_childNodes(self: *parser.Node, state: *SessionState) !NodeList { + const allocator = state.arena; var list = NodeList.init(); - errdefer list.deinit(alloc); + errdefer list.deinit(allocator); var n = try parser.nodeFirstChild(self) orelse return list; while (true) { - try list.append(alloc, n); + try list.append(allocator, n); n = try parser.nodeNextSibling(n) orelse return list; } } @@ -327,11 +321,10 @@ pub const Node = struct { // For now, it checks only if new nodes are not self. // TODO implements the others contraints. // see https://dom.spec.whatwg.org/#concept-node-tree - pub fn hierarchy(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !bool { - if (nodes == null) return true; - if (nodes.?.slice.len == 0) return true; + pub fn hierarchy(self: *parser.Node, nodes: []const *parser.Node) !bool { + if (nodes.len == 0) return true; - for (nodes.?.slice) |node| if (self == node) return false; + for (nodes) |node| if (self == node) return false; return true; } @@ -339,22 +332,21 @@ pub const Node = struct { // TODO according with https://dom.spec.whatwg.org/#parentnode, the // function must accept either node or string. // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn prepend(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void { - if (nodes == null) return; - if (nodes.?.slice.len == 0) return; + pub fn prepend(self: *parser.Node, nodes: []const *parser.Node) !void { + if (nodes.len == 0) return; // check hierarchy if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest; const first = try parser.nodeFirstChild(self); if (first == null) { - for (nodes.?.slice) |node| { + for (nodes) |node| { _ = try parser.nodeAppendChild(self, node); } return; } - for (nodes.?.slice) |node| { + for (nodes) |node| { _ = try parser.nodeInsertBefore(self, node, first.?); } } @@ -362,14 +354,13 @@ pub const Node = struct { // TODO according with https://dom.spec.whatwg.org/#parentnode, the // function must accept either node or string. // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn append(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void { - if (nodes == null) return; - if (nodes.?.slice.len == 0) return; + pub fn append(self: *parser.Node, nodes: []const *parser.Node) !void { + if (nodes.len == 0) return; // check hierarchy if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest; - for (nodes.?.slice) |node| { + for (nodes) |node| { _ = try parser.nodeAppendChild(self, node); } } @@ -377,9 +368,8 @@ pub const Node = struct { // TODO according with https://dom.spec.whatwg.org/#parentnode, the // function must accept either node or string. // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn replaceChildren(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void { - if (nodes == null) return; - if (nodes.?.slice.len == 0) return; + pub fn replaceChildren(self: *parser.Node, nodes: []const *parser.Node) !void { + if (nodes.len == 0) return; // check hierarchy if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest; @@ -388,7 +378,7 @@ pub const Node = struct { try removeChildren(self); // add new children - for (nodes.?.slice) |node| { + for (nodes) |node| { _ = try parser.nodeAppendChild(self, node); } } @@ -411,219 +401,192 @@ pub const Node = struct { pub fn deinit(_: *parser.Node, _: std.mem.Allocator) void {} }; -// Tests -// ----- +const testing = @import("../../testing.zig"); +test "Browser.DOM.node" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); -pub fn testExecFn( - alloc: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { + { + var err_out: ?[]const u8 = null; + try runner.exec( + \\ function trimAndReplace(str) { + \\ str = str.replace(/(\r\n|\n|\r)/gm,''); + \\ str = str.replace(/\s+/g, ' '); + \\ str = str.trim(); + \\ return str; + \\ } + , "trimAndReplace", &err_out); + } - // helper functions - const trim_and_replace = - \\function trimAndReplace(str) { - \\str = str.replace(/(\r\n|\n|\r)/gm,''); - \\str = str.replace(/\s+/g, ' '); - \\str = str.trim(); - \\return str; - \\} - ; - try runScript(js_env, alloc, trim_and_replace, "proto_test"); + try runner.testCases(&.{ + .{ "document.body.compareDocumentPosition(document.firstChild); ", "10" }, + .{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", "10" }, + .{ "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "20" }, + .{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", "0" }, + .{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", "2" }, + .{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "4" }, + }, .{}); - var node_compare_document_position = [_]Case{ - .{ .src = "document.body.compareDocumentPosition(document.firstChild); ", .ex = "10" }, - .{ .src = "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", .ex = "10" }, - .{ .src = "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", .ex = "20" }, - .{ .src = "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", .ex = "0" }, - .{ .src = "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", .ex = "2" }, - .{ .src = "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", .ex = "4" }, - }; - try checkCases(js_env, &node_compare_document_position); + try runner.testCases(&.{ + .{ "document.getElementById('content').getRootNode().__proto__.constructor.name", "HTMLDocument" }, + }, .{}); - var get_root_node = [_]Case{ - .{ .src = "document.getElementById('content').getRootNode().__proto__.constructor.name", .ex = "HTMLDocument" }, - }; - try checkCases(js_env, &get_root_node); - - var first_child = [_]Case{ + try runner.testCases(&.{ // for next test cases - .{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "let link = document.getElementById('link')", .ex = "undefined" }, - .{ .src = "let first_child = content.firstChild.nextSibling", .ex = "undefined" }, // nextSibling because of line return \n + .{ "let content = document.getElementById('content')", "undefined" }, + .{ "let link = document.getElementById('link')", "undefined" }, + .{ "let first_child = content.firstChild.nextSibling", "undefined" }, // nextSibling because of line return \n - .{ .src = "let body_first_child = document.body.firstChild", .ex = "undefined" }, - .{ .src = "body_first_child.localName", .ex = "div" }, - .{ .src = "body_first_child.__proto__.constructor.name", .ex = "HTMLDivElement" }, - .{ .src = "document.getElementById('para-empty').firstChild.firstChild", .ex = "null" }, - }; - try checkCases(js_env, &first_child); + .{ "let body_first_child = document.body.firstChild", "undefined" }, + .{ "body_first_child.localName", "div" }, + .{ "body_first_child.__proto__.constructor.name", "HTMLDivElement" }, + .{ "document.getElementById('para-empty').firstChild.firstChild", "null" }, + }, .{}); - var last_child = [_]Case{ - .{ .src = "let last_child = content.lastChild.previousSibling", .ex = "undefined" }, // previousSibling because of line return \n - .{ .src = "last_child.__proto__.constructor.name", .ex = "Comment" }, - }; - try checkCases(js_env, &last_child); + try runner.testCases(&.{ + .{ "let last_child = content.lastChild.previousSibling", "undefined" }, // previousSibling because of line return \n + .{ "last_child.__proto__.constructor.name", "Comment" }, + }, .{}); - var next_sibling = [_]Case{ - .{ .src = "let next_sibling = link.nextSibling.nextSibling", .ex = "undefined" }, - .{ .src = "next_sibling.localName", .ex = "p" }, - .{ .src = "next_sibling.__proto__.constructor.name", .ex = "HTMLParagraphElement" }, - .{ .src = "content.nextSibling.nextSibling", .ex = "null" }, - }; - try checkCases(js_env, &next_sibling); + try runner.testCases(&.{ + .{ "let next_sibling = link.nextSibling.nextSibling", "undefined" }, + .{ "next_sibling.localName", "p" }, + .{ "next_sibling.__proto__.constructor.name", "HTMLParagraphElement" }, + .{ "content.nextSibling.nextSibling", "null" }, + }, .{}); - var prev_sibling = [_]Case{ - .{ .src = "let prev_sibling = document.getElementById('para-empty').previousSibling.previousSibling", .ex = "undefined" }, - .{ .src = "prev_sibling.localName", .ex = "a" }, - .{ .src = "prev_sibling.__proto__.constructor.name", .ex = "HTMLAnchorElement" }, - .{ .src = "content.previousSibling", .ex = "null" }, - }; - try checkCases(js_env, &prev_sibling); + try runner.testCases(&.{ + .{ "let prev_sibling = document.getElementById('para-empty').previousSibling.previousSibling", "undefined" }, + .{ "prev_sibling.localName", "a" }, + .{ "prev_sibling.__proto__.constructor.name", "HTMLAnchorElement" }, + .{ "content.previousSibling", "null" }, + }, .{}); - var parent = [_]Case{ - .{ .src = "let parent = document.getElementById('para').parentElement", .ex = "undefined" }, - .{ .src = "parent.localName", .ex = "div" }, - .{ .src = "parent.__proto__.constructor.name", .ex = "HTMLDivElement" }, - .{ .src = "let h = content.parentElement.parentElement", .ex = "undefined" }, - .{ .src = "h.parentElement", .ex = "null" }, - .{ .src = "h.parentNode.__proto__.constructor.name", .ex = "HTMLDocument" }, - }; - try checkCases(js_env, &parent); + try runner.testCases(&.{ + .{ "let parent = document.getElementById('para').parentElement", "undefined" }, + .{ "parent.localName", "div" }, + .{ "parent.__proto__.constructor.name", "HTMLDivElement" }, + .{ "let h = content.parentElement.parentElement", "undefined" }, + .{ "h.parentElement", "null" }, + .{ "h.parentNode.__proto__.constructor.name", "HTMLDocument" }, + }, .{}); - var node_name = [_]Case{ - .{ .src = "first_child.nodeName === 'A'", .ex = "true" }, - .{ .src = "link.firstChild.nodeName === '#text'", .ex = "true" }, - .{ .src = "last_child.nodeName === '#comment'", .ex = "true" }, - .{ .src = "document.nodeName === '#document'", .ex = "true" }, - }; - try checkCases(js_env, &node_name); + try runner.testCases(&.{ + .{ "first_child.nodeName === 'A'", "true" }, + .{ "link.firstChild.nodeName === '#text'", "true" }, + .{ "last_child.nodeName === '#comment'", "true" }, + .{ "document.nodeName === '#document'", "true" }, + }, .{}); - var node_type = [_]Case{ - .{ .src = "first_child.nodeType === 1", .ex = "true" }, - .{ .src = "link.firstChild.nodeType === 3", .ex = "true" }, - .{ .src = "last_child.nodeType === 8", .ex = "true" }, - .{ .src = "document.nodeType === 9", .ex = "true" }, - }; - try checkCases(js_env, &node_type); + try runner.testCases(&.{ + .{ "first_child.nodeType === 1", "true" }, + .{ "link.firstChild.nodeType === 3", "true" }, + .{ "last_child.nodeType === 8", "true" }, + .{ "document.nodeType === 9", "true" }, + }, .{}); - var owner = [_]Case{ - .{ .src = "let owner = content.ownerDocument", .ex = "undefined" }, - .{ .src = "owner.__proto__.constructor.name", .ex = "HTMLDocument" }, - .{ .src = "document.ownerDocument", .ex = "null" }, - .{ .src = "let owner2 = document.createElement('div').ownerDocument", .ex = "undefined" }, - .{ .src = "owner2.__proto__.constructor.name", .ex = "HTMLDocument" }, - }; - try checkCases(js_env, &owner); + try runner.testCases(&.{ + .{ "let owner = content.ownerDocument", "undefined" }, + .{ "owner.__proto__.constructor.name", "HTMLDocument" }, + .{ "document.ownerDocument", "null" }, + .{ "let owner2 = document.createElement('div').ownerDocument", "undefined" }, + .{ "owner2.__proto__.constructor.name", "HTMLDocument" }, + }, .{}); - var connected = [_]Case{ - .{ .src = "content.isConnected", .ex = "true" }, - .{ .src = "document.isConnected", .ex = "true" }, - .{ .src = "document.createElement('div').isConnected", .ex = "false" }, - }; - try checkCases(js_env, &connected); + try runner.testCases(&.{ + .{ "content.isConnected", "true" }, + .{ "document.isConnected", "true" }, + .{ "document.createElement('div').isConnected", "false" }, + }, .{}); - var node_value = [_]Case{ - .{ .src = "last_child.nodeValue === 'comment'", .ex = "true" }, - .{ .src = "link.nodeValue === null", .ex = "true" }, - .{ .src = "let text = link.firstChild", .ex = "undefined" }, - .{ .src = "text.nodeValue === 'OK'", .ex = "true" }, - .{ .src = "text.nodeValue = 'OK modified'", .ex = "OK modified" }, - .{ .src = "text.nodeValue === 'OK modified'", .ex = "true" }, - .{ .src = "link.nodeValue = 'nothing'", .ex = "nothing" }, - }; - try checkCases(js_env, &node_value); + try runner.testCases(&.{ + .{ "last_child.nodeValue === 'comment'", "true" }, + .{ "link.nodeValue === null", "true" }, + .{ "let text = link.firstChild", "undefined" }, + .{ "text.nodeValue === 'OK'", "true" }, + .{ "text.nodeValue = 'OK modified'", "OK modified" }, + .{ "text.nodeValue === 'OK modified'", "true" }, + .{ "link.nodeValue = 'nothing'", "nothing" }, + }, .{}); - var node_text_content = [_]Case{ - .{ .src = "text.textContent === 'OK modified'", .ex = "true" }, - .{ .src = "trimAndReplace(content.textContent) === 'OK modified And'", .ex = "true" }, - .{ .src = "text.textContent = 'OK'", .ex = "OK" }, - .{ .src = "text.textContent", .ex = "OK" }, - .{ .src = "trimAndReplace(document.getElementById('para-empty').textContent)", .ex = "" }, - .{ .src = "document.getElementById('para-empty').textContent = 'OK'", .ex = "OK" }, - .{ .src = "document.getElementById('para-empty').firstChild.nodeName === '#text'", .ex = "true" }, - }; - try checkCases(js_env, &node_text_content); + try runner.testCases(&.{ + .{ "text.textContent === 'OK modified'", "true" }, + .{ "trimAndReplace(content.textContent) === 'OK modified And'", "true" }, + .{ "text.textContent = 'OK'", "OK" }, + .{ "text.textContent", "OK" }, + .{ "trimAndReplace(document.getElementById('para-empty').textContent)", "" }, + .{ "document.getElementById('para-empty').textContent = 'OK'", "OK" }, + .{ "document.getElementById('para-empty').firstChild.nodeName === '#text'", "true" }, + }, .{}); - var node_append_child = [_]Case{ - .{ .src = "let append = document.createElement('h1')", .ex = "undefined" }, - .{ .src = "content.appendChild(append).toString()", .ex = "[object HTMLHeadingElement]" }, - .{ .src = "content.lastChild.__proto__.constructor.name", .ex = "HTMLHeadingElement" }, - .{ .src = "content.appendChild(link).toString()", .ex = "[object HTMLAnchorElement]" }, - }; - try checkCases(js_env, &node_append_child); + try runner.testCases(&.{ + .{ "let append = document.createElement('h1')", "undefined" }, + .{ "content.appendChild(append).toString()", "[object HTMLHeadingElement]" }, + .{ "content.lastChild.__proto__.constructor.name", "HTMLHeadingElement" }, + .{ "content.appendChild(link).toString()", "[object HTMLAnchorElement]" }, + }, .{}); - var node_clone = [_]Case{ - .{ .src = "let clone = link.cloneNode()", .ex = "undefined" }, - .{ .src = "clone.toString()", .ex = "[object HTMLAnchorElement]" }, - .{ .src = "clone.parentNode === null", .ex = "true" }, - .{ .src = "clone.firstChild === null", .ex = "true" }, - .{ .src = "let clone_deep = link.cloneNode(true)", .ex = "undefined" }, - .{ .src = "clone_deep.firstChild.nodeName === '#text'", .ex = "true" }, - }; - try checkCases(js_env, &node_clone); + try runner.testCases(&.{ + .{ "let clone = link.cloneNode()", "undefined" }, + .{ "clone.toString()", "[object HTMLAnchorElement]" }, + .{ "clone.parentNode === null", "true" }, + .{ "clone.firstChild === null", "true" }, + .{ "let clone_deep = link.cloneNode(true)", "undefined" }, + .{ "clone_deep.firstChild.nodeName === '#text'", "true" }, + }, .{}); - var node_contains = [_]Case{ - .{ .src = "link.contains(text)", .ex = "true" }, - .{ .src = "text.contains(link)", .ex = "false" }, - }; - try checkCases(js_env, &node_contains); + try runner.testCases(&.{ + .{ "link.contains(text)", "true" }, + .{ "text.contains(link)", "false" }, + }, .{}); - var node_has_child_nodes = [_]Case{ - .{ .src = "link.hasChildNodes()", .ex = "true" }, - .{ .src = "text.hasChildNodes()", .ex = "false" }, - }; - try checkCases(js_env, &node_has_child_nodes); + try runner.testCases(&.{ + .{ "link.hasChildNodes()", "true" }, + .{ "text.hasChildNodes()", "false" }, + }, .{}); - var node_child_nodes = [_]Case{ - .{ .src = "link.childNodes.length", .ex = "1" }, - .{ .src = "text.childNodes.length", .ex = "0" }, - }; - try checkCases(js_env, &node_child_nodes); + try runner.testCases(&.{ + .{ "link.childNodes.length", "1" }, + .{ "text.childNodes.length", "0" }, + }, .{}); - var node_insert_before = [_]Case{ - .{ .src = "let insertBefore = document.createElement('a')", .ex = "undefined" }, - .{ .src = "link.insertBefore(insertBefore, text) !== undefined", .ex = "true" }, - .{ .src = "link.firstChild.localName === 'a'", .ex = "true" }, - }; - try checkCases(js_env, &node_insert_before); + try runner.testCases(&.{ + .{ "let insertBefore = document.createElement('a')", "undefined" }, + .{ "link.insertBefore(insertBefore, text) !== undefined", "true" }, + .{ "link.firstChild.localName === 'a'", "true" }, + }, .{}); - var node_is_default_namespace = [_]Case{ + try runner.testCases(&.{ // TODO: does not seems to work - // .{ .src = "link.isDefaultNamespace('')", .ex = "true" }, - .{ .src = "link.isDefaultNamespace('false')", .ex = "false" }, - }; - try checkCases(js_env, &node_is_default_namespace); + // .{ "link.isDefaultNamespace('')", "true" }, + .{ "link.isDefaultNamespace('false')", "false" }, + }, .{}); - var node_is_equal_node = [_]Case{ - .{ .src = "let equal1 = document.createElement('a')", .ex = "undefined" }, - .{ .src = "let equal2 = document.createElement('a')", .ex = "undefined" }, - .{ .src = "equal1.textContent = 'is equal'", .ex = "is equal" }, - .{ .src = "equal2.textContent = 'is equal'", .ex = "is equal" }, + try runner.testCases(&.{ + .{ "let equal1 = document.createElement('a')", "undefined" }, + .{ "let equal2 = document.createElement('a')", "undefined" }, + .{ "equal1.textContent = 'is equal'", "is equal" }, + .{ "equal2.textContent = 'is equal'", "is equal" }, // TODO: does not seems to work - // .{ .src = "equal1.isEqualNode(equal2)", .ex = "true" }, - }; - try checkCases(js_env, &node_is_equal_node); + // .{ "equal1.isEqualNode(equal2)", "true" }, + }, .{}); - var node_is_same_node = [_]Case{ - .{ .src = "document.body.isSameNode(document.body)", .ex = "true" }, - }; - try checkCases(js_env, &node_is_same_node); + try runner.testCases(&.{ + .{ "document.body.isSameNode(document.body)", "true" }, + }, .{}); - var node_normalize = [_]Case{ + try runner.testCases(&.{ // TODO: no test - .{ .src = "link.normalize()", .ex = "undefined" }, - }; - try checkCases(js_env, &node_normalize); + .{ "link.normalize()", "undefined" }, + }, .{}); - var node_remove_child = [_]Case{ - .{ .src = "content.removeChild(append) !== undefined", .ex = "true" }, - .{ .src = "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", .ex = "true" }, - }; - try checkCases(js_env, &node_remove_child); + try runner.testCases(&.{ + .{ "content.removeChild(append) !== undefined", "true" }, + .{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" }, + }, .{}); - var node_replace_child = [_]Case{ - .{ .src = "let replace = document.createElement('div')", .ex = "undefined" }, - .{ .src = "link.replaceChild(replace, insertBefore) !== undefined", .ex = "true" }, - }; - try checkCases(js_env, &node_replace_child); + try runner.testCases(&.{ + .{ "let replace = document.createElement('div')", "undefined" }, + .{ "link.replaceChild(replace, insertBefore) !== undefined", "true" }, + }, .{}); } diff --git a/src/dom/nodelist.zig b/src/browser/dom/nodelist.zig similarity index 64% rename from src/dom/nodelist.zig rename to src/browser/dom/nodelist.zig index 5dfc7031..2d27f4d0 100644 --- a/src/dom/nodelist.zig +++ b/src/browser/dom/nodelist.zig @@ -18,13 +18,11 @@ const std = @import("std"); -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); -const jsruntime = @import("jsruntime"); -const Callback = jsruntime.Callback; -const CallbackResult = jsruntime.CallbackResult; -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; +const JsObject = @import("../env.zig").JsObject; +const Callback = @import("../env.zig").Callback; +const SessionState = @import("../env.zig").SessionState; const NodeUnion = @import("node.zig").Union; const Node = @import("node.zig").Node; @@ -41,8 +39,6 @@ pub const Interfaces = .{ }; pub const NodeListIterator = struct { - pub const mem_guarantied = true; - coll: *NodeList, index: u32 = 0, @@ -69,8 +65,6 @@ pub const NodeListIterator = struct { }; pub const NodeListEntriesIterator = struct { - pub const mem_guarantied = true; - coll: *NodeList, index: u32 = 0, @@ -104,7 +98,6 @@ pub const NodeListEntriesIterator = struct { // implementation allows only static nodelist. // see https://dom.spec.whatwg.org/#old-style-collections pub const NodeList = struct { - pub const mem_guarantied = true; pub const Exception = DOMException; const NodesArrayList = std.ArrayListUnmanaged(*parser.Node); @@ -130,7 +123,7 @@ pub const NodeList = struct { return @intCast(self.nodes.items.len); } - pub fn _item(self: *NodeList, index: u32) !?NodeUnion { + pub fn _item(self: *const NodeList, index: u32) !?NodeUnion { if (index >= self.nodes.items.len) { return null; } @@ -139,17 +132,30 @@ pub const NodeList = struct { return try Node.toInterface(n); } - pub fn _forEach(self: *NodeList, alloc: std.mem.Allocator, cbk: Callback) !void { // TODO handle thisArg - var res = CallbackResult.init(alloc); - defer res.deinit(); + // This code works, but it's _MUCH_ slower than using postAttach. The benefit + // of this version, is that it's "live"..but we're talking many orders of + // magnitude slower. + // + // You can test it by commenting out `postAttach`, uncommenting this and + // running: + // zig build wpt -- tests/wpt/dom/nodes/NodeList-static-length-getter-tampered-indexOf-1.html + // + // I think this _is_ the right way to do it, but I must be doing something + // wrong to make it so slow. + // pub fn indexed_get(self: *const NodeList, index: u32, has_value: *bool) !?NodeUnion { + // return (try self._item(index)) orelse { + // has_value.* = false; + // return null; + // }; + // } + pub fn _forEach(self: *NodeList, cbk: Callback) !void { // TODO handle thisArg for (self.nodes.items, 0..) |n, i| { const ii: u32 = @intCast(i); - cbk.trycall(.{ n, ii, self }, &res) catch |e| { - log.err("callback error: {s}", .{res.result orelse "unknown"}); - log.debug("{s}", .{res.stack orelse "no stack trace"}); - - return e; + var result: Callback.Result = undefined; + cbk.tryCall(.{ n, ii, self }, &result) catch { + log.err("callback error: {s}", .{result.exception}); + log.debug("stack:\n{s}", .{result.stack orelse "???"}); }; } } @@ -171,38 +177,32 @@ pub const NodeList = struct { } // TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries - - pub fn postAttach(self: *NodeList, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void { - const ln = self.get_length(); - var i: u32 = 0; - while (i < ln) { - defer i += 1; - const k = try std.fmt.allocPrint(alloc, "{d}", .{i}); - - const node = try self._item(i) orelse unreachable; - try js_obj.set(k, node); + pub fn postAttach(self: *NodeList, js_obj: JsObject) !void { + const len = self.get_length(); + for (0..len) |i| { + const node = try self._item(@intCast(i)) orelse unreachable; + try js_obj.setIndex(i, node); } } }; -// Tests -// ----- +const testing = @import("../../testing.zig"); +test "Browser.DOM.NodeList" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var childnodes = [_]Case{ - .{ .src = "let list = document.getElementById('content').childNodes", .ex = "undefined" }, - .{ .src = "list.length", .ex = "9" }, - .{ .src = "list[0].__proto__.constructor.name", .ex = "Text" }, - .{ .src = - \\let i = 0; - \\list.forEach(function (n, idx) { - \\ i += idx; - \\}); - \\i; - , .ex = "36" }, - }; - try checkCases(js_env, &childnodes); + try runner.testCases(&.{ + .{ "let list = document.getElementById('content').childNodes", "undefined" }, + .{ "list.length", "9" }, + .{ "list[0].__proto__.constructor.name", "Text" }, + .{ + \\ let i = 0; + \\ list.forEach(function (n, idx) { + \\ i += idx; + \\ }); + \\ i; + , + "36", + }, + }, .{}); } diff --git a/src/dom/processing_instruction.zig b/src/browser/dom/processing_instruction.zig similarity index 71% rename from src/dom/processing_instruction.zig rename to src/browser/dom/processing_instruction.zig index fc932ec5..d8960490 100644 --- a/src/dom/processing_instruction.zig +++ b/src/browser/dom/processing_instruction.zig @@ -18,11 +18,7 @@ const std = @import("std"); -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); const Node = @import("node.zig").Node; // https://dom.spec.whatwg.org/#processinginstruction @@ -32,7 +28,6 @@ pub const ProcessingInstruction = struct { // TODO for libdom processing instruction inherit from node. // But the spec says it must inherit from CDATA. pub const prototype = *Node; - pub const mem_guarantied = true; pub fn get_target(self: *parser.ProcessingInstruction) ![]const u8 { // libdom stores the ProcessingInstruction target in the node's name. @@ -52,18 +47,18 @@ pub const ProcessingInstruction = struct { } }; -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var createProcessingInstruction = [_]Case{ - .{ .src = "let pi = document.createProcessingInstruction('foo', 'bar')", .ex = "undefined" }, - .{ .src = "pi.target", .ex = "foo" }, - .{ .src = "pi.data", .ex = "bar" }, - .{ .src = "pi.data = 'foo'", .ex = "foo" }, - .{ .src = "pi.data", .ex = "foo" }, +const testing = @import("../../testing.zig"); +test "Browser.DOM.ProcessingInstruction" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); - .{ .src = "let pi2 = pi.cloneNode()", .ex = "undefined" }, - }; - try checkCases(js_env, &createProcessingInstruction); + try runner.testCases(&.{ + .{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" }, + .{ "pi.target", "foo" }, + .{ "pi.data", "bar" }, + .{ "pi.data = 'foo'", "foo" }, + .{ "pi.data", "foo" }, + + .{ "let pi2 = pi.cloneNode()", "undefined" }, + }, .{}); } diff --git a/src/dom/text.zig b/src/browser/dom/text.zig similarity index 53% rename from src/dom/text.zig rename to src/browser/dom/text.zig index a2d3f9cf..aa99deca 100644 --- a/src/dom/text.zig +++ b/src/browser/dom/text.zig @@ -18,17 +18,12 @@ const std = @import("std"); -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); +const SessionState = @import("../env.zig").SessionState; const CharacterData = @import("character_data.zig").CharacterData; const CDATASection = @import("cdata_section.zig").CDATASection; -const UserContext = @import("../user_context.zig").UserContext; - // Text interfaces pub const Interfaces = .{ CDATASection, @@ -37,11 +32,10 @@ pub const Interfaces = .{ pub const Text = struct { pub const Self = parser.Text; pub const prototype = *CharacterData; - pub const mem_guarantied = true; - pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Text { + pub fn constructor(data: ?[]const u8, state: *const SessionState) !*parser.Text { return parser.documentCreateTextNode( - parser.documentHTMLToDocument(userctx.document), + parser.documentHTMLToDocument(state.document.?), data orelse "", ); } @@ -66,30 +60,28 @@ pub const Text = struct { // Tests // ----- -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var constructor = [_]Case{ - .{ .src = "let t = new Text('foo')", .ex = "undefined" }, - .{ .src = "t.data", .ex = "foo" }, +const testing = @import("../../testing.zig"); +test "Browser.DOM.Text" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); - .{ .src = "let emptyt = new Text()", .ex = "undefined" }, - .{ .src = "emptyt.data", .ex = "" }, - }; - try checkCases(js_env, &constructor); + try runner.testCases(&.{ + .{ "let t = new Text('foo')", "undefined" }, + .{ "t.data", "foo" }, - var get_whole_text = [_]Case{ - .{ .src = "let text = document.getElementById('link').firstChild", .ex = "undefined" }, - .{ .src = "text.wholeText === 'OK'", .ex = "true" }, - }; - try checkCases(js_env, &get_whole_text); + .{ "let emptyt = new Text()", "undefined" }, + .{ "emptyt.data", "" }, + }, .{}); - var split_text = [_]Case{ - .{ .src = "text.data = 'OK modified'", .ex = "OK modified" }, - .{ .src = "let split = text.splitText('OK'.length)", .ex = "undefined" }, - .{ .src = "split.data === ' modified'", .ex = "true" }, - .{ .src = "text.data === 'OK'", .ex = "true" }, - }; - try checkCases(js_env, &split_text); + try runner.testCases(&.{ + .{ "let text = document.getElementById('link').firstChild", "undefined" }, + .{ "text.wholeText === 'OK'", "true" }, + }, .{}); + + try runner.testCases(&.{ + .{ "text.data = 'OK modified'", "OK modified" }, + .{ "let split = text.splitText('OK'.length)", "undefined" }, + .{ "split.data === ' modified'", "true" }, + .{ "text.data === 'OK'", "true" }, + }, .{}); } diff --git a/src/dom/token_list.zig b/src/browser/dom/token_list.zig similarity index 56% rename from src/dom/token_list.zig rename to src/browser/dom/token_list.zig index 0ed75997..0472e81e 100644 --- a/src/dom/token_list.zig +++ b/src/browser/dom/token_list.zig @@ -18,12 +18,7 @@ const std = @import("std"); -const parser = @import("netsurf"); - -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; -const Variadic = jsruntime.Variadic; +const parser = @import("../netsurf.zig"); const DOMException = @import("exceptions.zig").DOMException; @@ -31,7 +26,6 @@ const DOMException = @import("exceptions.zig").DOMException; pub const DOMTokenList = struct { pub const Self = parser.TokenList; pub const Exception = DOMException; - pub const mem_guarantied = true; pub fn get_length(self: *parser.TokenList) !u32 { return parser.tokenListGetLength(self); @@ -45,16 +39,14 @@ pub const DOMTokenList = struct { return parser.tokenListContains(self, token); } - pub fn _add(self: *parser.TokenList, tokens: ?Variadic([]const u8)) !void { - if (tokens == null) return; - for (tokens.?.slice) |token| { + pub fn _add(self: *parser.TokenList, tokens: []const []const u8) !void { + for (tokens) |token| { try parser.tokenListAdd(self, token); } } - pub fn _remove(self: *parser.TokenList, tokens: ?Variadic([]const u8)) !void { - if (tokens == null) return; - for (tokens.?.slice) |token| { + pub fn _remove(self: *parser.TokenList, tokens: []const []const u8) !void { + for (tokens) |token| { try parser.tokenListRemove(self, token); } } @@ -113,52 +105,49 @@ pub const DOMTokenList = struct { // Tests // ----- -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var dynamiclist = [_]Case{ - .{ .src = "let gs = document.getElementById('para-empty')", .ex = "undefined" }, - .{ .src = "let cl = gs.classList", .ex = "undefined" }, - .{ .src = "gs.className", .ex = "ok empty" }, - .{ .src = "cl.value", .ex = "ok empty" }, - .{ .src = "cl.length", .ex = "2" }, - .{ .src = "gs.className = 'foo bar baz'", .ex = "foo bar baz" }, - .{ .src = "gs.className", .ex = "foo bar baz" }, - .{ .src = "cl.length", .ex = "3" }, - .{ .src = "gs.className = 'ok empty'", .ex = "ok empty" }, - .{ .src = "cl.length", .ex = "2" }, - }; - try checkCases(js_env, &dynamiclist); +const testing = @import("../../testing.zig"); +test "Browser.DOM.TokenList" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); - var testcases = [_]Case{ - .{ .src = "let cl2 = gs.classList", .ex = "undefined" }, - .{ .src = "cl2.length", .ex = "2" }, - .{ .src = "cl2.item(0)", .ex = "ok" }, - .{ .src = "cl2.item(1)", .ex = "empty" }, - .{ .src = "cl2.contains('ok')", .ex = "true" }, - .{ .src = "cl2.contains('nok')", .ex = "false" }, - .{ .src = "cl2.add('foo', 'bar', 'baz')", .ex = "undefined" }, - .{ .src = "cl2.length", .ex = "5" }, - .{ .src = "cl2.remove('foo', 'bar', 'baz')", .ex = "undefined" }, - .{ .src = "cl2.length", .ex = "2" }, - }; - try checkCases(js_env, &testcases); + try runner.testCases(&.{ + .{ "let gs = document.getElementById('para-empty')", "undefined" }, + .{ "let cl = gs.classList", "undefined" }, + .{ "gs.className", "ok empty" }, + .{ "cl.value", "ok empty" }, + .{ "cl.length", "2" }, + .{ "gs.className = 'foo bar baz'", "foo bar baz" }, + .{ "gs.className", "foo bar baz" }, + .{ "cl.length", "3" }, + .{ "gs.className = 'ok empty'", "ok empty" }, + .{ "cl.length", "2" }, + }, .{}); - var toogle = [_]Case{ - .{ .src = "let cl3 = gs.classList", .ex = "undefined" }, - .{ .src = "cl3.toggle('ok')", .ex = "false" }, - .{ .src = "cl3.toggle('ok')", .ex = "true" }, - .{ .src = "cl3.length", .ex = "2" }, - }; - try checkCases(js_env, &toogle); + try runner.testCases(&.{ + .{ "let cl2 = gs.classList", "undefined" }, + .{ "cl2.length", "2" }, + .{ "cl2.item(0)", "ok" }, + .{ "cl2.item(1)", "empty" }, + .{ "cl2.contains('ok')", "true" }, + .{ "cl2.contains('nok')", "false" }, + .{ "cl2.add('foo', 'bar', 'baz')", "undefined" }, + .{ "cl2.length", "5" }, + .{ "cl2.remove('foo', 'bar', 'baz')", "undefined" }, + .{ "cl2.length", "2" }, + }, .{}); - var replace = [_]Case{ - .{ .src = "let cl4 = gs.classList", .ex = "undefined" }, - .{ .src = "cl4.replace('ok', 'nok')", .ex = "true" }, - .{ .src = "cl4.value", .ex = "empty nok" }, - .{ .src = "cl4.replace('nok', 'ok')", .ex = "true" }, - .{ .src = "cl4.value", .ex = "empty ok" }, - }; - try checkCases(js_env, &replace); + try runner.testCases(&.{ + .{ "let cl3 = gs.classList", "undefined" }, + .{ "cl3.toggle('ok')", "false" }, + .{ "cl3.toggle('ok')", "true" }, + .{ "cl3.length", "2" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let cl4 = gs.classList", "undefined" }, + .{ "cl4.replace('ok', 'nok')", "true" }, + .{ "cl4.value", "empty nok" }, + .{ "cl4.replace('nok', 'ok')", "true" }, + .{ "cl4.value", "empty ok" }, + }, .{}); } diff --git a/src/dom/walker.zig b/src/browser/dom/walker.zig similarity index 98% rename from src/dom/walker.zig rename to src/browser/dom/walker.zig index 6f2c2fba..ad7ba5f7 100644 --- a/src/dom/walker.zig +++ b/src/browser/dom/walker.zig @@ -18,7 +18,7 @@ const std = @import("std"); -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); pub const Walker = union(enum) { walkerDepthFirst: WalkerDepthFirst, diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 23fff552..4c4e7996 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -19,8 +19,8 @@ const std = @import("std"); const File = std.fs.File; -const parser = @import("netsurf"); -const Walker = @import("../dom/walker.zig").WalkerChildren; +const parser = @import("netsurf.zig"); +const Walker = @import("dom/walker.zig").WalkerChildren; // writer must be a std.io.Writer pub fn writeHTML(doc: *parser.Document, writer: anytype) !void { diff --git a/src/browser/env.zig b/src/browser/env.zig new file mode 100644 index 00000000..0fd4370d --- /dev/null +++ b/src/browser/env.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +const parser = @import("netsurf.zig"); +const URL = @import("../url.zig").URL; +const js = @import("../runtime/js.zig"); +const storage = @import("storage/storage.zig"); +const generate = @import("../runtime/generate.zig"); +const Loop = @import("../runtime/loop.zig").Loop; +const HttpClient = @import("../http/client.zig").Client; +const Renderer = @import("browser.zig").Renderer; + +const Interfaces = generate.Tuple(.{ + @import("console/console.zig").Console, + @import("dom/dom.zig").Interfaces, + @import("events/event.zig").Interfaces, + @import("html/html.zig").Interfaces, + @import("iterator/iterator.zig").Interfaces, + @import("storage/storage.zig").Interfaces, + @import("url/url.zig").Interfaces, + @import("xhr/xhr.zig").Interfaces, + @import("xmlserializer/xmlserializer.zig").Interfaces, +}); + +pub const JsObject = Env.JsObject; +pub const Callback = Env.Callback; +pub const Env = js.Env(*SessionState, Interfaces{}); + +pub const SessionState = struct { + loop: *Loop, + url: *const URL, + renderer: *Renderer, + arena: std.mem.Allocator, + http_client: *HttpClient, + cookie_jar: *storage.CookieJar, + document: ?*parser.DocumentHTML, +}; diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig new file mode 100644 index 00000000..517b852d --- /dev/null +++ b/src/browser/events/event.zig @@ -0,0 +1,245 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const parser = @import("../netsurf.zig"); +const Callback = @import("../env.zig").Callback; +const generate = @import("../../runtime/generate.zig"); + +const DOMException = @import("../dom/exceptions.zig").DOMException; +const EventTarget = @import("../dom/event_target.zig").EventTarget; +const EventTargetUnion = @import("../dom/event_target.zig").Union; + +const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent; + +const log = std.log.scoped(.events); + +// Event interfaces +pub const Interfaces = .{ + Event, + ProgressEvent, +}; + +pub const Union = generate.Union(Interfaces); + +// https://dom.spec.whatwg.org/#event +pub const Event = struct { + pub const Self = parser.Event; + pub const Exception = DOMException; + + pub const EventInit = parser.EventInit; + + // JS + // -- + + pub const _CAPTURING_PHASE = 1; + pub const _AT_TARGET = 2; + pub const _BUBBLING_PHASE = 3; + + pub fn toInterface(evt: *parser.Event) !Union { + return switch (try parser.eventGetInternalType(evt)) { + .event => .{ .Event = evt }, + .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* }, + }; + } + + pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event { + const event = try parser.eventCreate(); + try parser.eventInit(event, eventType, opts orelse EventInit{}); + return event; + } + + // Getters + + pub fn get_type(self: *parser.Event) ![]const u8 { + return try parser.eventType(self); + } + + pub fn get_target(self: *parser.Event) !?EventTargetUnion { + const et = try parser.eventTarget(self); + if (et == null) return null; + return try EventTarget.toInterface(et.?); + } + + pub fn get_currentTarget(self: *parser.Event) !?EventTargetUnion { + const et = try parser.eventCurrentTarget(self); + if (et == null) return null; + return try EventTarget.toInterface(et.?); + } + + pub fn get_eventPhase(self: *parser.Event) !u8 { + return try parser.eventPhase(self); + } + + pub fn get_bubbles(self: *parser.Event) !bool { + return try parser.eventBubbles(self); + } + + pub fn get_cancelable(self: *parser.Event) !bool { + return try parser.eventCancelable(self); + } + + pub fn get_defaultPrevented(self: *parser.Event) !bool { + return try parser.eventDefaultPrevented(self); + } + + pub fn get_isTrusted(self: *parser.Event) !bool { + return try parser.eventIsTrusted(self); + } + + pub fn get_timestamp(self: *parser.Event) !u32 { + return try parser.eventTimestamp(self); + } + + // Methods + + pub fn _initEvent( + self: *parser.Event, + eventType: []const u8, + bubbles: ?bool, + cancelable: ?bool, + ) !void { + const opts = EventInit{ + .bubbles = bubbles orelse false, + .cancelable = cancelable orelse false, + }; + return try parser.eventInit(self, eventType, opts); + } + + pub fn _stopPropagation(self: *parser.Event) !void { + return try parser.eventStopPropagation(self); + } + + pub fn _stopImmediatePropagation(self: *parser.Event) !void { + return try parser.eventStopImmediatePropagation(self); + } + + pub fn _preventDefault(self: *parser.Event) !void { + return try parser.eventPreventDefault(self); + } +}; + +pub const EventHandler = struct { + fn handle(event: ?*parser.Event, data: parser.EventHandlerData) void { + var result: Callback.Result = undefined; + data.cbk.tryCall(.{if (event) |evt| Event.toInterface(evt) catch unreachable else null}, &result) catch { + log.err("event handler error: {s}", .{result.exception}); + log.debug("stack:\n{s}", .{result.stack orelse "???"}); + }; + } +}.handle; + +const testing = @import("../../testing.zig"); +test "Browser.Event" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "let content = document.getElementById('content')", "undefined" }, + .{ "let para = document.getElementById('para')", "undefined" }, + .{ "var nb = 0; var evt", "undefined" }, + }, .{}); + + try runner.testCases(&.{ + .{ + \\ content.addEventListener('target', function(e) { + \\ evt = e; nb = nb + 1; + \\ e.preventDefault(); + \\ }) + , + "undefined", + }, + .{ "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", "false" }, + .{ "nb", "1" }, + .{ "evt.target === content", "true" }, + .{ "evt.bubbles", "true" }, + .{ "evt.cancelable", "true" }, + .{ "evt.defaultPrevented", "true" }, + .{ "evt.isTrusted", "true" }, + .{ "evt.timestamp > 1704063600", "true" }, // 2024/01/01 00:00 + // event.type, event.currentTarget, event.phase checked in EventTarget + }, .{}); + + try runner.testCases(&.{ + .{ "nb = 0", "0" }, + .{ + \\ content.addEventListener('stop',function(e) { + \\ e.stopPropagation(); + \\ nb = nb + 1; + \\ }, true) + , + "undefined", + }, + // the following event listener will not be invoked + .{ + \\ para.addEventListener('stop',function(e) { + \\ nb = nb + 1; + \\ }) + , + "undefined", + }, + .{ "para.dispatchEvent(new Event('stop'))", "true" }, + .{ "nb", "1" }, // will be 2 if event was not stopped at content event listener + }, .{}); + + try runner.testCases(&.{ + .{ "nb = 0", "0" }, + .{ + \\ content.addEventListener('immediate', function(e) { + \\ e.stopImmediatePropagation(); + \\ nb = nb + 1; + \\ }) + , + "undefined", + }, + // the following event listener will not be invoked + .{ + \\ content.addEventListener('immediate', function(e) { + \\ nb = nb + 1; + \\ }) + , + "undefined", + }, + .{ "content.dispatchEvent(new Event('immediate'))", "true" }, + .{ "nb", "1" }, // will be 2 if event was not stopped at first content event listener + }, .{}); + + try runner.testCases(&.{ + .{ "nb = 0", "0" }, + .{ + \\ content.addEventListener('legacy', function(e) { + \\ evt = e; nb = nb + 1; + \\ }) + , + "undefined", + }, + .{ "let evtLegacy = document.createEvent('Event')", "undefined" }, + .{ "evtLegacy.initEvent('legacy')", "undefined" }, + .{ "content.dispatchEvent(evtLegacy)", "true" }, + .{ "nb", "1" }, + }, .{}); + + try runner.testCases(&.{ + .{ "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", "undefined" }, + .{ "document.addEventListener('count', cbk)", "undefined" }, + .{ "document.removeEventListener('count', cbk)", "undefined" }, + .{ "document.dispatchEvent(new Event('count'))", "true" }, + .{ "nb", "0" }, + }, .{}); +} diff --git a/src/html/document.zig b/src/browser/html/document.zig similarity index 57% rename from src/html/document.zig rename to src/browser/html/document.zig index 19567078..d434ee21 100644 --- a/src/html/document.zig +++ b/src/browser/html/document.zig @@ -18,11 +18,8 @@ const std = @import("std"); -const parser = @import("netsurf"); - -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; +const parser = @import("../netsurf.zig"); +const SessionState = @import("../env.zig").SessionState; const Node = @import("../dom/node.zig").Node; const Document = @import("../dom/document.zig").Document; @@ -32,15 +29,12 @@ const Location = @import("location.zig").Location; const collection = @import("../dom/html_collection.zig"); const Walker = @import("../dom/walker.zig").WalkerDepthFirst; - -const UserContext = @import("../user_context.zig").UserContext; const Cookie = @import("../storage/cookie.zig").Cookie; // WEB IDL https://html.spec.whatwg.org/#the-document-object pub const HTMLDocument = struct { pub const Self = parser.DocumentHTML; pub const prototype = *Document; - pub const mem_guarantied = true; pub const sub_type = "node"; @@ -84,20 +78,18 @@ pub const HTMLDocument = struct { } } - pub fn get_cookie(_: *parser.DocumentHTML, arena: std.mem.Allocator, userctx: UserContext) ![]const u8 { + pub fn get_cookie(_: *parser.DocumentHTML, state: *SessionState) ![]const u8 { var buf: std.ArrayListUnmanaged(u8) = .{}; - try userctx.cookie_jar.forRequest(&userctx.url.uri, buf.writer(arena), .{ .navigation = true }); + try state.cookie_jar.forRequest(&state.url.uri, buf.writer(state.arena), .{ .navigation = true }); return buf.items; } - pub fn set_cookie(_: *parser.DocumentHTML, userctx: UserContext, cookie_str: []const u8) ![]const u8 { + pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, state: *SessionState) ![]const u8 { // we use the cookie jar's allocator to parse the cookie because it // outlives the page's arena. - const c = try Cookie.parse(userctx.cookie_jar.allocator, &userctx.url.uri, cookie_str); + const c = try Cookie.parse(state.cookie_jar.allocator, &state.url.uri, cookie_str); errdefer c.deinit(); - - try userctx.cookie_jar.add(c, std.time.timestamp()); - + try state.cookie_jar.add(c, std.time.timestamp()); return cookie_str; } @@ -110,44 +102,45 @@ pub const HTMLDocument = struct { return v; } - pub fn _getElementsByName(self: *parser.DocumentHTML, alloc: std.mem.Allocator, name: []const u8) !NodeList { + pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, state: *SessionState) !NodeList { + const arena = state.arena; var list = NodeList.init(); - errdefer list.deinit(alloc); + errdefer list.deinit(arena); if (name.len == 0) return list; const root = parser.documentHTMLToNode(self); - var c = try collection.HTMLCollectionByName(alloc, root, name, false); + var c = try collection.HTMLCollectionByName(arena, root, name, false); const ln = try c.get_length(); var i: u32 = 0; while (i < ln) { const n = try c.item(i) orelse break; - try list.append(alloc, n); + try list.append(arena, n); i += 1; } return list; } - pub fn get_images(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection { - return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "img", false); + pub fn get_images(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection { + return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "img", false); } - pub fn get_embeds(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection { - return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "embed", false); + pub fn get_embeds(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection { + return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "embed", false); } - pub fn get_plugins(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection { - return get_embeds(self, alloc); + pub fn get_plugins(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection { + return get_embeds(self, state); } - pub fn get_forms(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection { - return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "form", false); + pub fn get_forms(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection { + return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "form", false); } - pub fn get_scripts(self: *parser.DocumentHTML, alloc: std.mem.Allocator) !collection.HTMLCollection { - return try collection.HTMLCollectionByTagName(alloc, parser.documentHTMLToNode(self), "script", false); + pub fn get_scripts(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection { + return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "script", false); } pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection { @@ -218,62 +211,57 @@ pub const HTMLDocument = struct { pub fn set_bgColor(_: *parser.DocumentHTML, _: []const u8) []const u8 { return ""; } - - pub fn deinit(_: *parser.DocumentHTML, _: std.mem.Allocator) void {} }; // Tests // ----- -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var constructor = [_]Case{ - .{ .src = "document.__proto__.constructor.name", .ex = "HTMLDocument" }, - .{ .src = "document.__proto__.__proto__.constructor.name", .ex = "Document" }, - .{ .src = "document.body.localName == 'body'", .ex = "true" }, - }; - try checkCases(js_env, &constructor); +const testing = @import("../../testing.zig"); - var getters = [_]Case{ - .{ .src = "document.domain", .ex = "" }, - .{ .src = "document.referrer", .ex = "" }, - .{ .src = "document.title", .ex = "" }, - .{ .src = "document.body.localName", .ex = "body" }, - .{ .src = "document.head.localName", .ex = "head" }, - .{ .src = "document.images.length", .ex = "0" }, - .{ .src = "document.embeds.length", .ex = "0" }, - .{ .src = "document.plugins.length", .ex = "0" }, - .{ .src = "document.scripts.length", .ex = "0" }, - .{ .src = "document.forms.length", .ex = "0" }, - .{ .src = "document.links.length", .ex = "1" }, - .{ .src = "document.applets.length", .ex = "0" }, - .{ .src = "document.anchors.length", .ex = "0" }, - .{ .src = "document.all.length", .ex = "8" }, - .{ .src = "document.currentScript", .ex = "null" }, - }; - try checkCases(js_env, &getters); +test "Browser.HTML.Document" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); - var titles = [_]Case{ - .{ .src = "document.title = 'foo'", .ex = "foo" }, - .{ .src = "document.title", .ex = "foo" }, - .{ .src = "document.title = ''", .ex = "" }, - }; - try checkCases(js_env, &titles); + try runner.testCases(&.{ + .{ "document.__proto__.constructor.name", "HTMLDocument" }, + .{ "document.__proto__.__proto__.constructor.name", "Document" }, + .{ "document.body.localName == 'body'", "true" }, + }, .{}); - var getElementsByName = [_]Case{ - .{ .src = "document.getElementById('link').setAttribute('name', 'foo')", .ex = "undefined" }, - .{ .src = "let list = document.getElementsByName('foo')", .ex = "undefined" }, - .{ .src = "list.length", .ex = "1" }, - }; - try checkCases(js_env, &getElementsByName); + try runner.testCases(&.{ + .{ "document.domain", "" }, + .{ "document.referrer", "" }, + .{ "document.title", "" }, + .{ "document.body.localName", "body" }, + .{ "document.head.localName", "head" }, + .{ "document.images.length", "0" }, + .{ "document.embeds.length", "0" }, + .{ "document.plugins.length", "0" }, + .{ "document.scripts.length", "0" }, + .{ "document.forms.length", "0" }, + .{ "document.links.length", "1" }, + .{ "document.applets.length", "0" }, + .{ "document.anchors.length", "0" }, + .{ "document.all.length", "8" }, + .{ "document.currentScript", "null" }, + }, .{}); - var cookie = [_]Case{ - .{ .src = "document.cookie", .ex = "" }, - .{ .src = "document.cookie = 'name=Oeschger; SameSite=None; Secure'", .ex = "name=Oeschger; SameSite=None; Secure" }, - .{ .src = "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", .ex = "favorite_food=tripe; SameSite=None; Secure" }, - .{ .src = "document.cookie", .ex = "name=Oeschger; favorite_food=tripe" }, - }; - try checkCases(js_env, &cookie); + try runner.testCases(&.{ + .{ "document.title = 'foo'", "foo" }, + .{ "document.title", "foo" }, + .{ "document.title = ''", "" }, + }, .{}); + + try runner.testCases(&.{ + .{ "document.getElementById('link').setAttribute('name', 'foo')", "undefined" }, + .{ "let list = document.getElementsByName('foo')", "undefined" }, + .{ "list.length", "1" }, + }, .{}); + + try runner.testCases(&.{ + .{ "document.cookie", "" }, + .{ "document.cookie = 'name=Oeschger; SameSite=None; Secure'", "name=Oeschger; SameSite=None; Secure" }, + .{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" }, + .{ "document.cookie", "name=Oeschger; favorite_food=tripe" }, + }, .{}); } diff --git a/src/html/elements.zig b/src/browser/html/elements.zig similarity index 69% rename from src/html/elements.zig rename to src/browser/html/elements.zig index d0ff13a4..0cee0cca 100644 --- a/src/html/elements.zig +++ b/src/browser/html/elements.zig @@ -17,16 +17,13 @@ // along with this program. If not, see . const std = @import("std"); -const parser = @import("netsurf"); -const generate = @import("../generate.zig"); +const parser = @import("../netsurf.zig"); +const generate = @import("../../runtime/generate.zig"); +const SessionState = @import("../env.zig").SessionState; -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - -const Element = @import("../dom/element.zig").Element; const URL = @import("../url/url.zig").URL; const Node = @import("../dom/node.zig").Node; +const Element = @import("../dom/element.zig").Element; // HTMLElement interfaces pub const Interfaces = .{ @@ -105,14 +102,11 @@ pub const Union = generate.Union(Interfaces); // Abstract class // -------------- -const CSSProperties = struct { - pub const mem_guarantied = true; -}; +const CSSProperties = struct {}; pub const HTMLElement = struct { pub const Self = parser.ElementHTML; pub const prototype = *Element; - pub const mem_guarantied = true; pub fn get_style(_: *parser.ElementHTML) CSSProperties { return .{}; @@ -148,7 +142,6 @@ pub const HTMLElement = struct { pub const HTMLMediaElement = struct { pub const Self = parser.MediaElement; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; // HTML elements @@ -157,14 +150,12 @@ pub const HTMLMediaElement = struct { pub const HTMLUnknownElement = struct { pub const Self = parser.Unknown; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; // https://html.spec.whatwg.org/#the-a-element pub const HTMLAnchorElement = struct { pub const Self = parser.Anchor; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; pub fn get_target(self: *parser.Anchor) ![]const u8 { return try parser.anchorGetTarget(self); @@ -174,7 +165,7 @@ pub const HTMLAnchorElement = struct { return try parser.anchorSetTarget(self, href); } - pub fn get_download(_: *parser.Anchor) ![]const u8 { + pub fn get_download(_: *const parser.Anchor) ![]const u8 { return ""; // TODO } @@ -218,47 +209,39 @@ pub const HTMLAnchorElement = struct { return try parser.nodeSetTextContent(parser.anchorToNode(self), v); } - inline fn url(self: *parser.Anchor, alloc: std.mem.Allocator) !URL { + inline fn url(self: *parser.Anchor, state: *SessionState) !URL { const href = try parser.anchorGetHref(self); - return URL.constructor(alloc, href, null); // TODO inject base url + return URL.constructor(href, null, state); // TODO inject base url } // TODO return a disposable string - pub fn get_origin(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { - var u = try url(self, alloc); - defer u.deinit(alloc); - - return try u.get_origin(alloc); + pub fn get_origin(self: *parser.Anchor, state: *SessionState) ![]const u8 { + var u = try url(self, state); + return try u.get_origin(state); } // TODO return a disposable string - pub fn get_protocol(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { - var u = try url(self, alloc); - defer u.deinit(alloc); - - return u.get_protocol(alloc); + pub fn get_protocol(self: *parser.Anchor, state: *SessionState) ![]const u8 { + var u = try url(self, state); + return u.get_protocol(state); } - pub fn set_protocol(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void { - var u = try url(self, alloc); - defer u.deinit(alloc); + pub fn set_protocol(self: *parser.Anchor, v: []const u8, state: *SessionState) !void { + const arena = state.arena; + var u = try url(self, state); u.uri.scheme = v; - const href = try u.format(alloc); - defer alloc.free(href); - + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } // TODO return a disposable string - pub fn get_host(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { - var u = try url(self, alloc); - defer u.deinit(alloc); - - return try u.get_host(alloc); + pub fn get_host(self: *parser.Anchor, state: *SessionState) ![]const u8 { + var u = try url(self, state); + return try u.get_host(state); } - pub fn set_host(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void { + pub fn set_host(self: *parser.Anchor, v: []const u8, state: *SessionState) !void { // search : separator var p: ?u16 = null; var h: []const u8 = undefined; @@ -270,8 +253,8 @@ pub const HTMLAnchorElement = struct { } } - var u = try url(self, alloc); - defer u.deinit(alloc); + const arena = state.arena; + var u = try url(self, state); if (p) |pp| { u.uri.host = .{ .raw = h }; @@ -281,40 +264,33 @@ pub const HTMLAnchorElement = struct { u.uri.port = null; } - const href = try u.format(alloc); - defer alloc.free(href); - + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } // TODO return a disposable string - pub fn get_hostname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { - var u = try url(self, alloc); - defer u.deinit(alloc); - - return try alloc.dupe(u8, u.get_hostname()); + pub fn get_hostname(self: *parser.Anchor, state: *SessionState) ![]const u8 { + var u = try url(self, state); + return try state.arena.dupe(u8, u.get_hostname()); } - pub fn set_hostname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void { - var u = try url(self, alloc); - defer u.deinit(alloc); - + pub fn set_hostname(self: *parser.Anchor, v: []const u8, state: *SessionState) !void { + const arena = state.arena; + var u = try url(self, state); u.uri.host = .{ .raw = v }; - const href = try u.format(alloc); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } // TODO return a disposable string - pub fn get_port(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { - var u = try url(self, alloc); - defer u.deinit(alloc); - - return try u.get_port(alloc); + pub fn get_port(self: *parser.Anchor, state: *SessionState) ![]const u8 { + var u = try url(self, state); + return try u.get_port(state); } - pub fn set_port(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void { - var u = try url(self, alloc); - defer u.deinit(alloc); + pub fn set_port(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void { + const arena = state.arena; + var u = try url(self, state); if (v != null and v.?.len > 0) { u.uri.port = try std.fmt.parseInt(u16, v.?, 10); @@ -322,407 +298,340 @@ pub const HTMLAnchorElement = struct { u.uri.port = null; } - const href = try u.format(alloc); - defer alloc.free(href); - + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } // TODO return a disposable string - pub fn get_username(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { - var u = try url(self, alloc); - defer u.deinit(alloc); - - return try alloc.dupe(u8, u.get_username()); + pub fn get_username(self: *parser.Anchor, state: *SessionState) ![]const u8 { + var u = try url(self, state); + return try state.arena.dupe(u8, u.get_username()); } - pub fn set_username(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void { - var u = try url(self, alloc); - defer u.deinit(alloc); + pub fn set_username(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void { + const arena = state.arena; + var u = try url(self, state); if (v) |vv| { u.uri.user = .{ .raw = vv }; } else { u.uri.user = null; } - const href = try u.format(alloc); - defer alloc.free(href); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } // TODO return a disposable string - pub fn get_password(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { - var u = try url(self, alloc); - defer u.deinit(alloc); - - return try alloc.dupe(u8, u.get_password()); + pub fn get_password(self: *parser.Anchor, state: *SessionState) ![]const u8 { + var u = try url(self, state); + return try state.arena.dupe(u8, u.get_password()); } - pub fn set_password(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void { - var u = try url(self, alloc); - defer u.deinit(alloc); + pub fn set_password(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void { + const arena = state.arena; + var u = try url(self, state); if (v) |vv| { u.uri.password = .{ .raw = vv }; } else { u.uri.password = null; } - const href = try u.format(alloc); - defer alloc.free(href); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } // TODO return a disposable string - pub fn get_pathname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { - var u = try url(self, alloc); - defer u.deinit(alloc); - - return try alloc.dupe(u8, u.get_pathname()); + pub fn get_pathname(self: *parser.Anchor, state: *SessionState) ![]const u8 { + var u = try url(self, state); + return try state.arena.dupe(u8, u.get_pathname()); } - pub fn set_pathname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void { - var u = try url(self, alloc); - defer u.deinit(alloc); - + pub fn set_pathname(self: *parser.Anchor, v: []const u8, state: *SessionState) !void { + const arena = state.arena; + var u = try url(self, state); u.uri.path = .{ .raw = v }; - const href = try u.format(alloc); - defer alloc.free(href); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } // TODO return a disposable string - pub fn get_search(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { - var u = try url(self, alloc); - defer u.deinit(alloc); - - return try u.get_search(alloc); + pub fn get_search(self: *parser.Anchor, state: *SessionState) ![]const u8 { + var u = try url(self, state); + return try u.get_search(state); } - pub fn set_search(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void { - var u = try url(self, alloc); - defer u.deinit(alloc); + pub fn set_search(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void { + const arena = state.arena; + var u = try url(self, state); if (v) |vv| { u.uri.query = .{ .raw = vv }; } else { u.uri.query = null; } - const href = try u.format(alloc); - defer alloc.free(href); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } // TODO return a disposable string - pub fn get_hash(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 { - var u = try url(self, alloc); - defer u.deinit(alloc); - - return try u.get_hash(alloc); + pub fn get_hash(self: *parser.Anchor, state: *SessionState) ![]const u8 { + var u = try url(self, state); + return try u.get_hash(state); } - pub fn set_hash(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void { - var u = try url(self, alloc); - defer u.deinit(alloc); + pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, state: *SessionState) !void { + const arena = state.arena; + var u = try url(self, state); if (v) |vv| { u.uri.fragment = .{ .raw = vv }; } else { u.uri.fragment = null; } - const href = try u.format(alloc); - defer alloc.free(href); + const href = try u.toString(arena); try parser.anchorSetHref(self, href); } - - pub fn deinit(_: *parser.Anchor, _: std.mem.Allocator) void {} }; pub const HTMLAppletElement = struct { pub const Self = parser.Applet; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLAreaElement = struct { pub const Self = parser.Area; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLAudioElement = struct { pub const Self = parser.Audio; pub const prototype = *HTMLMediaElement; - pub const mem_guarantied = true; }; pub const HTMLBRElement = struct { pub const Self = parser.BR; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLBaseElement = struct { pub const Self = parser.Base; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLBodyElement = struct { pub const Self = parser.Body; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLButtonElement = struct { pub const Self = parser.Button; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLCanvasElement = struct { pub const Self = parser.Canvas; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLDListElement = struct { pub const Self = parser.DList; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLDataElement = struct { pub const Self = parser.Data; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLDataListElement = struct { pub const Self = parser.DataList; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLDialogElement = struct { pub const Self = parser.Dialog; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLDirectoryElement = struct { pub const Self = parser.Directory; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLDivElement = struct { pub const Self = parser.Div; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLEmbedElement = struct { pub const Self = parser.Embed; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLFieldSetElement = struct { pub const Self = parser.FieldSet; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLFontElement = struct { pub const Self = parser.Font; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLFormElement = struct { pub const Self = parser.Form; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLFrameElement = struct { pub const Self = parser.Frame; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLFrameSetElement = struct { pub const Self = parser.FrameSet; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLHRElement = struct { pub const Self = parser.HR; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLHeadElement = struct { pub const Self = parser.Head; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLHeadingElement = struct { pub const Self = parser.Heading; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLHtmlElement = struct { pub const Self = parser.Html; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLIFrameElement = struct { pub const Self = parser.IFrame; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLImageElement = struct { pub const Self = parser.Image; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLInputElement = struct { pub const Self = parser.Input; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLLIElement = struct { pub const Self = parser.LI; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLLabelElement = struct { pub const Self = parser.Label; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLLegendElement = struct { pub const Self = parser.Legend; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLLinkElement = struct { pub const Self = parser.Link; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLMapElement = struct { pub const Self = parser.Map; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLMetaElement = struct { pub const Self = parser.Meta; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLMeterElement = struct { pub const Self = parser.Meter; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLModElement = struct { pub const Self = parser.Mod; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLOListElement = struct { pub const Self = parser.OList; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLObjectElement = struct { pub const Self = parser.Object; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLOptGroupElement = struct { pub const Self = parser.OptGroup; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLOptionElement = struct { pub const Self = parser.Option; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLOutputElement = struct { pub const Self = parser.Output; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLParagraphElement = struct { pub const Self = parser.Paragraph; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLParamElement = struct { pub const Self = parser.Param; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLPictureElement = struct { pub const Self = parser.Picture; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLPreElement = struct { pub const Self = parser.Pre; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLProgressElement = struct { pub const Self = parser.Progress; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLQuoteElement = struct { pub const Self = parser.Quote; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; // https://html.spec.whatwg.org/#the-script-element pub const HTMLScriptElement = struct { pub const Self = parser.Script; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; pub fn get_src(self: *parser.Script) !?[]const u8 { return try parser.elementGetAttribute( @@ -837,103 +746,86 @@ pub const HTMLScriptElement = struct { pub const HTMLSelectElement = struct { pub const Self = parser.Select; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLSourceElement = struct { pub const Self = parser.Source; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLSpanElement = struct { pub const Self = parser.Span; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLStyleElement = struct { pub const Self = parser.Style; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLTableElement = struct { pub const Self = parser.Table; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLTableCaptionElement = struct { pub const Self = parser.TableCaption; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLTableCellElement = struct { pub const Self = parser.TableCell; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLTableColElement = struct { pub const Self = parser.TableCol; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLTableRowElement = struct { pub const Self = parser.TableRow; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLTableSectionElement = struct { pub const Self = parser.TableSection; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLTemplateElement = struct { pub const Self = parser.Template; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLTextAreaElement = struct { pub const Self = parser.TextArea; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLTimeElement = struct { pub const Self = parser.Time; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLTitleElement = struct { pub const Self = parser.Title; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLTrackElement = struct { pub const Self = parser.Track; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLUListElement = struct { pub const Self = parser.UList; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub const HTMLVideoElement = struct { pub const Self = parser.Video; pub const prototype = *HTMLElement; - pub const mem_guarantied = true; }; pub fn toInterface(comptime T: type, e: *parser.Element) !T { @@ -1010,89 +902,84 @@ pub fn toInterface(comptime T: type, e: *parser.Element) !T { }; } -// Tests -// ----- +const testing = @import("../../testing.zig"); +test "Browser.HTML.Element" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var anchor = [_]Case{ - .{ .src = "let a = document.getElementById('link')", .ex = "undefined" }, - .{ .src = "a.target", .ex = "" }, - .{ .src = "a.target = '_blank'", .ex = "_blank" }, - .{ .src = "a.target", .ex = "_blank" }, - .{ .src = "a.target = ''", .ex = "" }, + try runner.testCases(&.{ + .{ "let a = document.getElementById('link')", "undefined" }, + .{ "a.target", "" }, + .{ "a.target = '_blank'", "_blank" }, + .{ "a.target", "_blank" }, + .{ "a.target = ''", "" }, - .{ .src = "a.href", .ex = "foo" }, - .{ .src = "a.href = 'https://lightpanda.io/'", .ex = "https://lightpanda.io/" }, - .{ .src = "a.href", .ex = "https://lightpanda.io/" }, + .{ "a.href", "foo" }, + .{ "a.href = 'https://lightpanda.io/'", "https://lightpanda.io/" }, + .{ "a.href", "https://lightpanda.io/" }, - .{ .src = "a.origin", .ex = "https://lightpanda.io" }, + .{ "a.origin", "https://lightpanda.io" }, - .{ .src = "a.host = 'lightpanda.io:443'", .ex = "lightpanda.io:443" }, - .{ .src = "a.host", .ex = "lightpanda.io:443" }, - .{ .src = "a.port", .ex = "443" }, - .{ .src = "a.hostname", .ex = "lightpanda.io" }, + .{ "a.host = 'lightpanda.io:443'", "lightpanda.io:443" }, + .{ "a.host", "lightpanda.io:443" }, + .{ "a.port", "443" }, + .{ "a.hostname", "lightpanda.io" }, - .{ .src = "a.host = 'lightpanda.io'", .ex = "lightpanda.io" }, - .{ .src = "a.host", .ex = "lightpanda.io" }, - .{ .src = "a.port", .ex = "" }, - .{ .src = "a.hostname", .ex = "lightpanda.io" }, + .{ "a.host = 'lightpanda.io'", "lightpanda.io" }, + .{ "a.host", "lightpanda.io" }, + .{ "a.port", "" }, + .{ "a.hostname", "lightpanda.io" }, - .{ .src = "a.host", .ex = "lightpanda.io" }, - .{ .src = "a.hostname", .ex = "lightpanda.io" }, - .{ .src = "a.hostname = 'foo.bar'", .ex = "foo.bar" }, - .{ .src = "a.href", .ex = "https://foo.bar/" }, + .{ "a.host", "lightpanda.io" }, + .{ "a.hostname", "lightpanda.io" }, + .{ "a.hostname = 'foo.bar'", "foo.bar" }, + .{ "a.href", "https://foo.bar/" }, - .{ .src = "a.search", .ex = "" }, - .{ .src = "a.search = 'q=bar'", .ex = "q=bar" }, - .{ .src = "a.search", .ex = "?q=bar" }, - .{ .src = "a.href", .ex = "https://foo.bar/?q=bar" }, + .{ "a.search", "" }, + .{ "a.search = 'q=bar'", "q=bar" }, + .{ "a.search", "?q=bar" }, + .{ "a.href", "https://foo.bar/?q=bar" }, - .{ .src = "a.hash", .ex = "" }, - .{ .src = "a.hash = 'frag'", .ex = "frag" }, - .{ .src = "a.hash", .ex = "#frag" }, - .{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" }, + .{ "a.hash", "" }, + .{ "a.hash = 'frag'", "frag" }, + .{ "a.hash", "#frag" }, + .{ "a.href", "https://foo.bar/?q=bar#frag" }, - .{ .src = "a.port", .ex = "" }, - .{ .src = "a.port = '443'", .ex = "443" }, - .{ .src = "a.host", .ex = "foo.bar:443" }, - .{ .src = "a.hostname", .ex = "foo.bar" }, - .{ .src = "a.href", .ex = "https://foo.bar:443/?q=bar#frag" }, - .{ .src = "a.port = null", .ex = "null" }, - .{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" }, + .{ "a.port", "" }, + .{ "a.port = '443'", "443" }, + .{ "a.host", "foo.bar:443" }, + .{ "a.hostname", "foo.bar" }, + .{ "a.href", "https://foo.bar:443/?q=bar#frag" }, + .{ "a.port = null", "null" }, + .{ "a.href", "https://foo.bar/?q=bar#frag" }, - .{ .src = "a.href = 'foo'", .ex = "foo" }, + .{ "a.href = 'foo'", "foo" }, - .{ .src = "a.type", .ex = "" }, - .{ .src = "a.type = 'text/html'", .ex = "text/html" }, - .{ .src = "a.type", .ex = "text/html" }, - .{ .src = "a.type = ''", .ex = "" }, + .{ "a.type", "" }, + .{ "a.type = 'text/html'", "text/html" }, + .{ "a.type", "text/html" }, + .{ "a.type = ''", "" }, - .{ .src = "a.text", .ex = "OK" }, - .{ .src = "a.text = 'foo'", .ex = "foo" }, - .{ .src = "a.text", .ex = "foo" }, - .{ .src = "a.text = 'OK'", .ex = "OK" }, - }; - try checkCases(js_env, &anchor); + .{ "a.text", "OK" }, + .{ "a.text = 'foo'", "foo" }, + .{ "a.text", "foo" }, + .{ "a.text = 'OK'", "OK" }, + }, .{}); - var script = [_]Case{ - .{ .src = "let script = document.createElement('script')", .ex = "undefined" }, - .{ .src = "script.src = 'foo.bar'", .ex = "foo.bar" }, + try runner.testCases(&.{ + .{ "let script = document.createElement('script')", "undefined" }, + .{ "script.src = 'foo.bar'", "foo.bar" }, - .{ .src = "script.async = true", .ex = "true" }, - .{ .src = "script.async", .ex = "true" }, - .{ .src = "script.async = false", .ex = "false" }, - .{ .src = "script.async", .ex = "false" }, - }; - try checkCases(js_env, &script); + .{ "script.async = true", "true" }, + .{ "script.async", "true" }, + .{ "script.async = false", "false" }, + .{ "script.async", "false" }, + }, .{}); - var innertext = [_]Case{ - .{ .src = "const backup = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "document.getElementById('content').innerText = 'foo';", .ex = "foo" }, - .{ .src = "document.getElementById('content').innerText", .ex = "foo" }, - .{ .src = "document.getElementById('content').innerHTML = backup; true;", .ex = "true" }, - }; - try checkCases(js_env, &innertext); + try runner.testCases(&.{ + .{ "const backup = document.getElementById('content')", "undefined" }, + .{ "document.getElementById('content').innerText = 'foo';", "foo" }, + .{ "document.getElementById('content').innerText", "foo" }, + .{ "document.getElementById('content').innerHTML = backup; true;", "true" }, + }, .{}); } diff --git a/src/html/history.zig b/src/browser/html/history.zig similarity index 70% rename from src/html/history.zig rename to src/browser/html/history.zig index f3e96dc0..0bd07f9c 100644 --- a/src/html/history.zig +++ b/src/browser/html/history.zig @@ -19,15 +19,9 @@ const std = @import("std"); const builtin = @import("builtin"); -const jsruntime = @import("jsruntime"); - -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface pub const History = struct { - pub const mem_guarantied = true; - const ScrollRestorationMode = enum { auto, manual, @@ -98,31 +92,31 @@ pub const History = struct { // Tests // ----- -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var history = [_]Case{ - .{ .src = "history.scrollRestoration", .ex = "auto" }, - .{ .src = "history.scrollRestoration = 'manual'", .ex = "manual" }, - .{ .src = "history.scrollRestoration = 'foo'", .ex = "foo" }, - .{ .src = "history.scrollRestoration", .ex = "manual" }, - .{ .src = "history.scrollRestoration = 'auto'", .ex = "auto" }, - .{ .src = "history.scrollRestoration", .ex = "auto" }, +const testing = @import("../../testing.zig"); +test "Browser.HTML.History" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); - .{ .src = "history.state", .ex = "null" }, + try runner.testCases(&.{ + .{ "history.scrollRestoration", "auto" }, + .{ "history.scrollRestoration = 'manual'", "manual" }, + .{ "history.scrollRestoration = 'foo'", "foo" }, + .{ "history.scrollRestoration", "manual" }, + .{ "history.scrollRestoration = 'auto'", "auto" }, + .{ "history.scrollRestoration", "auto" }, - .{ .src = "history.pushState({}, null, '')", .ex = "undefined" }, + .{ "history.state", "null" }, - .{ .src = "history.replaceState({}, null, '')", .ex = "undefined" }, + .{ "history.pushState({}, null, '')", "undefined" }, - .{ .src = "history.go()", .ex = "undefined" }, - .{ .src = "history.go(1)", .ex = "undefined" }, - .{ .src = "history.go(-1)", .ex = "undefined" }, + .{ "history.replaceState({}, null, '')", "undefined" }, - .{ .src = "history.forward()", .ex = "undefined" }, + .{ "history.go()", "undefined" }, + .{ "history.go(1)", "undefined" }, + .{ "history.go(-1)", "undefined" }, - .{ .src = "history.back()", .ex = "undefined" }, - }; - try checkCases(js_env, &history); + .{ "history.forward()", "undefined" }, + + .{ "history.back()", "undefined" }, + }, .{}); } diff --git a/src/html/html.zig b/src/browser/html/html.zig similarity index 96% rename from src/html/html.zig rename to src/browser/html/html.zig index 9aae1de2..513e3fe5 100644 --- a/src/html/html.zig +++ b/src/browser/html/html.zig @@ -16,8 +16,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -const generate = @import("../generate.zig"); - const HTMLDocument = @import("document.zig").HTMLDocument; const HTMLElem = @import("elements.zig"); const Window = @import("window.zig").Window; diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig new file mode 100644 index 00000000..08f6aa4c --- /dev/null +++ b/src/browser/html/location.zig @@ -0,0 +1,111 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const SessionState = @import("../env.zig").SessionState; + +const builtin = @import("builtin"); + +const URL = @import("../url/url.zig").URL; + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface +pub const Location = struct { + url: ?URL = null, + + pub fn get_href(self: *Location, state: *SessionState) ![]const u8 { + if (self.url) |*u| return u.get_href(state); + return ""; + } + + pub fn get_protocol(self: *Location, state: *SessionState) ![]const u8 { + if (self.url) |*u| return u.get_protocol(state); + return ""; + } + + pub fn get_host(self: *Location, state: *SessionState) ![]const u8 { + if (self.url) |*u| return u.get_host(state); + return ""; + } + + pub fn get_hostname(self: *Location) []const u8 { + if (self.url) |*u| return u.get_hostname(); + return ""; + } + + pub fn get_port(self: *Location, state: *SessionState) ![]const u8 { + if (self.url) |*u| return u.get_port(state); + return ""; + } + + pub fn get_pathname(self: *Location) []const u8 { + if (self.url) |*u| return u.get_pathname(); + return ""; + } + + pub fn get_search(self: *Location, state: *SessionState) ![]const u8 { + if (self.url) |*u| return u.get_search(state); + return ""; + } + + pub fn get_hash(self: *Location, state: *SessionState) ![]const u8 { + if (self.url) |*u| return u.get_hash(state); + return ""; + } + + pub fn get_origin(self: *Location, state: *SessionState) ![]const u8 { + if (self.url) |*u| return u.get_origin(state); + return ""; + } + + // TODO + pub fn _assign(_: *Location, url: []const u8) !void { + _ = url; + } + + // TODO + pub fn _replace(_: *Location, url: []const u8) !void { + _ = url; + } + + // TODO + pub fn _reload(_: *Location) !void {} + + pub fn _toString(self: *Location, state: *SessionState) ![]const u8 { + return try self.get_href(state); + } +}; + +const testing = @import("../../testing.zig"); +test "Browser.HTML.Location" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "location.href", "https://lightpanda.io/opensource-browser/" }, + .{ "document.location.href", "https://lightpanda.io/opensource-browser/" }, + + .{ "location.host", "lightpanda.io" }, + .{ "location.hostname", "lightpanda.io" }, + .{ "location.origin", "https://lightpanda.io" }, + .{ "location.pathname", "/opensource-browser/" }, + .{ "location.hash", "" }, + .{ "location.port", "" }, + .{ "location.search", "" }, + }, .{}); +} diff --git a/src/html/navigator.zig b/src/browser/html/navigator.zig similarity index 83% rename from src/html/navigator.zig rename to src/browser/html/navigator.zig index aebb7b19..e09f6f1a 100644 --- a/src/html/navigator.zig +++ b/src/browser/html/navigator.zig @@ -19,15 +19,9 @@ const std = @import("std"); const builtin = @import("builtin"); -const jsruntime = @import("jsruntime"); - -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; // https://html.spec.whatwg.org/multipage/system-state.html#navigator pub const Navigator = struct { - pub const mem_guarantied = true; - agent: []const u8 = "Lightpanda/1.0", version: []const u8 = "1.0", vendor: []const u8 = "", @@ -89,14 +83,14 @@ pub const Navigator = struct { // Tests // ----- -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var navigator = [_]Case{ - .{ .src = "navigator.userAgent", .ex = "Lightpanda/1.0" }, - .{ .src = "navigator.appVersion", .ex = "1.0" }, - .{ .src = "navigator.language", .ex = "en-US" }, - }; - try checkCases(js_env, &navigator); +const testing = @import("../../testing.zig"); +test "Browser.HTML.Navigator" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "navigator.userAgent", "Lightpanda/1.0" }, + .{ "navigator.appVersion", "1.0" }, + .{ "navigator.language", "en-US" }, + }, .{}); } diff --git a/src/html/window.zig b/src/browser/html/window.zig similarity index 86% rename from src/html/window.zig rename to src/browser/html/window.zig index 36b580ba..45aa7673 100644 --- a/src/html/window.zig +++ b/src/browser/html/window.zig @@ -18,17 +18,14 @@ const std = @import("std"); -const parser = @import("netsurf"); -const jsruntime = @import("jsruntime"); -const Callback = jsruntime.Callback; -const CallbackArg = jsruntime.CallbackArg; -const Loop = jsruntime.Loop; +const parser = @import("../netsurf.zig"); +const Callback = @import("../env.zig").Callback; +const SessionState = @import("../env.zig").SessionState; -const URL = @import("../../../url.zig").URL; -const EventTarget = @import("../dom/event_target.zig").EventTarget; const Navigator = @import("navigator.zig").Navigator; const History = @import("history.zig").History; const Location = @import("location.zig").Location; +const EventTarget = @import("../dom/event_target.zig").EventTarget; const storage = @import("../storage/storage.zig"); @@ -36,14 +33,12 @@ const storage = @import("../storage/storage.zig"); // https://html.spec.whatwg.org/multipage/nav-history-apis.html#window pub const Window = struct { pub const prototype = *EventTarget; - pub const mem_guarantied = true; - pub const global_type = true; // Extend libdom event target for pure zig struct. base: parser.EventTargetTBase = parser.EventTargetTBase{}, document: ?*parser.DocumentHTML = null, - target: []const u8, + target: []const u8 = "", history: History = .{}, location: Location = .{}, storage_shelf: ?*storage.Shelf = null, @@ -53,7 +48,7 @@ pub const Window = struct { timeoutid: u32 = 0, timeoutids: [512]u64 = undefined, - navigator: Navigator, + navigator: Navigator = .{}, pub fn create(target: ?[]const u8, navigator: ?Navigator) Window { return .{ @@ -121,11 +116,11 @@ pub const Window = struct { } // TODO handle callback arguments. - pub fn _setTimeout(self: *Window, loop: *Loop, cbk: Callback, delay: ?u32) !u32 { + pub fn _setTimeout(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 { if (self.timeoutid >= self.timeoutids.len) return error.TooMuchTimeout; const ddelay: u63 = delay orelse 0; - const id = try loop.timeout(ddelay * std.time.ns_per_ms, cbk); + const id = try state.loop.timeout(ddelay * std.time.ns_per_ms, cbk); self.timeoutids[self.timeoutid] = id; defer self.timeoutid += 1; @@ -133,12 +128,12 @@ pub const Window = struct { return self.timeoutid; } - pub fn _clearTimeout(self: *Window, loop: *Loop, id: u32) !void { + pub fn _clearTimeout(self: *Window, id: u32, state: *SessionState) !void { // I do would prefer return an error in this case, but it seems some JS // uses invalid id, in particular id 0. // So we silently ignore invalid id for now. if (id >= self.timeoutid) return; - try loop.cancel(self.timeoutids[id], null); + try state.loop.cancel(self.timeoutids[id], null); } }; diff --git a/src/iterator/iterator.zig b/src/browser/iterator/iterator.zig similarity index 97% rename from src/iterator/iterator.zig rename to src/browser/iterator/iterator.zig index d582be11..aa248509 100644 --- a/src/iterator/iterator.zig +++ b/src/browser/iterator/iterator.zig @@ -5,8 +5,6 @@ pub const Interfaces = .{ }; pub const U32Iterator = struct { - pub const mem_guarantied = true; - length: u32, index: u32 = 0, diff --git a/src/mimalloc/mimalloc.zig b/src/browser/mimalloc.zig similarity index 100% rename from src/mimalloc/mimalloc.zig rename to src/browser/mimalloc.zig diff --git a/src/browser/mime.zig b/src/browser/mime.zig index 2f885d5f..33e14cba 100644 --- a/src/browser/mime.zig +++ b/src/browser/mime.zig @@ -23,7 +23,6 @@ pub const Mime = struct { content_type: ContentType, params: []const u8 = "", charset: ?[]const u8 = null, - arena: std.heap.ArenaAllocator, pub const ContentTypeEnum = enum { text_xml, @@ -39,19 +38,15 @@ pub const Mime = struct { other: struct { type: []const u8, sub_type: []const u8 }, }; - pub fn parse(allocator: Allocator, input: []const u8) !Mime { + pub fn parse(arena: Allocator, input: []const u8) !Mime { if (input.len > 255) { return error.TooBig; } - - var arena = std.heap.ArenaAllocator.init(allocator); - errdefer arena.deinit(); - var trimmed = trim(input); const content_type, const type_len = try parseContentType(trimmed); if (type_len >= trimmed.len) { - return .{ .arena = arena, .content_type = content_type }; + return .{ .content_type = content_type }; } const params = trimLeft(trimmed[type_len..]); @@ -70,24 +65,19 @@ pub const Mime = struct { switch (name.len) { 7 => if (isCaseEqual("charset", name)) { - charset = try parseValue(arena.allocator(), value); + charset = try parseValue(arena, value); }, else => {}, } } return .{ - .arena = arena, .params = params, .charset = charset, .content_type = content_type, }; } - pub fn deinit(self: *Mime) void { - self.arena.deinit(); - } - pub fn isHTML(self: *const Mime) bool { return self.content_type == .text_html; } @@ -158,7 +148,7 @@ pub const Mime = struct { break :blk v; }; - fn parseValue(allocator: Allocator, value: []const u8) ![]const u8 { + fn parseValue(arena: Allocator, value: []const u8) ![]const u8 { if (value[0] != '"') { return value; } @@ -191,7 +181,7 @@ pub const Mime = struct { } value_pos = 1; - const owned = try allocator.alloc(u8, unescaped_len); + const owned = try arena.alloc(u8, unescaped_len); for (0..unescaped_len) |i| { switch (value[value_pos]) { '"' => break, @@ -344,8 +334,9 @@ test "Mime: parse charset" { test "Mime: isHTML" { const isHTML = struct { fn isHTML(expected: bool, input: []const u8) !void { - var mime = try Mime.parse(testing.allocator, input); - defer mime.deinit(); + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + var mime = try Mime.parse(arena.allocator(), input); try testing.expectEqual(expected, mime.isHTML()); } }.isHTML; @@ -364,8 +355,10 @@ const Expectation = struct { }; fn expect(expected: Expectation, input: []const u8) !void { - var actual = try Mime.parse(testing.allocator, input); - defer actual.deinit(); + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + + const actual = try Mime.parse(arena.allocator(), input); try testing.expectEqual( std.meta.activeTag(expected.content_type), diff --git a/src/netsurf/netsurf.zig b/src/browser/netsurf.zig similarity index 99% rename from src/netsurf/netsurf.zig rename to src/browser/netsurf.zig index 92c0bbea..eef41155 100644 --- a/src/netsurf/netsurf.zig +++ b/src/browser/netsurf.zig @@ -27,9 +27,10 @@ const c = @cImport({ @cInclude("events/mouse_event.h"); }); -const mimalloc = @import("mimalloc"); +const mimalloc = @import("mimalloc.zig"); -const Callback = @import("jsruntime").Callback; +const Callback = @import("env.zig").Callback; +const SessionState = @import("env.zig").SessionState; // init initializes netsurf lib. // init starts a mimalloc heap arena for the netsurf session. The caller must @@ -617,7 +618,7 @@ pub fn eventTargetHasListener( defer c.dom_event_listener_unref(listener); const ehd = EventHandlerDataInternal.fromListener(listener); if (ehd) |d| { - if (cbk_id == d.data.cbk.id()) { + if (cbk_id == d.data.cbk.id) { return lst; } } @@ -669,7 +670,7 @@ pub const EventHandlerData = struct { // deinitFunc implements the data deinitialization. deinitFunc: ?DeinitFunc = null, - pub const DeinitFunc = *const fn (data: ?*anyopaque, alloc: std.mem.Allocator) void; + pub const DeinitFunc = *const fn (data: ?*anyopaque, allocator: std.mem.Allocator) void; }; // EventHandlerDataInternal groups the EventHandlerFunc and the EventHandlerData. @@ -687,8 +688,9 @@ const EventHandlerDataInternal = struct { } fn deinit(self: *EventHandlerDataInternal, alloc: std.mem.Allocator) void { - if (self.data.deinitFunc) |d| d(self.data.data, alloc); - self.data.cbk.deinit(alloc); + if (self.data.deinitFunc) |d| { + d(self.data.data, alloc); + } alloc.destroy(self); } @@ -723,7 +725,7 @@ pub fn eventTargetAddEventListener( // When a function is used as an event handler, its this parameter is bound // to the DOM element on which the listener is placed. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#this_in_dom_event_handlers - try ehd.data.cbk.setThisArg(et); + try ehd.data.cbk.setThis(et); const ctx = @as(*anyopaque, @ptrCast(ehd)); var listener: ?*EventListener = undefined; diff --git a/src/polyfill/fetch.js b/src/browser/polyfill/fetch.js similarity index 100% rename from src/polyfill/fetch.js rename to src/browser/polyfill/fetch.js diff --git a/src/browser/polyfill/fetch.zig b/src/browser/polyfill/fetch.zig new file mode 100644 index 00000000..9581881e --- /dev/null +++ b/src/browser/polyfill/fetch.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +// fetch.js code comes from +// https://github.com/JakeChampion/fetch/blob/main/fetch.js +// +// The original code source is available in MIT license. +// +// The script comes from the built version from npm. +// You can get the package with the command: +// +// wget $(npm view whatwg-fetch dist.tarball) +// +// The source is the content of `package/dist/fetch.umd.js` file. +pub const source = @embedFile("fetch.js"); + +const testing = @import("../../testing.zig"); +test "Browser.fetch" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try @import("polyfill.zig").load(testing.allocator, runner.executor); + + try runner.testCases(&.{ + .{ + \\ var ok = false; + \\ const request = new Request("http://127.0.0.1:9582/loader"); + \\ fetch(request).then((response) => { ok = response.ok; }); + \\ false; + , + "false", + }, + // all events have been resolved. + .{ "ok", "true" }, + }, .{}); + + try runner.testCases(&.{ + .{ + \\ var ok2 = false; + \\ const request2 = new Request("http://127.0.0.1:9582/loader"); + \\ (async function () { resp = await fetch(request2); ok2 = resp.ok; }()); + \\ false; + , + "false", + }, + // all events have been resolved. + .{ "ok2", "true" }, + }, .{}); +} diff --git a/src/polyfill/polyfill.zig b/src/browser/polyfill/polyfill.zig similarity index 72% rename from src/polyfill/polyfill.zig rename to src/browser/polyfill/polyfill.zig index 65bb0590..9e682ea2 100644 --- a/src/polyfill/polyfill.zig +++ b/src/browser/polyfill/polyfill.zig @@ -19,10 +19,8 @@ const std = @import("std"); const builtin = @import("builtin"); -const jsruntime = @import("jsruntime"); -const Env = jsruntime.Env; - -const fetch = @import("fetch.zig").fetch_polyfill; +const Allocator = std.mem.Allocator; +const Env = @import("../env.zig").Env; const log = std.log.scoped(.polyfill); @@ -33,23 +31,23 @@ const modules = [_]struct { .{ .name = "polyfill-fetch", .source = @import("fetch.zig").source }, }; -pub fn load(alloc: std.mem.Allocator, env: *const Env) !void { - var try_catch: jsruntime.TryCatch = undefined; - try_catch.init(env); +pub fn load(allocator: Allocator, executor: *Env.Executor) !void { + var try_catch: Env.TryCatch = undefined; + try_catch.init(executor); defer try_catch.deinit(); for (modules) |m| { - const res = env.exec(m.source, m.name) catch { - if (try try_catch.err(alloc, env)) |msg| { - defer alloc.free(msg); + const res = executor.exec(m.source, m.name) catch |err| { + if (try try_catch.err(allocator)) |msg| { + defer allocator.free(msg); log.err("load {s}: {s}", .{ m.name, msg }); } - return; + return err; }; if (builtin.mode == .Debug) { - const msg = try res.toString(alloc, env); - defer alloc.free(msg); + const msg = try res.toString(allocator); + defer allocator.free(msg); log.debug("load {s}: {s}", .{ m.name, msg }); } } diff --git a/src/storage/cookie.zig b/src/browser/storage/cookie.zig similarity index 99% rename from src/storage/cookie.zig rename to src/browser/storage/cookie.zig index e8da2869..dc483cf7 100644 --- a/src/storage/cookie.zig +++ b/src/browser/storage/cookie.zig @@ -3,9 +3,9 @@ const Uri = std.Uri; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; -const http = @import("../http/client.zig"); -const DateTime = @import("../datetime.zig").DateTime; -const public_suffix_list = @import("../data/public_suffix_list.zig").lookup; +const http = @import("../../http/client.zig"); +const DateTime = @import("../../datetime.zig").DateTime; +const public_suffix_list = @import("../../data/public_suffix_list.zig").lookup; const log = std.log.scoped(.cookie); @@ -464,7 +464,7 @@ fn trimRight(str: []const u8) []const u8 { return std.mem.trimLeft(u8, str, &std.ascii.whitespace); } -const testing = @import("../testing.zig"); +const testing = @import("../../testing.zig"); test "cookie: findSecondLevelDomain" { const cases = [_]struct { []const u8, []const u8 }{ .{ "", "" }, diff --git a/src/storage/storage.zig b/src/browser/storage/storage.zig similarity index 87% rename from src/storage/storage.zig rename to src/browser/storage/storage.zig index 17fb8878..90a15584 100644 --- a/src/storage/storage.zig +++ b/src/browser/storage/storage.zig @@ -18,10 +18,7 @@ const std = @import("std"); -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; -const DOMError = @import("netsurf").DOMError; +const DOMError = @import("../netsurf.zig").DOMError; const log = std.log.scoped(.storage); @@ -103,7 +100,6 @@ pub const Bucket = struct { // https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface pub const Bottle = struct { - pub const mem_guarantied = true; const Map = std.StringHashMapUnmanaged([]const u8); // allocator is stored. we don't use the JS env allocator b/c the storage @@ -216,27 +212,27 @@ pub const Bottle = struct { // Tests // ----- -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var storage = [_]Case{ - .{ .src = "localStorage.length", .ex = "0" }, +const testing = @import("../../testing.zig"); +test "Browser.Storage.LocalStorage" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); - .{ .src = "localStorage.setItem('foo', 'bar')", .ex = "undefined" }, - .{ .src = "localStorage.length", .ex = "1" }, - .{ .src = "localStorage.getItem('foo')", .ex = "bar" }, - .{ .src = "localStorage.removeItem('foo')", .ex = "undefined" }, - .{ .src = "localStorage.length", .ex = "0" }, + try runner.testCases(&.{ + .{ "localStorage.length", "0" }, - // .{ .src = "localStorage['foo'] = 'bar'", .ex = "undefined" }, - // .{ .src = "localStorage['foo']", .ex = "bar" }, - // .{ .src = "localStorage.length", .ex = "1" }, + .{ "localStorage.setItem('foo', 'bar')", "undefined" }, + .{ "localStorage.length", "1" }, + .{ "localStorage.getItem('foo')", "bar" }, + .{ "localStorage.removeItem('foo')", "undefined" }, + .{ "localStorage.length", "0" }, - .{ .src = "localStorage.clear()", .ex = "undefined" }, - .{ .src = "localStorage.length", .ex = "0" }, - }; - try checkCases(js_env, &storage); + // .{ "localStorage['foo'] = 'bar'", "undefined" }, + // .{ "localStorage['foo']", "bar" }, + // .{ "localStorage.length", "1" }, + + .{ "localStorage.clear()", "undefined" }, + .{ "localStorage.length", "0" }, + }, .{}); } test "storage bottle" { diff --git a/src/url/query.zig b/src/browser/url/query.zig similarity index 99% rename from src/url/query.zig rename to src/browser/url/query.zig index 14fec1b6..b8afa834 100644 --- a/src/url/query.zig +++ b/src/browser/url/query.zig @@ -18,8 +18,8 @@ const std = @import("std"); -const Reader = @import("../str/parser.zig").Reader; -const asUint = @import("../str/parser.zig").asUint; +const Reader = @import("../../str/parser.zig").Reader; +const asUint = @import("../../str/parser.zig").asUint; // Values is a map with string key of string values. pub const Values = struct { diff --git a/src/url/url.zig b/src/browser/url/url.zig similarity index 50% rename from src/url/url.zig rename to src/browser/url/url.zig index 2e28210d..7c7bf0a9 100644 --- a/src/url/url.zig +++ b/src/browser/url/url.zig @@ -17,10 +17,7 @@ // along with this program. If not, see . const std = @import("std"); - -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; +const SessionState = @import("../env.zig").SessionState; const query = @import("query.zig"); @@ -31,14 +28,14 @@ pub const Interfaces = .{ // https://url.spec.whatwg.org/#url // -// TODO we could avoid many of these getter string allocation in two differents +// TODO we could avoid many of these getter string allocatoration in two differents // way: // // 1. We can eventually get the slice of scheme *with* the following char in // the underlying string. But I don't know if it's possible and how to do that. // I mean, if the rawuri contains `https://foo.bar`, uri.scheme is a slice // containing only `https`. I want `https:` so, in theory, I don't need to -// allocate data, I should be able to retrieve the scheme + the following `:` +// allocatorate data, I should be able to retrieve the scheme + the following `:` // from rawuri. // // 2. The other way would bu to copy the `std.Uri` code to ahve a dedicated @@ -47,9 +44,12 @@ pub const URL = struct { uri: std.Uri, search_params: URLSearchParams, - pub const mem_guarantied = true; - - pub fn constructor(arena: std.mem.Allocator, url: []const u8, base: ?[]const u8) !URL { + pub fn constructor( + url: []const u8, + base: ?[]const u8, + state: *SessionState, + ) !URL { + const arena = state.arena; const raw = try std.mem.concat(arena, u8, &[_][]const u8{ url, base orelse "" }); errdefer arena.free(raw); @@ -60,24 +60,15 @@ pub const URL = struct { pub fn init(arena: std.mem.Allocator, uri: std.Uri) !URL { return .{ .uri = uri, - .search_params = try URLSearchParams.constructor( + .search_params = try URLSearchParams.init( arena, uriComponentNullStr(uri.query), ), }; } - pub fn deinit(self: *URL, alloc: std.mem.Allocator) void { - self.search_params.deinit(alloc); - } - - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 - pub fn get_origin(self: *URL, alloc: std.mem.Allocator) ![]const u8 { - var buf = std.ArrayList(u8).init(alloc); - defer buf.deinit(); - + pub fn get_origin(self: *URL, state: *SessionState) ![]const u8 { + var buf = std.ArrayList(u8).init(state.arena); try self.uri.writeToStream(.{ .scheme = true, .authentication = false, @@ -86,31 +77,27 @@ pub const URL = struct { .query = false, .fragment = false, }, buf.writer()); - return try buf.toOwnedSlice(); + return buf.items; } // get_href returns the URL by writing all its components. // The query is replaced by a dump of search params. // - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 - pub fn get_href(self: *URL, alloc: std.mem.Allocator) ![]const u8 { + pub fn get_href(self: *URL, state: *SessionState) ![]const u8 { + const arena = state.arena; // retrieve the query search from search_params. const cur = self.uri.query; defer self.uri.query = cur; - var q = std.ArrayList(u8).init(alloc); - defer q.deinit(); + var q = std.ArrayList(u8).init(arena); try self.search_params.values.encode(q.writer()); self.uri.query = .{ .percent_encoded = q.items }; - return try self.format(alloc); + return try self.toString(arena); } // format the url with all its components. - pub fn format(self: *URL, alloc: std.mem.Allocator) ![]const u8 { - var buf = std.ArrayList(u8).init(alloc); - defer buf.deinit(); + pub fn toString(self: *URL, arena: std.mem.Allocator) ![]const u8 { + var buf = std.ArrayList(u8).init(arena); try self.uri.writeToStream(.{ .scheme = true, @@ -120,14 +107,11 @@ pub const URL = struct { .query = uriComponentNullStr(self.uri.query).len > 0, .fragment = uriComponentNullStr(self.uri.fragment).len > 0, }, buf.writer()); - return try buf.toOwnedSlice(); + return buf.items; } - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 - pub fn get_protocol(self: *URL, alloc: std.mem.Allocator) ![]const u8 { - return try std.mem.concat(alloc, u8, &[_][]const u8{ self.uri.scheme, ":" }); + pub fn get_protocol(self: *URL, state: *SessionState) ![]const u8 { + return try std.mem.concat(state.arena, u8, &[_][]const u8{ self.uri.scheme, ":" }); } pub fn get_username(self: *URL) []const u8 { @@ -138,12 +122,8 @@ pub const URL = struct { return uriComponentNullStr(self.uri.password); } - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 - pub fn get_host(self: *URL, alloc: std.mem.Allocator) ![]const u8 { - var buf = std.ArrayList(u8).init(alloc); - defer buf.deinit(); + pub fn get_host(self: *URL, state: *SessionState) ![]const u8 { + var buf = std.ArrayList(u8).init(state.arena); try self.uri.writeToStream(.{ .scheme = false, @@ -153,24 +133,20 @@ pub const URL = struct { .query = false, .fragment = false, }, buf.writer()); - return try buf.toOwnedSlice(); + return buf.items; } pub fn get_hostname(self: *URL) []const u8 { return uriComponentNullStr(self.uri.host); } - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 - pub fn get_port(self: *URL, alloc: std.mem.Allocator) ![]const u8 { - if (self.uri.port == null) return try alloc.dupe(u8, ""); - - var buf = std.ArrayList(u8).init(alloc); - defer buf.deinit(); + pub fn get_port(self: *URL, state: *SessionState) ![]const u8 { + const arena = state.arena; + if (self.uri.port == null) return try arena.dupe(u8, ""); + var buf = std.ArrayList(u8).init(arena); try std.fmt.formatInt(self.uri.port.?, 10, .lower, .{}, buf.writer()); - return try buf.toOwnedSlice(); + return buf.items; } pub fn get_pathname(self: *URL) []const u8 { @@ -178,35 +154,30 @@ pub const URL = struct { return uriComponentStr(self.uri.path); } - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 - pub fn get_search(self: *URL, alloc: std.mem.Allocator) ![]const u8 { - if (self.search_params.get_size() == 0) return try alloc.dupe(u8, ""); + pub fn get_search(self: *URL, state: *SessionState) ![]const u8 { + const arena = state.arena; + if (self.search_params.get_size() == 0) return try arena.dupe(u8, ""); var buf: std.ArrayListUnmanaged(u8) = .{}; - defer buf.deinit(alloc); - try buf.append(alloc, '?'); - try self.search_params.values.encode(buf.writer(alloc)); - return buf.toOwnedSlice(alloc); + try buf.append(arena, '?'); + try self.search_params.values.encode(buf.writer(arena)); + return buf.items; } - // the caller must free the returned string. - // TODO return a disposable string - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 - pub fn get_hash(self: *URL, alloc: std.mem.Allocator) ![]const u8 { - if (self.uri.fragment == null) return try alloc.dupe(u8, ""); + pub fn get_hash(self: *URL, state: *SessionState) ![]const u8 { + const arena = state.arena; + if (self.uri.fragment == null) return try arena.dupe(u8, ""); - return try std.mem.concat(alloc, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) }); + return try std.mem.concat(arena, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) }); } pub fn get_searchParams(self: *URL) *URLSearchParams { return &self.search_params; } - pub fn _toJSON(self: *URL, alloc: std.mem.Allocator) ![]const u8 { - return try self.get_href(alloc); + pub fn _toJSON(self: *URL, state: *SessionState) ![]const u8 { + return try self.get_href(state); } }; @@ -230,16 +201,14 @@ fn uriComponentStr(c: std.Uri.Component) []const u8 { pub const URLSearchParams = struct { values: query.Values, - pub const mem_guarantied = true; - - pub fn constructor(alloc: std.mem.Allocator, init: ?[]const u8) !URLSearchParams { - return .{ - .values = try query.parseQuery(alloc, init orelse ""), - }; + pub fn constructor(qs: ?[]const u8, state: *SessionState) !URLSearchParams { + return init(state.arena, qs); } - pub fn deinit(self: *URLSearchParams, _: std.mem.Allocator) void { - self.values.deinit(); + pub fn init(arena: std.mem.Allocator, qs: ?[]const u8) !URLSearchParams { + return .{ + .values = try query.parseQuery(arena, qs orelse ""), + }; } pub fn get_size(self: *URLSearchParams) u32 { @@ -269,47 +238,43 @@ pub const URLSearchParams = struct { pub fn _sort(_: *URLSearchParams) void {} }; -// Tests -// ----- +const testing = @import("../../testing.zig"); +test "Browser.URL" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var url = [_]Case{ - .{ .src = "var url = new URL('https://foo.bar/path?query#fragment')", .ex = "undefined" }, - .{ .src = "url.origin", .ex = "https://foo.bar" }, - .{ .src = "url.href", .ex = "https://foo.bar/path?query#fragment" }, - .{ .src = "url.protocol", .ex = "https:" }, - .{ .src = "url.username", .ex = "" }, - .{ .src = "url.password", .ex = "" }, - .{ .src = "url.host", .ex = "foo.bar" }, - .{ .src = "url.hostname", .ex = "foo.bar" }, - .{ .src = "url.port", .ex = "" }, - .{ .src = "url.pathname", .ex = "/path" }, - .{ .src = "url.search", .ex = "?query" }, - .{ .src = "url.hash", .ex = "#fragment" }, - .{ .src = "url.searchParams.get('query')", .ex = "" }, - }; - try checkCases(js_env, &url); + try runner.testCases(&.{ + .{ "var url = new URL('https://foo.bar/path?query#fragment')", "undefined" }, + .{ "url.origin", "https://foo.bar" }, + .{ "url.href", "https://foo.bar/path?query#fragment" }, + .{ "url.protocol", "https:" }, + .{ "url.username", "" }, + .{ "url.password", "" }, + .{ "url.host", "foo.bar" }, + .{ "url.hostname", "foo.bar" }, + .{ "url.port", "" }, + .{ "url.pathname", "/path" }, + .{ "url.search", "?query" }, + .{ "url.hash", "#fragment" }, + .{ "url.searchParams.get('query')", "" }, + }, .{}); - var qs = [_]Case{ - .{ .src = "var url = new URL('https://foo.bar/path?a=~&b=%7E#fragment')", .ex = "undefined" }, - .{ .src = "url.searchParams.get('a')", .ex = "~" }, - .{ .src = "url.searchParams.get('b')", .ex = "~" }, - .{ .src = "url.searchParams.append('c', 'foo')", .ex = "undefined" }, - .{ .src = "url.searchParams.get('c')", .ex = "foo" }, - .{ .src = "url.searchParams.size", .ex = "3" }, + try runner.testCases(&.{ + .{ "var url = new URL('https://foo.bar/path?a=~&b=%7E#fragment')", "undefined" }, + .{ "url.searchParams.get('a')", "~" }, + .{ "url.searchParams.get('b')", "~" }, + .{ "url.searchParams.append('c', 'foo')", "undefined" }, + .{ "url.searchParams.get('c')", "foo" }, + .{ "url.searchParams.size", "3" }, // search is dynamic - .{ .src = "url.search", .ex = "?a=%7E&b=%7E&c=foo" }, + .{ "url.search", "?a=%7E&b=%7E&c=foo" }, // href is dynamic - .{ .src = "url.href", .ex = "https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment" }, + .{ "url.href", "https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment" }, - .{ .src = "url.searchParams.delete('c', 'foo')", .ex = "undefined" }, - .{ .src = "url.searchParams.get('c')", .ex = "" }, - .{ .src = "url.searchParams.delete('a')", .ex = "undefined" }, - .{ .src = "url.searchParams.get('a')", .ex = "" }, - }; - try checkCases(js_env, &qs); + .{ "url.searchParams.delete('c', 'foo')", "undefined" }, + .{ "url.searchParams.get('c')", "" }, + .{ "url.searchParams.delete('a')", "undefined" }, + .{ "url.searchParams.get('a')", "" }, + }, .{}); } diff --git a/src/xhr/event_target.zig b/src/browser/xhr/event_target.zig similarity index 64% rename from src/xhr/event_target.zig rename to src/browser/xhr/event_target.zig index 69bab2d5..02feb929 100644 --- a/src/xhr/event_target.zig +++ b/src/browser/xhr/event_target.zig @@ -18,19 +18,19 @@ const std = @import("std"); -const jsruntime = @import("jsruntime"); -const Callback = jsruntime.Callback; +const Env = @import("../env.zig").Env; +const Callback = Env.Callback; const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventHandler = @import("../events/event.zig").EventHandler; -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); +const SessionState = @import("../env.zig").SessionState; const log = std.log.scoped(.xhr); pub const XMLHttpRequestEventTarget = struct { pub const prototype = *EventTarget; - pub const mem_guarantied = true; // Extend libdom event target for pure zig struct. base: parser.EventTargetTBase = parser.EventTargetTBase{}, @@ -60,7 +60,7 @@ pub const XMLHttpRequestEventTarget = struct { fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void { const et = @as(*parser.EventTarget, @ptrCast(self)); // check if event target has already this listener - const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id()); + const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id); if (lst == null) { return; } @@ -88,39 +88,46 @@ pub const XMLHttpRequestEventTarget = struct { return self.onloadend_cbk; } - pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - if (self.onloadstart_cbk) |cbk| try self.unregister(alloc, "loadstart", cbk); - try self.register(alloc, "loadstart", handler); + pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { + const arena = state.arena; + if (self.onloadstart_cbk) |cbk| try self.unregister(arena, "loadstart", cbk); + try self.register(arena, "loadstart", handler); self.onloadstart_cbk = handler; } - pub fn set_onprogress(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - if (self.onprogress_cbk) |cbk| try self.unregister(alloc, "progress", cbk); - try self.register(alloc, "progress", handler); + pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { + const arena = state.arena; + if (self.onprogress_cbk) |cbk| try self.unregister(arena, "progress", cbk); + try self.register(arena, "progress", handler); self.onprogress_cbk = handler; } - pub fn set_onabort(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - if (self.onabort_cbk) |cbk| try self.unregister(alloc, "abort", cbk); - try self.register(alloc, "abort", handler); + pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { + const arena = state.arena; + if (self.onabort_cbk) |cbk| try self.unregister(arena, "abort", cbk); + try self.register(arena, "abort", handler); self.onabort_cbk = handler; } - pub fn set_onload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - if (self.onload_cbk) |cbk| try self.unregister(alloc, "load", cbk); - try self.register(alloc, "load", handler); + pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { + const arena = state.arena; + if (self.onload_cbk) |cbk| try self.unregister(arena, "load", cbk); + try self.register(arena, "load", handler); self.onload_cbk = handler; } - pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - if (self.ontimeout_cbk) |cbk| try self.unregister(alloc, "timeout", cbk); - try self.register(alloc, "timeout", handler); + pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { + const arena = state.arena; + if (self.ontimeout_cbk) |cbk| try self.unregister(arena, "timeout", cbk); + try self.register(arena, "timeout", handler); self.ontimeout_cbk = handler; } - pub fn set_onloadend(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - if (self.onloadend_cbk) |cbk| try self.unregister(alloc, "loadend", cbk); - try self.register(alloc, "loadend", handler); + pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { + const arena = state.arena; + if (self.onloadend_cbk) |cbk| try self.unregister(arena, "loadend", cbk); + try self.register(arena, "loadend", handler); self.onloadend_cbk = handler; } - pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void { - parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), alloc) catch |e| { + pub fn deinit(self: *XMLHttpRequestEventTarget, state: *SessionState) void { + const arena = state.arena; + parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), arena) catch |e| { log.err("remove all listeners: {any}", .{e}); }; } diff --git a/src/xhr/progress_event.zig b/src/browser/xhr/progress_event.zig similarity index 63% rename from src/xhr/progress_event.zig rename to src/browser/xhr/progress_event.zig index d985c76f..4fd8b8a7 100644 --- a/src/xhr/progress_event.zig +++ b/src/browser/xhr/progress_event.zig @@ -18,11 +18,7 @@ const std = @import("std"); -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - -const parser = @import("netsurf"); +const parser = @import("../netsurf.zig"); const Event = @import("../events/event.zig").Event; const DOMException = @import("../dom/exceptions.zig").DOMException; @@ -30,7 +26,6 @@ const DOMException = @import("../dom/exceptions.zig").DOMException; pub const ProgressEvent = struct { pub const prototype = *Event; pub const Exception = DOMException; - pub const mem_guarantied = true; pub const EventInit = struct { lengthComputable: bool = false, @@ -59,32 +54,32 @@ pub const ProgressEvent = struct { }; } - pub fn get_lengthComputable(self: ProgressEvent) bool { + pub fn get_lengthComputable(self: *const ProgressEvent) bool { return self.lengthComputable; } - pub fn get_loaded(self: ProgressEvent) u64 { + pub fn get_loaded(self: *const ProgressEvent) u64 { return self.loaded; } - pub fn get_total(self: ProgressEvent) u64 { + pub fn get_total(self: *const ProgressEvent) u64 { return self.total; } }; -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var progress_event = [_]Case{ - .{ .src = "let pevt = new ProgressEvent('foo');", .ex = "undefined" }, - .{ .src = "pevt.loaded", .ex = "0" }, - .{ .src = "pevt instanceof ProgressEvent", .ex = "true" }, - .{ .src = "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", .ex = "undefined" }, - .{ .src = "document.addEventListener('foo', ccbk)", .ex = "undefined" }, - .{ .src = "document.dispatchEvent(pevt)", .ex = "true" }, - .{ .src = "eevt.type", .ex = "foo" }, - .{ .src = "eevt instanceof ProgressEvent", .ex = "true" }, - }; - try checkCases(js_env, &progress_event); +const testing = @import("../../testing.zig"); +test "Browser.XHR.ProgressEvent" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "let pevt = new ProgressEvent('foo');", "undefined" }, + .{ "pevt.loaded", "0" }, + .{ "pevt instanceof ProgressEvent", "true" }, + .{ "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", "undefined" }, + .{ "document.addEventListener('foo', ccbk)", "undefined" }, + .{ "document.dispatchEvent(pevt)", "true" }, + .{ "eevt.type", "foo" }, + .{ "eevt instanceof ProgressEvent", "true" }, + }, .{}); } diff --git a/src/xhr/xhr.zig b/src/browser/xhr/xhr.zig similarity index 76% rename from src/xhr/xhr.zig rename to src/browser/xhr/xhr.zig index 9c4e82b3..b0aa59d5 100644 --- a/src/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -17,27 +17,20 @@ // along with this program. If not, see . const std = @import("std"); +const Allocator = std.mem.Allocator; -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - -const DOMError = @import("netsurf").DOMError; +const DOMError = @import("../netsurf.zig").DOMError; const DOMException = @import("../dom/exceptions.zig").DOMException; const ProgressEvent = @import("progress_event.zig").ProgressEvent; const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEventTarget; -const Mime = @import("../browser/mime.zig").Mime; - -const Loop = jsruntime.Loop; -const URL = @import("../url.zig").URL; -const http = @import("../http/client.zig"); - -const parser = @import("netsurf"); - +const URL = @import("../../url.zig").URL; +const Mime = @import("../mime.zig").Mime; +const parser = @import("../netsurf.zig"); +const http = @import("../../http/client.zig"); +const SessionState = @import("../env.zig").SessionState; const CookieJar = @import("../storage/storage.zig").CookieJar; -const UserContext = @import("../user_context.zig").UserContext; const log = std.log.scoped(.xhr); @@ -51,7 +44,6 @@ pub const Interfaces = .{ pub const XMLHttpRequestUpload = struct { pub const prototype = *XMLHttpRequestEventTarget; - pub const mem_guarantied = true; proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, }; @@ -83,7 +75,7 @@ pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) { // Duplicate the body content. // The caller owns the allocated string. - fn dupe(self: XMLHttpRequestBodyInit, alloc: std.mem.Allocator) ![]const u8 { + fn dupe(self: XMLHttpRequestBodyInit, alloc: Allocator) ![]const u8 { return switch (self) { .Blob => error.NotImplemented, .BufferSource => error.NotImplemented, @@ -96,7 +88,7 @@ pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) { pub const XMLHttpRequest = struct { proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, - alloc: std.mem.Allocator, + arena: Allocator, client: *http.Client, request: ?http.Request = null, @@ -134,10 +126,6 @@ pub const XMLHttpRequest = struct { response_type: ResponseType = .Empty, response_headers: Headers, - // used by zig client to parse response headers. - // use 16KB for headers buffer size. - response_header_buffer: [1024 * 16]u8 = undefined, - response_status: u16 = 0, // TODO uncomment this field causes casting issue with @@ -151,7 +139,6 @@ pub const XMLHttpRequest = struct { send_flag: bool = false, pub const prototype = *XMLHttpRequestEventTarget; - pub const mem_guarantied = true; const State = enum(u16) { unsent = 0, @@ -174,41 +161,41 @@ pub const XMLHttpRequest = struct { const JSONValue = std.json.Value; const Headers = struct { - alloc: std.mem.Allocator, list: List, + arena: Allocator, const List = std.ArrayListUnmanaged(std.http.Header); - fn init(alloc: std.mem.Allocator) Headers { + fn init(arena: Allocator) Headers { return .{ - .alloc = alloc, - .list = List{}, + .arena = arena, + .list = .{}, }; } fn deinit(self: *Headers) void { self.free(); - self.list.deinit(self.alloc); + self.list.deinit(self.arena); } fn append(self: *Headers, k: []const u8, v: []const u8) !void { // duplicate strings - const kk = try self.alloc.dupe(u8, k); - const vv = try self.alloc.dupe(u8, v); - try self.list.append(self.alloc, .{ .name = kk, .value = vv }); + const kk = try self.arena.dupe(u8, k); + const vv = try self.arena.dupe(u8, v); + try self.list.append(self.arena, .{ .name = kk, .value = vv }); } // free all strings allocated. fn free(self: *Headers) void { for (self.list.items) |h| { - self.alloc.free(h.name); - self.alloc.free(h.value); + self.arena.free(h.name); + self.arena.free(h.value); } } fn clearAndFree(self: *Headers) void { self.free(); - self.list.clearAndFree(self.alloc); + self.list.clearAndFree(self.arena); } fn has(self: Headers, k: []const u8) bool { @@ -236,8 +223,8 @@ pub const XMLHttpRequest = struct { for (self.list.items, 0..) |h, i| { if (std.ascii.eqlIgnoreCase(k, h.name)) { const hh = self.list.swapRemove(i); - self.alloc.free(hh.name); - self.alloc.free(hh.value); + self.arena.free(hh.name); + self.arena.free(hh.value); } } self.append(k, v); @@ -286,17 +273,18 @@ pub const XMLHttpRequest = struct { const min_delay: u64 = 50000000; // 50ms - pub fn constructor(alloc: std.mem.Allocator, userctx: UserContext) !XMLHttpRequest { + pub fn constructor(session_state: *SessionState) !XMLHttpRequest { + const arena = session_state.arena; return .{ - .alloc = alloc, - .headers = Headers.init(alloc), - .response_headers = Headers.init(alloc), + .arena = arena, + .headers = Headers.init(arena), + .response_headers = Headers.init(arena), .method = undefined, .state = .unsent, .url = null, - .origin_url = userctx.url, - .client = userctx.http_client, - .cookie_jar = userctx.cookie_jar, + .origin_url = session_state.url, + .client = session_state.http_client, + .cookie_jar = session_state.cookie_jar, }; } @@ -307,10 +295,7 @@ pub const XMLHttpRequest = struct { self.response_obj = null; self.response_type = .Empty; - if (self.response_mime) |*mime| { - mime.deinit(); - self.response_mime = null; - } + self.response_mime = null; // TODO should we clearRetainingCapacity instead? self.headers.clearAndFree(); @@ -322,7 +307,7 @@ pub const XMLHttpRequest = struct { self.priv_state = .new; } - pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { + pub fn deinit(self: *XMLHttpRequest, alloc: Allocator) void { self.reset(); self.headers.deinit(); self.response_headers.deinit(); @@ -362,7 +347,6 @@ pub const XMLHttpRequest = struct { pub fn _open( self: *XMLHttpRequest, - alloc: std.mem.Allocator, method: []const u8, url: []const u8, asyn: ?bool, @@ -375,12 +359,13 @@ pub const XMLHttpRequest = struct { // TODO If this’s relevant global object is a Window object and its // associated Document is not fully active, then throw an // "InvalidStateError" DOMException. - - self.method = try validMethod(method); - self.reset(); - self.url = try self.origin_url.resolve(alloc, url); + self.method = try validMethod(method); + const arena = self.arena; + + self.url = try self.origin_url.resolve(arena, url); + log.debug("open url ({s})", .{self.url.?}); self.sync = if (asyn) |b| !b else false; @@ -466,7 +451,7 @@ pub const XMLHttpRequest = struct { } // TODO body can be either a XMLHttpRequestBodyInit or a document - pub fn _send(self: *XMLHttpRequest, loop: *Loop, alloc: std.mem.Allocator, body: ?[]const u8) !void { + pub fn _send(self: *XMLHttpRequest, body: ?[]const u8, session_state: *SessionState) !void { if (self.state != .opened) return DOMError.InvalidState; if (self.send_flag) return DOMError.InvalidState; @@ -485,7 +470,7 @@ pub const XMLHttpRequest = struct { { var arr: std.ArrayListUnmanaged(u8) = .{}; - try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(alloc), .{ + try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(session_state.arena), .{ .navigation = false, .origin_uri = &self.origin_url.uri, }); @@ -501,12 +486,12 @@ pub const XMLHttpRequest = struct { // var used_body: ?XMLHttpRequestBodyInit = null; if (body) |b| { if (self.method != .GET and self.method != .HEAD) { - request.body = try alloc.dupe(u8, b); + request.body = try session_state.arena.dupe(u8, b); try request.addHeader("Content-Type", "text/plain; charset=UTF-8", .{}); } } - try request.sendAsync(loop, self, .{}); + try request.sendAsync(session_state.loop, self, .{}); } pub fn onHttpResponse(self: *XMLHttpRequest, progress_: anyerror!http.Progress) !void { @@ -526,8 +511,15 @@ pub const XMLHttpRequest = struct { } // extract a mime type from headers. - const ct = header.get("content-type") orelse "text/xml"; - self.response_mime = Mime.parse(self.alloc, ct) catch |e| return self.onErr(e); + { + var raw: []const u8 = "text/xml"; + if (header.get("content-type")) |ct| { + raw = try self.arena.dupe(u8, ct); + } + self.response_mime = Mime.parse(self.arena, raw) catch |e| { + return self.onErr(e); + }; + } // TODO handle override mime type self.state = .headers_received; @@ -545,7 +537,7 @@ pub const XMLHttpRequest = struct { } if (progress.data) |data| { - try self.response_bytes.appendSlice(self.alloc, data); + try self.response_bytes.appendSlice(self.arena, data); } const loaded = self.response_bytes.items.len; @@ -636,7 +628,7 @@ pub const XMLHttpRequest = struct { return url.raw; } - pub fn get_responseXML(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response { + pub fn get_responseXML(self: *XMLHttpRequest) !?Response { if (self.response_type != .Empty and self.response_type != .Document) { return DOMError.InvalidState; } @@ -652,7 +644,7 @@ pub const XMLHttpRequest = struct { }; } - self.setResponseObjDocument(alloc); + self.setResponseObjDocument(); if (self.response_obj) |obj| { return switch (obj) { @@ -665,7 +657,7 @@ pub const XMLHttpRequest = struct { } // https://xhr.spec.whatwg.org/#the-response-attribute - pub fn get_response(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response { + pub fn get_response(self: *XMLHttpRequest) !?Response { if (self.response_type == .Empty or self.response_type == .Text) { if (self.state == .loading or self.state == .done) { return .{ .Text = try self.get_responseText() }; @@ -703,7 +695,7 @@ pub const XMLHttpRequest = struct { // Otherwise, if this’s response type is "document", set a // document response for this. if (self.response_type == .Document) { - self.setResponseObjDocument(alloc); + self.setResponseObjDocument(); } if (self.response_type == .JSON) { @@ -712,7 +704,7 @@ pub const XMLHttpRequest = struct { // TODO Let jsonObject be the result of running parse JSON from bytes // on this’s received bytes. If that threw an exception, then return // null. - self.setResponseObjJSON(alloc); + self.setResponseObjJSON(); } if (self.response_obj) |obj| { @@ -731,19 +723,23 @@ pub const XMLHttpRequest = struct { // If the par sing fails, a Failure is stored in response_obj. // TODO parse XML. // https://xhr.spec.whatwg.org/#response-object - fn setResponseObjDocument(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { + fn setResponseObjDocument(self: *XMLHttpRequest) void { const response_mime = &self.response_mime.?; const isHTML = response_mime.isHTML(); // TODO If finalMIME is not an HTML MIME type or an XML MIME type, then // return. - if (!isHTML) return; - - const ccharset = alloc.dupeZ(u8, response_mime.charset orelse "utf-8") catch { - self.response_obj = .{ .Failure = true }; + if (!isHTML) { return; - }; - defer alloc.free(ccharset); + } + + var ccharset: [:0]const u8 = "utf-8"; + if (response_mime.charset) |rc| { + ccharset = self.arena.dupeZ(u8, rc) catch { + self.response_obj = .{ .Failure = true }; + return; + }; + } var fbs = std.io.fixedBufferStream(self.response_bytes.items); const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch { @@ -760,12 +756,12 @@ pub const XMLHttpRequest = struct { } // setResponseObjJSON parses the received bytes as a std.json.Value. - fn setResponseObjJSON(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { + fn setResponseObjJSON(self: *XMLHttpRequest) void { // TODO should we use parseFromSliceLeaky if we expect the allocator is // already an arena? const p = std.json.parseFromSlice( JSONValue, - alloc, + self.arena, self.response_bytes.items, .{}, ) catch |e| { @@ -790,14 +786,12 @@ pub const XMLHttpRequest = struct { // TODO change the return type to express the string ownership and let // jsruntime free the string once copied to v8. // see https://github.com/lightpanda-io/jsruntime-lib/issues/195 - pub fn _getAllResponseHeaders(self: *XMLHttpRequest, alloc: std.mem.Allocator) ![]const u8 { + pub fn _getAllResponseHeaders(self: *XMLHttpRequest) ![]const u8 { if (self.response_headers.list.items.len == 0) return ""; self.response_headers.sort(); var buf: std.ArrayListUnmanaged(u8) = .{}; - errdefer buf.deinit(alloc); - - const w = buf.writer(alloc); + const w = buf.writer(self.arena); for (self.response_headers.list.items) |entry| { if (entry.value.len == 0) continue; @@ -822,101 +816,96 @@ pub const XMLHttpRequest = struct { } }; -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var send = [_]Case{ - .{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt = event; }", .ex = "undefined" }, - .{ .src = "const req = new XMLHttpRequest()", .ex = "undefined" }, +const testing = @import("../../testing.zig"); +test "Browser.XHR.XMLHttpRequest" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); - .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" }, - // Getter returning a callback crashes. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/200 - // .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; evt = event; }" }, - //.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" }, + try runner.testCases(&.{ + .{ "var nb = 0; var evt = null; function cbk(event) { nb ++; evt = event; }", "undefined" }, + .{ "const req = new XMLHttpRequest()", "undefined" }, - .{ .src = "req.open('GET', 'https://httpbin.io/html')", .ex = "undefined" }, - .{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" }, + .{ "req.onload = cbk", "function cbk(event) { nb ++; evt = event; }" }, + + .{ "req.onload", "function cbk(event) { nb ++; evt = event; }" }, + .{ "req.onload = cbk", "function cbk(event) { nb ++; evt = event; }" }, + + .{ "req.open('GET', 'https://127.0.0.1:9581/xhr')", "undefined" }, + .{ "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", "undefined" }, // ensure open resets values - .{ .src = "req.status", .ex = "0" }, - .{ .src = "req.statusText", .ex = "" }, - .{ .src = "req.getAllResponseHeaders()", .ex = "" }, - .{ .src = "req.getResponseHeader('Content-Type')", .ex = "null" }, - .{ .src = "req.responseText", .ex = "" }, + .{ "req.status ", "0" }, + .{ "req.statusText", "" }, + .{ "req.getAllResponseHeaders()", "" }, + .{ "req.getResponseHeader('Content-Type')", "null" }, + .{ "req.responseText", "" }, - .{ .src = "req.send(); nb", .ex = "0" }, + .{ "req.send(); nb", "0" }, + + // Each case executed waits for all loop callback calls. + // So the url has been retrieved. + .{ "nb", "1" }, + .{ "evt.type", "load" }, + .{ "evt.loaded > 0", "true" }, + .{ "evt instanceof ProgressEvent", "true" }, + .{ "req.status", "200" }, + .{ "req.statusText", "OK" }, + .{ "req.getResponseHeader('Content-Type')", "text/html; charset=utf-8" }, + .{ "req.getAllResponseHeaders().length", "61" }, + .{ "req.responseText.length", "100" }, + .{ "req.response.length == req.responseText.length", "true" }, + .{ "req.responseXML instanceof Document", "true" }, + }, .{}); + + try runner.testCases(&.{ + .{ "const req2 = new XMLHttpRequest()", "undefined" }, + .{ "req2.open('GET', 'https://127.0.0.1:9581/xhr')", "undefined" }, + .{ "req2.responseType = 'document'", "document" }, + + .{ "req2.send()", "undefined" }, // Each case executed waits for all loop callaback calls. // So the url has been retrieved. - .{ .src = "nb", .ex = "1" }, - .{ .src = "evt.type", .ex = "load" }, - .{ .src = "evt.loaded > 0", .ex = "true" }, - .{ .src = "evt instanceof ProgressEvent", .ex = "true" }, - .{ .src = "req.status", .ex = "200" }, - .{ .src = "req.statusText", .ex = "OK" }, - .{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=utf-8" }, - .{ .src = "req.getAllResponseHeaders().length > 64", .ex = "true" }, - .{ .src = "req.responseText.length > 64", .ex = "true" }, - .{ .src = "req.response.length == req.responseText.length", .ex = "true" }, - .{ .src = "req.responseXML instanceof Document", .ex = "true" }, - }; - try checkCases(js_env, &send); + .{ "req2.status", "200" }, + .{ "req2.statusText", "OK" }, + .{ "req2.response instanceof Document", "true" }, + .{ "req2.responseXML instanceof Document", "true" }, + }, .{}); - var document = [_]Case{ - .{ .src = "const req2 = new XMLHttpRequest()", .ex = "undefined" }, - .{ .src = "req2.open('GET', 'https://httpbin.io/html')", .ex = "undefined" }, - .{ .src = "req2.responseType = 'document'", .ex = "document" }, + try runner.testCases(&.{ + .{ "const req3 = new XMLHttpRequest()", "undefined" }, + .{ "req3.open('GET', 'https://127.0.0.1:9581/xhr/json')", "undefined" }, + .{ "req3.responseType = 'json'", "json" }, - .{ .src = "req2.send()", .ex = "undefined" }, + .{ "req3.send()", "undefined" }, // Each case executed waits for all loop callaback calls. // So the url has been retrieved. - .{ .src = "req2.status", .ex = "200" }, - .{ .src = "req2.statusText", .ex = "OK" }, - .{ .src = "req2.response instanceof Document", .ex = "true" }, - .{ .src = "req2.responseXML instanceof Document", .ex = "true" }, - }; - try checkCases(js_env, &document); + .{ "req3.status", "200" }, + .{ "req3.statusText", "OK" }, + .{ "req3.response.over", "9000!!!" }, + }, .{}); - var json = [_]Case{ - .{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" }, - .{ .src = "req3.open('GET', 'https://httpbin.io/json')", .ex = "undefined" }, - .{ .src = "req3.responseType = 'json'", .ex = "json" }, - - .{ .src = "req3.send()", .ex = "undefined" }, + try runner.testCases(&.{ + .{ "const req4 = new XMLHttpRequest()", "undefined" }, + .{ "req4.open('POST', 'https://127.0.0.1:9581/xhr')", "undefined" }, + .{ "req4.send('foo')", "undefined" }, // Each case executed waits for all loop callaback calls. // So the url has been retrieved. - .{ .src = "req3.status", .ex = "200" }, - .{ .src = "req3.statusText", .ex = "OK" }, - .{ .src = "req3.response.slideshow.author", .ex = "Yours Truly" }, - }; - try checkCases(js_env, &json); + .{ "req4.status", "200" }, + .{ "req4.statusText", "OK" }, + .{ "req4.responseText.length > 64", "true" }, + }, .{}); - var post = [_]Case{ - .{ .src = "const req4 = new XMLHttpRequest()", .ex = "undefined" }, - .{ .src = "req4.open('POST', 'https://httpbin.io/post')", .ex = "undefined" }, - .{ .src = "req4.send('foo')", .ex = "undefined" }, + try runner.testCases(&.{ + .{ "const req5 = new XMLHttpRequest()", "undefined" }, + .{ "req5.open('GET', 'https://127.0.0.1:9581/xhr')", "undefined" }, + .{ "var status = 0; req5.onload = function () { status = this.status };", "function () { status = this.status }" }, + .{ "req5.send()", "undefined" }, // Each case executed waits for all loop callaback calls. // So the url has been retrieved. - .{ .src = "req4.status", .ex = "200" }, - .{ .src = "req4.statusText", .ex = "OK" }, - .{ .src = "req4.responseText.length > 64", .ex = "true" }, - }; - try checkCases(js_env, &post); - - var cbk = [_]Case{ - .{ .src = "const req5 = new XMLHttpRequest()", .ex = "undefined" }, - .{ .src = "req5.open('GET', 'https://httpbin.io/json')", .ex = "undefined" }, - .{ .src = "var status = 0; req5.onload = function () { status = this.status };", .ex = "function () { status = this.status }" }, - .{ .src = "req5.send()", .ex = "undefined" }, - - // Each case executed waits for all loop callaback calls. - // So the url has been retrieved. - .{ .src = "status", .ex = "200" }, - }; - try checkCases(js_env, &cbk); + .{ "status", "200" }, + }, .{}); } diff --git a/src/xmlserializer/xmlserializer.zig b/src/browser/xmlserializer/xmlserializer.zig similarity index 54% rename from src/xmlserializer/xmlserializer.zig rename to src/browser/xmlserializer/xmlserializer.zig index a0153558..ea2a4697 100644 --- a/src/xmlserializer/xmlserializer.zig +++ b/src/browser/xmlserializer/xmlserializer.zig @@ -18,14 +18,11 @@ // const std = @import("std"); -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; +const SessionState = @import("../env.zig").SessionState; -const DOMError = @import("netsurf").DOMError; - -const parser = @import("netsurf"); -const dump = @import("../browser/dump.zig"); +const dump = @import("../dump.zig"); +const parser = @import("../netsurf.zig"); +const DOMError = parser.DOMError; pub const Interfaces = .{ XMLSerializer, @@ -33,39 +30,28 @@ pub const Interfaces = .{ // https://w3c.github.io/DOM-Parsing/#dom-xmlserializer-constructor pub const XMLSerializer = struct { - pub const mem_guarantied = true; - pub fn constructor() !XMLSerializer { return .{}; } - pub fn deinit(_: *XMLSerializer, _: std.mem.Allocator) void {} - - pub fn _serializeToString(_: XMLSerializer, alloc: std.mem.Allocator, root: *parser.Node) ![]const u8 { - var buf = std.ArrayList(u8).init(alloc); - defer buf.deinit(); - + pub fn _serializeToString(_: *const XMLSerializer, root: *parser.Node, state: *SessionState) ![]const u8 { + var buf = std.ArrayList(u8).init(state.arena); if (try parser.nodeType(root) == .document) { try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), buf.writer()); } else { try dump.writeNode(root, buf.writer()); } - // TODO express the caller owned the slice. - // https://github.com/lightpanda-io/jsruntime-lib/issues/195 - return try buf.toOwnedSlice(); + return buf.items; } }; -// Tests -// ----- +const testing = @import("../../testing.zig"); +test "Browser.XMLSerializer" { + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); + defer runner.deinit(); -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var serializer = [_]Case{ - .{ .src = "const s = new XMLSerializer()", .ex = "undefined" }, - .{ .src = "s.serializeToString(document.getElementById('para'))", .ex = "

And

" }, - }; - try checkCases(js_env, &serializer); + try runner.testCases(&.{ + .{ "const s = new XMLSerializer()", "undefined" }, + .{ "s.serializeToString(document.getElementById('para'))", "

And

" }, + }, .{}); } diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index f609be49..a7a6b658 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -17,9 +17,10 @@ // along with this program. If not, see . const std = @import("std"); -const parser = @import("netsurf"); const Allocator = std.mem.Allocator; +const parser = @import("../browser/netsurf.zig"); + pub const Id = u32; const log = std.log.scoped(.cdp_node); diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index ff00b5e9..146fc84e 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -72,13 +72,16 @@ pub fn CDPT(comptime TypeProvider: type) type { pub const Browser = TypeProvider.Browser; pub const Session = TypeProvider.Session; - pub fn init(app: *App, client: TypeProvider.Client) Self { + pub fn init(app: *App, client: TypeProvider.Client) !Self { const allocator = app.allocator; + const browser = try Browser.init(app); + errdefer browser.deinit(); + return .{ .client = client, + .browser = browser, .allocator = allocator, .browser_context = null, - .browser = Browser.init(app), .message_arena = std.heap.ArenaAllocator.init(allocator), .browser_context_pool = std.heap.MemoryPool(BrowserContext(Self)).init(allocator), }; diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 80e6b96b..e85ee7fe 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -17,10 +17,10 @@ // along with this program. If not, see . const std = @import("std"); -const parser = @import("netsurf"); const Node = @import("../Node.zig"); -const css = @import("../../dom/css.zig"); -const dom_node = @import("../../dom/node.zig"); +const css = @import("../../browser/dom/css.zig"); +const parser = @import("../../browser/netsurf.zig"); +const dom_node = @import("../../browser/dom/node.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { @@ -134,17 +134,20 @@ fn resolveNode(cmd: anytype) !void { // node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement // So we use the Node.Union when retrieve the value from the environment - const jsValue = try bc.session.env.findOrAddValue(try dom_node.Node.toInterface(node._node)); - const remoteObject = try bc.session.inspector.getRemoteObject(&bc.session.env, jsValue, params.objectGroup orelse ""); - defer remoteObject.deinit(); + const remote_object = try bc.session.inspector.getRemoteObject( + bc.session.executor, + params.objectGroup orelse "", + try dom_node.Node.toInterface(node._node), + ); + defer remote_object.deinit(); const arena = cmd.arena; return cmd.sendResult(.{ .object = .{ - .type = try remoteObject.getType(arena), - .subtype = try remoteObject.getSubtype(arena), - .className = try remoteObject.getClassName(arena), - .description = try remoteObject.getDescription(arena), - .objectId = try remoteObject.getObjectId(arena), + .type = try remote_object.getType(arena), + .subtype = try remote_object.getSubtype(arena), + .className = try remote_object.getClassName(arena), + .description = try remote_object.getDescription(arena), + .objectId = try remote_object.getObjectId(arena), } }, .{}); } diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index c2c833e3..1f15caf6 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -23,9 +23,9 @@ const Allocator = std.mem.Allocator; const Testing = @This(); const main = @import("cdp.zig"); -const parser = @import("netsurf"); const URL = @import("../url.zig").URL; const App = @import("../app.zig").App; +const parser = @import("../browser/netsurf.zig"); const base = @import("../testing.zig"); pub const allocator = base.allocator; @@ -40,7 +40,7 @@ const Browser = struct { session: ?*Session = null, arena: std.heap.ArenaAllocator, - pub fn init(app: *App) Browser { + pub fn init(app: *App) !Browser { return .{ .arena = std.heap.ArenaAllocator.init(app.allocator), }; @@ -61,8 +61,8 @@ const Browser = struct { self.session.?.* = .{ .page = null, .arena = arena, - .env = Env{}, - .inspector = Inspector{}, + .executor = .{}, + .inspector = .{}, }; return self.session.?; } @@ -78,7 +78,7 @@ const Browser = struct { const Session = struct { page: ?Page = null, arena: Allocator, - env: Env, + executor: Executor, inspector: Inspector, pub fn currentPage(self: *Session) ?*Page { @@ -107,19 +107,19 @@ const Session = struct { } }; -const Env = struct { - pub fn findOrAddValue(self: *Env, value: anytype) !@TypeOf(value) { // ? - _ = self; - return value; - } -}; +const Executor = struct {}; const Inspector = struct { - pub fn getRemoteObject(self: Inspector, env: *Env, jsValue: anytype, groupName: []const u8) !RemoteObject { + pub fn getRemoteObject( + self: *const Inspector, + executor: Executor, + group: []const u8, + value: anytype, + ) !RemoteObject { _ = self; - _ = env; - _ = jsValue; - _ = groupName; + _ = executor; + _ = group; + _ = value; return RemoteObject{}; } }; @@ -217,7 +217,7 @@ const TestContext = struct { self.client = Client.init(self.arena.allocator()); // Don't use the arena here. We want to detect leaks in CDP. // The arena is only for test-specific stuff - self.cdp_ = TestCDP.init(self.app, &self.client.?); + self.cdp_ = try TestCDP.init(self.app, &self.client.?); } return &self.cdp_.?; } diff --git a/src/dom/document.zig b/src/dom/document.zig deleted file mode 100644 index 0b8d0cd8..00000000 --- a/src/dom/document.zig +++ /dev/null @@ -1,468 +0,0 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const parser = @import("netsurf"); - -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; -const Variadic = jsruntime.Variadic; - -const Node = @import("node.zig").Node; -const NodeList = @import("nodelist.zig").NodeList; -const NodeUnion = @import("node.zig").Union; - -const collection = @import("html_collection.zig"); -const css = @import("css.zig"); - -const Element = @import("element.zig").Element; -const ElementUnion = @import("element.zig").Union; - -const DocumentType = @import("document_type.zig").DocumentType; -const DocumentFragment = @import("document_fragment.zig").DocumentFragment; -const DOMImplementation = @import("implementation.zig").DOMImplementation; - -const UserContext = @import("../user_context.zig").UserContext; - -// WEB IDL https://dom.spec.whatwg.org/#document -pub const Document = struct { - pub const Self = parser.Document; - pub const prototype = *Node; - pub const mem_guarantied = true; - - pub fn constructor(userctx: UserContext) !*parser.DocumentHTML { - const doc = try parser.documentCreateDocument( - try parser.documentHTMLGetTitle(userctx.document), - ); - - // we have to work w/ document instead of html document. - const ddoc = parser.documentHTMLToDocument(doc); - const ccur = parser.documentHTMLToDocument(userctx.document); - try parser.documentSetDocumentURI(ddoc, try parser.documentGetDocumentURI(ccur)); - try parser.documentSetInputEncoding(ddoc, try parser.documentGetInputEncoding(ccur)); - - return doc; - } - - // JS funcs - // -------- - pub fn get_implementation(_: *parser.Document) DOMImplementation { - return DOMImplementation{}; - } - - pub fn get_documentElement(self: *parser.Document) !?ElementUnion { - const e = try parser.documentGetDocumentElement(self); - if (e == null) return null; - return try Element.toInterface(e.?); - } - - pub fn get_documentURI(self: *parser.Document) ![]const u8 { - return try parser.documentGetDocumentURI(self); - } - - pub fn get_URL(self: *parser.Document) ![]const u8 { - return try get_documentURI(self); - } - - // TODO implement contentType - pub fn get_contentType(self: *parser.Document) []const u8 { - _ = self; - return "text/html"; - } - - // TODO implement compactMode - pub fn get_compatMode(self: *parser.Document) []const u8 { - _ = self; - return "CSS1Compat"; - } - - pub fn get_characterSet(self: *parser.Document) ![]const u8 { - return try parser.documentGetInputEncoding(self); - } - - // alias of get_characterSet - pub fn get_charset(self: *parser.Document) ![]const u8 { - return try get_characterSet(self); - } - - // alias of get_characterSet - pub fn get_inputEncoding(self: *parser.Document) ![]const u8 { - return try get_characterSet(self); - } - - pub fn get_doctype(self: *parser.Document) !?*parser.DocumentType { - return try parser.documentGetDoctype(self); - } - - pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !*parser.Event { - // TODO: for now only "Event" constructor is supported - // see table on https://dom.spec.whatwg.org/#dom-document-createevent $2 - if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) { - return try parser.eventCreate(); - } - return parser.DOMError.NotSupported; - } - - pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion { - const e = try parser.documentGetElementById(self, id) orelse return null; - return try Element.toInterface(e); - } - - pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion { - const e = try parser.documentCreateElement(self, tag_name); - return try Element.toInterface(e); - } - - pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion { - const e = try parser.documentCreateElementNS(self, ns, tag_name); - return try Element.toInterface(e); - } - - // We can't simply use libdom dom_document_get_elements_by_tag_name here. - // Indeed, netsurf implemented a previous dom spec when - // getElementsByTagName returned a NodeList. - // But since - // https://github.com/whatwg/dom/commit/190700b7c12ecfd3b5ebdb359ab1d6ea9cbf7749 - // the spec changed to return an HTMLCollection instead. - // That's why we reimplemented getElementsByTagName by using an - // HTMLCollection in zig here. - pub fn _getElementsByTagName( - self: *parser.Document, - alloc: std.mem.Allocator, - tag_name: []const u8, - ) !collection.HTMLCollection { - return try collection.HTMLCollectionByTagName(alloc, parser.documentToNode(self), tag_name, true); - } - - pub fn _getElementsByClassName( - self: *parser.Document, - alloc: std.mem.Allocator, - classNames: []const u8, - ) !collection.HTMLCollection { - return try collection.HTMLCollectionByClassName(alloc, parser.documentToNode(self), classNames, true); - } - - pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment { - return try parser.documentCreateDocumentFragment(self); - } - - pub fn _createTextNode(self: *parser.Document, data: []const u8) !*parser.Text { - return try parser.documentCreateTextNode(self, data); - } - - pub fn _createCDATASection(self: *parser.Document, data: []const u8) !*parser.CDATASection { - return try parser.documentCreateCDATASection(self, data); - } - - pub fn _createComment(self: *parser.Document, data: []const u8) !*parser.Comment { - return try parser.documentCreateComment(self, data); - } - - pub fn _createProcessingInstruction(self: *parser.Document, target: []const u8, data: []const u8) !*parser.ProcessingInstruction { - return try parser.documentCreateProcessingInstruction(self, target, data); - } - - pub fn _importNode(self: *parser.Document, node: *parser.Node, deep: ?bool) !NodeUnion { - const n = try parser.documentImportNode(self, node, deep orelse false); - return try Node.toInterface(n); - } - - pub fn _adoptNode(self: *parser.Document, node: *parser.Node) !NodeUnion { - const n = try parser.documentAdoptNode(self, node); - return try Node.toInterface(n); - } - - pub fn _createAttribute(self: *parser.Document, name: []const u8) !*parser.Attribute { - return try parser.documentCreateAttribute(self, name); - } - - pub fn _createAttributeNS(self: *parser.Document, ns: []const u8, qname: []const u8) !*parser.Attribute { - return try parser.documentCreateAttributeNS(self, ns, qname); - } - - // ParentNode - // https://dom.spec.whatwg.org/#parentnode - pub fn get_children(self: *parser.Document) !collection.HTMLCollection { - return try collection.HTMLCollectionChildren(parser.documentToNode(self), false); - } - - pub fn get_firstElementChild(self: *parser.Document) !?ElementUnion { - const elt = try parser.documentGetDocumentElement(self) orelse return null; - return try Element.toInterface(elt); - } - - pub fn get_lastElementChild(self: *parser.Document) !?ElementUnion { - const elt = try parser.documentGetDocumentElement(self) orelse return null; - return try Element.toInterface(elt); - } - - pub fn get_childElementCount(self: *parser.Document) !u32 { - _ = try parser.documentGetDocumentElement(self) orelse return 0; - return 1; - } - - pub fn _querySelector(self: *parser.Document, alloc: std.mem.Allocator, selector: []const u8) !?ElementUnion { - if (selector.len == 0) return null; - - const n = try css.querySelector(alloc, parser.documentToNode(self), selector); - - if (n == null) return null; - - return try Element.toInterface(parser.nodeToElement(n.?)); - } - - pub fn _querySelectorAll(self: *parser.Document, alloc: std.mem.Allocator, selector: []const u8) !NodeList { - return css.querySelectorAll(alloc, parser.documentToNode(self), selector); - } - - // TODO according with https://dom.spec.whatwg.org/#parentnode, the - // function must accept either node or string. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn _prepend(self: *parser.Document, nodes: ?Variadic(*parser.Node)) !void { - return Node.prepend(parser.documentToNode(self), nodes); - } - - // TODO according with https://dom.spec.whatwg.org/#parentnode, the - // function must accept either node or string. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn _append(self: *parser.Document, nodes: ?Variadic(*parser.Node)) !void { - return Node.append(parser.documentToNode(self), nodes); - } - - // TODO according with https://dom.spec.whatwg.org/#parentnode, the - // function must accept either node or string. - // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114 - pub fn _replaceChildren(self: *parser.Document, nodes: ?Variadic(*parser.Node)) !void { - return Node.replaceChildren(parser.documentToNode(self), nodes); - } - - pub fn deinit(_: *parser.Document, _: std.mem.Allocator) void {} -}; - -// Tests -// ----- - -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var constructor = [_]Case{ - .{ .src = "document.__proto__.__proto__.constructor.name", .ex = "Document" }, - .{ .src = "document.__proto__.__proto__.__proto__.constructor.name", .ex = "Node" }, - .{ .src = "document.__proto__.__proto__.__proto__.__proto__.constructor.name", .ex = "EventTarget" }, - - .{ .src = "let newdoc = new Document()", .ex = "undefined" }, - .{ .src = "newdoc.documentElement", .ex = "null" }, - .{ .src = "newdoc.children.length", .ex = "0" }, - .{ .src = "newdoc.getElementsByTagName('*').length", .ex = "0" }, - .{ .src = "newdoc.getElementsByTagName('*').item(0)", .ex = "null" }, - .{ .src = "newdoc.inputEncoding === document.inputEncoding", .ex = "true" }, - .{ .src = "newdoc.documentURI === document.documentURI", .ex = "true" }, - .{ .src = "newdoc.URL === document.URL", .ex = "true" }, - .{ .src = "newdoc.compatMode === document.compatMode", .ex = "true" }, - .{ .src = "newdoc.characterSet === document.characterSet", .ex = "true" }, - .{ .src = "newdoc.charset === document.charset", .ex = "true" }, - .{ .src = "newdoc.contentType === document.contentType", .ex = "true" }, - }; - try checkCases(js_env, &constructor); - - var getElementById = [_]Case{ - .{ .src = "let getElementById = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "getElementById.constructor.name", .ex = "HTMLDivElement" }, - .{ .src = "getElementById.localName", .ex = "div" }, - }; - try checkCases(js_env, &getElementById); - - var getElementsByTagName = [_]Case{ - .{ .src = "let getElementsByTagName = document.getElementsByTagName('p')", .ex = "undefined" }, - .{ .src = "getElementsByTagName.length", .ex = "2" }, - .{ .src = "getElementsByTagName.item(0).localName", .ex = "p" }, - .{ .src = "getElementsByTagName.item(1).localName", .ex = "p" }, - .{ .src = "let getElementsByTagNameAll = document.getElementsByTagName('*')", .ex = "undefined" }, - .{ .src = "getElementsByTagNameAll.length", .ex = "8" }, - .{ .src = "getElementsByTagNameAll.item(0).localName", .ex = "html" }, - .{ .src = "getElementsByTagNameAll.item(7).localName", .ex = "p" }, - .{ .src = "getElementsByTagNameAll.namedItem('para-empty-child').localName", .ex = "span" }, - }; - try checkCases(js_env, &getElementsByTagName); - - var getElementsByClassName = [_]Case{ - .{ .src = "let ok = document.getElementsByClassName('ok')", .ex = "undefined" }, - .{ .src = "ok.length", .ex = "2" }, - .{ .src = "let empty = document.getElementsByClassName('empty')", .ex = "undefined" }, - .{ .src = "empty.length", .ex = "1" }, - .{ .src = "let emptyok = document.getElementsByClassName('empty ok')", .ex = "undefined" }, - .{ .src = "emptyok.length", .ex = "1" }, - }; - try checkCases(js_env, &getElementsByClassName); - - var getDocumentElement = [_]Case{ - .{ .src = "let e = document.documentElement", .ex = "undefined" }, - .{ .src = "e.localName", .ex = "html" }, - }; - try checkCases(js_env, &getDocumentElement); - - var getCharacterSet = [_]Case{ - .{ .src = "document.characterSet", .ex = "UTF-8" }, - .{ .src = "document.charset", .ex = "UTF-8" }, - .{ .src = "document.inputEncoding", .ex = "UTF-8" }, - }; - try checkCases(js_env, &getCharacterSet); - - var getCompatMode = [_]Case{ - .{ .src = "document.compatMode", .ex = "CSS1Compat" }, - }; - try checkCases(js_env, &getCompatMode); - - var getContentType = [_]Case{ - .{ .src = "document.contentType", .ex = "text/html" }, - }; - try checkCases(js_env, &getContentType); - - var getDocumentURI = [_]Case{ - .{ .src = "document.documentURI", .ex = "about:blank" }, - .{ .src = "document.URL", .ex = "about:blank" }, - }; - try checkCases(js_env, &getDocumentURI); - - var getImplementation = [_]Case{ - .{ .src = "let impl = document.implementation", .ex = "undefined" }, - }; - try checkCases(js_env, &getImplementation); - - var new = [_]Case{ - .{ .src = "let d = new Document()", .ex = "undefined" }, - .{ .src = "d.characterSet", .ex = "UTF-8" }, - .{ .src = "d.URL", .ex = "about:blank" }, - .{ .src = "d.documentURI", .ex = "about:blank" }, - .{ .src = "d.compatMode", .ex = "CSS1Compat" }, - .{ .src = "d.contentType", .ex = "text/html" }, - }; - try checkCases(js_env, &new); - - var createDocumentFragment = [_]Case{ - .{ .src = "var v = document.createDocumentFragment()", .ex = "undefined" }, - .{ .src = "v.nodeName", .ex = "#document-fragment" }, - }; - try checkCases(js_env, &createDocumentFragment); - - var createTextNode = [_]Case{ - .{ .src = "var v = document.createTextNode('foo')", .ex = "undefined" }, - .{ .src = "v.nodeName", .ex = "#text" }, - }; - try checkCases(js_env, &createTextNode); - - var createCDATASection = [_]Case{ - .{ .src = "var v = document.createCDATASection('foo')", .ex = "undefined" }, - .{ .src = "v.nodeName", .ex = "#cdata-section" }, - }; - try checkCases(js_env, &createCDATASection); - - var createComment = [_]Case{ - .{ .src = "var v = document.createComment('foo')", .ex = "undefined" }, - .{ .src = "v.nodeName", .ex = "#comment" }, - .{ .src = "let v2 = v.cloneNode()", .ex = "undefined" }, - }; - try checkCases(js_env, &createComment); - - var createProcessingInstruction = [_]Case{ - .{ .src = "let pi = document.createProcessingInstruction('foo', 'bar')", .ex = "undefined" }, - .{ .src = "pi.target", .ex = "foo" }, - .{ .src = "let pi2 = pi.cloneNode()", .ex = "undefined" }, - }; - try checkCases(js_env, &createProcessingInstruction); - - var importNode = [_]Case{ - .{ .src = "let nimp = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "var v = document.importNode(nimp)", .ex = "undefined" }, - .{ .src = "v.nodeName", .ex = "DIV" }, - }; - try checkCases(js_env, &importNode); - - var createAttr = [_]Case{ - .{ .src = "var v = document.createAttribute('foo')", .ex = "undefined" }, - .{ .src = "v.nodeName", .ex = "foo" }, - }; - try checkCases(js_env, &createAttr); - - var parentNode = [_]Case{ - .{ .src = "document.children.length", .ex = "1" }, - .{ .src = "document.children.item(0).nodeName", .ex = "HTML" }, - .{ .src = "document.firstElementChild.nodeName", .ex = "HTML" }, - .{ .src = "document.lastElementChild.nodeName", .ex = "HTML" }, - .{ .src = "document.childElementCount", .ex = "1" }, - - .{ .src = "let nd = new Document()", .ex = "undefined" }, - .{ .src = "nd.children.length", .ex = "0" }, - .{ .src = "nd.children.item(0)", .ex = "null" }, - .{ .src = "nd.firstElementChild", .ex = "null" }, - .{ .src = "nd.lastElementChild", .ex = "null" }, - .{ .src = "nd.childElementCount", .ex = "0" }, - - .{ .src = "let emptydoc = document.createElement('html')", .ex = "undefined" }, - .{ .src = "emptydoc.prepend(document.createElement('html'))", .ex = "undefined" }, - - .{ .src = "let emptydoc2 = document.createElement('html')", .ex = "undefined" }, - .{ .src = "emptydoc2.append(document.createElement('html'))", .ex = "undefined" }, - }; - try checkCases(js_env, &parentNode); - - var querySelector = [_]Case{ - .{ .src = "document.querySelector('')", .ex = "null" }, - .{ .src = "document.querySelector('*').nodeName", .ex = "HTML" }, - .{ .src = "document.querySelector('#content').id", .ex = "content" }, - .{ .src = "document.querySelector('#para').id", .ex = "para" }, - .{ .src = "document.querySelector('.ok').id", .ex = "link" }, - .{ .src = "document.querySelector('a ~ p').id", .ex = "para-empty" }, - .{ .src = "document.querySelector(':root').nodeName", .ex = "HTML" }, - - .{ .src = "document.querySelectorAll('p').length", .ex = "2" }, - .{ .src = - \\Array.from(document.querySelectorAll('#content > p#para-empty')) - \\.map(row => row.querySelector('span').textContent) - \\.length; - , .ex = "1" }, - }; - try checkCases(js_env, &querySelector); - - // this test breaks the doc structure, keep it at the end of the test - // suite. - var adoptNode = [_]Case{ - .{ .src = "let nadop = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "var v = document.adoptNode(nadop)", .ex = "undefined" }, - .{ .src = "v.nodeName", .ex = "DIV" }, - }; - try checkCases(js_env, &adoptNode); - - const tags = comptime parser.Tag.all(); - var createElements: [(tags.len) * 2]Case = undefined; - inline for (tags, 0..) |tag, i| { - const tag_name = @tagName(tag); - createElements[i * 2] = Case{ - .src = "var " ++ tag_name ++ "Elem = document.createElement('" ++ tag_name ++ "')", - .ex = "undefined", - }; - createElements[(i * 2) + 1] = Case{ - .src = tag_name ++ "Elem.localName", - .ex = tag_name, - }; - } - try checkCases(js_env, &createElements); -} diff --git a/src/dom/event_target.zig b/src/dom/event_target.zig deleted file mode 100644 index ca688993..00000000 --- a/src/dom/event_target.zig +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const jsruntime = @import("jsruntime"); -const Callback = jsruntime.Callback; -const JSObjectID = jsruntime.JSObjectID; -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - -const parser = @import("netsurf"); -const EventHandler = @import("../events/event.zig").EventHandler; - -const DOMException = @import("exceptions.zig").DOMException; -const Nod = @import("node.zig"); - -// EventTarget interfaces -pub const Union = Nod.Union; - -// EventTarget implementation -pub const EventTarget = struct { - pub const Self = parser.EventTarget; - pub const Exception = DOMException; - pub const mem_guarantied = true; - - pub fn toInterface(et: *parser.EventTarget) !Union { - // NOTE: for now we state that all EventTarget are Nodes - // TODO: handle other types (eg. Window) - return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))); - } - - // JS funcs - // -------- - - pub fn _addEventListener( - self: *parser.EventTarget, - alloc: std.mem.Allocator, - eventType: []const u8, - cbk: Callback, - capture: ?bool, - // TODO: hanle EventListenerOptions - // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114 - ) !void { - - // check if event target has already this listener - const lst = try parser.eventTargetHasListener( - self, - eventType, - capture orelse false, - cbk.id(), - ); - if (lst != null) { - return; - } - - try parser.eventTargetAddEventListener( - self, - alloc, - eventType, - EventHandler, - .{ .cbk = cbk }, - capture orelse false, - ); - } - - pub fn _removeEventListener( - self: *parser.EventTarget, - alloc: std.mem.Allocator, - eventType: []const u8, - cbk_id: JSObjectID, - capture: ?bool, - // TODO: hanle EventListenerOptions - // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114 - ) !void { - - // check if event target has already this listener - const lst = try parser.eventTargetHasListener( - self, - eventType, - capture orelse false, - cbk_id.get(), - ); - if (lst == null) { - return; - } - - // remove listener - try parser.eventTargetRemoveEventListener( - self, - alloc, - eventType, - lst.?, - capture orelse false, - ); - } - - pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool { - return try parser.eventTargetDispatchEvent(self, event); - } - - pub fn deinit(self: *parser.EventTarget, alloc: std.mem.Allocator) void { - parser.eventTargetRemoveAllEventListeners(self, alloc) catch unreachable; - } -}; - -// Tests -// ----- - -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var common = [_]Case{ - .{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "let para = document.getElementById('para')", .ex = "undefined" }, - // NOTE: as some event properties will change during the event dispatching phases - // we need to copy thoses values in order to check them afterwards - .{ .src = - \\var nb = 0; var evt; var phase; var cur; - \\function cbk(event) { - \\evt = event; - \\phase = event.eventPhase; - \\cur = event.currentTarget; - \\nb ++; - \\} - , .ex = "undefined" }, - }; - try checkCases(js_env, &common); - - var basic = [_]Case{ - .{ .src = "content.addEventListener('basic', cbk)", .ex = "undefined" }, - .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" }, - .{ .src = "nb", .ex = "1" }, - .{ .src = "evt instanceof Event", .ex = "true" }, - .{ .src = "evt.type", .ex = "basic" }, - .{ .src = "phase", .ex = "2" }, - .{ .src = "cur.getAttribute('id')", .ex = "content" }, - }; - try checkCases(js_env, &basic); - - var basic_child = [_]Case{ - .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, - .{ .src = "para.dispatchEvent(new Event('basic'))", .ex = "true" }, - .{ .src = "nb", .ex = "0" }, // handler is not called, no capture, not the target, no bubbling - .{ .src = "evt === undefined", .ex = "true" }, - }; - try checkCases(js_env, &basic_child); - - var basic_twice = [_]Case{ - .{ .src = "nb = 0", .ex = "0" }, - .{ .src = "content.addEventListener('basic', cbk)", .ex = "undefined" }, - .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" }, - .{ .src = "nb", .ex = "1" }, - }; - try checkCases(js_env, &basic_twice); - - var basic_twice_capture = [_]Case{ - .{ .src = "nb = 0", .ex = "0" }, - .{ .src = "content.addEventListener('basic', cbk, true)", .ex = "undefined" }, - .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" }, - .{ .src = "nb", .ex = "2" }, - }; - try checkCases(js_env, &basic_twice_capture); - - var basic_remove = [_]Case{ - .{ .src = "nb = 0", .ex = "0" }, - .{ .src = "content.removeEventListener('basic', cbk)", .ex = "undefined" }, - .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" }, - .{ .src = "nb", .ex = "1" }, - }; - try checkCases(js_env, &basic_remove); - - var basic_capture_remove = [_]Case{ - .{ .src = "nb = 0", .ex = "0" }, - .{ .src = "content.removeEventListener('basic', cbk, true)", .ex = "undefined" }, - .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" }, - .{ .src = "nb", .ex = "0" }, - }; - try checkCases(js_env, &basic_capture_remove); - - var capture = [_]Case{ - .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, - .{ .src = "content.addEventListener('capture', cbk, true)", .ex = "undefined" }, - .{ .src = "content.dispatchEvent(new Event('capture'))", .ex = "true" }, - .{ .src = "nb", .ex = "1" }, - .{ .src = "evt instanceof Event", .ex = "true" }, - .{ .src = "evt.type", .ex = "capture" }, - .{ .src = "phase", .ex = "2" }, - .{ .src = "cur.getAttribute('id')", .ex = "content" }, - }; - try checkCases(js_env, &capture); - - var capture_child = [_]Case{ - .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, - .{ .src = "para.dispatchEvent(new Event('capture'))", .ex = "true" }, - .{ .src = "nb", .ex = "1" }, - .{ .src = "evt instanceof Event", .ex = "true" }, - .{ .src = "evt.type", .ex = "capture" }, - .{ .src = "phase", .ex = "1" }, - .{ .src = "cur.getAttribute('id')", .ex = "content" }, - }; - try checkCases(js_env, &capture_child); - - var bubbles = [_]Case{ - .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, - .{ .src = "content.addEventListener('bubbles', cbk)", .ex = "undefined" }, - .{ .src = "content.dispatchEvent(new Event('bubbles', {bubbles: true}))", .ex = "true" }, - .{ .src = "nb", .ex = "1" }, - .{ .src = "evt instanceof Event", .ex = "true" }, - .{ .src = "evt.type", .ex = "bubbles" }, - .{ .src = "evt.bubbles", .ex = "true" }, - .{ .src = "phase", .ex = "2" }, - .{ .src = "cur.getAttribute('id')", .ex = "content" }, - }; - try checkCases(js_env, &bubbles); - - var bubbles_child = [_]Case{ - .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, - .{ .src = "para.dispatchEvent(new Event('bubbles', {bubbles: true}))", .ex = "true" }, - .{ .src = "nb", .ex = "1" }, - .{ .src = "evt instanceof Event", .ex = "true" }, - .{ .src = "evt.type", .ex = "bubbles" }, - .{ .src = "phase", .ex = "3" }, - .{ .src = "cur.getAttribute('id')", .ex = "content" }, - }; - try checkCases(js_env, &bubbles_child); -} diff --git a/src/events/event.zig b/src/events/event.zig deleted file mode 100644 index 35069ceb..00000000 --- a/src/events/event.zig +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const generate = @import("../generate.zig"); - -const jsruntime = @import("jsruntime"); -const Callback = jsruntime.Callback; -const CallbackResult = jsruntime.CallbackResult; -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - -const parser = @import("netsurf"); - -const DOMException = @import("../dom/exceptions.zig").DOMException; -const EventTarget = @import("../dom/event_target.zig").EventTarget; -const EventTargetUnion = @import("../dom/event_target.zig").Union; - -const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent; - -const log = std.log.scoped(.events); - -// Event interfaces -pub const Interfaces = .{ - Event, - ProgressEvent, -}; - -pub const Union = generate.Union(Interfaces); - -// https://dom.spec.whatwg.org/#event -pub const Event = struct { - pub const Self = parser.Event; - pub const Exception = DOMException; - pub const mem_guarantied = true; - - pub const EventInit = parser.EventInit; - - // JS - // -- - - pub const _CAPTURING_PHASE = 1; - pub const _AT_TARGET = 2; - pub const _BUBBLING_PHASE = 3; - - pub fn toInterface(evt: *parser.Event) !Union { - return switch (try parser.eventGetInternalType(evt)) { - .event => .{ .Event = evt }, - .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* }, - }; - } - - pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event { - const event = try parser.eventCreate(); - try parser.eventInit(event, eventType, opts orelse EventInit{}); - return event; - } - - // Getters - - pub fn get_type(self: *parser.Event) ![]const u8 { - return try parser.eventType(self); - } - - pub fn get_target(self: *parser.Event) !?EventTargetUnion { - const et = try parser.eventTarget(self); - if (et == null) return null; - return try EventTarget.toInterface(et.?); - } - - pub fn get_currentTarget(self: *parser.Event) !?EventTargetUnion { - const et = try parser.eventCurrentTarget(self); - if (et == null) return null; - return try EventTarget.toInterface(et.?); - } - - pub fn get_eventPhase(self: *parser.Event) !u8 { - return try parser.eventPhase(self); - } - - pub fn get_bubbles(self: *parser.Event) !bool { - return try parser.eventBubbles(self); - } - - pub fn get_cancelable(self: *parser.Event) !bool { - return try parser.eventCancelable(self); - } - - pub fn get_defaultPrevented(self: *parser.Event) !bool { - return try parser.eventDefaultPrevented(self); - } - - pub fn get_isTrusted(self: *parser.Event) !bool { - return try parser.eventIsTrusted(self); - } - - pub fn get_timestamp(self: *parser.Event) !u32 { - return try parser.eventTimestamp(self); - } - - // Methods - - pub fn _initEvent( - self: *parser.Event, - eventType: []const u8, - bubbles: ?bool, - cancelable: ?bool, - ) !void { - const opts = EventInit{ - .bubbles = bubbles orelse false, - .cancelable = cancelable orelse false, - }; - return try parser.eventInit(self, eventType, opts); - } - - pub fn _stopPropagation(self: *parser.Event) !void { - return try parser.eventStopPropagation(self); - } - - pub fn _stopImmediatePropagation(self: *parser.Event) !void { - return try parser.eventStopImmediatePropagation(self); - } - - pub fn _preventDefault(self: *parser.Event) !void { - return try parser.eventPreventDefault(self); - } -}; - -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var common = [_]Case{ - .{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "let para = document.getElementById('para')", .ex = "undefined" }, - .{ .src = "var nb = 0; var evt", .ex = "undefined" }, - }; - try checkCases(js_env, &common); - - var basic = [_]Case{ - .{ .src = - \\content.addEventListener('target', - \\function(e) { - \\evt = e; nb = nb + 1; - \\e.preventDefault(); - \\}) - , .ex = "undefined" }, - .{ .src = "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", .ex = "false" }, - .{ .src = "nb", .ex = "1" }, - .{ .src = "evt.target === content", .ex = "true" }, - .{ .src = "evt.bubbles", .ex = "true" }, - .{ .src = "evt.cancelable", .ex = "true" }, - .{ .src = "evt.defaultPrevented", .ex = "true" }, - .{ .src = "evt.isTrusted", .ex = "true" }, - .{ .src = "evt.timestamp > 1704063600", .ex = "true" }, // 2024/01/01 00:00 - // event.type, event.currentTarget, event.phase checked in EventTarget - }; - try checkCases(js_env, &basic); - - var stop = [_]Case{ - .{ .src = "nb = 0", .ex = "0" }, - .{ .src = - \\content.addEventListener('stop', - \\function(e) { - \\e.stopPropagation(); - \\nb = nb + 1; - \\}, true) - , .ex = "undefined" }, - // the following event listener will not be invoked - .{ .src = - \\para.addEventListener('stop', - \\function(e) { - \\nb = nb + 1; - \\}) - , .ex = "undefined" }, - .{ .src = "para.dispatchEvent(new Event('stop'))", .ex = "true" }, - .{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at content event listener - }; - try checkCases(js_env, &stop); - - var stop_immediate = [_]Case{ - .{ .src = "nb = 0", .ex = "0" }, - .{ .src = - \\content.addEventListener('immediate', - \\function(e) { - \\e.stopImmediatePropagation(); - \\nb = nb + 1; - \\}) - , .ex = "undefined" }, - // the following event listener will not be invoked - .{ .src = - \\content.addEventListener('immediate', - \\function(e) { - \\nb = nb + 1; - \\}) - , .ex = "undefined" }, - .{ .src = "content.dispatchEvent(new Event('immediate'))", .ex = "true" }, - .{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at first content event listener - }; - try checkCases(js_env, &stop_immediate); - - var legacy = [_]Case{ - .{ .src = "nb = 0", .ex = "0" }, - .{ .src = - \\content.addEventListener('legacy', - \\function(e) { - \\evt = e; nb = nb + 1; - \\}) - , .ex = "undefined" }, - .{ .src = "let evtLegacy = document.createEvent('Event')", .ex = "undefined" }, - .{ .src = "evtLegacy.initEvent('legacy')", .ex = "undefined" }, - .{ .src = "content.dispatchEvent(evtLegacy)", .ex = "true" }, - .{ .src = "nb", .ex = "1" }, - }; - try checkCases(js_env, &legacy); - - var remove = [_]Case{ - .{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", .ex = "undefined" }, - .{ .src = "document.addEventListener('count', cbk)", .ex = "undefined" }, - .{ .src = "document.removeEventListener('count', cbk)", .ex = "undefined" }, - .{ .src = "document.dispatchEvent(new Event('count'))", .ex = "true" }, - .{ .src = "nb", .ex = "0" }, - }; - try checkCases(js_env, &remove); -} - -pub const EventHandler = struct { - fn handle(event: ?*parser.Event, data: parser.EventHandlerData) void { - // TODO get the allocator by another way? - var res = CallbackResult.init(data.cbk.nat_ctx.alloc); - defer res.deinit(); - - if (event) |evt| { - data.cbk.trycall(.{ - Event.toInterface(evt) catch unreachable, - }, &res) catch |e| log.err("event handler error: {any}", .{e}); - } else { - data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error (null event): {any}", .{e}); - } - - // in case of function error, we log the result and the trace. - if (!res.success) { - log.info("event handler error try catch: {s}", .{res.result orelse "unknown"}); - log.debug("{s}", .{res.stack orelse "no stack trace"}); - } - } -}.handle; diff --git a/src/html/location.zig b/src/html/location.zig deleted file mode 100644 index 95a62dae..00000000 --- a/src/html/location.zig +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -const std = @import("std"); - -const builtin = @import("builtin"); -const jsruntime = @import("jsruntime"); - -const URL = @import("../url/url.zig").URL; - -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - -// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface -pub const Location = struct { - pub const mem_guarantied = true; - - url: ?URL = null, - - pub fn deinit(_: *Location, _: std.mem.Allocator) void {} - - pub fn get_href(self: *Location, alloc: std.mem.Allocator) ![]const u8 { - if (self.url) |*u| return u.get_href(alloc); - - return ""; - } - - pub fn get_protocol(self: *Location, alloc: std.mem.Allocator) ![]const u8 { - if (self.url) |*u| return u.get_protocol(alloc); - - return ""; - } - - pub fn get_host(self: *Location, alloc: std.mem.Allocator) ![]const u8 { - if (self.url) |*u| return u.get_host(alloc); - - return ""; - } - - pub fn get_hostname(self: *Location) []const u8 { - if (self.url) |*u| return u.get_hostname(); - - return ""; - } - - pub fn get_port(self: *Location, alloc: std.mem.Allocator) ![]const u8 { - if (self.url) |*u| return u.get_port(alloc); - - return ""; - } - - pub fn get_pathname(self: *Location) []const u8 { - if (self.url) |*u| return u.get_pathname(); - - return ""; - } - - pub fn get_search(self: *Location, alloc: std.mem.Allocator) ![]const u8 { - if (self.url) |*u| return u.get_search(alloc); - - return ""; - } - - pub fn get_hash(self: *Location, alloc: std.mem.Allocator) ![]const u8 { - if (self.url) |*u| return u.get_hash(alloc); - - return ""; - } - - pub fn get_origin(self: *Location, alloc: std.mem.Allocator) ![]const u8 { - if (self.url) |*u| return u.get_origin(alloc); - - return ""; - } - - // TODO - pub fn _assign(_: *Location, url: []const u8) !void { - _ = url; - } - - // TODO - pub fn _replace(_: *Location, url: []const u8) !void { - _ = url; - } - - // TODO - pub fn _reload(_: *Location) !void {} - - pub fn _toString(self: *Location, alloc: std.mem.Allocator) ![]const u8 { - return try self.get_href(alloc); - } -}; - -// Tests -// ----- - -pub fn testExecFn( - _: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - var location = [_]Case{ - .{ .src = "location.href", .ex = "https://lightpanda.io/opensource-browser/" }, - .{ .src = "document.location.href", .ex = "https://lightpanda.io/opensource-browser/" }, - - .{ .src = "location.host", .ex = "lightpanda.io" }, - .{ .src = "location.hostname", .ex = "lightpanda.io" }, - .{ .src = "location.origin", .ex = "https://lightpanda.io" }, - .{ .src = "location.pathname", .ex = "/opensource-browser/" }, - .{ .src = "location.hash", .ex = "" }, - .{ .src = "location.port", .ex = "" }, - .{ .src = "location.search", .ex = "" }, - }; - try checkCases(js_env, &location); -} diff --git a/src/http/client.zig b/src/http/client.zig index b6a4c4f4..936669cd 100644 --- a/src/http/client.zig +++ b/src/http/client.zig @@ -27,9 +27,8 @@ const MemoryPool = std.heap.MemoryPool; const ArenaAllocator = std.heap.ArenaAllocator; const tls = @import("tls"); -const jsruntime = @import("jsruntime"); -const IO = jsruntime.IO; -const Loop = jsruntime.Loop; +const IO = @import("../runtime/loop.zig").IO; +const Loop = @import("../runtime/loop.zig").Loop; const log = std.log.scoped(.http_client); @@ -53,7 +52,7 @@ pub const Client = struct { }; pub fn init(allocator: Allocator, max_concurrent: usize, opts: Opts) !Client { - var root_ca = try tls.config.CertBundle.fromSystem(allocator); + var root_ca: tls.config.CertBundle = if (builtin.is_test) .{} else try tls.config.CertBundle.fromSystem(allocator); errdefer root_ca.deinit(allocator); const state_pool = try StatePool.init(allocator, max_concurrent); @@ -69,7 +68,9 @@ pub const Client = struct { pub fn deinit(self: *Client) void { const allocator = self.allocator; - self.root_ca.deinit(allocator); + if (builtin.is_test == false) { + self.root_ca.deinit(allocator); + } self.state_pool.deinit(allocator); } @@ -1907,7 +1908,7 @@ test "HttpClient: sync GET redirect" { } test "HttpClient: async connect error" { - var loop = try jsruntime.Loop.init(testing.allocator); + var loop = try Loop.init(testing.allocator); defer loop.deinit(); const Handler = struct { @@ -2193,7 +2194,7 @@ const TestResponse = struct { }; const CaptureHandler = struct { - loop: jsruntime.Loop, + loop: Loop, reset: Thread.ResetEvent, response: TestResponse, @@ -2201,7 +2202,7 @@ const CaptureHandler = struct { return .{ .reset = .{}, .response = TestResponse.init(), - .loop = try jsruntime.Loop.init(testing.allocator), + .loop = try Loop.init(testing.allocator), }; } diff --git a/src/main.zig b/src/main.zig index 80b5a8f6..2dfc339e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -20,25 +20,19 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; -const jsruntime = @import("jsruntime"); - -const App = @import("app.zig").App; -const Browser = @import("browser/browser.zig").Browser; const server = @import("server.zig"); +const App = @import("app.zig").App; +const Platform = @import("runtime/js.zig").Platform; +const Browser = @import("browser/browser.zig").Browser; -const parser = @import("netsurf"); -const apiweb = @import("apiweb.zig"); - -pub const Types = jsruntime.reflect(apiweb.Interfaces); -pub const UserContext = apiweb.UserContext; -pub const IO = @import("asyncio").Wrapper(jsruntime.Loop); +const parser = @import("browser/netsurf.zig"); const version = @import("build_info").git_commit; const log = std.log.scoped(.cli); pub const std_options = std.Options{ // Set the log level to info - .log_level = .debug, + .log_level = .info, // Define logFn to override the std implementation .logFn = logFn, @@ -60,23 +54,34 @@ pub fn main() !void { const args = try parseArgs(args_arena.allocator()); switch (args.mode) { - .help => args.printUsageAndExit(args.mode.help), + .help => { + args.printUsageAndExit(args.mode.help); + return std.process.cleanExit(); + }, .version => { std.debug.print("{s}\n", .{version}); return std.process.cleanExit(); }, + else => {}, + } + + const platform = Platform.init(); + defer platform.deinit(); + + var app = try App.init(alloc, .{ + .run_mode = args.mode, + .gc_hints = args.gcHints(), + .tls_verify_host = args.tlsVerifyHost(), + }); + defer app.deinit(); + app.telemetry.record(.{ .run = {} }); + + switch (args.mode) { .serve => |opts| { const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| { log.err("address (host:port) {any}\n", .{err}); return args.printUsageAndExit(false); }; - var app = try App.init(alloc, .{ - .run_mode = args.mode, - .tls_verify_host = opts.tls_verify_host, - }); - defer app.deinit(); - - app.telemetry.record(.{ .run = {} }); const timeout = std.time.ns_per_s * @as(u64, opts.timeout); server.run(app, address, timeout) catch |err| { @@ -88,19 +93,8 @@ pub fn main() !void { log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump }); const url = try @import("url.zig").URL.parse(opts.url, null); - var app = try App.init(alloc, .{ - .run_mode = args.mode, - .tls_verify_host = opts.tls_verify_host, - }); - defer app.deinit(); - app.telemetry.record(.{ .run = {} }); - - // vm - const vm = jsruntime.VM.init(); - defer vm.deinit(); - // browser - var browser = Browser.init(app); + var browser = try Browser.init(app); defer browser.deinit(); var session = try browser.newSession({}); @@ -126,6 +120,7 @@ pub fn main() !void { try page.dump(std.io.getStdOut()); } }, + else => unreachable, } } @@ -133,6 +128,21 @@ const Command = struct { mode: Mode, exec_name: []const u8, + fn gcHints(self: *const Command) bool { + return switch (self.mode) { + .serve => |opts| opts.gc_hints, + else => false, + }; + } + + fn tlsVerifyHost(self: *const Command) bool { + return switch (self.mode) { + .serve => |opts| opts.tls_verify_host, + .fetch => |opts| opts.tls_verify_host, + else => true, + }; + } + const Mode = union(App.RunMode) { help: bool, // false when being printed because of an error fetch: Fetch, @@ -144,6 +154,7 @@ const Command = struct { host: []const u8, port: u16, timeout: u16, + gc_hints: bool, tls_verify_host: bool, }; @@ -187,6 +198,9 @@ const Command = struct { \\--timeout Inactivity timeout in seconds before disconnecting clients \\ Defaults to 3 (seconds) \\ + \\--gc_hints Encourage V8 to cleanup garbage for each new browser context. + \\ Defaults to false + \\ \\--insecure_disable_tls_host_verification \\ Disables host verification on all HTTP requests. \\ This is an advanced option which should only be @@ -266,6 +280,11 @@ fn inferMode(opt: []const u8) ?App.RunMode { if (std.mem.eql(u8, opt, "--timeout")) { return .serve; } + + if (std.mem.eql(u8, opt, "--gc_hints")) { + return .serve; + } + return null; } @@ -276,6 +295,7 @@ fn parseServeArgs( var host: []const u8 = "127.0.0.1"; var port: u16 = 9222; var timeout: u16 = 3; + var gc_hints = false; var tls_verify_host = true; while (args.next()) |opt| { @@ -319,6 +339,11 @@ fn parseServeArgs( continue; } + if (std.mem.eql(u8, "--gc_hints", opt)) { + gc_hints = true; + continue; + } + log.err("Unknown option to serve command: '{s}'", .{opt}); return error.UnkownOption; } @@ -327,6 +352,7 @@ fn parseServeArgs( .host = host, .port = port, .timeout = timeout, + .gc_hints = gc_hints, .tls_verify_host = tls_verify_host, }; } @@ -388,3 +414,196 @@ fn logFn( // default std log function. std.log.defaultLog(level, scope, format, args); } + +test { + std.testing.refAllDecls(@This()); +} + +var test_wg: std.Thread.WaitGroup = .{}; +test "tests:beforeAll" { + try parser.init(); + test_wg.startMany(3); + _ = Platform.init(); + + { + const address = try std.net.Address.parseIp("127.0.0.1", 9582); + const thread = try std.Thread.spawn(.{}, serveHTTP, .{address}); + thread.detach(); + } + + { + const address = try std.net.Address.parseIp("127.0.0.1", 9581); + const thread = try std.Thread.spawn(.{}, serveHTTPS, .{address}); + thread.detach(); + } + + { + const address = try std.net.Address.parseIp("127.0.0.1", 9583); + const thread = try std.Thread.spawn(.{}, serveCDP, .{address}); + thread.detach(); + } + + // need to wait for the servers to be listening, else tests will fail because + // they aren't able to connect. + test_wg.wait(); +} + +test "tests:afterAll" { + parser.deinit(); +} + +fn serveHTTP(address: std.net.Address) !void { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var listener = try address.listen(.{ .reuse_address = true }); + defer listener.deinit(); + + test_wg.finish(); + + var read_buffer: [1024]u8 = undefined; + ACCEPT: while (true) { + defer _ = arena.reset(.{ .free_all = {} }); + const aa = arena.allocator(); + + var conn = try listener.accept(); + defer conn.stream.close(); + var http_server = std.http.Server.init(conn, &read_buffer); + + while (http_server.state == .ready) { + var request = http_server.receiveHead() catch |err| switch (err) { + error.HttpConnectionClosing => continue :ACCEPT, + else => { + std.debug.print("Test HTTP Server error: {}\n", .{err}); + return err; + }, + }; + + const path = request.head.target; + if (std.mem.eql(u8, path, "/loader")) { + try request.respond("Hello!", .{}); + } else if (std.mem.eql(u8, path, "/http_client/simple")) { + try request.respond("", .{}); + } else if (std.mem.eql(u8, path, "/http_client/redirect")) { + try request.respond("", .{ + .status = .moved_permanently, + .extra_headers = &.{.{ .name = "LOCATION", .value = "../http_client/echo" }}, + }); + } else if (std.mem.eql(u8, path, "/http_client/redirect/secure")) { + try request.respond("", .{ + .status = .moved_permanently, + .extra_headers = &.{.{ .name = "LOCATION", .value = "https://127.0.0.1:9581/http_client/body" }}, + }); + } else if (std.mem.eql(u8, path, "/http_client/echo")) { + var headers: std.ArrayListUnmanaged(std.http.Header) = .{}; + + var it = request.iterateHeaders(); + while (it.next()) |hdr| { + try headers.append(aa, .{ + .name = try std.fmt.allocPrint(aa, "_{s}", .{hdr.name}), + .value = hdr.value, + }); + } + + try request.respond("over 9000!", .{ + .status = .created, + .extra_headers = headers.items, + }); + } + } + } +} + +// This is a lot of work for testing TLS, but the TLS (async) code is complicated +// This "server" is written specifically to test the client. It assumes the client +// isn't a jerk. +fn serveHTTPS(address: std.net.Address) !void { + const tls = @import("tls"); + + var listener = try address.listen(.{ .reuse_address = true }); + defer listener.deinit(); + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + test_wg.finish(); + + var seed: u64 = undefined; + std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable; + var r = std.Random.DefaultPrng.init(seed); + const rand = r.random(); + + var read_buffer: [1024]u8 = undefined; + while (true) { + // defer _ = arena.reset(.{ .retain_with_limit = 1024 }); + // const aa = arena.allocator(); + + const stream = blk: { + const conn = try listener.accept(); + break :blk conn.stream; + }; + defer stream.close(); + + var conn = try tls.server(stream, .{ .auth = null }); + defer conn.close() catch {}; + + var pos: usize = 0; + while (true) { + const n = try conn.read(read_buffer[pos..]); + if (n == 0) { + break; + } + pos += n; + const header_end = std.mem.indexOf(u8, read_buffer[0..pos], "\r\n\r\n") orelse { + continue; + }; + var it = std.mem.splitScalar(u8, read_buffer[0..header_end], ' '); + _ = it.next() orelse unreachable; // method + const path = it.next() orelse unreachable; + + var fragment = false; + var response: []const u8 = undefined; + if (std.mem.eql(u8, path, "/http_client/simple")) { + fragment = true; + response = "HTTP/1.1 200 \r\nContent-Length: 0\r\n\r\n"; + } else if (std.mem.eql(u8, path, "/http_client/body")) { + fragment = true; + response = "HTTP/1.1 201 CREATED\r\nContent-Length: 20\r\n Another : HEaDer \r\n\r\n1234567890abcdefhijk"; + } else if (std.mem.eql(u8, path, "/http_client/redirect/insecure")) { + fragment = true; + response = "HTTP/1.1 307 GOTO\r\nLocation: http://127.0.0.1:9582/http_client/redirect\r\n\r\n"; + } else if (std.mem.eql(u8, path, "/xhr")) { + response = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 100\r\n\r\n" ++ ("1234567890" ** 10); + } else if (std.mem.eql(u8, path, "/xhr/json")) { + response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 18\r\n\r\n{\"over\":\"9000!!!\"}"; + } else { + // should not have an unknown path + unreachable; + } + + var unsent = response; + while (unsent.len > 0) { + const to_send = if (fragment) rand.intRangeAtMost(usize, 1, unsent.len) else unsent.len; + const sent = try conn.write(unsent[0..to_send]); + unsent = unsent[sent..]; + std.time.sleep(std.time.ns_per_us * 5); + } + break; + } + } +} + +fn serveCDP(address: std.net.Address) !void { + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; + var app = try App.init(gpa.allocator(), .{ + .run_mode = .serve, + .tls_verify_host = false, + }); + defer app.deinit(); + + test_wg.finish(); + server.run(app, address, std.time.ns_per_s * 2) catch |err| { + std.debug.print("CDP server error: {}", .{err}); + return err; + }; +} diff --git a/src/main_tests.zig b/src/main_tests.zig deleted file mode 100644 index 035e9d8f..00000000 --- a/src/main_tests.zig +++ /dev/null @@ -1,420 +0,0 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -const std = @import("std"); -const builtin = @import("builtin"); - -const jsruntime = @import("jsruntime"); -const generate = @import("generate.zig"); -const pretty = @import("pretty"); - -const parser = @import("netsurf"); -const apiweb = @import("apiweb.zig"); -const browser = @import("browser/browser.zig"); -const Window = @import("html/window.zig").Window; -const xhr = @import("xhr/xhr.zig"); -const storage = @import("storage/storage.zig"); -const URL = @import("url.zig").URL; - -const documentTestExecFn = @import("dom/document.zig").testExecFn; -const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn; -const nodeTestExecFn = @import("dom/node.zig").testExecFn; -const characterDataTestExecFn = @import("dom/character_data.zig").testExecFn; -const textTestExecFn = @import("dom/text.zig").testExecFn; -const elementTestExecFn = @import("dom/element.zig").testExecFn; -const HTMLCollectionTestExecFn = @import("dom/html_collection.zig").testExecFn; -const DOMExceptionTestExecFn = @import("dom/exceptions.zig").testExecFn; -const DOMImplementationExecFn = @import("dom/implementation.zig").testExecFn; -const NamedNodeMapExecFn = @import("dom/namednodemap.zig").testExecFn; -const DOMTokenListExecFn = @import("dom/token_list.zig").testExecFn; -const NodeListTestExecFn = @import("dom/nodelist.zig").testExecFn; -const AttrTestExecFn = @import("dom/attribute.zig").testExecFn; -const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn; -const ProcessingInstructionTestExecFn = @import("dom/processing_instruction.zig").testExecFn; -const CommentTestExecFn = @import("dom/comment.zig").testExecFn; -const DocumentFragmentTestExecFn = @import("dom/document_fragment.zig").testExecFn; -const EventTestExecFn = @import("events/event.zig").testExecFn; -const XHRTestExecFn = xhr.testExecFn; -const ProgressEventTestExecFn = @import("xhr/progress_event.zig").testExecFn; -const StorageTestExecFn = storage.testExecFn; -const URLTestExecFn = @import("url/url.zig").testExecFn; -const HTMLElementTestExecFn = @import("html/elements.zig").testExecFn; -const MutationObserverTestExecFn = @import("dom/mutation_observer.zig").testExecFn; - -pub const Types = jsruntime.reflect(apiweb.Interfaces); -pub const UserContext = @import("user_context.zig").UserContext; -pub const IO = @import("asyncio").Wrapper(jsruntime.Loop); - -var doc: *parser.DocumentHTML = undefined; - -fn testExecFn( - alloc: std.mem.Allocator, - js_env: *jsruntime.Env, - comptime execFn: jsruntime.ContextExecFn, -) anyerror!void { - try parser.init(); - defer parser.deinit(); - - // start JS env - try js_env.start(); - defer js_env.stop(); - - var storageShelf = storage.Shelf.init(alloc); - defer storageShelf.deinit(); - - // document - const file = try std.fs.cwd().openFile("test.html", .{}); - defer file.close(); - - doc = try parser.documentHTMLParse(file.reader(), "UTF-8"); - defer parser.documentHTMLClose(doc) catch |err| { - std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)}); - }; - - var http_client = try @import("http/client.zig").Client.init(alloc, 5, .{}); - defer http_client.deinit(); - - // alias global as self and window - var window = Window.create(null, null); - - const url = try URL.parse("https://lightpanda.io/opensource-browser/", null); - - var cookie_jar = storage.CookieJar.init(alloc); - defer cookie_jar.deinit(); - - var renderer = browser.Renderer.init(alloc); - defer renderer.elements.deinit(alloc); - defer renderer.positions.deinit(alloc); - - try js_env.setUserContext(.{ - .url = &url, - .document = doc, - .renderer = &renderer, - .cookie_jar = &cookie_jar, - .http_client = &http_client, - }); - - try window.replaceLocation(.{ .url = try url.toWebApi(alloc) }); - - try window.replaceDocument(doc); - window.setStorageShelf(&storageShelf); - - try js_env.bindGlobal(window); - - // run test - try execFn(alloc, js_env); -} - -fn testsAllExecFn( - alloc: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - const testFns = [_]jsruntime.ContextExecFn{ - documentTestExecFn, - HTMLDocumentTestExecFn, - nodeTestExecFn, - characterDataTestExecFn, - textTestExecFn, - elementTestExecFn, - HTMLCollectionTestExecFn, - DOMExceptionTestExecFn, - DOMImplementationExecFn, - NamedNodeMapExecFn, - DOMTokenListExecFn, - NodeListTestExecFn, - AttrTestExecFn, - CommentTestExecFn, - DocumentFragmentTestExecFn, - EventTargetTestExecFn, - EventTestExecFn, - XHRTestExecFn, - ProgressEventTestExecFn, - ProcessingInstructionTestExecFn, - StorageTestExecFn, - URLTestExecFn, - HTMLElementTestExecFn, - MutationObserverTestExecFn, - @import("polyfill/fetch.zig").testExecFn, - @import("html/navigator.zig").testExecFn, - @import("html/history.zig").testExecFn, - @import("html/location.zig").testExecFn, - @import("xmlserializer/xmlserializer.zig").testExecFn, - }; - - inline for (testFns) |testFn| { - try testExecFn(alloc, js_env, testFn); - } -} - -const usage = - \\usage: test [options] - \\ Run the tests. By default the command will run both js and unit tests. - \\ - \\ -h, --help Print this help message and exit. - \\ --browser run only browser js tests - \\ --unit run only js unit tests - \\ --json bench result is formatted in JSON. - \\ only browser tests are benchmarked. - \\ -; - -// Out list all the ouputs handled by benchmark result and written on stdout. -const Out = enum { - text, - json, -}; - -// Which tests must be run. -const Run = enum { - all, - browser, - unit, -}; - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); - const gpa_alloc = gpa.allocator(); - - var args = try std.process.argsWithAllocator(gpa_alloc); - defer args.deinit(); - - // ignore the exec name. - _ = args.next().?; - - var out: Out = .text; - var run: Run = .all; - - while (args.next()) |arg| { - if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) { - try std.io.getStdErr().writer().print(usage, .{}); - std.posix.exit(0); - } - if (std.mem.eql(u8, "--json", arg)) { - out = .json; - continue; - } - if (std.mem.eql(u8, "--browser", arg)) { - run = .browser; - continue; - } - if (std.mem.eql(u8, "--unit", arg)) { - run = .unit; - continue; - } - } - - // run js tests - if (run == .all or run == .browser) try run_js(out); - - // run standard unit tests. - if (run == .all or run == .unit) { - std.debug.print("\n", .{}); - for (builtin.test_functions) |test_fn| { - if (std.mem.startsWith(u8, test_fn.name, "http.client.test")) { - // covered by unit test, needs a dummy server started, which - // main_test doesn't do. - continue; - } - - try parser.init(); - defer parser.deinit(); - - std.testing.allocator_instance = .{}; - try test_fn.func(); - - if (std.testing.allocator_instance.deinit() == .leak) { - std.debug.print("======Memory Leak: {s}======\n", .{test_fn.name}); - } else { - std.debug.print("{s}\tOK\n", .{test_fn.name}); - } - } - } -} - -// Run js test and display the output depending of the output parameter. -fn run_js(out: Out) !void { - var bench_alloc = jsruntime.bench_allocator(std.testing.allocator); - - const start = try std.time.Instant.now(); - - // run js exectuion tests - try testJSRuntime(bench_alloc.allocator()); - - const duration = std.time.Instant.since(try std.time.Instant.now(), start); - const stats = bench_alloc.stats(); - - // get and display the results - if (out == .json) { - const res = [_]struct { - name: []const u8, - bench: struct { - duration: u64, - - alloc_nb: usize, - realloc_nb: usize, - alloc_size: usize, - }, - }{ - .{ .name = "browser", .bench = .{ - .duration = duration, - .alloc_nb = stats.alloc_nb, - .realloc_nb = stats.realloc_nb, - .alloc_size = stats.alloc_size, - } }, - // TODO get libdom bench info. - .{ .name = "libdom", .bench = .{ - .duration = duration, - .alloc_nb = 0, - .realloc_nb = 0, - .alloc_size = 0, - } }, - // TODO get v8 bench info. - .{ .name = "v8", .bench = .{ - .duration = duration, - .alloc_nb = 0, - .realloc_nb = 0, - .alloc_size = 0, - } }, - // TODO get main bench info. - .{ .name = "main", .bench = .{ - .duration = duration, - .alloc_nb = 0, - .realloc_nb = 0, - .alloc_size = 0, - } }, - }; - - try std.json.stringify(res, .{ .whitespace = .indent_2 }, std.io.getStdOut().writer()); - return; - } - - // display console result by default - const dur = pretty.Measure{ .unit = "ms", .value = duration / ms }; - const size = pretty.Measure{ .unit = "kb", .value = stats.alloc_size / kb }; - - const zerosize = pretty.Measure{ .unit = "kb", .value = 0 }; - - // benchmark table - const row_shape = .{ []const u8, pretty.Measure, u64, u64, pretty.Measure }; - const table = try pretty.GenerateTable(4, row_shape, pretty.TableConf{ .margin_left = " " }); - const header = .{ "FUNCTION", "DURATION", "ALLOCATIONS (nb)", "RE-ALLOCATIONS (nb)", "HEAP SIZE" }; - var t = table.init("Benchmark lightpanda πŸš€", header); - try t.addRow(.{ "browser", dur, stats.alloc_nb, stats.realloc_nb, size }); - try t.addRow(.{ "libdom", dur, 0, 0, zerosize }); // TODO get libdom bench info. - try t.addRow(.{ "v8", dur, 0, 0, zerosize }); // TODO get v8 bench info. - try t.addRow(.{ "main", dur, 0, 0, zerosize }); // TODO get main bench info. - try t.render(std.io.getStdOut().writer()); -} - -const kb = 1024; -const ms = std.time.ns_per_ms; - -test { - const dumpTest = @import("browser/dump.zig"); - std.testing.refAllDecls(dumpTest); - - const mimeTest = @import("browser/mime.zig"); - std.testing.refAllDecls(mimeTest); - - const cssTest = @import("css/css.zig"); - std.testing.refAllDecls(cssTest); - - const cssParserTest = @import("css/parser.zig"); - std.testing.refAllDecls(cssParserTest); - - const cssMatchTest = @import("css/match_test.zig"); - std.testing.refAllDecls(cssMatchTest); - - const cssLibdomTest = @import("css/libdom_test.zig"); - std.testing.refAllDecls(cssLibdomTest); - - const queryTest = @import("url/query.zig"); - std.testing.refAllDecls(queryTest); - - std.testing.refAllDecls(@import("generate.zig")); -} - -fn testJSRuntime(alloc: std.mem.Allocator) !void { - // create JS vm - const vm = jsruntime.VM.init(); - defer vm.deinit(); - - var arena_alloc = std.heap.ArenaAllocator.init(alloc); - defer arena_alloc.deinit(); - - try jsruntime.loadEnv(&arena_alloc, null, testsAllExecFn); -} - -test "DocumentHTMLParseFromStr" { - const file = try std.fs.cwd().openFile("test.html", .{}); - defer file.close(); - - const str = try file.readToEndAlloc(std.testing.allocator, std.math.maxInt(u32)); - defer std.testing.allocator.free(str); - - doc = try parser.documentHTMLParseFromStr(str); - parser.documentHTMLClose(doc) catch {}; -} - -// https://github.com/lightpanda-io/libdom/issues/4 -test "bug document html parsing #4" { - const file = try std.fs.cwd().openFile("tests/html/bug-html-parsing-4.html", .{}); - defer file.close(); - - doc = try parser.documentHTMLParse(file.reader(), "UTF-8"); - parser.documentHTMLClose(doc) catch {}; -} - -test "Window is a libdom event target" { - var window = Window.create(null, null); - - const event = try parser.eventCreate(); - try parser.eventInit(event, "foo", .{}); - - const et = parser.toEventTarget(Window, &window); - _ = try parser.eventTargetDispatchEvent(et, event); -} - -test "DocumentHTML is a libdom event target" { - doc = try parser.documentHTMLParseFromStr(""); - parser.documentHTMLClose(doc) catch {}; - - const event = try parser.eventCreate(); - try parser.eventInit(event, "foo", .{}); - - const et = parser.toEventTarget(parser.DocumentHTML, doc); - _ = try parser.eventTargetDispatchEvent(et, event); -} - -test "XMLHttpRequest.validMethod" { - // valid methods - for ([_][]const u8{ "get", "GET", "head", "HEAD" }) |tc| { - _ = try xhr.XMLHttpRequest.validMethod(tc); - } - - // forbidden - for ([_][]const u8{ "connect", "CONNECT" }) |tc| { - try std.testing.expectError(parser.DOMError.Security, xhr.XMLHttpRequest.validMethod(tc)); - } - - // syntax - for ([_][]const u8{ "foo", "BAR" }) |tc| { - try std.testing.expectError(parser.DOMError.Syntax, xhr.XMLHttpRequest.validMethod(tc)); - } -} diff --git a/src/main_unit_tests.zig b/src/main_unit_tests.zig deleted file mode 100644 index 23c2f9c6..00000000 --- a/src/main_unit_tests.zig +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -const std = @import("std"); -const tls = @import("tls"); -const parser = @import("netsurf"); -const builtin = @import("builtin"); - -const Allocator = std.mem.Allocator; - -test { - std.testing.refAllDecls(@import("url/query.zig")); - std.testing.refAllDecls(@import("browser/dump.zig")); - std.testing.refAllDecls(@import("browser/mime.zig")); - std.testing.refAllDecls(@import("css/css.zig")); - std.testing.refAllDecls(@import("css/libdom_test.zig")); - std.testing.refAllDecls(@import("css/match_test.zig")); - std.testing.refAllDecls(@import("css/parser.zig")); - std.testing.refAllDecls(@import("generate.zig")); - std.testing.refAllDecls(@import("storage/storage.zig")); - std.testing.refAllDecls(@import("storage/cookie.zig")); - std.testing.refAllDecls(@import("iterator/iterator.zig")); - std.testing.refAllDecls(@import("server.zig")); - std.testing.refAllDecls(@import("cdp/cdp.zig")); - std.testing.refAllDecls(@import("log.zig")); - std.testing.refAllDecls(@import("datetime.zig")); - std.testing.refAllDecls(@import("telemetry/telemetry.zig")); - std.testing.refAllDecls(@import("http/client.zig")); -} - -var wg: std.Thread.WaitGroup = .{}; -var gpa = std.heap.GeneralPurposeAllocator(.{}){}; -test "tests:beforeAll" { - try parser.init(); - wg.startMany(3); - - { - const address = try std.net.Address.parseIp("127.0.0.1", 9582); - const thread = try std.Thread.spawn(.{}, serveHTTP, .{address}); - thread.detach(); - } - - { - const address = try std.net.Address.parseIp("127.0.0.1", 9581); - const thread = try std.Thread.spawn(.{}, serveHTTPS, .{address}); - thread.detach(); - } - - { - const address = try std.net.Address.parseIp("127.0.0.1", 9583); - const thread = try std.Thread.spawn(.{}, serveCDP, .{address}); - thread.detach(); - } - - // need to wait for the servers to be listening, else tests will fail because - // they aren't able to connect. - wg.wait(); -} - -test "tests:afterAll" { - parser.deinit(); -} - -fn serveHTTP(address: std.net.Address) !void { - const allocator = gpa.allocator(); - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - - var listener = try address.listen(.{ .reuse_address = true }); - defer listener.deinit(); - - wg.finish(); - - var read_buffer: [1024]u8 = undefined; - ACCEPT: while (true) { - defer _ = arena.reset(.{ .retain_with_limit = 1024 }); - const aa = arena.allocator(); - - var conn = try listener.accept(); - defer conn.stream.close(); - var server = std.http.Server.init(conn, &read_buffer); - - while (server.state == .ready) { - var request = server.receiveHead() catch |err| switch (err) { - error.HttpConnectionClosing => continue :ACCEPT, - else => { - std.debug.print("Test HTTP Server error: {}\n", .{err}); - return err; - }, - }; - - const path = request.head.target; - if (std.mem.eql(u8, path, "/loader")) { - try request.respond("Hello!", .{}); - } else if (std.mem.eql(u8, path, "/http_client/simple")) { - try request.respond("", .{}); - } else if (std.mem.eql(u8, path, "/http_client/redirect")) { - try request.respond("", .{ - .status = .moved_permanently, - .extra_headers = &.{.{ .name = "LOCATION", .value = "../http_client/echo" }}, - }); - } else if (std.mem.eql(u8, path, "/http_client/redirect/secure")) { - try request.respond("", .{ - .status = .moved_permanently, - .extra_headers = &.{.{ .name = "LOCATION", .value = "https://127.0.0.1:9581/http_client/body" }}, - }); - } else if (std.mem.eql(u8, path, "/http_client/echo")) { - var headers: std.ArrayListUnmanaged(std.http.Header) = .{}; - - var it = request.iterateHeaders(); - while (it.next()) |hdr| { - try headers.append(aa, .{ - .name = try std.fmt.allocPrint(aa, "_{s}", .{hdr.name}), - .value = hdr.value, - }); - } - - try request.respond("over 9000!", .{ - .status = .created, - .extra_headers = headers.items, - }); - } - } - } -} - -// This is a lot of work for testing TLS, but the TLS (async) code is complicated -// This "server" is written specifically to test the client. It assumes the client -// isn't a jerk. -fn serveHTTPS(address: std.net.Address) !void { - const allocator = gpa.allocator(); - - var listener = try address.listen(.{ .reuse_address = true }); - defer listener.deinit(); - - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - - wg.finish(); - - var seed: u64 = undefined; - std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable; - var r = std.Random.DefaultPrng.init(seed); - const rand = r.random(); - - var read_buffer: [1024]u8 = undefined; - while (true) { - // defer _ = arena.reset(.{ .retain_with_limit = 1024 }); - // const aa = arena.allocator(); - - const stream = blk: { - const conn = try listener.accept(); - break :blk conn.stream; - }; - defer stream.close(); - - var conn = try tls.server(stream, .{ .auth = null }); - defer conn.close() catch {}; - - var pos: usize = 0; - while (true) { - const n = try conn.read(read_buffer[pos..]); - if (n == 0) { - break; - } - pos += n; - const header_end = std.mem.indexOf(u8, read_buffer[0..pos], "\r\n\r\n") orelse { - continue; - }; - var it = std.mem.splitScalar(u8, read_buffer[0..header_end], ' '); - _ = it.next() orelse unreachable; // method - const path = it.next() orelse unreachable; - - var response: []const u8 = undefined; - if (std.mem.eql(u8, path, "/http_client/simple")) { - response = "HTTP/1.1 200 \r\nContent-Length: 0\r\n\r\n"; - } else if (std.mem.eql(u8, path, "/http_client/body")) { - response = "HTTP/1.1 201 CREATED\r\nContent-Length: 20\r\n Another : HEaDer \r\n\r\n1234567890abcdefhijk"; - } else if (std.mem.eql(u8, path, "/http_client/redirect/insecure")) { - response = "HTTP/1.1 307 GOTO\r\nLocation: http://127.0.0.1:9582/http_client/redirect\r\n\r\n"; - } else { - // should not have an unknown path - unreachable; - } - - var unsent = response; - while (unsent.len > 0) { - const to_send = rand.intRangeAtMost(usize, 1, unsent.len); - const sent = try conn.write(unsent[0..to_send]); - unsent = unsent[sent..]; - std.time.sleep(std.time.ns_per_us * 5); - } - break; - } - } -} - -fn serveCDP(address: std.net.Address) !void { - const App = @import("app.zig").App; - var app = try App.init(gpa.allocator(), .{ .run_mode = .serve }); - defer app.deinit(); - - const server = @import("server.zig"); - wg.finish(); - server.run(app, address, std.time.ns_per_s * 2) catch |err| { - std.debug.print("CDP server error: {}", .{err}); - return err; - }; -} diff --git a/src/main_wpt.zig b/src/main_wpt.zig index 7cf2f077..eeabaf12 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -18,14 +18,10 @@ const std = @import("std"); -const jsruntime = @import("jsruntime"); - -const Suite = @import("wpt/testcase.zig").Suite; -const FileLoader = @import("wpt/fileloader.zig").FileLoader; const wpt = @import("wpt/run.zig"); - -const apiweb = @import("apiweb.zig"); -const HTMLElem = @import("html/elements.zig"); +const Suite = @import("wpt/testcase.zig").Suite; +const Platform = @import("runtime/js.zig").Platform; +const FileLoader = @import("wpt/fileloader.zig").FileLoader; const wpt_dir = "tests/wpt"; @@ -47,10 +43,10 @@ const Out = enum { text, }; -pub const Types = jsruntime.reflect(apiweb.Interfaces); -pub const GlobalType = apiweb.GlobalType; -pub const UserContext = apiweb.UserContext; -pub const IO = @import("asyncio").Wrapper(jsruntime.Loop); +pub const std_options = std.Options{ + // Set the log level to info + .log_level = .info, +}; // TODO For now the WPT tests run is specific to WPT. // It manually load js framwork libs, and run the first script w/ js content in @@ -122,8 +118,8 @@ pub fn main() !void { } // initialize VM JS lib. - const vm = jsruntime.VM.init(); - defer vm.deinit(); + const platform = Platform.init(); + defer platform.deinit(); // prepare libraries to load on each test case. var loader = FileLoader.init(alloc, wpt_dir); @@ -142,8 +138,9 @@ pub fn main() !void { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); - const res = wpt.run(&arena, wpt_dir, tc, &loader) catch |err| { - const suite = try Suite.init(alloc, tc, false, @errorName(err)); + var msg_out: ?[]const u8 = null; + const res = wpt.run(arena.allocator(), wpt_dir, tc, &loader, &msg_out) catch |err| { + const suite = try Suite.init(alloc, tc, false, if (msg_out) |msg| msg else @errorName(err)); try results.append(suite); if (out == .text) { @@ -152,9 +149,8 @@ pub fn main() !void { failures += 1; continue; }; - defer res.deinit(arena.allocator()); - const suite = try Suite.init(alloc, tc, res.ok, res.msg orelse ""); + const suite = try Suite.init(alloc, tc, true, res); try results.append(suite); if (out == .json) { diff --git a/src/polyfill/fetch.zig b/src/polyfill/fetch.zig deleted file mode 100644 index 5cf8b99d..00000000 --- a/src/polyfill/fetch.zig +++ /dev/null @@ -1,55 +0,0 @@ -const std = @import("std"); -const jsruntime = @import("jsruntime"); -const Case = jsruntime.test_utils.Case; -const checkCases = jsruntime.test_utils.checkCases; - -// fetch.js code comes from -// https://github.com/JakeChampion/fetch/blob/main/fetch.js -// -// The original code source is available in MIT license. -// -// The script comes from the built version from npm. -// You can get the package with the command: -// -// wget $(npm view whatwg-fetch dist.tarball) -// -// The source is the content of `package/dist/fetch.umd.js` file. -pub const source = @embedFile("fetch.js"); - -pub fn testExecFn( - alloc: std.mem.Allocator, - js_env: *jsruntime.Env, -) anyerror!void { - try @import("polyfill.zig").load(alloc, js_env); - - var fetch = [_]Case{ - .{ - .src = - \\var ok = false; - \\const request = new Request("https://httpbin.io/json"); - \\fetch(request) - \\ .then((response) => { ok = response.ok; }); - \\false; - , - .ex = "false", - }, - // all events have been resolved. - .{ .src = "ok", .ex = "true" }, - }; - try checkCases(js_env, &fetch); - - var fetch2 = [_]Case{ - .{ - .src = - \\var ok2 = false; - \\const request2 = new Request("https://httpbin.io/json"); - \\(async function () { resp = await fetch(request2); ok2 = resp.ok; }()); - \\false; - , - .ex = "false", - }, - // all events have been resolved. - .{ .src = "ok2", .ex = "true" }, - }; - try checkCases(js_env, &fetch2); -} diff --git a/src/generate.zig b/src/runtime/generate.zig similarity index 96% rename from src/generate.zig rename to src/runtime/generate.zig index 9b5c164e..1e1c5193 100644 --- a/src/generate.zig +++ b/src/runtime/generate.zig @@ -25,7 +25,7 @@ const Type = std.builtin.Type; // ----- // Generate a flatten tagged Union from a Tuple -pub fn Union(interfaces: anytype) type { +pub fn Union(comptime interfaces: anytype) type { // @setEvalBranchQuota(10000); const tuple = Tuple(interfaces){}; const fields = std.meta.fields(@TypeOf(tuple)); @@ -93,7 +93,7 @@ pub fn Union(interfaces: anytype) type { // Flattens and depuplicates a list of nested tuples. For example // input: {A, B, {C, B, D}, {A, E}} // output {A, B, C, D, E} -pub fn Tuple(args: anytype) type { +pub fn Tuple(comptime args: anytype) type { @setEvalBranchQuota(100000); const count = countInterfaces(args, 0); @@ -188,7 +188,7 @@ test "generate.Union" { const value = Union(.{ Astruct, Bstruct, .{Cstruct} }); const ti = @typeInfo(value).@"union"; try std.testing.expectEqual(3, ti.fields.len); - try std.testing.expectEqualStrings("*generate.test.generate.Union.Astruct.Other", @typeName(ti.fields[0].type)); + try std.testing.expectEqualStrings("*runtime.generate.test.generate.Union.Astruct.Other", @typeName(ti.fields[0].type)); try std.testing.expectEqualStrings(ti.fields[0].name, "Astruct"); try std.testing.expectEqual(Bstruct, ti.fields[1].type); try std.testing.expectEqualStrings(ti.fields[1].name, "Bstruct"); diff --git a/src/runtime/js.zig b/src/runtime/js.zig new file mode 100644 index 00000000..329a8728 --- /dev/null +++ b/src/runtime/js.zig @@ -0,0 +1,2303 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const builtin = @import("builtin"); +const v8 = @import("v8"); + +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; + +const log = std.log.scoped(.js); + +// Global, should only be initialized once. +pub const Platform = struct { + inner: v8.Platform, + + pub fn init() Platform { + const platform = v8.Platform.initDefault(0, true); + v8.initV8Platform(platform); + v8.initV8(); + return .{ .inner = platform }; + } + + pub fn deinit(self: Platform) void { + _ = v8.deinitV8(); + v8.deinitV8Platform(); + self.inner.deinit(); + } +}; + +// The Env maps to a V8 isolate, which represents a isolated sandbox for +// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings, +// and it's where we'll start Executors, which actually execute JavaScript. +// The `S` parameter is arbitrary state. When we start an Executor, an instance +// of S must be given. This instance is available to any Zig binding. +// The `types` parameter is a tuple of Zig structures we want to bind to V8. +pub fn Env(comptime S: type, comptime types: anytype) type { + const Types = @typeInfo(@TypeOf(types)).@"struct".fields; + + // Imagine we have a type Cat which has a getter: + // + // fn get_owner(self: *Cat) *Owner { + // return self.owner; + // } + // + // When we execute caller.getter, we'll end up doing something like: + // const res = @call(.auto, Cat.get_owner, .{cat_instance}); + // + // How do we turn `res`, which is an *Owner, into something we can return + // to v8? We need the ObjectTemplate associated with Owner. How do we + // get that? Well, we store all the ObjectTemplates in an array that's + // tied to env. So we do something like: + // + // env.templates[index_of_owner].initInstance(...); + // + // But how do we get that `index_of_owner`? `TypeLookup` is a struct + // that looks like: + // + // const TypeLookup = struct { + // comptime cat: usize = 0, + // comptime owner: usize = 1, + // ... + // } + // + // So to get the template index of `owner`, we can do: + // + // const index_id = @field(type_lookup, @typeName(@TypeOf(res)); + // + const TypeLookup = comptime blk: { + var fields: [Types.len]std.builtin.Type.StructField = undefined; + for (Types, 0..) |s, i| { + + // This prototype type check has nothing to do with building our + // TypeLookup. But we put it here, early, so that the rest of the + // code doesn't have to worry about checking if Struct.prototype is + // a pointer. + const Struct = @field(types, s.name); + if (@hasDecl(Struct, "prototype") and @typeInfo(Struct.prototype) != .pointer) { + @compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s} must be a pointer", .{ @typeName(Struct.prototype), @typeName(Struct) })); + } + + const R = Receiver(@field(types, s.name)); + fields[i] = .{ + .name = @typeName(R), + .type = usize, + .is_comptime = true, + .alignment = @alignOf(usize), + .default_value_ptr = @ptrCast(&i), + }; + } + break :blk @Type(.{ .@"struct" = .{ + .layout = .auto, + .decls = &.{}, + .is_tuple = false, + .fields = &fields, + } }); + }; + + // Creates a list where the index of a type contains its prototype index + // const Animal = struct{}; + // const Cat = struct{ + // pub const prototype = *Animal; + // }; + // + // Would create an array: [0, 0] + // Animal, at index, 0, has no prototype, so we set it to itself + // Cat, at index 1, has an Animal prototype, so we set it to 0. + // + // When we're trying to pass an argument to a Zig function, we'll know the + // target type (the function parameter type), and we'll have a + // TaggedAnyOpaque which will have the index of the type of that parameter. + // We'll use the PROTOTYPE_TABLE to see if the TaggedAnyType should be + // cast to a prototype. + const PROTOTYPE_TABLE = comptime blk: { + var table: [Types.len]u16 = undefined; + const TYPE_LOOKUP = TypeLookup{}; + for (Types, 0..) |s, i| { + var prototype_index = i; + const Struct = @field(types, s.name); + if (@hasDecl(Struct, "prototype")) { + const TI = @typeInfo(Struct.prototype); + const proto_name = @typeName(Receiver(TI.pointer.child)); + prototype_index = @field(TYPE_LOOKUP, proto_name); + } + table[i] = prototype_index; + } + break :blk table; + }; + + return struct { + allocator: Allocator, + + // the global isolate + isolate: v8.Isolate, + + // this is the global scope that all our classes are defined in + global_scope: v8.HandleScope, + + // just kept around because we need to free it on deinit + isolate_params: v8.CreateParams, + + // Given a type, we can lookup its index in TYPE_LOOKUP and then have + // access to its TunctionTemplate (the thing we need to create an instance + // of it) + // I.e.: + // const index = @field(TYPE_LOOKUP, @typeName(type_name)) + // const template = templates[index]; + templates: [Types.len]v8.FunctionTemplate, + + // Given a type index (retrieved via the TYPE_LOOKUP), we can retrieve + // the index of its prototype. Types without a prototype have their own + // index. + prototype_lookup: [Types.len]u16, + + // Sessions are cheap, we mostly do this so we can get a stable pointer + executor_pool: std.heap.MemoryPool(Executor), + + // Send a lowMemoryNotification whenever we stop an executor + gc_hints: bool, + + const Self = @This(); + + const State = S; + const TYPE_LOOKUP = TypeLookup{}; + + const Opts = struct { + gc_hints: bool = false, + }; + + pub fn init(allocator: Allocator, opts: Opts) !*Self { + var params = v8.initCreateParams(); + params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator(); + errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?); + + var isolate = v8.Isolate.init(¶ms); + errdefer isolate.deinit(); + + isolate.enter(); + errdefer isolate.exit(); + + var global_scope: v8.HandleScope = undefined; + v8.HandleScope.init(&global_scope, isolate); + errdefer global_scope.deinit(); + + const env = try allocator.create(Self); + errdefer allocator.destroy(env); + + env.* = .{ + .isolate = isolate, + .templates = undefined, + .allocator = allocator, + .isolate_params = params, + .gc_hints = opts.gc_hints, + .global_scope = global_scope, + .prototype_lookup = undefined, + .executor_pool = std.heap.MemoryPool(Executor).init(allocator), + }; + + // Populate our templates lookup. generateClass creates the + // v8.FunctionTemplate, which we store in our env.templates. + // The ordering doesn't matter. What matters is that, given a type + // we can get its index via: @field(TYPE_LOOKUP, type_name) + const templates = &env.templates; + inline for (Types, 0..) |s, i| { + templates[i] = env.generateClass(@field(types, s.name)); + } + + // Above, we've created all our our FunctionTemplates. Now that we + // have them all, we can hook up the prototypes. + inline for (Types, 0..) |s, i| { + const Struct = @field(types, s.name); + if (@hasDecl(Struct, "prototype")) { + const TI = @typeInfo(Struct.prototype); + const proto_name = @typeName(Receiver(TI.pointer.child)); + if (@hasField(TypeLookup, proto_name) == false) { + @compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ proto_name, @typeName(Struct) })); + } + // Hey, look! This is our first real usage of the TYPE_LOOKUP. + // Just like we said above, given a type, we can get its + // template index. + + const proto_index = @field(TYPE_LOOKUP, proto_name); + templates[i].inherit(templates[proto_index]); + } + } + + return env; + } + + pub fn deinit(self: *Self) void { + self.global_scope.deinit(); + self.isolate.exit(); + self.isolate.deinit(); + self.executor_pool.deinit(); + v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?); + self.allocator.destroy(self); + } + + pub fn runMicrotasks(self: *const Self) void { + self.isolate.performMicrotasksCheckpoint(); + } + + pub fn startExecutor(self: *Self, comptime Global: type, state: State, module_loader: anytype) !*Executor { + const isolate = self.isolate; + const templates = &self.templates; + + // Acts like an Arena. Most things V8 has to allocate from this point + // on will be tied to this handle_scope - which we deinit in + // stopExecutor + var handle_scope: v8.HandleScope = undefined; + v8.HandleScope.init(&handle_scope, isolate); + + // The global FunctionTemplate (i.e. Window). + const globals = v8.FunctionTemplate.initDefault(isolate); + + const global_template = globals.getInstanceTemplate(); + global_template.setInternalFieldCount(1); + self.attachClass(Global, globals); + + // All the FunctionTemplates that we created and setup in Env.init + // are now going to get associated with our global instance. + inline for (Types, 0..) |s, i| { + const Struct = @field(types, s.name); + const class_name = v8.String.initUtf8(isolate, comptime classNameForStruct(Struct)); + global_template.set(class_name.toName(), templates[i], v8.PropertyAttribute.None); + } + + // The global object (Window) has already been hooked into the v8 + // engine when the Env was initialized - like every other type. But + // But the V8 global is its own FunctionTemplate instance so even + // though it's also a Window, we need to set the prototype for this + // specific instance of the the Window. + if (@hasDecl(Global, "prototype")) { + const proto_type = Receiver(@typeInfo(Global.prototype).pointer.child); + const proto_name = @typeName(proto_type); + const proto_index = @field(TYPE_LOOKUP, proto_name); + globals.inherit(templates[proto_index]); + } + + const context = v8.Context.init(isolate, global_template, null); + context.enter(); + errdefer context.exit(); + + // This shouldn't be necessary, but it is: + // https://groups.google.com/g/v8-users/c/qAQQBmbi--8 + // TODO: see if newer V8 engines have a way around this. + inline for (Types, 0..) |s, i| { + const Struct = @field(types, s.name); + + if (@hasDecl(Struct, "prototype")) { + const proto_type = Receiver(@typeInfo(Struct.prototype).pointer.child); + const proto_name = @typeName(proto_type); + if (@hasField(TypeLookup, proto_name) == false) { + @compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name); + } + + const proto_index = @field(TYPE_LOOKUP, proto_name); + const proto_obj = templates[proto_index].getFunction(context).toObject(); + + const self_obj = templates[i].getFunction(context).toObject(); + _ = self_obj.setPrototype(context, proto_obj); + } + } + + const executor = try self.executor_pool.create(); + errdefer self.executor_pool.destroy(executor); + + { + // Given a context, we can get our executor. + // (we store a pointer to our executor in the context's + // embeddeder data) + const data = isolate.initBigIntU64(@intCast(@intFromPtr(executor))); + context.setEmbedderData(1, data); + } + + const allocator = self.allocator; + + executor.* = .{ + .state = state, + .context = context, + .isolate = isolate, + .templates = templates, + .handle_scope = handle_scope, + .call_arena = ArenaAllocator.init(allocator), + .scope_arena = ArenaAllocator.init(allocator), + .module_loader = .{ + .ptr = @ptrCast(module_loader), + .func = @TypeOf(module_loader.*).fetchModuleSource, + }, + }; + + errdefer self.stopExecutor(executor); + + // Custom exception + // NOTE: there is no way in v8 to subclass the Error built-in type + // TODO: this is an horrible hack + inline for (Types) |s| { + const Struct = @field(types, s.name); + if (@hasDecl(Struct, "ErrorSet")) { + const script = comptime classNameForStruct(Struct) ++ ".prototype.__proto__ = Error.prototype"; + _ = try executor.exec(script, "errorSubclass"); + } + } + + return executor; + } + + // In startExecutor we started a V8.Context. Here, we're going to + // deinit it. But V8 doesn't immediately free memory associated with + // a Context, it's managed by the garbage collector. So, when the + // `gc_hints` option is enabled, we'll use the `lowMemoryNotification` + // call on the isolate to encourage v8 to free the context. + pub fn stopExecutor(self: *Self, executor: *Executor) void { + executor.deinit(); + self.executor_pool.destroy(executor); + if (self.gc_hints) { + self.isolate.lowMemoryNotification(); + } + } + + // Give it a Zig struct, get back a v8.FunctionTemplate. + // The FunctionTemplate is a bit like a struct container - it's where + // we'll attach functions/getters/setters and where we'll "inherit" a + // prototype type (if there is any) + fn generateClass(self: *Self, comptime Struct: type) v8.FunctionTemplate { + const template = self.generateConstructor(Struct); + self.attachClass(Struct, template); + return template; + } + + // Normally this is called from generateClass. Where generateClass creates + // the constructor (hence, the FunctionTemplate), attachClass adds all + // of its functions, getters, setters, ... + // But it's extracted from generateClass because we also have 1 global + // object (i.e. the Window), which gets attached not only to the Window + // constructor/FunctionTemplate as normal, but also through the default + // FunctionTemplate of the isolate (in startExecutor) + fn attachClass(self: *Self, comptime Struct: type, template: v8.FunctionTemplate) void { + const template_proto = template.getPrototypeTemplate(); + inline for (@typeInfo(Struct).@"struct".decls) |declaration| { + const name = declaration.name; + if (comptime name[0] == '_') { + switch (@typeInfo(@TypeOf(@field(Struct, name)))) { + .@"fn" => self.generateMethod(Struct, name, template_proto), + else => self.generateAttribute(Struct, name, template, template_proto), + } + } else if (comptime std.mem.startsWith(u8, name, "get_")) { + self.generateProperty(Struct, name[4..], template_proto); + } + } + + if (@hasDecl(Struct, "get_symbol_toStringTag") == false) { + // If this WAS defined, then we would have created it in generateProperty. + // But if it isn't, we create a default one + const key = v8.Symbol.getToStringTag(self.isolate).toName(); + template_proto.setGetter(key, struct { + fn stringTag(_: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void { + const info = v8.PropertyCallbackInfo.initFromV8(raw_info); + const class_name = v8.String.initUtf8(info.getIsolate(), comptime classNameForStruct(Struct)); + info.getReturnValue().set(class_name); + } + }.stringTag); + } + + self.generateIndexer(Struct, template_proto); + self.generateNamedIndexer(Struct, template_proto); + } + + // Even if a struct doesn't have a `constructor` function, we still + // `generateConstructor`, because this is how we create our + // FunctionTemplate. Such classes exist, but they can't be instantiated + // via `new ClassName()` - but they could, for example, be created in + // Zig and returned from a function call, which is why we need the + // FunctionTemplate. + fn generateConstructor(self: *Self, comptime Struct: type) v8.FunctionTemplate { + const template = v8.FunctionTemplate.initCallback(self.isolate, struct { + fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { + const info = v8.FunctionCallbackInfo.initFromV8(raw_info); + var caller = Caller(Self).init(info); + defer caller.deinit(); + + // See comment above. We generateConstructor on all types + // in order to create the FunctionTemplate, but there might + // not be an actual "constructor" function. So if someone + // does `new ClassName()` where ClassName doesn't have + // a constructor function, we'll return an error. + if (@hasDecl(Struct, "constructor") == false) { + const isolate = caller.isolate; + const js_exception = isolate.throwException(createException(isolate, "illegal constructor")); + info.getReturnValue().set(js_exception); + return; + } + + // Safe to call now, because if Struct.constructor didn't + // exist, the above if block would have returned. + const named_function = NamedFunction(Struct, Struct.constructor, "constructor"){}; + caller.constructor(named_function, info) catch |err| { + caller.handleError(named_function, err, info); + }; + } + }.callback); + + if (comptime isEmpty(Receiver(Struct)) == false) { + // If the struct is empty, we won't store a Zig reference inside + // the JS object, so we don't need to set the internal field count + template.getInstanceTemplate().setInternalFieldCount(1); + } + + const class_name = v8.String.initUtf8(self.isolate, comptime classNameForStruct(Struct)); + template.setClassName(class_name); + return template; + } + + fn generateMethod(self: *Self, comptime Struct: type, comptime name: []const u8, template_proto: v8.ObjectTemplate) void { + var js_name: v8.Name = undefined; + if (comptime std.mem.eql(u8, name, "_symbol_iterator")) { + js_name = v8.Symbol.getIterator(self.isolate).toName(); + } else { + js_name = v8.String.initUtf8(self.isolate, name[1..]).toName(); + } + const function_template = v8.FunctionTemplate.initCallback(self.isolate, struct { + fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { + const info = v8.FunctionCallbackInfo.initFromV8(raw_info); + var caller = Caller(Self).init(info); + defer caller.deinit(); + + const named_function = NamedFunction(Struct, @field(Struct, name), name){}; + caller.method(named_function, info) catch |err| { + caller.handleError(named_function, err, info); + }; + } + }.callback); + template_proto.set(js_name, function_template, v8.PropertyAttribute.None); + } + + fn generateAttribute(self: *Self, comptime Struct: type, comptime name: []const u8, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void { + const zig_value = @field(Struct, name); + const js_value = simpleZigValueToJs(self.isolate, zig_value, true); + + const js_name = v8.String.initUtf8(self.isolate, name[1..]).toName(); + + // apply it both to the type itself + template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete); + + // andto instances of the type + template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete); + } + + fn generateProperty(self: *Self, comptime Struct: type, comptime name: []const u8, template_proto: v8.ObjectTemplate) void { + const getter = @field(Struct, "get_" ++ name); + const param_count = @typeInfo(@TypeOf(getter)).@"fn".params.len; + + var js_name: v8.Name = undefined; + if (comptime std.mem.eql(u8, name, "symbol_toStringTag")) { + if (param_count != 0) { + @compileError(@typeName(Struct) ++ ".get_symbol_toStringTag() cannot take any parameters"); + } + js_name = v8.Symbol.getToStringTag(self.isolate).toName(); + } else { + js_name = v8.String.initUtf8(self.isolate, name).toName(); + } + + const getter_callback = struct { + fn callback(_: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void { + const info = v8.PropertyCallbackInfo.initFromV8(raw_info); + var caller = Caller(Self).init(info); + defer caller.deinit(); + + const named_function = NamedFunction(Struct, getter, "get_" ++ name){}; + caller.getter(named_function, info) catch |err| { + caller.handleError(named_function, err, info); + }; + } + }.callback; + + const setter_name = "set_" ++ name; + if (@hasDecl(Struct, setter_name) == false) { + template_proto.setGetter(js_name, getter_callback); + return; + } + + const setter = @field(Struct, setter_name); + const setter_callback = struct { + fn callback(_: ?*const v8.C_Name, raw_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void { + const info = v8.PropertyCallbackInfo.initFromV8(raw_info); + var caller = Caller(Self).init(info); + defer caller.deinit(); + + const js_value = v8.Value{ .handle = raw_value.? }; + const named_function = NamedFunction(Struct, setter, "set_" ++ name){}; + caller.setter(named_function, js_value, info) catch |err| { + caller.handleError(named_function, err, info); + }; + } + }.callback; + template_proto.setGetterAndSetter(js_name, getter_callback, setter_callback); + } + + fn generateIndexer(_: *Self, comptime Struct: type, template_proto: v8.ObjectTemplate) void { + if (@hasDecl(Struct, "indexed_get") == false) { + return; + } + const configuration = v8.IndexedPropertyHandlerConfiguration{ + .getter = struct { + fn callback(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void { + const info = v8.PropertyCallbackInfo.initFromV8(raw_info); + var caller = Caller(Self).init(info); + defer caller.deinit(); + + const named_function = NamedFunction(Struct, Struct.indexed_get, "indexed_get"){}; + caller.getIndex(named_function, idx, info) catch |err| { + caller.handleError(named_function, err, info); + }; + } + }.callback, + }; + + // If you're trying to implement setter, read: + // https://groups.google.com/g/v8-users/c/8tahYBsHpgY/m/IteS7Wn2AAAJ + // The issue I had was + // (a) where to attache it: does it go ont he instance_template + // instead of the prototype? + // (b) defining the getter or query to respond with the + // PropertyAttribute to indicate if the property can be set + template_proto.setIndexedProperty(configuration, null); + } + + fn generateNamedIndexer(_: *Self, comptime Struct: type, template_proto: v8.ObjectTemplate) void { + if (@hasDecl(Struct, "named_get") == false) { + return; + } + const configuration = v8.NamedPropertyHandlerConfiguration{ + .getter = struct { + fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) void { + const info = v8.PropertyCallbackInfo.initFromV8(raw_info); + var caller = Caller(Self).init(info); + defer caller.deinit(); + + const named_function = NamedFunction(Struct, Struct.named_get, "named_get"){}; + caller.getNamedIndex(named_function, .{ .handle = c_name.? }, info) catch |err| { + caller.handleError(named_function, err, info); + }; + } + }.callback, + + // This is really cool. Without this, we'd intercept _all_ properties + // even those explicitly set. So, node.length for example would get routed + // to our `named_get`, rather than a `get_length`. This might be + // useful if we run into a type that we can't model properly in Zig. + .flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking, + }; + + // If you're trying to implement setter, read: + // https://groups.google.com/g/v8-users/c/8tahYBsHpgY/m/IteS7Wn2AAAJ + // The issue I had was + // (a) where to attache it: does it go ont he instance_template + // instead of the prototype? + // (b) defining the getter or query to respond with the + // PropertyAttribute to indicate if the property can be set + template_proto.setNamedProperty(configuration, null); + } + + // Turns a Zig value into a JS one. + fn zigValueToJs( + templates: []v8.FunctionTemplate, + isolate: v8.Isolate, + context: v8.Context, + value: anytype, + ) anyerror!v8.Value { + // Check if it's a "simple" type. This is extracted so that it can be + // reused by other parts of the code. "simple" types only require an + // isolate to create (specifically, they don't our templates array) + if (simpleZigValueToJs(isolate, value, false)) |js_value| { + return js_value; + } + + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .void, .bool, .int, .comptime_int, .float, .comptime_float, .array => { + // Need to do this to keep the compiler happy + // simpleZigValueToJs handles all of these cases. + unreachable; + }, + .pointer => |ptr| switch (ptr.size) { + .one => { + const type_name = @typeName(ptr.child); + if (@hasField(TypeLookup, type_name)) { + const template = templates[@field(TYPE_LOOKUP, type_name)]; + const js_obj = try Executor.mapZigInstanceToJs(context, template, value); + return js_obj.toValue(); + } + + const one_info = @typeInfo(ptr.child); + if (one_info == .array and one_info.array.child == u8) { + // Need to do this to keep the compiler happy + // If this was the case, simpleZigValueToJs would + // have handled it + unreachable; + } + }, + .slice => { + if (ptr.child == u8) { + // Need to do this to keep the compiler happy + // If this was the case, simpleZigValueToJs would + // have handled it + unreachable; + } + var js_arr = v8.Array.init(isolate, @intCast(value.len)); + var js_obj = js_arr.castTo(v8.Object); + + for (value, 0..) |v, i| { + const js_val = try zigValueToJs(templates, isolate, context, v); + if (js_obj.setValueAtIndex(context, @intCast(i), js_val) == false) { + return error.FailedToCreateArray; + } + } + return js_obj.toValue(); + }, + else => {}, + }, + .@"struct" => |s| { + const type_name = @typeName(T); + if (@hasField(TypeLookup, type_name)) { + const template = templates[@field(TYPE_LOOKUP, type_name)]; + const js_obj = try Executor.mapZigInstanceToJs(context, template, value); + return js_obj.toValue(); + } + + if (T == Callback) { + // we're returnig a callback + return value.func.toValue(); + } + + // return the struct as a JS object + const js_obj = v8.Object.init(isolate); + inline for (s.fields) |f| { + const js_val = try zigValueToJs(templates, isolate, context, @field(value, f.name)); + const key = v8.String.initUtf8(isolate, f.name); + if (!js_obj.setValue(context, key, js_val)) { + return error.CreateObjectFailure; + } + } + return js_obj.toValue(); + }, + .@"union" => |un| { + if (T == std.json.Value) { + return zigJsonToJs(isolate, context, value); + } + if (un.tag_type) |UnionTagType| { + inline for (un.fields) |field| { + if (value == @field(UnionTagType, field.name)) { + return zigValueToJs(templates, isolate, context, @field(value, field.name)); + } + } + unreachable; + } + @compileError("Cannot use untagged union: " ++ @typeName(T)); + }, + .optional => { + if (value) |v| { + return zigValueToJs(templates, isolate, context, v); + } + return v8.initNull(isolate).toValue(); + }, + .error_union => return zigValueToJs(templates, isolate, context, value catch |err| return err), + else => {}, + } + @compileLog(@typeInfo(T)); + @compileError("A function returns an unsupported type: " ++ @typeName(T)); + } + + const PersistentObject = v8.Persistent(v8.Object); + const PersistentFunction = v8.Persistent(v8.Function); + + // This is capable of executing JavaScript. + pub const Executor = struct { + state: State, + isolate: v8.Isolate, + + handle_scope: v8.HandleScope, + + // @intFromPtr of our Executor is stored in this context, so given + // a context, we can always get the Executor back. + context: v8.Context, + + // Arena whose lifetime is for a single getter/setter/function/etc. + // Largely used to get strings out of V8, like a stack trace from + // a TryCatch. The allocator will be owned by the Scope, but the + // arena itself is owned by the Executor so that we can re-use it + // from scope to scope. + call_arena: ArenaAllocator, + + // Arena whose lifetime is for a single page load, aka a Scope. Where + // the call_arena lives for a single function call, the scope_arena + // lives for the lifetime of the entire page. The allocator will be + // owned by the Scope, but the arena itself is owned by the Executor + // so that we can re-use it from scope to scope. + scope_arena: ArenaAllocator, + + // When we need to load a resource (i.e. an external script), we call + // this function to get the source. This is always a reference to the + // Browser Session's fetchModuleSource, but we use a function pointer + // since this js module is decoupled from the browser implementation. + module_loader: ModuleLoader, + + // A Scope maps to a Browser's Page. Here though, it's only a + // mechanism to organization page-specific memory. The Executor + // does all the work, but having all page-specific data structures + // grouped together helps keep things clean. + scope: ?Scope = null, + + // refernces the Env.template array + templates: []v8.FunctionTemplate, + + const ModuleLoader = struct { ptr: *anyopaque, func: *const fn (ptr: *anyopaque, specifier: []const u8) anyerror![]const u8 }; + + // no init, must be initialized via env.startExecutor() + + // not public, must be destroyed via env.stopExecutor() + fn deinit(self: *Executor) void { + if (self.scope) |*s| { + s.deinit(); + } + self.context.exit(); + self.handle_scope.deinit(); + self.call_arena.deinit(); + self.scope_arena.deinit(); + } + + // Executes the src + pub fn exec(self: *Executor, src: []const u8, name: ?[]const u8) !Value { + const isolate = self.isolate; + const context = self.context; + + var origin: ?v8.ScriptOrigin = null; + if (name) |n| { + const scr_name = v8.String.initUtf8(isolate, n); + origin = v8.ScriptOrigin.initDefault(isolate, scr_name.toValue()); + } + const scr_js = v8.String.initUtf8(isolate, src); + const scr = v8.Script.compile(context, scr_js, origin) catch { + return error.CompilationError; + }; + + const value = scr.run(context) catch { + return error.ExecutionError; + }; + + return self.createValue(value); + } + + // compile and eval a JS module + // It doesn't wait for callbacks execution + pub fn module(self: *Executor, src: []const u8, name: []const u8) !Value { + const context = self.context; + const m = try self.compileModule(src, name); + + // instantiate + // TODO handle ResolveModuleCallback parameters to load module's + // dependencies. + const ok = m.instantiate(context, resolveModuleCallback) catch { + return error.ExecutionError; + }; + + if (!ok) { + return error.ModuleInstantiationError; + } + + // evaluate + const value = m.evaluate(context) catch return error.ExecutionError; + return self.createValue(value); + } + + fn compileModule(self: *Executor, src: []const u8, name: []const u8) !v8.Module { + const isolate = self.isolate; + + // compile + const script_name = v8.String.initUtf8(isolate, name); + const script_source = v8.String.initUtf8(isolate, src); + + const origin = v8.ScriptOrigin.init( + self.isolate, + script_name.toValue(), + 0, // resource_line_offset + 0, // resource_column_offset + false, // resource_is_shared_cross_origin + -1, // script_id + null, // source_map_url + false, // resource_is_opaque + false, // is_wasm + true, // is_module + null, // host_defined_options + ); + + var script_comp_source: v8.ScriptCompilerSource = undefined; + v8.ScriptCompilerSource.init(&script_comp_source, script_source, origin, null); + defer script_comp_source.deinit(); + + return v8.ScriptCompiler.compileModule( + isolate, + &script_comp_source, + .kNoCompileOptions, + .kNoCacheNoReason, + ) catch return error.CompilationError; + } + + // Our scope maps to a "browser.Page". + // A v8.HandleScope is like an arena. Once created, any "Local" that + // v8 creates will be released (or at least, releasable by the v8 GC) + // when the handle_scope is freed. + // We also maintain our own "scope_arena" which allows us to have + // all page related memory easily managed. + pub fn startScope(self: *Executor, global: anytype) !void { + std.debug.assert(self.scope == null); + + var handle_scope: v8.HandleScope = undefined; + v8.HandleScope.init(&handle_scope, self.isolate); + self.scope = Scope{ + .handle_scope = handle_scope, + .arena = self.scope_arena.allocator(), + .call_arena = self.scope_arena.allocator(), + }; + _ = try self._mapZigInstanceToJs(self.context.getGlobal(), global); + } + + pub fn endScope(self: *Executor) void { + self.scope.?.deinit(); + self.scope = null; + _ = self.scope_arena.reset(.{ .retain_with_limit = 1024 * 64 }); + } + + // Wrap a v8.Value, largely so that we can provide a convenient + // toString function + fn createValue(self: *const Executor, value: v8.Value) Value { + return .{ + .value = value, + .executor = self, + }; + } + + fn zigValueToJs(self: *const Executor, value: anytype) !v8.Value { + return Self.zigValueToJs(self.templates, self.isolate, self.context, value); + } + + // See _mapZigInstanceToJs, this is wrapper that can be called + // without an Executor. This is possible because we store our + // executor in the EmbedderData of the v8.Context. So, as long as + // we have a v8.Context, we can get the executor. + fn mapZigInstanceToJs(context: v8.Context, js_obj_or_template: anytype, value: anytype) !PersistentObject { + const executor: *Executor = @ptrFromInt(context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); + return executor._mapZigInstanceToJs(js_obj_or_template, value); + } + + // To turn a Zig instance into a v8 object, we need to do a number of things. + // First, if it's a struct, we need to put it on the heap + // Second, if we've already returned this instance, we should return + // the same object. Hence, our executor maintains a map of Zig objects + // to v8.PersistentObject (the "identity_map"). + // Finally, if this is the first time we've seen this instance, we need to: + // 1 - get the FunctionTemplate (from our templates slice) + // 2 - Create the TaggedAnyOpaque so that, if needed, we can do the reverse + // (i.e. js -> zig) + // 3 - Create a v8.PersistentObject (because Zig owns this object, not v8) + // 4 - Store our TaggedAnyOpaque into the persistent object + // 5 - Update our identity_map (so that, if we return this same instance again, + // we can just grab it from the identity_map) + fn _mapZigInstanceToJs(self: *Executor, js_obj_or_template: anytype, value: anytype) !PersistentObject { + const scope = &self.scope.?; + const context = self.context; + const scope_arena = scope.arena; + + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .@"struct" => { + // Struct, has to be placed on the heap + const heap = try scope_arena.create(T); + heap.* = value; + return self._mapZigInstanceToJs(js_obj_or_template, heap); + }, + .pointer => |ptr| { + const gop = try scope.identity_map.getOrPut(scope_arena, @intFromPtr(value)); + if (gop.found_existing) { + // we've seen this instance before, return the same + // PersistentObject. + return gop.value_ptr.*; + } + + // Sometimes we're creating a new v8.Object, like when + // we're returning a value from a function. In those cases + // we have the FunctionTemplate, and we can get an object + // by calling initInstance its InstanceTemplate. + // Sometimes though we already have the v8.Objct to bind to + // for example, when we're executing a constructor, v8 has + // already created the "this" object. + const js_obj = switch (@TypeOf(js_obj_or_template)) { + v8.Object => js_obj_or_template, + v8.FunctionTemplate => js_obj_or_template.getInstanceTemplate().initInstance(context), + else => @compileError("mapZigInstanceToJs requires a v8.Object (constructors) or v8.FunctionTemplate, got: " ++ @typeName(@TypeOf(js_obj_or_template))), + }; + + const isolate = self.isolate; + + if (isEmpty(ptr.child) == false) { + // The TAO contains the pointer ot our Zig instance as + // well as any meta data we'll need to use it later. + // See the TaggedAnyOpaque struct for more details. + const tao = try scope_arena.create(TaggedAnyOpaque); + tao.* = .{ + .ptr = value, + .index = @field(TYPE_LOOKUP, @typeName(ptr.child)), + .sub_type = if (@hasDecl(ptr.child, "sub_type")) ptr.child.sub_type else null, + .offset = if (@typeInfo(ptr.child) != .@"opaque" and @hasField(ptr.child, "proto")) @offsetOf(ptr.child, "proto") else -1, + }; + + js_obj.setInternalField(0, v8.External.init(isolate, tao)); + } else { + // If the struct is empty, we don't need to do all + // the TOA stuff and setting the internal data. + // When we try to map this from JS->Zig, in + // typeTaggedAnyOpaque, we'll also know there that + // the type is empty and can create an empty instance. + } + + // Do not move this _AFTER_ the postAttach code. + // postAttach is likely to call back into this function + // mutating our identity_map, and making the gop pointers + // invalid. + const js_persistent = PersistentObject.init(isolate, js_obj); + gop.value_ptr.* = js_persistent; + + if (@hasDecl(ptr.child, "postAttach")) { + const obj_wrap = JsObject{ .js_obj = js_obj, .executor = self }; + switch (@typeInfo(@TypeOf(ptr.child.postAttach)).@"fn".params.len) { + 2 => try value.postAttach(obj_wrap), + 3 => try value.postAttach(self.state, obj_wrap), + else => @compileError(@typeName(ptr.child) ++ ".postAttach must take 2 or 3 parameters"), + } + } + + return js_persistent; + }, + else => @compileError("Expected a struct or pointer, got " ++ @typeName(T) ++ " (constructors must return struct or pointers)"), + } + } + + // Callback from V8, asking us to load a module. The "specifier" is + // the src of the module to load. + fn resolveModuleCallback( + c_context: ?*const v8.C_Context, + c_specifier: ?*const v8.C_String, + import_attributes: ?*const v8.C_FixedArray, + referrer: ?*const v8.C_Module, + ) callconv(.C) ?*const v8.C_Module { + _ = import_attributes; + _ = referrer; + + std.debug.assert(c_context != null); + const context = v8.Context{ .handle = c_context.? }; + + const self: *Executor = @ptrFromInt(context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); + + var buf: [1024]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buf); + + // build the specifier value. + const specifier = valueToString( + fba.allocator(), + .{ .handle = c_specifier.? }, + self.isolate, + context, + ) catch |e| { + log.err("resolveModuleCallback: get ref str: {any}", .{e}); + return null; + }; + + // not currently needed + // const referrer_module = if (referrer) |ref| v8.Module{ .handle = ref } else null; + const module_loader = self.module_loader; + const source = module_loader.func(module_loader.ptr, specifier) catch |err| { + log.err("fetchModuleSource for '{s}' fetch error: {}", .{ specifier, err }); + return null; + }; + + const m = self.compileModule(source, specifier) catch |err| { + log.err("fetchModuleSource for '{s}' compile error: {}", .{ specifier, err }); + return null; + }; + return m.handle; + } + }; + + // Loosely maps to a Browser Page. Executor does all the work, this just + // contains all the data structures / memory we need for a page. It helps + // to keep things organized. I.e. we have a single nullable, + // scope: ?Scope = null + // in executor, rather than having one for each of these. + pub const Scope = struct { + arena: Allocator, + call_arena: Allocator, + handle_scope: v8.HandleScope, + callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .{}, + identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{}, + + fn deinit(self: *Scope) void { + var it = self.identity_map.valueIterator(); + while (it.next()) |p| { + p.deinit(); + } + for (self.callbacks.items) |*cb| { + cb.deinit(); + } + self.handle_scope.deinit(); + } + + fn trackCallback(self: *Scope, pf: PersistentFunction) !void { + return self.callbacks.append(self.arena, pf); + } + }; + + pub const Callback = struct { + id: usize, + executor: *Executor, + this: ?v8.Object = null, + func: PersistentFunction, + + // We use this when mapping a JS value to a Zig object. We can't + // Say we have a Zig function that takes a Callback, we can't just + // check param.type == Callback, because Callback is a generic. + // So, as a quick hack, we can determine if the Zig type is a + // callback by checking @hasDecl(T, "_CALLBACK_ID_KLUDGE") + const _CALLBACK_ID_KLUDGE = true; + + pub const Result = struct { + stack: ?[]const u8, + exception: []const u8, + }; + + pub fn setThis(self: *Callback, value: anytype) !void { + const persistent_object = self.executor.scope.?.identity_map.get(@intFromPtr(value)) orelse { + return error.InvalidThisForCallback; + }; + self.this = persistent_object.castToObject(); + } + + pub fn call(self: *const Callback, args: anytype) !void { + return self.callWithThis(self.this orelse self.executor.context.getGlobal(), args); + } + + pub fn tryCall(self: *const Callback, args: anytype, result: *Result) !void { + var try_catch: TryCatch = undefined; + try_catch.init(self.executor); + defer try_catch.deinit(); + + self.call(args) catch |err| { + if (try_catch.hasCaught()) { + const allocator = self.executor.scope.?.call_arena; + result.stack = try_catch.stack(allocator) catch null; + result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err); + } else { + result.stack = null; + result.exception = @errorName(err); + } + return err; + }; + } + + fn callWithThis(self: *const @This(), js_this: v8.Object, args: anytype) !void { + const executor = self.executor; + + const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args; + const fields = @typeInfo(@TypeOf(aargs)).@"struct".fields; + var js_args: [fields.len]v8.Value = undefined; + inline for (fields, 0..) |f, i| { + js_args[i] = try executor.zigValueToJs(@field(aargs, f.name)); + } + + const result = self.func.castToFunction().call(executor.context, js_this, &js_args); + if (result == null) { + return error.JSExecCallback; + } + } + + // debug/helper to print the source of the JS callback + fn printFunc(self: *const @This()) !void { + const executor = self.executor; + const value = self.func.castToFunction().toValue(); + const src = try valueToString(executor.call_arena.allocator(), value, executor.isolate, executor.context); + std.debug.print("{s}\n", .{src}); + } + }; + + pub const JsObject = struct { + js_obj: v8.Object, + executor: *Executor, + + // If a Zig struct wants the Object parameter, it'll declare a + // function like: + // fn _length(self: *const NodeList, js_obj: Env.Object) usize + // + // When we're trying to call this function, we can't just do + // if (params[i].type.? == Object) + // Because there is _no_ object, there's only an Env.Object, where + // Env is a generic. + // We could probably figure out a way to do this, but simply checking + // for this declaration is _a lot_ easier. + const _JSOBJECT_ID_KLUDGE = true; + + pub fn setIndex(self: JsObject, index: usize, value: anytype) !void { + const key = switch (index) { + inline 0...1000 => |i| std.fmt.comptimePrint("{d}", .{i}), + else => try std.fmt.allocPrint(self.executor.scope_arena.allocator(), "{d}", .{index}), + }; + return self.set(key, value); + } + + pub fn set(self: JsObject, key: []const u8, value: anytype) !void { + const executor = self.executor; + + const js_key = v8.String.initUtf8(executor.isolate, key); + const js_value = try executor.zigValueToJs(value); + if (!self.js_obj.setValue(executor.context, js_key, js_value)) { + return error.FailedToSet; + } + } + }; + + pub const TryCatch = struct { + inner: v8.TryCatch, + executor: *const Executor, + + pub fn init(self: *TryCatch, executor: *const Executor) void { + self.executor = executor; + self.inner.init(executor.isolate); + } + + pub fn hasCaught(self: TryCatch) bool { + return self.inner.hasCaught(); + } + + // the caller needs to deinit the string returned + pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 { + const msg = self.inner.getException() orelse return null; + const executor = self.executor; + return try valueToString(allocator, msg, executor.isolate, executor.context); + } + + // the caller needs to deinit the string returned + pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 { + const executor = self.executor; + const s = self.inner.getStackTrace(executor.context) orelse return null; + return try valueToString(allocator, s, executor.isolate, executor.context); + } + + // a shorthand method to return either the entire stack message + // or just the exception message + // - in Debug mode return the stack if available + // - otherwhise return the exception if available + // the caller needs to deinit the string returned + pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 { + if (builtin.mode == .Debug) { + if (try self.stack(allocator)) |msg| { + return msg; + } + } + return try self.exception(allocator); + } + + pub fn deinit(self: *TryCatch) void { + self.inner.deinit(); + } + }; + + pub const Inspector = struct { + isolate: v8.Isolate, + inner: *v8.Inspector, + session: v8.InspectorSession, + + // We expect allocator to be an arena + pub fn init(allocator: Allocator, executor: *const Executor, ctx: anytype) !Inspector { + const ContextT = @TypeOf(ctx); + + const InspectorContainer = switch (@typeInfo(ContextT)) { + .@"struct" => ContextT, + .pointer => |ptr| ptr.child, + .void => NoopInspector, + else => @compileError("invalid context type"), + }; + + // If necessary, turn a void context into something we can safely ptrCast + const safe_context: *anyopaque = if (ContextT == void) @constCast(@ptrCast(&{})) else ctx; + + const isolate = executor.isolate; + const channel = v8.InspectorChannel.init(safe_context, InspectorContainer.onInspectorResponse, InspectorContainer.onInspectorEvent, isolate); + + const client = v8.InspectorClient.init(); + + const inner = try allocator.create(v8.Inspector); + v8.Inspector.init(inner, client, channel, isolate); + return .{ .inner = inner, .isolate = isolate, .session = inner.connect() }; + } + + pub fn deinit(self: *const Inspector) void { + self.session.deinit(); + self.inner.deinit(); + } + + pub fn send(self: *const Inspector, msg: []const u8) void { + self.session.dispatchProtocolMessage(self.isolate, msg); + } + + pub fn contextCreated( + self: *const Inspector, + executor: *const Executor, + name: []const u8, + origin: []const u8, + aux_data: ?[]const u8, + ) void { + self.inner.contextCreated(executor.context, name, origin, aux_data); + } + + // Retrieves the RemoteObject for a given value. + // The value is loaded through the Executor's mapZigInstanceToJs function, + // just like a method return value. Therefore, if we've mapped this + // value before, we'll get the existing JS PersistedObject and if not + // we'll create it and track it for cleanup when the scope ends. + pub fn getRemoteObject( + self: *const Inspector, + executor: *const Executor, + group: []const u8, + value: anytype, + ) !RemoteObject { + const js_value = try zigValueToJs( + executor.templates, + executor.isolate, + executor.context, + value, + ); + + // We do not want to expose this as a parameter for now + const generate_preview = false; + return self.session.wrapObject( + executor.isolate, + executor.context, + js_value, + group, + generate_preview, + ); + } + }; + + pub const RemoteObject = v8.RemoteObject; + + pub const Value = struct { + value: v8.Value, + executor: *const Executor, + + // the caller needs to deinit the string returned + pub fn toString(self: Value, allocator: Allocator) ![]const u8 { + const executor = self.executor; + return valueToString(allocator, self.value, executor.isolate, executor.context); + } + }; + + // Reverses the mapZigInstanceToJs, making sure that our TaggedAnyOpaque + // contains a ptr to the correct type. + fn typeTaggedAnyOpaque(comptime named_function: anytype, comptime R: type, js_obj: v8.Object) !R { + const ti = @typeInfo(R); + if (ti != .pointer) { + @compileError(std.fmt.comptimePrint( + "{s} has a non-pointer Zig parameter type: {s}", + .{ named_function.full_name, @typeName(R) }, + )); + } + + const T = ti.pointer.child; + if (comptime isEmpty(T)) { + // Empty structs aren't stored as TOAs and there's no data + // stored in the JSObject's IntenrnalField. Why bother when + // we can just return an empty struct here? + return @constCast(@as(*const T, &.{})); + } + + const type_name = @typeName(T); + if (@hasField(TypeLookup, type_name) == false) { + @compileError(std.fmt.comptimePrint( + "{s} has an unknown Zig type: {s}", + .{ named_function.full_name, @typeName(R) }, + )); + } + + const op = js_obj.getInternalField(0).castTo(v8.External).get(); + const toa: *TaggedAnyOpaque = @alignCast(@ptrCast(op)); + const expected_type_index = @field(TYPE_LOOKUP, type_name); + + var type_index = toa.index; + if (type_index == expected_type_index) { + return @alignCast(@ptrCast(toa.ptr)); + } + + // search through the prototype tree + while (true) { + const prototype_index = PROTOTYPE_TABLE[type_index]; + if (prototype_index == expected_type_index) { + // -1 is a sentinel value used for non-composition prototype + // This is used with netsurf and we just unsafely cast one + // type to another + const offset = toa.offset; + if (offset == -1) { + return @alignCast(@ptrCast(toa.ptr)); + } + + // A non-negative offset means we're using composition prototype + // (i.e. our struct has a "proto" field). the offset + // reresents the byte offset of the field. We can use that + // + the toa.ptr to get the field + return @ptrFromInt(@intFromPtr(toa.ptr) + @as(usize, @intCast(offset))); + } + if (prototype_index == type_index) { + return error.InvalidArgument; + } + type_index = prototype_index; + } + } + }; +} + +fn isEmpty(comptime T: type) bool { + return @typeInfo(T) != .@"opaque" and @sizeOf(T) == 0; +} + +// Responsible for calling Zig functions from JS invokations. This could +// probably just contained in Executor, but having this specific logic, which +// is somewhat repetitive between constructors, functions, getters, etc contained +// here does feel like it makes it clenaer. +fn Caller(comptime E: type) type { + const State = E.State; + const TYPE_LOOKUP = E.TYPE_LOOKUP; + const TypeLookup = @TypeOf(TYPE_LOOKUP); + + return struct { + context: v8.Context, + isolate: v8.Isolate, + executor: *E.Executor, + call_allocator: Allocator, + + const Self = @This(); + + // info is a v8.PropertyCallbackInfo or a v8.FunctionCallback + // All we really want from it is the isolate. + // executor = Isolate -> getCurrentContext -> getEmbedderData() + fn init(info: anytype) Self { + const isolate = info.getIsolate(); + const context = isolate.getCurrentContext(); + const executor: *E.Executor = @ptrFromInt(context.getEmbedderData(1).castTo(v8.BigInt).getUint64()); + + return .{ + .isolate = isolate, + .context = context, + .executor = executor, + .call_allocator = executor.scope.?.call_arena, + }; + } + + fn deinit(self: *Self) void { + _ = self; + // _ = self.executor.call_arena.reset(.{ .retain_with_limit = 4096 }); + } + + fn constructor(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void { + const S = named_function.S; + const args = try self.getArgs(named_function, 0, info); + const res = @call(.auto, S.constructor, args); + + const ReturnType = @typeInfo(@TypeOf(S.constructor)).@"fn".return_type orelse { + @compileError(@typeName(S) ++ " has a constructor without a return type"); + }; + + const this = info.getThis(); + if (@typeInfo(ReturnType) == .error_union) { + const non_error_res = res catch |err| return err; + _ = try E.Executor.mapZigInstanceToJs(self.context, this, non_error_res); + } else { + _ = try E.Executor.mapZigInstanceToJs(self.context, this, res); + } + info.getReturnValue().set(this); + } + + fn method(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void { + const S = named_function.S; + comptime assertSelfReceiver(named_function); + + var args = try self.getArgs(named_function, 1, info); + const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis()); + + // inject 'self' as the first parameter + @field(args, "0") = zig_instance; + + const res = @call(.auto, named_function.func, args); + info.getReturnValue().set(try self.zigValueToJs(res)); + } + + fn getter(self: *Self, comptime named_function: anytype, info: v8.PropertyCallbackInfo) !void { + const S = named_function.S; + const Getter = @TypeOf(named_function.func); + if (@typeInfo(Getter).@"fn".return_type == null) { + @compileError(@typeName(S) ++ " has a getter without a return type: " ++ @typeName(Getter)); + } + + var args: ParamterTypes(Getter) = undefined; + const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields; + switch (arg_fields.len) { + 0 => {}, // getters _can_ be parameterless + 1, 2 => { + const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis()); + comptime assertSelfReceiver(named_function); + @field(args, "0") = zig_instance; + if (comptime arg_fields.len == 2) { + comptime assertIsStateArg(named_function, 1); + @field(args, "1") = self.executor.state; + } + }, + else => @compileError(named_function.full_name + " has too many parmaters: " ++ @typeName(named_function.func)), + } + const res = @call(.auto, named_function.func, args); + info.getReturnValue().set(try self.zigValueToJs(res)); + } + + fn setter(self: *Self, comptime named_function: anytype, js_value: v8.Value, info: v8.PropertyCallbackInfo) !void { + const S = named_function.S; + comptime assertSelfReceiver(named_function); + + const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis()); + + const Setter = @TypeOf(named_function.func); + var args: ParamterTypes(Setter) = undefined; + const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields; + switch (arg_fields.len) { + 0 => unreachable, // assertSelfReceiver make sure of this + 1 => @compileError(named_function.full_name ++ " only has 1 parameter"), + 2, 3 => { + @field(args, "0") = zig_instance; + @field(args, "1") = try self.jsValueToZig(named_function, arg_fields[1].type, js_value); + if (comptime arg_fields.len == 3) { + comptime assertIsStateArg(named_function, 2); + @field(args, "2") = self.executor.state; + } + }, + else => @compileError(named_function.full_name ++ " setter with more than 3 parameters, why?"), + } + + if (@typeInfo(Setter).@"fn".return_type) |return_type| { + if (@typeInfo(return_type) == .error_union) { + _ = try @call(.auto, named_function.func, args); + return; + } + } + _ = @call(.auto, named_function.func, args); + } + + fn getIndex(self: *Self, comptime named_function: anytype, idx: u32, info: v8.PropertyCallbackInfo) !void { + const S = named_function.S; + const IndexedGet = @TypeOf(named_function.func); + if (@typeInfo(IndexedGet).@"fn".return_type == null) { + @compileError(named_function.full_name ++ " must have a return type"); + } + + var has_value = true; + + var args: ParamterTypes(IndexedGet) = undefined; + const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields; + switch (arg_fields.len) { + 0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"), + 3, 4 => { + const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis()); + comptime assertSelfReceiver(named_function); + @field(args, "0") = zig_instance; + @field(args, "1") = idx; + @field(args, "2") = &has_value; + if (comptime arg_fields.len == 4) { + comptime assertIsStateArg(named_function, 3); + @field(args, "3") = self.executor.state; + } + }, + else => @compileError(named_function.full_name ++ " has too many parmaters"), + } + + const res = @call(.auto, S.indexed_get, args); + if (has_value == false) { + // for an indexed parameter, say nodes[10000], we should return + // undefined, not null, if the index is out of rante + info.getReturnValue().set(try self.zigValueToJs({})); + } else { + info.getReturnValue().set(try self.zigValueToJs(res)); + } + } + + fn getNamedIndex(self: *Self, comptime named_function: anytype, name: v8.Name, info: v8.PropertyCallbackInfo) !void { + const S = named_function.S; + const NamedGet = @TypeOf(named_function.func); + if (@typeInfo(NamedGet).@"fn".return_type == null) { + @compileError(named_function.full_name ++ " must have a return type"); + } + + var has_value = true; + var args: ParamterTypes(NamedGet) = undefined; + const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields; + switch (arg_fields.len) { + 0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"), + 3, 4 => { + const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(S), info.getThis()); + comptime assertSelfReceiver(named_function); + @field(args, "0") = zig_instance; + @field(args, "1") = try self.nameToString(name); + @field(args, "2") = &has_value; + if (comptime arg_fields.len == 4) { + comptime assertIsStateArg(named_function, 3); + @field(args, "3") = self.executor.state; + } + }, + else => @compileError(named_function.full_name ++ " has too many parmaters"), + } + + const res = @call(.auto, S.named_get, args); + if (has_value == false) { + // for an indexed parameter, say nodes[10000], we should return + // undefined, not null, if the index is out of rante + info.getReturnValue().set(try self.zigValueToJs({})); + } else { + info.getReturnValue().set(try self.zigValueToJs(res)); + } + } + + fn nameToString(self: *Self, name: v8.Name) ![]const u8 { + return valueToString(self.call_allocator, .{ .handle = name.handle }, self.isolate, self.context); + } + + fn assertSelfReceiver(comptime named_function: anytype) void { + const params = @typeInfo(@TypeOf(named_function.func)).@"fn".params; + if (params.len == 0) { + @compileError(named_function.full_name ++ " must have a self parameter"); + } + const R = Receiver(named_function.S); + + const first_param = params[0].type.?; + if (first_param != *R and first_param != *const R) { + @compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{ named_function.full_name, @typeName(R), @typeName(R), @typeName(first_param) })); + } + } + + fn assertIsStateArg(comptime named_function: anytype, index: comptime_int) void { + const F = @TypeOf(named_function.func); + const params = @typeInfo(F).@"fn".params; + + const param = params[index].type.?; + if (param != State) { + @compileError(std.fmt.comptimePrint("The {d} parameter to {s} must be a {s}. Got: {s}", .{ index, named_function.full_name, @typeName(State), @typeName(param) })); + } + } + + fn handleError(self: *Self, comptime named_function: anytype, err: anyerror, info: anytype) void { + const isolate = self.isolate; + var js_err: ?v8.Value = switch (err) { + error.InvalidArgument => createTypeException(isolate, "invalid argument"), + error.OutOfMemory => createException(isolate, "out of memory"), + else => blk: { + const return_type = @typeInfo(@TypeOf(named_function.func)).@"fn".return_type orelse { + // void return type; + break :blk null; + }; + + if (@typeInfo(return_type) != .error_union) { + // type defines a custom exception, but this function should + // not fail. We failed somewhere inside of js.zig and + // should return the error as-is, since it isn't related + // to our Struct + break :blk null; + } + + const function_error_set = @typeInfo(return_type).error_union.error_set; + + const Exception = comptime getCustomException(named_function.S) orelse break :blk null; + if (function_error_set == Exception or isErrorSetException(Exception, err)) { + const custom_exception = Exception.init(self.call_allocator, err, named_function.js_name) catch |init_err| { + switch (init_err) { + // if a custom exceptions' init wants to return a + // different error, we need to think about how to + // handle that failure. + error.OutOfMemory => break :blk createException(isolate, "out of memory"), + } + }; + // ughh..how to handle an error here? + break :blk self.zigValueToJs(custom_exception) catch createException(isolate, "internal error"); + } + // this error isn't part of a custom exception + break :blk null; + }, + }; + + if (js_err == null) { + js_err = createException(isolate, @errorName(err)); + } + const js_exception = isolate.throwException(js_err.?); + info.getReturnValue().setValueHandle(js_exception.handle); + } + + // walk the prototype chain to see if a type declares a custom Exception + fn getCustomException(comptime Struct: type) ?type { + var S = Struct; + while (true) { + if (@hasDecl(S, "Exception")) { + return S.Exception; + } + if (@hasDecl(S, "prototype") == false) { + return null; + } + // long ago, we validated that every prototype declaration + // is a pointer. + S = @typeInfo(S.prototype).pointer.child; + } + } + + // Does the error we want to return belong to the custom exeception's ErrorSet + fn isErrorSetException(comptime Exception: type, err: anytype) bool { + const Entry = std.meta.Tuple(&.{ []const u8, void }); + const error_set = @typeInfo(Exception.ErrorSet).error_set.?; + const entries = comptime blk: { + var kv: [error_set.len]Entry = undefined; + for (error_set, 0..) |e, i| { + kv[i] = .{ e.name, {} }; + } + break :blk kv; + }; + const lookup = std.StaticStringMap(void).initComptime(entries); + return lookup.has(@errorName(err)); + } + + // If we call a method in javascript: cat.lives('nine'); + // + // Then we'd expect a Zig function with 2 parameters: a self and the string. + // In this case, offset == 1. Offset is always 1 for setters or methods. + // + // Offset is always 0 for constructors. + // + // For constructors, setters and methods, we can further increase offset + 1 + // if the first parameter is an instance of State. + // + // Finally, if the JS function is called with _more_ parameters and + // the last parameter in Zig is an array, we'll try to slurp the additional + // parameters into the array. + fn getArgs(self: *const Self, comptime named_function: anytype, comptime offset: usize, info: anytype) !ParamterTypes(@TypeOf(named_function.func)) { + const F = @TypeOf(named_function.func); + var args: ParamterTypes(F) = undefined; + + const params = @typeInfo(F).@"fn".params[offset..]; + // Except for the constructor, the first parameter is always `self` + // This isn't something we'll bind from JS, so skip it. + const params_to_map = blk: { + if (params.len == 0) { + return args; + } + + // If the last parameter is the State, set it, and exclude it + // from our params slice, because we don't want to bind it to + // a JS argument + if (comptime isState(params[params.len - 1].type.?)) { + @field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = self.executor.state; + break :blk params[0 .. params.len - 1]; + } + + // If the last parameter is a JsObject, set it, and exclude it + // from our params slice, because we don't want to bind it to + // a JS argument + if (comptime isJsObject(params[params.len - 1].type.?)) { + @field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{ + .handle = info.getThis(), + .executor = self.executor, + }; + + // AND the 2nd last parameter is state + if (params.len > 1 and comptime isState(params[params.len - 2].type.?)) { + @field(args, std.fmt.comptimePrint("{d}", .{params.len - 2 + offset})) = self.executor.state; + break :blk params[0 .. params.len - 2]; + } + + break :blk params[0 .. params.len - 1]; + } + + // we have neither a State nor a JsObject. All params must be + // bound to a JavaScript value. + break :blk params; + }; + + if (params_to_map.len == 0) { + return args; + } + + const js_parameter_count = info.length(); + const last_js_parameter = params_to_map.len - 1; + var is_variadic = false; + + { + // This is going to get complicated. If the last Zig paremeter + // is a slice AND the corresponding javascript parameter is + // NOT an an array, then we'll treat it as a variadic. + + const last_parameter_type = params_to_map[params_to_map.len - 1].type.?; + const last_parameter_type_info = @typeInfo(last_parameter_type); + if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) { + const slice_type = last_parameter_type_info.pointer.child; + const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter))); + if (corresponding_js_value.isArray() == false and slice_type != u8) { + is_variadic = true; + if (js_parameter_count == 0) { + @field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{}; + } else { + const arr = try self.call_allocator.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1); + for (arr, last_js_parameter..) |*a, i| { + const js_value = info.getArg(@as(u32, @intCast(i))); + a.* = try self.jsValueToZig(named_function, slice_type, js_value); + } + @field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr; + } + } + } + } + + inline for (params_to_map, 0..) |param, i| { + const field_index = comptime i + offset; + if (comptime i == params_to_map.len - 1) { + if (is_variadic) { + break; + } + } + + if (comptime isState(param.type.?)) { + @compileError("State must be the last parameter (or 2nd last if there's a JsObject): " ++ named_function.full_name); + } else if (comptime isJsObject(param.type.?)) { + @compileError("JsObject must be the last parameter: " ++ named_function.full_name); + } else if (i >= js_parameter_count) { + if (@typeInfo(param.type.?) != .optional) { + return error.InvalidArgument; + } + @field(args, tupleFieldName(field_index)) = null; + } else { + const js_value = info.getArg(@as(u32, @intCast(i))); + @field(args, tupleFieldName(field_index)) = self.jsValueToZig(named_function, param.type.?, js_value) catch { + return error.InvalidArgument; + }; + } + } + + return args; + } + + fn jsValueToZig(self: *const Self, comptime named_function: anytype, comptime T: type, js_value: v8.Value) !T { + switch (@typeInfo(T)) { + .optional => |o| { + if (js_value.isNull() or js_value.isUndefined()) { + return null; + } + return try self.jsValueToZig(named_function, o.child, js_value); + }, + .float => |f| switch (f.bits) { + 0...32 => return js_value.toF32(self.context), + 33...64 => return js_value.toF64(self.context), + else => {}, + }, + .int => return jsIntToZig(T, js_value, self.context), + .bool => return js_value.toBool(self.isolate), + .pointer => |ptr| switch (ptr.size) { + .one => { + if (!js_value.isObject()) { + return error.InvalidArgument; + } + if (@hasField(TypeLookup, @typeName(ptr.child))) { + const obj = js_value.castTo(v8.Object); + if (obj.internalFieldCount() == 0) { + return error.InvalidArgument; + } + return E.typeTaggedAnyOpaque(named_function, *Receiver(ptr.child), obj); + } + }, + .slice => { + if (ptr.child == u8) { + return valueToString(self.call_allocator, js_value, self.isolate, self.context); + } + + // TODO: TypedArray + // if (js_value.isArrayBufferView()) { + // const abv = js_value.castTo(v8.ArrayBufferView); + // const ab = abv.getBuffer(); + // const bs = v8.BackingStore.sharedPtrGet(&ab.getBackingStore()); + // const data = bs.getData(); + // var arr = @as([*]i32, @alignCast(@ptrCast(data)))[0..2]; + // std.debug.print("{d} {d} {d}\n", .{arr[0], arr[1], bs.getByteLength()}); + // arr[1] = 3333; + // return &.{}; + // } + + if (!js_value.isArray()) { + return error.InvalidArgument; + } + + const context = self.context; + const js_arr = js_value.castTo(v8.Array); + const js_obj = js_arr.castTo(v8.Object); + + // Newer version of V8 appear to have an optimized way + // to do this (V8::Array has an iterate method on it) + const arr = try self.call_allocator.alloc(ptr.child, js_arr.length()); + for (arr, 0..) |*a, i| { + a.* = try self.jsValueToZig(named_function, ptr.child, try js_obj.getAtIndex(context, @intCast(i))); + } + return arr; + }, + else => {}, + }, + .@"struct" => |s| { + if (@hasDecl(T, "_CALLBACK_ID_KLUDGE")) { + if (!js_value.isFunction()) { + return error.InvalidArgument; + } + + const executor = self.executor; + const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function)); + try executor.scope.?.trackCallback(func); + + return .{ + .func = func, + .executor = executor, + .id = js_value.castTo(v8.Object).getIdentityHash(), + }; + } + + if (!js_value.isObject()) { + return error.InvalidArgument; + } + const context = self.context; + const isolate = self.isolate; + const js_obj = js_value.castTo(v8.Object); + + var value: T = undefined; + inline for (s.fields) |field| { + const name = field.name; + const key = v8.String.initUtf8(isolate, name); + if (js_obj.has(context, key.toValue())) { + @field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(context, key)); + } else if (@typeInfo(field.type) == .optional) { + @field(value, name) = null; + } else { + if (field.defaultValue()) |dflt| { + @field(value, name) = dflt; + } else { + return error.JSWrongObject; + } + } + } + return value; + }, + else => {}, + } + + @compileError(std.fmt.comptimePrint("{s} has an unsupported parameter type: {s}", .{ named_function.full_name, @typeName(T) })); + } + + fn jsIntToZig(comptime T: type, js_value: v8.Value, context: v8.Context) !T { + const n = @typeInfo(T).int; + switch (n.signedness) { + .signed => switch (n.bits) { + 8 => return jsSignedIntToZig(i8, -128, 127, try js_value.toI32(context)), + 16 => return jsSignedIntToZig(i16, -32_768, 32_767, try js_value.toI32(context)), + 32 => return jsSignedIntToZig(i32, -2_147_483_648, 2_147_483_647, try js_value.toI32(context)), + 64 => { + if (js_value.isBigInt()) { + const v = js_value.castTo(v8.BigInt); + return v.getInt64(); + } + return jsSignedIntToZig(i64, -2_147_483_648, 2_147_483_647, try js_value.toI32(context)); + }, + else => {}, + }, + .unsigned => switch (n.bits) { + 8 => return jsUnsignedIntToZig(u8, 255, try js_value.toU32(context)), + 16 => return jsUnsignedIntToZig(u16, 65_535, try js_value.toU32(context)), + 32 => return jsUnsignedIntToZig(u32, 4_294_967_295, try js_value.toU32(context)), + 64 => { + if (js_value.isBigInt()) { + const v = js_value.castTo(v8.BigInt); + return v.getUint64(); + } + return jsUnsignedIntToZig(u64, 4_294_967_295, try js_value.toU32(context)); + }, + else => {}, + }, + } + @compileError("Only i8, i16, i32, i64, u8, u16, u32 and u64 are supported"); + } + + fn jsSignedIntToZig(comptime T: type, comptime min: comptime_int, max: comptime_int, maybe: i32) !T { + if (maybe >= min and maybe <= max) { + return @intCast(maybe); + } + return error.InvalidArgument; + } + + fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T { + if (maybe <= max) { + return @intCast(maybe); + } + return error.InvalidArgument; + } + + fn zigValueToJs(self: *const Self, value: anytype) !v8.Value { + return self.executor.zigValueToJs(value); + } + + fn isState(comptime T: type) bool { + const ti = @typeInfo(State); + const Const_State = if (ti == .pointer) *const ti.pointer.child else State; + return T == State or T == Const_State; + } + + fn isJsObject(comptime T: type) bool { + return @typeInfo(T) == .@"struct" and @hasDecl(T, "_JSOBJECT_ID_KLUDGE"); + } + }; +} + +// These are simple types that we can convert to JS with only an isolate. This +// is separated from the Caller's zigValueToJs to make it available when we +// don't have a caller (i.e., when setting static attributes on types) +fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool) if (fail) v8.Value else ?v8.Value { + switch (@typeInfo(@TypeOf(value))) { + .void => return v8.initUndefined(isolate).toValue(), + .bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)), + .int => |n| switch (n.signedness) { + .signed => { + if (value >= -2_147_483_648 and value <= 2_147_483_647) { + return v8.Integer.initI32(isolate, @intCast(value)).toValue(); + } + if (comptime n.bits <= 64) { + return v8.getValue(v8.BigInt.initI64(isolate, @intCast(value))); + } + @compileError(@typeName(value) ++ " is not supported"); + }, + .unsigned => { + if (value <= 4_294_967_295) { + return v8.Integer.initU32(isolate, @intCast(value)).toValue(); + } + if (comptime n.bits <= 64) { + return v8.getValue(v8.BigInt.initU64(isolate, @intCast(value))); + } + @compileError(@typeName(value) ++ " is not supported"); + }, + }, + .comptime_int => { + if (value >= 0) { + if (value <= 4_294_967_295) { + return v8.Integer.initU32(isolate, @intCast(value)).toValue(); + } + return v8.BigInt.initU64(isolate, @intCast(value)).toValue(); + } + if (value >= -2_147_483_648) { + return v8.Integer.initI32(isolate, @intCast(value)).toValue(); + } + return v8.BigInt.initI64(isolate, @intCast(value)).toValue(); + }, + .comptime_float => return v8.Number.init(isolate, value).toValue(), + .float => |f| switch (f.bits) { + 64 => return v8.Number.init(isolate, value).toValue(), + 32 => return v8.Number.init(isolate, @floatCast(value)).toValue(), + else => @compileError(@typeName(value) ++ " is not supported"), + }, + .pointer => |ptr| { + if (ptr.size == .slice and ptr.child == u8) { + return v8.String.initUtf8(isolate, value).toValue(); + } + if (ptr.size == .one) { + const one_info = @typeInfo(ptr.child); + if (one_info == .array and one_info.array.child == u8) { + return v8.String.initUtf8(isolate, value).toValue(); + } + } + }, + .array => return simpleZigValueToJs(isolate, &value, fail), + .optional => { + if (value) |v| { + return simpleZigValueToJs(isolate, v, fail); + } + return v8.initNull(isolate).toValue(); + }, + .@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail), + else => {}, + } + if (fail) { + @compileError("Unsupported Zig type " ++ @typeName(@TypeOf(value))); + } + return null; +} + +pub fn zigJsonToJs(isolate: v8.Isolate, context: v8.Context, value: std.json.Value) !v8.Value { + switch (value) { + .bool => |v| return simpleZigValueToJs(isolate, v, true), + .float => |v| return simpleZigValueToJs(isolate, v, true), + .integer => |v| return simpleZigValueToJs(isolate, v, true), + .string => |v| return simpleZigValueToJs(isolate, v, true), + .null => return isolate.initNull().toValue(), + + // TODO handle number_string. + // It is used to represent too big numbers. + .number_string => return error.TODO, + + .array => |v| { + const a = v8.Array.init(isolate, @intCast(v.items.len)); + const obj = a.castTo(v8.Object); + for (v.items, 0..) |array_value, i| { + const js_val = try zigJsonToJs(isolate, context, array_value); + if (!obj.setValueAtIndex(context, @intCast(i), js_val)) { + return error.JSObjectSetValue; + } + } + return obj.toValue(); + }, + .object => |v| { + var obj = v8.Object.init(isolate); + var it = v.iterator(); + while (it.next()) |kv| { + const js_key = v8.String.initUtf8(isolate, kv.key_ptr.*); + const js_val = try zigJsonToJs(isolate, context, kv.value_ptr.*); + if (!obj.setValue(context, js_key, js_val)) { + return error.JSObjectSetValue; + } + } + return obj.toValue(); + }, + } +} + +// Takes a function, and returns a tuple for its argument. Used when we +// @call a function +fn ParamterTypes(comptime F: type) type { + const params = @typeInfo(F).@"fn".params; + var fields: [params.len]std.builtin.Type.StructField = undefined; + + inline for (params, 0..) |param, i| { + fields[i] = .{ + .name = tupleFieldName(i), + .type = param.type.?, + .default_value_ptr = null, + .is_comptime = false, + .alignment = @alignOf(param.type.?), + }; + } + + return @Type(.{ .@"struct" = .{ + .layout = .auto, + .decls = &.{}, + .fields = &fields, + .is_tuple = true, + } }); +} + +fn tupleFieldName(comptime i: usize) [:0]const u8 { + return std.fmt.comptimePrint("{d}", .{i}); +} + +fn createException(isolate: v8.Isolate, msg: []const u8) v8.Value { + return v8.Exception.initError(v8.String.initUtf8(isolate, msg)); +} + +fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value { + return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg)); +} + +fn classNameForStruct(comptime Struct: type) []const u8 { + if (@hasDecl(Struct, "js_name")) { + return Struct.js_name; + } + @setEvalBranchQuota(10_000); + const full_name = @typeName(Struct); + const last = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse return full_name; + return full_name[last + 1 ..]; +} + +// When we return a Zig object to V8, we put it on the heap and pass it into +// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a +// function parameter, we know what type it _should_ be. Above, in Caller.method +// (for example), we know all the parameter types. So if a Zig function takes +// a single parameter (its receiver), we know what that type is. +// +// In a simple/perfect world, we could use this knowledge to cast the *anyopaque +// to the parameter type: +// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data); +// +// But there are 2 reasons we can't do that. +// +// == Reason 1 == +// The JS code might pass the wrong type: +// +// var cat = new Cat(); +// cat.setOwner(new Cat()); +// +// The zig _setOwner method expects the 2nd parameter to be an *Owner, but +// the JS code passed a *Cat. +// +// To solve this issue, we tag every returned value so that we can check what +// type it is. In the above case, we'd expect an *Owner, but the tag would tell +// us that we got a *Cat. We use the type index in our Types lookup as the tag. +// +// == Reason 2 == +// Because of prototype inheritance, even "correct" code can be a challenge. For +// example, say the above JavaScript is fixed: +// +// var cat = new Cat(); +// cat.setOwner(new Owner("Leto")); +// +// The issue is that setOwner might not expect an *Owner, but rather a +// *Person, which is the prototype for Owner. Now our Zig code is expecting +// a *Person, but it was (correctly) given an *Owner. +// For this reason, we also store the prototype's type index. +// +// One of the prototype mechanisms that we support is via composition. Owner +// can have a "proto: *Person" field. For this reason, we also store the offset +// of the proto field, so that, given an intFromPtr(*Owner) we can access its +// proto field. +// +// The other prototype mechanism that we support is for netsurf, where we just +// cast one type to another. In this case, we'll store an offset of -1 (as a +// sentinel to indicate that we should just cast directly). +const TaggedAnyOpaque = struct { + // The type of object this is. The type is captured as an index, which + // corresponds to both a field in TYPE_LOOKUP and the index of + // PROTOTYPE_TABLE + index: u16, + + // If this type has composition-based prototype, represents the byte-offset + // from ptr where the `proto` field is located. The value -1 represents + // unsafe prototype where we can just cast ptr to the destination type + // (this is used extensively with netsurf) + offset: i32, + + // Ptr to the Zig instance. Between the context where it's called (i.e. + // we have the comptime parameter info for all functions), and the index field + // we can figure out what type this is. + ptr: *anyopaque, + + // When we're asked to describe an object via the Inspector, we _must_ include + // the proper subtype (and description) fields in the returned JSON. + // V8 will give us a Value and ask us for the subtype. From the v8.Value we + // can get a v8.Object, and from the v8.Object, we can get out TaggedAnyOpaque + // which is where we store the subtype. + sub_type: ?[*c]const u8, +}; + +fn valueToString(allocator: Allocator, value: v8.Value, isolate: v8.Isolate, context: v8.Context) ![]u8 { + const str = try value.toString(context); + const len = str.lenUtf8(isolate); + const buf = try allocator.alloc(u8, len); + const n = str.writeUtf8(isolate, buf); + std.debug.assert(n == len); + return buf; +} + +const NoopInspector = struct { + pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {} + pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {} +}; + +// If we have a struct: +// const Cat = struct { +// pub fn meow(self: *Cat) void { ... } +// } +// Then obviously, the receiver of its methods are going to be a *Cat (or *const Cat) +// +// However, we can also do: +// const Cat = struct { +// pub const Self = OtherImpl; +// pub fn meow(self: *OtherImpl) void { ... } +// } +// In which case, as we see above, the receiver is derived from the Self declaration +fn Receiver(comptime S: type) type { + return if (@hasDecl(S, "Self")) S.Self else S; +} + +// We want the function name, or more precisely, the "Struct.function" for +// displaying helpful @compileError. +// However, there's no way to get the name from a std.Builtin.Fn, +// so we capture it early and mostly pass around this NamedFunction instance +// whenever we're trying to bind a function/getter/setter/etc so that we always +// have the main data (struct + function) along with the meta data for displaying +// better errors. +fn NamedFunction(comptime S: type, comptime function: anytype, comptime name: []const u8) type { + const full_name = @typeName(S) ++ "." ++ name; + const js_name = if (name[0] == '_') name[1..] else name; + return struct { + S: type = S, + full_name: []const u8 = full_name, + func: @TypeOf(function) = function, + js_name: []const u8 = js_name, + }; +} + +// This is called from V8. Whenever the v8 inspector has to describe a value +// it'll call this function to gets its [optional] subtype - which, from V8's +// point of view, is an arbitrary string. +pub export fn v8_inspector__Client__IMPL__valueSubtype( + _: *v8.c.InspectorClientImpl, + c_value: *const v8.C_Value, +) callconv(.C) [*c]const u8 { + const external_entry = getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null; + return if (external_entry.sub_type) |st| st else null; +} + +// Same as valueSubType above, but for the optional description field. +// From what I can tell, some drivers _need_ the description field to be +// present, even if it's empty. So if we have a subType for the value, we'll +// put an empty description. +pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype( + _: *v8.c.InspectorClientImpl, + context: *const v8.C_Context, + c_value: *const v8.C_Value, +) callconv(.C) [*c]const u8 { + _ = context; + + // We _must_ include a non-null description in order for the subtype value + // to be included. Besides that, I don't know if the value has any meaning + const external_entry = getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null; + return if (external_entry.sub_type == null) null else ""; +} + +fn getTaggedAnyOpaque(value: v8.Value) ?*TaggedAnyOpaque { + if (value.isObject() == false) { + return null; + } + const obj = value.castTo(v8.Object); + if (obj.internalFieldCount() == 0) { + return null; + } + + const external_data = obj.getInternalField(0).castTo(v8.External).get().?; + return @alignCast(@ptrCast(external_data)); +} + +test { + std.testing.refAllDecls(@import("test_primitive_types.zig")); + std.testing.refAllDecls(@import("test_complex_types.zig")); + std.testing.refAllDecls(@import("test_object_types.zig")); +} diff --git a/src/runtime/loop.zig b/src/runtime/loop.zig new file mode 100644 index 00000000..797b371e --- /dev/null +++ b/src/runtime/loop.zig @@ -0,0 +1,469 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const builtin = @import("builtin"); +const MemoryPool = std.heap.MemoryPool; + +pub const IO = @import("tigerbeetle-io").IO; + +const JSCallback = @import("../browser/env.zig").Env.Callback; + +const log = std.log.scoped(.loop); + +// SingleThreaded I/O Loop based on Tigerbeetle io_uring loop. +// On Linux it's using io_uring. +// On MacOS and Windows it's using kqueue/IOCP with a ring design. +// This is a thread-unsafe version without any lock on shared resources, +// use it only on a single thread. +// The loop provides I/O APIs based on callbacks. +// I/O APIs based on async/await might be added in the future. +pub const Loop = struct { + alloc: std.mem.Allocator, // TODO: unmanaged version ? + io: IO, + + // both events_nb are used to track how many callbacks are to be called. + // We use these counters to wait until all the events are finished. + js_events_nb: usize, + zig_events_nb: usize, + + cbk_error: bool = false, + + // js_ctx_id is incremented each time the loop is reset for JS. + // All JS callbacks store an initial js_ctx_id and compare before execution. + // If a ctx is outdated, the callback is ignored. + // This is a weak way to cancel all future JS callbacks. + js_ctx_id: u32 = 0, + + // zig_ctx_id is incremented each time the loop is reset for Zig. + // All Zig callbacks store an initial zig_ctx_id and compare before execution. + // If a ctx is outdated, the callback is ignored. + // This is a weak way to cancel all future Zig callbacks. + zig_ctx_id: u32 = 0, + + // The MacOS event loop doesn't support cancellation. We use this to track + // cancellation ids and, on the timeout callback, we can can check here + // to see if it's been cancelled. + cancelled: std.AutoHashMapUnmanaged(usize, void), + + cancel_pool: MemoryPool(ContextCancel), + timeout_pool: MemoryPool(ContextTimeout), + event_callback_pool: MemoryPool(EventCallbackContext), + + const Self = @This(); + pub const Completion = IO.Completion; + + pub const ConnectError = IO.ConnectError; + pub const RecvError = IO.RecvError; + pub const SendError = IO.SendError; + + pub fn init(alloc: std.mem.Allocator) !Self { + return Self{ + .alloc = alloc, + .cancelled = .{}, + .io = try IO.init(32, 0), + .js_events_nb = 0, + .zig_events_nb = 0, + .cancel_pool = MemoryPool(ContextCancel).init(alloc), + .timeout_pool = MemoryPool(ContextTimeout).init(alloc), + .event_callback_pool = MemoryPool(EventCallbackContext).init(alloc), + }; + } + + pub fn deinit(self: *Self) void { + // first disable callbacks for existing events. + // We don't want a callback re-create a setTimeout, it could create an + // infinite loop on wait for events. + self.resetJS(); + self.resetZig(); + + // run tail events. We do run the tail events to ensure all the + // contexts are correcly free. + while (self.eventsNb(.js) > 0 or self.eventsNb(.zig) > 0) { + self.io.run_for_ns(10 * std.time.ns_per_ms) catch |err| { + log.err("deinit run tail events: {any}", .{err}); + break; + }; + } + if (comptime CANCEL_SUPPORTED) { + self.io.cancel_all(); + } + self.io.deinit(); + self.cancel_pool.deinit(); + self.timeout_pool.deinit(); + self.event_callback_pool.deinit(); + self.cancelled.deinit(self.alloc); + } + + // Retrieve all registred I/O events completed by OS kernel, + // and execute sequentially their callbacks. + // Stops when there is no more I/O events registered on the loop. + // Note that I/O events callbacks might register more I/O events + // on the go when they are executed (ie. nested I/O events). + pub fn run(self: *Self) !void { + while (self.eventsNb(.js) > 0) { + try self.io.run_for_ns(10 * std.time.ns_per_ms); + // at each iteration we might have new events registred by previous callbacks + } + // TODO: return instead immediatly on the first JS callback error + // and let the caller decide what to do next + // (typically retrieve the exception through the TryCatch and + // continue the execution of callbacks with a new call to loop.run) + if (self.cbk_error) { + return error.JSExecCallback; + } + } + + const Event = enum { js, zig }; + + fn eventsPtr(self: *Self, comptime event: Event) *usize { + return switch (event) { + .zig => &self.zig_events_nb, + .js => &self.js_events_nb, + }; + } + + // Register events atomically + // - add 1 event and return previous value + fn addEvent(self: *Self, comptime event: Event) void { + _ = @atomicRmw(usize, self.eventsPtr(event), .Add, 1, .acq_rel); + } + // - remove 1 event and return previous value + fn removeEvent(self: *Self, comptime event: Event) void { + _ = @atomicRmw(usize, self.eventsPtr(event), .Sub, 1, .acq_rel); + } + // - get the number of current events + fn eventsNb(self: *Self, comptime event: Event) usize { + return @atomicLoad(usize, self.eventsPtr(event), .seq_cst); + } + + // JS callbacks APIs + // ----------------- + + // Timeout + + const ContextTimeout = struct { + loop: *Self, + js_cbk: ?JSCallback, + js_ctx_id: u32, + }; + + fn timeoutCallback( + ctx: *ContextTimeout, + completion: *IO.Completion, + result: IO.TimeoutError!void, + ) void { + const loop = ctx.loop; + defer { + loop.removeEvent(.js); + loop.timeout_pool.destroy(ctx); + loop.alloc.destroy(completion); + } + + if (comptime CANCEL_SUPPORTED == false) { + if (loop.cancelled.remove(@intFromPtr(completion))) { + return; + } + } + + // If the loop's context id has changed, don't call the js callback + // function. The callback's memory has already be cleaned and the + // events nb reset. + if (ctx.js_ctx_id != loop.js_ctx_id) return; + + // TODO: return the error to the callback + result catch |err| { + switch (err) { + error.Canceled => {}, + else => log.err("timeout callback: {any}", .{err}), + } + return; + }; + + // js callback + if (ctx.js_cbk) |*js_cbk| { + js_cbk.call(null) catch { + loop.cbk_error = true; + }; + } + } + + pub fn timeout(self: *Self, nanoseconds: u63, js_cbk: ?JSCallback) !usize { + const completion = try self.alloc.create(Completion); + errdefer self.alloc.destroy(completion); + completion.* = undefined; + + const ctx = try self.timeout_pool.create(); + errdefer self.timeout_pool.destroy(ctx); + ctx.* = ContextTimeout{ + .loop = self, + .js_cbk = js_cbk, + .js_ctx_id = self.js_ctx_id, + }; + + self.addEvent(.js); + self.io.timeout(*ContextTimeout, ctx, timeoutCallback, completion, nanoseconds); + return @intFromPtr(completion); + } + + const ContextCancel = struct { + loop: *Self, + js_cbk: ?JSCallback, + js_ctx_id: u32, + }; + + fn cancelCallback( + ctx: *ContextCancel, + completion: *IO.Completion, + result: IO.CancelOneError!void, + ) void { + const loop = ctx.loop; + + defer { + loop.removeEvent(.js); + loop.cancel_pool.destroy(ctx); + loop.alloc.destroy(completion); + } + + // If the loop's context id has changed, don't call the js callback + // function. The callback's memory has already be cleaned and the + // events nb reset. + if (ctx.js_ctx_id != loop.js_ctx_id) return; + + // TODO: return the error to the callback + result catch |err| { + switch (err) { + error.NotFound => log.debug("cancel callback: {any}", .{err}), + else => log.err("cancel callback: {any}", .{err}), + } + return; + }; + + // js callback + if (ctx.js_cbk) |*js_cbk| { + js_cbk.call(null) catch { + loop.cbk_error = true; + }; + } + } + + pub fn cancel(self: *Self, id: usize, js_cbk: ?JSCallback) !void { + const alloc = self.alloc; + if (comptime CANCEL_SUPPORTED == false) { + try self.cancelled.put(alloc, id, {}); + if (js_cbk) |cbk| { + cbk.call(null) catch { + self.cbk_error = true; + }; + } + return; + } + const comp_cancel: *IO.Completion = @ptrFromInt(id); + + const completion = try alloc.create(Completion); + errdefer alloc.destroy(completion); + completion.* = undefined; + + const ctx = self.alloc.create(ContextCancel) catch unreachable; + ctx.* = ContextCancel{ + .loop = self, + .js_cbk = js_cbk, + .js_ctx_id = self.js_ctx_id, + }; + + self.addEvent(.js); + self.io.cancel_one(*ContextCancel, ctx, cancelCallback, completion, comp_cancel); + } + + // Reset all existing JS callbacks. + // The existing events will happen and their memory will be cleanup but the + // corresponding callbacks will not be called. + pub fn resetJS(self: *Self) void { + self.js_ctx_id += 1; + self.cancelled.clearRetainingCapacity(); + } + + // Reset all existing Zig callbacks. + // The existing events will happen and their memory will be cleanup but the + // corresponding callbacks will not be called. + pub fn resetZig(self: *Self) void { + self.zig_ctx_id += 1; + } + + // IO callbacks APIs + // ----------------- + + // Connect + + pub fn connect( + self: *Self, + comptime Ctx: type, + ctx: *Ctx, + completion: *Completion, + comptime cbk: fn (ctx: *Ctx, _: *Completion, res: ConnectError!void) void, + socket: std.posix.socket_t, + address: std.net.Address, + ) !void { + const onConnect = struct { + fn onConnect(callback: *EventCallbackContext, completion_: *Completion, res: ConnectError!void) void { + defer callback.loop.event_callback_pool.destroy(callback); + callback.loop.removeEvent(.js); + cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res); + } + }.onConnect; + + const callback = try self.event_callback_pool.create(); + errdefer self.event_callback_pool.destroy(callback); + callback.* = .{ .loop = self, .ctx = ctx }; + + self.addEvent(.js); + self.io.connect(*EventCallbackContext, callback, onConnect, completion, socket, address); + } + + // Send + + pub fn send( + self: *Self, + comptime Ctx: type, + ctx: *Ctx, + completion: *Completion, + comptime cbk: fn (ctx: *Ctx, completion: *Completion, res: SendError!usize) void, + socket: std.posix.socket_t, + buf: []const u8, + ) !void { + const onSend = struct { + fn onSend(callback: *EventCallbackContext, completion_: *Completion, res: SendError!usize) void { + defer callback.loop.event_callback_pool.destroy(callback); + callback.loop.removeEvent(.js); + cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res); + } + }.onSend; + + const callback = try self.event_callback_pool.create(); + errdefer self.event_callback_pool.destroy(callback); + callback.* = .{ .loop = self, .ctx = ctx }; + + self.addEvent(.js); + self.io.send(*EventCallbackContext, callback, onSend, completion, socket, buf); + } + + // Recv + + pub fn recv( + self: *Self, + comptime Ctx: type, + ctx: *Ctx, + completion: *Completion, + comptime cbk: fn (ctx: *Ctx, completion: *Completion, res: RecvError!usize) void, + socket: std.posix.socket_t, + buf: []u8, + ) !void { + const onRecv = struct { + fn onRecv(callback: *EventCallbackContext, completion_: *Completion, res: RecvError!usize) void { + defer callback.loop.event_callback_pool.destroy(callback); + callback.loop.removeEvent(.js); + cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res); + } + }.onRecv; + + const callback = try self.event_callback_pool.create(); + errdefer self.event_callback_pool.destroy(callback); + callback.* = .{ .loop = self, .ctx = ctx }; + + self.addEvent(.js); + self.io.recv(*EventCallbackContext, callback, onRecv, completion, socket, buf); + } + + // Zig timeout + + const ContextZigTimeout = struct { + loop: *Self, + zig_ctx_id: u32, + + context: *anyopaque, + callback: *const fn ( + context: ?*anyopaque, + ) void, + }; + + fn zigTimeoutCallback( + ctx: *ContextZigTimeout, + completion: *IO.Completion, + result: IO.TimeoutError!void, + ) void { + const loop = ctx.loop; + defer { + loop.removeEvent(.zig); + loop.alloc.destroy(ctx); + loop.alloc.destroy(completion); + } + + // If the loop's context id has changed, don't call the js callback + // function. The callback's memory has already be cleaned and the + // events nb reset. + if (ctx.zig_ctx_id != loop.zig_ctx_id) return; + + result catch |err| { + switch (err) { + error.Canceled => {}, + else => log.err("zig timeout callback: {any}", .{err}), + } + return; + }; + + // callback + ctx.callback(ctx.context); + } + + // zigTimeout performs a timeout but the callback is a zig function. + pub fn zigTimeout( + self: *Self, + nanoseconds: u63, + comptime Context: type, + context: Context, + comptime callback: fn (context: Context) void, + ) void { + const completion = self.alloc.create(IO.Completion) catch unreachable; + completion.* = undefined; + const ctxtimeout = self.alloc.create(ContextZigTimeout) catch unreachable; + ctxtimeout.* = ContextZigTimeout{ + .loop = self, + .zig_ctx_id = self.zig_ctx_id, + .context = context, + .callback = struct { + fn wrapper(ctx: ?*anyopaque) void { + callback(@ptrCast(@alignCast(ctx))); + } + }.wrapper, + }; + + self.addEvent(.zig); + self.io.timeout(*ContextZigTimeout, ctxtimeout, zigTimeoutCallback, completion, nanoseconds); + } +}; + +const EventCallbackContext = struct { + ctx: *anyopaque, + loop: *Loop, +}; + +const CANCEL_SUPPORTED = switch (builtin.target.os.tag) { + .linux => true, + .macos, .tvos, .watchos, .ios => false, + else => @compileError("IO is not supported for platform"), +}; diff --git a/src/runtime/test_complex_types.zig b/src/runtime/test_complex_types.zig new file mode 100644 index 00000000..9794ad2b --- /dev/null +++ b/src/runtime/test_complex_types.zig @@ -0,0 +1,260 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const MyList = struct { + items: []u8, + + pub fn constructor(elem1: u8, elem2: u8, elem3: u8, state: State) MyList { + var items = state.arena.alloc(u8, 3) catch unreachable; + items[0] = elem1; + items[1] = elem2; + items[2] = elem3; + return .{ .items = items }; + } + + pub fn _first(self: *const MyList) u8 { + return self.items[0]; + } + + pub fn _symbol_iterator(self: *const MyList) IterableU8 { + return IterableU8.init(self.items); + } +}; + +const MyVariadic = struct { + member: u8, + + pub fn constructor() MyVariadic { + return .{ .member = 0 }; + } + + pub fn _len(_: *const MyVariadic, variadic: []bool) u64 { + return @as(u64, variadic.len); + } + + pub fn _first(_: *const MyVariadic, _: []const u8, variadic: []bool) bool { + return variadic[0]; + } + + pub fn _last(_: *const MyVariadic, variadic: []bool) bool { + return variadic[variadic.len - 1]; + } + + pub fn _empty(_: *const MyVariadic, _: []bool) bool { + return true; + } + + pub fn _myListLen(_: *const MyVariadic, variadic: []*const MyList) u8 { + return @as(u8, @intCast(variadic.len)); + } + + pub fn _myListFirst(_: *const MyVariadic, variadic: []*const MyList) ?u8 { + if (variadic.len == 0) return null; + return variadic[0]._first(); + } +}; + +const MyErrorUnion = struct { + pub fn constructor(is_err: bool) !MyErrorUnion { + if (is_err) return error.MyError; + return .{}; + } + + pub fn get_withoutError(_: *const MyErrorUnion) !u8 { + return 0; + } + + pub fn get_withError(_: *const MyErrorUnion) !u8 { + return error.MyError; + } + + pub fn set_withoutError(_: *const MyErrorUnion, _: bool) !void {} + + pub fn set_withError(_: *const MyErrorUnion, _: bool) !void { + return error.MyError; + } + + pub fn _funcWithoutError(_: *const MyErrorUnion) !void {} + + pub fn _funcWithError(_: *const MyErrorUnion) !void { + return error.MyError; + } +}; + +pub const MyException = struct { + err: ErrorSet, + + const errorNames = [_][]const u8{ + "MyCustomError", + }; + const errorMsgs = [_][]const u8{ + "Some custom message.", + }; + fn errorStrings(comptime i: usize) []const u8 { + return errorNames[0] ++ ": " ++ errorMsgs[i]; + } + + // interface definition + + pub const ErrorSet = error{ + MyCustomError, + }; + + pub fn init(_: Allocator, err: anyerror, _: []const u8) !MyException { + return .{ .err = @as(ErrorSet, @errorCast(err)) }; + } + + pub fn get_name(self: *const MyException) []const u8 { + return switch (self.err) { + ErrorSet.MyCustomError => errorNames[0], + }; + } + + pub fn get_message(self: *const MyException) []const u8 { + return switch (self.err) { + ErrorSet.MyCustomError => errorMsgs[0], + }; + } + + pub fn _toString(self: *const MyException) []const u8 { + return switch (self.err) { + ErrorSet.MyCustomError => errorStrings(0), + }; + } +}; + +const MyTypeWithException = struct { + pub const Exception = MyException; + + pub fn constructor() MyTypeWithException { + return .{}; + } + + pub fn _withoutError(_: *const MyTypeWithException) MyException.ErrorSet!void {} + + pub fn _withError(_: *const MyTypeWithException) MyException.ErrorSet!void { + return MyException.ErrorSet.MyCustomError; + } + + pub fn _superSetError(_: *const MyTypeWithException) !void { + return MyException.ErrorSet.MyCustomError; + } + + pub fn _outOfMemory(_: *const MyTypeWithException) !void { + return error.OutOfMemory; + } +}; + +const IterableU8 = Iterable(u8); + +pub fn Iterable(comptime T: type) type { + return struct { + const Self = @This(); + + items: []T, + index: usize = 0, + + pub fn init(items: []T) Self { + return .{ .items = items }; + } + + pub const Return = struct { + value: ?T, + done: bool, + }; + + pub fn _next(self: *Self) Return { + if (self.items.len > self.index) { + const val = self.items[self.index]; + self.index += 1; + return .{ .value = val, .done = false }; + } else { + return .{ .value = null, .done = true }; + } + } + }; +} + +const State = struct { + arena: Allocator, +}; + +const testing = @import("testing.zig"); +test "JS: complex types" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + + var runner = try testing.Runner(State, void, .{ + MyList, + IterableU8, + MyVariadic, + MyErrorUnion, + MyException, + MyTypeWithException, + }).init(.{ .arena = arena.allocator() }, {}); + + defer runner.deinit(); + + try runner.testCases(&.{ + .{ "let myList = new MyList(1, 2, 3);", "undefined" }, + .{ "myList.first();", "1" }, + .{ "let iter = myList[Symbol.iterator]();", "undefined" }, + .{ "iter.next().value;", "1" }, + .{ "iter.next().value;", "2" }, + .{ "iter.next().value;", "3" }, + .{ "iter.next().done;", "true" }, + .{ "let arr = Array.from(myList);", "undefined" }, + .{ "arr.length;", "3" }, + .{ "arr[0];", "1" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let myVariadic = new MyVariadic();", "undefined" }, + .{ "myVariadic.len(true, false, true)", "3" }, + .{ "myVariadic.first('a_str', true, false, true, false)", "true" }, + .{ "myVariadic.last(true, false)", "false" }, + .{ "myVariadic.empty()", "true" }, + .{ "myVariadic.myListLen(myList)", "1" }, + .{ "myVariadic.myListFirst(myList)", "1" }, + }, .{}); + + try runner.testCases(&.{ + .{ "var myErrorCstr = ''; try {new MyErrorUnion(true)} catch (error) {myErrorCstr = error}; myErrorCstr", "Error: MyError" }, + .{ "let myErrorUnion = new MyErrorUnion(false);", "undefined" }, + .{ "myErrorUnion.withoutError", "0" }, + .{ "var myErrorGetter = ''; try {myErrorUnion.withError} catch (error) {myErrorGetter = error}; myErrorGetter", "Error: MyError" }, + .{ "myErrorUnion.withoutError = true", "true" }, + .{ "var myErrorSetter = ''; try {myErrorUnion.withError = true} catch (error) {myErrorSetter = error}; myErrorSetter", "Error: MyError" }, + .{ "myErrorUnion.funcWithoutError()", "undefined" }, + .{ "var myErrorFunc = ''; try {myErrorUnion.funcWithError()} catch (error) {myErrorFunc = error}; myErrorFunc", "Error: MyError" }, + }, .{}); + + try runner.testCases(&.{ + .{ "MyException.prototype.__proto__ === Error.prototype", "true" }, + .{ "let myTypeWithException = new MyTypeWithException();", "undefined" }, + .{ "myTypeWithException.withoutError()", "undefined" }, + .{ "var myCustomError = ''; try {myTypeWithException.withError()} catch (error) {myCustomError = error}", "MyCustomError: Some custom message." }, + .{ "myCustomError instanceof MyException", "true" }, + .{ "myCustomError instanceof Error", "true" }, + .{ "var mySuperError = ''; try {myTypeWithException.superSetError()} catch (error) {mySuperError = error}", "MyCustomError: Some custom message." }, + .{ "var oomError = ''; try {myTypeWithException.outOfMemory()} catch (error) {oomError = error}; oomError", "Error: out of memory" }, + }, .{}); +} diff --git a/src/runtime/test_object_types.zig b/src/runtime/test_object_types.zig new file mode 100644 index 00000000..b3594dc0 --- /dev/null +++ b/src/runtime/test_object_types.zig @@ -0,0 +1,123 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +pub const Other = struct { + val: u8, + + fn init(val: u8) Other { + return .{ .val = val }; + } + + pub fn _val(self: *const Other) u8 { + return self.val; + } +}; + +pub const OtherUnion = union(enum) { + Other: Other, + Bool: bool, +}; + +pub const MyObject = struct { + val: bool, + + pub fn constructor(do_set: bool) MyObject { + return .{ + .val = do_set, + }; + } + + pub fn named_get(_: *const MyObject, name: []const u8, has_value: *bool) ?OtherUnion { + if (std.mem.eql(u8, name, "a")) { + has_value.* = true; + return .{ .Other = .{ .val = 4 } }; + } + if (std.mem.eql(u8, name, "c")) { + has_value.* = true; + return .{ .Bool = true }; + } + has_value.* = false; + return null; + } + + pub fn get_val(self: *const MyObject) bool { + return self.val; + } + + pub fn set_val(self: *MyObject, val: bool) void { + self.val = val; + } +}; + +pub const MyAPI = struct { + pub fn constructor() MyAPI { + return .{}; + } + + pub fn _obj(_: *const MyAPI) !MyObject { + return MyObject.constructor(true); + } +}; + +const State = struct { + arena: Allocator, +}; + +const testing = @import("testing.zig"); +test "JS: object types" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + + var runner = try testing.Runner(State, void, .{ + Other, + MyObject, + MyAPI, + }).init(.{ .arena = arena.allocator() }, {}); + + defer runner.deinit(); + + // v8 has 5 default "own" properties + const own_base = "5"; + + try runner.testCases(&.{ + .{ "Object.getOwnPropertyNames(MyObject).length;", own_base }, + .{ "let myObj = new MyObject(true);", "undefined" }, + // check object property + .{ "myObj.a.val()", "4" }, + .{ "myObj.b", "undefined" }, + .{ "Object.getOwnPropertyNames(myObj).length;", "0" }, + + // check if setter (pointer) still works + .{ "myObj.val", "true" }, + .{ "myObj.val = false", "false" }, + .{ "myObj.val", "false" }, + + .{ "let myObj2 = new MyObject(false);", "undefined" }, + .{ "myObj2.c", "true" }, + }, .{}); + + try runner.testCases(&.{ + .{ "let myAPI = new MyAPI();", "undefined" }, + .{ "let myObjIndirect = myAPI.obj();", "undefined" }, + // check object property + .{ "myObjIndirect.a.val()", "4" }, + }, .{}); +} diff --git a/src/runtime/test_primitive_types.zig b/src/runtime/test_primitive_types.zig new file mode 100644 index 00000000..022a8c91 --- /dev/null +++ b/src/runtime/test_primitive_types.zig @@ -0,0 +1,176 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); + +// TODO: use functions instead of "fake" struct once we handle function API generation +const Primitives = struct { + pub fn constructor() Primitives { + return .{}; + } + + // List of bytes (string) + pub fn _checkString(_: *const Primitives, v: []u8) []u8 { + return v; + } + + // Integers signed + + pub fn _checkI32(_: *const Primitives, v: i32) i32 { + return v; + } + + pub fn _checkI64(_: *const Primitives, v: i64) i64 { + return v; + } + + // Integers unsigned + + pub fn _checkU32(_: *const Primitives, v: u32) u32 { + return v; + } + + pub fn _checkU64(_: *const Primitives, v: u64) u64 { + return v; + } + + // Floats + + pub fn _checkF32(_: *const Primitives, v: f32) f32 { + return v; + } + + pub fn _checkF64(_: *const Primitives, v: f64) f64 { + return v; + } + + // Bool + pub fn _checkBool(_: *const Primitives, v: bool) bool { + return v; + } + + // Undefined + // TODO: there is a bug with this function + // void paramater does not work => avoid for now + // pub fn _checkUndefined(_: *const Primitives, v: void) void { + // return v; + // } + + // Null + pub fn _checkNullEmpty(_: *const Primitives, v: ?u32) bool { + return (v == null); + } + pub fn _checkNullNotEmpty(_: *const Primitives, v: ?u32) bool { + return (v != null); + } + + // Optionals + pub fn _checkOptional(_: *const Primitives, _: ?u8, v: u8, _: ?u8, _: ?u8) u8 { + return v; + } + pub fn _checkNonOptional(_: *const Primitives, v: u8) u8 { + std.debug.print("x: {d}\n", .{v}); + return v; + } + pub fn _checkOptionalReturn(_: *const Primitives) ?bool { + return true; + } + pub fn _checkOptionalReturnNull(_: *const Primitives) ?bool { + return null; + } + pub fn _checkOptionalReturnString(_: *const Primitives) ?[]const u8 { + return "ok"; + } +}; + +const testing = @import("testing.zig"); +test "JS: primitive types" { + var runner = try testing.Runner(void, void, .{Primitives}).init({}, {}); + defer runner.deinit(); + + // constructor + try runner.testCases(&.{ + .{ "let p = new Primitives();", "undefined" }, + }, .{}); + + // JS <> Native translation of primitive types + try runner.testCases(&.{ + .{ "p.checkString('ok ascii') === 'ok ascii';", "true" }, + .{ "p.checkString('ok emoji πŸš€') === 'ok emoji πŸš€';", "true" }, + .{ "p.checkString('ok chinese 鿍') === 'ok chinese 鿍';", "true" }, + + // String (JS liberal cases) + .{ "p.checkString(1) === '1';", "true" }, + .{ "p.checkString(null) === 'null';", "true" }, + .{ "p.checkString(undefined) === 'undefined';", "true" }, + + // Integers + + // signed + .{ "const min_i32 = -2147483648", "undefined" }, + .{ "p.checkI32(min_i32) === min_i32;", "true" }, + .{ "p.checkI32(min_i32-1) === min_i32-1;", "false" }, + .{ "try { p.checkI32(9007199254740995n) } catch(e) { e instanceof TypeError; }", "true" }, + + // unsigned + .{ "const max_u32 = 4294967295", "undefined" }, + .{ "p.checkU32(max_u32) === max_u32;", "true" }, + .{ "p.checkU32(max_u32+1) === max_u32+1;", "false" }, + + // int64 (with BigInt) + .{ "const big_int = 9007199254740995n", "undefined" }, + .{ "p.checkI64(big_int) === big_int", "true" }, + .{ "p.checkU64(big_int) === big_int;", "true" }, + .{ "p.checkI64(0) === 0;", "true" }, + .{ "p.checkI64(-1) === -1;", "true" }, + .{ "p.checkU64(0) === 0;", "true" }, + + // Floats + // use round 2 decimals for float to ensure equality + .{ "const r = function(x) {return Math.round(x * 100) / 100};", "undefined" }, + .{ "const double = 10.02;", "undefined" }, + .{ "r(p.checkF32(double)) === double;", "true" }, + .{ "r(p.checkF64(double)) === double;", "true" }, + + // Bool + .{ "p.checkBool(true);", "true" }, + .{ "p.checkBool(false);", "false" }, + .{ "p.checkBool(0);", "false" }, + .{ "p.checkBool(1);", "true" }, + + // Bool (JS liberal cases) + .{ "p.checkBool(null);", "false" }, + .{ "p.checkBool(undefined);", "false" }, + + // Undefined + // see TODO on Primitives.checkUndefined + // .{ "p.checkUndefined(undefined) === undefined;", "true" }, + + // Null + .{ "p.checkNullEmpty(null);", "true" }, + .{ "p.checkNullEmpty(undefined);", "true" }, + .{ "p.checkNullNotEmpty(1);", "true" }, + + // Optional + .{ "p.checkOptional(null, 3);", "3" }, + .{ "p.checkNonOptional();", "TypeError" }, + .{ "p.checkOptionalReturn() === true;", "true" }, + .{ "p.checkOptionalReturnNull() === null;", "true" }, + .{ "p.checkOptionalReturnString() === 'ok';", "true" }, + }, .{}); +} diff --git a/src/runtime/testing.zig b/src/runtime/testing.zig new file mode 100644 index 00000000..2a09b2e8 --- /dev/null +++ b/src/runtime/testing.zig @@ -0,0 +1,104 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const js = @import("js.zig"); +const generate = @import("generate.zig"); + +pub const allocator = std.testing.allocator; + +// Very similar to the JSRunner in src/testing.zig, but it isn't tied to the +// browser.Env or the browser.SessionState +pub fn Runner(comptime State: type, comptime Global: type, comptime types: anytype) type { + const AdjustedTypes = if (Global == void) generate.Tuple(.{ types, DefaultGlobal }) else types; + const Env = js.Env(State, AdjustedTypes{}); + + return struct { + env: *Env, + executor: *Env.Executor, + + const Self = @This(); + + pub fn init(state: State, global: Global) !*Self { + const runner = try allocator.create(Self); + errdefer allocator.destroy(runner); + + runner.env = try Env.init(allocator, .{}); + errdefer runner.env.deinit(); + + const G = if (Global == void) DefaultGlobal else Global; + + runner.executor = try runner.env.startExecutor(G, state, runner); + errdefer runner.env.stopExecutor(runner.executor); + + try runner.executor.startScope(if (Global == void) &default_global else global); + return runner; + } + + pub fn deinit(self: *Self) void { + self.executor.endScope(); + self.env.stopExecutor(self.executor); + self.env.deinit(); + allocator.destroy(self); + } + + const RunOpts = struct {}; + pub const Case = std.meta.Tuple(&.{ []const u8, []const u8 }); + pub fn testCases(self: *Self, cases: []const Case, _: RunOpts) !void { + for (cases, 0..) |case, i| { + var try_catch: Env.TryCatch = undefined; + try_catch.init(self.executor); + defer try_catch.deinit(); + + const value = self.executor.exec(case.@"0", null) catch |err| { + if (try try_catch.err(allocator)) |msg| { + defer allocator.free(msg); + if (isExpectedTypeError(case.@"1", msg)) { + continue; + } + std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" }); + } + return err; + }; + + const actual = try value.toString(allocator); + defer allocator.free(actual); + if (std.mem.eql(u8, case.@"1", actual) == false) { + std.debug.print("Expected:\n{s}\n\nGot:\n{s}\n\nCase: {d}\n{s}\n", .{ case.@"1", actual, i + 1, case.@"0" }); + return error.UnexpectedResult; + } + } + } + + pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 { + _ = ctx; + _ = specifier; + return error.DummyModuleLoader; + } + }; +} + +fn isExpectedTypeError(expected: []const u8, msg: []const u8) bool { + if (!std.mem.eql(u8, expected, "TypeError")) { + return false; + } + return std.mem.startsWith(u8, msg, "TypeError: "); +} + +var default_global = DefaultGlobal{}; +const DefaultGlobal = struct {}; diff --git a/src/server.zig b/src/server.zig index a071b441..471c2cf2 100644 --- a/src/server.zig +++ b/src/server.zig @@ -25,14 +25,15 @@ const posix = std.posix; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; -const jsruntime = @import("jsruntime"); -const Completion = jsruntime.IO.Completion; -const AcceptError = jsruntime.IO.AcceptError; -const RecvError = jsruntime.IO.RecvError; -const SendError = jsruntime.IO.SendError; -const CloseError = jsruntime.IO.CloseError; -const CancelError = jsruntime.IO.CancelOneError; -const TimeoutError = jsruntime.IO.TimeoutError; +const IO = @import("runtime/loop.zig").IO; +const Completion = IO.Completion; +const AcceptError = IO.AcceptError; +const RecvError = IO.RecvError; +const SendError = IO.SendError; +const CloseError = IO.CloseError; +const CancelError = IO.CancelOneError; +const TimeoutError = IO.TimeoutError; +const Loop = @import("runtime/loop.zig").Loop; const App = @import("app.zig").App; const CDP = @import("cdp/cdp.zig").CDP; @@ -51,7 +52,7 @@ const MAX_MESSAGE_SIZE = 256 * 1024 + 14; const Server = struct { app: *App, allocator: Allocator, - loop: *jsruntime.Loop, + loop: *Loop, // internal fields listener: posix.socket_t, @@ -453,7 +454,7 @@ pub const Client = struct { }; self.mode = .websocket; - self.cdp = CDP.init(self.server.app, self); + self.cdp = try CDP.init(self.server.app, self); return self.send(arena, response); } @@ -1023,10 +1024,6 @@ pub fn run( try posix.bind(listener, &address.any, address.getOsSockLen()); try posix.listen(listener, 1); - // create v8 vm - const vm = jsruntime.VM.init(); - defer vm.deinit(); - var loop = app.loop; const allocator = app.allocator; const json_version_response = try buildJSONVersionResponse(allocator, address); @@ -1451,7 +1448,7 @@ const MockCDP = struct { allocator: Allocator = testing.allocator, - fn init(_: Allocator, client: anytype, loop: *jsruntime.Loop) MockCDP { + fn init(_: Allocator, client: anytype, loop: *Loop) MockCDP { _ = loop; _ = client; return .{}; diff --git a/src/test_runner.zig b/src/test_runner.zig index f43b5735..c7e2ad0d 100644 --- a/src/test_runner.zig +++ b/src/test_runner.zig @@ -26,10 +26,6 @@ const BORDER = "=" ** 80; // use in custom panic handler var current_test: ?[]const u8 = null; -const jsruntime = @import("jsruntime"); -pub const Types = jsruntime.reflect(@import("generate.zig").Tuple(.{}){}); -pub const UserContext = @import("user_context.zig").UserContext; - pub const std_options = std.Options{ .log_level = .warn, @@ -38,6 +34,9 @@ pub const std_options = std.Options{ .side_channels_mitigations = .none, }; +pub var js_runner_duration: usize = 0; +pub var tracking_allocator = TrackingAllocator.init(std.testing.allocator); + pub fn main() !void { var mem: [8192]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&mem); @@ -50,6 +49,19 @@ pub fn main() !void { var slowest = SlowTracker.init(allocator, 5); defer slowest.deinit(); + var args = try std.process.argsWithAllocator(allocator); + defer args.deinit(); + + // ignore the exec name. + _ = args.next(); + var json_stats = false; + while (args.next()) |arg| { + if (std.mem.eql(u8, "--json", arg)) { + json_stats = true; + continue; + } + } + var pass: usize = 0; var fail: usize = 0; var skip: usize = 0; @@ -159,6 +171,38 @@ pub fn main() !void { printer.fmt("\n", .{}); try slowest.display(printer); printer.fmt("\n", .{}); + + // TODO: at the very least, `browser` should return real stats + if (json_stats) { + const stats = tracking_allocator.stats(); + try std.json.stringify(&.{ + .{ .name = "browser", .bench = .{ + .duration = js_runner_duration, + .alloc_nb = stats.allocation_count, + .realloc_nb = stats.reallocation_count, + .alloc_size = stats.allocated_bytes, + } }, + .{ .name = "libdom", .bench = .{ + .duration = js_runner_duration, + .alloc_nb = 0, + .realloc_nb = 0, + .alloc_size = 0, + } }, + .{ .name = "v8", .bench = .{ + .duration = js_runner_duration, + .alloc_nb = 0, + .realloc_nb = 0, + .alloc_size = 0, + } }, + .{ .name = "main", .bench = .{ + .duration = js_runner_duration, + .alloc_nb = 0, + .realloc_nb = 0, + .alloc_size = 0, + } }, + }, .{ .whitespace = .indent_2 }, std.io.getStdOut().writer()); + } + std.posix.exit(if (fail == 0) 0 else 1); } @@ -335,3 +379,90 @@ fn isSetup(t: std.builtin.TestFn) bool { fn isTeardown(t: std.builtin.TestFn) bool { return std.mem.endsWith(u8, t.name, "tests:afterAll"); } + +pub const TrackingAllocator = struct { + parent_allocator: Allocator, + free_count: usize = 0, + allocated_bytes: usize = 0, + allocation_count: usize = 0, + reallocation_count: usize = 0, + + const Stats = struct { + allocated_bytes: usize, + allocation_count: usize, + reallocation_count: usize, + }; + + fn init(parent_allocator: Allocator) TrackingAllocator { + return .{ + .parent_allocator = parent_allocator, + }; + } + + pub fn stats(self: *const TrackingAllocator) Stats { + return .{ + .allocated_bytes = self.allocated_bytes, + .allocation_count = self.allocation_count, + .reallocation_count = self.reallocation_count, + }; + } + + pub fn allocator(self: *TrackingAllocator) Allocator { + return .{ .ptr = self, .vtable = &.{ + .alloc = alloc, + .resize = resize, + .free = free, + .remap = remap, + } }; + } + + fn alloc( + ctx: *anyopaque, + len: usize, + alignment: std.mem.Alignment, + return_address: usize, + ) ?[*]u8 { + const self: *TrackingAllocator = @ptrCast(@alignCast(ctx)); + const result = self.parent_allocator.rawAlloc(len, alignment, return_address); + self.allocation_count += 1; + self.allocated_bytes += len; + return result; + } + + fn resize( + ctx: *anyopaque, + old_mem: []u8, + alignment: std.mem.Alignment, + new_len: usize, + ra: usize, + ) bool { + const self: *TrackingAllocator = @ptrCast(@alignCast(ctx)); + const result = self.parent_allocator.rawResize(old_mem, alignment, new_len, ra); + self.reallocation_count += 1; // TODO: only if result is not null? + return result; + } + + fn free( + ctx: *anyopaque, + old_mem: []u8, + alignment: std.mem.Alignment, + ra: usize, + ) void { + const self: *TrackingAllocator = @ptrCast(@alignCast(ctx)); + self.parent_allocator.rawFree(old_mem, alignment, ra); + self.free_count += 1; + } + + fn remap( + ctx: *anyopaque, + memory: []u8, + alignment: std.mem.Alignment, + new_len: usize, + ret_addr: usize, + ) ?[*]u8 { + const self: *TrackingAllocator = @ptrCast(@alignCast(ctx)); + const result = self.parent_allocator.rawRemap(memory, alignment, new_len, ret_addr); + self.reallocation_count += 1; // TODO: only if result is not null? + return result; + } +}; diff --git a/src/testing.zig b/src/testing.zig index 633d9087..9dbb8f79 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -17,15 +17,15 @@ // along with this program. If not, see . const std = @import("std"); +const Allocator = std.mem.Allocator; -const parser = @import("netsurf"); pub const allocator = std.testing.allocator; pub const expectError = std.testing.expectError; pub const expectString = std.testing.expectEqualStrings; pub const expectEqualSlices = std.testing.expectEqualSlices; const App = @import("app.zig").App; -const Allocator = std.mem.Allocator; +const parser = @import("browser/netsurf.zig"); // Merged std.testing.expectEqual and std.testing.expectString // can be useful when testing fields of an anytype an you don't know @@ -217,13 +217,13 @@ pub const Document = struct { } pub fn querySelectorAll(self: *Document, selector: []const u8) ![]const *parser.Node { - const css = @import("dom/css.zig"); + const css = @import("browser/dom/css.zig"); const node_list = try css.querySelectorAll(self.arena.allocator(), self.asNode(), selector); return node_list.nodes.items; } pub fn querySelector(self: *Document, selector: []const u8) !?*parser.Node { - const css = @import("dom/css.zig"); + const css = @import("browser/dom/css.zig"); return css.querySelector(self.arena.allocator(), self.asNode(), selector); } @@ -350,3 +350,163 @@ fn isJsonValue(a: std.json.Value, b: std.json.Value) bool { }, } } + +pub const tracking_allocator = @import("root").tracking_allocator.allocator(); +pub const JsRunner = struct { + const URL = @import("url.zig").URL; + const Env = @import("browser/env.zig").Env; + const Loop = @import("runtime/loop.zig").Loop; + const HttpClient = @import("http/client.zig").Client; + const storage = @import("browser/storage/storage.zig"); + const Window = @import("browser/html/window.zig").Window; + const Renderer = @import("browser/browser.zig").Renderer; + const SessionState = @import("browser/env.zig").SessionState; + + url: URL, + env: *Env, + loop: Loop, + window: Window, + state: SessionState, + arena: Allocator, + renderer: Renderer, + http_client: HttpClient, + executor: *Env.Executor, + storage_shelf: storage.Shelf, + cookie_jar: storage.CookieJar, + + fn init(parent_allocator: Allocator, opts: RunnerOpts) !*JsRunner { + parser.deinit(); + try parser.init(); + + const aa = try parent_allocator.create(std.heap.ArenaAllocator); + aa.* = std.heap.ArenaAllocator.init(parent_allocator); + errdefer aa.deinit(); + + const arena = aa.allocator(); + const runner = try arena.create(JsRunner); + runner.arena = arena; + + runner.env = try Env.init(arena, .{}); + errdefer runner.env.deinit(); + + runner.url = try URL.parse("https://lightpanda.io/opensource-browser/", null); + + runner.renderer = Renderer.init(arena); + runner.cookie_jar = storage.CookieJar.init(arena); + runner.loop = try Loop.init(arena); + errdefer runner.loop.deinit(); + + var html = std.io.fixedBufferStream(opts.html); + const document = try parser.documentHTMLParse(html.reader(), "UTF-8"); + + runner.state = .{ + .arena = arena, + .loop = &runner.loop, + .document = document, + .url = &runner.url, + .renderer = &runner.renderer, + .cookie_jar = &runner.cookie_jar, + .http_client = &runner.http_client, + }; + + runner.window = .{}; + try runner.window.replaceDocument(document); + try runner.window.replaceLocation(.{ + .url = try runner.url.toWebApi(arena), + }); + + runner.storage_shelf = storage.Shelf.init(arena); + runner.window.setStorageShelf(&runner.storage_shelf); + + runner.http_client = try HttpClient.init(arena, 1, .{ + .tls_verify_host = false, + }); + + runner.executor = try runner.env.startExecutor(Window, &runner.state, runner); + errdefer runner.env.stopExecutor(runner.executor); + + try runner.executor.startScope(&runner.window); + return runner; + } + + pub fn deinit(self: *JsRunner) void { + self.loop.deinit(); + self.executor.endScope(); + self.env.deinit(); + self.http_client.deinit(); + self.storage_shelf.deinit(); + + const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(self.arena.ptr)); + arena.deinit(); + arena.child_allocator.destroy(arena); + } + + const RunOpts = struct {}; + pub const Case = std.meta.Tuple(&.{ []const u8, []const u8 }); + pub fn testCases(self: *JsRunner, cases: []const Case, _: RunOpts) !void { + const start = try std.time.Instant.now(); + + for (cases, 0..) |case, i| { + var try_catch: Env.TryCatch = undefined; + try_catch.init(self.executor); + defer try_catch.deinit(); + + const value = self.executor.exec(case.@"0", null) catch |err| { + if (try try_catch.err(self.arena)) |msg| { + std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" }); + } + return err; + }; + try self.loop.run(); + @import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start); + + const actual = try value.toString(self.arena); + if (std.mem.eql(u8, case.@"1", actual) == false) { + std.debug.print("Expected:\n{s}\n\nGot:\n{s}\n\nCase: {d}\n{s}\n", .{ case.@"1", actual, i + 1, case.@"0" }); + return error.UnexpectedResult; + } + } + } + + pub fn exec(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !void { + _ = try self.eval(src, name, err_msg); + } + + pub fn eval(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !Env.Value { + var try_catch: Env.TryCatch = undefined; + try_catch.init(self.executor); + defer try_catch.deinit(); + + return self.executor.exec(src, name) catch |err| { + if (try try_catch.err(self.arena)) |msg| { + err_msg.* = msg; + std.debug.print("Error running script: {s}\n", .{msg}); + } + return err; + }; + } + + pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) ![]const u8 { + _ = ctx; + _ = specifier; + return error.DummyModuleLoader; + } +}; + +const RunnerOpts = struct { + html: []const u8 = + \\
+ \\ OK + \\

+ \\ + \\

+ \\

And

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