2 Commits

Author SHA1 Message Date
nikneym
c9cd0fad40 add custom mouse event test 2025-09-10 11:27:41 +03:00
nikneym
8f96ea457f remove unknown mouse event warning 2025-09-10 11:27:20 +03:00
251 changed files with 11042 additions and 15431 deletions

View File

@@ -5,7 +5,7 @@ inputs:
zig: zig:
description: 'Zig version to install' description: 'Zig version to install'
required: false required: false
default: '0.15.2' default: '0.15.1'
arch: arch:
description: 'CPU arch used to select the v8 lib' description: 'CPU arch used to select the v8 lib'
required: false required: false
@@ -17,11 +17,11 @@ inputs:
zig-v8: zig-v8:
description: 'zig v8 version to install' description: 'zig v8 version to install'
required: false required: false
default: 'v0.1.33' default: 'v0.1.28'
v8: v8:
description: 'v8 version to install' description: 'v8 version to install'
required: false required: false
default: '14.0.365.4' default: '13.6.233.8'
cache-dir: cache-dir:
description: 'cache dir to use' description: 'cache dir to use'
required: false required: false
@@ -67,23 +67,9 @@ runs:
mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/ mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a
- name: Cache libiconv - name: libiconv
id: cache-libiconv
uses: actions/cache@v4
env:
cache-name: cache-libiconv
with:
path: ${{ inputs.cache-dir }}/libiconv
key: vendor/libiconv/libiconv-1.17
- name: download libiconv
if: ${{ steps.cache-libiconv.outputs.cache-hit != 'true' }}
shell: bash shell: bash
run: make download-libiconv run: make install-libiconv
- name: build libiconv
shell: bash
run: make build-libiconv
- name: build mimalloc - name: build mimalloc
shell: bash shell: bash

View File

@@ -108,15 +108,6 @@ jobs:
go run runner/main.go go run runner/main.go
kill `cat LPD.pid` `cat PROXY.id` kill `cat LPD.pid` `cat PROXY.id`
- name: run request interception through proxy
run: |
export PROXY_USERNAME=username PROXY_PASSWORD=password
./proxy/proxy & echo $! > PROXY.id
./lightpanda serve & echo $! > LPD.pid
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
kill `cat LPD.pid` `cat PROXY.id`
cdp-and-hyperfine-bench: cdp-and-hyperfine-bench:
name: cdp-and-hyperfine-bench name: cdp-and-hyperfine-bench
needs: zig-build-release needs: zig-build-release

View File

@@ -5,7 +5,6 @@ env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }} AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }} AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
LIGHTPANDA_DISABLE_TELEMETRY: true
on: on:
schedule: schedule:

View File

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

5
.gitmodules vendored
View File

@@ -9,7 +9,7 @@
url = https://github.com/lightpanda-io/libdom.git/ url = https://github.com/lightpanda-io/libdom.git/
[submodule "vendor/netsurf/share/netsurf-buildsystem"] [submodule "vendor/netsurf/share/netsurf-buildsystem"]
path = vendor/netsurf/share/netsurf-buildsystem path = vendor/netsurf/share/netsurf-buildsystem
url = https://github.com/lightpanda-io/netsurf-buildsystem.git url = https://source.netsurf-browser.org/buildsystem.git
[submodule "vendor/netsurf/libhubbub"] [submodule "vendor/netsurf/libhubbub"]
path = vendor/netsurf/libhubbub path = vendor/netsurf/libhubbub
url = https://github.com/lightpanda-io/libhubbub.git/ url = https://github.com/lightpanda-io/libhubbub.git/
@@ -31,6 +31,3 @@
[submodule "vendor/curl"] [submodule "vendor/curl"]
path = vendor/curl path = vendor/curl
url = https://github.com/curl/curl.git url = https://github.com/curl/curl.git
[submodule "vendor/brotli"]
path = vendor/brotli
url = https://github.com/google/brotli

View File

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

View File

@@ -96,16 +96,9 @@ wpt-summary:
@printf "\e[36mBuilding wpt...\e[0m\n" @printf "\e[36mBuilding wpt...\e[0m\n"
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;) @$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
## Test - `grep` is used to filter out the huge compile command on build ## Test
ifeq ($(OS), macos)
test: test:
@script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \ @TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
| grep --line-buffered -v "^/.*zig test -freference-trace"
else
test:
@script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' /dev/null 2>&1 \
| grep --line-buffered -v "^/.*zig test -freference-trace"
endif
## Run demo/runner end to end tests ## Run demo/runner end to end tests
end2end: end2end:
@@ -206,16 +199,14 @@ download-libiconv:
ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","") ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","")
@mkdir -p vendor/libiconv @mkdir -p vendor/libiconv
@cd vendor/libiconv && \ @cd vendor/libiconv && \
curl -L https://github.com/lightpanda-io/libiconv/releases/download/1.17/libiconv-1.17.tar.gz | tar -xvzf - curl https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.17.tar.gz | tar -xvzf -
endif endif
build-libiconv: clean-libiconv install-libiconv: download-libiconv clean-libiconv
@cd vendor/libiconv/libiconv-1.17 && \ @cd vendor/libiconv/libiconv-1.17 && \
./configure --prefix=$(ICONV) --enable-static && \ ./configure --prefix=$(ICONV) --enable-static && \
make && make install make && make install
install-libiconv: download-libiconv build-libiconv
clean-libiconv: clean-libiconv:
ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","") ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
@cd vendor/libiconv/libiconv-1.17 && \ @cd vendor/libiconv/libiconv-1.17 && \

View File

@@ -18,7 +18,7 @@ Lightpanda is the open-source browser made for headless usage:
- Javascript execution - Javascript execution
- Support of Web APIs (partial, WIP) - Support of Web APIs (partial, WIP)
- Compatible with Playwright[^1], Puppeteer, chromedp through [CDP](https://chromedevtools.github.io/devtools-protocol/) - Compatible with Playwright[^1], Puppeteer, chromedp through CDP
Fast web automation for AI agents, LLM training, scraping and testing: Fast web automation for AI agents, LLM training, scraping and testing:
@@ -164,7 +164,7 @@ You can also follow the progress of our Javascript support in our dedicated [zig
### Prerequisites ### Prerequisites
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to
install it with the right version in order to build the project. install it with the right version in order to build the project.
Lightpanda also depends on Lightpanda also depends on
@@ -190,10 +190,10 @@ For systems with [Nix](https://nixos.org/download/), you can use the devShell:
nix develop nix develop
``` ```
For MacOS, you need [Xcode](https://developer.apple.com/xcode/) and the following pacakges from homebrew: For MacOS, you only need cmake:
``` ```
brew install cmake pkgconf brew install cmake
``` ```
### Install and build dependencies ### Install and build dependencies

View File

@@ -23,7 +23,7 @@ const Build = std.Build;
/// Do not rename this constant. It is scanned by some scripts to determine /// Do not rename this constant. It is scanned by some scripts to determine
/// which zig version to install. /// which zig version to install.
const recommended_zig_version = "0.15.2"; const recommended_zig_version = "0.15.1";
pub fn build(b: *Build) !void { pub fn build(b: *Build) !void {
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) { switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
@@ -245,7 +245,6 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
mod.addCMacro("HAVE_ASSERT_H", "1"); mod.addCMacro("HAVE_ASSERT_H", "1");
mod.addCMacro("HAVE_BASENAME", "1"); mod.addCMacro("HAVE_BASENAME", "1");
mod.addCMacro("HAVE_BOOL_T", "1"); mod.addCMacro("HAVE_BOOL_T", "1");
mod.addCMacro("HAVE_BROTLI", "1");
mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1"); mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1");
mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1"); mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
mod.addCMacro("HAVE_DLFCN_H", "1"); mod.addCMacro("HAVE_DLFCN_H", "1");
@@ -380,11 +379,9 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
} }
try buildZlib(b, mod); try buildZlib(b, mod);
try buildBrotli(b, mod);
try buildMbedtls(b, mod); try buildMbedtls(b, mod);
try buildNghttp2(b, mod); try buildNghttp2(b, mod);
try buildCurl(b, mod); try buildCurl(b, mod);
try buildAda(b, mod);
switch (target.result.os.tag) { switch (target.result.os.tag) {
.macos => { .macos => {
@@ -487,30 +484,6 @@ fn buildZlib(b: *Build, m: *Build.Module) !void {
} }); } });
} }
fn buildBrotli(b: *Build, m: *Build.Module) !void {
const brotli = b.addLibrary(.{
.name = "brotli",
.root_module = m,
});
const root = "vendor/brotli/c/";
brotli.addIncludePath(b.path(root ++ "include"));
brotli.addCSourceFiles(.{ .flags = &.{}, .files = &.{
root ++ "common/constants.c",
root ++ "common/context.c",
root ++ "common/dictionary.c",
root ++ "common/platform.c",
root ++ "common/shared_dictionary.c",
root ++ "common/transform.c",
root ++ "dec/bit_reader.c",
root ++ "dec/decode.c",
root ++ "dec/huffman.c",
root ++ "dec/prefix.c",
root ++ "dec/state.c",
root ++ "dec/static_init.c",
} });
}
fn buildMbedtls(b: *Build, m: *Build.Module) !void { fn buildMbedtls(b: *Build, m: *Build.Module) !void {
const mbedtls = b.addLibrary(.{ const mbedtls = b.addLibrary(.{
.name = "mbedtls", .name = "mbedtls",
@@ -850,34 +823,3 @@ fn buildCurl(b: *Build, m: *Build.Module) !void {
}, },
}); });
} }
pub fn buildAda(b: *Build, m: *Build.Module) !void {
const ada_dep = b.dependency("ada-singleheader", .{});
const ada_mod = b.createModule(.{
.root_source_file = b.path("vendor/ada/root.zig"),
});
const ada_lib = b.addLibrary(.{
.name = "ada",
.root_module = b.createModule(.{
.link_libcpp = true,
.target = m.resolved_target,
.optimize = m.optimize,
}),
.linkage = .static,
});
ada_lib.addCSourceFile(.{
.file = ada_dep.path("ada.cpp"),
.flags = &.{ "-std=c++20", "-O3" },
.language = .cpp,
});
ada_lib.installHeader(ada_dep.path("ada_c.h"), "ada_c.h");
// Link the library to ada module.
ada_mod.linkLibrary(ada_lib);
// Expose ada module to main module.
m.addImport("ada", ada_mod);
}

View File

@@ -5,13 +5,9 @@
.fingerprint = 0xda130f3af836cea0, .fingerprint = 0xda130f3af836cea0,
.dependencies = .{ .dependencies = .{
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/305bb3706716d32d59b2ffa674731556caa1002b.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d3040a953e5a37290dae20e7ddf138b7aeb5e67d.tar.gz",
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5", .hash = "v8-0.0.0-xddH6w_EAwA8vK0NAxfxfI7IcbnpkUAcXKNujn7qwnmY",
}, },
//.v8 = .{ .path = "../zig-v8-fork" } //.v8 = .{ .path = "../zig-v8-fork" }
.@"ada-singleheader" = .{
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
},
}, },
} }

12
flake.lock generated
View File

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

View File

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

View File

@@ -74,7 +74,6 @@ pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }), error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
else => return err, else => return err,
}; };
defer file.close();
const stat = try file.stat(); const stat = try file.stat();
var send_buffer: [4096]u8 = undefined; var send_buffer: [4096]u8 = undefined;

View File

@@ -4,7 +4,7 @@ const Allocator = std.mem.Allocator;
const log = @import("log.zig"); const log = @import("log.zig");
const Http = @import("http/Http.zig"); const Http = @import("http/Http.zig");
const Platform = @import("browser/js/Platform.zig"); const Platform = @import("runtime/js.zig").Platform;
const Telemetry = @import("telemetry/telemetry.zig").Telemetry; const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
const Notification = @import("notification.zig").Notification; const Notification = @import("notification.zig").Notification;
@@ -36,7 +36,6 @@ pub const App = struct {
http_connect_timeout_ms: ?u31 = null, http_connect_timeout_ms: ?u31 = null,
http_max_host_open: ?u8 = null, http_max_host_open: ?u8 = null,
http_max_concurrent: ?u8 = null, http_max_concurrent: ?u8 = null,
user_agent: [:0]const u8,
}; };
pub fn init(allocator: Allocator, config: Config) !*App { pub fn init(allocator: Allocator, config: Config) !*App {
@@ -54,7 +53,6 @@ pub const App = struct {
.http_proxy = config.http_proxy, .http_proxy = config.http_proxy,
.tls_verify_host = config.tls_verify_host, .tls_verify_host = config.tls_verify_host,
.proxy_bearer_token = config.proxy_bearer_token, .proxy_bearer_token = config.proxy_bearer_token,
.user_agent = config.user_agent,
}); });
errdefer http.deinit(); errdefer http.deinit();

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const log = @import("../log.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Scheduler = @This(); const Scheduler = @This();
@@ -37,10 +38,8 @@ pub fn init(allocator: Allocator) Scheduler {
} }
pub fn reset(self: *Scheduler) void { pub fn reset(self: *Scheduler) void {
// Our allocator is the page arena, it's been reset. We cannot use self.high_priority.clearRetainingCapacity();
// clearAndRetainCapacity, since that space is no longer ours self.low_priority.clearRetainingCapacity();
self.high_priority.clearAndFree();
self.low_priority.clearAndFree();
} }
const AddOpts = struct { const AddOpts = struct {
@@ -66,11 +65,14 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: Ad
}); });
} }
pub fn run(self: *Scheduler) !?i32 { pub fn runHighPriority(self: *Scheduler) !?i32 {
_ = try self.runQueue(&self.low_priority);
return self.runQueue(&self.high_priority); return self.runQueue(&self.high_priority);
} }
pub fn runLowPriority(self: *Scheduler) !?i32 {
return self.runQueue(&self.low_priority);
}
fn runQueue(self: *Scheduler, queue: *Queue) !?i32 { fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
// this is O(1) // this is O(1)
if (queue.count() == 0) { if (queue.count() == 0) {
@@ -125,24 +127,33 @@ test "Scheduler" {
var task = TestTask{ .allocator = testing.arena_allocator }; var task = TestTask{ .allocator = testing.arena_allocator };
var s = Scheduler.init(testing.arena_allocator); var s = Scheduler.init(testing.arena_allocator);
try testing.expectEqual(null, s.run()); try testing.expectEqual(null, s.runHighPriority());
try testing.expectEqual(0, task.calls.items.len); try testing.expectEqual(0, task.calls.items.len);
try s.add(&task, TestTask.run1, 3, .{}); try s.add(&task, TestTask.run1, 3, .{});
try testing.expectDelta(3, try s.run(), 1); try testing.expectDelta(3, try s.runHighPriority(), 1);
try testing.expectEqual(0, task.calls.items.len); try testing.expectEqual(0, task.calls.items.len);
std.Thread.sleep(std.time.ns_per_ms * 5); std.Thread.sleep(std.time.ns_per_ms * 5);
try testing.expectEqual(null, s.run()); try testing.expectEqual(null, s.runHighPriority());
try testing.expectEqualSlices(u32, &.{1}, task.calls.items); try testing.expectEqualSlices(u32, &.{1}, task.calls.items);
try s.add(&task, TestTask.run2, 3, .{}); try s.add(&task, TestTask.run2, 3, .{});
try s.add(&task, TestTask.run1, 2, .{}); try s.add(&task, TestTask.run1, 2, .{});
std.Thread.sleep(std.time.ns_per_ms * 5); std.Thread.sleep(std.time.ns_per_ms * 5);
try testing.expectDelta(null, try s.run(), 1); try testing.expectDelta(null, try s.runHighPriority(), 1);
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items); try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
std.Thread.sleep(std.time.ns_per_ms * 5);
// won't run low_priority
try testing.expectEqual(null, try s.runHighPriority());
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
//runs low_priority
try testing.expectDelta(2, try s.runLowPriority(), 1);
try testing.expectEqualSlices(u32, &.{ 1, 1, 2, 2 }, task.calls.items);
} }
const TestTask = struct { const TestTask = struct {

View File

@@ -18,10 +18,10 @@
const std = @import("std"); const std = @import("std");
const js = @import("js/js.zig");
const log = @import("../log.zig"); const log = @import("../log.zig");
const parser = @import("netsurf.zig"); const parser = @import("netsurf.zig");
const Env = @import("env.zig").Env;
const Page = @import("page.zig").Page; const Page = @import("page.zig").Page;
const DataURI = @import("DataURI.zig"); const DataURI = @import("DataURI.zig");
const Http = @import("../http/Http.zig"); const Http = @import("../http/Http.zig");
@@ -38,6 +38,9 @@ page: *Page,
// used to prevent recursive evalutaion // used to prevent recursive evalutaion
is_evaluating: bool, is_evaluating: bool,
// used to prevent executing scripts while we're doing a blocking load
is_blocking: bool = false,
// Only once this is true can deferred scripts be run // Only once this is true can deferred scripts be run
static_scripts_done: bool, static_scripts_done: bool,
@@ -45,6 +48,12 @@ static_scripts_done: bool,
// on shutdown/abort, we need to cleanup any pending ones. // on shutdown/abort, we need to cleanup any pending ones.
asyncs: OrderList, asyncs: OrderList,
// When an async script is ready to be evaluated, it's moved from asyncs to
// this list. You might think we can evaluate an async script as soon as it's
// done, but we can only evaluate scripts when `is_blocking == false`. So this
// becomes a list of scripts to execute on the next evaluate().
asyncs_ready: OrderList,
// Normal scripts (non-deferred & non-async). These must be executed in order // Normal scripts (non-deferred & non-async). These must be executed in order
scripts: OrderList, scripts: OrderList,
@@ -58,22 +67,6 @@ client: *Http.Client,
allocator: Allocator, allocator: Allocator,
buffer_pool: BufferPool, buffer_pool: BufferPool,
script_pool: std.heap.MemoryPool(PendingScript), script_pool: std.heap.MemoryPool(PendingScript),
sync_module_pool: std.heap.MemoryPool(SyncModule),
async_module_pool: std.heap.MemoryPool(AsyncModule),
// We can download multiple sync modules in parallel, but we want to process
// then in order. We can't use an OrderList, like the other script types,
// because the order we load them might not be the order we want to process
// them in (I'm not sure this is true, but as far as I can tell, v8 doesn't
// make any guarantees about the list of sub-module dependencies it gives us
// So this is more like a cache. When a SyncModule is complete, it's put here
// and can be requested as needed.
sync_modules: std.StringHashMapUnmanaged(*SyncModule),
// Mapping between module specifier and resolution.
// see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap
// importmap contains resolved urls.
importmap: std.StringHashMapUnmanaged([:0]const u8),
const OrderList = std.DoublyLinkedList; const OrderList = std.DoublyLinkedList;
@@ -85,51 +78,27 @@ pub fn init(browser: *Browser, page: *Page) ScriptManager {
.asyncs = .{}, .asyncs = .{},
.scripts = .{}, .scripts = .{},
.deferreds = .{}, .deferreds = .{},
.importmap = .empty, .asyncs_ready = .{},
.sync_modules = .empty,
.is_evaluating = false, .is_evaluating = false,
.allocator = allocator, .allocator = allocator,
.client = browser.http_client, .client = browser.http_client,
.static_scripts_done = false, .static_scripts_done = false,
.buffer_pool = BufferPool.init(allocator, 5), .buffer_pool = BufferPool.init(allocator, 5),
.script_pool = std.heap.MemoryPool(PendingScript).init(allocator), .script_pool = std.heap.MemoryPool(PendingScript).init(allocator),
.sync_module_pool = std.heap.MemoryPool(SyncModule).init(allocator),
.async_module_pool = std.heap.MemoryPool(AsyncModule).init(allocator),
}; };
} }
pub fn deinit(self: *ScriptManager) void { pub fn deinit(self: *ScriptManager) void {
self.reset(); self.reset();
var it = self.sync_modules.valueIterator();
while (it.next()) |value_ptr| {
value_ptr.*.buffer.deinit(self.allocator);
self.sync_module_pool.destroy(value_ptr.*);
}
self.buffer_pool.deinit(); self.buffer_pool.deinit();
self.script_pool.deinit(); self.script_pool.deinit();
self.sync_module_pool.deinit();
self.async_module_pool.deinit();
self.sync_modules.deinit(self.allocator);
// we don't deinit self.importmap b/c we use the page's arena for its
// allocations.
} }
pub fn reset(self: *ScriptManager) void { pub fn reset(self: *ScriptManager) void {
var it = self.sync_modules.valueIterator();
while (it.next()) |value_ptr| {
value_ptr.*.buffer.deinit(self.allocator);
self.sync_module_pool.destroy(value_ptr.*);
}
self.sync_modules.clearRetainingCapacity();
// Our allocator is the page arena, it's been reset. We cannot use
// clearAndRetainCapacity, since that space is no longer ours
self.importmap = .empty;
self.clearList(&self.asyncs); self.clearList(&self.asyncs);
self.clearList(&self.scripts); self.clearList(&self.scripts);
self.clearList(&self.deferreds); self.clearList(&self.deferreds);
self.clearList(&self.asyncs_ready);
self.static_scripts_done = false; self.static_scripts_done = false;
} }
@@ -142,7 +111,7 @@ fn clearList(_: *const ScriptManager, list: *OrderList) void {
std.debug.assert(list.first == null); std.debug.assert(list.first == null);
} }
pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime ctx: []const u8) !void { pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
if (try parser.elementGetAttribute(element, "nomodule") != null) { if (try parser.elementGetAttribute(element, "nomodule") != null) {
// these scripts should only be loaded if we don't support modules // these scripts should only be loaded if we don't support modules
// but since we do support modules, we can just skip them. // but since we do support modules, we can just skip them.
@@ -175,9 +144,6 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
if (std.ascii.eqlIgnoreCase(script_type, "module")) { if (std.ascii.eqlIgnoreCase(script_type, "module")) {
break :blk .module; break :blk .module;
} }
if (std.ascii.eqlIgnoreCase(script_type, "importmap")) {
break :blk .importmap;
}
// "type" could be anything, but only the above are ones we need to process. // "type" could be anything, but only the above are ones we need to process.
// Common other ones are application/json, application/ld+json, text/template // Common other ones are application/json, application/ld+json, text/template
@@ -191,12 +157,11 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
if (try parser.elementGetAttribute(element, "src")) |src| { if (try parser.elementGetAttribute(element, "src")) |src| {
if (try DataURI.parse(page.arena, src)) |data_uri| { if (try DataURI.parse(page.arena, src)) |data_uri| {
source = .{ .@"inline" = data_uri }; source = .{ .@"inline" = data_uri };
} else { }
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true }); remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true });
source = .{ .remote = .{} }; source = .{ .remote = .{} };
}
} else { } else {
const inline_source = parser.nodeTextContent(@ptrCast(element)) orelse return; const inline_source = try parser.nodeTextContent(@ptrCast(element)) orelse return;
source = .{ .@"inline" = inline_source }; source = .{ .@"inline" = inline_source };
} }
@@ -212,7 +177,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
if (source == .@"inline" and self.scripts.first == null) { if (source == .@"inline" and self.scripts.first == null) {
// inline script with no pending scripts, execute it immediately. // inline script with no pending scripts, execute it immediately.
// (if there is a pending script, then we cannot execute this immediately // (if there is a pending script, then we cannot execute this immediately
// as it needs to be executed in order) // as it needs to best executed in order)
return script.eval(page); return script.eval(page);
} }
@@ -228,23 +193,19 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
if (source == .@"inline") { if (source == .@"inline") {
// if we're here, it means that we have pending scripts (i.e. self.scripts // if we're here, it means that we have pending scripts (i.e. self.scripts
// is not empty). Because the script is inline, it's complete/ready, but // is not empty). Because the script is inline, it's complete/ready, but
// we need to process them in order. // we need to process them in order
pending_script.complete = true; pending_script.complete = true;
pending_script.getList().append(&pending_script.node); self.scripts.append(&pending_script.node);
return; return;
} else { } else {
log.debug(.http, "script queue", .{ log.debug(.http, "script queue", .{ .url = remote_url.? });
.ctx = ctx,
.url = remote_url.?,
.stack = page.js.stackTrace() catch "???",
});
} }
pending_script.getList().append(&pending_script.node); pending_script.getList().append(&pending_script.node);
errdefer pending_script.deinit(); errdefer pending_script.deinit();
var headers = try self.client.newHeaders(); var headers = try Http.Headers.init();
try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers); try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers);
try self.client.request(.{ try self.client.request(.{
@@ -262,144 +223,88 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
}); });
} }
// Resolve a module specifier to an valid URL. // @TODO: Improving this would have the simplest biggest performance improvement
pub fn resolveSpecifier(self: *ScriptManager, arena: Allocator, specifier: []const u8, base: []const u8) ![:0]const u8 { // for most sites.
// If the specifier is mapped in the importmap, return the pre-resolved value. //
if (self.importmap.get(specifier)) |s| { // For JS imports (both static and dynamic), we currently block to get the
return s; // result (the content of the file).
//
// For static imports, this is necessary, since v8 is expecting the compiled module
// as part of the function return. (we should try to pre-load the JavaScript
// source via module.GetModuleRequests(), but that's for a later time).
//
// For dynamic dynamic imports, this is not strictly necessary since the v8
// call returns a Promise; we could make this a normal get call, associated with
// the promise, and when done, resolve the promise.
//
// In both cases, for now at least, we just issue a "blocking" request. We block
// by ticking the http client until the script is complete.
//
// This uses the client.blockingRequest call which has a dedicated handle for
// these blocking requests. Because they are blocking, we're guaranteed to have
// only 1 at a time, thus the 1 reserved handle.
//
// You almost don't need the http client's blocking handle. In most cases, you
// should always have 1 free handle whenever you get here, because we always
// release the handle before executing the doneCallback. So, if a module does:
// import * as x from 'blah'
// And we need to load 'blah', there should always be 1 free handle - the handle
// of the http GET we just completed before executing the module.
// The exception to this, and the reason we need a special blocking handle, is
// for inline modules within the HTML page itself:
// <script type=module>import ....</script>
// Unlike external modules which can only ever be executed after releasing an
// http handle, these are executed without there necessarily being a free handle.
// Thus, Http/Client.zig maintains a dedicated handle for these calls.
pub fn blockingGet(self: *ScriptManager, url: [:0]const u8) !BlockingResult {
std.debug.assert(self.is_blocking == false);
self.is_blocking = true;
defer {
self.is_blocking = false;
// we blocked evaluation while loading this script, there could be
// scripts ready to process.
self.evaluate();
} }
return URL.stitch( var blocking = Blocking{
arena, .allocator = self.allocator,
specifier, .buffer_pool = &self.buffer_pool,
base,
.{ .alloc = .if_needed, .null_terminated = true },
);
}
pub fn getModule(self: *ScriptManager, url: [:0]const u8, referrer: []const u8) !void {
const gop = try self.sync_modules.getOrPut(self.allocator, url);
if (gop.found_existing) {
// already requested
return;
}
errdefer _ = self.sync_modules.remove(url);
const sync = try self.sync_module_pool.create();
errdefer self.sync_module_pool.destroy(sync);
sync.* = .{ .manager = self };
gop.value_ptr.* = sync;
var headers = try self.client.newHeaders();
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
log.debug(.http, "script queue", .{
.url = url,
.ctx = "module",
.referrer = referrer,
.stack = self.page.js.stackTrace() catch "???",
});
try self.client.request(.{
.url = url,
.ctx = sync,
.method = .GET,
.headers = headers,
.cookie_jar = self.page.cookie_jar,
.resource_type = .script,
.start_callback = if (log.enabled(.http, .debug)) SyncModule.startCallback else null,
.header_callback = SyncModule.headerCallback,
.data_callback = SyncModule.dataCallback,
.done_callback = SyncModule.doneCallback,
.error_callback = SyncModule.errorCallback,
});
}
pub fn waitForModule(self: *ScriptManager, url: [:0]const u8) !GetResult {
// Normally it's dangerous to hold on to map pointers. But here, the map
// can't change. It's possible that by calling `tick`, other entries within
// the map will have their value change, but the map itself is immutable
// during this tick.
const entry = self.sync_modules.getEntry(url) orelse {
return error.UnknownModule;
}; };
const sync = entry.value_ptr.*;
// We can have multiple scripts waiting for the same module in concurrency. var headers = try Http.Headers.init();
// We use the waiters to ensures only the last waiter deinit the resources. try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
sync.waiters += 1;
defer sync.waiters -= 1;
var client = self.client; var client = self.client;
while (true) { try client.blockingRequest(.{
switch (sync.state) {
.loading => {},
.done => {
if (sync.waiters == 1) {
// Our caller has its own higher level cache (caching the
// actual compiled module). There's no reason for us to keep
// this if we are the last waiter.
defer self.sync_module_pool.destroy(sync);
defer self.sync_modules.removeByPtr(entry.key_ptr);
return .{
.shared = false,
.buffer = sync.buffer,
.buffer_pool = &self.buffer_pool,
};
}
return .{
.shared = true,
.buffer = sync.buffer,
.buffer_pool = &self.buffer_pool,
};
},
.err => |err| return err,
}
// rely on http's timeout settings to avoid an endless/long loop.
_ = try client.tick(200);
}
}
pub fn getAsyncModule(self: *ScriptManager, url: [:0]const u8, cb: AsyncModule.Callback, cb_data: *anyopaque, referrer: []const u8) !void {
const async = try self.async_module_pool.create();
errdefer self.async_module_pool.destroy(async);
async.* = .{
.cb = cb,
.manager = self,
.cb_data = cb_data,
};
var headers = try self.client.newHeaders();
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
log.debug(.http, "script queue", .{
.url = url,
.ctx = "dynamic module",
.referrer = referrer,
.stack = self.page.js.stackTrace() catch "???",
});
try self.client.request(.{
.url = url, .url = url,
.method = .GET, .method = .GET,
.headers = headers, .headers = headers,
.cookie_jar = self.page.cookie_jar, .cookie_jar = self.page.cookie_jar,
.ctx = async, .ctx = &blocking,
.resource_type = .script, .resource_type = .script,
.start_callback = if (log.enabled(.http, .debug)) AsyncModule.startCallback else null, .start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null,
.header_callback = AsyncModule.headerCallback, .header_callback = Blocking.headerCallback,
.data_callback = AsyncModule.dataCallback, .data_callback = Blocking.dataCallback,
.done_callback = AsyncModule.doneCallback, .done_callback = Blocking.doneCallback,
.error_callback = AsyncModule.errorCallback, .error_callback = Blocking.errorCallback,
}); });
// rely on http's timeout settings to avoid an endless/long loop.
while (true) {
_ = try client.tick(200);
switch (blocking.state) {
.running => {},
.done => |result| return result,
.err => |err| return err,
}
}
} }
pub fn staticScriptsDone(self: *ScriptManager) void { pub fn staticScriptsDone(self: *ScriptManager) void {
std.debug.assert(self.static_scripts_done == false); std.debug.assert(self.static_scripts_done == false);
self.static_scripts_done = true; self.static_scripts_done = true;
self.evaluate();
} }
// try to evaluate completed scripts (in order). This is called whenever a script // try to evaluate completed scripts (in order). This is called whenever a script
@@ -413,10 +318,24 @@ fn evaluate(self: *ScriptManager) void {
return; return;
} }
if (self.is_blocking) {
// Cannot evaluate scripts while a blocking-load is in progress. Not
// only could that result in incorrect evaluation order, it could
// trigger another blocking request, while we're doing a blocking request.
return;
}
const page = self.page; const page = self.page;
self.is_evaluating = true; self.is_evaluating = true;
defer self.is_evaluating = false; defer self.is_evaluating = false;
// every script in asyncs_ready is ready to be evaluated.
while (self.asyncs_ready.first) |n| {
var pending_script: *PendingScript = @fieldParentPtr("node", n);
defer pending_script.deinit();
pending_script.script.eval(page);
}
while (self.scripts.first) |n| { while (self.scripts.first) |n| {
var pending_script: *PendingScript = @fieldParentPtr("node", n); var pending_script: *PendingScript = @fieldParentPtr("node", n);
if (pending_script.complete == false) { if (pending_script.complete == false) {
@@ -466,12 +385,6 @@ pub fn isDone(self: *const ScriptManager) bool {
self.deferreds.first == null; // and there are no more <script defer src=> to wait for self.deferreds.first == null; // and there are no more <script defer src=> to wait for
} }
fn asyncScriptIsDone(self: *ScriptManager) void {
if (self.isDone()) {
self.page.documentIsComplete();
}
}
fn startCallback(transfer: *Http.Transfer) !void { fn startCallback(transfer: *Http.Transfer) !void {
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx)); const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
script.startCallback(transfer) catch |err| { script.startCallback(transfer) catch |err| {
@@ -503,38 +416,6 @@ fn errorCallback(ctx: *anyopaque, err: anyerror) void {
script.errorCallback(err); script.errorCallback(err);
} }
fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
const content = script.source.content();
const Imports = struct {
imports: std.json.ArrayHashMap([]const u8),
};
const imports = try std.json.parseFromSliceLeaky(
Imports,
self.page.arena,
content,
.{ .allocate = .alloc_always },
);
var iter = imports.imports.map.iterator();
while (iter.next()) |entry| {
// > Relative URLs are resolved to absolute URL addresses using the
// > base URL of the document containing the import map.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_modules_using_import_maps
const resolved_url = try URL.stitch(
self.page.arena,
entry.value_ptr.*,
self.page.url.raw,
.{ .alloc = .if_needed, .null_terminated = true },
);
try self.importmap.put(self.page.arena, entry.key_ptr.*, resolved_url);
}
return;
}
// A script which is pending execution. // A script which is pending execution.
// It could be pending because: // It could be pending because:
// (a) we're still downloading its content or // (a) we're still downloading its content or
@@ -612,15 +493,11 @@ pub const PendingScript = struct {
const manager = self.manager; const manager = self.manager;
self.complete = true; self.complete = true;
if (!self.script.is_async) { if (self.script.is_async) {
manager.evaluate(); manager.asyncs.remove(&self.node);
return; manager.asyncs_ready.append(&self.node);
} }
// async script can be evaluated immediately manager.evaluate();
self.script.eval(manager.page);
self.deinit();
// asyncScriptIsDone must be run after the pending script is deinit.
manager.asyncScriptIsDone();
} }
fn errorCallback(self: *PendingScript, err: anyerror) void { fn errorCallback(self: *PendingScript, err: anyerror) void {
@@ -643,20 +520,8 @@ pub const PendingScript = struct {
// if async isn't known, it'll fallback to defer. // if async isn't known, it'll fallback to defer.
const script = &self.script; const script = &self.script;
// Module scripts are deferred by default.
// https://v8.dev/features/modules#defer
if (script.kind == .module) {
return &self.manager.deferreds;
}
// Script is not a module but inline, we ignore async/defer properties.
if (script.source == .@"inline") {
return &self.manager.scripts;
}
if (script.is_async) { if (script.is_async) {
return &self.manager.asyncs; return if (self.complete) &self.manager.asyncs_ready else &self.manager.asyncs;
} }
if (script.is_defer) { if (script.is_defer) {
@@ -678,12 +543,11 @@ const Script = struct {
const Kind = enum { const Kind = enum {
module, module,
javascript, javascript,
importmap,
}; };
const Callback = union(enum) { const Callback = union(enum) {
string: []const u8, string: []const u8,
function: js.Function, function: Env.Function,
}; };
const Source = union(enum) { const Source = union(enum) {
@@ -719,25 +583,8 @@ const Script = struct {
.cacheable = cacheable, .cacheable = cacheable,
}); });
// Handle importmap special case here: the content is a JSON containing const js_context = page.main_context;
// imports. var try_catch: Env.TryCatch = undefined;
if (self.kind == .importmap) {
page.script_manager.parseImportmap(self) catch |err| {
log.err(.browser, "parse importmap script", .{
.err = err,
.src = url,
.kind = self.kind,
.cacheable = cacheable,
});
self.executeCallback("onerror", page);
return;
};
self.executeCallback("onload", page);
return;
}
const js_context = page.js;
var try_catch: js.TryCatch = undefined;
try_catch.init(js_context); try_catch.init(js_context);
defer try_catch.deinit(); defer try_catch.deinit();
@@ -747,9 +594,8 @@ const Script = struct {
.javascript => _ = js_context.eval(content, url) catch break :blk false, .javascript => _ = js_context.eval(content, url) catch break :blk false,
.module => { .module => {
// We don't care about waiting for the evaluation here. // We don't care about waiting for the evaluation here.
js_context.module(false, content, url, cacheable) catch break :blk false; _ = js_context.module(content, url, cacheable) catch break :blk false;
}, },
.importmap => unreachable, // handled before the try/catch.
} }
break :blk true; break :blk true;
}; };
@@ -780,11 +626,11 @@ const Script = struct {
switch (callback) { switch (callback) {
.string => |str| { .string => |str| {
var try_catch: js.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(page.js); try_catch.init(page.main_context);
defer try_catch.deinit(); defer try_catch.deinit();
_ = page.js.exec(str, typ) catch |err| { _ = page.main_context.exec(str, typ) catch |err| {
const msg = try_catch.err(page.arena) catch @errorName(err) orelse "unknown"; const msg = try_catch.err(page.arena) catch @errorName(err) orelse "unknown";
log.warn(.user_script, "script callback", .{ log.warn(.user_script, "script callback", .{
.url = self.url, .url = self.url,
@@ -802,8 +648,11 @@ const Script = struct {
}; };
defer parser.eventDestroy(loadevt); defer parser.eventDestroy(loadevt);
var result: js.Function.Result = undefined; var result: Env.Function.Result = undefined;
const iface = Event.toInterface(loadevt); const iface = Event.toInterface(loadevt) catch |err| {
log.err(.browser, "SM event interface", .{ .err = err });
return;
};
f.tryCall(void, .{iface}, &result) catch { f.tryCall(void, .{iface}, &result) catch {
log.warn(.user_script, "script callback", .{ log.warn(.user_script, "script callback", .{
.url = self.url, .url = self.url,
@@ -905,17 +754,16 @@ const BufferPool = struct {
} }
}; };
const SyncModule = struct { const Blocking = struct {
manager: *ScriptManager, allocator: Allocator,
buffer_pool: *BufferPool,
state: State = .{ .running = {} },
buffer: std.ArrayListUnmanaged(u8) = .{}, buffer: std.ArrayListUnmanaged(u8) = .{},
state: State = .loading,
// number of waiters for the module.
waiters: u8 = 0,
const State = union(enum) { const State = union(enum) {
done, running: void,
loading,
err: anyerror, err: anyerror,
done: BlockingResult,
}; };
fn startCallback(transfer: *Http.Transfer) !void { fn startCallback(transfer: *Http.Transfer) !void {
@@ -931,13 +779,12 @@ const SyncModule = struct {
.content_type = header.contentType(), .content_type = header.contentType(),
}); });
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx));
if (header.status != 200) { if (header.status != 200) {
self.finished(.{ .err = error.InvalidStatusCode });
return error.InvalidStatusCode; return error.InvalidStatusCode;
} }
self.buffer = self.manager.buffer_pool.get(); var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
self.buffer = self.buffer_pool.get();
} }
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void { fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
@@ -947,8 +794,8 @@ const SyncModule = struct {
// .blocking = true, // .blocking = true,
// }); // });
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx)); var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
self.buffer.appendSlice(self.manager.allocator, data) catch |err| { self.buffer.appendSlice(self.allocator, data) catch |err| {
log.err(.http, "SM.dataCallback", .{ log.err(.http, "SM.dataCallback", .{
.err = err, .err = err,
.len = data.len, .len = data.len,
@@ -960,107 +807,29 @@ const SyncModule = struct {
} }
fn doneCallback(ctx: *anyopaque) !void { fn doneCallback(ctx: *anyopaque) !void {
var self: *SyncModule = @ptrCast(@alignCast(ctx)); var self: *Blocking = @ptrCast(@alignCast(ctx));
self.finished(.done); self.state = .{ .done = .{
}
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
var self: *SyncModule = @ptrCast(@alignCast(ctx));
self.finished(.{ .err = err });
}
fn finished(self: *SyncModule, state: State) void {
self.state = state;
}
};
pub const AsyncModule = struct {
cb: Callback,
cb_data: *anyopaque,
manager: *ScriptManager,
buffer: std.ArrayListUnmanaged(u8) = .{},
pub const Callback = *const fn (ptr: *anyopaque, result: anyerror!GetResult) void;
fn startCallback(transfer: *Http.Transfer) !void {
log.debug(.http, "script fetch start", .{ .req = transfer, .async = true });
}
fn headerCallback(transfer: *Http.Transfer) !void {
const header = &transfer.response_header.?;
log.debug(.http, "script header", .{
.req = transfer,
.async = true,
.status = header.status,
.content_type = header.contentType(),
});
if (header.status != 200) {
return error.InvalidStatusCode;
}
var self: *AsyncModule = @ptrCast(@alignCast(transfer.ctx));
self.buffer = self.manager.buffer_pool.get();
}
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
// too verbose
// log.debug(.http, "script data chunk", .{
// .req = transfer,
// .blocking = true,
// });
var self: *AsyncModule = @ptrCast(@alignCast(transfer.ctx));
self.buffer.appendSlice(self.manager.allocator, data) catch |err| {
log.err(.http, "SM.dataCallback", .{
.err = err,
.len = data.len,
.ascyn = true,
.transfer = transfer,
});
return err;
};
}
fn doneCallback(ctx: *anyopaque) !void {
var self: *AsyncModule = @ptrCast(@alignCast(ctx));
defer self.manager.async_module_pool.destroy(self);
self.cb(self.cb_data, .{
.shared = false,
.buffer = self.buffer, .buffer = self.buffer,
.buffer_pool = &self.manager.buffer_pool, .buffer_pool = self.buffer_pool,
}); } };
} }
fn errorCallback(ctx: *anyopaque, err: anyerror) void { fn errorCallback(ctx: *anyopaque, err: anyerror) void {
var self: *AsyncModule = @ptrCast(@alignCast(ctx)); var self: *Blocking = @ptrCast(@alignCast(ctx));
self.state = .{ .err = err };
if (err != error.Abort) { self.buffer_pool.release(self.buffer);
self.cb(self.cb_data, err);
}
if (self.buffer.items.len > 0) {
self.manager.buffer_pool.release(self.buffer);
}
self.manager.async_module_pool.destroy(self);
} }
}; };
pub const GetResult = struct { pub const BlockingResult = struct {
buffer: std.ArrayListUnmanaged(u8), buffer: std.ArrayListUnmanaged(u8),
buffer_pool: *BufferPool, buffer_pool: *BufferPool,
shared: bool,
pub fn deinit(self: *GetResult) void { pub fn deinit(self: *BlockingResult) void {
// if the result is shared, don't deinit.
if (self.shared) {
return;
}
self.buffer_pool.release(self.buffer); self.buffer_pool.release(self.buffer);
} }
pub fn src(self: *const GetResult) []const u8 { pub fn src(self: *const BlockingResult) []const u8 {
return self.buffer.items; return self.buffer.items;
} }
}; };

View File

@@ -1,189 +0,0 @@
const std = @import("std");
const log = @import("../log.zig");
const parser = @import("netsurf.zig");
const collection = @import("dom/html_collection.zig");
const Page = @import("page.zig").Page;
const SlotChangeMonitor = @This();
page: *Page,
event_node: parser.EventNode,
slots_changed: std.ArrayList(*parser.Slot),
// Monitors the document in order to trigger slotchange events.
pub fn init(page: *Page) !*SlotChangeMonitor {
// on the heap, we need a stable address for event_node
const self = try page.arena.create(SlotChangeMonitor);
self.* = .{
.page = page,
.slots_changed = .empty,
.event_node = .{ .func = mutationCallback },
};
const root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document));
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, root),
"DOMNodeInserted",
&self.event_node,
false,
);
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, root),
"DOMNodeRemoved",
&self.event_node,
false,
);
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, root),
"DOMAttrModified",
&self.event_node,
false,
);
return self;
}
// Given a element, finds its slot, if any.
pub fn findSlot(element: *parser.Element, page: *const Page) !?*parser.Slot {
const target_name = (try parser.elementGetAttribute(element, "slot")) orelse return null;
return findNamedSlot(element, target_name, page);
}
// Given an element and a name, find the slo, if any. This is only useful for
// MutationEvents where findSlot is unreliable because parser.elementGetAttribute(element, "slot")
// could return the new or old value.
fn findNamedSlot(element: *parser.Element, target_name: []const u8, page: *const Page) !?*parser.Slot {
// I believe elements need to be added as direct descendents of the host,
// so we don't need to go find the host, we just grab the parent.
const host = parser.nodeParentNode(@ptrCast(element)) orelse return null;
const state = page.getNodeState(host) orelse return null;
const shadow_root = state.shadow_root orelse return null;
// if we're here, we found a host, now find the slot
var nodes = collection.HTMLCollectionByTagName(
@ptrCast(@alignCast(shadow_root.proto)),
"slot",
.{ .include_root = false },
);
for (0..1000) |i| {
const n = (try nodes.item(@intCast(i))) orelse return null;
const slot_name = (try parser.elementGetAttribute(@ptrCast(n), "name")) orelse "";
if (std.mem.eql(u8, target_name, slot_name)) {
return @ptrCast(n);
}
}
return null;
}
// Event callback from the mutation event, signaling either the addition of
// a node, removal of a node, or a change in attribute
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
const mutation_event = parser.eventToMutationEvent(event);
const self: *SlotChangeMonitor = @fieldParentPtr("event_node", en);
self._mutationCallback(mutation_event) catch |err| {
log.err(.web_api, "slot change callback", .{ .err = err });
};
}
fn _mutationCallback(self: *SlotChangeMonitor, event: *parser.MutationEvent) !void {
const event_type = parser.eventType(@ptrCast(event));
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
return self.nodeAddedOrRemoved(@ptrCast(event_target));
}
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
return self.nodeAddedOrRemoved(@ptrCast(event_target));
}
if (std.mem.eql(u8, event_type, "DOMAttrModified")) {
const attribute_name = try parser.mutationEventAttributeName(event);
if (std.mem.eql(u8, attribute_name, "slot") == false) {
return;
}
const new_value = parser.mutationEventNewValue(event);
const prev_value = parser.mutationEventPrevValue(event);
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
return self.nodeAttributeChanged(@ptrCast(event_target), new_value, prev_value);
}
}
// A node was removed or added. If it's an element, and if it has a slot attribute
// then we'll dispatch a slotchange event.
fn nodeAddedOrRemoved(self: *SlotChangeMonitor, node: *parser.Node) !void {
if (parser.nodeType(node) != .element) {
return;
}
const el: *parser.Element = @ptrCast(node);
if (try findSlot(el, self.page)) |slot| {
return self.scheduleSlotChange(slot);
}
}
// An attribute was modified. If the attribute is "slot", then we'll trigger 1
// slotchange for the old slot (if there was one) and 1 slotchange for the new
// one (if there is one)
fn nodeAttributeChanged(self: *SlotChangeMonitor, node: *parser.Node, new_value: ?[]const u8, prev_value: ?[]const u8) !void {
if (parser.nodeType(node) != .element) {
return;
}
const el: *parser.Element = @ptrCast(node);
if (try findNamedSlot(el, prev_value orelse "", self.page)) |slot| {
try self.scheduleSlotChange(slot);
}
if (try findNamedSlot(el, new_value orelse "", self.page)) |slot| {
try self.scheduleSlotChange(slot);
}
}
// OK. Our MutationEvent is not a MutationObserver - it's an older, deprecated
// API. It gets dispatched in the middle of the change. While I'm sure it has
// some rules, from our point of view, it fires too early. DOMAttrModified fires
// before the attribute is actually updated and DOMNodeRemoved before the node
// is actually removed. This is a problem if the callback will call
// `slot.assignedNodes`, since that won't return the new state.
// So, we use the page schedule to schedule the dispatching of the slotchange
// event.
fn scheduleSlotChange(self: *SlotChangeMonitor, slot: *parser.Slot) !void {
for (self.slots_changed.items) |changed| {
if (slot == changed) {
return;
}
}
try self.slots_changed.append(self.page.arena, slot);
if (self.slots_changed.items.len == 1) {
// first item added, schedule the callback
try self.page.scheduler.add(self, scheduleCallback, 0, .{ .name = "slot change" });
}
}
// Callback from the schedule. Time to dispatch the slotchange event
fn scheduleCallback(ctx: *anyopaque) ?u32 {
var self: *SlotChangeMonitor = @ptrCast(@alignCast(ctx));
self._scheduleCallback() catch |err| {
log.err(.app, "slot change schedule", .{ .err = err });
};
return null;
}
fn _scheduleCallback(self: *SlotChangeMonitor) !void {
for (self.slots_changed.items) |slot| {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, "slotchange", .{});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(parser.Element, @ptrCast(@alignCast(slot))),
event,
);
}
self.slots_changed.clearRetainingCapacity();
}

View File

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

View File

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

View File

@@ -20,47 +20,48 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("../js/js.zig"); const Allocator = std.mem.Allocator;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const JsObject = @import("../env.zig").Env.JsObject;
pub const Console = struct { pub const Console = struct {
// TODO: configurable writer // TODO: configurable writer
timers: std.StringHashMapUnmanaged(u32) = .{}, timers: std.StringHashMapUnmanaged(u32) = .{},
counts: std.StringHashMapUnmanaged(u32) = .{}, counts: std.StringHashMapUnmanaged(u32) = .{},
pub fn _lp(values: []js.Object, page: *Page) !void { pub fn _lp(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) }); log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
} }
pub fn _log(values: []js.Object, page: *Page) !void { pub fn _log(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.info(.console, "info", .{ .args = try serializeValues(values, page) }); log.info(.console, "info", .{ .args = try serializeValues(values, page) });
} }
pub fn _info(values: []js.Object, page: *Page) !void { pub fn _info(values: []JsObject, page: *Page) !void {
return _log(values, page); return _log(values, page);
} }
pub fn _debug(values: []js.Object, page: *Page) !void { pub fn _debug(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) }); log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
} }
pub fn _warn(values: []js.Object, page: *Page) !void { pub fn _warn(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) }); log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
} }
pub fn _error(values: []js.Object, page: *Page) !void { pub fn _error(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
@@ -71,16 +72,6 @@ pub const Console = struct {
}); });
} }
pub fn _trace(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
log.debug(.console, "debug", .{
.stack = page.js.stackTrace() catch "???",
.args = try serializeValues(values, page),
});
}
pub fn _clear() void {} pub fn _clear() void {}
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void { pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
@@ -142,7 +133,7 @@ pub const Console = struct {
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value }); log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
} }
pub fn _assert(assertion: js.Object, values: []js.Object, page: *Page) !void { pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
if (assertion.isTruthy()) { if (assertion.isTruthy()) {
return; return;
} }
@@ -153,7 +144,7 @@ pub const Console = struct {
log.info(.console, "assertion failed", .{ .values = serialized_values }); log.info(.console, "assertion failed", .{ .values = serialized_values });
} }
fn serializeValues(values: []js.Object, page: *Page) ![]const u8 { fn serializeValues(values: []JsObject, page: *Page) ![]const u8 {
if (values.len == 0) { if (values.len == 0) {
return ""; return "";
} }
@@ -175,3 +166,74 @@ pub const Console = struct {
fn timestamp() u32 { fn timestamp() u32 {
return @import("../../datetime.zig").timestamp(); return @import("../../datetime.zig").timestamp();
} }
// const testing = @import("../../testing.zig");
// test "Browser.Console" {
// defer testing.reset();
// var runner = try testing.jsRunner(testing.tracking_allocator, .{});
// defer runner.deinit();
// {
// try runner.testCases(&.{
// .{ "console.log('a')", "undefined" },
// .{ "console.warn('hello world', 23, true, new Object())", "undefined" },
// }, .{});
// const captured = test_capture.captured.items;
// try testing.expectEqual("[info] args= 1: a", captured[0]);
// try testing.expectEqual("[warn] args= 1: hello world 2: 23 3: true 4: #<Object>", captured[1]);
// }
// {
// test_capture.reset();
// try runner.testCases(&.{
// .{ "console.countReset()", "undefined" },
// .{ "console.count()", "undefined" },
// .{ "console.count('teg')", "undefined" },
// .{ "console.count('teg')", "undefined" },
// .{ "console.count('teg')", "undefined" },
// .{ "console.count()", "undefined" },
// .{ "console.countReset('teg')", "undefined" },
// .{ "console.countReset()", "undefined" },
// .{ "console.count()", "undefined" },
// }, .{});
// const captured = test_capture.captured.items;
// try testing.expectEqual("[invalid counter] label=default", captured[0]);
// try testing.expectEqual("[count] label=default count=1", captured[1]);
// try testing.expectEqual("[count] label=teg count=1", captured[2]);
// try testing.expectEqual("[count] label=teg count=2", captured[3]);
// try testing.expectEqual("[count] label=teg count=3", captured[4]);
// try testing.expectEqual("[count] label=default count=2", captured[5]);
// try testing.expectEqual("[count reset] label=teg count=3", captured[6]);
// try testing.expectEqual("[count reset] label=default count=2", captured[7]);
// try testing.expectEqual("[count] label=default count=1", captured[8]);
// }
// {
// test_capture.reset();
// try runner.testCases(&.{
// .{ "console.assert(true)", "undefined" },
// .{ "console.assert('a', 2, 3, 4)", "undefined" },
// .{ "console.assert('')", "undefined" },
// .{ "console.assert('', 'x', true)", "undefined" },
// .{ "console.assert(false, 'x')", "undefined" },
// }, .{});
// const captured = test_capture.captured.items;
// try testing.expectEqual("[assertion failed] values=", captured[0]);
// try testing.expectEqual("[assertion failed] values= 1: x 2: true", captured[1]);
// try testing.expectEqual("[assertion failed] values= 1: x", captured[2]);
// }
// {
// test_capture.reset();
// try runner.testCases(&.{
// .{ "[1].forEach(console.log)", null },
// }, .{});
// const captured = test_capture.captured.items;
// try testing.expectEqual("[info] args= 1: 1 2: 0 3: [1]", captured[0]);
// }
// }

View File

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

View File

@@ -46,15 +46,17 @@ pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions)
// matchFirst call m.match with the first node that matches the selector s, from the // matchFirst call m.match with the first node that matches the selector s, from the
// descendants of n and returns true. If none matches, it returns false. // descendants of n and returns true. If none matches, it returns false.
pub fn matchFirst(s: *const Selector, node: anytype, m: anytype) !bool { pub fn matchFirst(s: *const Selector, node: anytype, m: anytype) !bool {
var child = node.firstChild(); var c = try node.firstChild();
while (child) |c| { while (true) {
if (try s.match(c)) { if (c == null) break;
try m.match(c);
if (try s.match(c.?)) {
try m.match(c.?);
return true; return true;
} }
if (try matchFirst(s, c, m)) return true; if (try matchFirst(s, c.?, m)) return true;
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return false; return false;
} }
@@ -62,11 +64,13 @@ pub fn matchFirst(s: *const Selector, node: anytype, m: anytype) !bool {
// matchAll call m.match with the all the nodes that matches the selector s, from the // matchAll call m.match with the all the nodes that matches the selector s, from the
// descendants of n. // descendants of n.
pub fn matchAll(s: *const Selector, node: anytype, m: anytype) !void { pub fn matchAll(s: *const Selector, node: anytype, m: anytype) !void {
var child = node.firstChild(); var c = try node.firstChild();
while (child) |c| { while (true) {
if (try s.match(c)) try m.match(c); if (c == null) break;
try matchAll(s, c, m);
child = c.nextSibling(); if (try s.match(c.?)) try m.match(c.?);
try matchAll(s, c.?, m);
c = try c.?.nextSibling();
} }
} }

View File

@@ -19,74 +19,68 @@
const std = @import("std"); const std = @import("std");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const css = @import("css.zig");
const Allocator = std.mem.Allocator;
// Node implementation with Netsurf Libdom C lib. // Node implementation with Netsurf Libdom C lib.
pub const Node = struct { pub const Node = struct {
node: *parser.Node, node: *parser.Node,
pub fn firstChild(n: Node) ?Node { pub fn firstChild(n: Node) !?Node {
const c = parser.nodeFirstChild(n.node); const c = try parser.nodeFirstChild(n.node);
if (c) |cc| return .{ .node = cc }; if (c) |cc| return .{ .node = cc };
return null; return null;
} }
pub fn lastChild(n: Node) ?Node { pub fn lastChild(n: Node) !?Node {
const c = parser.nodeLastChild(n.node); const c = try parser.nodeLastChild(n.node);
if (c) |cc| return .{ .node = cc }; if (c) |cc| return .{ .node = cc };
return null; return null;
} }
pub fn nextSibling(n: Node) ?Node { pub fn nextSibling(n: Node) !?Node {
const c = parser.nodeNextSibling(n.node); const c = try parser.nodeNextSibling(n.node);
if (c) |cc| return .{ .node = cc }; if (c) |cc| return .{ .node = cc };
return null; return null;
} }
pub fn prevSibling(n: Node) ?Node { pub fn prevSibling(n: Node) !?Node {
const c = parser.nodePreviousSibling(n.node); const c = try parser.nodePreviousSibling(n.node);
if (c) |cc| return .{ .node = cc }; if (c) |cc| return .{ .node = cc };
return null; return null;
} }
pub fn parent(n: Node) ?Node { pub fn parent(n: Node) !?Node {
const c = parser.nodeParentNode(n.node); const c = try parser.nodeParentNode(n.node);
if (c) |cc| return .{ .node = cc }; if (c) |cc| return .{ .node = cc };
return null; return null;
} }
pub fn isElement(n: Node) bool { pub fn isElement(n: Node) bool {
return parser.nodeType(n.node) == .element; const t = parser.nodeType(n.node) catch return false;
return t == .element;
} }
pub fn isDocument(n: Node) bool { pub fn isDocument(n: Node) bool {
return parser.nodeType(n.node) == .document; const t = parser.nodeType(n.node) catch return false;
return t == .document;
} }
pub fn isComment(n: Node) bool { pub fn isComment(n: Node) bool {
return parser.nodeType(n.node) == .comment; const t = parser.nodeType(n.node) catch return false;
return t == .comment;
} }
pub fn isText(n: Node) bool { pub fn isText(n: Node) bool {
return parser.nodeType(n.node) == .text; const t = parser.nodeType(n.node) catch return false;
return t == .text;
} }
pub fn text(n: Node) ?[]const u8 { pub fn isEmptyText(n: Node) !bool {
const data = parser.nodeTextContent(n.node); const data = try parser.nodeTextContent(n.node);
if (data == null) return null;
if (data.?.len == 0) return null;
return std.mem.trim(u8, data.?, &std.ascii.whitespace);
}
pub fn isEmptyText(n: Node) bool {
const data = parser.nodeTextContent(n.node);
if (data == null) return true; if (data == null) return true;
if (data.?.len == 0) return true; if (data.?.len == 0) return true;
@@ -94,7 +88,7 @@ pub const Node = struct {
} }
pub fn tag(n: Node) ![]const u8 { pub fn tag(n: Node) ![]const u8 {
return parser.nodeName(n.node); return try parser.nodeName(n.node);
} }
pub fn attr(n: Node, key: []const u8) !?[]const u8 { pub fn attr(n: Node, key: []const u8) !?[]const u8 {
@@ -106,318 +100,3 @@ pub const Node = struct {
return a.node == b.node; return a.node == b.node;
} }
}; };
const MatcherTest = struct {
const Nodes = std.ArrayListUnmanaged(Node);
nodes: Nodes,
allocator: Allocator,
fn init(allocator: Allocator) MatcherTest {
return .{
.nodes = .empty,
.allocator = allocator,
};
}
fn deinit(m: *MatcherTest) void {
m.nodes.deinit(m.allocator);
}
fn reset(m: *MatcherTest) void {
m.nodes.clearRetainingCapacity();
}
pub fn match(m: *MatcherTest, n: Node) !void {
try m.nodes.append(m.allocator, n);
}
};
test "Browser.CSS.Libdom: matchFirst" {
const alloc = std.testing.allocator;
parser.init();
defer parser.deinit();
var matcher = MatcherTest.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
html: []const u8,
exp: usize,
}{
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
.{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
.{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
.{ .q = ":containsOwn(\"Inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
.{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
.{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
.{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
.{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
};
for (testcases) |tc| {
matcher.reset();
const doc = try parser.documentHTMLParseFromStr(tc.html);
defer parser.documentHTMLClose(doc) catch {};
const s = css.parse(alloc, tc.q, .{}) catch |e| {
std.debug.print("parse, query: {s}\n", .{tc.q});
return e;
};
defer s.deinit(alloc);
const node = Node{ .node = parser.documentHTMLToNode(doc) };
_ = css.matchFirst(&s, node, &matcher) catch |e| {
std.debug.print("match, query: {s}\n", .{tc.q});
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("expectation, query: {s}\n", .{tc.q});
return e;
};
}
}
test "Browser.CSS.Libdom: matchAll" {
const alloc = std.testing.allocator;
parser.init();
defer parser.deinit();
var matcher = MatcherTest.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
html: []const u8,
exp: usize,
}{
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
.{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
.{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
.{ .q = ":containsOwn(\"Inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
.{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
.{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
.{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
.{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 2 },
};
for (testcases) |tc| {
matcher.reset();
const doc = try parser.documentHTMLParseFromStr(tc.html);
defer parser.documentHTMLClose(doc) catch {};
const s = css.parse(alloc, tc.q, .{}) catch |e| {
std.debug.print("parse, query: {s}\n", .{tc.q});
return e;
};
defer s.deinit(alloc);
const node = Node{ .node = parser.documentHTMLToNode(doc) };
_ = css.matchAll(&s, node, &matcher) catch |e| {
std.debug.print("match, query: {s}\n", .{tc.q});
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("expectation, query: {s}\n", .{tc.q});
return e;
};
}
}

View File

@@ -0,0 +1,330 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const css = @import("css.zig");
const Node = @import("libdom.zig").Node;
const parser = @import("../netsurf.zig");
const Allocator = std.mem.Allocator;
const Matcher = struct {
const Nodes = std.ArrayListUnmanaged(Node);
nodes: Nodes,
allocator: Allocator,
fn init(allocator: Allocator) Matcher {
return .{
.nodes = .empty,
.allocator = allocator,
};
}
fn deinit(m: *Matcher) void {
m.nodes.deinit(m.allocator);
}
fn reset(m: *Matcher) void {
m.nodes.clearRetainingCapacity();
}
pub fn match(m: *Matcher, n: Node) !void {
try m.nodes.append(m.allocator, n);
}
};
test "matchFirst" {
const alloc = std.testing.allocator;
var matcher = Matcher.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
html: []const u8,
exp: usize,
}{
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
};
for (testcases) |tc| {
matcher.reset();
const doc = try parser.documentHTMLParseFromStr(tc.html);
defer parser.documentHTMLClose(doc) catch {};
const s = css.parse(alloc, tc.q, .{}) catch |e| {
std.debug.print("parse, query: {s}\n", .{tc.q});
return e;
};
defer s.deinit(alloc);
const node = Node{ .node = parser.documentHTMLToNode(doc) };
_ = css.matchFirst(s, node, &matcher) catch |e| {
std.debug.print("match, query: {s}\n", .{tc.q});
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("expectation, query: {s}\n", .{tc.q});
return e;
};
}
}
test "matchAll" {
const alloc = std.testing.allocator;
var matcher = Matcher.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
html: []const u8,
exp: usize,
}{
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 2 },
};
for (testcases) |tc| {
matcher.reset();
const doc = try parser.documentHTMLParseFromStr(tc.html);
defer parser.documentHTMLClose(doc) catch {};
const s = css.parse(alloc, tc.q, .{}) catch |e| {
std.debug.print("parse, query: {s}\n", .{tc.q});
return e;
};
defer s.deinit(alloc);
const node = Node{ .node = parser.documentHTMLToNode(doc) };
_ = css.matchAll(s, node, &matcher) catch |e| {
std.debug.print("match, query: {s}\n", .{tc.q});
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("expectation, query: {s}\n", .{tc.q});
return e;
};
}
}

View File

@@ -0,0 +1,591 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const css = @import("css.zig");
// Node mock implementation for test only.
pub const Node = struct {
child: ?*const Node = null,
last: ?*const Node = null,
sibling: ?*const Node = null,
prev: ?*const Node = null,
par: ?*const Node = null,
name: []const u8 = "",
att: ?[]const u8 = null,
pub fn firstChild(n: *const Node) !?*const Node {
return n.child;
}
pub fn lastChild(n: *const Node) !?*const Node {
return n.last;
}
pub fn nextSibling(n: *const Node) !?*const Node {
return n.sibling;
}
pub fn prevSibling(n: *const Node) !?*const Node {
return n.prev;
}
pub fn parent(n: *const Node) !?*const Node {
return n.par;
}
pub fn isElement(_: *const Node) bool {
return true;
}
pub fn isDocument(_: *const Node) bool {
return false;
}
pub fn isComment(_: *const Node) bool {
return false;
}
pub fn isText(_: *const Node) bool {
return false;
}
pub fn isEmptyText(_: *const Node) !bool {
return false;
}
pub fn tag(n: *const Node) ![]const u8 {
return n.name;
}
pub fn attr(n: *const Node, _: []const u8) !?[]const u8 {
return n.att;
}
pub fn eql(a: *const Node, b: *const Node) bool {
return a == b;
}
};
const Matcher = struct {
const Nodes = std.ArrayListUnmanaged(*const Node);
nodes: Nodes,
allocator: Allocator,
fn init(allocator: Allocator) Matcher {
return .{
.nodes = .empty,
.allocator = allocator,
};
}
fn deinit(m: *Matcher) void {
m.nodes.deinit(self.allocator);
}
fn reset(m: *Matcher) void {
m.nodes.clearRetainingCapacity();
}
pub fn match(m: *Matcher, n: *const Node) !void {
try m.nodes.append(self.allocator, n);
}
};
test "matchFirst" {
const alloc = std.testing.allocator;
var matcher = Matcher.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
n: Node,
exp: usize,
}{
.{
.q = "address",
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
.exp = 1,
},
.{
.q = "#foo",
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
.exp = 1,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
.exp = 0,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 0,
},
.{
.q = "[foo!=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo!=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
.exp = 1,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 0,
},
.{
.q = "[foo^=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo$=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo*=rb]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
.exp = 0,
},
.{
.q = "strong, a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
.name = "a",
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
} } } },
.exp = 1,
},
.{
.q = ":not(p)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:has(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:has(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:haschild(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:haschild(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:lang(en)",
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "a:lang(en)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
.exp = 1,
},
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
_ = css.matchFirst(s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}
test "matchAll" {
const alloc = std.testing.allocator;
var matcher = Matcher.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
n: Node,
exp: usize,
}{
.{
.q = "address",
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
.exp = 1,
},
.{
.q = "#foo",
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
.exp = 1,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
.exp = 0,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 0,
},
.{
.q = "[foo!=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo!=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 2,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
.exp = 1,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 0,
},
.{
.q = "[foo^=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo$=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo*=rb]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
.exp = 0,
},
.{
.q = "strong, a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 2,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
.name = "a",
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
} } } },
.exp = 1,
},
.{
.q = ":not(p)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 2,
},
.{
.q = "p:has(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:has(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:haschild(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:haschild(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:lang(en)",
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "a:lang(en)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
.exp = 1,
},
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
css.matchAll(s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}
test "pseudo class" {
const alloc = std.testing.allocator;
var matcher = Matcher.init(alloc);
defer matcher.deinit();
var p1: Node = .{ .name = "p" };
var p2: Node = .{ .name = "p" };
var a1: Node = .{ .name = "a" };
p1.sibling = &p2;
p2.prev = &p1;
p2.sibling = &a1;
a1.prev = &p2;
var root: Node = .{ .child = &p1, .last = &a1 };
p1.par = &root;
p2.par = &root;
a1.par = &root;
const testcases = [_]struct {
q: []const u8,
n: Node,
exp: ?*const Node,
}{
.{ .q = "p:only-child", .n = root, .exp = null },
.{ .q = "a:only-of-type", .n = root, .exp = &a1 },
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
css.matchAll(s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
if (tc.exp) |exp_n| {
const exp: usize = 1;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
continue;
}
const exp: usize = 0;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}
test "nth pseudo class" {
const alloc = std.testing.allocator;
var matcher = Matcher.init(alloc);
defer matcher.deinit();
var p1: Node = .{ .name = "p" };
var p2: Node = .{ .name = "p" };
p1.sibling = &p2;
p2.prev = &p1;
var root: Node = .{ .child = &p1, .last = &p2 };
p1.par = &root;
p2.par = &root;
const testcases = [_]struct {
q: []const u8,
n: Node,
exp: ?*const Node,
}{
.{ .q = "a:nth-of-type(1)", .n = root, .exp = null },
.{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 },
.{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 },
.{ .q = "p:nth-of-type(0)", .n = root, .exp = null },
.{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 },
.{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 },
.{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 },
.{ .q = "p:nth-child(1)", .n = root, .exp = &p1 },
.{ .q = "p:nth-child(2)", .n = root, .exp = &p2 },
.{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 },
.{ .q = "p:nth-child(even)", .n = root, .exp = &p2 },
.{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 },
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
css.matchAll(s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
if (tc.exp) |exp_n| {
const exp: usize = 1;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
continue;
}
const exp: usize = 0;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}

View File

@@ -557,6 +557,8 @@ pub const Parser = struct {
const val = try buf.toOwnedSlice(allocator); const val = try buf.toOwnedSlice(allocator);
errdefer allocator.free(val); errdefer allocator.free(val);
lowerstr(val);
return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } }; return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } };
}, },
.matches, .matchesown => { .matches, .matchesown => {
@@ -821,8 +823,7 @@ pub const Parser = struct {
// nameStart returns whether c can be the first character of an identifier // nameStart returns whether c can be the first character of an identifier
// (not counting an initial hyphen, or an escape sequence). // (not counting an initial hyphen, or an escape sequence).
fn nameStart(c: u8) bool { fn nameStart(c: u8) bool {
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127 or return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127;
'0' <= c and c <= '9';
} }
// nameChar returns whether c can be a character within an identifier // nameChar returns whether c can be a character within an identifier
@@ -891,7 +892,7 @@ test "parser.parseIdentifier" {
err: bool = false, err: bool = false,
}{ }{
.{ .s = "x", .exp = "x" }, .{ .s = "x", .exp = "x" },
.{ .s = "96", .exp = "96", .err = false }, .{ .s = "96", .exp = "", .err = true },
.{ .s = "-x", .exp = "-x" }, .{ .s = "-x", .exp = "-x" },
.{ .s = "r\\e9 sumé", .exp = "résumé" }, .{ .s = "r\\e9 sumé", .exp = "résumé" },
.{ .s = "r\\0000e9 sumé", .exp = "résumé" }, .{ .s = "r\\0000e9 sumé", .exp = "résumé" },
@@ -976,7 +977,6 @@ test "parser.parse" {
.{ .s = ":root", .exp = .{ .pseudo_class = .root } }, .{ .s = ":root", .exp = .{ .pseudo_class = .root } },
.{ .s = ".\\:bar", .exp = .{ .class = ":bar" } }, .{ .s = ".\\:bar", .exp = .{ .class = ":bar" } },
.{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } }, .{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } },
.{ .s = "[class=75c0fa18a94b9e3a6b8e14d6cbe688a27f5da10a]", .exp = .{ .attribute = .{ .key = "class", .val = "75c0fa18a94b9e3a6b8e14d6cbe688a27f5da10a", .op = .eql } } },
}; };
for (testcases) |tc| { for (testcases) |tc| {

View File

@@ -17,8 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const css = @import("css.zig");
pub const AttributeOP = enum { pub const AttributeOP = enum {
eql, // = eql, // =
@@ -334,39 +332,41 @@ pub const Selector = union(enum) {
if (!try v.second.match(n)) return false; if (!try v.second.match(n)) return false;
// The first must match a ascendent. // The first must match a ascendent.
var parent = n.parent(); var p = try n.parent();
while (parent) |p| { while (p != null) {
if (try v.first.match(p)) { if (try v.first.match(p.?)) {
return true; return true;
} }
parent = p.parent(); p = try p.?.parent();
} }
return false; return false;
}, },
.child => { .child => {
const p = n.parent() orelse return false; const p = try n.parent();
return try v.second.match(n) and try v.first.match(p); if (p == null) return false;
return try v.second.match(n) and try v.first.match(p.?);
}, },
.next_sibling => { .next_sibling => {
if (!try v.second.match(n)) return false; if (!try v.second.match(n)) return false;
var child = n.prevSibling(); var c = try n.prevSibling();
while (child) |c| { while (c != null) {
if (c.isText() or c.isComment()) { if (c.?.isText() or c.?.isComment()) {
child = c.prevSibling(); c = try c.?.prevSibling();
continue; continue;
} }
return try v.first.match(c); return try v.first.match(c.?);
} }
return false; return false;
}, },
.subsequent_sibling => { .subsequent_sibling => {
if (!try v.second.match(n)) return false; if (!try v.second.match(n)) return false;
var child = n.prevSibling(); var c = try n.prevSibling();
while (child) |c| { while (c != null) {
if (try v.first.match(c)) return true; if (try v.first.match(c.?)) return true;
child = c.prevSibling(); c = try c.?.prevSibling();
} }
return false; return false;
}, },
@@ -432,25 +432,7 @@ pub const Selector = union(enum) {
else => Error.UnsupportedRelativePseudoClass, else => Error.UnsupportedRelativePseudoClass,
}; };
}, },
.pseudo_class_contains => |v| { .pseudo_class_contains => return Error.UnsupportedContainsPseudoClass, // TODO, need mem allocation.
// Only containsOwn is implemented.
if (v.own == false) return Error.UnsupportedContainsPseudoClass;
var child = n.firstChild();
while (child) |c| {
if (c.isText()) {
const text = c.text();
if (text) |_text| {
if (contains(_text, v.val, false)) { // we are case sensitive. Is this correct behavior?
return true;
}
}
}
child = c.nextSibling();
}
return false;
},
.pseudo_class_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation. .pseudo_class_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation.
.pseudo_class_nth => |v| { .pseudo_class_nth => |v| {
if (v.a == 0) { if (v.a == 0) {
@@ -475,16 +457,16 @@ pub const Selector = union(enum) {
.empty => { .empty => {
if (!n.isElement()) return false; if (!n.isElement()) return false;
var child = n.firstChild(); var c = try n.firstChild();
while (child) |c| { while (c != null) {
if (c.isElement()) return false; if (c.?.isElement()) return false;
if (c.isText()) { if (c.?.isText()) {
if (c.isEmptyText()) continue; if (try c.?.isEmptyText()) continue;
return false; return false;
} }
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return true; return true;
@@ -492,7 +474,7 @@ pub const Selector = union(enum) {
.root => { .root => {
if (!n.isElement()) return false; if (!n.isElement()) return false;
const p = n.parent(); const p = try n.parent();
return (p != null and p.?.isDocument()); return (p != null and p.?.isDocument());
}, },
.link => { .link => {
@@ -562,7 +544,7 @@ pub const Selector = union(enum) {
const ntag = try n.tag(); const ntag = try n.tag();
if (std.ascii.eqlIgnoreCase("input", ntag)) { if (std.ascii.eqlIgnoreCase("intput", ntag)) {
const ntype = try n.attr("type"); const ntype = try n.attr("type");
if (ntype == null) return false; if (ntype == null) return false;
@@ -607,23 +589,24 @@ pub const Selector = union(enum) {
} }
fn hasLegendInPreviousSiblings(n: anytype) anyerror!bool { fn hasLegendInPreviousSiblings(n: anytype) anyerror!bool {
var child = n.prevSibling(); var c = try n.prevSibling();
while (child) |c| { while (c != null) {
const ctag = try c.tag(); const ctag = try c.?.tag();
if (std.ascii.eqlIgnoreCase("legend", ctag)) return true; if (std.ascii.eqlIgnoreCase("legend", ctag)) return true;
child = c.prevSibling(); c = try c.?.prevSibling();
} }
return false; return false;
} }
fn inDisabledFieldset(n: anytype) anyerror!bool { fn inDisabledFieldset(n: anytype) anyerror!bool {
const p = n.parent() orelse return false; const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag(); const ntag = try n.tag();
const ptag = try p.tag(); const ptag = try p.?.tag();
if (std.ascii.eqlIgnoreCase("fieldset", ptag) and if (std.ascii.eqlIgnoreCase("fieldset", ptag) and
try p.attr("disabled") != null and try p.?.attr("disabled") != null and
(!std.ascii.eqlIgnoreCase("legend", ntag) or try hasLegendInPreviousSiblings(n))) (!std.ascii.eqlIgnoreCase("legend", ntag) or try hasLegendInPreviousSiblings(n)))
{ {
return true; return true;
@@ -639,7 +622,7 @@ pub const Selector = union(enum) {
// ``` // ```
// https://github.com/andybalholm/cascadia/blob/master/pseudo_classes.go#L434 // https://github.com/andybalholm/cascadia/blob/master/pseudo_classes.go#L434
return try inDisabledFieldset(p); return try inDisabledFieldset(p.?);
} }
fn langMatch(lang: []const u8, n: anytype) anyerror!bool { fn langMatch(lang: []const u8, n: anytype) anyerror!bool {
@@ -653,8 +636,10 @@ pub const Selector = union(enum) {
} }
// if the tag doesn't match, try the parent. // if the tag doesn't match, try the parent.
const p = n.parent() orelse return false; const p = try n.parent();
return langMatch(lang, p); if (p == null) return false;
return langMatch(lang, p.?);
} }
// onlyChildMatch implements :only-child // onlyChildMatch implements :only-child
@@ -662,24 +647,25 @@ pub const Selector = union(enum) {
fn onlyChildMatch(of_type: bool, n: anytype) anyerror!bool { fn onlyChildMatch(of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false; if (!n.isElement()) return false;
const p = n.parent() orelse return false; const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag(); const ntag = try n.tag();
var count: usize = 0; var count: usize = 0;
var child = p.firstChild(); var c = try p.?.firstChild();
// loop hover all n siblings. // loop hover all n siblings.
while (child) |c| { while (c != null) {
// ignore non elements or others tags if of-type is true. // ignore non elements or others tags if of-type is true.
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) { if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
child = c.nextSibling(); c = try c.?.nextSibling();
continue; continue;
} }
count += 1; count += 1;
if (count > 1) return false; if (count > 1) return false;
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return count == 1; return count == 1;
@@ -690,25 +676,27 @@ pub const Selector = union(enum) {
fn simpleNthLastChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool { fn simpleNthLastChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false; if (!n.isElement()) return false;
const p = n.parent() orelse return false; const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag(); const ntag = try n.tag();
var count: isize = 0; var count: isize = 0;
var child = p.lastChild(); var c = try p.?.lastChild();
// loop hover all n siblings. // loop hover all n siblings.
while (child) |c| { while (c != null) {
// ignore non elements or others tags if of-type is true. // ignore non elements or others tags if of-type is true.
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) { if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
child = c.prevSibling(); c = try c.?.prevSibling();
continue; continue;
} }
count += 1; count += 1;
if (n.eql(c)) return count == b; if (n.eql(c.?)) return count == b;
if (count >= b) return false; if (count >= b) return false;
child = c.prevSibling(); c = try c.?.prevSibling();
} }
return false; return false;
@@ -719,25 +707,27 @@ pub const Selector = union(enum) {
fn simpleNthChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool { fn simpleNthChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false; if (!n.isElement()) return false;
const p = n.parent() orelse return false; const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag(); const ntag = try n.tag();
var count: isize = 0; var count: isize = 0;
var child = p.firstChild(); var c = try p.?.firstChild();
// loop hover all n siblings. // loop hover all n siblings.
while (child) |c| { while (c != null) {
// ignore non elements or others tags if of-type is true. // ignore non elements or others tags if of-type is true.
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) { if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
child = c.nextSibling(); c = try c.?.nextSibling();
continue; continue;
} }
count += 1; count += 1;
if (n.eql(c)) return count == b; if (n.eql(c.?)) return count == b;
if (count >= b) return false; if (count >= b) return false;
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return false; return false;
@@ -749,27 +739,29 @@ pub const Selector = union(enum) {
fn nthChildMatch(a: isize, b: isize, last: bool, of_type: bool, n: anytype) anyerror!bool { fn nthChildMatch(a: isize, b: isize, last: bool, of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false; if (!n.isElement()) return false;
const p = n.parent() orelse return false; const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag(); const ntag = try n.tag();
var i: isize = -1; var i: isize = -1;
var count: isize = 0; var count: isize = 0;
var child = p.firstChild(); var c = try p.?.firstChild();
// loop hover all n siblings. // loop hover all n siblings.
while (child) |c| { while (c != null) {
// ignore non elements or others tags if of-type is true. // ignore non elements or others tags if of-type is true.
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) { if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
child = c.nextSibling(); c = try c.?.nextSibling();
continue; continue;
} }
count += 1; count += 1;
if (n.eql(c)) { if (n.eql(c.?)) {
i = count; i = count;
if (!last) break; if (!last) break;
} }
child = c.nextSibling(); c = try c.?.nextSibling();
} }
if (i == -1) return false; if (i == -1) return false;
@@ -782,21 +774,21 @@ pub const Selector = union(enum) {
} }
fn hasDescendantMatch(s: *const Selector, n: anytype) anyerror!bool { fn hasDescendantMatch(s: *const Selector, n: anytype) anyerror!bool {
var child = n.firstChild(); var c = try n.firstChild();
while (child) |c| { while (c != null) {
if (try s.match(c)) return true; if (try s.match(c.?)) return true;
if (c.isElement() and try hasDescendantMatch(s, c)) return true; if (c.?.isElement() and try hasDescendantMatch(s, c.?)) return true;
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return false; return false;
} }
fn hasChildMatch(s: *const Selector, n: anytype) anyerror!bool { fn hasChildMatch(s: *const Selector, n: anytype) anyerror!bool {
var child = n.firstChild(); var c = try n.firstChild();
while (child) |c| { while (c != null) {
if (try s.match(c)) return true; if (try s.match(c.?)) return true;
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return false; return false;
@@ -835,583 +827,3 @@ pub const Selector = union(enum) {
} }
} }
}; };
// NodeTest mock implementation for test only.
pub const NodeTest = struct {
child: ?*const NodeTest = null,
last: ?*const NodeTest = null,
sibling: ?*const NodeTest = null,
prev: ?*const NodeTest = null,
par: ?*const NodeTest = null,
name: []const u8 = "",
att: ?[]const u8 = null,
pub fn firstChild(n: *const NodeTest) ?*const NodeTest {
return n.child;
}
pub fn lastChild(n: *const NodeTest) ?*const NodeTest {
return n.last;
}
pub fn nextSibling(n: *const NodeTest) ?*const NodeTest {
return n.sibling;
}
pub fn prevSibling(n: *const NodeTest) ?*const NodeTest {
return n.prev;
}
pub fn parent(n: *const NodeTest) ?*const NodeTest {
return n.par;
}
pub fn isElement(_: *const NodeTest) bool {
return true;
}
pub fn isDocument(_: *const NodeTest) bool {
return false;
}
pub fn isComment(_: *const NodeTest) bool {
return false;
}
pub fn text(_: *const NodeTest) ?[]const u8 {
return null;
}
pub fn isText(_: *const NodeTest) bool {
return false;
}
pub fn isEmptyText(_: *const NodeTest) bool {
return false;
}
pub fn tag(n: *const NodeTest) ![]const u8 {
return n.name;
}
pub fn attr(n: *const NodeTest, _: []const u8) !?[]const u8 {
return n.att;
}
pub fn eql(a: *const NodeTest, b: *const NodeTest) bool {
return a == b;
}
};
const MatcherTest = struct {
const NodeTests = std.ArrayListUnmanaged(*const NodeTest);
nodes: NodeTests,
allocator: Allocator,
fn init(allocator: Allocator) MatcherTest {
return .{
.nodes = .empty,
.allocator = allocator,
};
}
fn deinit(m: *MatcherTest) void {
m.nodes.deinit(m.allocator);
}
fn reset(m: *MatcherTest) void {
m.nodes.clearRetainingCapacity();
}
pub fn match(m: *MatcherTest, n: *const NodeTest) !void {
try m.nodes.append(m.allocator, n);
}
};
test "Browser.CSS.Selector: matchFirst" {
const alloc = std.testing.allocator;
var matcher = MatcherTest.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
n: NodeTest,
exp: usize,
}{
.{
.q = "address",
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
.exp = 1,
},
.{
.q = "#foo",
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
.exp = 1,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
.exp = 0,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 0,
},
.{
.q = "[foo=1baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 0,
},
.{
.q = "[foo!=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo!=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
.exp = 1,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 0,
},
.{
.q = "[foo^=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo$=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo*=rb]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
.exp = 0,
},
.{
.q = "strong, a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
.name = "a",
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
} } } },
.exp = 1,
},
.{
.q = ":not(p)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:has(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:has(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:haschild(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:haschild(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:lang(en)",
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "a:lang(en)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
.exp = 1,
},
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
_ = css.matchFirst(&s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}
test "Browser.CSS.Selector: matchAll" {
const alloc = std.testing.allocator;
var matcher = MatcherTest.init(alloc);
defer matcher.deinit();
const testcases = [_]struct {
q: []const u8,
n: NodeTest,
exp: usize,
}{
.{
.q = "address",
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
.exp = 1,
},
.{
.q = "#foo",
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
.exp = 1,
},
.{
.q = ".t1",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
.exp = 1,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
.exp = 0,
},
.{
.q = "[foo]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 0,
},
.{
.q = "[foo!=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo!=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 2,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
.exp = 1,
},
.{
.q = "[foo~=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 0,
},
.{
.q = "[foo^=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo$=baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo*=rb]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
.exp = 1,
},
.{
.q = "[foo|=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
.exp = 0,
},
.{
.q = "strong, a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 2,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "p a",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
.name = "a",
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
} } } },
.exp = 1,
},
.{
.q = ":not(p)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 2,
},
.{
.q = "p:has(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:has(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:haschild(a)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 1,
},
.{
.q = "p:haschild(strong)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
.exp = 0,
},
.{
.q = "p:lang(en)",
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
.exp = 1,
},
.{
.q = "a:lang(en)",
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
.exp = 1,
},
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
css.matchAll(&s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}
test "Browser.CSS.Selector: pseudo class" {
const alloc = std.testing.allocator;
var matcher = MatcherTest.init(alloc);
defer matcher.deinit();
var p1: NodeTest = .{ .name = "p" };
var p2: NodeTest = .{ .name = "p" };
var a1: NodeTest = .{ .name = "a" };
p1.sibling = &p2;
p2.prev = &p1;
p2.sibling = &a1;
a1.prev = &p2;
var root: NodeTest = .{ .child = &p1, .last = &a1 };
p1.par = &root;
p2.par = &root;
a1.par = &root;
const testcases = [_]struct {
q: []const u8,
n: NodeTest,
exp: ?*const NodeTest,
}{
.{ .q = "p:only-child", .n = root, .exp = null },
.{ .q = "a:only-of-type", .n = root, .exp = &a1 },
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
css.matchAll(&s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
if (tc.exp) |exp_n| {
const exp: usize = 1;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
continue;
}
const exp: usize = 0;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}
test "Browser.CSS.Selector: nth pseudo class" {
const alloc = std.testing.allocator;
var matcher = MatcherTest.init(alloc);
defer matcher.deinit();
var p1: NodeTest = .{ .name = "p" };
var p2: NodeTest = .{ .name = "p" };
p1.sibling = &p2;
p2.prev = &p1;
var root: NodeTest = .{ .child = &p1, .last = &p2 };
p1.par = &root;
p2.par = &root;
const testcases = [_]struct {
q: []const u8,
n: NodeTest,
exp: ?*const NodeTest,
}{
.{ .q = "a:nth-of-type(1)", .n = root, .exp = null },
.{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 },
.{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 },
.{ .q = "p:nth-of-type(0)", .n = root, .exp = null },
.{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 },
.{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 },
.{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 },
.{ .q = "p:nth-child(1)", .n = root, .exp = &p1 },
.{ .q = "p:nth-child(2)", .n = root, .exp = &p2 },
.{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 },
.{ .q = "p:nth-child(even)", .n = root, .exp = &p2 },
.{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 },
};
for (testcases) |tc| {
matcher.reset();
const s = try css.parse(alloc, tc.q, .{});
defer s.deinit(alloc);
css.matchAll(&s, &tc.n, &matcher) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
if (tc.exp) |exp_n| {
const exp: usize = 1;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
continue;
}
const exp: usize = 0;
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
return e;
};
}
}

View File

@@ -19,6 +19,7 @@
const std = @import("std"); const std = @import("std");
const CSSRule = @import("CSSRule.zig"); const CSSRule = @import("CSSRule.zig");
const StyleSheet = @import("StyleSheet.zig").StyleSheet;
const CSSImportRule = CSSRule.CSSImportRule; const CSSImportRule = CSSRule.CSSImportRule;

View File

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

View File

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

View File

@@ -1,329 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const Node = @import("node.zig").Node;
const Element = @import("element.zig").Element;
pub const Interfaces = .{
IntersectionObserver,
Entry,
};
// This implementation attempts to be as less wrong as possible. Since we don't
// render, or know how things are positioned, our best guess isn't very good.
const IntersectionObserver = @This();
page: *Page,
root: *parser.Node,
callback: js.Function,
event_node: parser.EventNode,
observed_entries: std.ArrayList(Entry),
pending_elements: std.ArrayList(*parser.Element),
ready_elements: std.ArrayList(*parser.Element),
pub fn constructor(callback: js.Function, opts_: ?IntersectionObserverOptions, page: *Page) !*IntersectionObserver {
const opts = opts_ orelse IntersectionObserverOptions{};
const self = try page.arena.create(IntersectionObserver);
self.* = .{
.page = page,
.callback = callback,
.ready_elements = .{},
.observed_entries = .{},
.pending_elements = .{},
.event_node = .{ .func = mutationCallback },
.root = opts.root orelse parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
};
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, self.root),
"DOMNodeInserted",
&self.event_node,
false,
);
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, self.root),
"DOMNodeRemoved",
&self.event_node,
false,
);
return self;
}
pub fn _disconnect(self: *IntersectionObserver) !void {
// We don't free as it is on an arena
self.ready_elements = .{};
self.observed_entries = .{};
self.pending_elements = .{};
}
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element, page: *Page) !void {
for (self.observed_entries.items) |*observer| {
if (observer.target == target_element) {
return; // Already observed
}
}
if (self.isPending(target_element)) {
return; // Already pending
}
for (self.ready_elements.items) |element| {
if (element == target_element) {
return; // Already primed
}
}
// We can never fire callbacks synchronously. Code like React expects any
// callback to fire in the future (e.g. via microtasks).
try self.ready_elements.append(self.page.arena, target_element);
if (self.ready_elements.items.len == 1) {
// this is our first ready entry, schedule a callback
try page.scheduler.add(self, processReady, 0, .{
.name = "intersection ready",
});
}
}
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
if (self.removeObserved(target)) {
return;
}
for (self.ready_elements.items, 0..) |el, index| {
if (el == target) {
_ = self.ready_elements.swapRemove(index);
return;
}
}
for (self.pending_elements.items, 0..) |el, index| {
if (el == target) {
_ = self.pending_elements.swapRemove(index);
return;
}
}
}
pub fn _takeRecords(self: *IntersectionObserver) []Entry {
return self.observed_entries.items;
}
fn processReady(ctx: *anyopaque) ?u32 {
const self: *IntersectionObserver = @ptrCast(@alignCast(ctx));
self._processReady() catch |err| {
log.err(.web_api, "intersection ready", .{ .err = err });
};
return null;
}
fn _processReady(self: *IntersectionObserver) !void {
defer self.ready_elements.clearRetainingCapacity();
for (self.ready_elements.items) |element| {
// IntersectionObserver probably doesn't work like what your intuition
// thinks. As long as a node has a parent, even if that parent isn't
// connected and even if the two nodes don't intersect, it'll fire the
// callback once.
if (try Node.get_parentNode(@ptrCast(element)) == null) {
if (!self.isPending(element)) {
try self.pending_elements.append(self.page.arena, element);
}
continue;
}
try self.forceObserve(element);
}
}
fn isPending(self: *IntersectionObserver, element: *parser.Element) bool {
for (self.pending_elements.items) |el| {
if (el == element) {
return true;
}
}
return false;
}
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
const mutation_event = parser.eventToMutationEvent(event);
const self: *IntersectionObserver = @fieldParentPtr("event_node", en);
self._mutationCallback(mutation_event) catch |err| {
log.err(.web_api, "mutation callback", .{ .err = err, .source = "intersection observer" });
};
}
fn _mutationCallback(self: *IntersectionObserver, event: *parser.MutationEvent) !void {
const event_type = parser.eventType(@ptrCast(event));
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
if (parser.nodeType(node) != .element) {
return;
}
const el: *parser.Element = @ptrCast(node);
if (self.removePending(el)) {
// It was pending (because it wasn't in the root), but now it is
// we should observe it.
try self.forceObserve(el);
}
return;
}
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
if (parser.nodeType(node) != .element) {
return;
}
const el: *parser.Element = @ptrCast(node);
if (self.removeObserved(el)) {
// It _was_ observed, it no longer is in our root, but if it was
// to get re-added, it should be observed again (I think), so
// we add it to our pending list
try self.pending_elements.append(self.page.arena, el);
}
return;
}
// impossible event type
unreachable;
}
// Exists to skip the checks made _observe when called from a DOMNodeInserted
// event. In such events, the event handler has alread done the necessary
// checks.
fn forceObserve(self: *IntersectionObserver, target: *parser.Element) !void {
try self.observed_entries.append(self.page.arena, .{
.page = self.page,
.root = self.root,
.target = target,
});
var result: js.Function.Result = undefined;
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
log.debug(.user_script, "callback error", .{
.err = result.exception,
.stack = result.stack,
.source = "intersection observer",
});
};
}
fn removeObserved(self: *IntersectionObserver, target: *parser.Element) bool {
for (self.observed_entries.items, 0..) |*observer, index| {
if (observer.target == target) {
_ = self.observed_entries.swapRemove(index);
return true;
}
}
return false;
}
fn removePending(self: *IntersectionObserver, target: *parser.Element) bool {
for (self.pending_elements.items, 0..) |el, index| {
if (el == target) {
_ = self.pending_elements.swapRemove(index);
return true;
}
}
return false;
}
const IntersectionObserverOptions = struct {
root: ?*parser.Node = null, // Element or Document
rootMargin: ?[]const u8 = "0px 0px 0px 0px",
threshold: ?Threshold = .{ .single = 0.0 },
const Threshold = union(enum) {
single: f32,
list: []const f32,
};
};
// https://developer.mozilla.org/en-US/docs/Web/API/Entry
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
pub const Entry = struct {
page: *Page,
root: *parser.Node,
target: *parser.Element,
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
pub fn get_boundingClientRect(self: *const Entry) !Element.DOMRect {
return Element._getBoundingClientRect(self.target, self.page);
}
// Returns the ratio of the intersectionRect to the boundingClientRect.
pub fn get_intersectionRatio(_: *const Entry) f32 {
return 1.0;
}
// Returns a DOMRectReadOnly representing the target's visible area.
pub fn get_intersectionRect(self: *const Entry) !Element.DOMRect {
return Element._getBoundingClientRect(self.target, self.page);
}
// A Boolean value which is true if the target element intersects with the
// intersection observer's root. If this is true, then, the
// Entry describes a transition into a state of
// intersection; if it's false, then you know the transition is from
// intersecting to not-intersecting.
pub fn get_isIntersecting(_: *const Entry) bool {
return true;
}
// Returns a DOMRectReadOnly for the intersection observer's root.
pub fn get_rootBounds(self: *const Entry) !Element.DOMRect {
const root = self.root;
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
return self.page.renderer.boundingRect();
}
const root_type = parser.nodeType(root);
var element: *parser.Element = undefined;
switch (root_type) {
.element => element = parser.nodeToElement(root),
.document => {
const doc = parser.nodeToDocument(root);
element = (try parser.documentGetDocumentElement(doc)).?;
},
else => return error.InvalidState,
}
return Element._getBoundingClientRect(element, self.page);
}
// The Element whose intersection with the root changed.
pub fn get_target(self: *const Entry) *parser.Element {
return self.target;
}
// TODO: pub fn get_time(self: *const Entry)
};
const testing = @import("../../testing.zig");
test "Browser: DOM.IntersectionObserver" {
try testing.htmlRunner("dom/intersection_observer.html");
}

View File

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

View File

@@ -25,24 +25,24 @@ pub const Attr = struct {
pub const prototype = *Node; pub const prototype = *Node;
pub const subtype = .node; pub const subtype = .node;
pub fn get_namespaceURI(self: *parser.Attribute) ?[]const u8 { pub fn get_namespaceURI(self: *parser.Attribute) !?[]const u8 {
return parser.nodeGetNamespace(parser.attributeToNode(self)); return try parser.nodeGetNamespace(parser.attributeToNode(self));
} }
pub fn get_prefix(self: *parser.Attribute) ?[]const u8 { pub fn get_prefix(self: *parser.Attribute) !?[]const u8 {
return parser.nodeGetPrefix(parser.attributeToNode(self)); return try parser.nodeGetPrefix(parser.attributeToNode(self));
} }
pub fn get_localName(self: *parser.Attribute) ![]const u8 { pub fn get_localName(self: *parser.Attribute) ![]const u8 {
return parser.nodeLocalName(parser.attributeToNode(self)); return try parser.nodeLocalName(parser.attributeToNode(self));
} }
pub fn get_name(self: *parser.Attribute) ![]const u8 { pub fn get_name(self: *parser.Attribute) ![]const u8 {
return parser.attributeGetName(self); return try parser.attributeGetName(self);
} }
pub fn get_value(self: *parser.Attribute) !?[]const u8 { pub fn get_value(self: *parser.Attribute) !?[]const u8 {
return parser.attributeGetValue(self); return try parser.attributeGetValue(self);
} }
pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 { pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 {

View File

@@ -51,7 +51,7 @@ pub const CharacterData = struct {
} }
pub fn get_nextElementSibling(self: *parser.CharacterData) !?ElementUnion { pub fn get_nextElementSibling(self: *parser.CharacterData) !?ElementUnion {
const res = parser.nodeNextElementSibling(parser.characterDataToNode(self)); const res = try parser.nodeNextElementSibling(parser.characterDataToNode(self));
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -59,7 +59,7 @@ pub const CharacterData = struct {
} }
pub fn get_previousElementSibling(self: *parser.CharacterData) !?ElementUnion { pub fn get_previousElementSibling(self: *parser.CharacterData) !?ElementUnion {
const res = parser.nodePreviousElementSibling(parser.characterDataToNode(self)); const res = try parser.nodePreviousElementSibling(parser.characterDataToNode(self));
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -68,8 +68,8 @@ pub const CharacterData = struct {
// Read/Write attributes // Read/Write attributes
pub fn get_data(self: *parser.CharacterData) []const u8 { pub fn get_data(self: *parser.CharacterData) ![]const u8 {
return parser.characterDataData(self); return try parser.characterDataData(self);
} }
pub fn set_data(self: *parser.CharacterData, data: []const u8) !void { pub fn set_data(self: *parser.CharacterData, data: []const u8) !void {
@@ -96,18 +96,18 @@ pub const CharacterData = struct {
} }
pub fn _substringData(self: *parser.CharacterData, offset: u32, count: u32) ![]const u8 { pub fn _substringData(self: *parser.CharacterData, offset: u32, count: u32) ![]const u8 {
return parser.characterDataSubstringData(self, offset, count); return try parser.characterDataSubstringData(self, offset, count);
} }
// netsurf's CharacterData (text, comment) doesn't implement the // netsurf's CharacterData (text, comment) doesn't implement the
// dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode. // dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode.
pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) bool { pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) !bool {
if (parser.nodeType(@ptrCast(@alignCast(self))) != parser.nodeType(other_node)) { if (try parser.nodeType(@ptrCast(@alignCast(self))) != try parser.nodeType(other_node)) {
return false; return false;
} }
const other: *parser.CharacterData = @ptrCast(other_node); const other: *parser.CharacterData = @ptrCast(other_node);
if (std.mem.eql(u8, get_data(self), get_data(other)) == false) { if (std.mem.eql(u8, try get_data(self), try get_data(other)) == false) {
return false; return false;
} }

View File

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

View File

@@ -38,8 +38,8 @@ pub const DocumentFragment = struct {
); );
} }
pub fn _isEqualNode(self: *parser.DocumentFragment, other_node: *parser.Node) bool { pub fn _isEqualNode(self: *parser.DocumentFragment, other_node: *parser.Node) !bool {
const other_type = parser.nodeType(other_node); const other_type = try parser.nodeType(other_node);
if (other_type != .document_fragment) { if (other_type != .document_fragment) {
return false; return false;
} }

View File

@@ -29,21 +29,21 @@ pub const DocumentType = struct {
pub const subtype = .node; pub const subtype = .node;
pub fn get_name(self: *parser.DocumentType) ![]const u8 { pub fn get_name(self: *parser.DocumentType) ![]const u8 {
return parser.documentTypeGetName(self); return try parser.documentTypeGetName(self);
} }
pub fn get_publicId(self: *parser.DocumentType) []const u8 { pub fn get_publicId(self: *parser.DocumentType) ![]const u8 {
return parser.documentTypeGetPublicId(self); return try parser.documentTypeGetPublicId(self);
} }
pub fn get_systemId(self: *parser.DocumentType) []const u8 { pub fn get_systemId(self: *parser.DocumentType) ![]const u8 {
return parser.documentTypeGetSystemId(self); return try parser.documentTypeGetSystemId(self);
} }
// netsurf's DocumentType doesn't implement the dom_node_get_attributes // netsurf's DocumentType doesn't implement the dom_node_get_attributes
// and thus will crash if we try to call nodeIsEqualNode. // and thus will crash if we try to call nodeIsEqualNode.
pub fn _isEqualNode(self: *parser.DocumentType, other_node: *parser.Node) !bool { pub fn _isEqualNode(self: *parser.DocumentType, other_node: *parser.Node) !bool {
if (parser.nodeType(other_node) != .document_type) { if (try parser.nodeType(other_node) != .document_type) {
return false; return false;
} }
@@ -51,10 +51,10 @@ pub const DocumentType = struct {
if (std.mem.eql(u8, try get_name(self), try get_name(other)) == false) { if (std.mem.eql(u8, try get_name(self), try get_name(other)) == false) {
return false; return false;
} }
if (std.mem.eql(u8, get_publicId(self), get_publicId(other)) == false) { if (std.mem.eql(u8, try get_publicId(self), try get_publicId(other)) == false) {
return false; return false;
} }
if (std.mem.eql(u8, get_systemId(self), get_systemId(other)) == false) { if (std.mem.eql(u8, try get_systemId(self), try get_systemId(other)) == false) {
return false; return false;
} }
return true; return true;

View File

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

View File

@@ -18,7 +18,6 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
@@ -34,6 +33,7 @@ const HTMLElem = @import("../html/elements.zig");
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot; const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
const Animation = @import("Animation.zig"); const Animation = @import("Animation.zig");
const JsObject = @import("../env.zig").JsObject;
pub const Union = @import("../html/elements.zig").Union; pub const Union = @import("../html/elements.zig").Union;
@@ -61,7 +61,7 @@ pub const Element = struct {
pub fn toInterfaceT(comptime T: type, e: *parser.Element) !T { pub fn toInterfaceT(comptime T: type, e: *parser.Element) !T {
const tagname = try parser.elementGetTagName(e) orelse { const tagname = try parser.elementGetTagName(e) orelse {
// If the owner's document is HTML, assume we have an HTMLElement. // If the owner's document is HTML, assume we have an HTMLElement.
const doc = parser.nodeOwnerDocument(parser.elementToNode(e)); const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
if (doc != null and !doc.?.is_html) { if (doc != null and !doc.?.is_html) {
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) }; return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
} }
@@ -73,7 +73,7 @@ pub const Element = struct {
const tag = parser.Tag.fromString(tagname) catch { const tag = parser.Tag.fromString(tagname) catch {
// If the owner's document is HTML, assume we have an HTMLElement. // If the owner's document is HTML, assume we have an HTMLElement.
const doc = parser.nodeOwnerDocument(parser.elementToNode(e)); const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
if (doc != null and doc.?.is_html) { if (doc != null and doc.?.is_html) {
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) }; return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
} }
@@ -87,12 +87,12 @@ pub const Element = struct {
// JS funcs // JS funcs
// -------- // --------
pub fn get_namespaceURI(self: *parser.Element) ?[]const u8 { pub fn get_namespaceURI(self: *parser.Element) !?[]const u8 {
return parser.nodeGetNamespace(parser.elementToNode(self)); return try parser.nodeGetNamespace(parser.elementToNode(self));
} }
pub fn get_prefix(self: *parser.Element) ?[]const u8 { pub fn get_prefix(self: *parser.Element) !?[]const u8 {
return parser.nodeGetPrefix(parser.elementToNode(self)); return try parser.nodeGetPrefix(parser.elementToNode(self));
} }
pub fn get_localName(self: *parser.Element) ![]const u8 { pub fn get_localName(self: *parser.Element) ![]const u8 {
@@ -103,14 +103,6 @@ pub const Element = struct {
return try parser.nodeName(parser.elementToNode(self)); return try parser.nodeName(parser.elementToNode(self));
} }
pub fn get_dir(self: *parser.Element) ![]const u8 {
return try parser.elementGetAttribute(self, "dir") orelse "";
}
pub fn set_dir(self: *parser.Element, dir: []const u8) !void {
return parser.elementSetAttribute(self, "dir", dir);
}
pub fn get_id(self: *parser.Element) ![]const u8 { pub fn get_id(self: *parser.Element) ![]const u8 {
return try parser.elementGetAttribute(self, "id") orelse ""; return try parser.elementGetAttribute(self, "id") orelse "";
} }
@@ -135,10 +127,6 @@ pub const Element = struct {
return try parser.elementSetAttribute(self, "slot", slot); return try parser.elementSetAttribute(self, "slot", slot);
} }
pub fn get_assignedSlot(self: *parser.Element, page: *const Page) !?*parser.Slot {
return @import("../SlotChangeMonitor.zig").findSlot(self, page);
}
pub fn get_classList(self: *parser.Element) !*parser.TokenList { pub fn get_classList(self: *parser.Element) !*parser.TokenList {
return try parser.tokenListCreate(self, "class"); return try parser.tokenListCreate(self, "class");
} }
@@ -162,7 +150,7 @@ pub const Element = struct {
pub fn set_innerHTML(self: *parser.Element, str: []const u8, page: *Page) !void { pub fn set_innerHTML(self: *parser.Element, str: []const u8, page: *Page) !void {
const node = parser.elementToNode(self); const node = parser.elementToNode(self);
const doc = parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument; const doc = try parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
// parse the fragment // parse the fragment
const fragment = try parser.documentParseFragmentFromStr(doc, str); const fragment = try parser.documentParseFragmentFromStr(doc, str);
@@ -180,9 +168,9 @@ pub const Element = struct {
// or an actual document. In a blank page, something like: // or an actual document. In a blank page, something like:
// x.innerHTML = '<script></script>'; // x.innerHTML = '<script></script>';
// does _not_ create an empty script, but in a real page, it does. Weird. // does _not_ create an empty script, but in a real page, it does. Weird.
const html = parser.nodeFirstChild(fragment_node) orelse return; const html = try parser.nodeFirstChild(fragment_node) orelse return;
const head = parser.nodeFirstChild(html) orelse return; const head = try parser.nodeFirstChild(html) orelse return;
const body = parser.nodeNextSibling(head) orelse return; const body = try parser.nodeNextSibling(head) orelse return;
if (try parser.elementTag(self) == .template) { if (try parser.elementTag(self) == .template) {
// HTMLElementTemplate is special. We don't append these as children // HTMLElementTemplate is special. We don't append these as children
@@ -191,9 +179,11 @@ pub const Element = struct {
// a new fragment // a new fragment
const clean = try parser.documentCreateDocumentFragment(doc); const clean = try parser.documentCreateDocumentFragment(doc);
const children = try parser.nodeGetChildNodes(body); const children = try parser.nodeGetChildNodes(body);
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of // always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree // the nodeList and into the new tree
while (parser.nodeListItem(children, 0)) |child| { const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child); _ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child);
} }
@@ -207,102 +197,27 @@ pub const Element = struct {
{ {
// First, copy some of the head element // First, copy some of the head element
const children = try parser.nodeGetChildNodes(head); const children = try parser.nodeGetChildNodes(head);
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of // always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree // the nodeList and into the new tree
while (parser.nodeListItem(children, 0)) |child| { const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(node, child); _ = try parser.nodeAppendChild(node, child);
} }
} }
{ {
const children = try parser.nodeGetChildNodes(body); const children = try parser.nodeGetChildNodes(body);
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of // always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree // the nodeList and into the new tree
while (parser.nodeListItem(children, 0)) |child| { const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(node, child); _ = try parser.nodeAppendChild(node, child);
} }
} }
} }
/// Parses the given `input` string and inserts its children to an element at given `position`.
/// https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
///
/// TODO: Support for XML parsing and `TrustedHTML` instances.
pub fn _insertAdjacentHTML(self: *parser.Element, position: []const u8, input: []const u8) !void {
const self_node = parser.elementToNode(self);
const doc = parser.nodeOwnerDocument(self_node) orelse {
return parser.DOMError.WrongDocument;
};
// Parse the fragment.
// Should return error.Syntax on fail?
const fragment = try parser.documentParseFragmentFromStr(doc, input);
const fragment_node = parser.documentFragmentToNode(fragment);
// We always get it wrapped like so:
// <html><head></head><body>{ ... }</body></html>
// None of the following can be null.
const maybe_html = parser.nodeFirstChild(fragment_node);
std.debug.assert(maybe_html != null);
const html = maybe_html orelse return;
const maybe_body = parser.nodeLastChild(html);
std.debug.assert(maybe_body != null);
const body = maybe_body orelse return;
const children = try parser.nodeGetChildNodes(body);
// * `target_node` is `*Node` (where we actually insert),
// * `prev_node` is `?*Node`.
const target_node, const prev_node = blk: {
// Prefer case-sensitive match.
// "beforeend" was the most common case in my tests; we might adjust the order
// depending on which ones websites prefer most.
if (std.mem.eql(u8, position, "beforeend")) {
break :blk .{ self_node, null };
}
if (std.mem.eql(u8, position, "afterbegin")) {
// Get the first child; null indicates there are no children.
const first_child = parser.nodeFirstChild(self_node);
break :blk .{ self_node, first_child };
}
if (std.mem.eql(u8, position, "beforebegin")) {
// The node must have a parent node in order to use this variant.
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
// Parent cannot be Document.
// Should have checks for document_fragment and document_type?
if (parser.nodeType(parent) == .document) {
return error.NoModificationAllowed;
}
break :blk .{ parent, self_node };
}
if (std.mem.eql(u8, position, "afterend")) {
// The node must have a parent node in order to use this variant.
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
// Parent cannot be Document.
if (parser.nodeType(parent) == .document) {
return error.NoModificationAllowed;
}
// Get the next sibling or null; null indicates our node is the only one.
const sibling = parser.nodeNextSibling(self_node);
break :blk .{ parent, sibling };
}
// Thrown if:
// * position is not one of the four listed values.
// * The input is XML that is not well-formed.
return error.Syntax;
};
while (parser.nodeListItem(children, 0)) |child| {
_ = try parser.nodeInsertBefore(target_node, child, prev_node);
}
}
// The closest() method of the Element interface traverses the element and its parents (heading toward the document root) until it finds a node that matches the specified CSS selector. // The closest() method of the Element interface traverses the element and its parents (heading toward the document root) until it finds a node that matches the specified CSS selector.
// Returns the closest ancestor Element or itself, which matches the selectors. If there are no such element, null. // Returns the closest ancestor Element or itself, which matches the selectors. If there are no such element, null.
pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element { pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element {
@@ -319,7 +234,7 @@ pub const Element = struct {
} }
return parser.nodeToElement(current.node); return parser.nodeToElement(current.node);
} }
current = current.parent() orelse return null; current = try current.parent() orelse return null;
} }
} }
@@ -435,18 +350,28 @@ pub const Element = struct {
return try parser.elementRemoveAttributeNode(self, attr); return try parser.elementRemoveAttributeNode(self, attr);
} }
pub fn _getElementsByTagName(self: *parser.Element, tag_name: js.String) !collection.HTMLCollection { pub fn _getElementsByTagName(
return collection.HTMLCollectionByTagName( self: *parser.Element,
tag_name: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(
page.arena,
parser.elementToNode(self), parser.elementToNode(self),
tag_name.string, tag_name,
.{ .include_root = false }, .{ .include_root = false },
); );
} }
pub fn _getElementsByClassName(self: *parser.Element, class_names: js.String) !collection.HTMLCollection { pub fn _getElementsByClassName(
self: *parser.Element,
classNames: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByClassName( return try collection.HTMLCollectionByClassName(
page.arena,
parser.elementToNode(self), parser.elementToNode(self),
class_names.string, classNames,
.{ .include_root = false }, .{ .include_root = false },
); );
} }
@@ -482,13 +407,13 @@ pub const Element = struct {
// NonDocumentTypeChildNode // NonDocumentTypeChildNode
// https://dom.spec.whatwg.org/#interface-nondocumenttypechildnode // https://dom.spec.whatwg.org/#interface-nondocumenttypechildnode
pub fn get_previousElementSibling(self: *parser.Element) !?Union { pub fn get_previousElementSibling(self: *parser.Element) !?Union {
const res = parser.nodePreviousElementSibling(parser.elementToNode(self)); const res = try parser.nodePreviousElementSibling(parser.elementToNode(self));
if (res == null) return null; if (res == null) return null;
return try toInterface(res.?); return try toInterface(res.?);
} }
pub fn get_nextElementSibling(self: *parser.Element) !?Union { pub fn get_nextElementSibling(self: *parser.Element) !?Union {
const res = parser.nodeNextElementSibling(parser.elementToNode(self)); const res = try parser.nodeNextElementSibling(parser.elementToNode(self));
if (res == null) return null; if (res == null) return null;
return try toInterface(res.?); return try toInterface(res.?);
} }
@@ -501,7 +426,7 @@ pub const Element = struct {
while (true) { while (true) {
next = try walker.get_next(root, next) orelse return null; next = try walker.get_next(root, next) orelse return null;
// ignore non-element nodes. // ignore non-element nodes.
if (parser.nodeType(next.?) != .element) { if (try parser.nodeType(next.?) != .element) {
continue; continue;
} }
const e = parser.nodeToElement(next.?); const e = parser.nodeToElement(next.?);
@@ -549,7 +474,7 @@ pub const Element = struct {
// Returns a 0 DOMRect object if the element is eventually detached from the main window // Returns a 0 DOMRect object if the element is eventually detached from the main window
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect { pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect {
// Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes. // Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes.
if (!page.isNodeAttached(parser.elementToNode(self))) { if (!try page.isNodeAttached(parser.elementToNode(self))) {
return DOMRect{ return DOMRect{
.x = 0, .x = 0,
.y = 0, .y = 0,
@@ -568,7 +493,7 @@ pub const Element = struct {
// We do not render so it only always return the element's bounding rect. // We do not render so it only always return the element's bounding rect.
// Returns an empty array if the element is eventually detached from the main window // Returns an empty array if the element is eventually detached from the main window
pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect { pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect {
if (!page.isNodeAttached(parser.elementToNode(self))) { if (!try page.isNodeAttached(parser.elementToNode(self))) {
return &.{}; return &.{};
} }
const heap_ptr = try page.call_arena.create(DOMRect); const heap_ptr = try page.call_arena.create(DOMRect);
@@ -599,8 +524,6 @@ pub const Element = struct {
contentVisibilityAuto: bool, contentVisibilityAuto: bool,
opacityProperty: bool, opacityProperty: bool,
visibilityProperty: bool, visibilityProperty: bool,
checkVisibilityCSS: bool,
checkOpacity: bool,
}; };
pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool { pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool {
@@ -626,7 +549,7 @@ pub const Element = struct {
} }
// Not sure what to do if there is no owner document // Not sure what to do if there is no owner document
const doc = parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument; const doc = try parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument;
const fragment = try parser.documentCreateDocumentFragment(doc); const fragment = try parser.documentCreateDocumentFragment(doc);
const sr = try page.arena.create(ShadowRoot); const sr = try page.arena.create(ShadowRoot);
sr.* = .{ sr.* = .{
@@ -660,7 +583,7 @@ pub const Element = struct {
return sr; return sr;
} }
pub fn _animate(self: *parser.Element, effect: js.Object, opts: js.Object) !Animation { pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation {
_ = self; _ = self;
_ = opts; _ = opts;
return Animation.constructor(effect, null); return Animation.constructor(effect, null);
@@ -672,7 +595,7 @@ pub const Element = struct {
// for related elements JIT by walking the tree, but there could be // for related elements JIT by walking the tree, but there could be
// cases in libdom or the Zig WebAPI where this reference is kept // cases in libdom or the Zig WebAPI where this reference is kept
const as_node: *parser.Node = @ptrCast(self); const as_node: *parser.Node = @ptrCast(self);
const parent = parser.nodeParentNode(as_node) orelse return; const parent = try parser.nodeParentNode(as_node) orelse return;
_ = try Node._removeChild(parent, as_node); _ = try Node._removeChild(parent, as_node);
} }
}; };

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Env = @import("../env.zig").Env;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
@@ -34,7 +35,6 @@ pub const Union = union(enum) {
screen_orientation: *@import("../html/screen.zig").ScreenOrientation, screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
performance: *@import("performance.zig").Performance, performance: *@import("performance.zig").Performance,
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList, media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
navigation: *@import("../navigation/Navigation.zig"),
}; };
// EventTarget implementation // EventTarget implementation
@@ -48,7 +48,7 @@ pub const EventTarget = struct {
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union { pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
// libdom assumes that all event targets are libdom nodes. They are not. // libdom assumes that all event targets are libdom nodes. They are not.
switch (parser.eventTargetInternalType(et)) { switch (try parser.eventTargetInternalType(et)) {
.libdom_node => { .libdom_node => {
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) }; return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
}, },
@@ -83,11 +83,6 @@ pub const EventTarget = struct {
.media_query_list => { .media_query_list => {
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) }; return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
}, },
.navigation => {
const NavigationEventTarget = @import("../navigation/NavigationEventTarget.zig");
const base: *NavigationEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
return .{ .navigation = @fieldParentPtr("proto", base) };
},
} }
} }
@@ -106,9 +101,6 @@ pub const EventTarget = struct {
page: *Page, page: *Page,
) !void { ) !void {
_ = try EventHandler.register(page.arena, self, typ, listener, opts); _ = try EventHandler.register(page.arena, self, typ, listener, opts);
if (std.mem.eql(u8, typ, "slotchange")) {
try page.registerSlotChangeMonitor();
}
} }
const RemoveEventListenerOpts = union(enum) { const RemoveEventListenerOpts = union(enum) {
@@ -156,15 +148,8 @@ pub const EventTarget = struct {
); );
} }
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event, page: *Page) !bool { pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool {
const res = try parser.eventTargetDispatchEvent(self, event); return try parser.eventTargetDispatchEvent(self, event);
if (!parser.eventBubbles(event) or parser.eventIsStopped(event)) {
return res;
}
try page.window.dispatchForDocumentTarget(event);
return true;
} }
}; };

View File

@@ -23,6 +23,7 @@ const parser = @import("../netsurf.zig");
const Element = @import("element.zig").Element; const Element = @import("element.zig").Element;
const Union = @import("element.zig").Union; const Union = @import("element.zig").Union;
const JsThis = @import("../env.zig").JsThis;
const Walker = @import("walker.zig").Walker; const Walker = @import("walker.zig").Walker;
const Matcher = union(enum) { const Matcher = union(enum) {
@@ -51,13 +52,13 @@ pub const MatchByTagName = struct {
tag: []const u8, tag: []const u8,
is_wildcard: bool, is_wildcard: bool,
fn init(tag_name: []const u8) MatchByTagName { fn init(arena: Allocator, tag_name: []const u8) !MatchByTagName {
if (std.mem.eql(u8, tag_name, "*")) { if (std.mem.eql(u8, tag_name, "*")) {
return .{ .tag = "*", .is_wildcard = true }; return .{ .tag = "*", .is_wildcard = true };
} }
return .{ return .{
.tag = tag_name, .tag = try arena.dupe(u8, tag_name),
.is_wildcard = false, .is_wildcard = false,
}; };
} }
@@ -68,14 +69,15 @@ pub const MatchByTagName = struct {
}; };
pub fn HTMLCollectionByTagName( pub fn HTMLCollectionByTagName(
arena: Allocator,
root: ?*parser.Node, root: ?*parser.Node,
tag_name: []const u8, tag_name: []const u8,
opts: Opts, opts: Opts,
) HTMLCollection { ) !HTMLCollection {
return .{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByTagName = MatchByTagName.init(tag_name) }, .matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -84,9 +86,9 @@ pub fn HTMLCollectionByTagName(
pub const MatchByClassName = struct { pub const MatchByClassName = struct {
class_names: []const u8, class_names: []const u8,
fn init(class_names: []const u8) !MatchByClassName { fn init(arena: Allocator, class_names: []const u8) !MatchByClassName {
return .{ return .{
.class_names = class_names, .class_names = try arena.dupe(u8, class_names),
}; };
} }
@@ -105,14 +107,15 @@ pub const MatchByClassName = struct {
}; };
pub fn HTMLCollectionByClassName( pub fn HTMLCollectionByClassName(
arena: Allocator,
root: ?*parser.Node, root: ?*parser.Node,
class_names: []const u8, classNames: []const u8,
opts: Opts, opts: Opts,
) !HTMLCollection { ) !HTMLCollection {
return HTMLCollection{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByClassName = try MatchByClassName.init(class_names) }, .matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -121,8 +124,10 @@ pub fn HTMLCollectionByClassName(
pub const MatchByName = struct { pub const MatchByName = struct {
name: []const u8, name: []const u8,
fn init(name: []const u8) !MatchByName { fn init(arena: Allocator, name: []const u8) !MatchByName {
return .{ .name = name }; return .{
.name = try arena.dupe(u8, name),
};
} }
pub fn match(self: MatchByName, node: *parser.Node) !bool { pub fn match(self: MatchByName, node: *parser.Node) !bool {
@@ -133,6 +138,7 @@ pub const MatchByName = struct {
}; };
pub fn HTMLCollectionByName( pub fn HTMLCollectionByName(
arena: Allocator,
root: ?*parser.Node, root: ?*parser.Node,
name: []const u8, name: []const u8,
opts: Opts, opts: Opts,
@@ -140,7 +146,7 @@ pub fn HTMLCollectionByName(
return HTMLCollection{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByName = try MatchByName.init(name) }, .matcher = .{ .matchByName = try MatchByName.init(arena, name) },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -197,8 +203,8 @@ pub fn HTMLCollectionChildren(
}; };
} }
pub fn HTMLCollectionEmpty() HTMLCollection { pub fn HTMLCollectionEmpty() !HTMLCollection {
return .{ return HTMLCollection{
.root = null, .root = null,
.walker = .{ .walkerNone = .{} }, .walker = .{ .walkerNone = .{} },
.matcher = .{ .matchFalse = .{} }, .matcher = .{ .matchFalse = .{} },
@@ -220,11 +226,14 @@ pub const MatchByLinks = struct {
} }
}; };
pub fn HTMLCollectionByLinks(root: ?*parser.Node, opts: Opts) HTMLCollection { pub fn HTMLCollectionByLinks(
return .{ root: ?*parser.Node,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByLinks = .{} }, .matcher = .{ .matchByLinks = MatchByLinks{} },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -243,11 +252,14 @@ pub const MatchByAnchors = struct {
} }
}; };
pub fn HTMLCollectionByAnchors(root: ?*parser.Node, opts: Opts) HTMLCollection { pub fn HTMLCollectionByAnchors(
return .{ root: ?*parser.Node,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByAnchors = .{} }, .matcher = .{ .matchByAnchors = MatchByAnchors{} },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -286,7 +298,7 @@ const Opts = struct {
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection // WEB IDL https://dom.spec.whatwg.org/#htmlcollection
// HTMLCollection is re implemented in zig here because libdom // HTMLCollection is re implemented in zig here because libdom
// dom_html_collection expects a comparison function callback as argument. // dom_html_collection expects a comparison function callback as arguement.
// But we wanted a dynamically comparison here, according to the match tagname. // But we wanted a dynamically comparison here, according to the match tagname.
pub const HTMLCollection = struct { pub const HTMLCollection = struct {
matcher: Matcher, matcher: Matcher,
@@ -332,7 +344,7 @@ pub const HTMLCollection = struct {
var node = try self.start() orelse return 0; var node = try self.start() orelse return 0;
while (true) { while (true) {
if (parser.nodeType(node) == .element) { if (try parser.nodeType(node) == .element) {
if (try self.matcher.match(node)) { if (try self.matcher.match(node)) {
len += 1; len += 1;
} }
@@ -359,7 +371,7 @@ pub const HTMLCollection = struct {
} }
while (true) { while (true) {
if (parser.nodeType(node) == .element) { if (try parser.nodeType(node) == .element) {
if (try self.matcher.match(node)) { if (try self.matcher.match(node)) {
// check if we found the searched element. // check if we found the searched element.
if (i == index) { if (i == index) {
@@ -393,7 +405,7 @@ pub const HTMLCollection = struct {
var node = try self.start() orelse return null; var node = try self.start() orelse return null;
while (true) { while (true) {
if (parser.nodeType(node) == .element) { if (try parser.nodeType(node) == .element) {
if (try self.matcher.match(node)) { if (try self.matcher.match(node)) {
const elem = @as(*parser.Element, @ptrCast(node)); const elem = @as(*parser.Element, @ptrCast(node));
@@ -428,23 +440,24 @@ pub const HTMLCollection = struct {
return null; return null;
} }
pub fn indexed_get(self: *HTMLCollection, index: u32, has_value: *bool) !?Union { pub fn postAttach(self: *HTMLCollection, js_this: JsThis) !void {
return (try _item(self, index)) orelse { const len = try self.get_length();
has_value.* = false; for (0..len) |i| {
return undefined; const node = try self.item(@intCast(i)) orelse unreachable;
}; const e = @as(*parser.Element, @ptrCast(node));
} const as_interface = try Element.toInterface(e);
try js_this.setIndex(@intCast(i), as_interface, .{});
pub fn named_get(self: *const HTMLCollection, name: []const u8, has_value: *bool) !?Union { if (try item_name(e)) |name| {
// Even though an entry might have an empty id, the spec says // Even though an entry might have an empty id, the spec says
// that namedItem("") should always return null // that namedItem("") should always return null
if (name.len == 0) { if (name.len > 0) {
return null; // Named fields should not be enumerable (it is defined with
// the LegacyUnenumerableNamedProperties flag.)
try js_this.set(name, as_interface, .{ .DONT_ENUM = true });
}
}
} }
return (try _namedItem(self, name)) orelse {
has_value.* = false;
return undefined;
};
} }
}; };

View File

@@ -0,0 +1,186 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const Env = @import("../env.zig").Env;
const Element = @import("element.zig").Element;
pub const Interfaces = .{
IntersectionObserver,
IntersectionObserverEntry,
};
// This is supposed to listen to change between the root and observation targets.
// However, our rendered stores everything as 1 pixel sized boxes in a long row that never changes.
// As such, there are no changes to intersections between the root and any target.
// Instead we keep a list of all entries that are being observed.
// The callback is called with all entries everytime a new entry is added(observed).
// Potentially we should also call the callback at a regular interval.
// The returned Entries are phony, they always indicate full intersection.
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
pub const IntersectionObserver = struct {
page: *Page,
callback: Env.Function,
options: IntersectionObserverOptions,
observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry),
// new IntersectionObserver(callback)
// new IntersectionObserver(callback, options) [not supported yet]
pub fn constructor(callback: Env.Function, options_: ?IntersectionObserverOptions, page: *Page) !IntersectionObserver {
var options = IntersectionObserverOptions{
.root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
.rootMargin = "0px 0px 0px 0px",
.threshold = .{ .single = 0.0 },
};
if (options_) |*o| {
if (o.root) |root| {
options.root = root;
} // Other properties are not used due to the way we render
}
return .{
.page = page,
.callback = callback,
.options = options,
.observed_entries = .{},
};
}
pub fn _disconnect(self: *IntersectionObserver) !void {
self.observed_entries = .{}; // We don't free as it is on an arena
}
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element) !void {
for (self.observed_entries.items) |*observer| {
if (observer.target == target_element) {
return; // Already observed
}
}
try self.observed_entries.append(self.page.arena, .{
.page = self.page,
.target = target_element,
.options = &self.options,
});
var result: Env.Function.Result = undefined;
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
log.debug(.user_script, "callback error", .{
.err = result.exception,
.stack = result.stack,
.source = "intersection observer",
});
};
}
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
for (self.observed_entries.items, 0..) |*observer, index| {
if (observer.target == target) {
_ = self.observed_entries.swapRemove(index);
break;
}
}
}
pub fn _takeRecords(self: *IntersectionObserver) []IntersectionObserverEntry {
return self.observed_entries.items;
}
};
const IntersectionObserverOptions = struct {
root: ?*parser.Node, // Element or Document
rootMargin: ?[]const u8,
threshold: ?Threshold,
const Threshold = union(enum) {
single: f32,
list: []const f32,
};
};
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
pub const IntersectionObserverEntry = struct {
page: *Page,
target: *parser.Element,
options: *IntersectionObserverOptions,
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
return Element._getBoundingClientRect(self.target, self.page);
}
// Returns the ratio of the intersectionRect to the boundingClientRect.
pub fn get_intersectionRatio(_: *const IntersectionObserverEntry) f32 {
return 1.0;
}
// Returns a DOMRectReadOnly representing the target's visible area.
pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
return Element._getBoundingClientRect(self.target, self.page);
}
// A Boolean value which is true if the target element intersects with the
// intersection observer's root. If this is true, then, the
// IntersectionObserverEntry describes a transition into a state of
// intersection; if it's false, then you know the transition is from
// intersecting to not-intersecting.
pub fn get_isIntersecting(_: *const IntersectionObserverEntry) bool {
return true;
}
// Returns a DOMRectReadOnly for the intersection observer's root.
pub fn get_rootBounds(self: *const IntersectionObserverEntry) !Element.DOMRect {
const root = self.options.root.?;
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
return self.page.renderer.boundingRect();
}
const root_type = try parser.nodeType(root);
var element: *parser.Element = undefined;
switch (root_type) {
.element => element = parser.nodeToElement(root),
.document => {
const doc = parser.nodeToDocument(root);
element = (try parser.documentGetDocumentElement(doc)).?;
},
else => return error.InvalidState,
}
return Element._getBoundingClientRect(element, self.page);
}
// The Element whose intersection with the root changed.
pub fn get_target(self: *const IntersectionObserverEntry) *parser.Element {
return self.target;
}
// TODO: pub fn get_time(self: *const IntersectionObserverEntry)
};
const testing = @import("../../testing.zig");
test "Browser: DOM.IntersectionObserver" {
try testing.htmlRunner("dom/intersection_observer.html");
}

View File

@@ -17,12 +17,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Env = @import("../env.zig").Env;
const NodeList = @import("nodelist.zig").NodeList; const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = .{ pub const Interfaces = .{
@@ -35,21 +36,21 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver // WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
pub const MutationObserver = struct { pub const MutationObserver = struct {
page: *Page, page: *Page,
cbk: js.Function, cbk: Env.Function,
connected: bool,
scheduled: bool, scheduled: bool,
observers: std.ArrayListUnmanaged(*Observer),
// List of records which were observed. When the call scope ends, we need to // List of records which were observed. When the call scope ends, we need to
// execute our callback with it. // execute our callback with it.
observed: std.ArrayListUnmanaged(MutationRecord), observed: std.ArrayListUnmanaged(MutationRecord),
pub fn constructor(cbk: js.Function, page: *Page) !MutationObserver { pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
return .{ return .{
.cbk = cbk, .cbk = cbk,
.page = page, .page = page,
.observed = .{}, .observed = .{},
.connected = true,
.scheduled = false, .scheduled = false,
.observers = .empty,
}; };
} }
@@ -68,17 +69,15 @@ pub const MutationObserver = struct {
.event_node = .{ .id = self.cbk.id, .func = Observer.handle }, .event_node = .{ .id = self.cbk.id, .func = Observer.handle },
}; };
try self.observers.append(arena, observer);
// register node's events // register node's events
if (options.childList or options.subtree) { if (options.childList or options.subtree) {
observer.dom_node_inserted_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMNodeInserted", "DOMNodeInserted",
&observer.event_node, &observer.event_node,
false, false,
); );
observer.dom_node_removed_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMNodeRemoved", "DOMNodeRemoved",
&observer.event_node, &observer.event_node,
@@ -86,7 +85,7 @@ pub const MutationObserver = struct {
); );
} }
if (options.attr()) { if (options.attr()) {
observer.dom_node_attribute_modified_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMAttrModified", "DOMAttrModified",
&observer.event_node, &observer.event_node,
@@ -94,7 +93,7 @@ pub const MutationObserver = struct {
); );
} }
if (options.cdata()) { if (options.cdata()) {
observer.dom_cdata_modified_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMCharacterDataModified", "DOMCharacterDataModified",
&observer.event_node, &observer.event_node,
@@ -102,7 +101,7 @@ pub const MutationObserver = struct {
); );
} }
if (options.subtree) { if (options.subtree) {
observer.dom_subtree_modified_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMSubtreeModified", "DOMSubtreeModified",
&observer.event_node, &observer.event_node,
@@ -113,6 +112,10 @@ pub const MutationObserver = struct {
fn callback(ctx: *anyopaque) ?u32 { fn callback(ctx: *anyopaque) ?u32 {
const self: *MutationObserver = @ptrCast(@alignCast(ctx)); const self: *MutationObserver = @ptrCast(@alignCast(ctx));
if (self.connected == false) {
self.scheduled = true;
return null;
}
self.scheduled = false; self.scheduled = false;
const records = self.observed.items; const records = self.observed.items;
@@ -122,8 +125,8 @@ pub const MutationObserver = struct {
defer self.observed.clearRetainingCapacity(); defer self.observed.clearRetainingCapacity();
var result: js.Function.Result = undefined; var result: Env.Function.Result = undefined;
self.cbk.tryCallWithThis(void, self, .{records}, &result) catch { self.cbk.tryCall(void, .{records}, &result) catch {
log.debug(.user_script, "callback error", .{ log.debug(.user_script, "callback error", .{
.err = result.exception, .err = result.exception,
.stack = result.stack, .stack = result.stack,
@@ -133,55 +136,9 @@ pub const MutationObserver = struct {
return null; return null;
} }
// TODO
pub fn _disconnect(self: *MutationObserver) !void { pub fn _disconnect(self: *MutationObserver) !void {
for (self.observers.items) |observer| { self.connected = false;
const event_target = parser.toEventTarget(parser.Node, observer.node);
if (observer.dom_node_inserted_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMNodeInserted",
listener,
false,
);
}
if (observer.dom_node_removed_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMNodeRemoved",
listener,
false,
);
}
if (observer.dom_node_attribute_modified_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMAttrModified",
listener,
false,
);
}
if (observer.dom_cdata_modified_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMCharacterDataModified",
listener,
false,
);
}
if (observer.dom_subtree_modified_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMSubtreeModified",
listener,
false,
);
}
}
self.observers.clearRetainingCapacity();
} }
// TODO // TODO
@@ -266,12 +223,6 @@ const Observer = struct {
event_node: parser.EventNode, event_node: parser.EventNode,
dom_node_inserted_listener: ?*parser.EventListener = null,
dom_node_removed_listener: ?*parser.EventListener = null,
dom_node_attribute_modified_listener: ?*parser.EventListener = null,
dom_cdata_modified_listener: ?*parser.EventListener = null,
dom_subtree_modified_listener: ?*parser.EventListener = null,
fn appliesTo( fn appliesTo(
self: *const Observer, self: *const Observer,
target: *parser.Node, target: *parser.Node,
@@ -327,13 +278,13 @@ const Observer = struct {
var mutation_observer = self.mutation_observer; var mutation_observer = self.mutation_observer;
const node = blk: { const node = blk: {
const event_target = parser.eventTarget(event) orelse return; const event_target = try parser.eventTarget(event) orelse return;
break :blk parser.eventTargetToNode(event_target); break :blk parser.eventTargetToNode(event_target);
}; };
const mutation_event = parser.eventToMutationEvent(event); const mutation_event = parser.eventToMutationEvent(event);
const event_type = blk: { const event_type = blk: {
const t = parser.eventType(event); const t = try parser.eventType(event);
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return; break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
}; };
@@ -351,12 +302,12 @@ const Observer = struct {
.DOMAttrModified => { .DOMAttrModified => {
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null; record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
if (self.options.attributeOldValue) { if (self.options.attributeOldValue) {
record.old_value = parser.mutationEventPrevValue(mutation_event); record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
} }
}, },
.DOMCharacterDataModified => { .DOMCharacterDataModified => {
if (self.options.characterDataOldValue) { if (self.options.characterDataOldValue) {
record.old_value = parser.mutationEventPrevValue(mutation_event); record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
} }
}, },
.DOMNodeInserted => { .DOMNodeInserted => {

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const generate = @import("../js/generate.zig"); const generate = @import("../../runtime/generate.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const EventTarget = @import("event_target.zig").EventTarget; const EventTarget = @import("event_target.zig").EventTarget;
@@ -67,7 +67,7 @@ pub const Node = struct {
pub const subtype = .node; pub const subtype = .node;
pub fn toInterface(node: *parser.Node) !Union { pub fn toInterface(node: *parser.Node) !Union {
return switch (parser.nodeType(node)) { return switch (try parser.nodeType(node)) {
.element => try Element.toInterfaceT( .element => try Element.toInterfaceT(
Union, Union,
@as(*parser.Element, @ptrCast(node)), @as(*parser.Element, @ptrCast(node)),
@@ -124,7 +124,7 @@ pub const Node = struct {
} }
pub fn get_firstChild(self: *parser.Node) !?Union { pub fn get_firstChild(self: *parser.Node) !?Union {
const res = parser.nodeFirstChild(self); const res = try parser.nodeFirstChild(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -132,7 +132,7 @@ pub const Node = struct {
} }
pub fn get_lastChild(self: *parser.Node) !?Union { pub fn get_lastChild(self: *parser.Node) !?Union {
const res = parser.nodeLastChild(self); const res = try parser.nodeLastChild(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -140,7 +140,7 @@ pub const Node = struct {
} }
pub fn get_nextSibling(self: *parser.Node) !?Union { pub fn get_nextSibling(self: *parser.Node) !?Union {
const res = parser.nodeNextSibling(self); const res = try parser.nodeNextSibling(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -148,7 +148,7 @@ pub const Node = struct {
} }
pub fn get_previousSibling(self: *parser.Node) !?Union { pub fn get_previousSibling(self: *parser.Node) !?Union {
const res = parser.nodePreviousSibling(self); const res = try parser.nodePreviousSibling(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -156,7 +156,7 @@ pub const Node = struct {
} }
pub fn get_parentNode(self: *parser.Node) !?Union { pub fn get_parentNode(self: *parser.Node) !?Union {
const res = parser.nodeParentNode(self); const res = try parser.nodeParentNode(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -164,7 +164,7 @@ pub const Node = struct {
} }
pub fn get_parentElement(self: *parser.Node) !?ElementUnion { pub fn get_parentElement(self: *parser.Node) !?ElementUnion {
const res = parser.nodeParentElement(self); const res = try parser.nodeParentElement(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -176,11 +176,11 @@ pub const Node = struct {
} }
pub fn get_nodeType(self: *parser.Node) !u8 { pub fn get_nodeType(self: *parser.Node) !u8 {
return @intFromEnum(parser.nodeType(self)); return @intFromEnum(try parser.nodeType(self));
} }
pub fn get_ownerDocument(self: *parser.Node) !?*parser.DocumentHTML { pub fn get_ownerDocument(self: *parser.Node) !?*parser.DocumentHTML {
const res = parser.nodeOwnerDocument(self); const res = try parser.nodeOwnerDocument(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -190,12 +190,12 @@ pub const Node = struct {
pub fn get_isConnected(self: *parser.Node) !bool { pub fn get_isConnected(self: *parser.Node) !bool {
var node = self; var node = self;
while (true) { while (true) {
const node_type = parser.nodeType(node); const node_type = try parser.nodeType(node);
if (node_type == .document) { if (node_type == .document) {
return true; return true;
} }
if (parser.nodeParentNode(node)) |parent| { if (try parser.nodeParentNode(node)) |parent| {
// didn't find a document, but node has a parent, let's see // didn't find a document, but node has a parent, let's see
// if it's connected; // if it's connected;
node = parent; node = parent;
@@ -222,15 +222,15 @@ pub const Node = struct {
// Read/Write attributes // Read/Write attributes
pub fn get_nodeValue(self: *parser.Node) !?[]const u8 { pub fn get_nodeValue(self: *parser.Node) !?[]const u8 {
return parser.nodeValue(self); return try parser.nodeValue(self);
} }
pub fn set_nodeValue(self: *parser.Node, data: []u8) !void { pub fn set_nodeValue(self: *parser.Node, data: []u8) !void {
try parser.nodeSetValue(self, data); try parser.nodeSetValue(self, data);
} }
pub fn get_textContent(self: *parser.Node) ?[]const u8 { pub fn get_textContent(self: *parser.Node) !?[]const u8 {
return parser.nodeTextContent(self); return try parser.nodeTextContent(self);
} }
pub fn set_textContent(self: *parser.Node, data: []u8) !void { pub fn set_textContent(self: *parser.Node, data: []u8) !void {
@@ -240,8 +240,8 @@ pub const Node = struct {
// Methods // Methods
pub fn _appendChild(self: *parser.Node, child: *parser.Node) !Union { pub fn _appendChild(self: *parser.Node, child: *parser.Node) !Union {
const self_owner = parser.nodeOwnerDocument(self); const self_owner = try parser.nodeOwnerDocument(self);
const child_owner = parser.nodeOwnerDocument(child); const child_owner = try parser.nodeOwnerDocument(child);
// If the node to be inserted has a different ownerDocument than the parent node, // If the node to be inserted has a different ownerDocument than the parent node,
// modern browsers automatically adopt the node and its descendants into // modern browsers automatically adopt the node and its descendants into
@@ -272,14 +272,14 @@ pub const Node = struct {
return 0; return 0;
} }
const docself = parser.nodeOwnerDocument(self) orelse blk: { const docself = try parser.nodeOwnerDocument(self) orelse blk: {
if (parser.nodeType(self) == .document) { if (try parser.nodeType(self) == .document) {
break :blk @as(*parser.Document, @ptrCast(self)); break :blk @as(*parser.Document, @ptrCast(self));
} }
break :blk null; break :blk null;
}; };
const docother = parser.nodeOwnerDocument(other) orelse blk: { const docother = try parser.nodeOwnerDocument(other) orelse blk: {
if (parser.nodeType(other) == .document) { if (try parser.nodeType(other) == .document) {
break :blk @as(*parser.Document, @ptrCast(other)); break :blk @as(*parser.Document, @ptrCast(other));
} }
break :blk null; break :blk null;
@@ -299,8 +299,8 @@ pub const Node = struct {
@intFromEnum(parser.DocumentPosition.contained_by); @intFromEnum(parser.DocumentPosition.contained_by);
} }
const rootself = parser.nodeGetRootNode(self); const rootself = try parser.nodeGetRootNode(self);
const rootother = parser.nodeGetRootNode(other); const rootother = try parser.nodeGetRootNode(other);
if (rootself != rootother) { if (rootself != rootother) {
return @intFromEnum(parser.DocumentPosition.disconnected) + return @intFromEnum(parser.DocumentPosition.disconnected) +
@intFromEnum(parser.DocumentPosition.implementation_specific) + @intFromEnum(parser.DocumentPosition.implementation_specific) +
@@ -347,8 +347,8 @@ pub const Node = struct {
return 0; return 0;
} }
pub fn _contains(self: *parser.Node, other: *parser.Node) bool { pub fn _contains(self: *parser.Node, other: *parser.Node) !bool {
return parser.nodeContains(self, other); return try parser.nodeContains(self, other);
} }
// Returns itself or ancestor object inheriting from Node. // Returns itself or ancestor object inheriting from Node.
@@ -360,44 +360,32 @@ pub const Node = struct {
node: Union, node: Union,
}; };
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult { pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
const composed = if (options) |opts| opts.composed else false; if (options) |options_| if (options_.composed) {
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
};
var current_root = parser.nodeGetRootNode(self); const root = try parser.nodeGetRootNode(self);
if (page.getNodeState(root)) |state| {
while (true) {
const node_type = parser.nodeType(current_root);
if (node_type == .document_fragment) {
if (parser.documentFragmentGetHost(@ptrCast(current_root))) |host| {
if (page.getNodeState(host)) |state| {
if (state.shadow_root) |sr| { if (state.shadow_root) |sr| {
if (!composed) {
return .{ .shadow_root = sr }; return .{ .shadow_root = sr };
} }
current_root = parser.nodeGetRootNode(@ptrCast(sr.host));
continue;
}
}
}
}
break;
} }
return .{ .node = try Node.toInterface(current_root) }; return .{ .node = try Node.toInterface(root) };
} }
pub fn _hasChildNodes(self: *parser.Node) bool { pub fn _hasChildNodes(self: *parser.Node) !bool {
return parser.nodeHasChildNodes(self); return try parser.nodeHasChildNodes(self);
} }
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList { pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
const allocator = page.arena; const allocator = page.arena;
var list: NodeList = .{}; var list: NodeList = .{};
var n = parser.nodeFirstChild(self) orelse return list; var n = try parser.nodeFirstChild(self) orelse return list;
while (true) { while (true) {
try list.append(allocator, n); try list.append(allocator, n);
n = parser.nodeNextSibling(n) orelse return list; n = try parser.nodeNextSibling(n) orelse return list;
} }
} }
@@ -406,8 +394,8 @@ pub const Node = struct {
return _appendChild(self, new_node); return _appendChild(self, new_node);
} }
const self_owner = parser.nodeOwnerDocument(self); const self_owner = try parser.nodeOwnerDocument(self);
const new_node_owner = parser.nodeOwnerDocument(new_node); const new_node_owner = try parser.nodeOwnerDocument(new_node);
// If the node to be inserted has a different ownerDocument than the parent node, // If the node to be inserted has a different ownerDocument than the parent node,
// modern browsers automatically adopt the node and its descendants into // modern browsers automatically adopt the node and its descendants into
@@ -427,7 +415,7 @@ pub const Node = struct {
} }
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool { pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
return parser.nodeIsDefaultNamespace(self, namespace); return try parser.nodeIsDefaultNamespace(self, namespace);
} }
pub fn _isEqualNode(self: *parser.Node, other: *parser.Node) !bool { pub fn _isEqualNode(self: *parser.Node, other: *parser.Node) !bool {
@@ -435,10 +423,10 @@ pub const Node = struct {
return try parser.nodeIsEqualNode(self, other); return try parser.nodeIsEqualNode(self, other);
} }
pub fn _isSameNode(self: *parser.Node, other: *parser.Node) bool { pub fn _isSameNode(self: *parser.Node, other: *parser.Node) !bool {
// TODO: other is not an optional parameter, but can be null. // TODO: other is not an optional parameter, but can be null.
// NOTE: there is no need to use isSameNode(); instead use the === strict equality operator // NOTE: there is no need to use isSameNode(); instead use the === strict equality operator
return parser.nodeIsSameNode(self, other); return try parser.nodeIsSameNode(self, other);
} }
pub fn _lookupPrefix(self: *parser.Node, namespace: ?[]const u8) !?[]const u8 { pub fn _lookupPrefix(self: *parser.Node, namespace: ?[]const u8) !?[]const u8 {
@@ -473,7 +461,7 @@ pub const Node = struct {
// Check if the hierarchy node tree constraints are respected. // Check if the hierarchy node tree constraints are respected.
// For now, it checks only if new nodes are not self. // For now, it checks only if new nodes are not self.
// TODO implements the others constraints. // TODO implements the others contraints.
// see https://dom.spec.whatwg.org/#concept-node-tree // see https://dom.spec.whatwg.org/#concept-node-tree
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool { pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
for (nodes) |n| { for (nodes) |n| {
@@ -494,9 +482,9 @@ pub const Node = struct {
return parser.DOMError.HierarchyRequest; return parser.DOMError.HierarchyRequest;
} }
const doc = (parser.nodeOwnerDocument(self)) orelse return; const doc = (try parser.nodeOwnerDocument(self)) orelse return;
if (parser.nodeFirstChild(self)) |first| { if (try parser.nodeFirstChild(self)) |first| {
for (nodes) |node| { for (nodes) |node| {
_ = try parser.nodeInsertBefore(self, try node.toNode(doc), first); _ = try parser.nodeInsertBefore(self, try node.toNode(doc), first);
} }
@@ -518,7 +506,7 @@ pub const Node = struct {
return parser.DOMError.HierarchyRequest; return parser.DOMError.HierarchyRequest;
} }
const doc = (parser.nodeOwnerDocument(self)) orelse return; const doc = (try parser.nodeOwnerDocument(self)) orelse return;
for (nodes) |node| { for (nodes) |node| {
_ = try parser.nodeAppendChild(self, try node.toNode(doc)); _ = try parser.nodeAppendChild(self, try node.toNode(doc));
} }
@@ -537,7 +525,7 @@ pub const Node = struct {
// remove existing children // remove existing children
try removeChildren(self); try removeChildren(self);
const doc = (parser.nodeOwnerDocument(self)) orelse return; const doc = (try parser.nodeOwnerDocument(self)) orelse return;
// add new children // add new children
for (nodes) |node| { for (nodes) |node| {
_ = try parser.nodeAppendChild(self, try node.toNode(doc)); _ = try parser.nodeAppendChild(self, try node.toNode(doc));
@@ -545,30 +533,30 @@ pub const Node = struct {
} }
pub fn removeChildren(self: *parser.Node) !void { pub fn removeChildren(self: *parser.Node) !void {
if (!parser.nodeHasChildNodes(self)) return; if (!try parser.nodeHasChildNodes(self)) return;
const children = try parser.nodeGetChildNodes(self); const children = try parser.nodeGetChildNodes(self);
const ln = parser.nodeListLength(children); const ln = try parser.nodeListLength(children);
var i: u32 = 0; var i: u32 = 0;
while (i < ln) { while (i < ln) {
defer i += 1; defer i += 1;
// we always retrieve the 0 index child on purpose: libdom nodelist // we always retrieve the 0 index child on purpose: libdom nodelist
// are dynamic. So the next child to remove is always as pos 0. // are dynamic. So the next child to remove is always as pos 0.
const child = parser.nodeListItem(children, 0) orelse continue; const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeRemoveChild(self, child); _ = try parser.nodeRemoveChild(self, child);
} }
} }
pub fn before(self: *parser.Node, nodes: []const NodeOrText) !void { pub fn before(self: *parser.Node, nodes: []const NodeOrText) !void {
const parent = parser.nodeParentNode(self) orelse return; const parent = try parser.nodeParentNode(self) orelse return;
const doc = (parser.nodeOwnerDocument(parent)) orelse return; const doc = (try parser.nodeOwnerDocument(parent)) orelse return;
var sibling: ?*parser.Node = self; var sibling: ?*parser.Node = self;
// have to find the first sibling that isn't in nodes // have to find the first sibling that isn't in nodes
CHECK: while (sibling) |s| { CHECK: while (sibling) |s| {
for (nodes) |n| { for (nodes) |n| {
if (n.is(s)) { if (n.is(s)) {
sibling = parser.nodePreviousSibling(s); sibling = try parser.nodePreviousSibling(s);
continue :CHECK; continue :CHECK;
} }
} }
@@ -576,7 +564,7 @@ pub const Node = struct {
} }
if (sibling == null) { if (sibling == null) {
sibling = parser.nodeFirstChild(parent); sibling = try parser.nodeFirstChild(parent);
} }
if (sibling) |ref_node| { if (sibling) |ref_node| {
@@ -590,15 +578,15 @@ pub const Node = struct {
} }
pub fn after(self: *parser.Node, nodes: []const NodeOrText) !void { pub fn after(self: *parser.Node, nodes: []const NodeOrText) !void {
const parent = parser.nodeParentNode(self) orelse return; const parent = try parser.nodeParentNode(self) orelse return;
const doc = (parser.nodeOwnerDocument(parent)) orelse return; const doc = (try parser.nodeOwnerDocument(parent)) orelse return;
// have to find the first sibling that isn't in nodes // have to find the first sibling that isn't in nodes
var sibling = parser.nodeNextSibling(self); var sibling = try parser.nodeNextSibling(self);
CHECK: while (sibling) |s| { CHECK: while (sibling) |s| {
for (nodes) |n| { for (nodes) |n| {
if (n.is(s)) { if (n.is(s)) {
sibling = parser.nodeNextSibling(s); sibling = try parser.nodeNextSibling(s);
continue :CHECK; continue :CHECK;
} }
} }
@@ -643,7 +631,262 @@ pub const Node = struct {
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: DOM.Node" { test "Browser.DOM.node" {
try testing.htmlRunner("dom/node.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
try testing.htmlRunner("dom/node_owner.html"); defer runner.deinit();
{
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);
}
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" },
}, .{});
try runner.testCases(&.{
.{ "document.getElementById('content').getRootNode().__proto__.constructor.name", "HTMLDocument" },
}, .{});
try runner.testCases(&.{
// for next test cases
.{ "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
.{ "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" },
}, .{});
try runner.testCases(&.{
.{ "let last_child = content.lastChild.previousSibling", "undefined" }, // previousSibling because of line return \n
.{ "last_child.__proto__.constructor.name", "Comment" },
}, .{});
try runner.testCases(&.{
.{ "let next_sibling = link.nextSibling.nextSibling", "undefined" },
.{ "next_sibling.localName", "p" },
.{ "next_sibling.__proto__.constructor.name", "HTMLParagraphElement" },
.{ "content.nextSibling.nextSibling", "null" },
}, .{});
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" },
}, .{});
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" },
}, .{});
try runner.testCases(&.{
.{ "first_child.nodeName === 'A'", "true" },
.{ "link.firstChild.nodeName === '#text'", "true" },
.{ "last_child.nodeName === '#comment'", "true" },
.{ "document.nodeName === '#document'", "true" },
}, .{});
try runner.testCases(&.{
.{ "first_child.nodeType === 1", "true" },
.{ "link.firstChild.nodeType === 3", "true" },
.{ "last_child.nodeType === 8", "true" },
.{ "document.nodeType === 9", "true" },
}, .{});
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" },
}, .{});
try runner.testCases(&.{
.{ "content.isConnected", "true" },
.{ "document.isConnected", "true" },
.{ "const connDiv = document.createElement('div')", null },
.{ "connDiv.isConnected", "false" },
.{ "const connParentDiv = document.createElement('div')", null },
.{ "connParentDiv.appendChild(connDiv)", null },
.{ "connDiv.isConnected", "false" },
.{ "content.appendChild(connParentDiv)", null },
.{ "connDiv.isConnected", "true" },
}, .{});
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" },
}, .{});
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" },
}, .{});
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]" },
}, .{});
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" },
}, .{});
try runner.testCases(&.{
.{ "link.contains(text)", "true" },
.{ "text.contains(link)", "false" },
}, .{});
try runner.testCases(&.{
.{ "link.hasChildNodes()", "true" },
.{ "text.hasChildNodes()", "false" },
}, .{});
try runner.testCases(&.{
.{ "link.childNodes.length", "1" },
.{ "text.childNodes.length", "0" },
}, .{});
try runner.testCases(&.{
.{ "let insertBefore = document.createElement('a')", "undefined" },
.{ "link.insertBefore(insertBefore, text) !== undefined", "true" },
.{ "link.firstChild.localName === 'a'", "true" },
.{ "let insertBefore2 = document.createElement('b')", null },
.{ "link.insertBefore(insertBefore2, null).localName", "b" },
.{ "link.childNodes[link.childNodes.length - 1].localName", "b" },
}, .{});
try runner.testCases(&.{
// TODO: does not seems to work
// .{ "link.isDefaultNamespace('')", "true" },
.{ "link.isDefaultNamespace('false')", "false" },
}, .{});
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
// .{ "equal1.isEqualNode(equal2)", "true" },
}, .{});
try runner.testCases(&.{
.{ "document.body.isSameNode(document.body)", "true" },
}, .{});
try runner.testCases(&.{
// TODO: no test
.{ "link.normalize()", "undefined" },
}, .{});
try runner.testCases(&.{
.{ "link.baseURI", "https://lightpanda.io/opensource-browser/" },
}, .{});
try runner.testCases(&.{
.{ "content.removeChild(append) !== undefined", "true" },
.{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" },
}, .{});
try runner.testCases(&.{
.{ "let replace = document.createElement('div')", "undefined" },
.{ "link.replaceChild(replace, insertBefore) !== undefined", "true" },
}, .{});
try runner.testCases(&.{
.{ "Node.ELEMENT_NODE", "1" },
.{ "Node.ATTRIBUTE_NODE", "2" },
.{ "Node.TEXT_NODE", "3" },
.{ "Node.CDATA_SECTION_NODE", "4" },
.{ "Node.PROCESSING_INSTRUCTION_NODE", "7" },
.{ "Node.COMMENT_NODE", "8" },
.{ "Node.DOCUMENT_NODE", "9" },
.{ "Node.DOCUMENT_TYPE_NODE", "10" },
.{ "Node.DOCUMENT_FRAGMENT_NODE", "11" },
.{ "Node.ENTITY_REFERENCE_NODE", "5" },
.{ "Node.ENTITY_NODE", "6" },
.{ "Node.NOTATION_NODE", "12" },
}, .{});
}
test "Browser.DOM.node.owner" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
\\ <div id="target-container">
\\ <p id="reference-node">
\\ I am the original reference node.
\\ </p>
\\ </div>"
});
defer runner.deinit();
try runner.testCases(&.{
.{
\\ const parser = new DOMParser();
\\ const newDoc = parser.parseFromString('<div id="new-node"><p>Hey</p><span>Marked</span></div>', 'text/html');
\\ const newNode = newDoc.getElementById('new-node');
\\ const parent = document.getElementById('target-container');
\\ const referenceNode = document.getElementById('reference-node');
\\ parent.insertBefore(newNode, referenceNode);
\\ const k = document.getElementById('new-node');
\\ const ptag = k.querySelector('p');
\\ const spanTag = k.querySelector('span');
\\ const anotherDoc = parser.parseFromString('<div id="another-new-node"></div>', 'text/html');
\\ const anotherNewNode = anotherDoc.getElementById('another-new-node');
\\
\\ parent.appendChild(anotherNewNode)
,
"[object HTMLDivElement]",
},
.{ "parent.ownerDocument === newNode.ownerDocument", "true" },
.{ "parent.ownerDocument === anotherNewNode.ownerDocument", "true" },
.{ "newNode.firstChild.nodeName", "P" },
.{ "ptag.ownerDocument === parent.ownerDocument", "true" },
.{ "spanTag.ownerDocument === parent.ownerDocument", "true" },
.{ "parent.contains(newNode)", "true" },
.{ "parent.contains(anotherNewNode)", "true" },
.{ "anotherDoc.contains(anotherNewNode)", "false" },
.{ "newDoc.contains(newNode)", "false" },
}, .{});
} }

View File

@@ -17,8 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
pub const NodeFilter = struct { pub const NodeFilter = struct {
@@ -43,13 +43,10 @@ pub const NodeFilter = struct {
const VerifyResult = enum { accept, skip, reject }; const VerifyResult = enum { accept, skip, reject };
pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !VerifyResult { pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !VerifyResult {
const node_type = parser.nodeType(node); const node_type = try parser.nodeType(node);
// Verify that we can show this node type. // Verify that we can show this node type.
// Per the DOM spec, what_to_show filters which nodes to return, but should
// still traverse children. So we return .skip (not .reject) when the node
// type doesn't match.
if (!switch (node_type) { if (!switch (node_type) {
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0, .attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0, .cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
@@ -63,7 +60,7 @@ pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !Veri
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0, .notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0, .processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
.text => what_to_show & NodeFilter._SHOW_TEXT != 0, .text => what_to_show & NodeFilter._SHOW_TEXT != 0,
}) return .skip; }) return .reject;
// Verify that we aren't filtering it out. // Verify that we aren't filtering it out.
if (filter) |f| { if (filter) |f| {
@@ -78,6 +75,15 @@ pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !Veri
} }
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: DOM.NodeFilter" { test "Browser.DOM.NodeFilter" {
try testing.htmlRunner("dom/node_filter.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "NodeFilter.FILTER_ACCEPT", "1" },
.{ "NodeFilter.FILTER_REJECT", "2" },
.{ "NodeFilter.FILTER_SKIP", "3" },
.{ "NodeFilter.SHOW_ALL", "4294967295" },
.{ "NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT", "129" },
}, .{});
} }

View File

@@ -18,8 +18,8 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const NodeFilter = @import("node_filter.zig"); const NodeFilter = @import("node_filter.zig");
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
const NodeUnion = @import("node.zig").Union; const NodeUnion = @import("node.zig").Union;
@@ -37,7 +37,7 @@ pub const NodeIterator = struct {
reference_node: *parser.Node, reference_node: *parser.Node,
what_to_show: u32, what_to_show: u32,
filter: ?NodeIteratorOpts, filter: ?NodeIteratorOpts,
filter_func: ?js.Function, filter_func: ?Env.Function,
pointer_before_current: bool = true, pointer_before_current: bool = true,
// used to track / block recursive filters // used to track / block recursive filters
is_in_callback: bool = false, is_in_callback: bool = false,
@@ -45,15 +45,15 @@ pub const NodeIterator = struct {
// One of the few cases where null and undefined resolve to different default. // One of the few cases where null and undefined resolve to different default.
// We need the raw JsObject so that we can probe the tri state: // We need the raw JsObject so that we can probe the tri state:
// null, undefined or i32. // null, undefined or i32.
pub const WhatToShow = js.Object; pub const WhatToShow = Env.JsObject;
pub const NodeIteratorOpts = union(enum) { pub const NodeIteratorOpts = union(enum) {
function: js.Function, function: Env.Function,
object: struct { acceptNode: js.Function }, object: struct { acceptNode: Env.Function },
}; };
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator { pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator {
var filter_func: ?js.Function = null; var filter_func: ?Env.Function = null;
if (filter) |f| { if (filter) |f| {
filter_func = switch (f) { filter_func = switch (f) {
.function => |func| func, .function => |func| func,
@@ -74,10 +74,10 @@ pub const NodeIterator = struct {
return .{ return .{
.root = node, .root = node,
.filter = filter,
.reference_node = node, .reference_node = node,
.filter_func = filter_func,
.what_to_show = what_to_show, .what_to_show = what_to_show,
.filter = filter,
.filter_func = filter_func,
}; };
} }
@@ -115,30 +115,17 @@ pub const NodeIterator = struct {
if (try self.firstChild(self.reference_node)) |child| { if (try self.firstChild(self.reference_node)) |child| {
self.reference_node = child; self.reference_node = child;
self.pointer_before_current = false;
return try Node.toInterface(child); return try Node.toInterface(child);
} }
var current = self.reference_node; var current = self.reference_node;
while (current != self.root) { while (current != self.root) {
// Try to get next sibling (including .skip/.reject nodes we need to descend into) if (try self.nextSibling(current)) |sibling| {
if (try self.nextSiblingOrSkipReject(current)) |result| { self.reference_node = sibling;
if (result.should_descend) { return try Node.toInterface(sibling);
// This is a .skip/.reject node - try to find acceptable children within it
if (try self.firstChild(result.node)) |child| {
self.reference_node = child;
return try Node.toInterface(child);
}
// No acceptable children, continue looking at this node's siblings
current = result.node;
continue;
}
// This is an .accept node - return it
self.reference_node = result.node;
return try Node.toInterface(result.node);
} }
current = (parser.nodeParentNode(current)) orelse break; current = (try parser.nodeParentNode(current)) orelse break;
} }
return null; return null;
@@ -160,7 +147,7 @@ pub const NodeIterator = struct {
} }
var current = self.reference_node; var current = self.reference_node;
while (parser.nodePreviousSibling(current)) |previous| { while (try parser.nodePreviousSibling(current)) |previous| {
current = previous; current = previous;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
@@ -202,11 +189,11 @@ pub const NodeIterator = struct {
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node { fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node); const children = try parser.nodeGetChildNodes(node);
const child_count = parser.nodeListLength(children); const child_count = try parser.nodeListLength(children);
for (0..child_count) |i| { for (0..child_count) |i| {
const index: u32 = @intCast(i); const index: u32 = @intCast(i);
const child = (parser.nodeListItem(children, index)) orelse return null; const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker .accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
@@ -219,12 +206,12 @@ pub const NodeIterator = struct {
fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node { fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node); const children = try parser.nodeGetChildNodes(node);
const child_count = parser.nodeListLength(children); const child_count = try parser.nodeListLength(children);
var index: u32 = child_count; var index: u32 = child_count;
while (index > 0) { while (index > 0) {
index -= 1; index -= 1;
const child = (parser.nodeListItem(children, index)) orelse return null; const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker .accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
@@ -242,7 +229,7 @@ pub const NodeIterator = struct {
var current = node; var current = node;
while (true) { while (true) {
if (current == self.root) return null; if (current == self.root) return null;
current = (parser.nodeParentNode(current)) orelse return null; current = (try parser.nodeParentNode(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current, .accept => return current,
@@ -256,7 +243,7 @@ pub const NodeIterator = struct {
var current = node; var current = node;
while (true) { while (true) {
current = (parser.nodeNextSibling(current)) orelse return null; current = (try parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current, .accept => return current,
@@ -267,22 +254,6 @@ pub const NodeIterator = struct {
return null; return null;
} }
// Get the next sibling that is either acceptable or should be descended into (skip/reject)
fn nextSiblingOrSkipReject(self: *const NodeIterator, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
var current = node;
while (true) {
current = (parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return .{ .node = current, .should_descend = false },
.skip, .reject => return .{ .node = current, .should_descend = true },
}
}
return null;
}
fn callbackStart(self: *NodeIterator) !void { fn callbackStart(self: *NodeIterator) !void {
if (self.is_in_callback) { if (self.is_in_callback) {
// this is the correct DOMExeption // this is the correct DOMExeption
@@ -297,6 +268,71 @@ pub const NodeIterator = struct {
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: DOM.NodeIterator" { test "Browser.DOM.NodeFilter" {
try testing.htmlRunner("dom/node_iterator.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{
\\ const nodeIterator = document.createNodeIterator(
\\ document.body,
\\ NodeFilter.SHOW_ELEMENT,
\\ {
\\ acceptNode(node) {
\\ return NodeFilter.FILTER_ACCEPT;
\\ },
\\ },
\\ );
\\ nodeIterator.nextNode().nodeName;
,
"BODY",
},
.{ "nodeIterator.nextNode().nodeName", "DIV" },
.{ "nodeIterator.nextNode().nodeName", "A" },
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
.{ "nodeIterator.nextNode().nodeName", "A" }, // pointer_before_current flips
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
.{ "nodeIterator.previousNode().nodeName", "DIV" },
.{ "nodeIterator.previousNode().nodeName", "BODY" },
.{ "nodeIterator.previousNode()", "null" }, // Not HEAD since body is root
.{ "nodeIterator.previousNode()", "null" }, // Keeps returning null
.{ "nodeIterator.nextNode().nodeName", "BODY" },
.{ "nodeIterator.nextNode().nodeName", null },
.{ "nodeIterator.nextNode().nodeName", null },
.{ "nodeIterator.nextNode().nodeName", null },
.{ "nodeIterator.nextNode().nodeName", "SPAN" },
.{ "nodeIterator.nextNode().nodeName", "P" },
.{ "nodeIterator.nextNode()", "null" }, // Just the last one
.{ "nodeIterator.nextNode()", "null" }, // Keeps returning null
.{ "nodeIterator.previousNode().nodeName", "P" },
}, .{});
try runner.testCases(&.{
.{
\\ const notationIterator = document.createNodeIterator(
\\ document.body,
\\ NodeFilter.SHOW_NOTATION,
\\ );
\\ notationIterator.nextNode();
,
"null",
},
.{ "notationIterator.previousNode()", "null" },
}, .{});
try runner.testCases(&.{
.{ "nodeIterator.filter.acceptNode(document.body)", "1" },
.{ "notationIterator.filter", "null" },
.{
\\ const rejectIterator = document.createNodeIterator(
\\ document.body,
\\ NodeFilter.SHOW_ALL,
\\ (e => { return NodeFilter.FILTER_REJECT}),
\\ );
\\ rejectIterator.filter(document.body);
,
"2",
},
}, .{});
} }

View File

@@ -17,12 +17,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const JsThis = @import("../env.zig").JsThis;
const Function = @import("../env.zig").Function;
const NodeUnion = @import("node.zig").Union; const NodeUnion = @import("node.zig").Union;
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
@@ -100,20 +101,13 @@ pub const NodeList = struct {
nodes: NodesArrayList = .{}, nodes: NodesArrayList = .{},
pub fn deinit(self: *NodeList, allocator: Allocator) void { pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void {
self.nodes.deinit(allocator); // TODO unref all nodes
self.nodes.deinit(alloc);
} }
pub fn ensureTotalCapacity(self: *NodeList, allocator: Allocator, n: usize) !void { pub fn append(self: *NodeList, alloc: std.mem.Allocator, node: *parser.Node) !void {
return self.nodes.ensureTotalCapacity(allocator, n); try self.nodes.append(alloc, node);
}
pub fn append(self: *NodeList, allocator: Allocator, node: *parser.Node) !void {
try self.nodes.append(allocator, node);
}
pub fn appendAssumeCapacity(self: *NodeList, node: *parser.Node) void {
self.nodes.appendAssumeCapacity(node);
} }
pub fn get_length(self: *const NodeList) u32 { pub fn get_length(self: *const NodeList) u32 {
@@ -146,10 +140,10 @@ pub const NodeList = struct {
// }; // };
// } // }
pub fn _forEach(self: *NodeList, cbk: js.Function) !void { // TODO handle thisArg pub fn _forEach(self: *NodeList, cbk: Function) !void { // TODO handle thisArg
for (self.nodes.items, 0..) |n, i| { for (self.nodes.items, 0..) |n, i| {
const ii: u32 = @intCast(i); const ii: u32 = @intCast(i);
var result: js.Function.Result = undefined; var result: Function.Result = undefined;
cbk.tryCall(void, .{ n, ii, self }, &result) catch { cbk.tryCall(void, .{ n, ii, self }, &result) catch {
log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack }); log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack });
}; };
@@ -173,7 +167,7 @@ pub const NodeList = struct {
} }
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries // TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
pub fn postAttach(self: *NodeList, js_this: js.This) !void { pub fn postAttach(self: *NodeList, js_this: JsThis) !void {
const len = self.get_length(); const len = self.get_length();
for (0..len) |i| { for (0..len) |i| {
const node = try self._item(@intCast(i)) orelse unreachable; const node = try self._item(@intCast(i)) orelse unreachable;
@@ -183,6 +177,22 @@ pub const NodeList = struct {
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: DOM.NodeList" { test "Browser.DOM.NodeList" {
try testing.htmlRunner("dom/node_list.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let list = document.getElementById('content').childNodes", "undefined" },
.{ "list.length", "9" },
.{ "list[0].__proto__.constructor.name", "Text" },
.{
\\ let i = 0;
\\ list.forEach(function (n, idx) {
\\ i += idx;
\\ });
\\ i;
,
"36",
},
}, .{});
} }

View File

@@ -18,9 +18,9 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const milliTimestamp = @import("../../datetime.zig").milliTimestamp; const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
@@ -61,7 +61,7 @@ pub const Performance = struct {
return milliTimestamp() - self.time_origin; return milliTimestamp() - self.time_origin;
} }
pub fn _mark(_: *Performance, name: js.String, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark { pub fn _mark(_: *Performance, name: []const u8, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
const mark: PerformanceMark = try .constructor(name, _options, page); const mark: PerformanceMark = try .constructor(name, _options, page);
// TODO: Should store this in an entries list // TODO: Should store this in an entries list
return mark; return mark;
@@ -148,14 +148,14 @@ pub const PerformanceMark = struct {
pub const prototype = *PerformanceEntry; pub const prototype = *PerformanceEntry;
proto: PerformanceEntry, proto: PerformanceEntry,
detail: ?js.Object, detail: ?Env.JsObject,
const Options = struct { const Options = struct {
detail: ?js.Object = null, detail: ?Env.JsObject = null,
startTime: ?f64 = null, startTime: ?f64 = null,
}; };
pub fn constructor(name: js.String, _options: ?Options, page: *Page) !PerformanceMark { pub fn constructor(name: []const u8, _options: ?Options, page: *Page) !PerformanceMark {
const perf = &page.window.performance; const perf = &page.window.performance;
const options = _options orelse Options{}; const options = _options orelse Options{};
@@ -166,12 +166,14 @@ pub const PerformanceMark = struct {
} }
const detail = if (options.detail) |d| try d.persist() else null; const detail = if (options.detail) |d| try d.persist() else null;
const proto = PerformanceEntry{ .name = name.string, .entry_type = .mark, .start_time = start_time };
const duped_name = try page.arena.dupe(u8, name);
const proto = PerformanceEntry{ .name = duped_name, .entry_type = .mark, .start_time = start_time };
return .{ .proto = proto, .detail = detail }; return .{ .proto = proto, .detail = detail };
} }
pub fn get_detail(self: *const PerformanceMark) ?js.Object { pub fn get_detail(self: *const PerformanceMark) ?Env.JsObject {
return self.detail; return self.detail;
} }
}; };
@@ -195,12 +197,28 @@ test "Performance: now" {
} }
var after = perf._now(); var after = perf._now();
while (after <= now) { // Loop until after > now while (after <= now) { // Loop untill after > now
try testing.expectEqual(after, now); try testing.expectEqual(after, now);
after = perf._now(); after = perf._now();
} }
} }
test "Browser: Performance.Mark" { test "Browser.Performance.Mark" {
try testing.htmlRunner("dom/performance.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let performance = window.performance", null },
.{ "performance instanceof Performance", "true" },
.{ "let mark1 = performance.mark(\"start\")", null },
.{ "mark1 instanceof PerformanceMark", "true" },
.{ "mark1.name", "start" },
.{ "mark1.entryType", "mark" },
.{ "mark1.duration", "0" },
.{ "mark1.detail", "null" },
.{ "let mark2 = performance.mark(\"start\", {startTime: 32939393.9})", null },
.{ "mark2.startTime", "32939393.9" },
}, .{});
} }

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const PerformanceEntry = @import("performance.zig").PerformanceEntry; const PerformanceEntry = @import("performance.zig").PerformanceEntry;
@@ -25,7 +25,7 @@ const PerformanceEntry = @import("performance.zig").PerformanceEntry;
pub const PerformanceObserver = struct { pub const PerformanceObserver = struct {
pub const _supportedEntryTypes = [0][]const u8{}; pub const _supportedEntryTypes = [0][]const u8{};
pub fn constructor(cbk: js.Function) PerformanceObserver { pub fn constructor(cbk: Env.Function) PerformanceObserver {
_ = cbk; _ = cbk;
return .{}; return .{};
} }
@@ -53,6 +53,11 @@ const Options = struct {
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: DOM.PerformanceObserver" { test "Browser.DOM.PerformanceObserver" {
try testing.htmlRunner("dom/performance_observer.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "PerformanceObserver.supportedEntryTypes.length", "0" },
}, .{});
} }

View File

@@ -48,7 +48,7 @@ pub const ProcessingInstruction = struct {
} }
pub fn get_data(self: *parser.ProcessingInstruction) !?[]const u8 { pub fn get_data(self: *parser.ProcessingInstruction) !?[]const u8 {
return parser.nodeValue(parser.processingInstructionToNode(self)); return try parser.nodeValue(parser.processingInstructionToNode(self));
} }
pub fn set_data(self: *parser.ProcessingInstruction, data: []u8) !void { pub fn set_data(self: *parser.ProcessingInstruction, data: []u8) !void {
@@ -58,7 +58,7 @@ pub const ProcessingInstruction = struct {
// netsurf's ProcessInstruction doesn't implement the dom_node_get_attributes // netsurf's ProcessInstruction doesn't implement the dom_node_get_attributes
// and thus will crash if we try to call nodeIsEqualNode. // and thus will crash if we try to call nodeIsEqualNode.
pub fn _isEqualNode(self: *parser.ProcessingInstruction, other_node: *parser.Node) !bool { pub fn _isEqualNode(self: *parser.ProcessingInstruction, other_node: *parser.Node) !bool {
if (parser.nodeType(other_node) != .processing_instruction) { if (try parser.nodeType(other_node) != .processing_instruction) {
return false; return false;
} }
@@ -87,6 +87,30 @@ pub const ProcessingInstruction = struct {
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: DOM.ProcessingInstruction" { test "Browser.DOM.ProcessingInstruction" {
try testing.htmlRunner("dom/processing_instruction.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
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" },
.{ "pi2.nodeType", "7" },
}, .{});
try runner.testCases(&.{
.{ "let pi11 = document.createProcessingInstruction('target1', 'data1');", "undefined" },
.{ "let pi12 = document.createProcessingInstruction('target2', 'data2');", "undefined" },
.{ "let pi13 = document.createProcessingInstruction('target1', 'data1');", "undefined" },
.{ "pi11.isEqualNode(pi11)", "true" },
.{ "pi11.isEqualNode(pi13)", "true" },
.{ "pi11.isEqualNode(pi12)", "false" },
.{ "pi12.isEqualNode(pi13)", "false" },
.{ "pi11.isEqualNode(document)", "false" },
.{ "document.isEqualNode(pi11)", "false" },
}, .{});
} }

View File

@@ -92,7 +92,7 @@ pub const Range = struct {
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void { pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
try ensureValidOffset(node, offset_); try ensureValidOffset(node, offset_);
const offset: u32 = @intCast(offset_); const offset: u32 = @intCast(offset_);
const position = compare(node, offset, self.proto.end_node, self.proto.end_offset) catch |err| switch (err) { const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
error.WrongDocument => blk: { error.WrongDocument => blk: {
// allow a node with a different root than the current, or // allow a node with a different root than the current, or
// a disconnected one. Treat it as if it's "after", so that // a disconnected one. Treat it as if it's "after", so that
@@ -103,7 +103,7 @@ pub const Range = struct {
}; };
if (position == 1) { if (position == 1) {
// if we're setting the node after the current end, the end must // if we're setting the node after the current start, the end must
// be set too. // be set too.
self.proto.end_offset = offset; self.proto.end_offset = offset;
self.proto.end_node = node; self.proto.end_node = node;
@@ -176,10 +176,10 @@ pub const Range = struct {
self.proto.end_node = node; self.proto.end_node = node;
// Set end_offset // Set end_offset
switch (parser.nodeType(node)) { switch (try parser.nodeType(node)) {
.text, .cdata_section, .comment, .processing_instruction => { .text, .cdata_section, .comment, .processing_instruction => {
// For text-like nodes, end_offset should be the length of the text data // For text-like nodes, end_offset should be the length of the text data
if (parser.nodeValue(node)) |text_data| { if (try parser.nodeValue(node)) |text_data| {
self.proto.end_offset = @intCast(text_data.len); self.proto.end_offset = @intCast(text_data.len);
} else { } else {
self.proto.end_offset = 0; self.proto.end_offset = 0;
@@ -188,7 +188,7 @@ pub const Range = struct {
else => { else => {
// For element and other nodes, end_offset is the number of children // For element and other nodes, end_offset is the number of children
const child_nodes = try parser.nodeGetChildNodes(node); const child_nodes = try parser.nodeGetChildNodes(node);
const child_count = parser.nodeListLength(child_nodes); const child_count = try parser.nodeListLength(child_nodes);
self.proto.end_offset = @intCast(child_count); self.proto.end_offset = @intCast(child_count);
}, },
} }
@@ -211,7 +211,7 @@ pub const Range = struct {
pub fn _comparePoint(self: *const Range, node: *parser.Node, offset_: i32) !i32 { pub fn _comparePoint(self: *const Range, node: *parser.Node, offset_: i32) !i32 {
const start = self.proto.start_node; const start = self.proto.start_node;
if (parser.nodeGetRootNode(start) != parser.nodeGetRootNode(node)) { if (try parser.nodeGetRootNode(start) != try parser.nodeGetRootNode(node)) {
// WPT really wants this error to be first. Later, when we check // WPT really wants this error to be first. Later, when we check
// if the relative position is 'disconnected', it'll also catch this // if the relative position is 'disconnected', it'll also catch this
// case, but WPT will complain because it sometimes also sends // case, but WPT will complain because it sometimes also sends
@@ -219,7 +219,7 @@ pub const Range = struct {
return error.WrongDocument; return error.WrongDocument;
} }
if (parser.nodeType(node) == .document_type) { if (try parser.nodeType(node) == .document_type) {
return error.InvalidNodeType; return error.InvalidNodeType;
} }
@@ -245,8 +245,8 @@ pub const Range = struct {
} }
pub fn _intersectsNode(self: *const Range, node: *parser.Node) !bool { pub fn _intersectsNode(self: *const Range, node: *parser.Node) !bool {
const start_root = parser.nodeGetRootNode(self.proto.start_node); const start_root = try parser.nodeGetRootNode(self.proto.start_node);
const node_root = parser.nodeGetRootNode(node); const node_root = try parser.nodeGetRootNode(node);
if (start_root != node_root) { if (start_root != node_root) {
return false; return false;
} }
@@ -299,29 +299,29 @@ fn ensureValidOffset(node: *parser.Node, offset: i32) !void {
fn nodeLength(node: *parser.Node) !usize { fn nodeLength(node: *parser.Node) !usize {
switch (try isTextual(node)) { switch (try isTextual(node)) {
true => return ((parser.nodeTextContent(node)) orelse "").len, true => return ((try parser.nodeTextContent(node)) orelse "").len,
false => { false => {
const children = try parser.nodeGetChildNodes(node); const children = try parser.nodeGetChildNodes(node);
return @intCast(parser.nodeListLength(children)); return @intCast(try parser.nodeListLength(children));
}, },
} }
} }
fn isTextual(node: *parser.Node) !bool { fn isTextual(node: *parser.Node) !bool {
return switch (parser.nodeType(node)) { return switch (try parser.nodeType(node)) {
.text, .comment, .cdata_section => true, .text, .comment, .cdata_section => true,
else => false, else => false,
}; };
} }
fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } { fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } {
const parent = (parser.nodeParentNode(child)) orelse return error.InvalidNodeType; const parent = (try parser.nodeParentNode(child)) orelse return error.InvalidNodeType;
const children = try parser.nodeGetChildNodes(parent); const children = try parser.nodeGetChildNodes(parent);
const ln = parser.nodeListLength(children); const ln = try parser.nodeListLength(children);
var i: u32 = 0; var i: u32 = 0;
while (i < ln) { while (i < ln) {
defer i += 1; defer i += 1;
const c = parser.nodeListItem(children, i) orelse continue; const c = try parser.nodeListItem(children, i) orelse continue;
if (c == child) { if (c == child) {
return .{ parent, i }; return .{ parent, i };
} }
@@ -363,7 +363,7 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
if (position & @intFromEnum(parser.DocumentPosition.contains) == @intFromEnum(parser.DocumentPosition.contains)) { if (position & @intFromEnum(parser.DocumentPosition.contains) == @intFromEnum(parser.DocumentPosition.contains)) {
// node_a contains node_b // node_a contains node_b
var child = node_b; var child = node_b;
while (parser.nodeParentNode(child)) |parent| { while (try parser.nodeParentNode(child)) |parent| {
if (parent == node_a) { if (parent == node_a) {
// child.parentNode == node_a // child.parentNode == node_a
break; break;
@@ -378,13 +378,51 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
const child_parent, const child_index = try getParentAndIndex(child); const child_parent, const child_index = try getParentAndIndex(child);
std.debug.assert(node_a == child_parent); std.debug.assert(node_a == child_parent);
return if (offset_a <= child_index) -1 else 1; return if (child_index < offset_a) -1 else 1;
} }
return -1; return -1;
} }
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: Range" { test "Browser.Range" {
try testing.htmlRunner("dom/range.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
// Test Range constructor
.{ "let range = new Range()", "undefined" },
.{ "range instanceof Range", "true" },
.{ "range instanceof AbstractRange", "true" },
// Test initial state - collapsed range
.{ "range.collapsed", "true" },
.{ "range.startOffset", "0" },
.{ "range.endOffset", "0" },
.{ "range.startContainer instanceof HTMLDocument", "true" },
.{ "range.endContainer instanceof HTMLDocument", "true" },
// Test document.createRange()
.{ "let docRange = document.createRange()", "undefined" },
.{ "docRange instanceof Range", "true" },
.{ "docRange.collapsed", "true" },
}, .{});
try runner.testCases(&.{
.{ "const container = document.getElementById('content');", null },
// Test text range
.{ "const commentNode = container.childNodes[7];", null },
.{ "commentNode.nodeValue", "comment" },
.{ "const textRange = document.createRange();", null },
.{ "textRange.selectNodeContents(commentNode)", "undefined" },
.{ "textRange.startOffset", "0" },
.{ "textRange.endOffset", "7" }, // length of `comment`
// Test Node range
.{ "const nodeRange = document.createRange();", null },
.{ "nodeRange.selectNodeContents(container)", "undefined" },
.{ "nodeRange.startOffset", "0" },
.{ "nodeRange.endOffset", "9" }, // length of container.childNodes
}, .{});
} }

View File

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

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const dump = @import("../dump.zig"); const dump = @import("../dump.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const js = @import(".././js/js.zig"); const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
const Element = @import("element.zig").Element; const Element = @import("element.zig").Element;
@@ -34,7 +34,7 @@ pub const ShadowRoot = struct {
mode: Mode, mode: Mode,
host: *parser.Element, host: *parser.Element,
proto: *parser.DocumentFragment, proto: *parser.DocumentFragment,
adopted_style_sheets: ?js.Object = null, adopted_style_sheets: ?Env.JsObject = null,
pub const Mode = enum { pub const Mode = enum {
open, open,
@@ -45,17 +45,17 @@ pub const ShadowRoot = struct {
return Element.toInterface(self.host); return Element.toInterface(self.host);
} }
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object { pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject {
if (self.adopted_style_sheets) |obj| { if (self.adopted_style_sheets) |obj| {
return obj; return obj;
} }
const obj = try page.js.createArray(0).persist(); const obj = try page.main_context.newArray(0).persist();
self.adopted_style_sheets = obj; self.adopted_style_sheets = obj;
return obj; return obj;
} }
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: js.Object) !void { pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
self.adopted_style_sheets = try sheets.persist(); self.adopted_style_sheets = try sheets.persist();
} }
@@ -67,7 +67,7 @@ pub const ShadowRoot = struct {
pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void { pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void {
const sr_doc = parser.documentFragmentToNode(self.proto); const sr_doc = parser.documentFragmentToNode(self.proto);
const doc = parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument; const doc = try parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument;
try Node.removeChildren(sr_doc); try Node.removeChildren(sr_doc);
const str = str_ orelse return; const str = str_ orelse return;
@@ -80,22 +80,76 @@ pub const ShadowRoot = struct {
// element. // element.
// For ShadowRoot, it appears the only the children within the body should // For ShadowRoot, it appears the only the children within the body should
// be set. // be set.
const html = parser.nodeFirstChild(fragment_node) orelse return; const html = try parser.nodeFirstChild(fragment_node) orelse return;
const head = parser.nodeFirstChild(html) orelse return; const head = try parser.nodeFirstChild(html) orelse return;
const body = parser.nodeNextSibling(head) orelse return; const body = try parser.nodeNextSibling(head) orelse return;
const children = try parser.nodeGetChildNodes(body); const children = try parser.nodeGetChildNodes(body);
const ln = parser.nodeListLength(children); const ln = try parser.nodeListLength(children);
for (0..ln) |_| { for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of // always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree // the nodeList and into the new tree
const child = parser.nodeListItem(children, 0) orelse continue; const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(sr_doc, child); _ = try parser.nodeAppendChild(sr_doc, child);
} }
} }
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: DOM.ShadowRoot" { test "Browser.DOM.ShadowRoot" {
try testing.htmlRunner("dom/shadow_root.html"); defer testing.reset();
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
\\ <div id=conflict>nope</div>
});
defer runner.deinit();
try runner.testCases(&.{
.{ "const div1 = document.createElement('div');", null },
.{ "let sr1 = div1.attachShadow({mode: 'open'})", null },
.{ "sr1.host == div1", "true" },
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
.{ "div1.shadowRoot == sr1", "true" },
.{ "try { div1.attachShadow({mode: 'closed'}) } catch (e) { e }", "Error: NotSupportedError" },
.{ " sr1.append(document.createElement('div'))", null },
.{ " sr1.append(document.createElement('span'))", null },
.{ "sr1.childElementCount", "2" },
// re-attaching clears it
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
.{ "sr1.childElementCount", "0" },
}, .{});
try runner.testCases(&.{
.{ "const div2 = document.createElement('di2');", null },
.{ "let sr2 = div2.attachShadow({mode: 'closed'})", null },
.{ "sr2.host == div2", "true" },
.{ "div2.shadowRoot", "null" }, // null when attached with 'closed'
}, .{});
try runner.testCases(&.{
.{ "sr2.getElementById('conflict')", "null" },
.{ "const n1 = document.createElement('div')", null },
.{ "n1.id = 'conflict'", null },
.{ "sr2.append(n1)", null },
.{ "sr2.getElementById('conflict') == n1", "true" },
}, .{});
try runner.testCases(&.{
.{ "const acss = sr2.adoptedStyleSheets", null },
.{ "acss.length", "0" },
.{ "acss.push(new CSSStyleSheet())", null },
.{ "sr2.adoptedStyleSheets.length", "1" },
}, .{});
try runner.testCases(&.{
.{ "sr1.innerHTML = '<p>hello</p>'", null },
.{ "sr1.innerHTML", "<p>hello</p>" },
.{ "sr1.querySelector('*')", "[object HTMLParagraphElement]" },
.{ "sr1.innerHTML = null", null },
.{ "sr1.innerHTML", "" },
.{ "sr1.querySelector('*')", "null" },
}, .{});
} }

View File

@@ -56,7 +56,31 @@ pub const Text = struct {
} }
}; };
// Tests
// -----
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: DOM.Text" { test "Browser.DOM.Text" {
try testing.htmlRunner("dom/text.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let t = new Text('foo')", "undefined" },
.{ "t.data", "foo" },
.{ "let emptyt = new Text()", "undefined" },
.{ "emptyt.data", "" },
}, .{});
try runner.testCases(&.{
.{ "let text = document.getElementById('link').firstChild", "undefined" },
.{ "text.wholeText === 'OK'", "true" },
}, .{});
try runner.testCases(&.{
.{ "text.data = 'OK modified'", "OK modified" },
.{ "let split = text.splitText('OK'.length)", "undefined" },
.{ "split.data === ' modified'", "true" },
.{ "text.data === 'OK'", "true" },
}, .{});
} }

View File

@@ -18,11 +18,12 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const iterator = @import("../iterator/iterator.zig"); const iterator = @import("../iterator/iterator.zig");
const Function = @import("../env.zig").Function;
const JsObject = @import("../env.zig").JsObject;
const DOMException = @import("exceptions.zig").DOMException; const DOMException = @import("exceptions.zig").DOMException;
pub const Interfaces = .{ pub const Interfaces = .{
@@ -136,10 +137,10 @@ pub const DOMTokenList = struct {
} }
// TODO handle thisArg // TODO handle thisArg
pub fn _forEach(self: *parser.TokenList, cbk: js.Function, this_arg: js.Object) !void { pub fn _forEach(self: *parser.TokenList, cbk: Function, this_arg: JsObject) !void {
var entries = _entries(self); var entries = _entries(self);
while (try entries._next()) |entry| { while (try entries._next()) |entry| {
var result: js.Function.Result = undefined; var result: Function.Result = undefined;
cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch { cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
log.debug(.user_script, "callback error", .{ log.debug(.user_script, "callback error", .{
.err = result.exception, .err = result.exception,
@@ -168,7 +169,77 @@ pub const Iterator = struct {
} }
}; };
// Tests
// -----
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: DOM.TokenList" { test "Browser.DOM.TokenList" {
try testing.htmlRunner("dom/token_list.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
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" },
}, .{});
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" },
}, .{});
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" },
}, .{});
try runner.testCases(&.{
.{ "let cl5 = gs.classList", "undefined" },
.{ "let keys = [...cl5.keys()]", "undefined" },
.{ "keys.length", "2" },
.{ "keys[0]", "0" },
.{ "keys[1]", "1" },
.{ "let values = [...cl5.values()]", "undefined" },
.{ "values.length", "2" },
.{ "values[0]", "empty" },
.{ "values[1]", "ok" },
.{ "let entries = [...cl5.entries()]", "undefined" },
.{ "entries.length", "2" },
.{ "entries[0]", "0,empty" },
.{ "entries[1]", "1,ok" },
}, .{});
try runner.testCases(&.{
.{ "let cl6 = gs.classList", "undefined" },
.{ "cl6.value = 'a b ccc'", "a b ccc" },
.{ "cl6.value", "a b ccc" },
.{ "cl6.toString()", "a b ccc" },
}, .{});
} }

View File

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

View File

@@ -44,39 +44,39 @@ pub const WalkerDepthFirst = struct {
var n = cur orelse root; var n = cur orelse root;
// TODO deinit next // TODO deinit next
if (parser.nodeFirstChild(n)) |next| { if (try parser.nodeFirstChild(n)) |next| {
return next; return next;
} }
// TODO deinit next // TODO deinit next
if (parser.nodeNextSibling(n)) |next| { if (try parser.nodeNextSibling(n)) |next| {
return next; return next;
} }
// TODO deinit parent // TODO deinit parent
// Back to the parent of cur. // Back to the parent of cur.
// If cur has no parent, then the iteration is over. // If cur has no parent, then the iteration is over.
var parent = parser.nodeParentNode(n) orelse return null; var parent = try parser.nodeParentNode(n) orelse return null;
// TODO deinit lastchild // TODO deinit lastchild
var lastchild = parser.nodeLastChild(parent); var lastchild = try parser.nodeLastChild(parent);
while (n != root and n == lastchild) { while (n != root and n == lastchild) {
n = parent; n = parent;
// TODO deinit parent // TODO deinit parent
// Back to the prev's parent. // Back to the prev's parent.
// If prev has no parent, then the loop must stop. // If prev has no parent, then the loop must stop.
parent = parser.nodeParentNode(n) orelse break; parent = try parser.nodeParentNode(n) orelse break;
// TODO deinit lastchild // TODO deinit lastchild
lastchild = parser.nodeLastChild(parent); lastchild = try parser.nodeLastChild(parent);
} }
if (n == root) { if (n == root) {
return null; return null;
} }
return parser.nodeNextSibling(n); return try parser.nodeNextSibling(n);
} }
}; };
@@ -84,14 +84,14 @@ pub const WalkerDepthFirst = struct {
pub const WalkerChildren = struct { pub const WalkerChildren = struct {
pub fn get_next(_: WalkerChildren, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node { pub fn get_next(_: WalkerChildren, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
// On walk start, we return the first root's child. // On walk start, we return the first root's child.
if (cur == null) return parser.nodeFirstChild(root); if (cur == null) return try parser.nodeFirstChild(root);
// If cur is root, then return null. // If cur is root, then return null.
// This is a special case, if the root is included in the walk, we // This is a special case, if the root is included in the walk, we
// don't want to go further to find children. // don't want to go further to find children.
if (root == cur.?) return null; if (root == cur.?) return null;
return parser.nodeNextSibling(cur.?); return try parser.nodeNextSibling(cur.?);
} }
}; };

View File

@@ -26,13 +26,7 @@ pub const Opts = struct {
// set to include element shadowroots in the dump // set to include element shadowroots in the dump
page: ?*const Page = null, page: ?*const Page = null,
strip_mode: StripMode = .{}, exclude_scripts: bool = false,
pub const StripMode = struct {
js: bool = false,
ui: bool = false,
css: bool = false,
};
}; };
// writer must be a std.io.Writer // writer must be a std.io.Writer
@@ -47,8 +41,8 @@ pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !voi
try writer.writeAll("<!DOCTYPE "); try writer.writeAll("<!DOCTYPE ");
try writer.writeAll(try parser.documentTypeGetName(doc_type)); try writer.writeAll(try parser.documentTypeGetName(doc_type));
const public_id = parser.documentTypeGetPublicId(doc_type); const public_id = try parser.documentTypeGetPublicId(doc_type);
const system_id = parser.documentTypeGetSystemId(doc_type); const system_id = try parser.documentTypeGetSystemId(doc_type);
if (public_id.len != 0 and system_id.len != 0) { if (public_id.len != 0 and system_id.len != 0) {
try writer.writeAll(" PUBLIC \""); try writer.writeAll(" PUBLIC \"");
try writeEscapedAttributeValue(writer, public_id); try writeEscapedAttributeValue(writer, public_id);
@@ -69,11 +63,11 @@ pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !voi
} }
pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerror!void { pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerror!void {
switch (parser.nodeType(node)) { switch (try parser.nodeType(node)) {
.element => { .element => {
// open the tag // open the tag
const tag_type = try parser.nodeHTMLGetTagType(node) orelse .undef; const tag_type = try parser.nodeHTMLGetTagType(node) orelse .undef;
if (try isStripped(tag_type, node, opts.strip_mode)) { if (opts.exclude_scripts and try isScriptOrRelated(tag_type, node)) {
return; return;
} }
@@ -110,7 +104,7 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerro
if (try isVoid(parser.nodeToElement(node))) return; if (try isVoid(parser.nodeToElement(node))) return;
if (tag_type == .script) { if (tag_type == .script) {
try writer.writeAll(parser.nodeTextContent(node) orelse ""); try writer.writeAll(try parser.nodeTextContent(node) orelse "");
} else { } else {
// write the children // write the children
// TODO avoid recursion // TODO avoid recursion
@@ -123,17 +117,17 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerro
try writer.writeAll(">"); try writer.writeAll(">");
}, },
.text => { .text => {
const v = parser.nodeValue(node) orelse return; const v = try parser.nodeValue(node) orelse return;
try writeEscapedTextNode(writer, v); try writeEscapedTextNode(writer, v);
}, },
.cdata_section => { .cdata_section => {
const v = parser.nodeValue(node) orelse return; const v = try parser.nodeValue(node) orelse return;
try writer.writeAll("<![CDATA["); try writer.writeAll("<![CDATA[");
try writer.writeAll(v); try writer.writeAll(v);
try writer.writeAll("]]>"); try writer.writeAll("]]>");
}, },
.comment => { .comment => {
const v = parser.nodeValue(node) orelse return; const v = try parser.nodeValue(node) orelse return;
try writer.writeAll("<!--"); try writer.writeAll("<!--");
try writer.writeAll(v); try writer.writeAll(v);
try writer.writeAll("-->"); try writer.writeAll("-->");
@@ -165,22 +159,9 @@ pub fn writeChildren(root: *parser.Node, opts: Opts, writer: *std.Io.Writer) !vo
} }
} }
fn isStripped(tag_type: parser.Tag, node: *parser.Node, strip_mode: Opts.StripMode) !bool { // When `exclude_scripts` is passed to dump, we don't include <script> tags.
if (strip_mode.js and try isJsRelated(tag_type, node)) { // We also want to omit <link rel=preload as=ascript>
return true; fn isScriptOrRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
}
if (strip_mode.css and try isCssRelated(tag_type, node)) {
return true;
}
if (strip_mode.ui and try isUIRelated(tag_type, node)) {
return true;
}
return false;
}
fn isJsRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
if (tag_type == .script) { if (tag_type == .script) {
return true; return true;
} }
@@ -197,34 +178,6 @@ fn isJsRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
return false; return false;
} }
fn isCssRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
if (tag_type == .style) {
return true;
}
if (tag_type == .link) {
const el = parser.nodeToElement(node);
const rel = try parser.elementGetAttribute(el, "rel") orelse return false;
return std.ascii.eqlIgnoreCase(rel, "stylesheet");
}
return false;
}
fn isUIRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
if (try isCssRelated(tag_type, node)) {
return true;
}
if (tag_type == .img or tag_type == .picture or tag_type == .video) {
return true;
}
if (tag_type == .undef) {
const name = try parser.nodeLocalName(node);
if (std.mem.eql(u8, name, "svg")) {
return true;
}
}
return false;
}
// area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr // area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
// https://html.spec.whatwg.org/#void-elements // https://html.spec.whatwg.org/#void-elements
fn isVoid(elem: *parser.Element) !bool { fn isVoid(elem: *parser.Element) !bool {
@@ -236,10 +189,10 @@ fn isVoid(elem: *parser.Element) !bool {
}; };
} }
fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void { fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
var v = value; var v = value;
while (v.len > 0) { while (v.len > 0) {
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', 194 }) orelse { const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
return writer.writeAll(v); return writer.writeAll(v);
}; };
try writer.writeAll(v[0..index]); try writer.writeAll(v[0..index]);
@@ -247,22 +200,13 @@ fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void {
'&' => try writer.writeAll("&amp;"), '&' => try writer.writeAll("&amp;"),
'<' => try writer.writeAll("&lt;"), '<' => try writer.writeAll("&lt;"),
'>' => try writer.writeAll("&gt;"), '>' => try writer.writeAll("&gt;"),
194 => {
// non breaking space
if (v.len > index + 1 and v[index + 1] == 160) {
try writer.writeAll("&nbsp;");
v = v[index + 2 ..];
continue;
}
try writer.writeByte(194);
},
else => unreachable, else => unreachable,
} }
v = v[index + 1 ..]; v = v[index + 1 ..];
} }
} }
fn writeEscapedAttributeValue(writer: *std.Io.Writer, value: []const u8) !void { fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
var v = value; var v = value;
while (v.len > 0) { while (v.len > 0) {
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse { const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
@@ -282,7 +226,7 @@ fn writeEscapedAttributeValue(writer: *std.Io.Writer, value: []const u8) !void {
const testing = std.testing; const testing = std.testing;
test "dump.writeHTML" { test "dump.writeHTML" {
parser.init(); try parser.init();
defer parser.deinit(); defer parser.deinit();
try testWriteHTML( try testWriteHTML(

View File

@@ -19,7 +19,7 @@
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Page = @import("../page.zig").Page; const Env = @import("../env.zig").Env;
// https://encoding.spec.whatwg.org/#interface-textdecoder // https://encoding.spec.whatwg.org/#interface-textdecoder
const TextDecoder = @This(); const TextDecoder = @This();
@@ -37,7 +37,6 @@ const Options = struct {
fatal: bool, fatal: bool,
ignore_bom: bool, ignore_bom: bool,
stream: std.ArrayList(u8),
pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder { pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
if (label_) |l| { if (label_) |l| {
@@ -48,7 +47,6 @@ pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
} }
const opts = opts_ orelse Options{}; const opts = opts_ orelse Options{};
return .{ return .{
.stream = .empty,
.fatal = opts.fatal, .fatal = opts.fatal,
.ignore_bom = opts.ignoreBOM, .ignore_bom = opts.ignoreBOM,
}; };
@@ -66,34 +64,18 @@ pub fn get_fatal(self: *const TextDecoder) bool {
return self.fatal; return self.fatal;
} }
const DecodeOptions = struct { // TODO: Should accept an ArrayBuffer, TypedArray or DataView
stream: bool = false, // js.zig will currently only map a TypedArray to our []const u8.
}; pub fn _decode(self: *const TextDecoder, v: []const u8) ![]const u8 {
pub fn _decode(self: *TextDecoder, str_: ?[]const u8, opts_: ?DecodeOptions, page: *Page) ![]const u8 { if (self.fatal and !std.unicode.utf8ValidateSlice(v)) {
var str = str_ orelse return "";
const opts: DecodeOptions = opts_ orelse .{};
if (self.stream.items.len > 0) {
try self.stream.appendSlice(page.arena, str);
str = self.stream.items;
}
if (self.fatal and !std.unicode.utf8ValidateSlice(str)) {
if (opts.stream) {
if (self.stream.items.len == 0) {
try self.stream.appendSlice(page.arena, str);
}
return "";
}
return error.InvalidUtf8; return error.InvalidUtf8;
} }
self.stream.clearRetainingCapacity(); if (self.ignore_bom == false and std.mem.startsWith(u8, v, &.{ 0xEF, 0xBB, 0xBF })) {
if (self.ignore_bom == false and std.mem.startsWith(u8, str, &.{ 0xEF, 0xBB, 0xBF })) { return v[3..];
return str[3..];
} }
return str; return v;
} }
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

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

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

@@ -0,0 +1,49 @@
const std = @import("std");
const Page = @import("page.zig").Page;
const js = @import("../runtime/js.zig");
const generate = @import("../runtime/generate.zig");
const WebApis = struct {
// Wrapped like this for debug ergonomics.
// When we create our Env, a few lines down, we define it as:
// pub const Env = js.Env(*Page, WebApis);
//
// If there's a compile time error witht he Env, it's type will be readable,
// i.e.: runtime.js.Env(*browser.env.Page, browser.env.WebApis)
//
// But if we didn't wrap it in the struct, like we once didn't, and defined
// env as:
// pub const Env = js.Env(*Page, Interfaces);
//
// Because Interfaces is an anynoumous type, it doesn't have a friendly name
// and errors would be something like:
// runtime.js.Env(*browser.Page, .{...A HUNDRED TYPES...})
pub const Interfaces = generate.Tuple(.{
@import("crypto/crypto.zig").Crypto,
@import("console/console.zig").Console,
@import("css/css.zig").Interfaces,
@import("cssom/cssom.zig").Interfaces,
@import("dom/dom.zig").Interfaces,
@import("dom/shadow_root.zig").ShadowRoot,
@import("encoding/encoding.zig").Interfaces,
@import("events/event.zig").Interfaces,
@import("html/html.zig").Interfaces,
@import("iterator/iterator.zig").Interfaces,
@import("storage/storage.zig").Interfaces,
@import("url/url.zig").Interfaces,
@import("xhr/xhr.zig").Interfaces,
@import("xhr/form_data.zig").Interfaces,
@import("xhr/File.zig"),
@import("xmlserializer/xmlserializer.zig").Interfaces,
});
};
pub const JsThis = Env.JsThis;
pub const JsObject = Env.JsObject;
pub const Function = Env.Function;
pub const Promise = Env.Promise;
pub const PromiseResolver = Env.PromiseResolver;
pub const Env = js.Env(*Page, WebApis);
pub const Global = @import("html/window.zig").Window;

View File

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

View File

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

View File

@@ -17,12 +17,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const generate = @import("../js/generate.zig"); const generate = @import("../../runtime/generate.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Node = @import("../dom/node.zig").Node; const Node = @import("../dom/node.zig").Node;
@@ -34,26 +33,11 @@ const AbortSignal = @import("../html/AbortController.zig").AbortSignal;
const CustomEvent = @import("custom_event.zig").CustomEvent; const CustomEvent = @import("custom_event.zig").CustomEvent;
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent; const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
const MouseEvent = @import("mouse_event.zig").MouseEvent; const MouseEvent = @import("mouse_event.zig").MouseEvent;
const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent; const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent; const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent;
// Event interfaces // Event interfaces
pub const Interfaces = .{ pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent };
Event,
CustomEvent,
ProgressEvent,
MouseEvent,
KeyboardEvent,
ErrorEvent,
MessageEvent,
PopStateEvent,
CompositionEvent,
NavigationCurrentEntryChangeEvent,
};
pub const Union = generate.Union(Interfaces); pub const Union = generate.Union(Interfaces);
@@ -71,20 +55,14 @@ pub const Event = struct {
pub const _AT_TARGET = 2; pub const _AT_TARGET = 2;
pub const _BUBBLING_PHASE = 3; pub const _BUBBLING_PHASE = 3;
pub fn toInterface(evt: *parser.Event) Union { pub fn toInterface(evt: *parser.Event) !Union {
return switch (parser.eventGetInternalType(evt)) { return switch (try parser.eventGetInternalType(evt)) {
.event, .abort_signal, .xhr_event => .{ .Event = evt }, .event, .abort_signal, .xhr_event => .{ .Event = evt },
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* }, .custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* }, .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) }, .mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
.error_event => .{ .ErrorEvent = (@as(*ErrorEvent, @fieldParentPtr("proto", evt))).* }, .error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* }, .message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
.composition_event => .{ .CompositionEvent = (@as(*CompositionEvent, @fieldParentPtr("proto", evt))).* },
.navigation_current_entry_change_event => .{
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
},
}; };
} }
@@ -96,46 +74,46 @@ pub const Event = struct {
// Getters // Getters
pub fn get_type(self: *parser.Event) []const u8 { pub fn get_type(self: *parser.Event) ![]const u8 {
return parser.eventType(self); return try parser.eventType(self);
} }
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion { pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
const et = parser.eventTarget(self); const et = try parser.eventTarget(self);
if (et == null) return null; if (et == null) return null;
return try EventTarget.toInterface(et.?, page); return try EventTarget.toInterface(et.?, page);
} }
pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion { pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion {
const et = parser.eventCurrentTarget(self); const et = try parser.eventCurrentTarget(self);
if (et == null) return null; if (et == null) return null;
return try EventTarget.toInterface(et.?, page); return try EventTarget.toInterface(et.?, page);
} }
pub fn get_eventPhase(self: *parser.Event) u8 { pub fn get_eventPhase(self: *parser.Event) !u8 {
return parser.eventPhase(self); return try parser.eventPhase(self);
} }
pub fn get_bubbles(self: *parser.Event) bool { pub fn get_bubbles(self: *parser.Event) !bool {
return parser.eventBubbles(self); return try parser.eventBubbles(self);
} }
pub fn get_cancelable(self: *parser.Event) bool { pub fn get_cancelable(self: *parser.Event) !bool {
return parser.eventCancelable(self); return try parser.eventCancelable(self);
} }
pub fn get_defaultPrevented(self: *parser.Event) bool { pub fn get_defaultPrevented(self: *parser.Event) !bool {
return parser.eventDefaultPrevented(self); return try parser.eventDefaultPrevented(self);
} }
pub fn get_isTrusted(self: *parser.Event) bool { pub fn get_isTrusted(self: *parser.Event) !bool {
return parser.eventIsTrusted(self); return try parser.eventIsTrusted(self);
} }
// Even though this is supposed to to provide microsecond resolution, browser // Even though this is supposed to to provide microsecond resolution, browser
// return coarser values to protect against fingerprinting. libdom returns // return coarser values to protect against fingerprinting. libdom returns
// seconds, which is good enough. // seconds, which is good enough.
pub fn get_timeStamp(self: *parser.Event) u64 { pub fn get_timeStamp(self: *parser.Event) !u32 {
return parser.eventTimestamp(self); return parser.eventTimestamp(self);
} }
@@ -155,22 +133,22 @@ pub const Event = struct {
} }
pub fn _stopPropagation(self: *parser.Event) !void { pub fn _stopPropagation(self: *parser.Event) !void {
return parser.eventStopPropagation(self); return try parser.eventStopPropagation(self);
} }
pub fn _stopImmediatePropagation(self: *parser.Event) !void { pub fn _stopImmediatePropagation(self: *parser.Event) !void {
return parser.eventStopImmediatePropagation(self); return try parser.eventStopImmediatePropagation(self);
} }
pub fn _preventDefault(self: *parser.Event) !void { pub fn _preventDefault(self: *parser.Event) !void {
return parser.eventPreventDefault(self); return try parser.eventPreventDefault(self);
} }
pub fn _composedPath(self: *parser.Event, page: *Page) ![]const EventTargetUnion { pub fn _composedPath(self: *parser.Event, page: *Page) ![]const EventTargetUnion {
const et_ = parser.eventTarget(self); const et_ = try parser.eventTarget(self);
const et = et_ orelse return &.{}; const et = et_ orelse return &.{};
var node: ?*parser.Node = switch (parser.eventTargetInternalType(et)) { var node: ?*parser.Node = switch (try parser.eventTargetInternalType(et)) {
.libdom_node => @as(*parser.Node, @ptrCast(et)), .libdom_node => @as(*parser.Node, @ptrCast(et)),
.plain => parser.eventTargetToNode(et), .plain => parser.eventTargetToNode(et),
else => { else => {
@@ -186,8 +164,8 @@ pub const Event = struct {
.node = try Node.toInterface(n), .node = try Node.toInterface(n),
}); });
node = parser.nodeParentNode(n); node = try parser.nodeParentNode(n);
if (node == null and parser.nodeType(n) == .document_fragment) { if (node == null and try parser.nodeType(n) == .document_fragment) {
// we have a non-continuous hook from a shadowroot to its host ( // we have a non-continuous hook from a shadowroot to its host (
// it's parent element). libdom doesn't really support ShdowRoots // it's parent element). libdom doesn't really support ShdowRoots
// and, for the most part, that works out well since it naturally // and, for the most part, that works out well since it naturally
@@ -228,15 +206,18 @@ pub const Event = struct {
pub const EventHandler = struct { pub const EventHandler = struct {
once: bool, once: bool,
capture: bool, capture: bool,
callback: js.Function, callback: Function,
node: parser.EventNode, node: parser.EventNode,
listener: *parser.EventListener, listener: *parser.EventListener,
pub const Listener = union(enum) { const Env = @import("../env.zig").Env;
function: js.Function, const Function = Env.Function;
object: js.Object,
pub fn callback(self: Listener, target: *parser.EventTarget) !?js.Function { pub const Listener = union(enum) {
function: Function,
object: Env.JsObject,
pub fn callback(self: Listener, target: *parser.EventTarget) !?Function {
return switch (self) { return switch (self) {
.function => |func| try func.withThis(target), .function => |func| try func.withThis(target),
.object => |obj| blk: { .object => |obj| blk: {
@@ -335,9 +316,13 @@ pub const EventHandler = struct {
} }
fn handle(node: *parser.EventNode, event: *parser.Event) void { fn handle(node: *parser.EventNode, event: *parser.Event) void {
const ievent = Event.toInterface(event); const ievent = Event.toInterface(event) catch |err| {
log.err(.app, "toInterface error", .{ .err = err });
return;
};
const self: *EventHandler = @fieldParentPtr("node", node); const self: *EventHandler = @fieldParentPtr("node", node);
var result: js.Function.Result = undefined; var result: Function.Result = undefined;
self.callback.tryCall(void, .{ievent}, &result) catch { self.callback.tryCall(void, .{ievent}, &result) catch {
log.debug(.user_script, "callback error", .{ log.debug(.user_script, "callback error", .{
.err = result.exception, .err = result.exception,
@@ -347,8 +332,8 @@ pub const EventHandler = struct {
}; };
if (self.once) { if (self.once) {
const target = parser.eventTarget(event).?; const target = (parser.eventTarget(event) catch return).?;
const typ = parser.eventType(event); const typ = parser.eventType(event) catch return;
parser.eventTargetRemoveEventListener( parser.eventTargetRemoveEventListener(
target, target,
typ, typ,
@@ -403,40 +388,6 @@ const SignalCallback = struct {
} }
}; };
pub fn DirectEventHandler(
comptime TargetT: type,
target: *TargetT,
event_type: []const u8,
maybe_listener: ?EventHandler.Listener,
cb: *?js.Function,
page_arena: std.mem.Allocator,
) !void {
const event_target = parser.toEventTarget(TargetT, target);
// Check if we have a listener set.
if (cb.*) |callback| {
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
std.debug.assert(listener != null);
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
}
if (maybe_listener) |listener| {
switch (listener) {
// If an object is given as listener, do nothing.
.object => {},
.function => |callback| {
_ = try EventHandler.register(page_arena, event_target, event_type, listener, null) orelse unreachable;
cb.* = callback;
return;
},
}
}
// Just unset the listener.
cb.* = null;
}
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: Event" { test "Browser: Event" {
try testing.htmlRunner("events/event.html"); try testing.htmlRunner("events/event.html");

View File

@@ -1,159 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const builtin = @import("builtin");
const parser = @import("../netsurf.zig");
const Event = @import("event.zig").Event;
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
const UIEvent = Event;
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
pub const KeyboardEvent = struct {
pub const Self = parser.KeyboardEvent;
pub const prototype = *UIEvent;
pub const ConstructorOptions = struct {
key: []const u8 = "",
code: []const u8 = "",
location: parser.KeyboardEventOpts.LocationCode = .standard,
repeat: bool = false,
isComposing: bool = false,
// Currently not supported but we take as argument.
charCode: u32 = 0,
// Currently not supported but we take as argument.
keyCode: u32 = 0,
// Currently not supported but we take as argument.
which: u32 = 0,
ctrlKey: bool = false,
shiftKey: bool = false,
altKey: bool = false,
metaKey: bool = false,
};
pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*parser.KeyboardEvent {
const options: ConstructorOptions = maybe_options orelse .{};
const event = try parser.keyboardEventCreate();
parser.eventSetInternalType(@ptrCast(event), .keyboard_event);
try parser.keyboardEventInit(
event,
event_type,
.{
.key = options.key,
.code = options.code,
.location = options.location,
.repeat = options.repeat,
.is_composing = options.isComposing,
.ctrl_key = options.ctrlKey,
.shift_key = options.shiftKey,
.alt_key = options.altKey,
.meta_key = options.metaKey,
},
);
return event;
}
// Returns the modifier state for given modifier key.
pub fn _getModifierState(self: *Self, key: []const u8) bool {
// Chrome and Firefox do case-sensitive match, here we prefer the same.
if (std.mem.eql(u8, key, "Alt")) {
return get_altKey(self);
}
if (std.mem.eql(u8, key, "AltGraph")) {
return (get_altKey(self) and get_ctrlKey(self));
}
if (std.mem.eql(u8, key, "Control")) {
return get_ctrlKey(self);
}
if (std.mem.eql(u8, key, "Shift")) {
return get_shiftKey(self);
}
if (std.mem.eql(u8, key, "Meta") or std.mem.eql(u8, key, "OS")) {
return get_metaKey(self);
}
// Special case for IE.
if (comptime builtin.os.tag == .windows) {
if (std.mem.eql(u8, key, "Win")) {
return get_metaKey(self);
}
}
// getModifierState() also accepts a deprecated virtual modifier named "Accel".
// event.getModifierState("Accel") returns true when at least one of
// KeyboardEvent.ctrlKey or KeyboardEvent.metaKey is true.
//
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState#accel_virtual_modifier
if (std.mem.eql(u8, key, "Accel")) {
return (get_ctrlKey(self) or get_metaKey(self));
}
// TODO: Add support for "CapsLock", "ScrollLock".
return false;
}
// Getters.
pub fn get_altKey(self: *Self) bool {
return parser.keyboardEventKeyIsSet(self, .alt);
}
pub fn get_ctrlKey(self: *Self) bool {
return parser.keyboardEventKeyIsSet(self, .ctrl);
}
pub fn get_metaKey(self: *Self) bool {
return parser.keyboardEventKeyIsSet(self, .meta);
}
pub fn get_shiftKey(self: *Self) bool {
return parser.keyboardEventKeyIsSet(self, .shift);
}
pub fn get_isComposing(self: *Self) bool {
return self.is_composing;
}
pub fn get_location(self: *Self) u32 {
return self.location;
}
pub fn get_key(self: *Self) ![]const u8 {
return parser.keyboardEventGetKey(self);
}
pub fn get_repeat(self: *Self) bool {
return self.repeat;
}
};
const testing = @import("../../testing.zig");
test "Browser: Events.Keyboard" {
try testing.htmlRunner("events/keyboard.html");
}

View File

@@ -21,6 +21,7 @@ const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Event = @import("event.zig").Event; const Event = @import("event.zig").Event;
const JsObject = @import("../env.zig").JsObject;
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain. // TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
@@ -54,8 +55,8 @@ pub const MouseEvent = struct {
pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent { pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent {
const opts = opts_ orelse MouseEventInit{}; const opts = opts_ orelse MouseEventInit{};
const mouse_event = try parser.mouseEventCreate(); var mouse_event = try parser.mouseEventCreate();
parser.eventSetInternalType(@ptrCast(mouse_event), .mouse_event); try parser.eventSetInternalType(@ptrCast(&mouse_event), .mouse_event);
try parser.mouseEventInit(mouse_event, event_type, .{ try parser.mouseEventInit(mouse_event, event_type, .{
.x = opts.clientX, .x = opts.clientX,
@@ -67,10 +68,6 @@ pub const MouseEvent = struct {
.button = @intFromEnum(opts.button), .button = @intFromEnum(opts.button),
}); });
if (!std.mem.eql(u8, event_type, "click")) {
log.warn(.browser, "unsupported mouse event", .{ .event = event_type });
}
return mouse_event; return mouse_event;
} }

View File

@@ -1,225 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const URL = @import("../../url.zig").URL;
const Page = @import("../page.zig").Page;
const iterator = @import("../iterator/iterator.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
const Headers = @This();
// Case-Insensitive String HashMap.
// This allows us to avoid having to allocate lowercase keys all the time.
const HeaderHashMap = std.HashMapUnmanaged([]const u8, []const u8, struct {
pub fn hash(_: @This(), s: []const u8) u64 {
var buf: [64]u8 = undefined;
var hasher = std.hash.Wyhash.init(s.len);
var key = s;
while (key.len >= 64) {
const lower = std.ascii.lowerString(buf[0..], key[0..64]);
hasher.update(lower);
key = key[64..];
}
if (key.len > 0) {
const lower = std.ascii.lowerString(buf[0..key.len], key);
hasher.update(lower);
}
return hasher.final();
}
pub fn eql(_: @This(), a: []const u8, b: []const u8) bool {
return std.ascii.eqlIgnoreCase(a, b);
}
}, 80);
headers: HeaderHashMap = .empty,
// They can either be:
//
// 1. An array of string pairs.
// 2. An object with string keys to string values.
// 3. Another Headers object.
pub const HeadersInit = union(enum) {
// List of Pairs of []const u8
strings: []const [2][]const u8,
// Headers
headers: *Headers,
// Mappings
object: js.Object,
};
pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
const arena = page.arena;
var headers: HeaderHashMap = .empty;
if (_init) |init| {
switch (init) {
.strings => |kvs| {
for (kvs) |pair| {
const key = try arena.dupe(u8, pair[0]);
const value = try arena.dupe(u8, pair[1]);
try headers.put(arena, key, value);
}
},
.headers => |hdrs| {
var iter = hdrs.headers.iterator();
while (iter.next()) |entry| {
try headers.put(arena, entry.key_ptr.*, entry.value_ptr.*);
}
},
.object => |obj| {
var iter = obj.nameIterator();
while (try iter.next()) |name_value| {
const name = try name_value.toString(arena);
const value = try obj.get(name);
const value_string = try value.toString(arena);
try headers.put(arena, name, value_string);
}
},
}
}
return .{
.headers = headers,
};
}
pub fn append(self: *Headers, name: []const u8, value: []const u8, allocator: std.mem.Allocator) !void {
const key = try allocator.dupe(u8, name);
const gop = try self.headers.getOrPut(allocator, key);
if (gop.found_existing) {
// If we found it, append the value.
const new_value = try std.fmt.allocPrint(allocator, "{s}, {s}", .{ gop.value_ptr.*, value });
gop.value_ptr.* = new_value;
} else {
// Otherwise, we should just put it in.
gop.value_ptr.* = try allocator.dupe(u8, value);
}
}
pub fn _append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
const arena = page.arena;
try self.append(name, value, arena);
}
pub fn _delete(self: *Headers, name: []const u8) void {
_ = self.headers.remove(name);
}
pub const HeadersEntryIterator = struct {
slot: [2][]const u8,
iter: HeaderHashMap.Iterator,
// TODO: these SHOULD be in lexigraphical order but I'm not sure how actually
// important that is.
pub fn _next(self: *HeadersEntryIterator) ?[2][]const u8 {
if (self.iter.next()) |entry| {
self.slot[0] = entry.key_ptr.*;
self.slot[1] = entry.value_ptr.*;
return self.slot;
} else {
return null;
}
}
};
pub fn _entries(self: *const Headers) HeadersEntryIterable {
return .{
.inner = .{
.slot = undefined,
.iter = self.headers.iterator(),
},
};
}
pub fn _forEach(self: *Headers, callback_fn: js.Function, this_arg: ?js.Object) !void {
var iter = self.headers.iterator();
const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn;
while (iter.next()) |entry| {
try cb.call(void, .{ entry.key_ptr.*, entry.value_ptr.*, self });
}
}
pub fn _get(self: *const Headers, name: []const u8) ?[]const u8 {
return self.headers.get(name);
}
pub fn _has(self: *const Headers, name: []const u8) bool {
return self.headers.contains(name);
}
pub const HeadersKeyIterator = struct {
iter: HeaderHashMap.KeyIterator,
pub fn _next(self: *HeadersKeyIterator) ?[]const u8 {
if (self.iter.next()) |key| {
return key.*;
} else {
return null;
}
}
};
pub fn _keys(self: *const Headers) HeadersKeyIterable {
return .{ .inner = .{ .iter = self.headers.keyIterator() } };
}
pub fn _set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
const arena = page.arena;
const key = try arena.dupe(u8, name);
const gop = try self.headers.getOrPut(arena, key);
gop.value_ptr.* = try arena.dupe(u8, value);
}
pub const HeadersValueIterator = struct {
iter: HeaderHashMap.ValueIterator,
pub fn _next(self: *HeadersValueIterator) ?[]const u8 {
if (self.iter.next()) |value| {
return value.*;
} else {
return null;
}
}
};
pub fn _values(self: *const Headers) HeadersValueIterable {
return .{ .inner = .{ .iter = self.headers.valueIterator() } };
}
pub const HeadersKeyIterable = iterator.Iterable(HeadersKeyIterator, "HeadersKeyIterator");
pub const HeadersValueIterable = iterator.Iterable(HeadersValueIterator, "HeadersValueIterator");
pub const HeadersEntryIterable = iterator.Iterable(HeadersEntryIterator, "HeadersEntryIterator");
const testing = @import("../../testing.zig");
test "fetch: Headers" {
try testing.htmlRunner("fetch/headers.html");
}

View File

@@ -1,283 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const URL = @import("../../url.zig").URL;
const Page = @import("../page.zig").Page;
const Response = @import("./Response.zig");
const Http = @import("../../http/Http.zig");
const ReadableStream = @import("../streams/ReadableStream.zig");
const Headers = @import("Headers.zig");
const HeadersInit = @import("Headers.zig").HeadersInit;
pub const RequestInput = union(enum) {
string: []const u8,
request: *Request,
};
pub const RequestCache = enum {
default,
@"no-store",
reload,
@"no-cache",
@"force-cache",
@"only-if-cached",
pub fn fromString(str: []const u8) ?RequestCache {
for (std.enums.values(RequestCache)) |cache| {
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
return cache;
}
} else {
return null;
}
}
pub fn toString(self: RequestCache) []const u8 {
return @tagName(self);
}
};
pub const RequestCredentials = enum {
omit,
@"same-origin",
include,
pub fn fromString(str: []const u8) ?RequestCredentials {
for (std.enums.values(RequestCredentials)) |cache| {
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
return cache;
}
} else {
return null;
}
}
pub fn toString(self: RequestCredentials) []const u8 {
return @tagName(self);
}
};
pub const RequestMode = enum {
cors,
@"no-cors",
@"same-origin",
navigate,
pub fn fromString(str: []const u8) ?RequestMode {
for (std.enums.values(RequestMode)) |cache| {
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
return cache;
}
} else {
return null;
}
}
pub fn toString(self: RequestMode) []const u8 {
return @tagName(self);
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
pub const RequestInit = struct {
body: ?[]const u8 = null,
cache: ?[]const u8 = null,
credentials: ?[]const u8 = null,
headers: ?HeadersInit = null,
integrity: ?[]const u8 = null,
method: ?[]const u8 = null,
mode: ?[]const u8 = null,
};
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
const Request = @This();
method: Http.Method,
url: [:0]const u8,
cache: RequestCache,
credentials: RequestCredentials,
// no-cors is default is not built with constructor.
mode: RequestMode = .@"no-cors",
headers: Headers,
body: ?[]const u8,
body_used: bool = false,
integrity: []const u8,
pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Request {
const arena = page.arena;
const options: RequestInit = _options orelse .{};
const url: [:0]const u8 = blk: switch (input) {
.string => |str| {
break :blk try URL.stitch(arena, str, page.url.raw, .{ .null_terminated = true });
},
.request => |req| {
break :blk try arena.dupeZ(u8, req.url);
},
};
const cache = (if (options.cache) |cache| RequestCache.fromString(cache) else null) orelse RequestCache.default;
const credentials = (if (options.credentials) |creds| RequestCredentials.fromString(creds) else null) orelse RequestCredentials.@"same-origin";
const integrity = if (options.integrity) |integ| try arena.dupe(u8, integ) else "";
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
const mode = (if (options.mode) |mode| RequestMode.fromString(mode) else null) orelse RequestMode.cors;
const method: Http.Method = blk: {
if (options.method) |given_method| {
for (std.enums.values(Http.Method)) |method| {
if (std.ascii.eqlIgnoreCase(given_method, @tagName(method))) {
break :blk method;
}
} else {
return error.TypeError;
}
} else {
break :blk Http.Method.GET;
}
};
// Can't have a body on .GET or .HEAD.
const body: ?[]const u8 = blk: {
if (method == .GET or method == .HEAD) {
break :blk null;
} else break :blk if (options.body) |body| try arena.dupe(u8, body) else null;
};
return .{
.method = method,
.url = url,
.cache = cache,
.credentials = credentials,
.mode = mode,
.headers = headers,
.body = body,
.integrity = integrity,
};
}
pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream {
if (self.body) |body| {
const stream = try ReadableStream.constructor(null, null, page);
try stream.queue.append(page.arena, .{ .string = body });
return stream;
} else return null;
}
pub fn get_bodyUsed(self: *const Request) bool {
return self.body_used;
}
pub fn get_cache(self: *const Request) RequestCache {
return self.cache;
}
pub fn get_credentials(self: *const Request) RequestCredentials {
return self.credentials;
}
pub fn get_headers(self: *Request) *Headers {
return &self.headers;
}
pub fn get_integrity(self: *const Request) []const u8 {
return self.integrity;
}
// TODO: If we ever support the Navigation API, we need isHistoryNavigation
// https://developer.mozilla.org/en-US/docs/Web/API/Request/isHistoryNavigation
pub fn get_method(self: *const Request) []const u8 {
return @tagName(self.method);
}
pub fn get_mode(self: *const Request) RequestMode {
return self.mode;
}
pub fn get_url(self: *const Request) []const u8 {
return self.url;
}
pub fn _clone(self: *Request) !Request {
// Not allowed to clone if the body was used.
if (self.body_used) {
return error.TypeError;
}
// OK to just return the same fields BECAUSE
// all of these fields are read-only and can't be modified.
return Request{
.body = self.body,
.body_used = self.body_used,
.cache = self.cache,
.credentials = self.credentials,
.headers = self.headers,
.method = self.method,
.integrity = self.integrity,
.url = self.url,
};
}
pub fn _bytes(self: *Response, page: *Page) !js.Promise {
if (self.body_used) {
return error.TypeError;
}
self.body_used = true;
return page.js.resolvePromise(self.body);
}
pub fn _json(self: *Response, page: *Page) !js.Promise {
if (self.body_used) {
return error.TypeError;
}
self.body_used = true;
if (self.body) |body| {
const p = std.json.parseFromSliceLeaky(
std.json.Value,
page.call_arena,
body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
return error.SyntaxError;
};
return page.js.resolvePromise(p);
}
return page.js.resolvePromise(null);
}
pub fn _text(self: *Response, page: *Page) !js.Promise {
if (self.body_used) {
return error.TypeError;
}
self.body_used = true;
return page.js.resolvePromise(self.body);
}
const testing = @import("../../testing.zig");
test "fetch: Request" {
try testing.htmlRunner("fetch/request.html");
}

View File

@@ -1,209 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const HttpClient = @import("../../http/Client.zig");
const Http = @import("../../http/Http.zig");
const URL = @import("../../url.zig").URL;
const ReadableStream = @import("../streams/ReadableStream.zig");
const Headers = @import("Headers.zig");
const HeadersInit = @import("Headers.zig").HeadersInit;
const Mime = @import("../mime.zig").Mime;
const Page = @import("../page.zig").Page;
// https://developer.mozilla.org/en-US/docs/Web/API/Response
const Response = @This();
status: u16 = 200,
status_text: []const u8 = "",
headers: Headers,
mime: ?Mime = null,
url: []const u8 = "",
body: ?[]const u8 = null,
body_used: bool = false,
redirected: bool = false,
type: ResponseType = .basic,
const ResponseBody = union(enum) {
string: []const u8,
};
const ResponseOptions = struct {
status: u16 = 200,
statusText: ?[]const u8 = null,
headers: ?HeadersInit = null,
};
pub const ResponseType = enum {
basic,
cors,
@"error",
@"opaque",
opaqueredirect,
pub fn fromString(str: []const u8) ?ResponseType {
for (std.enums.values(ResponseType)) |cache| {
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
return cache;
}
} else {
return null;
}
}
pub fn toString(self: ResponseType) []const u8 {
return @tagName(self);
}
};
pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Page) !Response {
const arena = page.arena;
const options: ResponseOptions = _options orelse .{};
const body = blk: {
if (_input) |input| {
switch (input) {
.string => |str| {
break :blk try arena.dupe(u8, str);
},
}
} else {
break :blk null;
}
};
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
const status_text = if (options.statusText) |st| try arena.dupe(u8, st) else "";
return .{
.body = body,
.headers = headers,
.status = options.status,
.status_text = status_text,
};
}
pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
const stream = try ReadableStream.constructor(null, null, page);
if (self.body) |body| {
try stream.queue.append(page.arena, .{ .string = body });
}
return stream;
}
pub fn get_bodyUsed(self: *const Response) bool {
return self.body_used;
}
pub fn get_headers(self: *Response) *Headers {
return &self.headers;
}
pub fn get_ok(self: *const Response) bool {
return self.status >= 200 and self.status <= 299;
}
pub fn get_redirected(self: *const Response) bool {
return self.redirected;
}
pub fn get_status(self: *const Response) u16 {
return self.status;
}
pub fn get_statusText(self: *const Response) []const u8 {
return self.status_text;
}
pub fn get_type(self: *const Response) ResponseType {
return self.type;
}
pub fn get_url(self: *const Response) []const u8 {
return self.url;
}
pub fn _clone(self: *const Response) !Response {
if (self.body_used) {
return error.TypeError;
}
// OK to just return the same fields BECAUSE
// all of these fields are read-only and can't be modified.
return Response{
.body = self.body,
.body_used = self.body_used,
.mime = self.mime,
.headers = self.headers,
.redirected = self.redirected,
.status = self.status,
.url = self.url,
.type = self.type,
};
}
pub fn _bytes(self: *Response, page: *Page) !js.Promise {
if (self.body_used) {
return error.TypeError;
}
self.body_used = true;
return page.js.resolvePromise(self.body);
}
pub fn _json(self: *Response, page: *Page) !js.Promise {
if (self.body_used) {
return error.TypeError;
}
if (self.body) |body| {
self.body_used = true;
const p = std.json.parseFromSliceLeaky(
std.json.Value,
page.call_arena,
body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
return error.SyntaxError;
};
return page.js.resolvePromise(p);
}
return page.js.resolvePromise(null);
}
pub fn _text(self: *Response, page: *Page) !js.Promise {
if (self.body_used) {
return error.TypeError;
}
self.body_used = true;
return page.js.resolvePromise(self.body);
}
const testing = @import("../../testing.zig");
test "fetch: Response" {
try testing.htmlRunner("fetch/response.html");
}

View File

@@ -1,243 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const Http = @import("../../http/Http.zig");
const HttpClient = @import("../../http/Client.zig");
const Mime = @import("../mime.zig").Mime;
const Headers = @import("Headers.zig");
const RequestInput = @import("Request.zig").RequestInput;
const RequestInit = @import("Request.zig").RequestInit;
const Request = @import("Request.zig");
const Response = @import("Response.zig");
pub const Interfaces = .{
@import("Headers.zig"),
@import("Headers.zig").HeadersEntryIterable,
@import("Headers.zig").HeadersKeyIterable,
@import("Headers.zig").HeadersValueIterable,
@import("Request.zig"),
@import("Response.zig"),
};
pub const FetchContext = struct {
page: *Page,
arena: std.mem.Allocator,
promise_resolver: js.PersistentPromiseResolver,
method: Http.Method,
url: []const u8,
body: std.ArrayListUnmanaged(u8) = .empty,
headers: std.ArrayListUnmanaged([]const u8) = .empty,
status: u16 = 0,
mime: ?Mime = null,
mode: Request.RequestMode,
transfer: ?*HttpClient.Transfer = null,
/// This effectively takes ownership of the FetchContext.
///
/// We just return the underlying slices used for `headers`
/// and for `body` here to avoid an allocation.
pub fn toResponse(self: *const FetchContext) !Response {
var headers: Headers = .{};
// seems to be the highest priority
const same_origin = try self.page.isSameOrigin(self.url);
// If the mode is "no-cors", we need to return this opaque/stripped Response.
// https://developer.mozilla.org/en-US/docs/Web/API/Response/type
if (!same_origin and self.mode == .@"no-cors") {
return Response{
.status = 0,
.headers = headers,
.mime = self.mime,
.body = null,
.url = self.url,
.type = .@"opaque",
};
}
// convert into Headers
for (self.headers.items) |hdr| {
var iter = std.mem.splitScalar(u8, hdr, ':');
const name = iter.next() orelse "";
const value = iter.next() orelse "";
try headers.append(name, value, self.arena);
}
const resp_type: Response.ResponseType = blk: {
if (same_origin or std.mem.startsWith(u8, self.url, "data:")) {
break :blk .basic;
}
break :blk switch (self.mode) {
.cors => .cors,
.@"same-origin", .navigate => .basic,
.@"no-cors" => unreachable,
};
};
return Response{
.status = self.status,
.headers = headers,
.mime = self.mime,
.body = self.body.items,
.url = self.url,
.type = resp_type,
};
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !js.Promise {
const arena = page.arena;
const req = try Request.constructor(input, options, page);
var headers = try page.http_client.newHeaders();
// Copy our headers into the HTTP headers.
var header_iter = req.headers.headers.iterator();
while (header_iter.next()) |entry| {
const combined = try std.fmt.allocPrintSentinel(
page.arena,
"{s}: {s}",
.{ entry.key_ptr.*, entry.value_ptr.* },
0,
);
try headers.add(combined.ptr);
}
try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers);
const resolver = try page.js.createPromiseResolver(.page);
const fetch_ctx = try arena.create(FetchContext);
fetch_ctx.* = .{
.page = page,
.arena = arena,
.promise_resolver = resolver,
.method = req.method,
.url = req.url,
.mode = req.mode,
};
try page.http_client.request(.{
.ctx = @ptrCast(fetch_ctx),
.url = req.url,
.method = req.method,
.headers = headers,
.body = req.body,
.cookie_jar = page.cookie_jar,
.resource_type = .fetch,
.start_callback = struct {
fn startCallback(transfer: *HttpClient.Transfer) !void {
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
log.debug(.fetch, "request start", .{ .method = self.method, .url = self.url, .source = "fetch" });
self.transfer = transfer;
}
}.startCallback,
.header_callback = struct {
fn headerCallback(transfer: *HttpClient.Transfer) !void {
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
const header = &transfer.response_header.?;
log.debug(.fetch, "request header", .{
.source = "fetch",
.method = self.method,
.url = self.url,
.status = header.status,
});
if (header.contentType()) |ct| {
self.mime = Mime.parse(ct) catch {
return error.MimeParsing;
};
}
if (transfer.getContentLength()) |cl| {
try self.body.ensureTotalCapacity(self.arena, cl);
}
var it = transfer.responseHeaderIterator();
while (it.next()) |hdr| {
const joined = try std.fmt.allocPrint(self.arena, "{s}: {s}", .{ hdr.name, hdr.value });
try self.headers.append(self.arena, joined);
}
self.status = header.status;
}
}.headerCallback,
.data_callback = struct {
fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
try self.body.appendSlice(self.arena, data);
}
}.dataCallback,
.done_callback = struct {
fn doneCallback(ctx: *anyopaque) !void {
const self: *FetchContext = @ptrCast(@alignCast(ctx));
self.transfer = null;
log.info(.fetch, "request complete", .{
.source = "fetch",
.method = self.method,
.url = self.url,
.status = self.status,
});
const response = try self.toResponse();
try self.promise_resolver.resolve(response);
}
}.doneCallback,
.error_callback = struct {
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *FetchContext = @ptrCast(@alignCast(ctx));
self.transfer = null;
log.err(.fetch, "error", .{
.url = self.url,
.err = err,
.source = "fetch error",
});
// We throw an Abort error when the page is getting closed so,
// in this case, we don't need to reject the promise.
if (err != error.Abort) {
self.promise_resolver.reject(@errorName(err)) catch unreachable;
}
}
}.errorCallback,
});
return resolver.promise();
}
const testing = @import("../../testing.zig");
test "fetch: fetch" {
try testing.htmlRunner("fetch/fetch.html");
}

View File

@@ -17,9 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
@@ -91,7 +91,7 @@ pub const AbortSignal = struct {
self.reason = reason_ orelse DEFAULT_REASON; self.reason = reason_ orelse DEFAULT_REASON;
const abort_event = try parser.eventCreate(); const abort_event = try parser.eventCreate();
parser.eventSetInternalType(abort_event, .abort_signal); try parser.eventSetInternalType(abort_event, .abort_signal);
defer parser.eventDestroy(abort_event); defer parser.eventDestroy(abort_event);
try parser.eventInit(abort_event, "abort", .{}); try parser.eventInit(abort_event, "abort", .{});
@@ -113,12 +113,12 @@ pub const AbortSignal = struct {
} }
const ThrowIfAborted = union(enum) { const ThrowIfAborted = union(enum) {
exception: js.Exception, exception: Env.Exception,
undefined: void, undefined: void,
}; };
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted { pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
if (self.aborted) { if (self.aborted) {
const ex = page.js.throw(self.reason orelse DEFAULT_REASON); const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON);
return .{ .exception = ex }; return .{ .exception = ex };
} }
return .{ .undefined = {} }; return .{ .undefined = {} };
@@ -138,6 +138,44 @@ const TimeoutCallback = struct {
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: HTML.AbortController" { test "Browser.HTML.AbortController" {
try testing.htmlRunner("html/abort_controller.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "var called = 0", null },
.{ "var a1 = new AbortController()", null },
.{ "var s1 = a1.signal", null },
.{ "s1.throwIfAborted()", "undefined" },
.{ "s1.reason", "undefined" },
.{ "var target;", null },
.{
\\ s1.addEventListener('abort', (e) => {
\\ called += 1;
\\ target = e.target;
\\
\\ });
,
null,
},
.{ "a1.abort()", null },
.{ "s1.aborted", "true" },
.{ "target == s1", "true" },
.{ "s1.reason", "AbortError" },
.{ "called", "1" },
}, .{});
try runner.testCases(&.{
.{ "var s2 = AbortSignal.abort('over 9000')", null },
.{ "s2.aborted", "true" },
.{ "s2.reason", "over 9000" },
.{ "AbortSignal.abort().reason", "AbortError" },
}, .{});
try runner.testCases(&.{
.{ "var s3 = AbortSignal.timeout(10)", null },
.{ "s3.aborted", "true" },
.{ "s3.reason", "TimeoutError" },
.{ "try { s3.throwIfAborted() } catch (e) { e }", "Error: TimeoutError" },
}, .{});
} }

View File

@@ -17,8 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -27,7 +26,7 @@ const DataSet = @This();
element: *parser.Element, element: *parser.Element,
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !js.UndefinedOr([]const u8) { pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !Env.UndefinedOr([]const u8) {
const normalized_name = try normalize(page.call_arena, name); const normalized_name = try normalize(page.call_arena, name);
if (try parser.elementGetAttribute(self.element, normalized_name)) |value| { if (try parser.elementGetAttribute(self.element, normalized_name)) |value| {
return .{ .value = value }; return .{ .value = value };
@@ -77,6 +76,22 @@ fn normalize(allocator: Allocator, name: []const u8) ![]const u8 {
} }
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: HTML.DataSet" { test "Browser.HTML.DataSet" {
try testing.htmlRunner("html/dataset.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
defer runner.deinit();
try runner.testCases(&.{
.{ "let el1 = document.createElement('div')", null },
.{ "el1.dataset.x", "undefined" },
.{ "el1.dataset.x = '123'", "123" },
.{ "delete el1.dataset.x", "true" },
.{ "el1.dataset.x", "undefined" },
.{ "delete el1.dataset.other", "true" }, // yes, this is right
.{ "let ds1 = el1.dataset", null },
.{ "ds1.helloWorld = 'yes'", null },
.{ "el1.getAttribute('data-hello-world')", "yes" },
.{ "el1.setAttribute('data-this-will-work', 'positive')", null },
.{ "ds1.thisWillWork", "positive" },
}, .{});
} }

View File

@@ -1,183 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const Window = @import("window.zig").Window;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
const History = @This();
const ScrollRestorationMode = enum {
pub const ENUM_JS_USE_TAG = true;
auto,
manual,
};
scroll_restoration: ScrollRestorationMode = .auto,
pub fn get_length(_: *History, page: *Page) u32 {
return @intCast(page.session.navigation.entries.items.len);
}
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
return self.scroll_restoration;
}
pub fn set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
self.scroll_restoration = mode;
}
pub fn get_state(_: *History, page: *Page) !?js.Value {
if (page.session.navigation.currentEntry().state) |state| {
const value = try js.Value.fromJson(page.js, state);
return value;
} else {
return null;
}
}
pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
const json = state.toJson(arena) catch return error.DataClone;
_ = try page.session.navigation.pushEntry(url, json, page, true);
}
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
const entry = page.session.navigation.currentEntry();
const json = try state.toJson(arena);
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
entry.state = json;
entry.url = url;
}
pub fn go(_: *const History, delta: i32, page: *Page) !void {
// 0 behaves the same as no argument, both reloading the page.
const current = page.session.navigation.index;
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
if (index_s < 0 or index_s > page.session.navigation.entries.items.len - 1) {
return;
}
const index = @as(usize, @intCast(index_s));
const entry = page.session.navigation.entries.items[index];
if (entry.url) |url| {
if (try page.isSameOrigin(url)) {
PopStateEvent.dispatch(entry.state, page);
}
}
_ = try page.session.navigation.navigate(entry.url, .{ .traverse = index }, page);
}
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
try self.go(_delta orelse 0, page);
}
pub fn _back(self: *History, page: *Page) !void {
try self.go(-1, page);
}
pub fn _forward(self: *History, page: *Page) !void {
try self.go(1, page);
}
const parser = @import("../netsurf.zig");
const Event = @import("../events/event.zig").Event;
pub const PopStateEvent = struct {
pub const prototype = *Event;
pub const union_make_copy = true;
pub const EventInit = struct {
state: ?[]const u8 = null,
};
proto: parser.Event,
state: ?[]const u8,
pub fn constructor(event_type: []const u8, opts: ?EventInit) !PopStateEvent {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .pop_state);
const o = opts orelse EventInit{};
return .{
.proto = event.*,
.state = o.state,
};
}
// `hasUAVisualTransition` is not implemented. It isn't baseline so this is okay.
pub fn get_state(self: *const PopStateEvent, page: *Page) !?js.Value {
if (self.state) |state| {
const value = try js.Value.fromJson(page.js, state);
return value;
} else {
return null;
}
}
pub fn dispatch(state: ?[]const u8, page: *Page) void {
log.debug(.script_event, "dispatch popstate event", .{
.type = "popstate",
.source = "history",
});
var evt = PopStateEvent.constructor("popstate", .{ .state = state }) catch |err| {
log.err(.app, "event constructor error", .{
.err = err,
.type = "popstate",
.source = "history",
});
return;
};
_ = parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, &page.window),
&evt.proto,
) catch |err| {
log.err(.app, "dispatch popstate event error", .{
.err = err,
.type = "popstate",
.source = "history",
});
};
}
};
const testing = @import("../../testing.zig");
test "Browser: HTML.History" {
try testing.htmlRunner("html/history/history.html");
try testing.htmlRunner("html/history/history2.html");
}

View File

@@ -42,12 +42,12 @@ pub const HTMLDocument = struct {
// JS funcs // JS funcs
// -------- // --------
pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 { pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
// libdom's document_html get_domain always returns null, this is // libdom's document_html get_domain always returns null, this is
// the way MDN recommends getting the domain anyways, since document.domain // the way MDN recommends getting the domain anyways, since document.domain
// is deprecated. // is deprecated.
const location = try parser.documentHTMLGetLocation(Location, self) orelse return ""; const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
return location.get_host(); return location.get_host(page);
} }
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 { pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
@@ -115,69 +115,67 @@ pub const HTMLDocument = struct {
} }
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList { pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList {
const arena = page.arena;
var list: NodeList = .{}; var list: NodeList = .{};
if (name.len == 0) { if (name.len == 0) return list;
return list;
}
const root = parser.documentHTMLToNode(self); const root = parser.documentHTMLToNode(self);
var c = try collection.HTMLCollectionByName(root, name, .{ var c = try collection.HTMLCollectionByName(arena, root, name, .{
.include_root = false, .include_root = false,
}); });
const ln = try c.get_length(); const ln = try c.get_length();
try list.ensureTotalCapacity(page.arena, ln);
var i: u32 = 0; var i: u32 = 0;
while (i < ln) : (i += 1) { while (i < ln) {
const n = try c.item(i) orelse break; const n = try c.item(i) orelse break;
list.appendAssumeCapacity(n); try list.append(arena, n);
i += 1;
} }
return list; return list;
} }
pub fn get_images(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "img", .{ return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_embeds(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "embed", .{ return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_plugins(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return get_embeds(self); return get_embeds(self, page);
} }
pub fn get_forms(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "form", .{ return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_scripts(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "script", .{ return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_applets(_: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection {
return collection.HTMLCollectionEmpty(); return try collection.HTMLCollectionEmpty();
} }
pub fn get_links(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_links(self: *parser.DocumentHTML) !collection.HTMLCollection {
return collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{ return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_anchors(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection {
return collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{ return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{
.include_root = false, .include_root = false,
}); });
} }
@@ -195,7 +193,7 @@ pub const HTMLDocument = struct {
} }
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void { pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null }); return page.navigateFromWebAPI(url, .{ .reason = .script });
} }
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 { pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
@@ -316,7 +314,116 @@ pub const HTMLDocument = struct {
} }
}; };
// Tests
// -----
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: HTML.Document" {
try testing.htmlRunner("html/document.html"); test "Browser.HTML.Document" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "document.__proto__.constructor.name", "HTMLDocument" },
.{ "document.__proto__.__proto__.constructor.name", "Document" },
.{ "document.body.localName == 'body'", "true" },
}, .{});
try runner.testCases(&.{
.{ "document.domain", "lightpanda.io" },
.{ "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" },
}, .{});
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" },
.{ "document.cookie = 'IgnoreMy=Ghost; HttpOnly'", null }, // "" should be returned, but the framework overrules it atm
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
}, .{});
try runner.testCases(&.{
.{ "document.elementFromPoint(0.5, 0.5)", "null" }, // Return null since we only return element s when they have previously been localized
.{ "document.elementsFromPoint(0.5, 0.5)", "" },
.{
\\ let div1 = document.createElement('div');
\\ document.body.appendChild(div1);
\\ div1.getClientRects();
,
null,
},
.{ "document.elementFromPoint(0.5, 0.5)", "[object HTMLDivElement]" },
.{ "let elems = document.elementsFromPoint(0.5, 0.5)", null },
.{ "elems.length", "3" },
.{ "elems[0]", "[object HTMLDivElement]" },
.{ "elems[1]", "[object HTMLBodyElement]" },
.{ "elems[2]", "[object HTMLHtmlElement]" },
}, .{});
try runner.testCases(&.{
.{
\\ let a = document.createElement('a');
\\ a.href = "https://lightpanda.io";
\\ document.body.appendChild(a);
\\ a.getClientRects();
, // Note this will be placed after the div of previous test
null,
},
.{ "let a_again = document.elementFromPoint(1.5, 0.5)", null },
.{ "a_again", "[object HTMLAnchorElement]" },
.{ "a_again.href", "https://lightpanda.io" },
.{ "let a_agains = document.elementsFromPoint(1.5, 0.5)", null },
.{ "a_agains[0].href", "https://lightpanda.io" },
}, .{});
try runner.testCases(&.{
.{ "!document.all", "true" },
.{ "!!document.all", "false" },
.{ "document.all(5)", "[object HTMLParagraphElement]" },
.{ "document.all('content')", "[object HTMLDivElement]" },
}, .{});
try runner.testCases(&.{
.{ "document.defaultView.document == document", "true" },
}, .{});
try runner.testCases(&.{
.{ "document.readyState", "loading" },
}, .{});
try HTMLDocument.documentIsLoaded(runner.page.window.document, runner.page);
try runner.testCases(&.{
.{ "document.readyState", "interactive" },
}, .{});
try HTMLDocument.documentIsComplete(runner.page.window.document, runner.page);
try runner.testCases(&.{
.{ "document.readyState", "complete" },
}, .{});
} }

View File

@@ -18,9 +18,9 @@
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const generate = @import("../js/generate.zig"); const generate = @import("../../runtime/generate.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const urlStitch = @import("../../url.zig").URL.stitch; const urlStitch = @import("../../url.zig").URL.stitch;
@@ -133,14 +133,14 @@ pub const HTMLElement = struct {
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 { pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
const n = @as(*parser.Node, @ptrCast(e)); const n = @as(*parser.Node, @ptrCast(e));
return parser.nodeTextContent(n) orelse ""; return try parser.nodeTextContent(n) orelse "";
} }
pub fn set_innerText(e: *parser.ElementHTML, s: []const u8) !void { pub fn set_innerText(e: *parser.ElementHTML, s: []const u8) !void {
const n = @as(*parser.Node, @ptrCast(e)); const n = @as(*parser.Node, @ptrCast(e));
// create text node. // create text node.
const doc = parser.nodeOwnerDocument(n) orelse return error.NoDocument; const doc = try parser.nodeOwnerDocument(n) orelse return error.NoDocument;
const t = try parser.documentCreateTextNode(doc, s); const t = try parser.documentCreateTextNode(doc, s);
// remove existing children. // remove existing children.
@@ -167,12 +167,12 @@ pub const HTMLElement = struct {
focusVisible: bool, focusVisible: bool,
}; };
pub fn _focus(e: *parser.ElementHTML, _: ?FocusOpts, page: *Page) !void { pub fn _focus(e: *parser.ElementHTML, _: ?FocusOpts, page: *Page) !void {
if (!page.isNodeAttached(@ptrCast(e))) { if (!try page.isNodeAttached(@ptrCast(e))) {
return; return;
} }
const Document = @import("../dom/document.zig").Document; const Document = @import("../dom/document.zig").Document;
const root_node = parser.nodeGetRootNode(@ptrCast(e)); const root_node = try parser.nodeGetRootNode(@ptrCast(e));
try Document.setFocus(@ptrCast(root_node), e, page); try Document.setFocus(@ptrCast(root_node), e, page);
} }
}; };
@@ -218,40 +218,40 @@ pub const HTMLAnchorElement = struct {
} }
pub fn get_href(self: *parser.Anchor) ![]const u8 { pub fn get_href(self: *parser.Anchor) ![]const u8 {
return parser.anchorGetHref(self); return try parser.anchorGetHref(self);
} }
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void { pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void {
const full = try urlStitch(page.call_arena, href, page.url.raw, .{}); const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
return parser.anchorSetHref(self, full); return try parser.anchorSetHref(self, full);
} }
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 { pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
return parser.anchorGetHrefLang(self); return try parser.anchorGetHrefLang(self);
} }
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void { pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
return parser.anchorSetHrefLang(self, href); return try parser.anchorSetHrefLang(self, href);
} }
pub fn get_type(self: *parser.Anchor) ![]const u8 { pub fn get_type(self: *parser.Anchor) ![]const u8 {
return parser.anchorGetType(self); return try parser.anchorGetType(self);
} }
pub fn set_type(self: *parser.Anchor, t: []const u8) !void { pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
return parser.anchorSetType(self, t); return try parser.anchorSetType(self, t);
} }
pub fn get_rel(self: *parser.Anchor) ![]const u8 { pub fn get_rel(self: *parser.Anchor) ![]const u8 {
return parser.anchorGetRel(self); return try parser.anchorGetRel(self);
} }
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void { pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
return parser.anchorSetRel(self, t); return try parser.anchorSetRel(self, t);
} }
pub fn get_text(self: *parser.Anchor) !?[]const u8 { pub fn get_text(self: *parser.Anchor) !?[]const u8 {
return parser.nodeTextContent(parser.anchorToNode(self)); return try parser.nodeTextContent(parser.anchorToNode(self));
} }
pub fn set_text(self: *parser.Anchor, v: []const u8) !void { pub fn set_text(self: *parser.Anchor, v: []const u8) !void {
@@ -269,175 +269,182 @@ pub const HTMLAnchorElement = struct {
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| { if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
} }
return error.NotProvided; return .empty;
} }
// TODO return a disposable string
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = try url(self, page);
defer u.destructor(); return try u.get_origin(page);
return u.get_origin(page);
} }
// TODO return a disposable string
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = try url(self, page);
defer u.destructor(); return u.get_protocol(page);
return page.call_arena.dupe(u8, u.get_protocol());
} }
pub fn set_protocol(self: *parser.Anchor, protocol: []const u8, page: *Page) !void { pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
defer u.destructor();
try u.set_protocol(protocol);
const href = try u._toString(page); u.uri.scheme = v;
return parser.anchorSetHref(self, href); const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return ""; var u = try url(self, page);
defer u.destructor(); return try u.get_host(page);
return page.call_arena.dupe(u8, u.get_host());
} }
pub fn set_host(self: *parser.Anchor, host: []const u8, page: *Page) !void { pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void {
var u = try url(self, page); // search : separator
defer u.destructor(); var p: ?u16 = null;
try u.set_host(host); var h: []const u8 = undefined;
for (v, 0..) |c, i| {
if (c == ':') {
h = v[0..i];
p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
break;
}
}
const href = try u._toString(page); const arena = page.arena;
return parser.anchorSetHref(self, href); var u = try url(self, page);
if (p) |pp| {
u.uri.host = .{ .raw = h };
u.uri.port = pp;
} else {
u.uri.host = .{ .raw = v };
u.uri.port = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
} }
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_hostname());
}
pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
var u = try url(self, page); var u = try url(self, page);
defer u.destructor(); return u.get_hostname();
try u.set_hostname(hostname);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
u.uri.host = .{ .raw = v };
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return ""; var u = try url(self, page);
defer u.destructor(); return try u.get_port(page);
return page.call_arena.dupe(u8, u.get_port());
} }
pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void { pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
defer u.destructor();
if (maybe_port) |port| { if (v != null and v.?.len > 0) {
try u.set_port(port); u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
} else { } else {
u.clearPort(); u.uri.port = null;
} }
const href = try u._toString(page); const href = try u.toString(arena);
return parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
const username = u.get_username();
if (username.len == 0) {
return "";
}
return page.call_arena.dupe(u8, username);
}
pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
var u = try url(self, page); var u = try url(self, page);
defer u.destructor(); return u.get_username();
const username = if (maybe_username) |username| username else "";
try u.set_username(username);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
if (v) |vv| {
u.uri.user = .{ .raw = vv };
} else {
u.uri.user = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_password());
}
pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
var u = try url(self, page); var u = try url(self, page);
defer u.destructor(); return try page.arena.dupe(u8, u.get_password());
const password = if (maybe_password) |password| password else "";
try u.set_password(password);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
if (v) |vv| {
u.uri.password = .{ .raw = vv };
} else {
u.uri.password = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return ""; var u = try url(self, page);
defer u.destructor(); return u.get_pathname();
return page.call_arena.dupe(u8, u.get_pathname());
} }
pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void { pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
defer u.destructor(); u.uri.path = .{ .raw = v };
const href = try u.toString(arena);
try u.set_pathname(pathname); try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return ""; var u = try url(self, page);
defer u.destructor(); return try u.get_search(page);
// This allocates in page arena so no need to dupe.
return u.get_search(page);
} }
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
var u = try url(self, page); var u = try url(self, page);
defer u.destructor();
try u.set_search(v, page); try u.set_search(v, page);
const href = try u._toString(page); const href = try u.toString(page.call_arena);
return parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_hash());
}
pub fn set_hash(self: *parser.Anchor, maybe_hash: ?[]const u8, page: *Page) !void {
var u = try url(self, page); var u = try url(self, page);
defer u.destructor(); return try u.get_hash(page);
if (maybe_hash) |hash| {
try u.set_hash(hash);
} else {
u.clearHash();
} }
const href = try u._toString(page); pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
return parser.anchorSetHref(self, href); const arena = page.arena;
var u = try url(self, page);
if (v) |vv| {
u.uri.fragment = .{ .raw = vv };
} else {
u.uri.fragment = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
} }
}; };
@@ -725,9 +732,6 @@ pub const HTMLInputElement = struct {
pub fn set_value(self: *parser.Input, value: []const u8) !void { pub fn set_value(self: *parser.Input, value: []const u8) !void {
try parser.inputSetValue(self, value); try parser.inputSetValue(self, value);
} }
pub fn _select(_: *parser.Input) void {
log.debug(.web_api, "not implemented", .{ .feature = "HTMLInputElement select" });
}
}; };
pub const HTMLLIElement = struct { pub const HTMLLIElement = struct {
@@ -753,21 +757,13 @@ pub const HTMLLinkElement = struct {
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const subtype = .node; pub const subtype = .node;
pub fn get_rel(self: *parser.Link) ![]const u8 {
return parser.linkGetRel(self);
}
pub fn set_rel(self: *parser.Link, rel: []const u8) !void {
return parser.linkSetRel(self, rel);
}
pub fn get_href(self: *parser.Link) ![]const u8 { pub fn get_href(self: *parser.Link) ![]const u8 {
return parser.linkGetHref(self); return try parser.linkGetHref(self);
} }
pub fn set_href(self: *parser.Link, href: []const u8, page: *const Page) !void { pub fn set_href(self: *parser.Link, href: []const u8, page: *const Page) !void {
const full = try urlStitch(page.call_arena, href, page.url.raw, .{}); const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
return parser.linkSetHref(self, full); return try parser.linkSetHref(self, full);
} }
}; };
@@ -883,7 +879,7 @@ pub const HTMLScriptElement = struct {
// s.src = '...'; // s.src = '...';
// This should load the script. // This should load the script.
// addFromElement protects against double execution. // addFromElement protects against double execution.
try page.script_manager.addFromElement(@ptrCast(@alignCast(self)), "dynamic"); try page.script_manager.addFromElement(@ptrCast(@alignCast(self)));
} }
} }
@@ -981,37 +977,22 @@ pub const HTMLScriptElement = struct {
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule"); return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule");
} }
pub fn get_nonce(self: *parser.Script) !?[]const u8 { pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function {
return try parser.elementGetAttribute(
parser.scriptToElt(self),
"nonce",
) orelse "";
}
pub fn set_nonce(self: *parser.Script, v: []const u8) !void {
try parser.elementSetAttribute(
parser.scriptToElt(self),
"nonce",
v,
);
}
pub fn get_onload(self: *parser.Script, page: *Page) !?js.Function {
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null; const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
return state.onload; return state.onload;
} }
pub fn set_onload(self: *parser.Script, function: ?js.Function, page: *Page) !void { pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.onload = function; state.onload = function;
} }
pub fn get_onerror(self: *parser.Script, page: *Page) !?js.Function { pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function {
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null; const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
return state.onerror; return state.onerror;
} }
pub fn set_onerror(self: *parser.Script, function: ?js.Function, page: *Page) !void { pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.onerror = function; state.onerror = function;
} }
@@ -1046,29 +1027,40 @@ pub const HTMLSlotElement = struct {
flatten: bool = false, flatten: bool = false,
}; };
pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion { pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
return findAssignedSlotNodes(self, opts_, false, page);
}
// This should return Union, instead of NodeUnion, but we want to re-use
// findAssignedSlotNodes. Returning NodeUnion is fine, as long as every element
// within is an Element. This could be more efficient
pub fn _assignedElements(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
return findAssignedSlotNodes(self, opts_, true, page);
}
fn findAssignedSlotNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, element_only: bool, page: *Page) ![]NodeUnion {
const opts = opts_ orelse AssignedNodesOpts{ .flatten = false }; const opts = opts_ orelse AssignedNodesOpts{ .flatten = false };
if (opts.flatten) { if (try findAssignedSlotNodes(self, opts, page)) |nodes| {
log.debug(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" }); return nodes;
}
if (!opts.flatten) {
return &.{};
} }
const node: *parser.Node = @ptrCast(@alignCast(self)); const node: *parser.Node = @ptrCast(@alignCast(self));
const nl = try parser.nodeGetChildNodes(node);
const len = try parser.nodeListLength(nl);
if (len == 0) {
return &.{};
}
var assigned = try page.call_arena.alloc(NodeUnion, len);
var i: usize = 0;
while (true) : (i += 1) {
const child = try parser.nodeListItem(nl, @intCast(i)) orelse break;
assigned[i] = try Node.toInterface(child);
}
return assigned[0..i];
}
fn findAssignedSlotNodes(self: *parser.Slot, opts: AssignedNodesOpts, page: *Page) !?[]NodeUnion {
if (opts.flatten) {
log.warn(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
}
// First we look for any explicitly assigned nodes (via the slot attribute)
{
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name"); const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
var root = parser.nodeGetRootNode(node); const node: *parser.Node = @ptrCast(@alignCast(self));
var root = try parser.nodeGetRootNode(node);
if (page.getNodeState(root)) |state| { if (page.getNodeState(root)) |state| {
if (state.shadow_root) |sr| { if (state.shadow_root) |sr| {
root = @ptrCast(@alignCast(sr.host)); root = @ptrCast(@alignCast(sr.host));
@@ -1080,8 +1072,8 @@ pub const HTMLSlotElement = struct {
var next: ?*parser.Node = null; var next: ?*parser.Node = null;
while (true) { while (true) {
next = try w.get_next(root, next) orelse break; next = try w.get_next(root, next) orelse break;
if (parser.nodeType(next.?) != .element) { if (try parser.nodeType(next.?) != .element) {
if (slot_name == null and !element_only) { if (slot_name == null) {
// default slot (with no name), takes everything // default slot (with no name), takes everything
try arr.append(page.call_arena, try Node.toInterface(next.?)); try arr.append(page.call_arena, try Node.toInterface(next.?));
} }
@@ -1096,34 +1088,7 @@ pub const HTMLSlotElement = struct {
continue; continue;
} }
} }
if (arr.items.len > 0) { return if (arr.items.len == 0) null else arr.items;
return arr.items;
}
if (!opts.flatten) {
return &.{};
}
}
// Since, we have no explicitly assigned nodes and flatten == false,
// we'll collect the children of the slot - the defaults.
{
const nl = try parser.nodeGetChildNodes(node);
const len = parser.nodeListLength(nl);
if (len == 0) {
return &.{};
}
var assigned = try page.call_arena.alloc(NodeUnion, len);
var i: usize = 0;
while (true) : (i += 1) {
const child = parser.nodeListItem(nl, @intCast(i)) orelse break;
if (!element_only or parser.nodeType(child) == .element) {
assigned[i] = try Node.toInterface(child);
}
}
return assigned[0..i];
}
} }
fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool { fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool {
@@ -1320,39 +1285,336 @@ pub fn toInterfaceFromTag(comptime T: type, e: *parser.Element, tag: parser.Tag)
} }
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: HTML.Element" { test "Browser.HTML.Element" {
try testing.htmlRunner("html/element.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let link = document.getElementById('link')", "undefined" },
.{ "link.target", "" },
.{ "link.target = '_blank'", "_blank" },
.{ "link.target", "_blank" },
.{ "link.target = ''", "" },
.{ "link.href", "foo" },
.{ "link.href = 'https://lightpanda.io/'", "https://lightpanda.io/" },
.{ "link.href", "https://lightpanda.io/" },
.{ "link.origin", "https://lightpanda.io" },
.{ "link.host = 'lightpanda.io:443'", "lightpanda.io:443" },
.{ "link.host", "lightpanda.io:443" },
.{ "link.port", "443" },
.{ "link.hostname", "lightpanda.io" },
.{ "link.host = 'lightpanda.io'", "lightpanda.io" },
.{ "link.host", "lightpanda.io" },
.{ "link.port", "" },
.{ "link.hostname", "lightpanda.io" },
.{ "link.host", "lightpanda.io" },
.{ "link.hostname", "lightpanda.io" },
.{ "link.hostname = 'foo.bar'", "foo.bar" },
.{ "link.href", "https://foo.bar/" },
.{ "link.search", "" },
.{ "link.search = 'q=bar'", "q=bar" },
.{ "link.search", "?q=bar" },
.{ "link.href", "https://foo.bar/?q=bar" },
.{ "link.hash", "" },
.{ "link.hash = 'frag'", "frag" },
.{ "link.hash", "#frag" },
.{ "link.href", "https://foo.bar/?q=bar#frag" },
.{ "link.port", "" },
.{ "link.port = '443'", "443" },
.{ "link.host", "foo.bar:443" },
.{ "link.hostname", "foo.bar" },
.{ "link.href", "https://foo.bar:443/?q=bar#frag" },
.{ "link.port = null", "null" },
.{ "link.href", "https://foo.bar/?q=bar#frag" },
.{ "link.href = 'foo'", "foo" },
.{ "link.type", "" },
.{ "link.type = 'text/html'", "text/html" },
.{ "link.type", "text/html" },
.{ "link.type = ''", "" },
.{ "link.text", "OK" },
.{ "link.text = 'foo'", "foo" },
.{ "link.text", "foo" },
.{ "link.text = 'OK'", "OK" },
}, .{});
try runner.testCases(&.{
.{ "let script = document.createElement('script')", "undefined" },
.{ "script.src = 'foo.bar'", "foo.bar" },
.{ "script.async = true", "true" },
.{ "script.async", "true" },
.{ "script.async = false", "false" },
.{ "script.async", "false" },
}, .{});
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" },
}, .{});
try runner.testCases(&.{
.{ "let click_count = 0;", "undefined" },
.{ "let clickCbk = function() { click_count++ }", "undefined" },
.{ "document.getElementById('content').addEventListener('click', clickCbk);", "undefined" },
.{ "document.getElementById('content').click()", "undefined" },
.{ "click_count", "1" },
}, .{});
try runner.testCases(&.{
.{ "let style = document.getElementById('content').style", "undefined" },
.{ "style.cssText = 'color: red; font-size: 12px; margin: 5px !important;'", "color: red; font-size: 12px; margin: 5px !important;" },
.{ "style.length", "3" },
.{ "style.setProperty('background-color', 'blue')", "undefined" },
.{ "style.getPropertyValue('background-color')", "blue" },
.{ "style.length", "4" },
}, .{});
// Image
try runner.testCases(&.{
// Testing constructors
.{ "(new Image).width", "0" },
.{ "(new Image).height", "0" },
.{ "(new Image(4)).width", "4" },
.{ "(new Image(4, 6)).height", "6" },
// Testing ulong property
.{ "let fruit = new Image", null },
.{ "fruit.width", "0" },
.{ "fruit.width = 5", "5" },
.{ "fruit.width", "5" },
.{ "fruit.width = '15'", "15" },
.{ "fruit.width", "15" },
.{ "fruit.width = 'apple'", "apple" },
.{ "fruit.width;", "0" },
// Testing string property
.{ "let lyric = new Image", null },
.{ "lyric.src", "" },
.{ "lyric.src = 'okay'", "okay" },
.{ "lyric.src", "okay" },
.{ "lyric.src = 15", "15" },
.{ "lyric.src", "15" },
}, .{});
try runner.testCases(&.{
.{ "let a = document.createElement('a');", null },
.{ "a.href", "" },
.{ "a.host", "" },
.{ "a.href = 'about'", null },
.{ "a.href", "https://lightpanda.io/opensource-browser/about" },
}, .{});
// detached node cannot be focused
try runner.testCases(&.{
.{ "const focused = document.activeElement", null },
.{ "document.createElement('a').focus()", null },
.{ "document.activeElement === focused", "true" },
}, .{});
try runner.testCases(&.{
.{ "let l2 = document.createElement('link');", null },
.{ "l2.href", "" },
.{ "l2.href = 'https://lightpanda.io/opensource-browser/15'", null },
.{ "l2.href", "https://lightpanda.io/opensource-browser/15" },
.{ "l2.href = '/over/9000'", null },
.{ "l2.href", "https://lightpanda.io/over/9000" },
}, .{});
} }
test "Browser: HTML.HtmlLinkElement" { test "Browser.HTML.Element.DataSet" {
try testing.htmlRunner("html/link.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=x data-power='over 9000' data-empty data-some-long-key=ok></div>" });
defer runner.deinit();
try runner.testCases(&.{ .{ "let div = document.getElementById('x')", null }, .{ "div.dataset.nope", "undefined" }, .{ "div.dataset.power", "over 9000" }, .{ "div.dataset.empty", "" }, .{ "div.dataset.someLongKey", "ok" }, .{ "delete div.dataset.power", "true" }, .{ "div.dataset.power", "undefined" } }, .{});
} }
test "Browser: HTML.HtmlImageElement" { test "Browser.HTML.HtmlInputElement.properties" {
try testing.htmlRunner("html/image.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io/noslashattheend" });
defer runner.deinit();
var alloc = std.heap.ArenaAllocator.init(runner.allocator);
defer alloc.deinit();
const arena = alloc.allocator();
try runner.testCases(&.{.{ "let elem_input = document.createElement('input')", null }}, .{});
try runner.testCases(&.{.{ "elem_input.form", "null" }}, .{}); // Initial value
// Valid input.form is tested separately :Browser.HTML.HtmlInputElement.propeties.form
try testProperty(arena, &runner, "elem_input.form", "null", &.{.{ .input = "'foo'" }}); // Invalid
try runner.testCases(&.{.{ "elem_input.accept", "" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.accept", null, &str_valids); // Valid
try runner.testCases(&.{.{ "elem_input.alt", "" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.alt", null, &str_valids); // Valid
try runner.testCases(&.{.{ "elem_input.disabled", "false" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.disabled", null, &bool_valids); // Valid
try runner.testCases(&.{.{ "elem_input.maxLength", "-1" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.maxLength", null, &.{.{ .input = "5" }}); // Valid
try testProperty(arena, &runner, "elem_input.maxLength", "0", &.{.{ .input = "'banana'" }}); // Invalid
try runner.testCases(&.{.{ "try { elem_input.maxLength = -45 } catch(e) {e}", "Error: NegativeValueNotAllowed" }}, .{}); // Error
try runner.testCases(&.{.{ "elem_input.name", "" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.name", null, &str_valids); // Valid
try runner.testCases(&.{.{ "elem_input.readOnly", "false" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.readOnly", null, &bool_valids); // Valid
try runner.testCases(&.{.{ "elem_input.size", "20" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.size", null, &.{.{ .input = "5" }}); // Valid
try testProperty(arena, &runner, "elem_input.size", "20", &.{.{ .input = "-26" }}); // Invalid
try runner.testCases(&.{.{ "try { elem_input.size = 0 } catch(e) {e}", "Error: ZeroNotAllowed" }}, .{}); // Error
try runner.testCases(&.{.{ "try { elem_input.size = 'banana' } catch(e) {e}", "Error: ZeroNotAllowed" }}, .{}); // Error
try runner.testCases(&.{.{ "elem_input.src", "" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.src", null, &.{
.{ .input = "'foo'", .expected = "https://lightpanda.io/foo" }, // TODO stitch should work with spaces -> %20
.{ .input = "-3", .expected = "https://lightpanda.io/-3" },
.{ .input = "''", .expected = "https://lightpanda.io/noslashattheend" },
});
try runner.testCases(&.{.{ "elem_input.type", "text" }}, .{}); // Initial value
try testProperty(arena, &runner, "elem_input.type", null, &.{.{ .input = "'checkbox'", .expected = "checkbox" }}); // Valid
try testProperty(arena, &runner, "elem_input.type", "text", &.{.{ .input = "'5'" }}); // Invalid
// Properties that are related
try runner.testCases(&.{
.{ "let input_checked = document.createElement('input')", null },
.{ "input_checked.defaultChecked", "false" },
.{ "input_checked.checked", "false" },
.{ "input_checked.defaultChecked = true", "true" },
.{ "input_checked.defaultChecked", "true" },
.{ "input_checked.checked", "true" }, // Also perceived as true
.{ "input_checked.checked = false", "false" },
.{ "input_checked.defaultChecked", "true" },
.{ "input_checked.checked", "false" },
.{ "input_checked.defaultChecked = true", "true" },
.{ "input_checked.checked", "false" }, // Still false
}, .{});
try runner.testCases(&.{
.{ "let input_value = document.createElement('input')", null },
.{ "input_value.defaultValue", "" },
.{ "input_value.value", "" },
.{ "input_value.defaultValue = 3.1", "3.1" },
.{ "input_value.defaultValue", "3.1" },
.{ "input_value.value", "3.1" }, // Also perceived as 3.1
.{ "input_value.value = 'mango'", "mango" },
.{ "input_value.defaultValue", "3.1" },
.{ "input_value.value", "mango" },
.{ "input_value.defaultValue = true", "true" },
.{ "input_value.value", "mango" }, // Still mango
}, .{});
} }
test "Browser: HTML.HtmlInputElement" { test "Browser.HTML.HtmlInputElement.properties.form" {
try testing.htmlRunner("html/input.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
\\ <form action="test.php" target="_blank">
\\ <p>
\\ <label>First name: <input type="text" name="first-name" /></label>
\\ </p>
\\ </form>
});
defer runner.deinit();
try runner.testCases(&.{
.{ "let elem_input = document.querySelector('input')", null },
.{ "elem_input.form", "[object HTMLFormElement]" }, // Initial value
.{ "elem_input.form = 'foo'", null },
.{ "elem_input.form", "[object HTMLFormElement]" }, // Invalid
}, .{});
} }
test "Browser: HTML.HtmlTemplateElement" { test "Browser.HTML.HTMLTemplateElement" {
try testing.htmlRunner("html/template.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=c></div>" });
defer runner.deinit();
try runner.testCases(&.{
.{ "let t = document.createElement('template')", null },
.{ "let d = document.createElement('div')", null },
.{ "d.id = 'abc'", null },
.{ "t.content.append(d)", null },
.{ "document.getElementById('abc')", "null" },
.{ "document.getElementById('c').appendChild(t.content.cloneNode(true))", null },
.{ "document.getElementById('abc').id", "abc" },
.{ "t.innerHTML = '<span>over</span><p>9000!</p>';", null },
.{ "t.content.childNodes.length", "2" },
.{ "t.content.childNodes[0].tagName", "SPAN" },
.{ "t.content.childNodes[0].innerHTML", "over" },
.{ "t.content.childNodes[1].tagName", "P" },
.{ "t.content.childNodes[1].innerHTML", "9000!" },
}, .{});
} }
test "Browser: HTML.HtmlStyleElement" { test "Browser.HTML.HTMLStyleElement" {
try testing.htmlRunner("html/style.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
defer runner.deinit();
try runner.testCases(&.{
.{ "let s = document.createElement('style')", null },
.{ "s.sheet.type", "text/css" },
.{ "s.sheet == s.sheet", "true" },
.{ "document.createElement('style').sheet == s.sheet", "false" },
}, .{});
} }
test "Browser: HTML.HtmlScriptElement" { test "Browser: HTML.HTMLScriptElement" {
try testing.htmlRunner("html/script/script.html");
try testing.htmlRunner("html/script/inline_defer.html"); try testing.htmlRunner("html/script/inline_defer.html");
try testing.htmlRunner("html/script/import.html");
try testing.htmlRunner("html/script/dynamic_import.html");
try testing.htmlRunner("html/script/importmap.html");
try testing.htmlRunner("html/script/order.html");
} }
test "Browser: HTML.HtmlSlotElement" { test "Browser: HTML.HTMLSlotElement" {
try testing.htmlRunner("html/slot.html"); try testing.htmlRunner("html/html_slot_element.html");
}
const Check = struct {
input: []const u8,
expected: ?[]const u8 = null, // Needed when input != expected
};
const bool_valids = [_]Check{
.{ .input = "true" },
.{ .input = "''", .expected = "false" },
.{ .input = "13.5", .expected = "true" },
};
const str_valids = [_]Check{
.{ .input = "'foo'", .expected = "foo" },
.{ .input = "5", .expected = "5" },
.{ .input = "''", .expected = "" },
.{ .input = "document", .expected = "[object HTMLDocument]" },
};
// .{ "elem.type = '5'", "5" },
// .{ "elem.type", "text" },
fn testProperty(
arena: std.mem.Allocator,
runner: *testing.JsRunner,
elem_dot_prop: []const u8,
always: ?[]const u8, // Ignores checks' expected if set
checks: []const Check,
) !void {
for (checks) |check| {
try runner.testCases(&.{
.{ try std.mem.concat(arena, u8, &.{ elem_dot_prop, " = ", check.input }), null },
.{ elem_dot_prop, always orelse check.expected orelse check.input },
}, .{});
}
} }

View File

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

View File

@@ -21,6 +21,7 @@ const Allocator = std.mem.Allocator;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const HTMLElement = @import("elements.zig").HTMLElement; const HTMLElement = @import("elements.zig").HTMLElement;
const FormData = @import("../xhr/form_data.zig").FormData;
pub const HTMLFormElement = struct { pub const HTMLFormElement = struct {
pub const Self = parser.Form; pub const Self = parser.Form;

View File

@@ -0,0 +1,120 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
pub const History = struct {
const ScrollRestorationMode = enum {
auto,
manual,
};
scrollRestoration: ScrollRestorationMode = .auto,
state: std.json.Value = .null,
// count tracks the history length until we implement correctly pushstate.
count: u32 = 0,
pub fn get_length(self: *History) u32 {
// TODO return the real history length value.
return self.count;
}
pub fn get_scrollRestoration(self: *History) []const u8 {
return switch (self.scrollRestoration) {
.auto => "auto",
.manual => "manual",
};
}
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
if (std.mem.eql(u8, "manual", mode)) self.scrollRestoration = .manual;
if (std.mem.eql(u8, "auto", mode)) self.scrollRestoration = .auto;
}
pub fn get_state(self: *History) std.json.Value {
return self.state;
}
// TODO implement the function
// data must handle any argument. We could expect a std.json.Value but
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
pub fn _pushState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
self.count += 1;
_ = url;
_ = data;
}
// TODO implement the function
// data must handle any argument. We could expect a std.json.Value but
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
pub fn _replaceState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
_ = self;
_ = url;
_ = data;
}
// TODO implement the function
pub fn _go(self: *History, delta: ?i32) void {
_ = self;
_ = delta;
}
// TODO implement the function
pub fn _back(self: *History) void {
_ = self;
}
// TODO implement the function
pub fn _forward(self: *History) void {
_ = self;
}
};
// Tests
// -----
const testing = @import("../../testing.zig");
test "Browser.HTML.History" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "history.scrollRestoration", "auto" },
.{ "history.scrollRestoration = 'manual'", "manual" },
.{ "history.scrollRestoration = 'foo'", "foo" },
.{ "history.scrollRestoration", "manual" },
.{ "history.scrollRestoration = 'auto'", "auto" },
.{ "history.scrollRestoration", "auto" },
.{ "history.state", "null" },
.{ "history.pushState({}, null, '')", "undefined" },
.{ "history.replaceState({}, null, '')", "undefined" },
.{ "history.go()", "undefined" },
.{ "history.go(1)", "undefined" },
.{ "history.go(-1)", "undefined" },
.{ "history.forward()", "undefined" },
.{ "history.back()", "undefined" },
}, .{});
}

View File

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

View File

@@ -16,8 +16,10 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const HTMLElement = @import("elements.zig").HTMLElement; const HTMLElement = @import("elements.zig").HTMLElement;
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#htmliframeelement // https://html.spec.whatwg.org/multipage/iframe-embed-object.html#htmliframeelement

View File

@@ -16,82 +16,91 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const Uri = @import("std").Uri;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const URL = @import("../url/url.zig").URL; const URL = @import("../url/url.zig").URL;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface
pub const Location = struct { pub const Location = struct {
url: URL, url: ?URL = null,
/// Initializes the `Location` to be used in `Window`.
/// Browsers give such initial values when user not navigated yet:
/// Chrome -> chrome://new-tab-page/
/// Firefox -> about:newtab
/// Safari -> favorites://
pub fn init(url: []const u8) !Location {
return .{ .url = try .initForLocation(url) };
}
pub fn get_href(self: *Location, page: *Page) ![]const u8 { pub fn get_href(self: *Location, page: *Page) ![]const u8 {
return self.url.get_href(page); if (self.url) |*u| return u.get_href(page);
return "";
} }
pub fn set_href(_: *const Location, href: []const u8, page: *Page) !void { pub fn get_protocol(self: *Location, page: *Page) ![]const u8 {
return page.navigateFromWebAPI(href, .{ .reason = .script }, .{ .push = null }); if (self.url) |*u| return u.get_protocol(page);
return "";
} }
pub fn get_protocol(self: *Location) []const u8 { pub fn get_host(self: *Location, page: *Page) ![]const u8 {
return self.url.get_protocol(); if (self.url) |*u| return u.get_host(page);
} return "";
pub fn get_host(self: *Location) []const u8 {
return self.url.get_host();
} }
pub fn get_hostname(self: *Location) []const u8 { pub fn get_hostname(self: *Location) []const u8 {
return self.url.get_hostname(); if (self.url) |*u| return u.get_hostname();
return "";
} }
pub fn get_port(self: *Location) []const u8 { pub fn get_port(self: *Location, page: *Page) ![]const u8 {
return self.url.get_port(); if (self.url) |*u| return u.get_port(page);
return "";
} }
pub fn get_pathname(self: *Location) []const u8 { pub fn get_pathname(self: *Location) []const u8 {
return self.url.get_pathname(); if (self.url) |*u| return u.get_pathname();
return "";
} }
pub fn get_search(self: *Location, page: *Page) ![]const u8 { pub fn get_search(self: *Location, page: *Page) ![]const u8 {
return self.url.get_search(page); if (self.url) |*u| return u.get_search(page);
return "";
} }
pub fn get_hash(self: *Location) []const u8 { pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
return self.url.get_hash(); if (self.url) |*u| return u.get_hash(page);
return "";
} }
pub fn get_origin(self: *Location, page: *Page) ![]const u8 { pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
return self.url.get_origin(page); if (self.url) |*u| return u.get_origin(page);
return "";
} }
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void { pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null }); return page.navigateFromWebAPI(url, .{ .reason = .script });
} }
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void { pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }, .replace); return page.navigateFromWebAPI(url, .{ .reason = .script });
} }
pub fn _reload(_: *const Location, page: *Page) !void { pub fn _reload(_: *const Location, page: *Page) !void {
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script }, .reload); return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script });
} }
pub fn _toString(self: *Location, page: *Page) ![]const u8 { pub fn _toString(self: *Location, page: *Page) ![]const u8 {
return self.get_href(page); return try self.get_href(page);
} }
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: HTML.Location" { test "Browser.HTML.Location" {
try testing.htmlRunner("html/location.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "location.href", "https://lightpanda.io/opensource-browser/" },
.{ "document.location.href", "https://lightpanda.io/opensource-browser/" },
.{ "location.host", "lightpanda.io" },
.{ "location.hostname", "lightpanda.io" },
.{ "location.origin", "https://lightpanda.io" },
.{ "location.pathname", "/opensource-browser/" },
.{ "location.hash", "" },
.{ "location.port", "" },
.{ "location.search", "" },
}, .{});
} }

View File

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

View File

@@ -80,7 +80,17 @@ pub const Navigator = struct {
} }
}; };
// Tests
// -----
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: HTML.Navigator" { test "Browser.HTML.Navigator" {
try testing.htmlRunner("html/navigator.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "navigator.userAgent", "Lightpanda/1.0" },
.{ "navigator.appVersion", "1.0" },
.{ "navigator.language", "en-US" },
}, .{});
} }

View File

@@ -199,6 +199,80 @@ pub const HTMLOptionsCollection = struct {
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: HTML.Select" { test "Browser.HTML.Select" {
try testing.htmlRunner("html/select.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
\\ <form id=f1>
\\ <select id=s1 name=s1><option>o1<option>o2</select>
\\ </form>
\\ <select id=s2></select>
});
defer runner.deinit();
try runner.testCases(&.{
.{ "const s = document.getElementById('s1');", null },
.{ "s.form", "[object HTMLFormElement]" },
.{ "document.getElementById('s2').form", "null" },
.{ "s.disabled", "false" },
.{ "s.disabled = true", null },
.{ "s.disabled", "true" },
.{ "s.disabled = false", null },
.{ "s.disabled", "false" },
.{ "s.multiple", "false" },
.{ "s.multiple = true", null },
.{ "s.multiple", "true" },
.{ "s.multiple = false", null },
.{ "s.multiple", "false" },
.{ "s.name;", "s1" },
.{ "s.name = 'sel1';", null },
.{ "s.name", "sel1" },
.{ "s.length;", "2" },
.{ "s.selectedIndex", "0" },
.{ "s.selectedIndex = 2", null }, // out of range
.{ "s.selectedIndex", "-1" },
.{ "s.selectedIndex = -1", null },
.{ "s.selectedIndex", "-1" },
.{ "s.selectedIndex = 0", null },
.{ "s.selectedIndex", "0" },
.{ "s.selectedIndex = 1", null },
.{ "s.selectedIndex", "1" },
.{ "s.selectedIndex = -323", null },
.{ "s.selectedIndex", "-1" },
.{ "let options = s.options", null },
.{ "options.length", "2" },
.{ "options.item(1).value", "o2" },
.{ "options.selectedIndex", "-1" },
.{ "let o3 = document.createElement('option');", null },
.{ "o3.value = 'o3';", null },
.{ "options.add(o3)", null },
.{ "options.length", "3" },
.{ "options.item(2).value", "o3" },
.{ "let o4 = document.createElement('option');", null },
.{ "o4.value = 'o4';", null },
.{ "options.add(o4, 1)", null },
.{ "options.length", "4" },
.{ "options.item(1).value", "o4" },
.{ "let o5 = document.createElement('option');", null },
.{ "o5.value = 'o5';", null },
.{ "options.add(o5, o3)", null },
.{ "options.length", "5" },
.{ "options.item(3).value", "o5" },
.{ "options.remove(3)", null },
.{ "options.length", "4" },
.{ "options.item(3).value", "o3" },
}, .{});
} }

View File

@@ -25,12 +25,17 @@ pub const SVGElement = struct {
// Currently the prototype chain is not implemented (will not be returned by toInterface()) // Currently the prototype chain is not implemented (will not be returned by toInterface())
// For that we need parser.SvgElement and the derived types with tags in the v-table. // For that we need parser.SvgElement and the derived types with tags in the v-table.
pub const prototype = *Element; pub const prototype = *Element;
// While this is a Node, could consider not exposing the subtype until we have // While this is a Node, could consider not exposing the subtype untill we have
// a Self type to cast to. // a Self type to cast to.
pub const subtype = .node; pub const subtype = .node;
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: HTML.SVGElement" { test "Browser.HTML.SVGElement" {
try testing.htmlRunner("html/svg.html"); var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "'AString' instanceof SVGElement", "false" },
}, .{});
} }

View File

@@ -18,14 +18,13 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Navigator = @import("navigator.zig").Navigator; const Navigator = @import("navigator.zig").Navigator;
const History = @import("History.zig"); const History = @import("history.zig").History;
const Navigation = @import("../navigation/Navigation.zig");
const Location = @import("location.zig").Location; const Location = @import("location.zig").Location;
const Crypto = @import("../crypto/crypto.zig").Crypto; const Crypto = @import("../crypto/crypto.zig").Crypto;
const Console = @import("../console/console.zig").Console; const Console = @import("../console/console.zig").Console;
@@ -36,15 +35,11 @@ const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
const Screen = @import("screen.zig").Screen; const Screen = @import("screen.zig").Screen;
const domcss = @import("../dom/css.zig"); const domcss = @import("../dom/css.zig");
const Css = @import("../css/css.zig").Css; const Css = @import("../css/css.zig").Css;
const EventHandler = @import("../events/event.zig").EventHandler;
const Request = @import("../fetch/Request.zig"); const Function = Env.Function;
const fetchFn = @import("../fetch/fetch.zig").fetch; const JsObject = Env.JsObject;
const storage = @import("../storage/storage.zig"); const storage = @import("../storage/storage.zig");
const ErrorEvent = @import("error_event.zig").ErrorEvent;
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
// https://dom.spec.whatwg.org/#interface-window-extensions // https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window // https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
@@ -56,7 +51,8 @@ pub const Window = struct {
document: *parser.DocumentHTML, document: *parser.DocumentHTML,
target: []const u8 = "", target: []const u8 = "",
location: Location, history: History = .{},
location: Location = .{},
storage_shelf: ?*storage.Shelf = null, storage_shelf: ?*storage.Shelf = null,
// counter for having unique timer ids // counter for having unique timer ids
@@ -69,10 +65,6 @@ pub const Window = struct {
performance: Performance, performance: Performance,
screen: Screen = .{}, screen: Screen = .{},
css: Css = .{}, css: Css = .{},
scroll_x: u32 = 0,
scroll_y: u32 = 0,
onload_callback: ?js.Function = null,
onpopstate_callback: ?js.Function = null,
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window { pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
var fbs = std.io.fixedBufferStream(""); var fbs = std.io.fixedBufferStream("");
@@ -83,7 +75,6 @@ pub const Window = struct {
return .{ return .{
.document = html_doc, .document = html_doc,
.target = target orelse "", .target = target orelse "",
.location = try .init("about:blank"),
.navigator = navigator orelse .{}, .navigator = navigator orelse .{},
.performance = Performance.init(), .performance = Performance.init(),
}; };
@@ -94,10 +85,6 @@ pub const Window = struct {
try parser.documentHTMLSetLocation(Location, self.document, &self.location); try parser.documentHTMLSetLocation(Location, self.document, &self.location);
} }
pub fn changeLocation(self: *Window, new_url: []const u8, page: *Page) !void {
return self.location.url.reinit(new_url, page);
}
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void { pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
self.document = doc; self.document = doc;
@@ -108,28 +95,12 @@ pub const Window = struct {
self.storage_shelf = shelf; self.storage_shelf = shelf;
} }
pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !js.Promise { pub fn get_window(self: *Window) *Window {
return fetchFn(input, options, page); return self;
} }
/// Returns `onload_callback`. pub fn get_navigator(self: *Window) *Navigator {
pub fn get_onload(self: *const Window) ?js.Function { return &self.navigator;
return self.onload_callback;
}
/// Sets `onload_callback`.
pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
try DirectEventHandler(Window, self, "load", maybe_listener, &self.onload_callback, page.arena);
}
/// Returns `onpopstate_callback`.
pub fn get_onpopstate(self: *const Window) ?js.Function {
return self.onpopstate_callback;
}
/// Sets `onpopstate_callback`.
pub fn set_onpopstate(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
try DirectEventHandler(Window, self, "popstate", maybe_listener, &self.onpopstate_callback, page.arena);
} }
pub fn get_location(self: *Window) *Location { pub fn get_location(self: *Window) *Location {
@@ -137,7 +108,23 @@ pub const Window = struct {
} }
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void { pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null }); return page.navigateFromWebAPI(url, .{ .reason = .script });
}
pub fn get_console(self: *Window) *Console {
return &self.console;
}
pub fn get_crypto(self: *Window) *Crypto {
return &self.crypto;
}
pub fn get_self(self: *Window) *Window {
return self;
}
pub fn get_parent(self: *Window) *Window {
return self;
} }
// frames return the window itself, but accessing it via a pseudo // frames return the window itself, but accessing it via a pseudo
@@ -177,16 +164,16 @@ pub const Window = struct {
return frames.get_length(); return frames.get_length();
} }
pub fn get_top(self: *Window) *Window {
return self;
}
pub fn get_document(self: *Window) ?*parser.DocumentHTML { pub fn get_document(self: *Window) ?*parser.DocumentHTML {
return self.document; return self.document;
} }
pub fn get_history(_: *Window, page: *Page) *History { pub fn get_history(self: *Window) *History {
return &page.session.history; return &self.history;
}
pub fn get_navigation(_: *Window, page: *Page) *Navigation {
return &page.session.navigation;
} }
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present. // The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
@@ -215,11 +202,19 @@ pub const Window = struct {
return &self.storage_shelf.?.bucket.session; return &self.storage_shelf.?.bucket.session;
} }
pub fn get_performance(self: *Window) *Performance {
return &self.performance;
}
pub fn get_screen(self: *Window) *Screen {
return &self.screen;
}
pub fn get_CSS(self: *Window) *Css { pub fn get_CSS(self: *Window) *Css {
return &self.css; return &self.css;
} }
pub fn _requestAnimationFrame(self: *Window, cbk: js.Function, page: *Page) !u32 { pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
return self.createTimeout(cbk, 5, page, .{ return self.createTimeout(cbk, 5, page, .{
.animation_frame = true, .animation_frame = true,
.name = "animationFrame", .name = "animationFrame",
@@ -231,11 +226,11 @@ pub const Window = struct {
_ = self.timers.remove(id); _ = self.timers.remove(id);
} }
pub fn _setTimeout(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 { pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" }); return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
} }
pub fn _setInterval(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 { pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" }); return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
} }
@@ -247,22 +242,14 @@ pub const Window = struct {
_ = self.timers.remove(id); _ = self.timers.remove(id);
} }
pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !u32 { pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" }); return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
} }
pub fn _setImmediate(self: *Window, cbk: js.Function, page: *Page) !u32 { pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
return self.createTimeout(cbk, 0, page, .{ .name = "setImmediate" });
}
pub fn _clearImmediate(self: *Window, id: u32) void {
_ = self.timers.remove(id);
}
pub fn _matchMedia(_: *const Window, media: js.String) !MediaQueryList {
return .{ return .{
.matches = false, // TODO? .matches = false, // TODO?
.media = media.string, .media = try page.arena.dupe(u8, media),
}; };
} }
@@ -281,33 +268,14 @@ pub const Window = struct {
return out; return out;
} }
pub fn _reportError(self: *Window, err: js.Object, page: *Page) !void {
var error_event = try ErrorEvent.constructor("error", .{
.@"error" = err,
});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, self),
@as(*parser.Event, &error_event.proto),
);
if (parser.eventDefaultPrevented(&error_event.proto) == false) {
const err_string = err.toString() catch "Unknown error";
log.info(.user_script, "error", .{
.err = err_string,
.stack = page.stackTrace() catch "???",
.source = "window.reportError",
});
}
}
const CreateTimeoutOpts = struct { const CreateTimeoutOpts = struct {
name: []const u8, name: []const u8,
args: []js.Object = &.{}, args: []Env.JsObject = &.{},
repeat: bool = false, repeat: bool = false,
animation_frame: bool = false, animation_frame: bool = false,
low_priority: bool = false, low_priority: bool = false,
}; };
fn createTimeout(self: *Window, cbk: js.Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 { fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
const delay = delay_ orelse 0; const delay = delay_ orelse 0;
if (self.timers.count() > 512) { if (self.timers.count() > 512) {
return error.TooManyTimeout; return error.TooManyTimeout;
@@ -327,9 +295,9 @@ pub const Window = struct {
errdefer _ = self.timers.remove(timer_id); errdefer _ = self.timers.remove(timer_id);
const args = opts.args; const args = opts.args;
var persisted_args: []js.Object = &.{}; var persisted_args: []Env.JsObject = &.{};
if (args.len > 0) { if (args.len > 0) {
persisted_args = try page.arena.alloc(js.Object, args.len); persisted_args = try page.arena.alloc(Env.JsObject, args.len);
for (args, persisted_args) |a, *ca| { for (args, persisted_args) |a, *ca| {
ca.* = try a.persist(); ca.* = try a.persist();
} }
@@ -370,20 +338,12 @@ pub const Window = struct {
const Opts = struct { const Opts = struct {
top: i32, top: i32,
left: i32, left: i32,
behavior: []const u8 = "", behavior: []const u8,
}; };
}; };
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32) !void { pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?u32) !void {
switch (opts) { _ = opts;
.x => |x| { _ = y;
self.scroll_x = @intCast(@max(x, 0));
self.scroll_y = @intCast(@max(0, y orelse 0));
},
.opts => |o| {
self.scroll_y = @intCast(@max(0, o.top));
self.scroll_x = @intCast(@max(0, o.left));
},
}
{ {
const scroll_event = try parser.eventCreate(); const scroll_event = try parser.eventCreate();
@@ -407,28 +367,6 @@ pub const Window = struct {
); );
} }
} }
pub fn _scroll(self: *Window, opts: ScrollToOpts, y: ?i32) !void {
// just an alias for scrollTo
return self._scrollTo(opts, y);
}
pub fn get_scrollX(self: *const Window) u32 {
return self.scroll_x;
}
pub fn get_scrollY(self: *const Window) u32 {
return self.scroll_y;
}
pub fn get_pageXOffset(self: *const Window) u32 {
// just an alias for scrollX
return self.get_scrollX();
}
pub fn get_pageYOffset(self: *const Window) u32 {
// just an alias for scrollY
return self.get_scrollY();
}
// libdom's document doesn't have a parent, which is correct, but // libdom's document doesn't have a parent, which is correct, but
// breaks the event bubbling that happens for many events from // breaks the event bubbling that happens for many events from
@@ -446,18 +384,6 @@ pub const Window = struct {
// and thus the target has already been set to the document. // and thus the target has already been set to the document.
return self.base.redispatchEvent(evt); return self.base.redispatchEvent(evt);
} }
pub fn postAttach(self: *Window, js_this: js.This) !void {
try js_this.set("top", self, .{});
try js_this.set("self", self, .{});
try js_this.set("parent", self, .{});
try js_this.set("window", self, .{});
try js_this.set("crypto", &self.crypto, .{});
try js_this.set("screen", &self.screen, .{});
try js_this.set("console", &self.console, .{});
try js_this.set("navigator", &self.navigator, .{});
try js_this.set("performance", &self.performance, .{});
}
}; };
const TimerCallback = struct { const TimerCallback = struct {
@@ -468,13 +394,13 @@ const TimerCallback = struct {
repeat: ?u32, repeat: ?u32,
// The JavaScript callback to execute // The JavaScript callback to execute
cbk: js.Function, cbk: Function,
animation_frame: bool = false, animation_frame: bool = false,
window: *Window, window: *Window,
args: []js.Object = &.{}, args: []Env.JsObject = &.{},
fn run(ctx: *anyopaque) ?u32 { fn run(ctx: *anyopaque) ?u32 {
const self: *TimerCallback = @ptrCast(@alignCast(ctx)); const self: *TimerCallback = @ptrCast(@alignCast(ctx));
@@ -488,7 +414,7 @@ const TimerCallback = struct {
return null; return null;
} }
var result: js.Function.Result = undefined; var result: Function.Result = undefined;
var call: anyerror!void = undefined; var call: anyerror!void = undefined;
if (self.animation_frame) { if (self.animation_frame) {

View File

@@ -1,561 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const Page = @import("../page.zig").Page;
const types = @import("types.zig");
const Context = @import("Context.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const CALL_ARENA_RETAIN = 1024 * 16;
// Responsible for calling Zig functions from JS invocations. This could
// probably just contained in ExecutionWorld, but having this specific logic, which
// is somewhat repetitive between constructors, functions, getters, etc contained
// here does feel like it makes it cleaner.
const Caller = @This();
context: *Context,
v8_context: v8.Context,
isolate: v8.Isolate,
call_arena: Allocator,
// info is a v8.PropertyCallbackInfo or a v8.FunctionCallback
// All we really want from it is the isolate.
// executor = Isolate -> getCurrentContext -> getEmbedderData()
pub fn init(info: anytype) Caller {
const isolate = info.getIsolate();
const v8_context = isolate.getCurrentContext();
const context: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
context.call_depth += 1;
return .{
.context = context,
.isolate = isolate,
.v8_context = v8_context,
.call_arena = context.call_arena,
};
}
pub fn deinit(self: *Caller) void {
const context = self.context;
const call_depth = context.call_depth - 1;
// Because of callbacks, calls can be nested. Because of this, we
// can't clear the call_arena after _every_ call. Imagine we have
// arr.forEach((i) => { console.log(i); }
//
// First we call forEach. Inside of our forEach call,
// we call console.log. If we reset the call_arena after this call,
// it'll reset it for the `forEach` call after, which might still
// need the data.
//
// Therefore, we keep a call_depth, and only reset the call_arena
// when a top-level (call_depth == 0) function ends.
if (call_depth == 0) {
const arena: *ArenaAllocator = @ptrCast(@alignCast(context.call_arena.ptr));
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
// Set this _after_ we've executed the above code, so that if the
// above code executes any callbacks, they aren't being executed
// at scope 0, which would be wrong.
context.call_depth = call_depth;
}
pub fn constructor(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
const args = try self.getArgs(Struct, named_function, 0, info);
const res = @call(.auto, Struct.constructor, args);
const ReturnType = @typeInfo(@TypeOf(Struct.constructor)).@"fn".return_type orelse {
@compileError(@typeName(Struct) ++ " has a constructor without a return type");
};
const this = info.getThis();
if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err;
_ = try self.context.mapZigInstanceToJs(this, non_error_res);
} else {
_ = try self.context.mapZigInstanceToJs(this, res);
}
info.getReturnValue().set(this);
}
pub fn method(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
if (comptime isSelfReceiver(Struct, named_function) == false) {
return self.function(Struct, named_function, info);
}
const context = self.context;
const func = @field(Struct, named_function.name);
var args = try self.getArgs(Struct, named_function, 1, info);
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
// inject 'self' as the first parameter
@field(args, "0") = zig_instance;
const res = @call(.auto, func, args);
info.getReturnValue().set(try context.zigValueToJs(res));
}
pub fn function(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
const context = self.context;
const func = @field(Struct, named_function.name);
const args = try self.getArgs(Struct, named_function, 0, info);
const res = @call(.auto, func, args);
info.getReturnValue().set(try context.zigValueToJs(res));
}
pub fn getIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, idx: u32, info: v8.PropertyCallbackInfo) !u8 {
const context = self.context;
const func = @field(Struct, named_function.name);
const IndexedGet = @TypeOf(func);
if (@typeInfo(IndexedGet).@"fn".return_type == null) {
@compileError(named_function.full_name ++ " must have a return type");
}
var has_value = true;
var args: ParamterTypes(IndexedGet) = undefined;
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
switch (arg_fields.len) {
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
3, 4 => {
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
comptime assertSelfReceiver(Struct, named_function);
@field(args, "0") = zig_instance;
@field(args, "1") = idx;
@field(args, "2") = &has_value;
if (comptime arg_fields.len == 4) {
comptime assertIsPageArg(Struct, named_function, 3);
@field(args, "3") = context.page;
}
},
else => @compileError(named_function.full_name ++ " has too many parmaters"),
}
const res = @call(.auto, func, args);
if (has_value == false) {
return v8.Intercepted.No;
}
info.getReturnValue().set(try context.zigValueToJs(res));
return v8.Intercepted.Yes;
}
pub fn getNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
const context = self.context;
const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function);
var has_value = true;
var args = try self.getArgs(Struct, named_function, 3, info);
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
@field(args, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = &has_value;
const res = @call(.auto, func, args);
if (has_value == false) {
return v8.Intercepted.No;
}
info.getReturnValue().set(try self.context.zigValueToJs(res));
return v8.Intercepted.Yes;
}
pub fn setNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo) !u8 {
const context = self.context;
const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function);
var has_value = true;
var args = try self.getArgs(Struct, named_function, 4, info);
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
@field(args, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = try context.jsValueToZig(named_function, @TypeOf(@field(args, "2")), js_value);
@field(args, "3") = &has_value;
const res = @call(.auto, func, args);
return namedSetOrDeleteCall(res, has_value);
}
pub fn deleteNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
const context = self.context;
const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function);
var has_value = true;
var args = try self.getArgs(Struct, named_function, 3, info);
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
@field(args, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = &has_value;
const res = @call(.auto, func, args);
return namedSetOrDeleteCall(res, has_value);
}
fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 {
if (@typeInfo(@TypeOf(res)) == .error_union) {
_ = try res;
}
if (has_value == false) {
return v8.Intercepted.No;
}
return v8.Intercepted.Yes;
}
fn nameToString(self: *Caller, name: v8.Name) ![]const u8 {
return self.context.valueToString(.{ .handle = name.handle }, .{});
}
fn isSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) bool {
return checkSelfReceiver(Struct, named_function, false);
}
fn assertSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) void {
_ = checkSelfReceiver(Struct, named_function, true);
}
fn checkSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction, comptime fail: bool) bool {
const func = @field(Struct, named_function.name);
const params = @typeInfo(@TypeOf(func)).@"fn".params;
if (params.len == 0) {
if (fail) {
@compileError(named_function.full_name ++ " must have a self parameter");
}
return false;
}
const R = types.Receiver(Struct);
const first_param = params[0].type.?;
if (first_param != *R and first_param != *const R) {
if (fail) {
@compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{
named_function.full_name,
@typeName(R),
@typeName(R),
@typeName(first_param),
}));
}
return false;
}
return true;
}
fn assertIsPageArg(comptime Struct: type, comptime named_function: NamedFunction, index: comptime_int) void {
const F = @TypeOf(@field(Struct, named_function.name));
const param = @typeInfo(F).@"fn".params[index].type.?;
if (isPage(param)) {
return;
}
@compileError(std.fmt.comptimePrint("The {d} parameter to {s} must be a *Page or *const Page. Got: {s}", .{ index, named_function.full_name, @typeName(param) }));
}
pub fn handleError(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, err: anyerror, info: anytype) void {
const isolate = self.isolate;
if (comptime @import("builtin").mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
if (log.enabled(.js, .warn)) {
self.logFunctionCallError(err, named_function.full_name, info);
}
}
var js_err: ?v8.Value = switch (err) {
error.InvalidArgument => createTypeException(isolate, "invalid argument"),
error.OutOfMemory => js._createException(isolate, "out of memory"),
error.IllegalConstructor => js._createException(isolate, "Illegal Contructor"),
else => blk: {
const func = @field(Struct, named_function.name);
const return_type = @typeInfo(@TypeOf(func)).@"fn".return_type orelse {
// void return type;
break :blk null;
};
if (@typeInfo(return_type) != .error_union) {
// type defines a custom exception, but this function should
// not fail. We failed somewhere inside of js.zig and
// should return the error as-is, since it isn't related
// to our Struct
break :blk null;
}
const function_error_set = @typeInfo(return_type).error_union.error_set;
const E = comptime getCustomException(Struct) orelse break :blk null;
if (function_error_set == E or isErrorSetException(E, err)) {
const custom_exception = E.init(self.call_arena, err, named_function.js_name) catch |init_err| {
switch (init_err) {
// if a custom exceptions' init wants to return a
// different error, we need to think about how to
// handle that failure.
error.OutOfMemory => break :blk js._createException(isolate, "out of memory"),
}
};
// ughh..how to handle an error here?
break :blk self.context.zigValueToJs(custom_exception) catch js._createException(isolate, "internal error");
}
// this error isn't part of a custom exception
break :blk null;
},
};
if (js_err == null) {
js_err = js._createException(isolate, @errorName(err));
}
const js_exception = isolate.throwException(js_err.?);
info.getReturnValue().setValueHandle(js_exception.handle);
}
// walk the prototype chain to see if a type declares a custom Exception
fn getCustomException(comptime Struct: type) ?type {
var S = Struct;
while (true) {
if (@hasDecl(S, "Exception")) {
return S.Exception;
}
if (@hasDecl(S, "prototype") == false) {
return null;
}
// long ago, we validated that every prototype declaration
// is a pointer.
S = @typeInfo(S.prototype).pointer.child;
}
}
// Does the error we want to return belong to the custom exeception's ErrorSet
fn isErrorSetException(comptime E: type, err: anytype) bool {
const Entry = std.meta.Tuple(&.{ []const u8, void });
const error_set = @typeInfo(E.ErrorSet).error_set.?;
const entries = comptime blk: {
var kv: [error_set.len]Entry = undefined;
for (error_set, 0..) |e, i| {
kv[i] = .{ e.name, {} };
}
break :blk kv;
};
const lookup = std.StaticStringMap(void).initComptime(entries);
return lookup.has(@errorName(err));
}
// If we call a method in javascript: cat.lives('nine');
//
// Then we'd expect a Zig function with 2 parameters: a self and the string.
// In this case, offset == 1. Offset is always 1 for setters or methods.
//
// Offset is always 0 for constructors.
//
// For constructors, setters and methods, we can further increase offset + 1
// if the first parameter is an instance of Page.
//
// Finally, if the JS function is called with _more_ parameters and
// the last parameter in Zig is an array, we'll try to slurp the additional
// parameters into the array.
fn getArgs(self: *const Caller, comptime Struct: type, comptime named_function: NamedFunction, comptime offset: usize, info: anytype) !ParamterTypes(@TypeOf(@field(Struct, named_function.name))) {
const context = self.context;
const F = @TypeOf(@field(Struct, named_function.name));
var args: ParamterTypes(F) = undefined;
const params = @typeInfo(F).@"fn".params[offset..];
// Except for the constructor, the first parameter is always `self`
// This isn't something we'll bind from JS, so skip it.
const params_to_map = blk: {
if (params.len == 0) {
return args;
}
// If the last parameter is the Page, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime isPage(params[params.len - 1].type.?)) {
@field(args, tupleFieldName(params.len - 1 + offset)) = self.context.page;
break :blk params[0 .. params.len - 1];
}
// If the last parameter is a special JsThis, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime params[params.len - 1].type.? == js.This) {
@field(args, tupleFieldName(params.len - 1 + offset)) = .{ .obj = .{
.context = context,
.js_obj = info.getThis(),
} };
// AND the 2nd last parameter is state
if (params.len > 1 and comptime isPage(params[params.len - 2].type.?)) {
@field(args, tupleFieldName(params.len - 2 + offset)) = self.context.page;
break :blk params[0 .. params.len - 2];
}
break :blk params[0 .. params.len - 1];
}
// we have neither a Page nor a JsObject. All params must be
// bound to a JavaScript value.
break :blk params;
};
if (params_to_map.len == 0) {
return args;
}
const js_parameter_count = info.length();
const last_js_parameter = params_to_map.len - 1;
var is_variadic = false;
{
// This is going to get complicated. If the last Zig parameter
// is a slice AND the corresponding javascript parameter is
// NOT an an array, then we'll treat it as a variadic.
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
const last_parameter_type_info = @typeInfo(last_parameter_type);
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
const slice_type = last_parameter_type_info.pointer.child;
const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter)));
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
is_variadic = true;
if (js_parameter_count == 0) {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
} else if (js_parameter_count >= params_to_map.len) {
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
for (arr, last_js_parameter..) |*a, i| {
const js_value = info.getArg(@as(u32, @intCast(i)));
a.* = try context.jsValueToZig(named_function, slice_type, js_value);
}
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
} else {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
}
}
}
}
inline for (params_to_map, 0..) |param, i| {
const field_index = comptime i + offset;
if (comptime i == params_to_map.len - 1) {
if (is_variadic) {
break;
}
}
if (comptime isPage(param.type.?)) {
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ named_function.full_name);
} else if (comptime param.type.? == js.This) {
@compileError("JsThis must be the last parameter: " ++ named_function.full_name);
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument;
}
@field(args, tupleFieldName(field_index)) = null;
} else {
const js_value = info.getArg(@as(u32, @intCast(i)));
@field(args, tupleFieldName(field_index)) = context.jsValueToZig(named_function, param.type.?, js_value) catch {
return error.InvalidArgument;
};
}
}
return args;
}
// This is extracted to speed up compilation. When left inlined in handleError,
// this can add as much as 10 seconds of compilation time.
fn logFunctionCallError(self: *Caller, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void {
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
log.info(.js, "function call error", .{
.name = function_name,
.err = err,
.args = args_dump,
.stack = self.context.stackTrace() catch |err1| @errorName(err1),
});
}
fn serializeFunctionArgs(self: *Caller, info: v8.FunctionCallbackInfo) ![]const u8 {
const separator = log.separator();
const js_parameter_count = info.length();
const context = self.context;
var arr: std.ArrayListUnmanaged(u8) = .{};
for (0..js_parameter_count) |i| {
const js_value = info.getArg(@intCast(i));
const value_string = try context.valueToDetailString(js_value);
const value_type = try context.jsStringToZig(try js_value.typeOf(self.isolate), .{});
try std.fmt.format(arr.writer(context.call_arena), "{s}{d}: {s} ({s})", .{
separator,
i + 1,
value_string,
value_type,
});
}
return arr.items;
}
// We want the function name, or more precisely, the "Struct.function" for
// displaying helpful @compileError.
// However, there's no way to get the name from a std.Builtin.Fn, so we create
// a NamedFunction as part of our binding, and pass it around incase we need
// to display an error
pub const NamedFunction = struct {
name: []const u8,
js_name: []const u8,
full_name: []const u8,
pub fn init(comptime Struct: type, comptime name: []const u8) NamedFunction {
return .{
.name = name,
.js_name = if (name[0] == '_') name[1..] else name,
.full_name = @typeName(Struct) ++ "." ++ name,
};
}
};
// Takes a function, and returns a tuple for its argument. Used when we
// @call a function
fn ParamterTypes(comptime F: type) type {
const params = @typeInfo(F).@"fn".params;
var fields: [params.len]std.builtin.Type.StructField = undefined;
inline for (params, 0..) |param, i| {
fields[i] = .{
.name = tupleFieldName(i),
.type = param.type.?,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(param.type.?),
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.fields = &fields,
.is_tuple = true,
} });
}
fn tupleFieldName(comptime i: usize) [:0]const u8 {
return switch (i) {
0 => "0",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "6",
7 => "7",
8 => "8",
9 => "9",
else => std.fmt.comptimePrint("{d}", .{i}),
};
}
fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page;
}
fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value {
return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg));
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,539 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const types = @import("types.zig");
const Types = types.Types;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const Platform = @import("Platform.zig");
const Inspector = @import("Inspector.zig");
const ExecutionWorld = @import("ExecutionWorld.zig");
const NamedFunction = Caller.NamedFunction;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
// The Env maps to a V8 isolate, which represents a isolated sandbox for
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
// and it's where we'll start ExecutionWorlds, which actually execute JavaScript.
// The `S` parameter is arbitrary state. When we start an ExecutionWorld, an instance
// of S must be given. This instance is available to any Zig binding.
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
const Env = @This();
allocator: Allocator,
platform: *const Platform,
// the global isolate
isolate: v8.Isolate,
// just kept around because we need to free it on deinit
isolate_params: *v8.CreateParams,
// Given a type, we can lookup its index in TYPE_LOOKUP and then have
// access to its TunctionTemplate (the thing we need to create an instance
// of it)
// I.e.:
// const index = @field(TYPE_LOOKUP, @typeName(type_name))
// const template = templates[index];
templates: [Types.len]v8.FunctionTemplate,
// Given a type index (retrieved via the TYPE_LOOKUP), we can retrieve
// the index of its prototype. Types without a prototype have their own
// index.
prototype_lookup: [Types.len]u16,
meta_lookup: [Types.len]types.Meta,
context_id: usize,
const Opts = struct {};
pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
// var params = v8.initCreateParams();
var params = try allocator.create(v8.CreateParams);
errdefer allocator.destroy(params);
v8.c.v8__Isolate__CreateParams__CONSTRUCT(params);
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
var isolate = v8.Isolate.init(params);
errdefer isolate.deinit();
// This is the callback that runs whenever a module is dynamically imported.
isolate.setHostImportModuleDynamicallyCallback(Context.dynamicModuleCallback);
isolate.setPromiseRejectCallback(promiseRejectCallback);
isolate.setMicrotasksPolicy(v8.c.kExplicit);
isolate.enter();
errdefer isolate.exit();
isolate.setHostInitializeImportMetaObjectCallback(Context.metaObjectCallback);
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
const env = try allocator.create(Env);
errdefer allocator.destroy(env);
env.* = .{
.context_id = 0,
.platform = platform,
.isolate = isolate,
.templates = undefined,
.allocator = allocator,
.isolate_params = params,
.meta_lookup = undefined,
.prototype_lookup = undefined,
};
// Populate our templates lookup. generateClass creates the
// v8.FunctionTemplate, which we store in our env.templates.
// The ordering doesn't matter. What matters is that, given a type
// we can get its index via: @field(types.LOOKUP, type_name)
const templates = &env.templates;
inline for (Types, 0..) |s, i| {
@setEvalBranchQuota(10_000);
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, generateClass(s.defaultValue().?, isolate)).castToFunctionTemplate();
}
// Above, we've created all our our FunctionTemplates. Now that we
// have them all, we can hook up the prototypes.
const meta_lookup = &env.meta_lookup;
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) {
const TI = @typeInfo(Struct.prototype);
const proto_name = @typeName(types.Receiver(TI.pointer.child));
if (@hasField(types.Lookup, proto_name) == false) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ proto_name, @typeName(Struct) }));
}
// Hey, look! This is our first real usage of the types.LOOKUP.
// Just like we said above, given a type, we can get its
// template index.
const proto_index = @field(types.LOOKUP, proto_name);
templates[i].inherit(templates[proto_index]);
}
// while we're here, let's populate our meta lookup
const subtype: ?types.Sub = if (@hasDecl(Struct, "subtype")) Struct.subtype else null;
const proto_offset = comptime blk: {
if (!@hasField(Struct, "proto")) {
break :blk 0;
}
const proto_info = std.meta.fieldInfo(Struct, .proto);
if (@typeInfo(proto_info.type) == .pointer) {
// we store the offset as a negative, to so that,
// when we reverse this, we know that it's
// behind a pointer that we need to resolve.
break :blk -@offsetOf(Struct, "proto");
}
break :blk @offsetOf(Struct, "proto");
};
meta_lookup[i] = .{
.index = i,
.subtype = subtype,
.proto_offset = proto_offset,
};
}
return env;
}
pub fn deinit(self: *Env) void {
self.isolate.exit();
self.isolate.deinit();
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
self.allocator.destroy(self.isolate_params);
self.allocator.destroy(self);
}
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector {
return Inspector.init(arena, self.isolate, ctx);
}
pub fn runMicrotasks(self: *const Env) void {
self.isolate.performMicrotasksCheckpoint();
}
pub fn pumpMessageLoop(self: *const Env) bool {
return self.platform.inner.pumpMessageLoop(self.isolate, false);
}
pub fn runIdleTasks(self: *const Env) void {
return self.platform.inner.runIdleTasks(self.isolate, 1);
}
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
return .{
.env = self,
.context = null,
.context_arena = ArenaAllocator.init(self.allocator),
};
}
// V8 doesn't immediately free memory associated with
// a Context, it's managed by the garbage collector. We use the
// `lowMemoryNotification` call on the isolate to encourage v8 to free
// any contexts which have been freed.
pub fn lowMemoryNotification(self: *Env) void {
var handle_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&handle_scope, self.isolate);
defer handle_scope.deinit();
self.isolate.lowMemoryNotification();
}
pub fn dumpMemoryStats(self: *Env) void {
const stats = self.isolate.getHeapStatistics();
std.debug.print(
\\ Total Heap Size: {d}
\\ Total Heap Size Executable: {d}
\\ Total Physical Size: {d}
\\ Total Available Size: {d}
\\ Used Heap Size: {d}
\\ Heap Size Limit: {d}
\\ Malloced Memory: {d}
\\ External Memory: {d}
\\ Peak Malloced Memory: {d}
\\ Number Of Native Contexts: {d}
\\ Number Of Detached Contexts: {d}
\\ Total Global Handles Size: {d}
\\ Used Global Handles Size: {d}
\\ Zap Garbage: {any}
\\
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
}
fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
const msg = v8.PromiseRejectMessage.initFromC(v8_msg);
const isolate = msg.getPromise().toObject().getIsolate();
const context = Context.fromIsolate(isolate);
const value =
if (msg.getValue()) |v8_value| context.valueToString(v8_value, .{}) catch |err| @errorName(err) else "no value";
log.debug(.js, "unhandled rejection", .{ .value = value });
}
// Give it a Zig struct, get back a v8.FunctionTemplate.
// The FunctionTemplate is a bit like a struct container - it's where
// we'll attach functions/getters/setters and where we'll "inherit" a
// prototype type (if there is any)
fn generateClass(comptime Struct: type, isolate: v8.Isolate) v8.FunctionTemplate {
const template = generateConstructor(Struct, isolate);
attachClass(Struct, isolate, template);
return template;
}
// Normally this is called from generateClass. Where generateClass creates
// the constructor (hence, the FunctionTemplate), attachClass adds all
// of its functions, getters, setters, ...
// But it's extracted from generateClass because we also have 1 global
// object (i.e. the Window), which gets attached not only to the Window
// constructor/FunctionTemplate as normal, but also through the default
// FunctionTemplate of the isolate (in createContext)
pub fn attachClass(comptime Struct: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
const template_proto = template.getPrototypeTemplate();
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
const name = declaration.name;
if (comptime name[0] == '_') {
switch (@typeInfo(@TypeOf(@field(Struct, name)))) {
.@"fn" => generateMethod(Struct, name, isolate, template_proto),
else => |ti| if (!comptime js.isComplexAttributeType(ti)) {
generateAttribute(Struct, name, isolate, template, template_proto);
},
}
} else if (comptime std.mem.startsWith(u8, name, "get_")) {
generateProperty(Struct, name[4..], isolate, template_proto);
} else if (comptime std.mem.startsWith(u8, name, "static_")) {
generateFunction(Struct, name[7..], isolate, template);
}
}
if (@hasDecl(Struct, "get_symbol_toStringTag") == false) {
// If this WAS defined, then we would have created it in generateProperty.
// But if it isn't, we create a default one
const string_tag_callback = v8.FunctionTemplate.initCallback(isolate, struct {
fn stringTag(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
const class_name = v8.String.initUtf8(info.getIsolate(), comptime js.classNameForStruct(Struct));
info.getReturnValue().set(class_name);
}
}.stringTag);
const key = v8.Symbol.getToStringTag(isolate).toName();
template_proto.setAccessorGetter(key, string_tag_callback);
}
generateIndexer(Struct, template_proto);
generateNamedIndexer(Struct, template.getInstanceTemplate());
generateUndetectable(Struct, template.getInstanceTemplate());
}
// Even if a struct doesn't have a `constructor` function, we still
// `generateConstructor`, because this is how we create our
// FunctionTemplate. Such classes exist, but they can't be instantiated
// via `new ClassName()` - but they could, for example, be created in
// Zig and returned from a function call, which is why we need the
// FunctionTemplate.
fn generateConstructor(comptime Struct: type, isolate: v8.Isolate) v8.FunctionTemplate {
const template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
// See comment above. We generateConstructor on all types
// in order to create the FunctionTemplate, but there might
// not be an actual "constructor" function. So if someone
// does `new ClassName()` where ClassName doesn't have
// a constructor function, we'll return an error.
if (@hasDecl(Struct, "constructor") == false) {
const iso = caller.isolate;
log.warn(.js, "Illegal constructor call", .{ .name = @typeName(Struct) });
const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
info.getReturnValue().set(js_exception);
return;
}
// Safe to call now, because if Struct.constructor didn't
// exist, the above if block would have returned.
const named_function = comptime NamedFunction.init(Struct, "constructor");
caller.constructor(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
if (comptime types.isEmpty(types.Receiver(Struct)) == false) {
// If the struct is empty, we won't store a Zig reference inside
// the JS object, so we don't need to set the internal field count
template.getInstanceTemplate().setInternalFieldCount(1);
}
const class_name = v8.String.initUtf8(isolate, comptime js.classNameForStruct(Struct));
template.setClassName(class_name);
return template;
}
fn generateMethod(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void {
var js_name: v8.Name = undefined;
if (comptime std.mem.eql(u8, name, "_symbol_iterator")) {
js_name = v8.Symbol.getIterator(isolate).toName();
} else {
js_name = v8.String.initUtf8(isolate, name[1..]).toName();
}
const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, name);
caller.method(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
}
fn generateFunction(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
const js_name = v8.String.initUtf8(isolate, name).toName();
const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "static_" ++ name);
caller.function(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
template.set(js_name, function_template, v8.PropertyAttribute.None);
}
fn generateAttribute(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void {
const zig_value = @field(Struct, name);
const js_value = js.simpleZigValueToJs(isolate, zig_value, true);
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
// apply it both to the type itself
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
// and to instances of the type
template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
}
fn generateProperty(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void {
var js_name: v8.Name = undefined;
if (comptime std.mem.eql(u8, name, "symbol_toStringTag")) {
js_name = v8.Symbol.getToStringTag(isolate).toName();
} else {
js_name = v8.String.initUtf8(isolate, name).toName();
}
const getter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "get_" ++ name);
caller.method(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
const setter_name = "set_" ++ name;
if (@hasDecl(Struct, setter_name) == false) {
template_proto.setAccessorGetter(js_name, getter_callback);
return;
}
const setter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
std.debug.assert(info.length() == 1);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "set_" ++ name);
caller.method(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
template_proto.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
}
fn generateIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
if (@hasDecl(Struct, "indexed_get") == false) {
return;
}
const configuration = v8.IndexedPropertyHandlerConfiguration{
.getter = struct {
fn callback(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "indexed_get");
return caller.getIndex(Struct, named_function, idx, info) catch |err| blk: {
caller.handleError(Struct, named_function, err, info);
break :blk v8.Intercepted.No;
};
}
}.callback,
};
// If you're trying to implement setter, read:
// https://groups.google.com/g/v8-users/c/8tahYBsHpgY/m/IteS7Wn2AAAJ
// The issue I had was
// (a) where to attache it: does it go on the instance_template
// instead of the prototype?
// (b) defining the getter or query to respond with the
// PropertyAttribute to indicate if the property can be set
template_proto.setIndexedProperty(configuration, null);
}
fn generateNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
if (@hasDecl(Struct, "named_get") == false) {
return;
}
var configuration = v8.NamedPropertyHandlerConfiguration{
.getter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "named_get");
return caller.getNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
caller.handleError(Struct, named_function, err, info);
break :blk v8.Intercepted.No;
};
}
}.callback,
// This is really cool. Without this, we'd intercept _all_ properties
// even those explicitly set. So, node.length for example would get routed
// to our `named_get`, rather than a `get_length`. This might be
// useful if we run into a type that we can't model properly in Zig.
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
};
if (@hasDecl(Struct, "named_set")) {
configuration.setter = struct {
fn callback(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "named_set");
return caller.setNamedIndex(Struct, named_function, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info) catch |err| blk: {
caller.handleError(Struct, named_function, err, info);
break :blk v8.Intercepted.No;
};
}
}.callback;
}
if (@hasDecl(Struct, "named_delete")) {
configuration.deleter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "named_delete");
return caller.deleteNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
caller.handleError(Struct, named_function, err, info);
break :blk v8.Intercepted.No;
};
}
}.callback;
}
template_proto.setNamedProperty(configuration, null);
}
fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void {
const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction");
if (has_js_call_as_function) {
template.setCallAsFunctionHandler(struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "jsCallAsFunction");
caller.method(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
}
if (@hasDecl(Struct, "mark_as_undetectable") and Struct.mark_as_undetectable) {
if (!has_js_call_as_function) {
@compileError(@typeName(Struct) ++ ": mark_as_undetectable required jsCallAsFunction to be defined. This is a hard-coded requirement in V8, because mark_as_undetectable only exists for HTMLAllCollection which is also callable.");
}
template.markAsUndetectable();
}
}

View File

@@ -1,251 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const Page = @import("../page.zig").Page;
const ScriptManager = @import("../ScriptManager.zig");
const types = @import("types.zig");
const Types = types.Types;
const Env = @import("Env.zig");
const Context = @import("Context.zig");
const ArenaAllocator = std.heap.ArenaAllocator;
const CONTEXT_ARENA_RETAIN = 1024 * 64;
// ExecutionWorld closely models a JS World.
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#World
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
const ExecutionWorld = @This();
env: *Env,
// Arena whose lifetime is for a single page load. Where
// the call_arena lives for a single function call, the context_arena
// lives for the lifetime of the entire page. The allocator will be
// owned by the Context, but the arena itself is owned by the ExecutionWorld
// so that we can re-use it from context to context.
context_arena: ArenaAllocator,
// Currently a context maps to a Browser's Page. Here though, it's only a
// mechanism to organization page-specific memory. The ExecutionWorld
// does all the work, but having all page-specific data structures
// grouped together helps keep things clean.
context: ?Context = null,
// no init, must be initialized via env.newExecutionWorld()
pub fn deinit(self: *ExecutionWorld) void {
if (self.context != null) {
self.removeContext();
}
self.context_arena.deinit();
}
// Only the top Context in the Main ExecutionWorld should hold a handle_scope.
// A v8.HandleScope is like an arena. Once created, any "Local" that
// v8 creates will be released (or at least, releasable by the v8 GC)
// when the handle_scope is freed.
// We also maintain our own "context_arena" which allows us to have
// all page related memory easily managed.
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_callback: ?js.GlobalMissingCallback) !*Context {
std.debug.assert(self.context == null);
const env = self.env;
const isolate = env.isolate;
const Global = @TypeOf(page.window);
const templates = &self.env.templates;
var v8_context: v8.Context = blk: {
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
const js_global = v8.FunctionTemplate.initDefault(isolate);
Env.attachClass(Global, isolate, js_global);
const global_template = js_global.getInstanceTemplate();
global_template.setInternalFieldCount(1);
// Configure the missing property callback on the global
// object.
if (global_callback != null) {
const configuration = v8.NamedPropertyHandlerConfiguration{
.getter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
const context = Context.fromIsolate(info.getIsolate());
const property = context.valueToString(.{ .handle = c_name.? }, .{}) catch "???";
if (context.global_callback.?.missing(property, context)) {
return v8.Intercepted.Yes;
}
return v8.Intercepted.No;
}
}.callback,
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
};
global_template.setNamedProperty(configuration, null);
}
// All the FunctionTemplates that we created and setup in Env.init
// are now going to get associated with our global instance.
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
const class_name = v8.String.initUtf8(isolate, comptime js.classNameForStruct(Struct));
global_template.set(class_name.toName(), templates[i], v8.PropertyAttribute.None);
}
// The global object (Window) has already been hooked into the v8
// engine when the Env was initialized - like every other type.
// But the V8 global is its own FunctionTemplate instance so even
// though it's also a Window, we need to set the prototype for this
// specific instance of the the Window.
if (@hasDecl(Global, "prototype")) {
const proto_type = types.Receiver(@typeInfo(Global.prototype).pointer.child);
const proto_name = @typeName(proto_type);
const proto_index = @field(types.LOOKUP, proto_name);
js_global.inherit(templates[proto_index]);
}
const context_local = v8.Context.init(isolate, global_template, null);
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
v8_context.enter();
errdefer if (enter) v8_context.exit();
defer if (!enter) v8_context.exit();
// This shouldn't be necessary, but it is:
// https://groups.google.com/g/v8-users/c/qAQQBmbi--8
// TODO: see if newer V8 engines have a way around this.
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) {
const proto_type = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
const proto_name = @typeName(proto_type);
if (@hasField(types.Lookup, proto_name) == false) {
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name);
}
const proto_index = @field(types.LOOKUP, proto_name);
const proto_obj = templates[proto_index].getFunction(v8_context).toObject();
const self_obj = templates[i].getFunction(v8_context).toObject();
_ = self_obj.setPrototype(v8_context, proto_obj);
}
}
break :blk v8_context;
};
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
var handle_scope: ?v8.HandleScope = null;
if (enter) {
handle_scope = @as(v8.HandleScope, undefined);
v8.HandleScope.init(&handle_scope.?, isolate);
}
errdefer if (enter) handle_scope.?.deinit();
{
// If we want to overwrite the built-in console, we have to
// delete the built-in one.
const js_obj = v8_context.getGlobal();
const console_key = v8.String.initUtf8(isolate, "console");
if (js_obj.deleteValue(v8_context, console_key) == false) {
return error.ConsoleDeleteError;
}
}
const context_id = env.context_id;
env.context_id = context_id + 1;
self.context = Context{
.page = page,
.id = context_id,
.isolate = isolate,
.v8_context = v8_context,
.templates = &env.templates,
.meta_lookup = &env.meta_lookup,
.handle_scope = handle_scope,
.script_manager = &page.script_manager,
.call_arena = page.call_arena,
.arena = self.context_arena.allocator(),
.global_callback = global_callback,
};
var context = &self.context.?;
{
// Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out
const data = isolate.initBigIntU64(@intCast(@intFromPtr(context)));
v8_context.setEmbedderData(1, data);
}
// Custom exception
// NOTE: there is no way in v8 to subclass the Error built-in type
// TODO: this is an horrible hack
inline for (Types) |s| {
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "ErrorSet")) {
const script = comptime js.classNameForStruct(Struct) ++ ".prototype.__proto__ = Error.prototype";
_ = try context.exec(script, "errorSubclass");
}
}
// Primitive attributes are set directly on the FunctionTemplate
// when we setup the environment. But we cannot set more complex
// types (v8 will crash).
//
// Plus, just to create more complex types, we always need a
// context, i.e. an Array has to have a Context to exist.
//
// As far as I can tell, getting the FunctionTemplate's object
// and setting values directly on it, for each context, is the
// way to do this.
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
const name = declaration.name;
if (comptime name[0] == '_') {
const value = @field(Struct, name);
if (comptime js.isComplexAttributeType(@typeInfo(@TypeOf(value)))) {
const js_obj = templates[i].getFunction(v8_context).toObject();
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
const js_val = try context.zigValueToJs(value);
if (!js_obj.setValue(v8_context, js_name, js_val)) {
log.fatal(.app, "set class attribute", .{
.@"struct" = @typeName(Struct),
.name = name,
});
}
}
}
}
}
try context.setupGlobal();
return context;
}
pub fn removeContext(self: *ExecutionWorld) void {
// Force running the micro task to drain the queue before reseting the
// context arena.
// Tasks in the queue are relying to the arena memory could be present in
// the queue. Running them later could lead to invalid memory accesses.
self.env.runMicrotasks();
self.context.?.deinit();
self.context = null;
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
}
pub fn terminateExecution(self: *const ExecutionWorld) void {
self.env.isolate.terminateExecution();
}
pub fn resumeExecution(self: *const ExecutionWorld) void {
self.env.isolate.cancelTerminateExecution();
}

View File

@@ -1,144 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const PersistentFunction = v8.Persistent(v8.Function);
const Allocator = std.mem.Allocator;
const Function = @This();
id: usize,
context: *js.Context,
this: ?v8.Object = null,
func: PersistentFunction,
pub const Result = struct {
stack: ?[]const u8,
exception: []const u8,
};
pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 {
const name = self.func.castToFunction().getName();
return self.context.valueToString(name, .{ .allocator = allocator });
}
pub fn setName(self: *const Function, name: []const u8) void {
const v8_name = v8.String.initUtf8(self.context.isolate, name);
self.func.castToFunction().setName(v8_name);
}
pub fn withThis(self: *const Function, value: anytype) !Function {
const this_obj = if (@TypeOf(value) == js.Object)
value.js_obj
else
(try self.context.zigValueToJs(value)).castTo(v8.Object);
return .{
.id = self.id,
.this = this_obj,
.func = self.func,
.context = self.context,
};
}
pub fn newInstance(self: *const Function, result: *Result) !js.Object {
const context = self.context;
var try_catch: js.TryCatch = undefined;
try_catch.init(context);
defer try_catch.deinit();
// This creates a new instance using this Function as a constructor.
// This returns a generic Object
const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse {
if (try_catch.hasCaught()) {
const allocator = context.call_arena;
result.stack = try_catch.stack(allocator) catch null;
result.exception = (try_catch.exception(allocator) catch "???") orelse "???";
} else {
result.stack = null;
result.exception = "???";
}
return error.JsConstructorFailed;
};
return .{
.context = context,
.js_obj = js_obj,
};
}
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
return self.callWithThis(T, self.getThis(), args);
}
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T {
return self.tryCallWithThis(T, self.getThis(), args, result);
}
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !T {
var try_catch: js.TryCatch = undefined;
try_catch.init(self.context);
defer try_catch.deinit();
return self.callWithThis(T, this, args) catch |err| {
if (try_catch.hasCaught()) {
const allocator = self.context.call_arena;
result.stack = try_catch.stack(allocator) catch null;
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
} else {
result.stack = null;
result.exception = @errorName(err);
}
return err;
};
}
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
const context = self.context;
const js_this = try context.valueToExistingObject(this);
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
const js_args: []const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
.@"struct" => |s| blk: {
const fields = s.fields;
var js_args: [fields.len]v8.Value = undefined;
inline for (fields, 0..) |f, i| {
js_args[i] = try context.zigValueToJs(@field(aargs, f.name));
}
const cargs: [fields.len]v8.Value = js_args;
break :blk &cargs;
},
.pointer => blk: {
var values = try context.call_arena.alloc(v8.Value, args.len);
for (args, 0..) |a, i| {
values[i] = try context.zigValueToJs(a);
}
break :blk values;
},
else => @compileError("JS Function called with invalid paremter type"),
};
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
if (result == null) {
return error.JSExecCallback;
}
if (@typeInfo(T) == .void) return {};
const named_function = comptime Caller.NamedFunction.init(T, "callResult");
return context.jsValueToZig(named_function, T, result.?);
}
fn getThis(self: *const Function) v8.Object {
return self.this orelse self.context.v8_context.getGlobal();
}
pub fn src(self: *const Function) ![]const u8 {
const value = self.func.castToFunction().toValue();
return self.context.valueToString(value, .{});
}

View File

@@ -1,125 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Context = @import("Context.zig");
const Allocator = std.mem.Allocator;
const Inspector = @This();
pub const RemoteObject = v8.RemoteObject;
isolate: v8.Isolate,
inner: *v8.Inspector,
session: v8.InspectorSession,
// We expect allocator to be an arena
pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector {
const ContextT = @TypeOf(ctx);
const InspectorContainer = switch (@typeInfo(ContextT)) {
.@"struct" => ContextT,
.pointer => |ptr| ptr.child,
.void => NoopInspector,
else => @compileError("invalid context type"),
};
// If necessary, turn a void context into something we can safely ptrCast
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
const channel = v8.InspectorChannel.init(safe_context, InspectorContainer.onInspectorResponse, InspectorContainer.onInspectorEvent, isolate);
const client = v8.InspectorClient.init();
const inner = try allocator.create(v8.Inspector);
v8.Inspector.init(inner, client, channel, isolate);
return .{ .inner = inner, .isolate = isolate, .session = inner.connect() };
}
pub fn deinit(self: *const Inspector) void {
self.session.deinit();
self.inner.deinit();
}
pub fn send(self: *const Inspector, msg: []const u8) void {
// Can't assume the main Context exists (with its HandleScope)
// available when doing this. Pages (and thus the HandleScope)
// comes and goes, but CDP can keep sending messages.
const isolate = self.isolate;
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
self.session.dispatchProtocolMessage(isolate, msg);
}
// From CDP docs
// https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-ExecutionContextDescription
// ----
// - name: Human readable name describing given context.
// - origin: Execution context origin (ie. URL who initialised the request)
// - auxData: Embedder-specific auxiliary data likely matching
// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
// - is_default_context: Whether the execution context is default, should match the auxData
pub fn contextCreated(
self: *const Inspector,
context: *const Context,
name: []const u8,
origin: []const u8,
aux_data: ?[]const u8,
is_default_context: bool,
) void {
self.inner.contextCreated(context.v8_context, name, origin, aux_data, is_default_context);
}
// Retrieves the RemoteObject for a given value.
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
// just like a method return value. Therefore, if we've mapped this
// value before, we'll get the existing JS PersistedObject and if not
// we'll create it and track it for cleanup when the context ends.
pub fn getRemoteObject(
self: *const Inspector,
context: *Context,
group: []const u8,
value: anytype,
) !RemoteObject {
const js_value = try context.zigValueToJs(value);
// We do not want to expose this as a parameter for now
const generate_preview = false;
return self.session.wrapObject(
context.isolate,
context.v8_context,
js_value,
group,
generate_preview,
);
}
// Gets a value by object ID regardless of which context it is in.
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !?*anyopaque {
const unwrapped = try self.session.unwrapObject(allocator, object_id);
// The values context and groupId are not used here
const toa = getTaggedAnyOpaque(unwrapped.value) orelse return null;
if (toa.subtype == null or toa.subtype != .node) return error.ObjectIdIsNotANode;
return toa.ptr;
}
const NoopInspector = struct {
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
};
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
if (value.isObject() == false) {
return null;
}
const obj = value.castTo(v8.Object);
if (obj.internalFieldCount() == 0) {
return null;
}
const external_data = obj.getInternalField(0).castTo(v8.External).get().?;
return @ptrCast(@alignCast(external_data));
}

View File

@@ -1,149 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const PersistentObject = v8.Persistent(v8.Object);
const Allocator = std.mem.Allocator;
const Object = @This();
js_obj: v8.Object,
context: *js.Context,
pub const SetOpts = packed struct(u32) {
READ_ONLY: bool = false,
DONT_ENUM: bool = false,
DONT_DELETE: bool = false,
_: u29 = 0,
};
pub fn setIndex(self: Object, index: u32, value: anytype, opts: SetOpts) !void {
@setEvalBranchQuota(10000);
const key = switch (index) {
inline 0...20 => |i| std.fmt.comptimePrint("{d}", .{i}),
else => try std.fmt.allocPrint(self.context.arena, "{d}", .{index}),
};
return self.set(key, value, opts);
}
pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{ FailedToSet, OutOfMemory }!void {
const context = self.context;
const js_key = v8.String.initUtf8(context.isolate, key);
const js_value = try context.zigValueToJs(value);
const res = self.js_obj.defineOwnProperty(context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false;
if (!res) {
return error.FailedToSet;
}
}
pub fn get(self: Object, key: []const u8) !js.Value {
const context = self.context;
const js_key = v8.String.initUtf8(context.isolate, key);
const js_val = try self.js_obj.getValue(context.v8_context, js_key);
return context.createValue(js_val);
}
pub fn isTruthy(self: Object) bool {
const js_value = self.js_obj.toValue();
return js_value.toBool(self.context.isolate);
}
pub fn toString(self: Object) ![]const u8 {
const js_value = self.js_obj.toValue();
return self.context.valueToString(js_value, .{});
}
pub fn toDetailString(self: Object) ![]const u8 {
const js_value = self.js_obj.toValue();
return self.context.valueToDetailString(js_value);
}
pub fn format(self: Object, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
return writer.writeAll(try self.toString());
}
pub fn toJson(self: Object, allocator: Allocator) ![]u8 {
const json_string = try v8.Json.stringify(self.context.v8_context, self.js_obj.toValue(), null);
const str = try self.context.jsStringToZig(json_string, .{ .allocator = allocator });
return str;
}
pub fn persist(self: Object) !Object {
var context = self.context;
const js_obj = self.js_obj;
const persisted = PersistentObject.init(context.isolate, js_obj);
try context.js_object_list.append(context.arena, persisted);
return .{
.context = context,
.js_obj = persisted.castToObject(),
};
}
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
if (self.isNullOrUndefined()) {
return null;
}
const context = self.context;
const js_name = v8.String.initUtf8(context.isolate, name);
const js_value = try self.js_obj.getValue(context.v8_context, js_name.toName());
if (!js_value.isFunction()) {
return null;
}
return try context.createFunction(js_value);
}
pub fn isNull(self: Object) bool {
return self.js_obj.toValue().isNull();
}
pub fn isUndefined(self: Object) bool {
return self.js_obj.toValue().isUndefined();
}
pub fn triState(self: Object, comptime Struct: type, comptime name: []const u8, comptime T: type) !TriState(T) {
if (self.isNull()) {
return .{ .null = {} };
}
if (self.isUndefined()) {
return .{ .undefined = {} };
}
return .{ .value = try self.toZig(Struct, name, T) };
}
pub fn isNullOrUndefined(self: Object) bool {
return self.js_obj.toValue().isNullOrUndefined();
}
pub fn nameIterator(self: Object) js.ValueIterator {
const context = self.context;
const js_obj = self.js_obj;
const array = js_obj.getPropertyNames(context.v8_context);
const count = array.length();
return .{
.count = count,
.context = context,
.js_obj = array.castTo(v8.Object),
};
}
pub fn toZig(self: Object, comptime Struct: type, comptime name: []const u8, comptime T: type) !T {
const named_function = comptime Caller.NamedFunction.init(Struct, name);
return self.context.jsValueToZig(named_function, T, self.js_obj.toValue());
}
pub fn TriState(comptime T: type) type {
return union(enum) {
null: void,
undefined: void,
value: T,
};
}

View File

@@ -1,21 +0,0 @@
const js = @import("js.zig");
const v8 = js.v8;
const Platform = @This();
inner: v8.Platform,
pub fn init() !Platform {
if (v8.initV8ICU() == false) {
return error.FailedToInitializeICU;
}
const platform = v8.Platform.initDefault(0, true);
v8.initV8Platform(platform);
v8.initV8();
return .{ .inner = platform };
}
pub fn deinit(self: Platform) void {
_ = v8.deinitV8();
v8.deinitV8Platform();
self.inner.deinit();
}

View File

@@ -1,25 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
// This only exists so that we know whether a function wants the opaque
// JS argument (js.Object), or if it wants the receiver as an opaque
// value.
// js.Object is normally used when a method wants an opaque JS object
// that it'll pass into a callback.
// This is used when the function wants to do advanced manipulation
// of the v8.Object bound to the instance. For example, postAttach is an
// example of using This.
const This = @This();
obj: js.Object,
pub fn setIndex(self: This, index: u32, value: anytype, opts: js.Object.SetOpts) !void {
return self.obj.setIndex(index, value, opts);
}
pub fn set(self: This, key: []const u8, value: anytype, opts: js.Object.SetOpts) !void {
return self.obj.set(key, value, opts);
}

View File

@@ -1,64 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
const TryCatch = @This();
inner: v8.TryCatch,
context: *const js.Context,
pub fn init(self: *TryCatch, context: *const js.Context) void {
self.context = context;
self.inner.init(context.isolate);
}
pub fn hasCaught(self: TryCatch) bool {
return self.inner.hasCaught();
}
// the caller needs to deinit the string returned
pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 {
const msg = self.inner.getException() orelse return null;
return try self.context.valueToString(msg, .{ .allocator = allocator });
}
// the caller needs to deinit the string returned
pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
const context = self.context;
const s = self.inner.getStackTrace(context.v8_context) orelse return null;
return try context.valueToString(s, .{ .allocator = allocator });
}
// the caller needs to deinit the string returned
pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 {
const context = self.context;
const msg = self.inner.getMessage() orelse return null;
const sl = msg.getSourceLine(context.v8_context) orelse return null;
return try context.jsStringToZig(sl, .{ .allocator = allocator });
}
pub fn sourceLineNumber(self: TryCatch) ?u32 {
const context = self.context;
const msg = self.inner.getMessage() orelse return null;
return msg.getLineNumber(context.v8_context);
}
// a shorthand method to return either the entire stack message
// or just the exception message
// - in Debug mode return the stack if available
// - otherwise return the exception if available
// the caller needs to deinit the string returned
pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 {
if (comptime @import("builtin").mode == .Debug) {
if (try self.stack(allocator)) |msg| {
return msg;
}
}
return try self.exception(allocator);
}
pub fn deinit(self: *TryCatch) void {
self.inner.deinit();
}

View File

@@ -1,504 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
pub const v8 = @import("v8");
const types = @import("types.zig");
const log = @import("../../log.zig");
const Page = @import("../page.zig").Page;
const Allocator = std.mem.Allocator;
pub const Env = @import("Env.zig");
pub const ExecutionWorld = @import("ExecutionWorld.zig");
pub const Context = @import("Context.zig");
pub const Inspector = @import("Inspector.zig");
// TODO: Is "This" really necessary?
pub const This = @import("This.zig");
pub const Object = @import("Object.zig");
pub const TryCatch = @import("TryCatch.zig");
pub const Function = @import("Function.zig");
const Caller = @import("Caller.zig");
const NamedFunction = Context.NamedFunction;
// If a function returns a []i32, should that map to a plain-old
// JavaScript array, or a Int32Array? It's ambiguous. By default, we'll
// map arrays/slices to the JavaScript arrays. If you want a TypedArray
// wrap it in this.
// Also, this type has nothing to do with the Env. But we place it here
// for consistency. Want a callback? Env.Callback. Want a JsObject?
// Env.JsObject. Want a TypedArray? Env.TypedArray.
pub fn TypedArray(comptime T: type) type {
return struct {
pub const _TYPED_ARRAY_ID_KLUDGE = true;
values: []const T,
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
return .{ .values = try allocator.dupe(T, self.values) };
}
};
}
pub const PromiseResolver = struct {
context: *Context,
resolver: v8.PromiseResolver,
pub fn promise(self: PromiseResolver) Promise {
return self.resolver.getPromise();
}
pub fn resolve(self: PromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
// resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.resolve(context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToResolvePromise;
}
}
pub fn reject(self: PromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
// resolver.reject will return null if the promise isn't pending
const ok = self.resolver.reject(context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToRejectPromise;
}
}
};
pub const PersistentPromiseResolver = struct {
context: *Context,
resolver: v8.Persistent(v8.PromiseResolver),
pub fn deinit(self: *PersistentPromiseResolver) void {
self.resolver.deinit();
}
pub fn promise(self: PersistentPromiseResolver) Promise {
return self.resolver.castToPromiseResolver().getPromise();
}
pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
// resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToResolvePromise;
}
}
pub fn reject(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
// resolver.reject will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToRejectPromise;
}
}
};
pub const Promise = v8.Promise;
// When doing jsValueToZig, string ([]const u8) are managed by the
// call_arena. That means that if the API wants to persist the string
// (which is relatively common), it needs to dupe it again.
// If the parameter is an Env.String rather than a []const u8, then
// the page's arena will be used (rather than the call arena).
pub const String = struct {
string: []const u8,
};
pub const Exception = struct {
inner: v8.Value,
context: *const Context,
// the caller needs to deinit the string returned
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.inner, .{ .allocator = allocator });
}
};
pub const Value = struct {
value: v8.Value,
context: *const Context,
// the caller needs to deinit the string returned
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.value, .{ .allocator = allocator });
}
pub fn fromJson(ctx: *Context, json: []const u8) !Value {
const json_string = v8.String.initUtf8(ctx.isolate, json);
const value = try v8.Json.parse(ctx.v8_context, json_string);
return Value{ .context = ctx, .value = value };
}
};
pub const ValueIterator = struct {
count: u32,
idx: u32 = 0,
js_obj: v8.Object,
context: *const Context,
pub fn next(self: *ValueIterator) !?Value {
const idx = self.idx;
if (idx == self.count) {
return null;
}
self.idx += 1;
const context = self.context;
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx);
return context.createValue(js_val);
}
};
pub fn UndefinedOr(comptime T: type) type {
return union(enum) {
undefined: void,
value: T,
};
}
// An interface for types that want to have their jsScopeEnd function be
// called when the call context ends
const CallScopeEndCallback = struct {
ptr: *anyopaque,
callScopeEndFn: *const fn (ptr: *anyopaque) void,
fn init(ptr: anytype) CallScopeEndCallback {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn callScopeEnd(pointer: *anyopaque) void {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.pointer.child.jsCallScopeEnd(self);
}
};
return .{
.ptr = ptr,
.callScopeEndFn = gen.callScopeEnd,
};
}
pub fn callScopeEnd(self: CallScopeEndCallback) void {
self.callScopeEndFn(self.ptr);
}
};
// Callback called on global's property missing.
// Return true to intercept the execution or false to let the call
// continue the chain.
pub const GlobalMissingCallback = struct {
ptr: *anyopaque,
missingFn: *const fn (ptr: *anyopaque, name: []const u8, ctx: *Context) bool,
pub fn init(ptr: anytype) GlobalMissingCallback {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn missing(pointer: *anyopaque, name: []const u8, ctx: *Context) bool {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.pointer.child.missing(self, name, ctx);
}
};
return .{
.ptr = ptr,
.missingFn = gen.missing,
};
}
pub fn missing(self: GlobalMissingCallback, name: []const u8, ctx: *Context) bool {
return self.missingFn(self.ptr, name, ctx);
}
};
// Attributes that return a primitive type are setup directly on the
// FunctionTemplate when the Env is setup. More complex types need a v8.Context
// and cannot be set directly on the FunctionTemplate.
// We default to saying types are primitives because that's mostly what
// we have. If we add a new complex type that isn't explictly handled here,
// we'll get a compiler error in simpleZigValueToJs, and can then explicitly
// add the type here.
pub fn isComplexAttributeType(ti: std.builtin.Type) bool {
return switch (ti) {
.array => true,
else => false,
};
}
// These are simple types that we can convert to JS with only an isolate. This
// is separated from the Caller's zigValueToJs to make it available when we
// don't have a caller (i.e., when setting static attributes on types)
pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool) if (fail) v8.Value else ?v8.Value {
switch (@typeInfo(@TypeOf(value))) {
.void => return v8.initUndefined(isolate).toValue(),
.null => return v8.initNull(isolate).toValue(),
.bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)),
.int => |n| switch (n.signedness) {
.signed => {
if (value >= -2_147_483_648 and value <= 2_147_483_647) {
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
}
if (comptime n.bits <= 64) {
return v8.getValue(v8.BigInt.initI64(isolate, @intCast(value)));
}
@compileError(@typeName(value) ++ " is not supported");
},
.unsigned => {
if (value <= 4_294_967_295) {
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
}
if (comptime n.bits <= 64) {
return v8.getValue(v8.BigInt.initU64(isolate, @intCast(value)));
}
@compileError(@typeName(value) ++ " is not supported");
},
},
.comptime_int => {
if (value >= 0) {
if (value <= 4_294_967_295) {
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
}
return v8.BigInt.initU64(isolate, @intCast(value)).toValue();
}
if (value >= -2_147_483_648) {
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
}
return v8.BigInt.initI64(isolate, @intCast(value)).toValue();
},
.comptime_float => return v8.Number.init(isolate, value).toValue(),
.float => |f| switch (f.bits) {
64 => return v8.Number.init(isolate, value).toValue(),
32 => return v8.Number.init(isolate, @floatCast(value)).toValue(),
else => @compileError(@typeName(value) ++ " is not supported"),
},
.pointer => |ptr| {
if (ptr.size == .slice and ptr.child == u8) {
return v8.String.initUtf8(isolate, value).toValue();
}
if (ptr.size == .one) {
const one_info = @typeInfo(ptr.child);
if (one_info == .array and one_info.array.child == u8) {
return v8.String.initUtf8(isolate, value).toValue();
}
}
},
.array => return simpleZigValueToJs(isolate, &value, fail),
.optional => {
if (value) |v| {
return simpleZigValueToJs(isolate, v, fail);
}
return v8.initNull(isolate).toValue();
},
.@"struct" => {
const T = @TypeOf(value);
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
const values = value.values;
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
const len = values.len;
const bits = switch (@typeInfo(value_type)) {
.int => |n| n.bits,
.float => |f| f.bits,
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
};
var array_buffer: v8.ArrayBuffer = undefined;
if (len == 0) {
array_buffer = v8.ArrayBuffer.init(isolate, 0);
} else {
const buffer_len = len * bits / 8;
const backing_store = v8.BackingStore.init(isolate, buffer_len);
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
}
switch (@typeInfo(value_type)) {
.int => |n| switch (n.signedness) {
.unsigned => switch (n.bits) {
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
.signed => switch (n.bits) {
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
},
.float => |f| switch (f.bits) {
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
else => {},
}
// We normally don't fail in this function unless fail == true
// but this can never be valid.
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
}
},
.@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail),
.@"enum" => {
const T = @TypeOf(value);
if (@hasDecl(T, "toString")) {
// This should be deprecated in favor of the ENUM_JS_USE_TAG.
return simpleZigValueToJs(isolate, value.toString(), fail);
}
if (@hasDecl(T, "ENUM_JS_USE_TAG")) {
return simpleZigValueToJs(isolate, @tagName(value), fail);
}
},
else => {},
}
if (fail) {
@compileError("Unsupported Zig type " ++ @typeName(@TypeOf(value)));
}
return null;
}
pub fn _createException(isolate: v8.Isolate, msg: []const u8) v8.Value {
return v8.Exception.initError(v8.String.initUtf8(isolate, msg));
}
pub fn classNameForStruct(comptime Struct: type) []const u8 {
if (@hasDecl(Struct, "js_name")) {
return Struct.js_name;
}
@setEvalBranchQuota(10_000);
const full_name = @typeName(Struct);
const last = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse return full_name;
return full_name[last + 1 ..];
}
// When we return a Zig object to V8, we put it on the heap and pass it into
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
// function parameter, we know what type it _should_ be. Above, in Caller.method
// (for example), we know all the parameter types. So if a Zig function takes
// a single parameter (its receiver), we know what that type is.
//
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
// to the parameter type:
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
//
// But there are 2 reasons we can't do that.
//
// == Reason 1 ==
// The JS code might pass the wrong type:
//
// var cat = new Cat();
// cat.setOwner(new Cat());
//
// The zig _setOwner method expects the 2nd parameter to be an *Owner, but
// the JS code passed a *Cat.
//
// To solve this issue, we tag every returned value so that we can check what
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
//
// == Reason 2 ==
// Because of prototype inheritance, even "correct" code can be a challenge. For
// example, say the above JavaScript is fixed:
//
// var cat = new Cat();
// cat.setOwner(new Owner("Leto"));
//
// The issue is that setOwner might not expect an *Owner, but rather a
// *Person, which is the prototype for Owner. Now our Zig code is expecting
// a *Person, but it was (correctly) given an *Owner.
// For this reason, we also store the prototype's type index.
//
// One of the prototype mechanisms that we support is via composition. Owner
// can have a "proto: *Person" field. For this reason, we also store the offset
// of the proto field, so that, given an intFromPtr(*Owner) we can access its
// proto field.
//
// The other prototype mechanism that we support is for netsurf, where we just
// cast one type to another. In this case, we'll store an offset of -1 (as a
// sentinel to indicate that we should just cast directly).
pub const TaggedAnyOpaque = struct {
// The type of object this is. The type is captured as an index, which
// corresponds to both a field in TYPE_LOOKUP and the index of
// PROTOTYPE_TABLE
index: u16,
// Ptr to the Zig instance. Between the context where it's called (i.e.
// we have the comptime parameter info for all functions), and the index field
// we can figure out what type this is.
ptr: *anyopaque,
// When we're asked to describe an object via the Inspector, we _must_ include
// the proper subtype (and description) fields in the returned JSON.
// V8 will give us a Value and ask us for the subtype. From the v8.Value we
// can get a v8.Object, and from the v8.Object, we can get out TaggedAnyOpaque
// which is where we store the subtype.
subtype: ?types.Sub,
};
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
// included (e.g. in the wpt build).
// This is called from V8. Whenever the v8 inspector has to describe a value
// it'll call this function to gets its [optional] subtype - which, from V8's
// point of view, is an arbitrary string.
pub export fn v8_inspector__Client__IMPL__valueSubtype(
_: *v8.c.InspectorClientImpl,
c_value: *const v8.C_Value,
) callconv(.c) [*c]const u8 {
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
return if (external_entry.subtype) |st| @tagName(st) else null;
}
// Same as valueSubType above, but for the optional description field.
// From what I can tell, some drivers _need_ the description field to be
// present, even if it's empty. So if we have a subType for the value, we'll
// put an empty description.
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
_: *v8.c.InspectorClientImpl,
v8_context: *const v8.C_Context,
c_value: *const v8.C_Value,
) callconv(.c) [*c]const u8 {
_ = v8_context;
// We _must_ include a non-null description in order for the subtype value
// to be included. Besides that, I don't know if the value has any meaning
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
return if (external_entry.subtype == null) null else "";
}

View File

@@ -1,184 +0,0 @@
const std = @import("std");
const generate = @import("generate.zig");
const Interfaces = generate.Tuple(.{
@import("../crypto/crypto.zig").Crypto,
@import("../console/console.zig").Console,
@import("../css/css.zig").Interfaces,
@import("../cssom/cssom.zig").Interfaces,
@import("../dom/dom.zig").Interfaces,
@import("../dom/shadow_root.zig").ShadowRoot,
@import("../encoding/encoding.zig").Interfaces,
@import("../events/event.zig").Interfaces,
@import("../html/html.zig").Interfaces,
@import("../iterator/iterator.zig").Interfaces,
@import("../storage/storage.zig").Interfaces,
@import("../url/url.zig").Interfaces,
@import("../xhr/xhr.zig").Interfaces,
@import("../navigation/root.zig").Interfaces,
@import("../xhr/form_data.zig").Interfaces,
@import("../xhr/File.zig"),
@import("../xmlserializer/xmlserializer.zig").Interfaces,
@import("../fetch/fetch.zig").Interfaces,
@import("../streams/streams.zig").Interfaces,
});
pub const Types = @typeInfo(Interfaces).@"struct".fields;
// Imagine we have a type Cat which has a getter:
//
// fn get_owner(self: *Cat) *Owner {
// return self.owner;
// }
//
// When we execute caller.getter, we'll end up doing something like:
// const res = @call(.auto, Cat.get_owner, .{cat_instance});
//
// How do we turn `res`, which is an *Owner, into something we can return
// to v8? We need the ObjectTemplate associated with Owner. How do we
// get that? Well, we store all the ObjectTemplates in an array that's
// tied to env. So we do something like:
//
// env.templates[index_of_owner].initInstance(...);
//
// But how do we get that `index_of_owner`? `Lookup` is a struct
// that looks like:
//
// const Lookup = struct {
// comptime cat: usize = 0,
// comptime owner: usize = 1,
// ...
// }
//
// So to get the template index of `owner`, we can do:
//
// const index_id = @field(type_lookup, @typeName(@TypeOf(res));
//
pub const Lookup = blk: {
var fields: [Types.len]std.builtin.Type.StructField = undefined;
for (Types, 0..) |s, i| {
// This prototype type check has nothing to do with building our
// Lookup. But we put it here, early, so that the rest of the
// code doesn't have to worry about checking if Struct.prototype is
// a pointer.
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype") and @typeInfo(Struct.prototype) != .pointer) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s} must be a pointer", .{ @typeName(Struct.prototype), @typeName(Struct) }));
}
fields[i] = .{
.name = @typeName(Receiver(Struct)),
.type = usize,
.is_comptime = true,
.alignment = @alignOf(usize),
.default_value_ptr = &i,
};
}
break :blk @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.is_tuple = false,
.fields = &fields,
} });
};
pub const LOOKUP = Lookup{};
// Creates a list where the index of a type contains its prototype index
// const Animal = struct{};
// const Cat = struct{
// pub const prototype = *Animal;
// };
//
// Would create an array: [0, 0]
// Animal, at index, 0, has no prototype, so we set it to itself
// Cat, at index 1, has an Animal prototype, so we set it to 0.
//
// When we're trying to pass an argument to a Zig function, we'll know the
// target type (the function parameter type), and we'll have a
// TaggedAnyOpaque which will have the index of the type of that parameter.
// We'll use the PROTOTYPE_TABLE to see if the TaggedAnyType should be
// cast to a prototype.
pub const PROTOTYPE_TABLE = blk: {
var table: [Types.len]u16 = undefined;
for (Types, 0..) |s, i| {
var prototype_index = i;
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) {
const TI = @typeInfo(Struct.prototype);
const proto_name = @typeName(Receiver(TI.pointer.child));
prototype_index = @field(LOOKUP, proto_name);
}
table[i] = prototype_index;
}
break :blk table;
};
// This is essentially meta data for each type. Each is stored in env.meta_lookup
// The index for a type can be retrieved via:
// const index = @field(TYPE_LOOKUP, @typeName(Receiver(Struct)));
// const meta = env.meta_lookup[index];
pub const Meta = struct {
// Every type is given a unique index. That index is used to lookup various
// things, i.e. the prototype chain.
index: u16,
// We store the type's subtype here, so that when we create an instance of
// the type, and bind it to JavaScript, we can store the subtype along with
// the created TaggedAnyOpaque.s
subtype: ?Sub,
// If this type has composition-based prototype, represents the byte-offset
// from ptr where the `proto` field is located. A negative offsets is used
// to indicate that the prototype field is behind a pointer.
proto_offset: i32,
};
pub const Sub = enum {
@"error",
array,
arraybuffer,
dataview,
date,
generator,
iterator,
map,
node,
promise,
proxy,
regexp,
set,
typedarray,
wasmvalue,
weakmap,
weakset,
webassemblymemory,
};
// When we map a Zig instance into a JsObject, we'll normally store the a
// TaggedAnyOpaque (TAO) inside of the JsObject's internal field. This requires
// ensuring that the instance template has an InternalFieldCount of 1. However,
// for empty objects, we don't need to store the TAO, because we can't just cast
// one empty object to another, so for those, as an optimization, we do not set
// the InternalFieldCount.
pub fn isEmpty(comptime T: type) bool {
return @typeInfo(T) != .@"opaque" and @sizeOf(T) == 0 and @hasDecl(T, "js_legacy_factory") == false;
}
// If we have a struct:
// const Cat = struct {
// pub fn meow(self: *Cat) void { ... }
// }
// Then obviously, the receiver of its methods are going to be a *Cat (or *const Cat)
//
// However, we can also do:
// const Cat = struct {
// pub const Self = OtherImpl;
// pub fn meow(self: *OtherImpl) void { ... }
// }
// In which case, as we see above, the receiver is derived from the Self declaration
pub fn Receiver(comptime Struct: type) type {
return if (@hasDecl(Struct, "Self")) Struct.Self else Struct;
}

View File

@@ -26,9 +26,14 @@ const c = @cImport({
@cInclude("mimalloc.h"); @cInclude("mimalloc.h");
}); });
const Error = error{
HeapNotNull,
HeapNull,
};
var heap: ?*c.mi_heap_t = null; var heap: ?*c.mi_heap_t = null;
pub fn create() void { pub fn create() Error!void {
std.debug.assert(heap == null); std.debug.assert(heap == null);
heap = c.mi_heap_new(); heap = c.mi_heap_new();
std.debug.assert(heap != null); std.debug.assert(heap != null);
@@ -40,45 +45,6 @@ pub fn destroy() void {
heap = null; heap = null;
} }
pub fn getRSS() i64 {
if (@import("builtin").mode != .Debug) {
// just don't trust my implementation, plus a caller might not know
// that this requires parsing some unstructured data
@compileError("Only available in debug builds");
}
var buf: [1024 * 8]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var writer = std.Io.Writer.Allocating.init(fba.allocator());
c.mi_stats_print_out(struct {
fn print(msg: [*c]const u8, data: ?*anyopaque) callconv(.c) void {
const w: *std.Io.Writer = @ptrCast(@alignCast(data.?));
w.writeAll(std.mem.span(msg)) catch |err| {
std.debug.print("Failed to write mimalloc data: {}\n", .{err});
};
}
}.print, &writer.writer);
const data = writer.written();
const index = std.mem.indexOf(u8, data, "rss: ") orelse return -1;
const sep = std.mem.indexOfScalarPos(u8, data, index + 5, ' ') orelse return -2;
const value = std.fmt.parseFloat(f64, data[index + 5 .. sep]) catch return -3;
const unit = data[sep + 1 ..];
if (std.mem.startsWith(u8, unit, "KiB,")) {
return @as(i64, @intFromFloat(value)) * 1024;
}
if (std.mem.startsWith(u8, unit, "MiB,")) {
return @as(i64, @intFromFloat(value)) * 1024 * 1024;
}
if (std.mem.startsWith(u8, unit, "GiB,")) {
return @as(i64, @intFromFloat(value)) * 1024 * 1024 * 1024;
}
return -4;
}
pub export fn m_alloc(size: usize) callconv(.c) ?*anyopaque { pub export fn m_alloc(size: usize) callconv(.c) ?*anyopaque {
std.debug.assert(heap != null); std.debug.assert(heap != null);
return c.mi_heap_malloc(heap.?, size); return c.mi_heap_malloc(heap.?, size);

View File

@@ -17,19 +17,18 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
pub const Mime = struct { pub const Mime = struct {
content_type: ContentType, content_type: ContentType,
params: []const u8 = "", params: []const u8 = "",
// IANA defines max. charset value length as 40. charset: ?[:0]const u8 = null,
// We keep 41 for null-termination since HTML parser expects in this format.
charset: [41]u8 = default_charset,
/// String "UTF-8" continued by null characters. pub const unknown = Mime{
pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36; .params = "",
.charset = null,
/// Mime with unknown Content-Type, empty params and empty charset. .content_type = .{ .unknown = {} },
pub const unknown = Mime{ .content_type = .{ .unknown = {} } }; };
pub const ContentTypeEnum = enum { pub const ContentTypeEnum = enum {
text_xml, text_xml,
@@ -53,34 +52,6 @@ pub const Mime = struct {
other: struct { type: []const u8, sub_type: []const u8 }, other: struct { type: []const u8, sub_type: []const u8 },
}; };
/// Returns the null-terminated charset value.
pub fn charsetString(mime: *const Mime) [:0]const u8 {
return @ptrCast(&mime.charset);
}
/// Removes quotes of value if quotes are given.
///
/// Currently we don't validate the charset.
/// See section 2.3 Naming Requirements:
/// https://datatracker.ietf.org/doc/rfc2978/
fn parseCharset(value: []const u8) error{ CharsetTooBig, Invalid }![]const u8 {
// Cannot be larger than 40.
// https://datatracker.ietf.org/doc/rfc2978/
if (value.len > 40) return error.CharsetTooBig;
// If the first char is a quote, look for a pair.
if (value[0] == '"') {
if (value.len < 3 or value[value.len - 1] != '"') {
return error.Invalid;
}
return value[1 .. value.len - 1];
}
// No quotes.
return value;
}
pub fn parse(input: []u8) !Mime { pub fn parse(input: []u8) !Mime {
if (input.len > 255) { if (input.len > 255) {
return error.TooBig; return error.TooBig;
@@ -98,7 +69,7 @@ pub const Mime = struct {
const params = trimLeft(normalized[type_len..]); const params = trimLeft(normalized[type_len..]);
var charset: [41]u8 = undefined; var charset: ?[:0]const u8 = null;
var it = std.mem.splitScalar(u8, params, ';'); var it = std.mem.splitScalar(u8, params, ';');
while (it.next()) |attr| { while (it.next()) |attr| {
@@ -116,14 +87,35 @@ pub const Mime = struct {
switch (attribute_name) { switch (attribute_name) {
.charset => { .charset => {
// We used to have a proper value parser, but we currently
// only care about the charset attribute, plus only about
// the UTF-8 value. It's a lot easier to do it this way,
// and it doesn't require an allocation to (a) unescape the
// value or (b) ensure the correct lifetime.
if (value.len == 0) { if (value.len == 0) {
break; break;
} }
var attribute_value = value;
if (value[0] == '"') {
if (value.len < 3 or value[value.len - 1] != '"') {
return error.Invalid;
}
attribute_value = value[1 .. value.len - 1];
}
const attribute_value = try parseCharset(value); if (std.ascii.eqlIgnoreCase(attribute_value, "utf-8")) {
@memcpy(charset[0..attribute_value.len], attribute_value); charset = "UTF-8";
// Null-terminate right after attribute value. } else if (std.ascii.eqlIgnoreCase(attribute_value, "iso-8859-1")) {
charset[attribute_value.len] = 0; charset = "ISO-8859-1";
} else {
// we only care about null (which we default to UTF-8)
// or UTF-8. If this is actually set (i.e. not null)
// and isn't UTF-8, we'll just put a dummy value. If
// we want to capture the actual value, we'll need to
// dupe/allocate it. Since, for now, we don't need that
// we can avoid the allocation.
charset = "lightpanda:UNSUPPORTED";
}
}, },
} }
} }
@@ -371,33 +363,21 @@ test "Mime: parse charset" {
try expect(.{ try expect(.{
.content_type = .{ .text_xml = {} }, .content_type = .{ .text_xml = {} },
.charset = "utf-8", .charset = "UTF-8",
.params = "charset=utf-8", .params = "charset=utf-8",
}, "text/xml; charset=utf-8"); }, "text/xml; charset=utf-8");
try expect(.{ try expect(.{
.content_type = .{ .text_xml = {} }, .content_type = .{ .text_xml = {} },
.charset = "utf-8", .charset = "UTF-8",
.params = "charset=\"utf-8\"", .params = "charset=\"utf-8\"",
}, "text/xml;charset=\"UTF-8\""); }, "text/xml;charset=\"utf-8\"");
try expect(.{
.content_type = .{ .text_html = {} },
.charset = "iso-8859-1",
.params = "charset=\"iso-8859-1\"",
}, "text/html; charset=\"iso-8859-1\"");
try expect(.{
.content_type = .{ .text_html = {} },
.charset = "iso-8859-1",
.params = "charset=\"iso-8859-1\"",
}, "text/html; charset=\"ISO-8859-1\"");
try expect(.{ try expect(.{
.content_type = .{ .text_xml = {} }, .content_type = .{ .text_xml = {} },
.charset = "custom-non-standard-charset-value", .charset = "lightpanda:UNSUPPORTED",
.params = "charset=\"custom-non-standard-charset-value\"", .params = "charset=\"\\\\ \\\" \"",
}, "text/xml;charset=\"custom-non-standard-charset-value\""); }, "text/xml;charset=\"\\\\ \\\" \" ");
} }
test "Mime: isHTML" { test "Mime: isHTML" {
@@ -510,10 +490,8 @@ fn expect(expected: Expectation, input: []const u8) !void {
try testing.expectEqual(expected.params, actual.params); try testing.expectEqual(expected.params, actual.params);
if (expected.charset) |ec| { if (expected.charset) |ec| {
// We remove the null characters for testing purposes here. try testing.expectEqual(ec, actual.charset.?);
try testing.expectEqual(ec, actual.charsetString()[0..ec.len]);
} else { } else {
const m: Mime = .unknown; try testing.expectEqual(null, actual.charset);
try testing.expectEqual(m.charsetString(), actual.charsetString());
} }
} }

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