mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-24 05:33:16 +00:00
Merge branch 'main' into mcp
This commit is contained in:
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
|||||||
zig-v8:
|
zig-v8:
|
||||||
description: 'zig v8 version to install'
|
description: 'zig v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
default: 'v0.2.9'
|
default: 'v0.3.1'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
2
.github/workflows/e2e-integration-test.yml
vendored
2
.github/workflows/e2e-integration-test.yml
vendored
@@ -63,6 +63,6 @@ jobs:
|
|||||||
|
|
||||||
- name: run end to end integration tests
|
- name: run end to end integration tests
|
||||||
run: |
|
run: |
|
||||||
./lightpanda serve & echo $! > LPD.pid
|
./lightpanda serve --log_level error & echo $! > LPD.pid
|
||||||
go run integration/main.go
|
go run integration/main.go
|
||||||
kill `cat LPD.pid`
|
kill `cat LPD.pid`
|
||||||
|
|||||||
88
.github/workflows/wpt.yml
vendored
88
.github/workflows/wpt.yml
vendored
@@ -15,11 +15,11 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
wpt:
|
wpt-build-release:
|
||||||
name: web platform tests json output
|
name: zig build release
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 90
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -30,11 +30,85 @@ jobs:
|
|||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: build wpt
|
- name: zig build release
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -- version
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
|
- name: upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
path: |
|
||||||
|
zig-out/bin/lightpanda
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
wpt-build-runner:
|
||||||
|
name: build wpt runner
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 90
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
repository: 'lightpanda-io/demo'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
cd ./wptrunner
|
||||||
|
CGO_ENABLED=0 go build
|
||||||
|
|
||||||
|
- name: upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: wptrunner
|
||||||
|
path: |
|
||||||
|
wptrunner/wptrunner
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
run-wpt:
|
||||||
|
name: web platform tests json output
|
||||||
|
needs:
|
||||||
|
- wpt-build-release
|
||||||
|
- wpt-build-runner
|
||||||
|
|
||||||
|
# use a self host runner.
|
||||||
|
runs-on: lpd-bench-hetzner
|
||||||
|
timeout-minutes: 90
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: fork
|
||||||
|
repository: 'lightpanda-io/wpt'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# The hosts are configured manually on the self host runner.
|
||||||
|
# - name: create custom hosts
|
||||||
|
# run: ./wpt make-hosts-file | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
|
- name: generate manifest
|
||||||
|
run: ./wpt manifest
|
||||||
|
|
||||||
|
- name: download lightpanda release
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
- name: download wptrunner
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: wptrunner
|
||||||
|
|
||||||
|
- run: chmod a+x ./wptrunner
|
||||||
|
|
||||||
- name: run test with json output
|
- name: run test with json output
|
||||||
run: zig-out/bin/lightpanda-wpt --json > wpt.json
|
run: |
|
||||||
|
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
||||||
|
sleep 10s
|
||||||
|
./wptrunner -lpd-path ./lightpanda -json -concurrency 1 > wpt.json
|
||||||
|
kill `cat WPT.pid`
|
||||||
|
|
||||||
- name: write commit
|
- name: write commit
|
||||||
run: |
|
run: |
|
||||||
@@ -51,7 +125,7 @@ jobs:
|
|||||||
|
|
||||||
perf-fmt:
|
perf-fmt:
|
||||||
name: perf-fmt
|
name: perf-fmt
|
||||||
needs: wpt
|
needs: run-wpt
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|||||||
15
.gitmodules
vendored
15
.gitmodules
vendored
@@ -1,15 +0,0 @@
|
|||||||
[submodule "tests/wpt"]
|
|
||||||
path = tests/wpt
|
|
||||||
url = https://github.com/lightpanda-io/wpt
|
|
||||||
[submodule "vendor/nghttp2"]
|
|
||||||
path = vendor/nghttp2
|
|
||||||
url = https://github.com/nghttp2/nghttp2.git
|
|
||||||
[submodule "vendor/zlib"]
|
|
||||||
path = vendor/zlib
|
|
||||||
url = https://github.com/madler/zlib.git
|
|
||||||
[submodule "vendor/curl"]
|
|
||||||
path = vendor/curl
|
|
||||||
url = https://github.com/curl/curl.git
|
|
||||||
[submodule "vendor/brotli"]
|
|
||||||
path = vendor/brotli
|
|
||||||
url = https://github.com/google/brotli
|
|
||||||
@@ -3,7 +3,7 @@ FROM debian:stable-slim
|
|||||||
ARG MINISIG=0.12
|
ARG MINISIG=0.12
|
||||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||||
ARG V8=14.0.365.4
|
ARG V8=14.0.365.4
|
||||||
ARG ZIG_V8=v0.2.9
|
ARG ZIG_V8=v0.3.1
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update -yq && \
|
RUN apt-get update -yq && \
|
||||||
|
|||||||
18
Makefile
18
Makefile
@@ -47,7 +47,7 @@ help:
|
|||||||
|
|
||||||
# $(ZIG) commands
|
# $(ZIG) commands
|
||||||
# ------------
|
# ------------
|
||||||
.PHONY: build build-v8-snapshot build-dev run run-release shell test bench wpt data end2end
|
.PHONY: build build-v8-snapshot build-dev run run-release shell test bench data end2end
|
||||||
|
|
||||||
## Build v8 snapshot
|
## Build v8 snapshot
|
||||||
build-v8-snapshot:
|
build-v8-snapshot:
|
||||||
@@ -82,15 +82,6 @@ shell:
|
|||||||
@printf "\033[36mBuilding shell...\033[0m\n"
|
@printf "\033[36mBuilding shell...\033[0m\n"
|
||||||
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
|
|
||||||
## Run WPT tests
|
|
||||||
wpt:
|
|
||||||
@printf "\033[36mBuilding wpt...\033[0m\n"
|
|
||||||
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
|
||||||
|
|
||||||
wpt-summary:
|
|
||||||
@printf "\033[36mBuilding wpt...\033[0m\n"
|
|
||||||
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
|
||||||
|
|
||||||
## Test - `grep` is used to filter out the huge compile command on build
|
## Test - `grep` is used to filter out the huge compile command on build
|
||||||
ifeq ($(OS), macos)
|
ifeq ($(OS), macos)
|
||||||
test:
|
test:
|
||||||
@@ -111,13 +102,8 @@ end2end:
|
|||||||
# ------------
|
# ------------
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
|
|
||||||
## Install and build dependencies for release
|
install: build
|
||||||
install: install-submodule
|
|
||||||
|
|
||||||
data:
|
data:
|
||||||
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
||||||
|
|
||||||
## Init and update git submodule
|
|
||||||
install-submodule:
|
|
||||||
@git submodule init && \
|
|
||||||
git submodule update
|
|
||||||
|
|||||||
76
README.md
76
README.md
@@ -220,18 +220,6 @@ For **MacOS**, you need cmake and [Rust](https://rust-lang.org/tools/install/).
|
|||||||
brew install cmake
|
brew install cmake
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Git submodules
|
|
||||||
|
|
||||||
The project uses git submodules for dependencies.
|
|
||||||
|
|
||||||
To init or update the submodules in the `vendor/` directory:
|
|
||||||
|
|
||||||
```
|
|
||||||
make install-submodule
|
|
||||||
```
|
|
||||||
|
|
||||||
This is an alias for `git submodule init && git submodule update`.
|
|
||||||
|
|
||||||
### Build and run
|
### Build and run
|
||||||
|
|
||||||
You an build the entire browser with `make build` or `make build-dev` for debug
|
You an build the entire browser with `make build` or `make build-dev` for debug
|
||||||
@@ -281,35 +269,75 @@ make end2end
|
|||||||
Lightpanda is tested against the standardized [Web Platform
|
Lightpanda is tested against the standardized [Web Platform
|
||||||
Tests](https://web-platform-tests.org/).
|
Tests](https://web-platform-tests.org/).
|
||||||
|
|
||||||
The relevant tests cases are committed in a [dedicated repository](https://github.com/lightpanda-io/wpt) which is fetched by the `make install-submodule` command.
|
We use [a fork](https://github.com/lightpanda-io/wpt/tree/fork) including a custom
|
||||||
|
[`testharnessreport.js`](https://github.com/lightpanda-io/wpt/commit/01a3115c076a3ad0c84849dbbf77a6e3d199c56f).
|
||||||
All the tests cases executed are located in the `tests/wpt` sub-directory.
|
|
||||||
|
|
||||||
For reference, you can easily execute a WPT test case with your browser via
|
For reference, you can easily execute a WPT test case with your browser via
|
||||||
[wpt.live](https://wpt.live).
|
[wpt.live](https://wpt.live).
|
||||||
|
|
||||||
|
#### Configure WPT HTTP server
|
||||||
|
|
||||||
|
To run the test, you must clone the repository, configure the custom hosts and generate the
|
||||||
|
`MANIFEST.json` file.
|
||||||
|
|
||||||
|
Clone the repository with the `fork` branch.
|
||||||
|
```
|
||||||
|
git clone -b fork --depth=1 git@github.com:lightpanda-io/wpt.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter into the `wpt/` dir.
|
||||||
|
|
||||||
|
Install custom domains in your `/etc/hosts`
|
||||||
|
```
|
||||||
|
./wpt make-hosts-file | sudo tee -a /etc/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate `MANIFEST.json`
|
||||||
|
```
|
||||||
|
./wpt manifest
|
||||||
|
```
|
||||||
|
Use the [WPT's setup
|
||||||
|
guide](https://web-platform-tests.org/running-tests/from-local-system.html) for
|
||||||
|
details.
|
||||||
|
|
||||||
#### Run WPT test suite
|
#### Run WPT test suite
|
||||||
|
|
||||||
To run all the tests:
|
An external [Go](https://go.dev) runner is provided by
|
||||||
|
[github.com/lightpanda-io/demo/](https://github.com/lightpanda-io/demo/)
|
||||||
|
repository, located into `wptrunner/` dir.
|
||||||
|
You need to clone the project first.
|
||||||
|
|
||||||
|
First start the WPT's HTTP server from your `wpt/` clone dir.
|
||||||
|
```
|
||||||
|
./wpt serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a Lightpanda browser
|
||||||
|
|
||||||
```
|
```
|
||||||
make wpt
|
zig build run -- --insecure_disable_tls_host_verification
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can start the wptrunner from the Demo's clone dir:
|
||||||
|
```
|
||||||
|
cd wptrunner && go run .
|
||||||
```
|
```
|
||||||
|
|
||||||
Or one specific test:
|
Or one specific test:
|
||||||
|
|
||||||
```
|
```
|
||||||
make wpt Node-childNodes.html
|
cd wptrunner && go run . Node-childNodes.html
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Add a new WPT test case
|
`wptrunner` command accepts `--summary` and `--json` options modifying output.
|
||||||
|
Also `--concurrency` define the concurrency limit.
|
||||||
|
|
||||||
We add new relevant tests cases files when we implemented changes in Lightpanda.
|
:warning: Running the whole test suite will take a long time. In this case,
|
||||||
|
it's useful to build in `releaseFast` mode to make tests faster.
|
||||||
|
|
||||||
To add a new test, copy the file you want from the [WPT
|
```
|
||||||
repo](https://github.com/web-platform-tests/wpt) into the `tests/wpt` directory.
|
zig build -Doptimize=ReleaseFast run
|
||||||
|
```
|
||||||
:warning: Please keep the original directory tree structure of `tests/wpt`.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,36 @@
|
|||||||
.{
|
.{
|
||||||
.name = .browser,
|
.name = .browser,
|
||||||
.paths = .{""},
|
|
||||||
.version = "0.0.0",
|
.version = "0.0.0",
|
||||||
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
|
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
|
||||||
.minimum_zig_version = "0.15.2",
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.2.9.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.1.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH689vBACgpqFVEhT2wxRin-qQQSOcKJoM37MVo0rU",
|
.hash = "v8-0.0.0-xddH64J7BAC81mkf6G9RbEJxS-W3TIRl5iFnShwbqCqy",
|
||||||
|
|
||||||
},
|
},
|
||||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||||
|
.brotli = .{
|
||||||
|
// v1.2.0
|
||||||
|
.url = "https://github.com/google/brotli/archive/028fb5a23661f123017c060daa546b55cf4bde29.tar.gz",
|
||||||
|
.hash = "N-V-__8AAJudKgCQCuIiH6MJjAiIJHfg_tT_Ew-0vZwVkCo_",
|
||||||
|
},
|
||||||
|
.zlib = .{
|
||||||
|
.url = "https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz",
|
||||||
|
.hash = "N-V-__8AAJ2cNgAgfBtAw33Bxfu1IWISDeKKSr3DAqoAysIJ",
|
||||||
|
},
|
||||||
|
.nghttp2 = .{
|
||||||
|
.url = "https://github.com/nghttp2/nghttp2/releases/download/v1.68.0/nghttp2-1.68.0.tar.gz",
|
||||||
|
.hash = "N-V-__8AAL15vQCI63ZL6Zaz5hJg6JTEgYXGbLnMFSnf7FT3",
|
||||||
|
},
|
||||||
.@"boringssl-zig" = .{
|
.@"boringssl-zig" = .{
|
||||||
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
|
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
|
||||||
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
|
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
|
||||||
},
|
},
|
||||||
|
.curl = .{
|
||||||
|
.url = "https://github.com/curl/curl/releases/download/curl-8_18_0/curl-8.18.0.tar.gz",
|
||||||
|
.hash = "N-V-__8AALp9QAGn6CCHZ6fK_FfMyGtG824LSHYHHasM3w-y",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
.paths = .{""},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,13 +191,15 @@ pub const Mcp = struct {
|
|||||||
pub const DumpFormat = enum {
|
pub const DumpFormat = enum {
|
||||||
html,
|
html,
|
||||||
markdown,
|
markdown,
|
||||||
|
wpt,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Fetch = struct {
|
pub const Fetch = struct {
|
||||||
url: [:0]const u8,
|
url: [:0]const u8,
|
||||||
dump_mode: ?DumpFormat = null,
|
dump_mode: ?DumpFormat = null,
|
||||||
common: Common = .{},
|
common: Common = .{},
|
||||||
withbase: bool = false,
|
with_base: bool = false,
|
||||||
|
with_frames: bool = false,
|
||||||
strip: dump.Opts.Strip = .{},
|
strip: dump.Opts.Strip = .{},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -348,6 +350,8 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
|||||||
\\
|
\\
|
||||||
\\--with_base Add a <base> tag in dump. Defaults to false.
|
\\--with_base Add a <base> tag in dump. Defaults to false.
|
||||||
\\
|
\\
|
||||||
|
\\--with_frames Includes the contents of iframes. Defaults to false.
|
||||||
|
\\
|
||||||
++ common_options ++
|
++ common_options ++
|
||||||
\\
|
\\
|
||||||
\\serve command
|
\\serve command
|
||||||
@@ -454,6 +458,10 @@ fn inferMode(opt: []const u8) ?RunMode {
|
|||||||
return .fetch;
|
return .fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, opt, "--with_frames")) {
|
||||||
|
return .fetch;
|
||||||
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, opt, "--host")) {
|
if (std.mem.eql(u8, opt, "--host")) {
|
||||||
return .serve;
|
return .serve;
|
||||||
}
|
}
|
||||||
@@ -571,7 +579,8 @@ fn parseFetchArgs(
|
|||||||
args: *std.process.ArgIterator,
|
args: *std.process.ArgIterator,
|
||||||
) !Fetch {
|
) !Fetch {
|
||||||
var dump_mode: ?DumpFormat = null;
|
var dump_mode: ?DumpFormat = null;
|
||||||
var withbase: bool = false;
|
var with_base: bool = false;
|
||||||
|
var with_frames: bool = false;
|
||||||
var url: ?[:0]const u8 = null;
|
var url: ?[:0]const u8 = null;
|
||||||
var common: Common = .{};
|
var common: Common = .{};
|
||||||
var strip: dump.Opts.Strip = .{};
|
var strip: dump.Opts.Strip = .{};
|
||||||
@@ -602,7 +611,12 @@ fn parseFetchArgs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, "--with_base", opt)) {
|
if (std.mem.eql(u8, "--with_base", opt)) {
|
||||||
withbase = true;
|
with_base = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, "--with_frames", opt)) {
|
||||||
|
with_frames = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,7 +672,8 @@ fn parseFetchArgs(
|
|||||||
.dump_mode = dump_mode,
|
.dump_mode = dump_mode,
|
||||||
.strip = strip,
|
.strip = strip,
|
||||||
.common = common,
|
.common = common,
|
||||||
.withbase = withbase,
|
.with_base = with_base,
|
||||||
|
.with_frames = with_frames,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1375
src/Net.zig
Normal file
1375
src/Net.zig
Normal file
File diff suppressed because it is too large
Load Diff
715
src/Server.zig
715
src/Server.zig
@@ -30,6 +30,7 @@ const log = @import("log.zig");
|
|||||||
const App = @import("App.zig");
|
const App = @import("App.zig");
|
||||||
const Config = @import("Config.zig");
|
const Config = @import("Config.zig");
|
||||||
const CDP = @import("cdp/cdp.zig").CDP;
|
const CDP = @import("cdp/cdp.zig").CDP;
|
||||||
|
const Net = @import("Net.zig");
|
||||||
const Http = @import("http/Http.zig");
|
const Http = @import("http/Http.zig");
|
||||||
const HttpClient = @import("http/Client.zig");
|
const HttpClient = @import("http/Client.zig");
|
||||||
|
|
||||||
@@ -265,21 +266,7 @@ pub const Client = struct {
|
|||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
app: *App,
|
app: *App,
|
||||||
http: *HttpClient,
|
http: *HttpClient,
|
||||||
json_version_response: []const u8,
|
ws: Net.WsConnection,
|
||||||
reader: Reader(true),
|
|
||||||
socket: posix.socket_t,
|
|
||||||
socket_flags: usize,
|
|
||||||
send_arena: ArenaAllocator,
|
|
||||||
timeout_ms: u32,
|
|
||||||
|
|
||||||
const EMPTY_PONG = [_]u8{ 138, 0 };
|
|
||||||
|
|
||||||
// CLOSE, 2 length, code
|
|
||||||
const CLOSE_NORMAL = [_]u8{ 136, 2, 3, 232 }; // code: 1000
|
|
||||||
const CLOSE_TOO_BIG = [_]u8{ 136, 2, 3, 241 }; // 1009
|
|
||||||
const CLOSE_PROTOCOL_ERROR = [_]u8{ 136, 2, 3, 234 }; //code: 1002
|
|
||||||
// "private-use" close codes must be from 4000-49999
|
|
||||||
const CLOSE_TIMEOUT = [_]u8{ 136, 2, 15, 160 }; // code: 4000
|
|
||||||
|
|
||||||
fn init(
|
fn init(
|
||||||
socket: posix.socket_t,
|
socket: posix.socket_t,
|
||||||
@@ -288,40 +275,28 @@ pub const Client = struct {
|
|||||||
json_version_response: []const u8,
|
json_version_response: []const u8,
|
||||||
timeout_ms: u32,
|
timeout_ms: u32,
|
||||||
) !Client {
|
) !Client {
|
||||||
|
var ws = try Net.WsConnection.init(socket, allocator, json_version_response, timeout_ms);
|
||||||
|
errdefer ws.deinit();
|
||||||
|
|
||||||
if (log.enabled(.app, .info)) {
|
if (log.enabled(.app, .info)) {
|
||||||
var client_address: std.net.Address = undefined;
|
const client_address = ws.getAddress() catch null;
|
||||||
var socklen: posix.socklen_t = @sizeOf(net.Address);
|
|
||||||
try std.posix.getsockname(socket, &client_address.any, &socklen);
|
|
||||||
log.info(.app, "client connected", .{ .ip = client_address });
|
log.info(.app, "client connected", .{ .ip = client_address });
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket_flags = try posix.fcntl(socket, posix.F.GETFL, 0);
|
|
||||||
const nonblocking = @as(u32, @bitCast(posix.O{ .NONBLOCK = true }));
|
|
||||||
// we expect the socket to come to us as nonblocking
|
|
||||||
lp.assert(socket_flags & nonblocking == nonblocking, "Client.init blocking", .{});
|
|
||||||
|
|
||||||
var reader = try Reader(true).init(allocator);
|
|
||||||
errdefer reader.deinit();
|
|
||||||
|
|
||||||
const http = try app.http.createClient(allocator);
|
const http = try app.http.createClient(allocator);
|
||||||
errdefer http.deinit();
|
errdefer http.deinit();
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.socket = socket,
|
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.app = app,
|
.app = app,
|
||||||
.http = http,
|
.http = http,
|
||||||
.json_version_response = json_version_response,
|
.ws = ws,
|
||||||
.reader = reader,
|
|
||||||
.mode = .{ .http = {} },
|
.mode = .{ .http = {} },
|
||||||
.socket_flags = socket_flags,
|
|
||||||
.send_arena = ArenaAllocator.init(allocator),
|
|
||||||
.timeout_ms = timeout_ms,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(self: *Client) void {
|
fn stop(self: *Client) void {
|
||||||
posix.shutdown(self.socket, .recv) catch {};
|
self.ws.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(self: *Client) void {
|
fn deinit(self: *Client) void {
|
||||||
@@ -329,15 +304,14 @@ pub const Client = struct {
|
|||||||
.cdp => |*cdp| cdp.deinit(),
|
.cdp => |*cdp| cdp.deinit(),
|
||||||
.http => {},
|
.http => {},
|
||||||
}
|
}
|
||||||
self.reader.deinit();
|
self.ws.deinit();
|
||||||
self.send_arena.deinit();
|
|
||||||
self.http.deinit();
|
self.http.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start(self: *Client) void {
|
fn start(self: *Client) void {
|
||||||
const http = self.http;
|
const http = self.http;
|
||||||
http.cdp_client = .{
|
http.cdp_client = .{
|
||||||
.socket = self.socket,
|
.socket = self.ws.socket,
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
.blocking_read_start = Client.blockingReadStart,
|
.blocking_read_start = Client.blockingReadStart,
|
||||||
.blocking_read = Client.blockingRead,
|
.blocking_read = Client.blockingRead,
|
||||||
@@ -352,8 +326,9 @@ pub const Client = struct {
|
|||||||
|
|
||||||
fn httpLoop(self: *Client, http: *HttpClient) !void {
|
fn httpLoop(self: *Client, http: *HttpClient) !void {
|
||||||
lp.assert(self.mode == .http, "Client.httpLoop invalid mode", .{});
|
lp.assert(self.mode == .http, "Client.httpLoop invalid mode", .{});
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const status = http.tick(self.timeout_ms) catch |err| {
|
const status = http.tick(self.ws.timeout_ms) catch |err| {
|
||||||
log.err(.app, "http tick", .{ .err = err });
|
log.err(.app, "http tick", .{ .err = err });
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -371,13 +346,9 @@ pub const Client = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.cdpLoop(http);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cdpLoop(self: *Client, http: *HttpClient) !void {
|
|
||||||
var cdp = &self.mode.cdp;
|
var cdp = &self.mode.cdp;
|
||||||
var last_message = timestamp(.monotonic);
|
var last_message = timestamp(.monotonic);
|
||||||
var ms_remaining = self.timeout_ms;
|
var ms_remaining = self.ws.timeout_ms;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
switch (cdp.pageWait(ms_remaining)) {
|
switch (cdp.pageWait(ms_remaining)) {
|
||||||
@@ -386,7 +357,7 @@ pub const Client = struct {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
last_message = timestamp(.monotonic);
|
last_message = timestamp(.monotonic);
|
||||||
ms_remaining = self.timeout_ms;
|
ms_remaining = self.ws.timeout_ms;
|
||||||
},
|
},
|
||||||
.no_page => {
|
.no_page => {
|
||||||
const status = http.tick(ms_remaining) catch |err| {
|
const status = http.tick(ms_remaining) catch |err| {
|
||||||
@@ -401,7 +372,7 @@ pub const Client = struct {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
last_message = timestamp(.monotonic);
|
last_message = timestamp(.monotonic);
|
||||||
ms_remaining = self.timeout_ms;
|
ms_remaining = self.ws.timeout_ms;
|
||||||
},
|
},
|
||||||
.done => {
|
.done => {
|
||||||
const elapsed = timestamp(.monotonic) - last_message;
|
const elapsed = timestamp(.monotonic) - last_message;
|
||||||
@@ -417,7 +388,7 @@ pub const Client = struct {
|
|||||||
|
|
||||||
fn blockingReadStart(ctx: *anyopaque) bool {
|
fn blockingReadStart(ctx: *anyopaque) bool {
|
||||||
const self: *Client = @ptrCast(@alignCast(ctx));
|
const self: *Client = @ptrCast(@alignCast(ctx));
|
||||||
_ = posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags & ~@as(u32, @bitCast(posix.O{ .NONBLOCK = true }))) catch |err| {
|
self.ws.setBlocking(true) catch |err| {
|
||||||
log.warn(.app, "CDP blockingReadStart", .{ .err = err });
|
log.warn(.app, "CDP blockingReadStart", .{ .err = err });
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -431,7 +402,7 @@ pub const Client = struct {
|
|||||||
|
|
||||||
fn blockingReadStop(ctx: *anyopaque) bool {
|
fn blockingReadStop(ctx: *anyopaque) bool {
|
||||||
const self: *Client = @ptrCast(@alignCast(ctx));
|
const self: *Client = @ptrCast(@alignCast(ctx));
|
||||||
_ = posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags) catch |err| {
|
self.ws.setBlocking(false) catch |err| {
|
||||||
log.warn(.app, "CDP blockingReadStop", .{ .err = err });
|
log.warn(.app, "CDP blockingReadStop", .{ .err = err });
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -439,7 +410,7 @@ pub const Client = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn readSocket(self: *Client) bool {
|
fn readSocket(self: *Client) bool {
|
||||||
const n = posix.read(self.socket, self.readBuf()) catch |err| {
|
const n = self.ws.read() catch |err| {
|
||||||
log.warn(.app, "CDP read", .{ .err = err });
|
log.warn(.app, "CDP read", .{ .err = err });
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -449,16 +420,10 @@ pub const Client = struct {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.processData(n) catch false;
|
return self.processData() catch false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn readBuf(self: *Client) []u8 {
|
fn processData(self: *Client) !bool {
|
||||||
return self.reader.readBuf();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn processData(self: *Client, len: usize) !bool {
|
|
||||||
self.reader.len += len;
|
|
||||||
|
|
||||||
switch (self.mode) {
|
switch (self.mode) {
|
||||||
.cdp => |*cdp| return self.processWebsocketMessage(cdp),
|
.cdp => |*cdp| return self.processWebsocketMessage(cdp),
|
||||||
.http => return self.processHTTPRequest(),
|
.http => return self.processHTTPRequest(),
|
||||||
@@ -466,8 +431,8 @@ pub const Client = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn processHTTPRequest(self: *Client) !bool {
|
fn processHTTPRequest(self: *Client) !bool {
|
||||||
lp.assert(self.reader.pos == 0, "Client.HTTP pos", .{ .pos = self.reader.pos });
|
lp.assert(self.ws.reader.pos == 0, "Client.HTTP pos", .{ .pos = self.ws.reader.pos });
|
||||||
const request = self.reader.buf[0..self.reader.len];
|
const request = self.ws.reader.buf[0..self.ws.reader.len];
|
||||||
|
|
||||||
if (request.len > Config.CDP_MAX_HTTP_REQUEST_SIZE) {
|
if (request.len > Config.CDP_MAX_HTTP_REQUEST_SIZE) {
|
||||||
self.writeHTTPErrorResponse(413, "Request too large");
|
self.writeHTTPErrorResponse(413, "Request too large");
|
||||||
@@ -481,7 +446,7 @@ pub const Client = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the next incoming data can go to the front of our buffer
|
// the next incoming data can go to the front of our buffer
|
||||||
defer self.reader.len = 0;
|
defer self.ws.reader.len = 0;
|
||||||
return self.handleHTTPRequest(request) catch |err| {
|
return self.handleHTTPRequest(request) catch |err| {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
error.NotFound => self.writeHTTPErrorResponse(404, "Not found"),
|
error.NotFound => self.writeHTTPErrorResponse(404, "Not found"),
|
||||||
@@ -521,15 +486,15 @@ pub const Client = struct {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, url, "/json/version")) {
|
if (std.mem.eql(u8, url, "/json/version") or std.mem.eql(u8, url, "/json/version/")) {
|
||||||
try self.send(self.json_version_response);
|
try self.ws.send(self.ws.json_version_response);
|
||||||
// Chromedp (a Go driver) does an http request to /json/version
|
// Chromedp (a Go driver) does an http request to /json/version
|
||||||
// then to / (websocket upgrade) using a different connection.
|
// then to / (websocket upgrade) using a different connection.
|
||||||
// Since we only allow 1 connection at a time, the 2nd one (the
|
// Since we only allow 1 connection at a time, the 2nd one (the
|
||||||
// websocket upgrade) blocks until the first one times out.
|
// websocket upgrade) blocks until the first one times out.
|
||||||
// We can avoid that by closing the connection. json_version_response
|
// We can avoid that by closing the connection. json_version_response
|
||||||
// has a Connection: Close header too.
|
// has a Connection: Close header too.
|
||||||
try posix.shutdown(self.socket, .recv);
|
self.ws.shutdown();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,581 +502,31 @@ pub const Client = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn upgradeConnection(self: *Client, request: []u8) !void {
|
fn upgradeConnection(self: *Client, request: []u8) !void {
|
||||||
// our caller already confirmed that we have a trailing \r\n\r\n
|
try self.ws.upgrade(request);
|
||||||
const request_line_end = std.mem.indexOfScalar(u8, request, '\r') orelse unreachable;
|
|
||||||
const request_line = request[0..request_line_end];
|
|
||||||
|
|
||||||
if (!std.ascii.endsWithIgnoreCase(request_line, "http/1.1")) {
|
|
||||||
return error.InvalidProtocol;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need to extract the sec-websocket-key value
|
|
||||||
var key: []const u8 = "";
|
|
||||||
|
|
||||||
// we need to make sure that we got all the necessary headers + values
|
|
||||||
var required_headers: u8 = 0;
|
|
||||||
|
|
||||||
// can't std.mem.split because it forces the iterated value to be const
|
|
||||||
// (we could @constCast...)
|
|
||||||
|
|
||||||
var buf = request[request_line_end + 2 ..];
|
|
||||||
|
|
||||||
while (buf.len > 4) {
|
|
||||||
const index = std.mem.indexOfScalar(u8, buf, '\r') orelse unreachable;
|
|
||||||
const separator = std.mem.indexOfScalar(u8, buf[0..index], ':') orelse return error.InvalidRequest;
|
|
||||||
|
|
||||||
const name = std.mem.trim(u8, toLower(buf[0..separator]), &std.ascii.whitespace);
|
|
||||||
const value = std.mem.trim(u8, buf[(separator + 1)..index], &std.ascii.whitespace);
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, name, "upgrade")) {
|
|
||||||
if (!std.ascii.eqlIgnoreCase("websocket", value)) {
|
|
||||||
return error.InvalidUpgradeHeader;
|
|
||||||
}
|
|
||||||
required_headers |= 1;
|
|
||||||
} else if (std.mem.eql(u8, name, "sec-websocket-version")) {
|
|
||||||
if (value.len != 2 or value[0] != '1' or value[1] != '3') {
|
|
||||||
return error.InvalidVersionHeader;
|
|
||||||
}
|
|
||||||
required_headers |= 2;
|
|
||||||
} else if (std.mem.eql(u8, name, "connection")) {
|
|
||||||
// find if connection header has upgrade in it, example header:
|
|
||||||
// Connection: keep-alive, Upgrade
|
|
||||||
if (std.ascii.indexOfIgnoreCase(value, "upgrade") == null) {
|
|
||||||
return error.InvalidConnectionHeader;
|
|
||||||
}
|
|
||||||
required_headers |= 4;
|
|
||||||
} else if (std.mem.eql(u8, name, "sec-websocket-key")) {
|
|
||||||
key = value;
|
|
||||||
required_headers |= 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = index + 2;
|
|
||||||
buf = buf[next..];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (required_headers != 15) {
|
|
||||||
return error.MissingHeaders;
|
|
||||||
}
|
|
||||||
|
|
||||||
// our caller has already made sure this request ended in \r\n\r\n
|
|
||||||
// so it isn't something we need to check again
|
|
||||||
|
|
||||||
const allocator = self.send_arena.allocator();
|
|
||||||
|
|
||||||
const response = blk: {
|
|
||||||
// Response to an ugprade request is always this, with
|
|
||||||
// the Sec-Websocket-Accept value a spacial sha1 hash of the
|
|
||||||
// request "sec-websocket-version" and a magic value.
|
|
||||||
|
|
||||||
const template =
|
|
||||||
"HTTP/1.1 101 Switching Protocols\r\n" ++
|
|
||||||
"Upgrade: websocket\r\n" ++
|
|
||||||
"Connection: upgrade\r\n" ++
|
|
||||||
"Sec-Websocket-Accept: 0000000000000000000000000000\r\n\r\n";
|
|
||||||
|
|
||||||
// The response will be sent via the IO Loop and thus has to have its
|
|
||||||
// own lifetime.
|
|
||||||
const res = try allocator.dupe(u8, template);
|
|
||||||
|
|
||||||
// magic response
|
|
||||||
const key_pos = res.len - 32;
|
|
||||||
var h: [20]u8 = undefined;
|
|
||||||
var hasher = std.crypto.hash.Sha1.init(.{});
|
|
||||||
hasher.update(key);
|
|
||||||
// websocket spec always used this value
|
|
||||||
hasher.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
|
||||||
hasher.final(&h);
|
|
||||||
|
|
||||||
_ = std.base64.standard.Encoder.encode(res[key_pos .. key_pos + 28], h[0..]);
|
|
||||||
|
|
||||||
break :blk res;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.mode = .{ .cdp = try CDP.init(self.app, self.http, self) };
|
self.mode = .{ .cdp = try CDP.init(self.app, self.http, self) };
|
||||||
return self.send(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn writeHTTPErrorResponse(self: *Client, comptime status: u16, comptime body: []const u8) void {
|
fn writeHTTPErrorResponse(self: *Client, comptime status: u16, comptime body: []const u8) void {
|
||||||
const response = std.fmt.comptimePrint(
|
self.ws.sendHttpError(status, body);
|
||||||
"HTTP/1.1 {d} \r\nConnection: Close\r\nContent-Length: {d}\r\n\r\n{s}",
|
|
||||||
.{ status, body.len, body },
|
|
||||||
);
|
|
||||||
|
|
||||||
// we're going to close this connection anyways, swallowing any
|
|
||||||
// error seems safe
|
|
||||||
self.send(response) catch {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn processWebsocketMessage(self: *Client, cdp: *CDP) !bool {
|
fn processWebsocketMessage(self: *Client, cdp: *CDP) !bool {
|
||||||
var reader = &self.reader;
|
return self.ws.processMessages(cdp);
|
||||||
while (true) {
|
|
||||||
const msg = reader.next() catch |err| {
|
|
||||||
switch (err) {
|
|
||||||
error.TooLarge => self.send(&CLOSE_TOO_BIG) catch {},
|
|
||||||
error.NotMasked => self.send(&CLOSE_PROTOCOL_ERROR) catch {},
|
|
||||||
error.ReservedFlags => self.send(&CLOSE_PROTOCOL_ERROR) catch {},
|
|
||||||
error.InvalidMessageType => self.send(&CLOSE_PROTOCOL_ERROR) catch {},
|
|
||||||
error.ControlTooLarge => self.send(&CLOSE_PROTOCOL_ERROR) catch {},
|
|
||||||
error.InvalidContinuation => self.send(&CLOSE_PROTOCOL_ERROR) catch {},
|
|
||||||
error.NestedFragementation => self.send(&CLOSE_PROTOCOL_ERROR) catch {},
|
|
||||||
error.OutOfMemory => {}, // don't borther trying to send an error in this case
|
|
||||||
}
|
|
||||||
return err;
|
|
||||||
} orelse break;
|
|
||||||
|
|
||||||
switch (msg.type) {
|
|
||||||
.pong => {},
|
|
||||||
.ping => try self.sendPong(msg.data),
|
|
||||||
.close => {
|
|
||||||
self.send(&CLOSE_NORMAL) catch {};
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
.text, .binary => if (cdp.handleMessage(msg.data) == false) {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if (msg.cleanup_fragment) {
|
|
||||||
reader.cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We might have read part of the next message. Our reader potentially
|
|
||||||
// has to move data around in its buffer to make space.
|
|
||||||
reader.compact();
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sendPong(self: *Client, data: []const u8) !void {
|
pub fn sendAllocator(self: *Client) Allocator {
|
||||||
if (data.len == 0) {
|
return self.ws.send_arena.allocator();
|
||||||
return self.send(&EMPTY_PONG);
|
|
||||||
}
|
|
||||||
var header_buf: [10]u8 = undefined;
|
|
||||||
const header = websocketHeader(&header_buf, .pong, data.len);
|
|
||||||
|
|
||||||
const allocator = self.send_arena.allocator();
|
|
||||||
var framed = try allocator.alloc(u8, header.len + data.len);
|
|
||||||
@memcpy(framed[0..header.len], header);
|
|
||||||
@memcpy(framed[header.len..], data);
|
|
||||||
return self.send(framed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// called by CDP
|
|
||||||
// Websocket frames have a variable length header. For server-client,
|
|
||||||
// it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have
|
|
||||||
// writev, so we need to get creative. We'll JSON serialize to a
|
|
||||||
// buffer, where the first 10 bytes are reserved. We can then backfill
|
|
||||||
// the header and send the slice.
|
|
||||||
pub fn sendJSON(self: *Client, message: anytype, opts: std.json.Stringify.Options) !void {
|
pub fn sendJSON(self: *Client, message: anytype, opts: std.json.Stringify.Options) !void {
|
||||||
const allocator = self.send_arena.allocator();
|
return self.ws.sendJSON(message, opts);
|
||||||
|
|
||||||
var aw = try std.Io.Writer.Allocating.initCapacity(allocator, 512);
|
|
||||||
|
|
||||||
// reserve space for the maximum possible header
|
|
||||||
try aw.writer.writeAll(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
|
|
||||||
try std.json.Stringify.value(message, opts, &aw.writer);
|
|
||||||
const framed = fillWebsocketHeader(aw.toArrayList());
|
|
||||||
return self.send(framed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sendJSONRaw(
|
pub fn sendJSONRaw(self: *Client, buf: std.ArrayList(u8)) !void {
|
||||||
self: *Client,
|
return self.ws.sendJSONRaw(buf);
|
||||||
buf: std.ArrayList(u8),
|
|
||||||
) !void {
|
|
||||||
// Dangerous API!. We assume the caller has reserved the first 10
|
|
||||||
// bytes in `buf`.
|
|
||||||
const framed = fillWebsocketHeader(buf);
|
|
||||||
return self.send(framed);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(self: *Client, data: []const u8) !void {
|
|
||||||
var pos: usize = 0;
|
|
||||||
var changed_to_blocking: bool = false;
|
|
||||||
defer _ = self.send_arena.reset(.{ .retain_with_limit = 1024 * 32 });
|
|
||||||
|
|
||||||
defer if (changed_to_blocking) {
|
|
||||||
// We had to change our socket to blocking me to get our write out
|
|
||||||
// We need to change it back to non-blocking.
|
|
||||||
_ = posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags) catch |err| {
|
|
||||||
log.err(.app, "CDP restore nonblocking", .{ .err = err });
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
LOOP: while (pos < data.len) {
|
|
||||||
const written = posix.write(self.socket, data[pos..]) catch |err| switch (err) {
|
|
||||||
error.WouldBlock => {
|
|
||||||
// self.socket is nonblocking, because we don't want to block
|
|
||||||
// reads. But our life is a lot easier if we block writes,
|
|
||||||
// largely, because we don't have to maintain a queue of pending
|
|
||||||
// writes (which would each need their own allocations). So
|
|
||||||
// if we get a WouldBlock error, we'll switch the socket to
|
|
||||||
// blocking and switch it back to non-blocking after the write
|
|
||||||
// is complete. Doesn't seem particularly efficiently, but
|
|
||||||
// this should virtually never happen.
|
|
||||||
lp.assert(changed_to_blocking == false, "Client.double block", .{});
|
|
||||||
changed_to_blocking = true;
|
|
||||||
_ = try posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags & ~@as(u32, @bitCast(posix.O{ .NONBLOCK = true })));
|
|
||||||
continue :LOOP;
|
|
||||||
},
|
|
||||||
else => return err,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (written == 0) {
|
|
||||||
return error.Closed;
|
|
||||||
}
|
|
||||||
pos += written;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// WebSocket message reader. Given websocket message, acts as an iterator that
|
|
||||||
// can return zero or more Messages. When next returns null, any incomplete
|
|
||||||
// message will remain in reader.data
|
|
||||||
fn Reader(comptime EXPECT_MASK: bool) type {
|
|
||||||
return struct {
|
|
||||||
allocator: Allocator,
|
|
||||||
|
|
||||||
// position in buf of the start of the next message
|
|
||||||
pos: usize = 0,
|
|
||||||
|
|
||||||
// position in buf up until where we have valid data
|
|
||||||
// (any new reads must be placed after this)
|
|
||||||
len: usize = 0,
|
|
||||||
|
|
||||||
// we add 140 to allow 1 control message (ping/pong/close) to be
|
|
||||||
// fragmented into a normal message.
|
|
||||||
buf: []u8,
|
|
||||||
|
|
||||||
fragments: ?Fragments = null,
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
fn init(allocator: Allocator) !Self {
|
|
||||||
const buf = try allocator.alloc(u8, 16 * 1024);
|
|
||||||
return .{
|
|
||||||
.buf = buf,
|
|
||||||
.allocator = allocator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(self: *Self) void {
|
|
||||||
self.cleanup();
|
|
||||||
self.allocator.free(self.buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cleanup(self: *Self) void {
|
|
||||||
if (self.fragments) |*f| {
|
|
||||||
f.message.deinit(self.allocator);
|
|
||||||
self.fragments = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn readBuf(self: *Self) []u8 {
|
|
||||||
// We might have read a partial http or websocket message.
|
|
||||||
// Subsequent reads must read from where we left off.
|
|
||||||
return self.buf[self.len..];
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next(self: *Self) !?Message {
|
|
||||||
LOOP: while (true) {
|
|
||||||
var buf = self.buf[self.pos..self.len];
|
|
||||||
|
|
||||||
const length_of_len, const message_len = extractLengths(buf) orelse {
|
|
||||||
// we don't have enough bytes
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const byte1 = buf[0];
|
|
||||||
|
|
||||||
if (byte1 & 112 != 0) {
|
|
||||||
return error.ReservedFlags;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (comptime EXPECT_MASK) {
|
|
||||||
if (buf[1] & 128 != 128) {
|
|
||||||
// client -> server messages _must_ be masked
|
|
||||||
return error.NotMasked;
|
|
||||||
}
|
|
||||||
} else if (buf[1] & 128 != 0) {
|
|
||||||
// server -> client are never masked
|
|
||||||
return error.Masked;
|
|
||||||
}
|
|
||||||
|
|
||||||
var is_control = false;
|
|
||||||
var is_continuation = false;
|
|
||||||
var message_type: Message.Type = undefined;
|
|
||||||
switch (byte1 & 15) {
|
|
||||||
0 => is_continuation = true,
|
|
||||||
1 => message_type = .text,
|
|
||||||
2 => message_type = .binary,
|
|
||||||
8 => {
|
|
||||||
is_control = true;
|
|
||||||
message_type = .close;
|
|
||||||
},
|
|
||||||
9 => {
|
|
||||||
is_control = true;
|
|
||||||
message_type = .ping;
|
|
||||||
},
|
|
||||||
10 => {
|
|
||||||
is_control = true;
|
|
||||||
message_type = .pong;
|
|
||||||
},
|
|
||||||
else => return error.InvalidMessageType,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_control) {
|
|
||||||
if (message_len > 125) {
|
|
||||||
return error.ControlTooLarge;
|
|
||||||
}
|
|
||||||
} else if (message_len > Config.CDP_MAX_MESSAGE_SIZE) {
|
|
||||||
return error.TooLarge;
|
|
||||||
} else if (message_len > self.buf.len) {
|
|
||||||
const len = self.buf.len;
|
|
||||||
self.buf = try growBuffer(self.allocator, self.buf, message_len);
|
|
||||||
buf = self.buf[0..len];
|
|
||||||
// we need more data
|
|
||||||
return null;
|
|
||||||
} else if (buf.len < message_len) {
|
|
||||||
// we need more data
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// prefix + length_of_len + mask
|
|
||||||
const header_len = 2 + length_of_len + if (comptime EXPECT_MASK) 4 else 0;
|
|
||||||
|
|
||||||
const payload = buf[header_len..message_len];
|
|
||||||
if (comptime EXPECT_MASK) {
|
|
||||||
mask(buf[header_len - 4 .. header_len], payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// whatever happens after this, we know where the next message starts
|
|
||||||
self.pos += message_len;
|
|
||||||
|
|
||||||
const fin = byte1 & 128 == 128;
|
|
||||||
|
|
||||||
if (is_continuation) {
|
|
||||||
const fragments = &(self.fragments orelse return error.InvalidContinuation);
|
|
||||||
if (fragments.message.items.len + message_len > Config.CDP_MAX_MESSAGE_SIZE) {
|
|
||||||
return error.TooLarge;
|
|
||||||
}
|
|
||||||
|
|
||||||
try fragments.message.appendSlice(self.allocator, payload);
|
|
||||||
|
|
||||||
if (fin == false) {
|
|
||||||
// maybe we have more parts of the message waiting
|
|
||||||
continue :LOOP;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this continuation is done!
|
|
||||||
return .{
|
|
||||||
.type = fragments.type,
|
|
||||||
.data = fragments.message.items,
|
|
||||||
.cleanup_fragment = true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const can_be_fragmented = message_type == .text or message_type == .binary;
|
|
||||||
if (self.fragments != null and can_be_fragmented) {
|
|
||||||
// if this isn't a continuation, then we can't have fragments
|
|
||||||
return error.NestedFragementation;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fin == false) {
|
|
||||||
if (can_be_fragmented == false) {
|
|
||||||
return error.InvalidContinuation;
|
|
||||||
}
|
|
||||||
|
|
||||||
// not continuation, and not fin. It has to be the first message
|
|
||||||
// in a fragmented message.
|
|
||||||
var fragments = Fragments{ .message = .{}, .type = message_type };
|
|
||||||
try fragments.message.appendSlice(self.allocator, payload);
|
|
||||||
self.fragments = fragments;
|
|
||||||
continue :LOOP;
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.data = payload,
|
|
||||||
.type = message_type,
|
|
||||||
.cleanup_fragment = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extractLengths(buf: []const u8) ?struct { usize, usize } {
|
|
||||||
if (buf.len < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const length_of_len: usize = switch (buf[1] & 127) {
|
|
||||||
126 => 2,
|
|
||||||
127 => 8,
|
|
||||||
else => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (buf.len < length_of_len + 2) {
|
|
||||||
// we definitely don't have enough buf yet
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message_len = switch (length_of_len) {
|
|
||||||
2 => @as(u16, @intCast(buf[3])) | @as(u16, @intCast(buf[2])) << 8,
|
|
||||||
8 => @as(u64, @intCast(buf[9])) | @as(u64, @intCast(buf[8])) << 8 | @as(u64, @intCast(buf[7])) << 16 | @as(u64, @intCast(buf[6])) << 24 | @as(u64, @intCast(buf[5])) << 32 | @as(u64, @intCast(buf[4])) << 40 | @as(u64, @intCast(buf[3])) << 48 | @as(u64, @intCast(buf[2])) << 56,
|
|
||||||
else => buf[1] & 127,
|
|
||||||
} + length_of_len + 2 + if (comptime EXPECT_MASK) 4 else 0; // +2 for header prefix, +4 for mask;
|
|
||||||
|
|
||||||
return .{ length_of_len, message_len };
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is called after we've processed complete websocket messages (this
|
|
||||||
// only applies to websocket messages).
|
|
||||||
// There are three cases:
|
|
||||||
// 1 - We don't have any incomplete data (for a subsequent message) in buf.
|
|
||||||
// This is the easier to handle, we can set pos & len to 0.
|
|
||||||
// 2 - We have part of the next message, but we know it'll fit in the
|
|
||||||
// remaining buf. We don't need to do anything
|
|
||||||
// 3 - We have part of the next message, but either it won't fight into the
|
|
||||||
// remaining buffer, or we don't know (because we don't have enough
|
|
||||||
// of the header to tell the length). We need to "compact" the buffer
|
|
||||||
fn compact(self: *Self) void {
|
|
||||||
const pos = self.pos;
|
|
||||||
const len = self.len;
|
|
||||||
|
|
||||||
lp.assert(pos <= len, "Client.Reader.compact precondition", .{ .pos = pos, .len = len });
|
|
||||||
|
|
||||||
// how many (if any) partial bytes do we have
|
|
||||||
const partial_bytes = len - pos;
|
|
||||||
|
|
||||||
if (partial_bytes == 0) {
|
|
||||||
// We have no partial bytes. Setting these to 0 ensures that we
|
|
||||||
// get the best utilization of our buffer
|
|
||||||
self.pos = 0;
|
|
||||||
self.len = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const partial = self.buf[pos..len];
|
|
||||||
|
|
||||||
// If we have enough bytes of the next message to tell its length
|
|
||||||
// we'll be able to figure out whether we need to do anything or not.
|
|
||||||
if (extractLengths(partial)) |length_meta| {
|
|
||||||
const next_message_len = length_meta.@"1";
|
|
||||||
// if this isn't true, then we have a full message and it
|
|
||||||
// should have been processed.
|
|
||||||
lp.assert(pos <= len, "Client.Reader.compact postcondition", .{ .next_len = next_message_len, .partial = partial_bytes });
|
|
||||||
|
|
||||||
const missing_bytes = next_message_len - partial_bytes;
|
|
||||||
|
|
||||||
const free_space = self.buf.len - len;
|
|
||||||
if (missing_bytes < free_space) {
|
|
||||||
// we have enough space in our buffer, as is,
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're here because we either don't have enough bytes of the next
|
|
||||||
// message, or we know that it won't fit in our buffer as-is.
|
|
||||||
std.mem.copyForwards(u8, self.buf, partial);
|
|
||||||
self.pos = 0;
|
|
||||||
self.len = partial_bytes;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn growBuffer(allocator: Allocator, buf: []u8, required_capacity: usize) ![]u8 {
|
|
||||||
// from std.ArrayList
|
|
||||||
var new_capacity = buf.len;
|
|
||||||
while (true) {
|
|
||||||
new_capacity +|= new_capacity / 2 + 8;
|
|
||||||
if (new_capacity >= required_capacity) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug(.app, "CDP buffer growth", .{ .from = buf.len, .to = new_capacity });
|
|
||||||
|
|
||||||
if (allocator.resize(buf, new_capacity)) {
|
|
||||||
return buf.ptr[0..new_capacity];
|
|
||||||
}
|
|
||||||
const new_buffer = try allocator.alloc(u8, new_capacity);
|
|
||||||
@memcpy(new_buffer[0..buf.len], buf);
|
|
||||||
allocator.free(buf);
|
|
||||||
return new_buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Fragments = struct {
|
|
||||||
type: Message.Type,
|
|
||||||
message: std.ArrayList(u8),
|
|
||||||
};
|
|
||||||
|
|
||||||
const Message = struct {
|
|
||||||
type: Type,
|
|
||||||
data: []const u8,
|
|
||||||
cleanup_fragment: bool,
|
|
||||||
|
|
||||||
const Type = enum {
|
|
||||||
text,
|
|
||||||
binary,
|
|
||||||
close,
|
|
||||||
ping,
|
|
||||||
pong,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// These are the only websocket types that we're currently sending
|
|
||||||
const OpCode = enum(u8) {
|
|
||||||
text = 128 | 1,
|
|
||||||
close = 128 | 8,
|
|
||||||
pong = 128 | 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn fillWebsocketHeader(buf: std.ArrayList(u8)) []const u8 {
|
|
||||||
// can't use buf[0..10] here, because the header length
|
|
||||||
// is variable. If it's just 2 bytes, for example, we need the
|
|
||||||
// framed message to be:
|
|
||||||
// h1, h2, data
|
|
||||||
// If we use buf[0..10], we'd get:
|
|
||||||
// h1, h2, 0, 0, 0, 0, 0, 0, 0, 0, data
|
|
||||||
|
|
||||||
var header_buf: [10]u8 = undefined;
|
|
||||||
|
|
||||||
// -10 because we reserved 10 bytes for the header above
|
|
||||||
const header = websocketHeader(&header_buf, .text, buf.items.len - 10);
|
|
||||||
const start = 10 - header.len;
|
|
||||||
|
|
||||||
const message = buf.items;
|
|
||||||
@memcpy(message[start..10], header);
|
|
||||||
return message[start..];
|
|
||||||
}
|
|
||||||
|
|
||||||
// makes the assumption that our caller reserved the first
|
|
||||||
// 10 bytes for the header
|
|
||||||
fn websocketHeader(buf: []u8, op_code: OpCode, payload_len: usize) []const u8 {
|
|
||||||
lp.assert(buf.len == 10, "Websocket.Header", .{ .len = buf.len });
|
|
||||||
|
|
||||||
const len = payload_len;
|
|
||||||
buf[0] = 128 | @intFromEnum(op_code); // fin | opcode
|
|
||||||
|
|
||||||
if (len <= 125) {
|
|
||||||
buf[1] = @intCast(len);
|
|
||||||
return buf[0..2];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (len < 65536) {
|
|
||||||
buf[1] = 126;
|
|
||||||
buf[2] = @intCast((len >> 8) & 0xFF);
|
|
||||||
buf[3] = @intCast(len & 0xFF);
|
|
||||||
return buf[0..4];
|
|
||||||
}
|
|
||||||
|
|
||||||
buf[1] = 127;
|
|
||||||
buf[2] = 0;
|
|
||||||
buf[3] = 0;
|
|
||||||
buf[4] = 0;
|
|
||||||
buf[5] = 0;
|
|
||||||
buf[6] = @intCast((len >> 24) & 0xFF);
|
|
||||||
buf[7] = @intCast((len >> 16) & 0xFF);
|
|
||||||
buf[8] = @intCast((len >> 8) & 0xFF);
|
|
||||||
buf[9] = @intCast(len & 0xFF);
|
|
||||||
return buf[0..10];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
// --------
|
// --------
|
||||||
|
|
||||||
@@ -1139,48 +554,6 @@ fn buildJSONVersionResponse(
|
|||||||
|
|
||||||
pub const timestamp = @import("datetime.zig").timestamp;
|
pub const timestamp = @import("datetime.zig").timestamp;
|
||||||
|
|
||||||
// In-place string lowercase
|
|
||||||
fn toLower(str: []u8) []u8 {
|
|
||||||
for (str, 0..) |c, i| {
|
|
||||||
str[i] = std.ascii.toLower(c);
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zig is in a weird backend transition right now. Need to determine if
|
|
||||||
// SIMD is even available.
|
|
||||||
const backend_supports_vectors = switch (builtin.zig_backend) {
|
|
||||||
.stage2_llvm, .stage2_c => true,
|
|
||||||
else => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Websocket messages from client->server are masked using a 4 byte XOR mask
|
|
||||||
fn mask(m: []const u8, payload: []u8) void {
|
|
||||||
var data = payload;
|
|
||||||
|
|
||||||
if (!comptime backend_supports_vectors) return simpleMask(m, data);
|
|
||||||
|
|
||||||
const vector_size = std.simd.suggestVectorLength(u8) orelse @sizeOf(usize);
|
|
||||||
if (data.len >= vector_size) {
|
|
||||||
const mask_vector = std.simd.repeat(vector_size, @as(@Vector(4, u8), m[0..4].*));
|
|
||||||
while (data.len >= vector_size) {
|
|
||||||
const slice = data[0..vector_size];
|
|
||||||
const masked_data_slice: @Vector(vector_size, u8) = slice.*;
|
|
||||||
slice.* = masked_data_slice ^ mask_vector;
|
|
||||||
data = data[vector_size..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
simpleMask(m, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used when SIMD isn't available, or for any remaining part of the message
|
|
||||||
// which is too small to effectively use SIMD.
|
|
||||||
fn simpleMask(m: []const u8, payload: []u8) void {
|
|
||||||
for (payload, 0..) |b, i| {
|
|
||||||
payload[i] = b ^ m[i & 3];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
test "server: buildJSONVersionResponse" {
|
test "server: buildJSONVersionResponse" {
|
||||||
const address = try net.Address.parseIp4("127.0.0.1", 9001);
|
const address = try net.Address.parseIp4("127.0.0.1", 9001);
|
||||||
@@ -1391,22 +764,6 @@ test "Client: close message" {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "server: mask" {
|
|
||||||
var buf: [4000]u8 = undefined;
|
|
||||||
const messages = [_][]const u8{ "1234", "1234" ** 99, "1234" ** 999 };
|
|
||||||
for (messages) |message| {
|
|
||||||
// we need the message to be mutable since mask operates in-place
|
|
||||||
const payload = buf[0..message.len];
|
|
||||||
@memcpy(payload, message);
|
|
||||||
|
|
||||||
mask(&.{ 1, 2, 200, 240 }, payload);
|
|
||||||
try testing.expectEqual(false, std.mem.eql(u8, payload, message));
|
|
||||||
|
|
||||||
mask(&.{ 1, 2, 200, 240 }, payload);
|
|
||||||
try testing.expectEqual(true, std.mem.eql(u8, payload, message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test "server: 404" {
|
test "server: 404" {
|
||||||
var c = try createTestClient();
|
var c = try createTestClient();
|
||||||
defer c.deinit();
|
defer c.deinit();
|
||||||
@@ -1542,7 +899,7 @@ fn createTestClient() !TestClient {
|
|||||||
const TestClient = struct {
|
const TestClient = struct {
|
||||||
stream: std.net.Stream,
|
stream: std.net.Stream,
|
||||||
buf: [1024]u8 = undefined,
|
buf: [1024]u8 = undefined,
|
||||||
reader: Reader(false),
|
reader: Net.Reader(false),
|
||||||
|
|
||||||
fn deinit(self: *TestClient) void {
|
fn deinit(self: *TestClient) void {
|
||||||
self.stream.close();
|
self.stream.close();
|
||||||
@@ -1609,7 +966,7 @@ const TestClient = struct {
|
|||||||
"Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n\r\n", res);
|
"Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n\r\n", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn readWebsocketMessage(self: *TestClient) !?Message {
|
fn readWebsocketMessage(self: *TestClient) !?Net.Message {
|
||||||
while (true) {
|
while (true) {
|
||||||
const n = try self.stream.read(self.reader.readBuf());
|
const n = try self.stream.read(self.reader.readBuf());
|
||||||
if (n == 0) {
|
if (n == 0) {
|
||||||
|
|||||||
@@ -87,19 +87,29 @@ pub fn closeSession(self: *Browser) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMicrotasks(self: *const Browser) void {
|
pub fn runMicrotasks(self: *Browser) void {
|
||||||
self.env.runMicrotasks();
|
self.env.runMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMacrotasks(self: *Browser) !?u64 {
|
pub fn runMacrotasks(self: *Browser) !?u64 {
|
||||||
return try self.env.runMacrotasks();
|
const env = &self.env;
|
||||||
|
|
||||||
|
const time_to_next = try self.env.runMacrotasks();
|
||||||
|
env.pumpMessageLoop();
|
||||||
|
|
||||||
|
// either of the above could have queued more microtasks
|
||||||
|
env.runMicrotasks();
|
||||||
|
|
||||||
|
return time_to_next;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMessageLoop(self: *const Browser) void {
|
pub fn hasBackgroundTasks(self: *Browser) bool {
|
||||||
while (self.env.pumpMessageLoop()) {
|
return self.env.hasBackgroundTasks();
|
||||||
if (comptime IS_DEBUG) {
|
}
|
||||||
log.debug(.browser, "pumpMessageLoop", .{});
|
pub fn waitForBackgroundTasks(self: *Browser) void {
|
||||||
}
|
self.env.waitForBackgroundTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn runIdleTasks(self: *const Browser) void {
|
||||||
self.env.runIdleTasks();
|
self.env.runIdleTasks();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,12 @@ pub const EventManager = @This();
|
|||||||
|
|
||||||
page: *Page,
|
page: *Page,
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
|
// Used as an optimization in Page._documentIsComplete. If we know there are no
|
||||||
|
// 'load' listeners in the document, we can skip dispatching the per-resource
|
||||||
|
// 'load' event (e.g. amazon product page has no listener and ~350 resources)
|
||||||
|
has_dom_load_listener: bool,
|
||||||
listener_pool: std.heap.MemoryPool(Listener),
|
listener_pool: std.heap.MemoryPool(Listener),
|
||||||
|
ignore_list: std.ArrayList(*Listener),
|
||||||
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
||||||
lookup: std.HashMapUnmanaged(
|
lookup: std.HashMapUnmanaged(
|
||||||
EventKey,
|
EventKey,
|
||||||
@@ -72,10 +77,12 @@ pub fn init(arena: Allocator, page: *Page) EventManager {
|
|||||||
.page = page,
|
.page = page,
|
||||||
.lookup = .{},
|
.lookup = .{},
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
|
.ignore_list = .{},
|
||||||
.list_pool = .init(arena),
|
.list_pool = .init(arena),
|
||||||
.listener_pool = .init(arena),
|
.listener_pool = .init(arena),
|
||||||
.dispatch_depth = 0,
|
.dispatch_depth = 0,
|
||||||
.deferred_removals = .{},
|
.deferred_removals = .{},
|
||||||
|
.has_dom_load_listener = false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +113,10 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
// Allocate the type string we'll use in both listener and key
|
// Allocate the type string we'll use in both listener and key
|
||||||
const type_string = try String.init(self.arena, typ, .{});
|
const type_string = try String.init(self.arena, typ, .{});
|
||||||
|
|
||||||
|
if (type_string.eql(comptime .wrap("load")) and target._type == .node) {
|
||||||
|
self.has_dom_load_listener = true;
|
||||||
|
}
|
||||||
|
|
||||||
const gop = try self.lookup.getOrPut(self.arena, .{
|
const gop = try self.lookup.getOrPut(self.arena, .{
|
||||||
.type_string = type_string,
|
.type_string = type_string,
|
||||||
.event_target = @intFromPtr(target),
|
.event_target = @intFromPtr(target),
|
||||||
@@ -146,6 +157,11 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
};
|
};
|
||||||
// append the listener to the list of listeners for this target
|
// append the listener to the list of listeners for this target
|
||||||
gop.value_ptr.*.append(&listener.node);
|
gop.value_ptr.*.append(&listener.node);
|
||||||
|
|
||||||
|
// Track load listeners for script execution ignore list
|
||||||
|
if (type_string.eql(comptime .wrap("load"))) {
|
||||||
|
try self.ignore_list.append(self.arena, listener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
|
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
|
||||||
@@ -158,6 +174,10 @@ pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callba
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clearIgnoreList(self: *EventManager) void {
|
||||||
|
self.ignore_list.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
// Dispatching can be recursive from the compiler's point of view, so we need to
|
// Dispatching can be recursive from the compiler's point of view, so we need to
|
||||||
// give it an explicit error set so that other parts of the code can use and
|
// give it an explicit error set so that other parts of the code can use and
|
||||||
// inferred error.
|
// inferred error.
|
||||||
@@ -169,7 +189,24 @@ const DispatchError = error{
|
|||||||
ExecutionError,
|
ExecutionError,
|
||||||
JsException,
|
JsException,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const DispatchOpts = struct {
|
||||||
|
// A "load" event triggered by a script (in ScriptManager) should not trigger
|
||||||
|
// a "load" listener added within that script. Therefore, any "load" listener
|
||||||
|
// that we add go into an ignore list until after the script finishes executing.
|
||||||
|
// The ignore list is only checked when apply_ignore == true, which is only
|
||||||
|
// set by the ScriptManager when raising the script's "load" event.
|
||||||
|
apply_ignore: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!void {
|
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!void {
|
||||||
|
return self.dispatchOpts(target, event, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void {
|
||||||
|
event.acquireRef();
|
||||||
|
defer event.deinit(false, self.page);
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
||||||
}
|
}
|
||||||
@@ -186,7 +223,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
|
|||||||
};
|
};
|
||||||
|
|
||||||
switch (target._type) {
|
switch (target._type) {
|
||||||
.node => |node| try self.dispatchNode(node, event, &was_handled),
|
.node => |node| try self.dispatchNode(node, event, &was_handled, opts),
|
||||||
.xhr,
|
.xhr,
|
||||||
.window,
|
.window,
|
||||||
.abort_signal,
|
.abort_signal,
|
||||||
@@ -197,13 +234,14 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
|
|||||||
.screen,
|
.screen,
|
||||||
.screen_orientation,
|
.screen_orientation,
|
||||||
.visual_viewport,
|
.visual_viewport,
|
||||||
|
.file_reader,
|
||||||
.generic,
|
.generic,
|
||||||
=> {
|
=> {
|
||||||
const list = self.lookup.get(.{
|
const list = self.lookup.get(.{
|
||||||
.event_target = @intFromPtr(target),
|
.event_target = @intFromPtr(target),
|
||||||
.type_string = event._type_string,
|
.type_string = event._type_string,
|
||||||
}) orelse return;
|
}) orelse return;
|
||||||
try self.dispatchAll(list, target, event, &was_handled);
|
try self.dispatchAll(list, target, event, &was_handled, opts);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,6 +256,9 @@ const DispatchWithFunctionOptions = struct {
|
|||||||
inject_target: bool = true,
|
inject_target: bool = true,
|
||||||
};
|
};
|
||||||
pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *Event, function_: ?js.Function, comptime opts: DispatchWithFunctionOptions) !void {
|
pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *Event, function_: ?js.Function, comptime opts: DispatchWithFunctionOptions) !void {
|
||||||
|
event.acquireRef();
|
||||||
|
defer event.deinit(false, self.page);
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.event, "dispatchWithFunction", .{ .type = event._type_string.str(), .context = opts.context, .has_function = function_ != null });
|
log.debug(.event, "dispatchWithFunction", .{ .type = event._type_string.str(), .context = opts.context, .has_function = function_ != null });
|
||||||
}
|
}
|
||||||
@@ -249,10 +290,10 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
|||||||
.event_target = @intFromPtr(target),
|
.event_target = @intFromPtr(target),
|
||||||
.type_string = event._type_string,
|
.type_string = event._type_string,
|
||||||
}) orelse return;
|
}) orelse return;
|
||||||
try self.dispatchAll(list, target, event, &was_dispatched);
|
try self.dispatchAll(list, target, event, &was_dispatched, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void {
|
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool, comptime opts: DispatchOpts) !void {
|
||||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||||
|
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
@@ -309,11 +350,14 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
|||||||
node = n._parent;
|
node = n._parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Even though the window isn't part of the DOM, events always propagate
|
// Even though the window isn't part of the DOM, most events propagate
|
||||||
// through it in the capture phase (unless we stopped at a shadow boundary)
|
// through it in the capture phase (unless we stopped at a shadow boundary)
|
||||||
if (path_len < path_buffer.len) {
|
// The only explicit exception is "load"
|
||||||
path_buffer[path_len] = page.window.asEventTarget();
|
if (event._type_string.eql(comptime .wrap("load")) == false) {
|
||||||
path_len += 1;
|
if (path_len < path_buffer.len) {
|
||||||
|
path_buffer[path_len] = page.window.asEventTarget();
|
||||||
|
path_len += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = path_buffer[0..path_len];
|
const path = path_buffer[0..path_len];
|
||||||
@@ -330,7 +374,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
|||||||
.event_target = @intFromPtr(current_target),
|
.event_target = @intFromPtr(current_target),
|
||||||
.type_string = event._type_string,
|
.type_string = event._type_string,
|
||||||
})) |list| {
|
})) |list| {
|
||||||
try self.dispatchPhase(list, current_target, event, was_handled, true);
|
try self.dispatchPhase(list, current_target, event, was_handled, comptime .init(true, opts));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,7 +408,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
|||||||
.type_string = event._type_string,
|
.type_string = event._type_string,
|
||||||
.event_target = @intFromPtr(target_et),
|
.event_target = @intFromPtr(target_et),
|
||||||
})) |list| {
|
})) |list| {
|
||||||
try self.dispatchPhase(list, target_et, event, was_handled, null);
|
try self.dispatchPhase(list, target_et, event, was_handled, comptime .init(null, opts));
|
||||||
if (event._stop_propagation) {
|
if (event._stop_propagation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -381,13 +425,25 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
|||||||
.type_string = event._type_string,
|
.type_string = event._type_string,
|
||||||
.event_target = @intFromPtr(current_target),
|
.event_target = @intFromPtr(current_target),
|
||||||
})) |list| {
|
})) |list| {
|
||||||
try self.dispatchPhase(list, current_target, event, was_handled, false);
|
try self.dispatchPhase(list, current_target, event, was_handled, comptime .init(false, opts));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime capture_only: ?bool) !void {
|
const DispatchPhaseOpts = struct {
|
||||||
|
capture_only: ?bool = null,
|
||||||
|
apply_ignore: bool = false,
|
||||||
|
|
||||||
|
fn init(capture_only: ?bool, opts: DispatchOpts) DispatchPhaseOpts {
|
||||||
|
return .{
|
||||||
|
.capture_only = capture_only,
|
||||||
|
.apply_ignore = opts.apply_ignore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime opts: DispatchPhaseOpts) !void {
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
|
|
||||||
// Track dispatch depth for deferred removal
|
// Track dispatch depth for deferred removal
|
||||||
@@ -413,7 +469,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
// Iterate through the list, stopping after we've encountered the last_listener
|
// Iterate through the list, stopping after we've encountered the last_listener
|
||||||
var node = list.first;
|
var node = list.first;
|
||||||
var is_done = false;
|
var is_done = false;
|
||||||
while (node) |n| {
|
node_loop: while (node) |n| {
|
||||||
if (is_done) {
|
if (is_done) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -423,7 +479,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
node = n.next;
|
node = n.next;
|
||||||
|
|
||||||
// Skip non-matching listeners
|
// Skip non-matching listeners
|
||||||
if (comptime capture_only) |capture| {
|
if (comptime opts.capture_only) |capture| {
|
||||||
if (listener.capture != capture) {
|
if (listener.capture != capture) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -442,6 +498,14 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (comptime opts.apply_ignore) {
|
||||||
|
for (self.ignore_list.items) |ignored| {
|
||||||
|
if (ignored == listener) {
|
||||||
|
continue :node_loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
|
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
|
||||||
if (listener.once) {
|
if (listener.once) {
|
||||||
self.removeListener(list, listener);
|
self.removeListener(list, listener);
|
||||||
@@ -486,8 +550,8 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Non-Node dispatching (XHR, Window without propagation)
|
// Non-Node dispatching (XHR, Window without propagation)
|
||||||
fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool) !void {
|
fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime opts: DispatchOpts) !void {
|
||||||
return self.dispatchPhase(list, current_target, event, was_handled, null);
|
return self.dispatchPhase(list, current_target, event, was_handled, comptime .init(null, opts));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?js.Function.Global {
|
fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?js.Function.Global {
|
||||||
@@ -757,7 +821,6 @@ const ActivationState = struct {
|
|||||||
.bubbles = true,
|
.bubbles = true,
|
||||||
.cancelable = false,
|
.cancelable = false,
|
||||||
}, page);
|
}, page);
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
|
|
||||||
const target = input.asElement().asEventTarget();
|
const target = input.asElement().asEventTarget();
|
||||||
try page._event_manager.dispatch(target, event);
|
try page._event_manager.dispatch(target, event);
|
||||||
|
|||||||
@@ -42,12 +42,96 @@ const Allocator = std.mem.Allocator;
|
|||||||
const IS_DEBUG = builtin.mode == .Debug;
|
const IS_DEBUG = builtin.mode == .Debug;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
// Shared across all frames of a Page.
|
||||||
const Factory = @This();
|
const Factory = @This();
|
||||||
|
|
||||||
_page: *Page,
|
|
||||||
_arena: Allocator,
|
_arena: Allocator,
|
||||||
_slab: SlabAllocator,
|
_slab: SlabAllocator,
|
||||||
|
|
||||||
|
pub fn init(arena: Allocator) !*Factory {
|
||||||
|
const self = try arena.create(Factory);
|
||||||
|
self.* = .{
|
||||||
|
._arena = arena,
|
||||||
|
._slab = SlabAllocator.init(arena, 128),
|
||||||
|
};
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a root object
|
||||||
|
pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||||
|
return self.eventTargetWithAllocator(self._slab.allocator(), child);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eventTargetWithAllocator(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ EventTarget, @TypeOf(child) },
|
||||||
|
).allocate(allocator);
|
||||||
|
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = .{
|
||||||
|
._type = unionInit(EventTarget.Type, chain.get(1)),
|
||||||
|
};
|
||||||
|
chain.setLeaf(1, child);
|
||||||
|
|
||||||
|
return chain.get(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn standaloneEventTarget(self: *Factory, child: anytype) !*EventTarget {
|
||||||
|
const allocator = self._slab.allocator();
|
||||||
|
const et = try allocator.create(EventTarget);
|
||||||
|
et.* = .{ ._type = unionInit(EventTarget.Type, child) };
|
||||||
|
return et;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a root object
|
||||||
|
pub fn event(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Event, @TypeOf(child) },
|
||||||
|
).allocate(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setLeaf(1, child);
|
||||||
|
|
||||||
|
return chain.get(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uiEvent(_: *const Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Event, UIEvent, @TypeOf(child) },
|
||||||
|
).allocate(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setMiddle(1, UIEvent.Type);
|
||||||
|
chain.setLeaf(2, child);
|
||||||
|
|
||||||
|
return chain.get(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mouseEvent(_: *const Factory, arena: Allocator, typ: String, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
|
||||||
|
const chain = try PrototypeChain(
|
||||||
|
&.{ Event, UIEvent, MouseEvent, @TypeOf(child) },
|
||||||
|
).allocate(arena);
|
||||||
|
|
||||||
|
// Special case: Event has a _type_string field, so we need manual setup
|
||||||
|
const event_ptr = chain.get(0);
|
||||||
|
event_ptr.* = try eventInit(arena, typ, chain.get(1));
|
||||||
|
chain.setMiddle(1, UIEvent.Type);
|
||||||
|
|
||||||
|
// Set MouseEvent with all its fields
|
||||||
|
const mouse_ptr = chain.get(2);
|
||||||
|
mouse_ptr.* = mouse;
|
||||||
|
mouse_ptr._proto = chain.get(1);
|
||||||
|
mouse_ptr._type = unionInit(MouseEvent.Type, chain.get(3));
|
||||||
|
|
||||||
|
chain.setLeaf(3, child);
|
||||||
|
|
||||||
|
return chain.get(3);
|
||||||
|
}
|
||||||
|
|
||||||
fn PrototypeChain(comptime types: []const type) type {
|
fn PrototypeChain(comptime types: []const type) type {
|
||||||
return struct {
|
return struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
@@ -151,94 +235,14 @@ fn AutoPrototypeChain(comptime types: []const type) type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(arena: Allocator, page: *Page) Factory {
|
fn eventInit(arena: Allocator, typ: String, value: anytype) !Event {
|
||||||
return .{
|
|
||||||
._page = page,
|
|
||||||
._arena = arena,
|
|
||||||
._slab = SlabAllocator.init(arena, 128),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is a root object
|
|
||||||
pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ EventTarget, @TypeOf(child) },
|
|
||||||
).allocate(allocator);
|
|
||||||
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = .{
|
|
||||||
._type = unionInit(EventTarget.Type, chain.get(1)),
|
|
||||||
};
|
|
||||||
chain.setLeaf(1, child);
|
|
||||||
|
|
||||||
return chain.get(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn standaloneEventTarget(self: *Factory, child: anytype) !*EventTarget {
|
|
||||||
const allocator = self._slab.allocator();
|
|
||||||
const et = try allocator.create(EventTarget);
|
|
||||||
et.* = .{ ._type = unionInit(EventTarget.Type, child) };
|
|
||||||
return et;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is a root object
|
|
||||||
pub fn event(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ Event, @TypeOf(child) },
|
|
||||||
).allocate(arena);
|
|
||||||
|
|
||||||
// Special case: Event has a _type_string field, so we need manual setup
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
|
|
||||||
chain.setLeaf(1, child);
|
|
||||||
|
|
||||||
return chain.get(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn uiEvent(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ Event, UIEvent, @TypeOf(child) },
|
|
||||||
).allocate(arena);
|
|
||||||
|
|
||||||
// Special case: Event has a _type_string field, so we need manual setup
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
|
|
||||||
chain.setMiddle(1, UIEvent.Type);
|
|
||||||
chain.setLeaf(2, child);
|
|
||||||
|
|
||||||
return chain.get(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mouseEvent(self: *Factory, arena: Allocator, typ: String, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
|
|
||||||
const chain = try PrototypeChain(
|
|
||||||
&.{ Event, UIEvent, MouseEvent, @TypeOf(child) },
|
|
||||||
).allocate(arena);
|
|
||||||
|
|
||||||
// Special case: Event has a _type_string field, so we need manual setup
|
|
||||||
const event_ptr = chain.get(0);
|
|
||||||
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
|
|
||||||
chain.setMiddle(1, UIEvent.Type);
|
|
||||||
|
|
||||||
// Set MouseEvent with all its fields
|
|
||||||
const mouse_ptr = chain.get(2);
|
|
||||||
mouse_ptr.* = mouse;
|
|
||||||
mouse_ptr._proto = chain.get(1);
|
|
||||||
mouse_ptr._type = unionInit(MouseEvent.Type, chain.get(3));
|
|
||||||
|
|
||||||
chain.setLeaf(3, child);
|
|
||||||
|
|
||||||
return chain.get(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eventInit(self: *const Factory, arena: Allocator, typ: String, value: anytype) !Event {
|
|
||||||
// Round to 2ms for privacy (browsers do this)
|
// Round to 2ms for privacy (browsers do this)
|
||||||
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
|
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
|
||||||
const time_stamp = (raw_timestamp / 2) * 2;
|
const time_stamp = (raw_timestamp / 2) * 2;
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
|
._rc = 0,
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
._page = self._page,
|
|
||||||
._type = unionInit(Event.Type, value),
|
._type = unionInit(Event.Type, value),
|
||||||
._type_string = typ,
|
._type_string = typ,
|
||||||
._time_stamp = time_stamp,
|
._time_stamp = time_stamp,
|
||||||
@@ -384,7 +388,7 @@ pub fn destroy(self: *Factory, value: anytype) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (comptime @hasField(S, "_proto")) {
|
if (comptime @hasField(S, "_proto")) {
|
||||||
self.destroyChain(value, true, 0, std.mem.Alignment.@"1");
|
self.destroyChain(value, 0, std.mem.Alignment.@"1");
|
||||||
} else {
|
} else {
|
||||||
self.destroyStandalone(value);
|
self.destroyStandalone(value);
|
||||||
}
|
}
|
||||||
@@ -398,7 +402,6 @@ pub fn destroyStandalone(self: *Factory, value: anytype) void {
|
|||||||
fn destroyChain(
|
fn destroyChain(
|
||||||
self: *Factory,
|
self: *Factory,
|
||||||
value: anytype,
|
value: anytype,
|
||||||
comptime first: bool,
|
|
||||||
old_size: usize,
|
old_size: usize,
|
||||||
old_align: std.mem.Alignment,
|
old_align: std.mem.Alignment,
|
||||||
) void {
|
) void {
|
||||||
@@ -410,23 +413,8 @@ fn destroyChain(
|
|||||||
const new_size = current_size + @sizeOf(S);
|
const new_size = current_size + @sizeOf(S);
|
||||||
const new_align = std.mem.Alignment.max(old_align, std.mem.Alignment.of(S));
|
const new_align = std.mem.Alignment.max(old_align, std.mem.Alignment.of(S));
|
||||||
|
|
||||||
// This is initially called from a deinit. We don't want to call that
|
|
||||||
// same deinit. So when this is the first time destroyChain is called
|
|
||||||
// we don't call deinit (because we're in that deinit)
|
|
||||||
if (!comptime first) {
|
|
||||||
// But if it isn't the first time
|
|
||||||
if (@hasDecl(S, "deinit")) {
|
|
||||||
// And it has a deinit, we'll call it
|
|
||||||
switch (@typeInfo(@TypeOf(S.deinit)).@"fn".params.len) {
|
|
||||||
1 => value.deinit(),
|
|
||||||
2 => value.deinit(self._page),
|
|
||||||
else => @compileLog(@typeName(S) ++ " has an invalid deinit function"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (@hasField(S, "_proto")) {
|
if (@hasField(S, "_proto")) {
|
||||||
self.destroyChain(value._proto, false, new_size, new_align);
|
self.destroyChain(value._proto, new_size, new_align);
|
||||||
} else {
|
} else {
|
||||||
// no proto so this is the head of the chain.
|
// no proto so this is the head of the chain.
|
||||||
// we use this as the ptr to the start of the chain.
|
// we use this as the ptr to the start of the chain.
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ params: []const u8 = "",
|
|||||||
// IANA defines max. charset value length as 40.
|
// IANA defines max. charset value length as 40.
|
||||||
// We keep 41 for null-termination since HTML parser expects in this format.
|
// We keep 41 for null-termination since HTML parser expects in this format.
|
||||||
charset: [41]u8 = default_charset,
|
charset: [41]u8 = default_charset,
|
||||||
charset_len: usize = 5,
|
charset_len: usize = default_charset_len,
|
||||||
|
|
||||||
/// String "UTF-8" continued by null characters.
|
/// String "UTF-8" continued by null characters.
|
||||||
pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
||||||
|
const default_charset_len = 5;
|
||||||
|
|
||||||
/// Mime with unknown Content-Type, empty params and empty charset.
|
/// Mime with unknown Content-Type, empty params and empty charset.
|
||||||
pub const unknown = Mime{ .content_type = .{ .unknown = {} } };
|
pub const unknown = Mime{ .content_type = .{ .unknown = {} } };
|
||||||
@@ -127,17 +128,17 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
|
|
||||||
const params = trimLeft(normalized[type_len..]);
|
const params = trimLeft(normalized[type_len..]);
|
||||||
|
|
||||||
var charset: [41]u8 = undefined;
|
var charset: [41]u8 = default_charset;
|
||||||
var charset_len: usize = undefined;
|
var charset_len: usize = default_charset_len;
|
||||||
|
|
||||||
var it = std.mem.splitScalar(u8, params, ';');
|
var it = std.mem.splitScalar(u8, params, ';');
|
||||||
while (it.next()) |attr| {
|
while (it.next()) |attr| {
|
||||||
const i = std.mem.indexOfScalarPos(u8, attr, 0, '=') orelse return error.Invalid;
|
const i = std.mem.indexOfScalarPos(u8, attr, 0, '=') orelse continue;
|
||||||
const name = trimLeft(attr[0..i]);
|
const name = trimLeft(attr[0..i]);
|
||||||
|
|
||||||
const value = trimRight(attr[i + 1 ..]);
|
const value = trimRight(attr[i + 1 ..]);
|
||||||
if (value.len == 0) {
|
if (value.len == 0) {
|
||||||
return error.Invalid;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attribute_name = std.meta.stringToEnum(enum {
|
const attribute_name = std.meta.stringToEnum(enum {
|
||||||
@@ -150,7 +151,7 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attribute_value = try parseCharset(value);
|
const attribute_value = parseCharset(value) catch continue;
|
||||||
@memcpy(charset[0..attribute_value.len], attribute_value);
|
@memcpy(charset[0..attribute_value.len], attribute_value);
|
||||||
// Null-terminate right after attribute value.
|
// Null-terminate right after attribute value.
|
||||||
charset[attribute_value.len] = 0;
|
charset[attribute_value.len] = 0;
|
||||||
@@ -334,6 +335,19 @@ test "Mime: invalid" {
|
|||||||
"text/ html",
|
"text/ html",
|
||||||
"text / html",
|
"text / html",
|
||||||
"text/html other",
|
"text/html other",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (invalids) |invalid| {
|
||||||
|
const mutable_input = try testing.arena_allocator.dupe(u8, invalid);
|
||||||
|
try testing.expectError(error.Invalid, Mime.parse(mutable_input));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Mime: malformed parameters are ignored" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
// These should all parse successfully as text/html with malformed params ignored
|
||||||
|
const valid_with_malformed_params = [_][]const u8{
|
||||||
"text/html; x",
|
"text/html; x",
|
||||||
"text/html; x=",
|
"text/html; x=",
|
||||||
"text/html; x= ",
|
"text/html; x= ",
|
||||||
@@ -342,11 +356,13 @@ test "Mime: invalid" {
|
|||||||
"text/html; charset=\"\"",
|
"text/html; charset=\"\"",
|
||||||
"text/html; charset=\"",
|
"text/html; charset=\"",
|
||||||
"text/html; charset=\"\\",
|
"text/html; charset=\"\\",
|
||||||
|
"text/html;\"",
|
||||||
};
|
};
|
||||||
|
|
||||||
for (invalids) |invalid| {
|
for (valid_with_malformed_params) |input| {
|
||||||
const mutable_input = try testing.arena_allocator.dupe(u8, invalid);
|
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||||
try testing.expectError(error.Invalid, Mime.parse(mutable_input));
|
const mime = try Mime.parse(mutable_input);
|
||||||
|
try testing.expectEqual(.text_html, std.meta.activeTag(mime.content_type));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +451,12 @@ test "Mime: parse charset" {
|
|||||||
.charset = "custom-non-standard-charset-value",
|
.charset = "custom-non-standard-charset-value",
|
||||||
.params = "charset=\"custom-non-standard-charset-value\"",
|
.params = "charset=\"custom-non-standard-charset-value\"",
|
||||||
}, "text/xml;charset=\"custom-non-standard-charset-value\"");
|
}, "text/xml;charset=\"custom-non-standard-charset-value\"");
|
||||||
|
|
||||||
|
try expect(.{
|
||||||
|
.content_type = .{ .text_html = {} },
|
||||||
|
.charset = "UTF-8",
|
||||||
|
.params = "x=\"",
|
||||||
|
}, "text/html;x=\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Mime: isHTML" {
|
test "Mime: isHTML" {
|
||||||
|
|||||||
@@ -63,15 +63,14 @@ const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
|
|||||||
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
|
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
|
||||||
|
|
||||||
const Http = App.Http;
|
const Http = App.Http;
|
||||||
|
const Net = @import("../Net.zig");
|
||||||
const ArenaPool = App.ArenaPool;
|
const ArenaPool = App.ArenaPool;
|
||||||
|
|
||||||
const timestamp = @import("../datetime.zig").timestamp;
|
const timestamp = @import("../datetime.zig").timestamp;
|
||||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
||||||
|
|
||||||
const WebApiURL = @import("webapi/URL.zig");
|
const WebApiURL = @import("webapi/URL.zig");
|
||||||
const global_event_handlers = @import("webapi/global_event_handlers.zig");
|
const GlobalEventHandlersLookup = @import("webapi/global_event_handlers.zig").Lookup;
|
||||||
const GlobalEventHandlersLookup = global_event_handlers.Lookup;
|
|
||||||
const GlobalEventHandler = global_event_handlers.Handler;
|
|
||||||
|
|
||||||
var default_url = WebApiURL{ ._raw = "about:blank" };
|
var default_url = WebApiURL{ ._raw = "about:blank" };
|
||||||
pub var default_location: Location = Location{ ._url = &default_url };
|
pub var default_location: Location = Location{ ._url = &default_url };
|
||||||
@@ -139,7 +138,7 @@ _blob_urls: std.StringHashMapUnmanaged(*Blob) = .{},
|
|||||||
|
|
||||||
/// `load` events that'll be fired before window's `load` event.
|
/// `load` events that'll be fired before window's `load` event.
|
||||||
/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it.
|
/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it.
|
||||||
_to_load: std.ArrayList(*Element) = .{},
|
_to_load: std.ArrayList(*Element.Html) = .{},
|
||||||
|
|
||||||
_script_manager: ScriptManager,
|
_script_manager: ScriptManager,
|
||||||
|
|
||||||
@@ -175,7 +174,7 @@ _upgrading_element: ?*Node = null,
|
|||||||
_undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{},
|
_undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{},
|
||||||
|
|
||||||
// for heap allocations and managing WebAPI objects
|
// for heap allocations and managing WebAPI objects
|
||||||
_factory: Factory,
|
_factory: *Factory,
|
||||||
|
|
||||||
_load_state: LoadState = .waiting,
|
_load_state: LoadState = .waiting,
|
||||||
|
|
||||||
@@ -249,14 +248,15 @@ pub fn init(self: *Page, id: u32, session: *Session, parent: ?*Page) !void {
|
|||||||
}
|
}
|
||||||
const browser = session.browser;
|
const browser = session.browser;
|
||||||
const arena_pool = browser.arena_pool;
|
const arena_pool = browser.arena_pool;
|
||||||
const page_arena = try arena_pool.acquire();
|
|
||||||
errdefer arena_pool.release(page_arena);
|
const page_arena = if (parent) |p| p.arena else try arena_pool.acquire();
|
||||||
|
errdefer if (parent == null) arena_pool.release(page_arena);
|
||||||
|
|
||||||
|
var factory = if (parent) |p| p._factory else try Factory.init(page_arena);
|
||||||
|
|
||||||
const call_arena = try arena_pool.acquire();
|
const call_arena = try arena_pool.acquire();
|
||||||
errdefer arena_pool.release(call_arena);
|
errdefer arena_pool.release(call_arena);
|
||||||
|
|
||||||
var factory = Factory.init(page_arena, self);
|
|
||||||
|
|
||||||
const document = (try factory.document(Node.Document.HTMLDocument{
|
const document = (try factory.document(Node.Document.HTMLDocument{
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
})).asDocument();
|
})).asDocument();
|
||||||
@@ -309,15 +309,17 @@ pub fn init(self: *Page, id: u32, session: *Session, parent: ?*Page) !void {
|
|||||||
self.js = try browser.env.createContext(self);
|
self.js = try browser.env.createContext(self);
|
||||||
errdefer self.js.deinit();
|
errdefer self.js.deinit();
|
||||||
|
|
||||||
|
document._page = self;
|
||||||
|
|
||||||
if (comptime builtin.is_test == false) {
|
if (comptime builtin.is_test == false) {
|
||||||
// HTML test runner manually calls these as necessary
|
// HTML test runner manually calls these as necessary
|
||||||
try self.js.scheduler.add(session.browser, struct {
|
try self.js.scheduler.add(session.browser, struct {
|
||||||
fn runMessageLoop(ctx: *anyopaque) !?u32 {
|
fn runIdleTasks(ctx: *anyopaque) !?u32 {
|
||||||
const b: *@import("Browser.zig") = @ptrCast(@alignCast(ctx));
|
const b: *@import("Browser.zig") = @ptrCast(@alignCast(ctx));
|
||||||
b.runMessageLoop();
|
b.runIdleTasks();
|
||||||
return 250;
|
return 200;
|
||||||
}
|
}
|
||||||
}.runMessageLoop, 250, .{ .name = "page.messageLoop" });
|
}.runIdleTasks, 200, .{ .name = "page.runIdleTasks", .low_priority = true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,13 +353,16 @@ pub fn deinit(self: *Page) void {
|
|||||||
var it = self._arena_pool_leak_track.valueIterator();
|
var it = self._arena_pool_leak_track.valueIterator();
|
||||||
while (it.next()) |value_ptr| {
|
while (it.next()) |value_ptr| {
|
||||||
if (value_ptr.count > 0) {
|
if (value_ptr.count > 0) {
|
||||||
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner, .type = self._type });
|
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner, .type = self._type, .url = self.url });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.arena_pool.release(self.call_arena);
|
self.arena_pool.release(self.call_arena);
|
||||||
self.arena_pool.release(self.arena);
|
|
||||||
|
if (self.parent == null) {
|
||||||
|
self.arena_pool.release(self.arena);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn base(self: *const Page) [:0]const u8 {
|
pub fn base(self: *const Page) [:0]const u8 {
|
||||||
@@ -420,7 +425,7 @@ pub fn releaseArena(self: *Page, allocator: Allocator) void {
|
|||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;
|
const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;
|
||||||
if (found.count != 1) {
|
if (found.count != 1) {
|
||||||
log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count, .type = self._type });
|
log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count, .type = self._type, .url = self.url });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
found.count = 0;
|
found.count = 0;
|
||||||
@@ -564,7 +569,7 @@ fn scheduleNavigationWithArena(self: *Page, arena: Allocator, request_url: []con
|
|||||||
arena,
|
arena,
|
||||||
self.base(),
|
self.base(),
|
||||||
request_url,
|
request_url,
|
||||||
.{ .always_dupe = true },
|
.{ .always_dupe = true, .encode = true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const session = self._session;
|
const session = self._session;
|
||||||
@@ -637,13 +642,12 @@ pub fn documentIsLoaded(self: *Page) void {
|
|||||||
self._load_state = .load;
|
self._load_state = .load;
|
||||||
self.document._ready_state = .interactive;
|
self.document._ready_state = .interactive;
|
||||||
self._documentIsLoaded() catch |err| {
|
self._documentIsLoaded() catch |err| {
|
||||||
log.err(.page, "document is loaded", .{ .err = err, .type = self._type });
|
log.err(.page, "document is loaded", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _documentIsLoaded(self: *Page) !void {
|
pub fn _documentIsLoaded(self: *Page) !void {
|
||||||
const event = try Event.initTrusted(.wrap("DOMContentLoaded"), .{ .bubbles = true }, self);
|
const event = try Event.initTrusted(.wrap("DOMContentLoaded"), .{ .bubbles = true }, self);
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
try self._event_manager.dispatch(
|
try self._event_manager.dispatch(
|
||||||
self.document.asEventTarget(),
|
self.document.asEventTarget(),
|
||||||
event,
|
event,
|
||||||
@@ -661,10 +665,9 @@ pub fn iframeCompletedLoading(self: *Page, iframe: *Element.Html.IFrame) void {
|
|||||||
defer ls.deinit();
|
defer ls.deinit();
|
||||||
|
|
||||||
const event = Event.initTrusted(comptime .wrap("load"), .{}, self) catch |err| {
|
const event = Event.initTrusted(comptime .wrap("load"), .{}, self) catch |err| {
|
||||||
log.err(.page, "iframe event init", .{ .err = err });
|
log.err(.page, "iframe event init", .{ .err = err, .url = iframe._src });
|
||||||
break :blk;
|
break :blk;
|
||||||
};
|
};
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
self._event_manager.dispatch(iframe.asNode().asEventTarget(), event) catch |err| {
|
self._event_manager.dispatch(iframe.asNode().asEventTarget(), event) catch |err| {
|
||||||
log.warn(.js, "iframe onload", .{ .err = err, .url = iframe._src });
|
log.warn(.js, "iframe onload", .{ .err = err, .url = iframe._src });
|
||||||
};
|
};
|
||||||
@@ -701,7 +704,7 @@ pub fn documentIsComplete(self: *Page) void {
|
|||||||
|
|
||||||
self._load_state = .complete;
|
self._load_state = .complete;
|
||||||
self._documentIsComplete() catch |err| {
|
self._documentIsComplete() catch |err| {
|
||||||
log.err(.page, "document is complete", .{ .err = err, .type = self._type });
|
log.err(.page, "document is complete", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IS_DEBUG) {
|
if (IS_DEBUG) {
|
||||||
@@ -720,23 +723,15 @@ pub fn documentIsComplete(self: *Page) void {
|
|||||||
fn _documentIsComplete(self: *Page) !void {
|
fn _documentIsComplete(self: *Page) !void {
|
||||||
self.document._ready_state = .complete;
|
self.document._ready_state = .complete;
|
||||||
|
|
||||||
|
// Run load events before window.load.
|
||||||
|
try self.dispatchLoad();
|
||||||
|
|
||||||
var ls: JS.Local.Scope = undefined;
|
var ls: JS.Local.Scope = undefined;
|
||||||
self.js.localScope(&ls);
|
self.js.localScope(&ls);
|
||||||
defer ls.deinit();
|
defer ls.deinit();
|
||||||
|
|
||||||
// Dispatch `_to_load` events before window.load.
|
|
||||||
for (self._to_load.items) |element| {
|
|
||||||
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
try self._event_manager.dispatch(element.asEventTarget(), event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// `_to_load` can be cleaned here.
|
|
||||||
self._to_load.clearAndFree(self.arena);
|
|
||||||
|
|
||||||
// Dispatch window.load event.
|
// Dispatch window.load event.
|
||||||
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
// This event is weird, it's dispatched directly on the window, but
|
// This event is weird, it's dispatched directly on the window, but
|
||||||
// with the document as the target.
|
// with the document as the target.
|
||||||
event._target = self.document.asEventTarget();
|
event._target = self.document.asEventTarget();
|
||||||
@@ -748,7 +743,6 @@ fn _documentIsComplete(self: *Page) !void {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent();
|
const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent();
|
||||||
defer if (!pageshow_event._v8_handoff) pageshow_event.deinit(false);
|
|
||||||
try self._event_manager.dispatchWithFunction(
|
try self._event_manager.dispatchWithFunction(
|
||||||
self.window.asEventTarget(),
|
self.window.asEventTarget(),
|
||||||
pageshow_event,
|
pageshow_event,
|
||||||
@@ -806,7 +800,7 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
|||||||
} orelse .unknown;
|
} orelse .unknown;
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.page, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len, .type = self._type });
|
log.debug(.page, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len, .type = self._type, .url = self.url });
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (mime.content_type) {
|
switch (mime.content_type) {
|
||||||
@@ -853,7 +847,7 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
|
|||||||
var self: *Page = @ptrCast(@alignCast(ctx));
|
var self: *Page = @ptrCast(@alignCast(ctx));
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.page, "navigate done", .{ .type = self._type });
|
log.debug(.page, "navigate done", .{ .type = self._type, .url = self.url });
|
||||||
}
|
}
|
||||||
|
|
||||||
//We need to handle different navigation types differently.
|
//We need to handle different navigation types differently.
|
||||||
@@ -872,11 +866,6 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
|
|||||||
.html => |buf| {
|
.html => |buf| {
|
||||||
parser.parse(buf.items);
|
parser.parse(buf.items);
|
||||||
self._script_manager.staticScriptsDone();
|
self._script_manager.staticScriptsDone();
|
||||||
if (self._script_manager.isDone()) {
|
|
||||||
// No scripts, or just inline scripts that were already processed
|
|
||||||
// we need to trigger this ourselves
|
|
||||||
self.documentIsComplete();
|
|
||||||
}
|
|
||||||
self._parse_state = .complete;
|
self._parse_state = .complete;
|
||||||
},
|
},
|
||||||
.text => |*buf| {
|
.text => |*buf| {
|
||||||
@@ -931,248 +920,17 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
|
|||||||
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||||
var self: *Page = @ptrCast(@alignCast(ctx));
|
var self: *Page = @ptrCast(@alignCast(ctx));
|
||||||
|
|
||||||
log.err(.page, "navigate failed", .{ .err = err, .type = self._type });
|
log.err(.page, "navigate failed", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
self._parse_state = .{ .err = err };
|
self._parse_state = .{ .err = err };
|
||||||
|
|
||||||
// In case of error, we want to complete the page with a custom HTML
|
// In case of error, we want to complete the page with a custom HTML
|
||||||
// containing the error.
|
// containing the error.
|
||||||
pageDoneCallback(ctx) catch |e| {
|
pageDoneCallback(ctx) catch |e| {
|
||||||
log.err(.browser, "pageErrorCallback", .{ .err = e, .type = self._type });
|
log.err(.browser, "pageErrorCallback", .{ .err = e, .type = self._type, .url = self.url });
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wait(self: *Page, wait_ms: u32) Session.WaitResult {
|
|
||||||
return self._wait(wait_ms) catch |err| {
|
|
||||||
switch (err) {
|
|
||||||
error.JsError => {}, // already logged (with hopefully more context)
|
|
||||||
else => {
|
|
||||||
// There may be errors from the http/client or ScriptManager
|
|
||||||
// that we should not treat as an error like this. Will need
|
|
||||||
// to run this through more real-world sites and see if we need
|
|
||||||
// to expand the switch (err) to have more customized logs for
|
|
||||||
// specific messages.
|
|
||||||
log.err(.browser, "page wait", .{ .err = err, .type = self._type });
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return .done;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
|
|
||||||
if (comptime IS_DEBUG) {
|
|
||||||
std.debug.assert(self._type == .root);
|
|
||||||
}
|
|
||||||
|
|
||||||
var timer = try std.time.Timer.start();
|
|
||||||
var ms_remaining = wait_ms;
|
|
||||||
|
|
||||||
const browser = self._session.browser;
|
|
||||||
var http_client = browser.http_client;
|
|
||||||
|
|
||||||
// I'd like the page to know NOTHING about cdp_socket / CDP, but the
|
|
||||||
// fact is that the behavior of wait changes depending on whether or
|
|
||||||
// not we're using CDP.
|
|
||||||
// If we aren't using CDP, as soon as we think there's nothing left
|
|
||||||
// to do, we can exit - we'de done.
|
|
||||||
// But if we are using CDP, we should wait for the whole `wait_ms`
|
|
||||||
// because the http_click.tick() also monitors the CDP socket. And while
|
|
||||||
// we could let CDP poll http (like it does for HTTP requests), the fact
|
|
||||||
// is that we know more about the timing of stuff (e.g. how long to
|
|
||||||
// poll/sleep) in the page.
|
|
||||||
const exit_when_done = http_client.cdp_client == null;
|
|
||||||
|
|
||||||
// for debugging
|
|
||||||
// defer self.printWaitAnalysis();
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
switch (self._parse_state) {
|
|
||||||
.pre, .raw, .text, .image => {
|
|
||||||
// The main page hasn't started/finished navigating.
|
|
||||||
// There's no JS to run, and no reason to run the scheduler.
|
|
||||||
if (http_client.active == 0 and exit_when_done) {
|
|
||||||
// haven't started navigating, I guess.
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
// Either we have active http connections, or we're in CDP
|
|
||||||
// mode with an extra socket. Either way, we're waiting
|
|
||||||
// for http traffic
|
|
||||||
if (try http_client.tick(@intCast(ms_remaining)) == .cdp_socket) {
|
|
||||||
// exit_when_done is explicitly set when there isn't
|
|
||||||
// an extra socket, so it should not be possibl to
|
|
||||||
// get an cdp_socket message when exit_when_done
|
|
||||||
// is true.
|
|
||||||
if (IS_DEBUG) {
|
|
||||||
std.debug.assert(exit_when_done == false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// data on a socket we aren't handling, return to caller
|
|
||||||
return .cdp_socket;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.html, .complete => {
|
|
||||||
if (self._queued_navigation != null) {
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The HTML page was parsed. We now either have JS scripts to
|
|
||||||
// download, or scheduled tasks to execute, or both.
|
|
||||||
|
|
||||||
// scheduler.run could trigger new http transfers, so do not
|
|
||||||
// store http_client.active BEFORE this call and then use
|
|
||||||
// it AFTER.
|
|
||||||
const ms_to_next_task = try browser.runMacrotasks();
|
|
||||||
|
|
||||||
const http_active = http_client.active;
|
|
||||||
const total_network_activity = http_active + http_client.intercepted;
|
|
||||||
if (self._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
|
||||||
self.notifyNetworkAlmostIdle();
|
|
||||||
}
|
|
||||||
if (self._notified_network_idle.check(total_network_activity == 0)) {
|
|
||||||
self.notifyNetworkIdle();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (http_active == 0 and exit_when_done) {
|
|
||||||
// we don't need to consider http_client.intercepted here
|
|
||||||
// because exit_when_done is true, and that can only be
|
|
||||||
// the case when interception isn't possible.
|
|
||||||
if (comptime IS_DEBUG) {
|
|
||||||
std.debug.assert(http_client.intercepted == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ms = ms_to_next_task orelse blk: {
|
|
||||||
if (wait_ms - ms_remaining < 100) {
|
|
||||||
if (comptime builtin.is_test) {
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
// Look, we want to exit ASAP, but we don't want
|
|
||||||
// to exit so fast that we've run none of the
|
|
||||||
// background jobs.
|
|
||||||
break :blk 50;
|
|
||||||
}
|
|
||||||
// No http transfers, no cdp extra socket, no
|
|
||||||
// scheduled tasks, we're done.
|
|
||||||
return .done;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ms > ms_remaining) {
|
|
||||||
// Same as above, except we have a scheduled task,
|
|
||||||
// it just happens to be too far into the future
|
|
||||||
// compared to how long we were told to wait.
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have a task to run in the not-so-distant future.
|
|
||||||
// You might think we can just sleep until that task is
|
|
||||||
// ready, but we should continue to run lowPriority tasks
|
|
||||||
// in the meantime, and that could unblock things. So
|
|
||||||
// we'll just sleep for a bit, and then restart our wait
|
|
||||||
// loop to see if anything new can be processed.
|
|
||||||
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20))));
|
|
||||||
} else {
|
|
||||||
// We're here because we either have active HTTP
|
|
||||||
// connections, or exit_when_done == false (aka, there's
|
|
||||||
// an cdp_socket registered with the http client).
|
|
||||||
// We should continue to run lowPriority tasks, so we
|
|
||||||
// minimize how long we'll poll for network I/O.
|
|
||||||
const ms_to_wait = @min(200, @min(ms_remaining, ms_to_next_task orelse 200));
|
|
||||||
if (try http_client.tick(ms_to_wait) == .cdp_socket) {
|
|
||||||
// data on a socket we aren't handling, return to caller
|
|
||||||
return .cdp_socket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.err => |err| {
|
|
||||||
self._parse_state = .{ .raw_done = @errorName(err) };
|
|
||||||
return err;
|
|
||||||
},
|
|
||||||
.raw_done => {
|
|
||||||
if (exit_when_done) {
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
// we _could_ http_client.tick(ms_to_wait), but this has
|
|
||||||
// the same result, and I feel is more correct.
|
|
||||||
return .no_page;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const ms_elapsed = timer.lap() / 1_000_000;
|
|
||||||
if (ms_elapsed >= ms_remaining) {
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
ms_remaining -= @intCast(ms_elapsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn printWaitAnalysis(self: *Page) void {
|
|
||||||
std.debug.print("load_state: {s}\n", .{@tagName(self._load_state)});
|
|
||||||
std.debug.print("parse_state: {s}\n", .{@tagName(std.meta.activeTag(self._parse_state))});
|
|
||||||
{
|
|
||||||
std.debug.print("\nactive requests: {d}\n", .{self._session.browser.http_client.active});
|
|
||||||
var n_ = self._session.browser.http_client.handles.in_use.first;
|
|
||||||
while (n_) |n| {
|
|
||||||
const handle: *Http.Client.Handle = @fieldParentPtr("node", n);
|
|
||||||
const transfer = Http.Transfer.fromEasy(handle.conn.easy) catch |err| {
|
|
||||||
std.debug.print(" - failed to load transfer: {any}\n", .{err});
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
std.debug.print(" - {f}\n", .{transfer});
|
|
||||||
n_ = n.next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std.debug.print("\nqueued requests: {d}\n", .{self._session.browser.http_client.queue.len()});
|
|
||||||
var n_ = self._session.browser.http_client.queue.first;
|
|
||||||
while (n_) |n| {
|
|
||||||
const transfer: *Http.Transfer = @fieldParentPtr("_node", n);
|
|
||||||
std.debug.print(" - {f}\n", .{transfer});
|
|
||||||
n_ = n.next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std.debug.print("\ndeferreds: {d}\n", .{self._script_manager.defer_scripts.len()});
|
|
||||||
var n_ = self._script_manager.defer_scripts.first;
|
|
||||||
while (n_) |n| {
|
|
||||||
const script: *ScriptManager.Script = @fieldParentPtr("node", n);
|
|
||||||
std.debug.print(" - {s} complete: {any}\n", .{ script.url, script.complete });
|
|
||||||
n_ = n.next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std.debug.print("\nasyncs: {d}\n", .{self._script_manager.async_scripts.len()});
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std.debug.print("\nasyncs ready: {d}\n", .{self._script_manager.ready_scripts.len()});
|
|
||||||
var n_ = self._script_manager.ready_scripts.first;
|
|
||||||
while (n_) |n| {
|
|
||||||
const script: *ScriptManager.Script = @fieldParentPtr("node", n);
|
|
||||||
std.debug.print(" - {s} complete: {any}\n", .{ script.url, script.complete });
|
|
||||||
n_ = n.next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = milliTimestamp(.monotonic);
|
|
||||||
{
|
|
||||||
std.debug.print("\nhigh_priority schedule: {d}\n", .{self.js.scheduler.high_priority.count()});
|
|
||||||
var it = self.js.scheduler.high_priority.iterator();
|
|
||||||
while (it.next()) |task| {
|
|
||||||
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.run_at - now });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std.debug.print("\nlow_priority schedule: {d}\n", .{self.js.scheduler.low_priority.count()});
|
|
||||||
var it = self.js.scheduler.low_priority.iterator();
|
|
||||||
while (it.next()) |task| {
|
|
||||||
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.run_at - now });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn isGoingAway(self: *const Page) bool {
|
pub fn isGoingAway(self: *const Page) bool {
|
||||||
return self._queued_navigation != null;
|
return self._queued_navigation != null;
|
||||||
}
|
}
|
||||||
@@ -1186,6 +944,7 @@ pub fn scriptAddedCallback(self: *Page, comptime from_parser: bool, script: *Ele
|
|||||||
self._script_manager.addFromElement(from_parser, script, "parsing") catch |err| {
|
self._script_manager.addFromElement(from_parser, script, "parsing") catch |err| {
|
||||||
log.err(.page, "page.scriptAddedCallback", .{
|
log.err(.page, "page.scriptAddedCallback", .{
|
||||||
.err = err,
|
.err = err,
|
||||||
|
.url = self.url,
|
||||||
.src = script.asElement().getAttributeSafe(comptime .wrap("src")),
|
.src = script.asElement().getAttributeSafe(comptime .wrap("src")),
|
||||||
.type = self._type,
|
.type = self._type,
|
||||||
});
|
});
|
||||||
@@ -1201,7 +960,7 @@ pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const src = try iframe.getSrc(self);
|
const src = iframe.asElement().getAttributeSafe(comptime .wrap("src")) orelse return;
|
||||||
if (src.len == 0) {
|
if (src.len == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1223,8 +982,16 @@ pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void {
|
|||||||
.timestamp = timestamp(.monotonic),
|
.timestamp = timestamp(.monotonic),
|
||||||
});
|
});
|
||||||
|
|
||||||
page_frame.navigate(src, .{ .reason = .initialFrameNavigation }) catch |err| {
|
// navigate will dupe the url
|
||||||
log.warn(.page, "iframe navigate failure", .{ .url = src, .err = err });
|
const url = try URL.resolve(
|
||||||
|
self.call_arena,
|
||||||
|
self.base(),
|
||||||
|
src,
|
||||||
|
.{ .encode = true },
|
||||||
|
);
|
||||||
|
|
||||||
|
page_frame.navigate(url, .{ .reason = .initialFrameNavigation }) catch |err| {
|
||||||
|
log.warn(.page, "iframe navigate failure", .{ .url = url, .err = err });
|
||||||
self._pending_loads -= 1;
|
self._pending_loads -= 1;
|
||||||
iframe._content_window = null;
|
iframe._content_window = null;
|
||||||
page_frame.deinit();
|
page_frame.deinit();
|
||||||
@@ -1272,7 +1039,7 @@ pub fn domChanged(self: *Page) void {
|
|||||||
|
|
||||||
self._intersection_check_scheduled = true;
|
self._intersection_check_scheduled = true;
|
||||||
self.js.queueIntersectionChecks() catch |err| {
|
self.js.queueIntersectionChecks() catch |err| {
|
||||||
log.err(.page, "page.schedIntersectChecks", .{ .err = err, .type = self._type });
|
log.err(.page, "page.schedIntersectChecks", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1351,29 +1118,6 @@ pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Elemen
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets an inline event listener (`onload`, `onclick`, `onwheel` etc.);
|
|
||||||
/// overrides the listener if there's already one.
|
|
||||||
pub fn setAttrListener(
|
|
||||||
self: *Page,
|
|
||||||
element: *Element,
|
|
||||||
listener_type: GlobalEventHandler,
|
|
||||||
listener_callback: JS.Function.Global,
|
|
||||||
) !void {
|
|
||||||
if (comptime IS_DEBUG) {
|
|
||||||
log.debug(.event, "Page.setAttrListener", .{
|
|
||||||
.element = element,
|
|
||||||
.listener_type = listener_type,
|
|
||||||
.type = self._type,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const gop = try self._element_attr_listeners.getOrPut(self.arena, .{
|
|
||||||
.target = element.asEventTarget(),
|
|
||||||
.handler = listener_type,
|
|
||||||
});
|
|
||||||
gop.value_ptr.* = listener_callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void {
|
pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void {
|
||||||
return self._performance_observers.append(self.arena, observer);
|
return self._performance_observers.append(self.arena, observer);
|
||||||
}
|
}
|
||||||
@@ -1393,7 +1137,7 @@ pub fn notifyPerformanceObservers(self: *Page, entry: *Performance.Entry) !void
|
|||||||
for (self._performance_observers.items) |observer| {
|
for (self._performance_observers.items) |observer| {
|
||||||
if (observer.interested(entry)) {
|
if (observer.interested(entry)) {
|
||||||
observer._entries.append(self.arena, entry) catch |err| {
|
observer._entries.append(self.arena, entry) catch |err| {
|
||||||
log.err(.page, "notifyPerformanceObservers", .{ .err = err, .type = self._type });
|
log.err(.page, "notifyPerformanceObservers", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1458,6 +1202,18 @@ pub fn checkIntersections(self: *Page) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dispatchLoad(self: *Page) !void {
|
||||||
|
const has_dom_load_listener = self._event_manager.has_dom_load_listener;
|
||||||
|
for (self._to_load.items) |html_element| {
|
||||||
|
if (has_dom_load_listener or html_element.hasAttributeFunction(.onload, self)) {
|
||||||
|
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
||||||
|
try self._event_manager.dispatch(html_element.asEventTarget(), event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We drained everything.
|
||||||
|
self._to_load.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scheduleMutationDelivery(self: *Page) !void {
|
pub fn scheduleMutationDelivery(self: *Page) !void {
|
||||||
if (self._mutation_delivery_scheduled) {
|
if (self._mutation_delivery_scheduled) {
|
||||||
return;
|
return;
|
||||||
@@ -1488,7 +1244,7 @@ pub fn performScheduledIntersectionChecks(self: *Page) void {
|
|||||||
}
|
}
|
||||||
self._intersection_check_scheduled = false;
|
self._intersection_check_scheduled = false;
|
||||||
self.checkIntersections() catch |err| {
|
self.checkIntersections() catch |err| {
|
||||||
log.err(.page, "page.schedIntersectChecks", .{ .err = err, .type = self._type });
|
log.err(.page, "page.schedIntersectChecks", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1504,7 +1260,7 @@ pub fn deliverIntersections(self: *Page) void {
|
|||||||
i -= 1;
|
i -= 1;
|
||||||
const observer = self._intersection_observers.items[i];
|
const observer = self._intersection_observers.items[i];
|
||||||
observer.deliverEntries(self) catch |err| {
|
observer.deliverEntries(self) catch |err| {
|
||||||
log.err(.page, "page.deliverIntersections", .{ .err = err, .type = self._type });
|
log.err(.page, "page.deliverIntersections", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1522,7 +1278,7 @@ pub fn deliverMutations(self: *Page) void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (self._mutation_delivery_depth > 100) {
|
if (self._mutation_delivery_depth > 100) {
|
||||||
log.err(.page, "page.MutationLimit", .{ .type = self._type });
|
log.err(.page, "page.MutationLimit", .{ .type = self._type, .url = self.url });
|
||||||
self._mutation_delivery_depth = 0;
|
self._mutation_delivery_depth = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1531,7 +1287,7 @@ pub fn deliverMutations(self: *Page) void {
|
|||||||
while (it) |node| : (it = node.next) {
|
while (it) |node| : (it = node.next) {
|
||||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||||
observer.deliverRecords(self) catch |err| {
|
observer.deliverRecords(self) catch |err| {
|
||||||
log.err(.page, "page.deliverMutations", .{ .err = err, .type = self._type });
|
log.err(.page, "page.deliverMutations", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1549,7 +1305,7 @@ pub fn deliverSlotchangeEvents(self: *Page) void {
|
|||||||
|
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
var slots = self.call_arena.alloc(*Element.Html.Slot, pending) catch |err| {
|
var slots = self.call_arena.alloc(*Element.Html.Slot, pending) catch |err| {
|
||||||
log.err(.page, "deliverSlotchange.append", .{ .err = err, .type = self._type });
|
log.err(.page, "deliverSlotchange.append", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1562,14 +1318,12 @@ pub fn deliverSlotchangeEvents(self: *Page) void {
|
|||||||
|
|
||||||
for (slots) |slot| {
|
for (slots) |slot| {
|
||||||
const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self) catch |err| {
|
const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self) catch |err| {
|
||||||
log.err(.page, "deliverSlotchange.init", .{ .err = err, .type = self._type });
|
log.err(.page, "deliverSlotchange.init", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
|
|
||||||
const target = slot.asNode().asEventTarget();
|
const target = slot.asNode().asEventTarget();
|
||||||
_ = target.dispatchEvent(event, self) catch |err| {
|
_ = target.dispatchEvent(event, self) catch |err| {
|
||||||
log.err(.page, "deliverSlotchange.dispatch", .{ .err = err, .type = self._type });
|
log.err(.page, "deliverSlotchange.dispatch", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1601,10 +1355,8 @@ pub fn appendNew(self: *Page, parent: *Node, child: Node.NodeOrText) !void {
|
|||||||
if (parent.lastChild()) |sibling| {
|
if (parent.lastChild()) |sibling| {
|
||||||
if (sibling.is(CData.Text)) |tn| {
|
if (sibling.is(CData.Text)) |tn| {
|
||||||
const cdata = tn._proto;
|
const cdata = tn._proto;
|
||||||
const existing = cdata.getData();
|
const existing = cdata.getData().str();
|
||||||
// @metric
|
cdata._data = try String.concat(self.arena, &.{ existing, txt });
|
||||||
// Inefficient, but we don't expect this to happen often.
|
|
||||||
cdata._data = try std.mem.concat(self.arena, u8, &.{ existing, txt });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1624,7 +1376,7 @@ pub fn appendNew(self: *Page, parent: *Node, child: Node.NodeOrText) !void {
|
|||||||
// called from the parser when the node and all its children have been added
|
// called from the parser when the node and all its children have been added
|
||||||
pub fn nodeComplete(self: *Page, node: *Node) !void {
|
pub fn nodeComplete(self: *Page, node: *Node) !void {
|
||||||
Node.Build.call(node, "complete", .{ node, self }) catch |err| {
|
Node.Build.call(node, "complete", .{ node, self }) catch |err| {
|
||||||
log.err(.bug, "build.complete", .{ .tag = node.getNodeName(&self.buf), .err = err, .type = self._type });
|
log.err(.bug, "build.complete", .{ .tag = node.getNodeName(&self.buf), .err = err, .type = self._type, .url = self.url });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
return self.nodeIsReady(true, node);
|
return self.nodeIsReady(true, node);
|
||||||
@@ -2323,7 +2075,7 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
|
|||||||
|
|
||||||
var caught: JS.TryCatch.Caught = undefined;
|
var caught: JS.TryCatch.Caught = undefined;
|
||||||
_ = ls.toLocal(def.constructor).newInstance(&caught) catch |err| {
|
_ = ls.toLocal(def.constructor).newInstance(&caught) catch |err| {
|
||||||
log.warn(.js, "custom element constructor", .{ .name = name, .err = err, .caught = caught, .type = self._type });
|
log.warn(.js, "custom element constructor", .{ .name = name, .err = err, .caught = caught, .type = self._type, .url = self.url });
|
||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2381,7 +2133,7 @@ fn createHtmlElementT(self: *Page, comptime E: type, namespace: Element.Namespac
|
|||||||
const node = element.asNode();
|
const node = element.asNode();
|
||||||
if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) {
|
if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) {
|
||||||
@call(.auto, @field(E.Build, "created"), .{ node, self }) catch |err| {
|
@call(.auto, @field(E.Build, "created"), .{ node, self }) catch |err| {
|
||||||
log.err(.page, "build.created", .{ .tag = node.getNodeName(&self.buf), .err = err, .type = self._type });
|
log.err(.page, "build.created", .{ .tag = node.getNodeName(&self.buf), .err = err, .type = self._type, .url = self.url });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2434,28 +2186,24 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn createTextNode(self: *Page, text: []const u8) !*Node {
|
pub fn createTextNode(self: *Page, text: []const u8) !*Node {
|
||||||
// might seem unlikely that we get an intern hit, but we'll get some nodes
|
|
||||||
// with just '\n'
|
|
||||||
const owned_text = try self.dupeString(text);
|
|
||||||
const cd = try self._factory.node(CData{
|
const cd = try self._factory.node(CData{
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
._type = .{ .text = .{
|
._type = .{ .text = .{
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
} },
|
} },
|
||||||
._data = owned_text,
|
._data = try self.dupeSSO(text),
|
||||||
});
|
});
|
||||||
cd._type.text._proto = cd;
|
cd._type.text._proto = cd;
|
||||||
return cd.asNode();
|
return cd.asNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createComment(self: *Page, text: []const u8) !*Node {
|
pub fn createComment(self: *Page, text: []const u8) !*Node {
|
||||||
const owned_text = try self.dupeString(text);
|
|
||||||
const cd = try self._factory.node(CData{
|
const cd = try self._factory.node(CData{
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
._type = .{ .comment = .{
|
._type = .{ .comment = .{
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
} },
|
} },
|
||||||
._data = owned_text,
|
._data = try self.dupeSSO(text),
|
||||||
});
|
});
|
||||||
cd._type.comment._proto = cd;
|
cd._type.comment._proto = cd;
|
||||||
return cd.asNode();
|
return cd.asNode();
|
||||||
@@ -2467,8 +2215,6 @@ pub fn createCDATASection(self: *Page, data: []const u8) !*Node {
|
|||||||
return error.InvalidCharacterError;
|
return error.InvalidCharacterError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const owned_data = try self.dupeString(data);
|
|
||||||
|
|
||||||
// First allocate the Text node separately
|
// First allocate the Text node separately
|
||||||
const text_node = try self._factory.create(CData.Text{
|
const text_node = try self._factory.create(CData.Text{
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
@@ -2480,7 +2226,7 @@ pub fn createCDATASection(self: *Page, data: []const u8) !*Node {
|
|||||||
._type = .{ .cdata_section = .{
|
._type = .{ .cdata_section = .{
|
||||||
._proto = text_node,
|
._proto = text_node,
|
||||||
} },
|
} },
|
||||||
._data = owned_data,
|
._data = try self.dupeSSO(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up the back pointer from Text to CData
|
// Set up the back pointer from Text to CData
|
||||||
@@ -2502,7 +2248,6 @@ pub fn createProcessingInstruction(self: *Page, target: []const u8, data: []cons
|
|||||||
try validateXmlName(target);
|
try validateXmlName(target);
|
||||||
|
|
||||||
const owned_target = try self.dupeString(target);
|
const owned_target = try self.dupeString(target);
|
||||||
const owned_data = try self.dupeString(data);
|
|
||||||
|
|
||||||
const pi = try self._factory.create(CData.ProcessingInstruction{
|
const pi = try self._factory.create(CData.ProcessingInstruction{
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
@@ -2512,7 +2257,7 @@ pub fn createProcessingInstruction(self: *Page, target: []const u8, data: []cons
|
|||||||
const cd = try self._factory.node(CData{
|
const cd = try self._factory.node(CData{
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
._type = .{ .processing_instruction = pi },
|
._type = .{ .processing_instruction = pi },
|
||||||
._data = owned_data,
|
._data = try self.dupeSSO(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up the back pointer from ProcessingInstruction to CData
|
// Set up the back pointer from ProcessingInstruction to CData
|
||||||
@@ -2585,6 +2330,10 @@ pub fn dupeString(self: *Page, value: []const u8) ![]const u8 {
|
|||||||
return self.arena.dupe(u8, value);
|
return self.arena.dupe(u8, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dupeSSO(self: *Page, value: []const u8) !String {
|
||||||
|
return String.init(self.arena, value, .{ .dupe = true });
|
||||||
|
}
|
||||||
|
|
||||||
const RemoveNodeOpts = struct {
|
const RemoveNodeOpts = struct {
|
||||||
will_be_reconnected: bool,
|
will_be_reconnected: bool,
|
||||||
};
|
};
|
||||||
@@ -2851,7 +2600,7 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
|
|||||||
|
|
||||||
pub fn attributeChange(self: *Page, element: *Element, name: String, value: String, old_value: ?String) void {
|
pub fn attributeChange(self: *Page, element: *Element, name: String, value: String, old_value: ?String) void {
|
||||||
_ = Element.Build.call(element, "attributeChange", .{ element, name, value, self }) catch |err| {
|
_ = Element.Build.call(element, "attributeChange", .{ element, name, value, self }) catch |err| {
|
||||||
log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err, .type = self._type });
|
log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
|
|
||||||
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, value, self);
|
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, value, self);
|
||||||
@@ -2860,7 +2609,7 @@ pub fn attributeChange(self: *Page, element: *Element, name: String, value: Stri
|
|||||||
while (it) |node| : (it = node.next) {
|
while (it) |node| : (it = node.next) {
|
||||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||||
observer.notifyAttributeChange(element, name, old_value, self) catch |err| {
|
observer.notifyAttributeChange(element, name, old_value, self) catch |err| {
|
||||||
log.err(.page, "attributeChange.notifyObserver", .{ .err = err, .type = self._type });
|
log.err(.page, "attributeChange.notifyObserver", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2877,7 +2626,7 @@ pub fn attributeChange(self: *Page, element: *Element, name: String, value: Stri
|
|||||||
|
|
||||||
pub fn attributeRemove(self: *Page, element: *Element, name: String, old_value: String) void {
|
pub fn attributeRemove(self: *Page, element: *Element, name: String, old_value: String) void {
|
||||||
_ = Element.Build.call(element, "attributeRemove", .{ element, name, self }) catch |err| {
|
_ = Element.Build.call(element, "attributeRemove", .{ element, name, self }) catch |err| {
|
||||||
log.err(.bug, "build.attributeRemove", .{ .tag = element.getTag(), .name = name, .err = err, .type = self._type });
|
log.err(.bug, "build.attributeRemove", .{ .tag = element.getTag(), .name = name, .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
|
|
||||||
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, null, self);
|
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, null, self);
|
||||||
@@ -2886,7 +2635,7 @@ pub fn attributeRemove(self: *Page, element: *Element, name: String, old_value:
|
|||||||
while (it) |node| : (it = node.next) {
|
while (it) |node| : (it = node.next) {
|
||||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||||
observer.notifyAttributeChange(element, name, old_value, self) catch |err| {
|
observer.notifyAttributeChange(element, name, old_value, self) catch |err| {
|
||||||
log.err(.page, "attributeRemove.notifyObserver", .{ .err = err, .type = self._type });
|
log.err(.page, "attributeRemove.notifyObserver", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2903,11 +2652,11 @@ pub fn attributeRemove(self: *Page, element: *Element, name: String, old_value:
|
|||||||
|
|
||||||
fn signalSlotChange(self: *Page, slot: *Element.Html.Slot) void {
|
fn signalSlotChange(self: *Page, slot: *Element.Html.Slot) void {
|
||||||
self._slots_pending_slotchange.put(self.arena, slot, {}) catch |err| {
|
self._slots_pending_slotchange.put(self.arena, slot, {}) catch |err| {
|
||||||
log.err(.page, "signalSlotChange.put", .{ .err = err, .type = self._type });
|
log.err(.page, "signalSlotChange.put", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.scheduleSlotchangeDelivery() catch |err| {
|
self.scheduleSlotchangeDelivery() catch |err| {
|
||||||
log.err(.page, "signalSlotChange.schedule", .{ .err = err, .type = self._type });
|
log.err(.page, "signalSlotChange.schedule", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2947,7 +2696,7 @@ fn updateElementAssignedSlot(self: *Page, element: *Element) void {
|
|||||||
// Recursively search through the shadow root for a matching slot
|
// Recursively search through the shadow root for a matching slot
|
||||||
if (findMatchingSlot(shadow_root.asNode(), slot_name)) |slot| {
|
if (findMatchingSlot(shadow_root.asNode(), slot_name)) |slot| {
|
||||||
self._element_assigned_slots.put(self.arena, element, slot) catch |err| {
|
self._element_assigned_slots.put(self.arena, element, slot) catch |err| {
|
||||||
log.err(.page, "updateElementAssignedSlot.put", .{ .err = err, .type = self._type });
|
log.err(.page, "updateElementAssignedSlot.put", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2988,13 +2737,13 @@ pub fn setCustomizedBuiltInDefinition(self: *Page, element: *Element, definition
|
|||||||
pub fn characterDataChange(
|
pub fn characterDataChange(
|
||||||
self: *Page,
|
self: *Page,
|
||||||
target: *Node,
|
target: *Node,
|
||||||
old_value: []const u8,
|
old_value: String,
|
||||||
) void {
|
) void {
|
||||||
var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;
|
var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;
|
||||||
while (it) |node| : (it = node.next) {
|
while (it) |node| : (it = node.next) {
|
||||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||||
observer.notifyCharacterDataChange(target, old_value, self) catch |err| {
|
observer.notifyCharacterDataChange(target, old_value, self) catch |err| {
|
||||||
log.err(.page, "cdataChange.notifyObserver", .{ .err = err, .type = self._type });
|
log.err(.page, "cdataChange.notifyObserver", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3021,7 +2770,7 @@ pub fn childListChange(
|
|||||||
while (it) |node| : (it = node.next) {
|
while (it) |node| : (it = node.next) {
|
||||||
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
const observer: *MutationObserver = @fieldParentPtr("node", node);
|
||||||
observer.notifyChildListChange(target, added_nodes, removed_nodes, previous_sibling, next_sibling, self) catch |err| {
|
observer.notifyChildListChange(target, added_nodes, removed_nodes, previous_sibling, next_sibling, self) catch |err| {
|
||||||
log.err(.page, "childListChange.notifyObserver", .{ .err = err, .type = self._type });
|
log.err(.page, "childListChange.notifyObserver", .{ .err = err, .type = self._type, .url = self.url });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3072,7 +2821,7 @@ fn nodeIsReady(self: *Page, comptime from_parser: bool, node: *Node) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.scriptAddedCallback(from_parser, script) catch |err| {
|
self.scriptAddedCallback(from_parser, script) catch |err| {
|
||||||
log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "script", .type = self._type });
|
log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "script", .type = self._type, .url = self.url });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
} else if (node.is(Element.Html.IFrame)) |iframe| {
|
} else if (node.is(Element.Html.IFrame)) |iframe| {
|
||||||
@@ -3082,9 +2831,19 @@ fn nodeIsReady(self: *Page, comptime from_parser: bool, node: *Node) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.iframeAddedCallback(iframe) catch |err| {
|
self.iframeAddedCallback(iframe) catch |err| {
|
||||||
log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "iframe", .type = self._type });
|
log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "iframe", .type = self._type, .url = self.url });
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
} else if (node.is(Element.Html.Link)) |link| {
|
||||||
|
link.linkAddedCallback(self) catch |err| {
|
||||||
|
log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "link", .type = self._type });
|
||||||
|
return error.LinkLoadError;
|
||||||
|
};
|
||||||
|
} else if (node.is(Element.Html.Style)) |style| {
|
||||||
|
style.styleAddedCallback(self) catch |err| {
|
||||||
|
log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "style", .type = self._type });
|
||||||
|
return error.StyleLoadError;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3242,8 +3001,6 @@ pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
|
|||||||
.clientX = x,
|
.clientX = x,
|
||||||
.clientY = y,
|
.clientY = y,
|
||||||
}, self)).asEvent();
|
}, self)).asEvent();
|
||||||
|
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
try self._event_manager.dispatch(target.asEventTarget(), event);
|
try self._event_manager.dispatch(target.asEventTarget(), event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3267,12 +3024,12 @@ pub fn handleClick(self: *Page, target: *Node) !void {
|
|||||||
// Check target attribute - don't navigate if opening in new window/tab
|
// Check target attribute - don't navigate if opening in new window/tab
|
||||||
const target_val = anchor.getTarget();
|
const target_val = anchor.getTarget();
|
||||||
if (target_val.len > 0 and !std.mem.eql(u8, target_val, "_self")) {
|
if (target_val.len > 0 and !std.mem.eql(u8, target_val, "_self")) {
|
||||||
log.warn(.not_implemented, "a.target", .{ .type = self._type });
|
log.warn(.not_implemented, "a.target", .{ .type = self._type, .url = self.url });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (try element.hasAttribute(comptime .wrap("download"), self)) {
|
if (try element.hasAttribute(comptime .wrap("download"), self)) {
|
||||||
log.warn(.browser, "a.download", .{ .type = self._type });
|
log.warn(.browser, "a.download", .{ .type = self._type, .url = self.url });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3301,8 +3058,6 @@ pub fn handleClick(self: *Page, target: *Node) !void {
|
|||||||
|
|
||||||
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
||||||
const event = keyboard_event.asEvent();
|
const event = keyboard_event.asEvent();
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
|
|
||||||
const element = self.window._document._active_element orelse return;
|
const element = self.window._document._active_element orelse return;
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.page, "page keydown", .{
|
log.debug(.page, "page keydown", .{
|
||||||
@@ -3316,7 +3071,7 @@ pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handleKeydown(self: *Page, target: *Node, event: *Event) !void {
|
pub fn handleKeydown(self: *Page, target: *Node, event: *Event) !void {
|
||||||
const keyboard_event = event.as(KeyboardEvent);
|
const keyboard_event = event.is(KeyboardEvent) orelse return;
|
||||||
const key = keyboard_event.getKey();
|
const key = keyboard_event.getKey();
|
||||||
|
|
||||||
if (key == .Dead) {
|
if (key == .Dead) {
|
||||||
@@ -3372,10 +3127,8 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
|
|||||||
const form_element = form.asElement();
|
const form_element = form.asElement();
|
||||||
|
|
||||||
if (submit_opts.fire_event) {
|
if (submit_opts.fire_event) {
|
||||||
const submit_event = try Event.initTrusted(comptime .wrap("submit"), .{ .bubbles = true, .cancelable = true }, self);
|
|
||||||
defer if (!submit_event._v8_handoff) submit_event.deinit(false);
|
|
||||||
|
|
||||||
const onsubmit_handler = try form.asHtmlElement().getOnSubmit(self);
|
const onsubmit_handler = try form.asHtmlElement().getOnSubmit(self);
|
||||||
|
const submit_event = try Event.initTrusted(comptime .wrap("submit"), .{ .bubbles = true, .cancelable = true }, self);
|
||||||
|
|
||||||
var ls: JS.Local.Scope = undefined;
|
var ls: JS.Local.Scope = undefined;
|
||||||
self.js.localScope(&ls);
|
self.js.localScope(&ls);
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ const std = @import("std");
|
|||||||
const lp = @import("lightpanda");
|
const lp = @import("lightpanda");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const js = @import("js/js.zig");
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
|
const Http = @import("../http/Http.zig");
|
||||||
|
const String = @import("../string.zig").String;
|
||||||
|
|
||||||
|
const js = @import("js/js.zig");
|
||||||
const URL = @import("URL.zig");
|
const URL = @import("URL.zig");
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const Browser = @import("Browser.zig");
|
const Browser = @import("Browser.zig");
|
||||||
const Http = @import("../http/Http.zig");
|
|
||||||
|
|
||||||
const Element = @import("webapi/Element.zig");
|
const Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
@@ -581,12 +582,6 @@ fn evaluate(self: *ScriptManager) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isDone(self: *const ScriptManager) bool {
|
|
||||||
return self.static_scripts_done and // page is done processing initial html
|
|
||||||
self.defer_scripts.first == null and // no deferred scripts
|
|
||||||
self.async_scripts.first == null; // no async scripts
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
||||||
const content = script.source.content();
|
const content = script.source.content();
|
||||||
|
|
||||||
@@ -627,8 +622,19 @@ pub const Script = struct {
|
|||||||
node: std.DoublyLinkedList.Node,
|
node: std.DoublyLinkedList.Node,
|
||||||
script_element: ?*Element.Html.Script,
|
script_element: ?*Element.Html.Script,
|
||||||
manager: *ScriptManager,
|
manager: *ScriptManager,
|
||||||
|
|
||||||
|
// for debugging a rare production issue
|
||||||
header_callback_called: bool = false,
|
header_callback_called: bool = false,
|
||||||
|
|
||||||
|
// for debugging a rare production issue
|
||||||
|
debug_transfer_id: u32 = 0,
|
||||||
|
debug_transfer_tries: u8 = 0,
|
||||||
|
debug_transfer_aborted: bool = false,
|
||||||
|
debug_transfer_bytes_received: usize = 0,
|
||||||
|
debug_transfer_notified_fail: bool = false,
|
||||||
|
debug_transfer_redirecting: bool = false,
|
||||||
|
debug_transfer_intercept_state: u8 = 0,
|
||||||
|
|
||||||
const Kind = enum {
|
const Kind = enum {
|
||||||
module,
|
module,
|
||||||
javascript,
|
javascript,
|
||||||
@@ -696,8 +702,31 @@ pub const Script = struct {
|
|||||||
// temp debug, trying to figure out why the next assert sometimes
|
// temp debug, trying to figure out why the next assert sometimes
|
||||||
// fails. Is the buffer just corrupt or is headerCallback really
|
// fails. Is the buffer just corrupt or is headerCallback really
|
||||||
// being called twice?
|
// being called twice?
|
||||||
lp.assert(self.header_callback_called == false, "ScriptManager.Header recall", .{});
|
lp.assert(self.header_callback_called == false, "ScriptManager.Header recall", .{
|
||||||
|
.m = @tagName(std.meta.activeTag(self.mode)),
|
||||||
|
.a1 = self.debug_transfer_id,
|
||||||
|
.a2 = self.debug_transfer_tries,
|
||||||
|
.a3 = self.debug_transfer_aborted,
|
||||||
|
.a4 = self.debug_transfer_bytes_received,
|
||||||
|
.a5 = self.debug_transfer_notified_fail,
|
||||||
|
.a6 = self.debug_transfer_redirecting,
|
||||||
|
.a7 = self.debug_transfer_intercept_state,
|
||||||
|
.b1 = transfer.id,
|
||||||
|
.b2 = transfer._tries,
|
||||||
|
.b3 = transfer.aborted,
|
||||||
|
.b4 = transfer.bytes_received,
|
||||||
|
.b5 = transfer._notified_fail,
|
||||||
|
.b6 = transfer._redirecting,
|
||||||
|
.b7 = @intFromEnum(transfer._intercept_state),
|
||||||
|
});
|
||||||
self.header_callback_called = true;
|
self.header_callback_called = true;
|
||||||
|
self.debug_transfer_id = transfer.id;
|
||||||
|
self.debug_transfer_tries = transfer._tries;
|
||||||
|
self.debug_transfer_aborted = transfer.aborted;
|
||||||
|
self.debug_transfer_bytes_received = transfer.bytes_received;
|
||||||
|
self.debug_transfer_notified_fail = transfer._notified_fail;
|
||||||
|
self.debug_transfer_redirecting = transfer._redirecting;
|
||||||
|
self.debug_transfer_intercept_state = @intFromEnum(transfer._intercept_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
||||||
@@ -830,13 +859,15 @@ pub const Script = struct {
|
|||||||
.kind = self.kind,
|
.kind = self.kind,
|
||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
self.executeCallback("error", local.toLocal(script_element._on_error), page);
|
self.executeCallback(comptime .wrap("error"), page);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.executeCallback("load", local.toLocal(script_element._on_load), page);
|
self.executeCallback(comptime .wrap("load"), page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer page._event_manager.clearIgnoreList();
|
||||||
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(local);
|
try_catch.init(local);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
@@ -855,19 +886,18 @@ pub const Script = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.browser, "executed script", .{ .src = url, .success = success, .on_load = script_element._on_load != null });
|
log.debug(.browser, "executed script", .{ .src = url, .success = success });
|
||||||
}
|
}
|
||||||
|
|
||||||
defer {
|
defer {
|
||||||
// We should run microtasks even if script execution fails.
|
local.runMacrotasks(); // also runs microtasks
|
||||||
local.runMicrotasks();
|
|
||||||
_ = page.js.scheduler.run() catch |err| {
|
_ = page.js.scheduler.run() catch |err| {
|
||||||
log.err(.page, "scheduler", .{ .err = err });
|
log.err(.page, "scheduler", .{ .err = err });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
self.executeCallback("load", local.toLocal(script_element._on_load), page);
|
self.executeCallback(comptime .wrap("load"), page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,14 +908,12 @@ pub const Script = struct {
|
|||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
|
|
||||||
self.executeCallback("error", local.toLocal(script_element._on_error), page);
|
self.executeCallback(comptime .wrap("error"), page);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void {
|
fn executeCallback(self: *const Script, typ: String, page: *Page) void {
|
||||||
const cb = cb_ orelse return;
|
|
||||||
|
|
||||||
const Event = @import("webapi/Event.zig");
|
const Event = @import("webapi/Event.zig");
|
||||||
const event = Event.initTrusted(comptime .wrap(typ), .{}, page) catch |err| {
|
const event = Event.initTrusted(typ, .{}, page) catch |err| {
|
||||||
log.warn(.js, "script internal callback", .{
|
log.warn(.js, "script internal callback", .{
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
.type = typ,
|
.type = typ,
|
||||||
@@ -893,14 +921,11 @@ pub const Script = struct {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
page._event_manager.dispatchOpts(self.script_element.?.asNode().asEventTarget(), event, .{ .apply_ignore = true }) catch |err| {
|
||||||
|
|
||||||
var caught: js.TryCatch.Caught = undefined;
|
|
||||||
cb.tryCall(void, .{event}, &caught) catch {
|
|
||||||
log.warn(.js, "script callback", .{
|
log.warn(.js, "script callback", .{
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
.type = typ,
|
.type = typ,
|
||||||
.caught = caught,
|
.err = err,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1020,23 +1045,35 @@ fn parseDataURI(allocator: Allocator, src: []const u8) !?[]const u8 {
|
|||||||
|
|
||||||
const uri = src[5..];
|
const uri = src[5..];
|
||||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
||||||
|
const data = uri[data_starts + 1 ..];
|
||||||
|
|
||||||
var data = uri[data_starts + 1 ..];
|
const unescaped = try URL.unescape(allocator, data);
|
||||||
|
|
||||||
// Extract the encoding.
|
|
||||||
const metadata = uri[0..data_starts];
|
const metadata = uri[0..data_starts];
|
||||||
if (std.mem.endsWith(u8, metadata, ";base64")) {
|
if (std.mem.endsWith(u8, metadata, ";base64") == false) {
|
||||||
const decoder = std.base64.standard.Decoder;
|
return unescaped;
|
||||||
const decoded_size = try decoder.calcSizeForSlice(data);
|
|
||||||
|
|
||||||
const buffer = try allocator.alloc(u8, decoded_size);
|
|
||||||
errdefer allocator.free(buffer);
|
|
||||||
|
|
||||||
try decoder.decode(buffer, data);
|
|
||||||
data = buffer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
// Forgiving base64 decode per WHATWG spec:
|
||||||
|
// https://infra.spec.whatwg.org/#forgiving-base64-decode
|
||||||
|
// Step 1: Remove all ASCII whitespace
|
||||||
|
var stripped = try std.ArrayList(u8).initCapacity(allocator, unescaped.len);
|
||||||
|
for (unescaped) |c| {
|
||||||
|
if (!std.ascii.isWhitespace(c)) {
|
||||||
|
stripped.appendAssumeCapacity(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const trimmed = std.mem.trimRight(u8, stripped.items, "=");
|
||||||
|
|
||||||
|
// Length % 4 == 1 is invalid
|
||||||
|
if (trimmed.len % 4 == 1) {
|
||||||
|
return error.InvalidCharacterError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded_size = std.base64.standard_no_pad.Decoder.calcSizeForSlice(trimmed) catch return error.InvalidCharacterError;
|
||||||
|
const buffer = try allocator.alloc(u8, decoded_size);
|
||||||
|
std.base64.standard_no_pad.Decoder.decode(buffer, trimmed) catch return error.InvalidCharacterError;
|
||||||
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
|||||||
error.JsError => {}, // already logged (with hopefully more context)
|
error.JsError => {}, // already logged (with hopefully more context)
|
||||||
else => log.err(.browser, "session wait", .{
|
else => log.err(.browser, "session wait", .{
|
||||||
.err = err,
|
.err = err,
|
||||||
|
.url = page.url,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
return .done;
|
return .done;
|
||||||
@@ -240,6 +241,9 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
// it AFTER.
|
// it AFTER.
|
||||||
const ms_to_next_task = try browser.runMacrotasks();
|
const ms_to_next_task = try browser.runMacrotasks();
|
||||||
|
|
||||||
|
// Each call to this runs scheduled load events.
|
||||||
|
try page.dispatchLoad();
|
||||||
|
|
||||||
const http_active = http_client.active;
|
const http_active = http_client.active;
|
||||||
const total_network_activity = http_active + http_client.intercepted;
|
const total_network_activity = http_active + http_client.intercepted;
|
||||||
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
||||||
@@ -257,7 +261,7 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
std.debug.assert(http_client.intercepted == 0);
|
std.debug.assert(http_client.intercepted == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ms = ms_to_next_task orelse blk: {
|
const ms: u64 = ms_to_next_task orelse blk: {
|
||||||
if (wait_ms - ms_remaining < 100) {
|
if (wait_ms - ms_remaining < 100) {
|
||||||
if (comptime builtin.is_test) {
|
if (comptime builtin.is_test) {
|
||||||
return .done;
|
return .done;
|
||||||
@@ -267,6 +271,14 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
// background jobs.
|
// background jobs.
|
||||||
break :blk 50;
|
break :blk 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (browser.hasBackgroundTasks()) {
|
||||||
|
// _we_ have nothing to run, but v8 is working on
|
||||||
|
// background tasks. We'll wait for them.
|
||||||
|
browser.waitForBackgroundTasks();
|
||||||
|
break :blk 20;
|
||||||
|
}
|
||||||
|
|
||||||
// No http transfers, no cdp extra socket, no
|
// No http transfers, no cdp extra socket, no
|
||||||
// scheduled tasks, we're done.
|
// scheduled tasks, we're done.
|
||||||
return .done;
|
return .done;
|
||||||
@@ -292,8 +304,14 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
// an cdp_socket registered with the http client).
|
// an cdp_socket registered with the http client).
|
||||||
// We should continue to run lowPriority tasks, so we
|
// We should continue to run lowPriority tasks, so we
|
||||||
// minimize how long we'll poll for network I/O.
|
// minimize how long we'll poll for network I/O.
|
||||||
const ms_to_wait = @min(200, @min(ms_remaining, ms_to_next_task orelse 200));
|
var ms_to_wait = @min(200, ms_to_next_task orelse 200);
|
||||||
if (try http_client.tick(ms_to_wait) == .cdp_socket) {
|
if (ms_to_wait > 10 and browser.hasBackgroundTasks()) {
|
||||||
|
// if we have background tasks, we don't want to wait too
|
||||||
|
// long for a message from the client. We want to go back
|
||||||
|
// to the top of the loop and run macrotasks.
|
||||||
|
ms_to_wait = 10;
|
||||||
|
}
|
||||||
|
if (try http_client.tick(@min(ms_remaining, ms_to_wait)) == .cdp_socket) {
|
||||||
// data on a socket we aren't handling, return to caller
|
// data on a socket we aren't handling, return to caller
|
||||||
return .cdp_socket;
|
return .cdp_socket;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,44 +20,61 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const ResolveOpts = struct {
|
const ResolveOpts = struct {
|
||||||
|
encode: bool = false,
|
||||||
always_dupe: bool = false,
|
always_dupe: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// path is anytype, so that it can be used with both []const u8 and [:0]const u8
|
// path is anytype, so that it can be used with both []const u8 and [:0]const u8
|
||||||
pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime opts: ResolveOpts) ![:0]const u8 {
|
pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime opts: ResolveOpts) ![:0]const u8 {
|
||||||
const PT = @TypeOf(path);
|
const PT = @TypeOf(path);
|
||||||
if (base.len == 0 or isCompleteHTTPUrl(path)) {
|
if (base.len == 0 or isCompleteHTTPUrl(path)) {
|
||||||
if (comptime opts.always_dupe or !isNullTerminated(PT)) {
|
if (comptime opts.always_dupe or !isNullTerminated(PT)) {
|
||||||
return allocator.dupeZ(u8, path);
|
const duped = try allocator.dupeZ(u8, path);
|
||||||
|
return processResolved(allocator, duped, opts);
|
||||||
|
}
|
||||||
|
if (comptime opts.encode) {
|
||||||
|
return processResolved(allocator, path, opts);
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.len == 0) {
|
if (path.len == 0) {
|
||||||
if (comptime opts.always_dupe) {
|
if (comptime opts.always_dupe) {
|
||||||
return allocator.dupeZ(u8, base);
|
const duped = try allocator.dupeZ(u8, base);
|
||||||
|
return processResolved(allocator, duped, opts);
|
||||||
|
}
|
||||||
|
if (comptime opts.encode) {
|
||||||
|
return processResolved(allocator, base, opts);
|
||||||
}
|
}
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path[0] == '?') {
|
if (path[0] == '?') {
|
||||||
const base_path_end = std.mem.indexOfAny(u8, base, "?#") orelse base.len;
|
const base_path_end = std.mem.indexOfAny(u8, base, "?#") orelse base.len;
|
||||||
return std.mem.joinZ(allocator, "", &.{ base[0..base_path_end], path });
|
const result = try std.mem.joinZ(allocator, "", &.{ base[0..base_path_end], path });
|
||||||
|
return processResolved(allocator, result, opts);
|
||||||
}
|
}
|
||||||
if (path[0] == '#') {
|
if (path[0] == '#') {
|
||||||
const base_fragment_start = std.mem.indexOfScalar(u8, base, '#') orelse base.len;
|
const base_fragment_start = std.mem.indexOfScalar(u8, base, '#') orelse base.len;
|
||||||
return std.mem.joinZ(allocator, "", &.{ base[0..base_fragment_start], path });
|
const result = try std.mem.joinZ(allocator, "", &.{ base[0..base_fragment_start], path });
|
||||||
|
return processResolved(allocator, result, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.startsWith(u8, path, "//")) {
|
if (std.mem.startsWith(u8, path, "//")) {
|
||||||
// network-path reference
|
// network-path reference
|
||||||
const index = std.mem.indexOfScalar(u8, base, ':') orelse {
|
const index = std.mem.indexOfScalar(u8, base, ':') orelse {
|
||||||
if (comptime isNullTerminated(PT)) {
|
if (comptime isNullTerminated(PT)) {
|
||||||
|
if (comptime opts.encode) {
|
||||||
|
return processResolved(allocator, path, opts);
|
||||||
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
return allocator.dupeZ(u8, path);
|
const duped = try allocator.dupeZ(u8, path);
|
||||||
|
return processResolved(allocator, duped, opts);
|
||||||
};
|
};
|
||||||
const protocol = base[0 .. index + 1];
|
const protocol = base[0 .. index + 1];
|
||||||
return std.mem.joinZ(allocator, "", &.{ protocol, path });
|
const result = try std.mem.joinZ(allocator, "", &.{ protocol, path });
|
||||||
|
return processResolved(allocator, result, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheme_end = std.mem.indexOf(u8, base, "://");
|
const scheme_end = std.mem.indexOf(u8, base, "://");
|
||||||
@@ -65,7 +82,8 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
|||||||
const path_start = std.mem.indexOfScalarPos(u8, base, authority_start, '/') orelse base.len;
|
const path_start = std.mem.indexOfScalarPos(u8, base, authority_start, '/') orelse base.len;
|
||||||
|
|
||||||
if (path[0] == '/') {
|
if (path[0] == '/') {
|
||||||
return std.mem.joinZ(allocator, "", &.{ base[0..path_start], path });
|
const result = try std.mem.joinZ(allocator, "", &.{ base[0..path_start], path });
|
||||||
|
return processResolved(allocator, result, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalized_base: []const u8 = base[0..path_start];
|
var normalized_base: []const u8 = base[0..path_start];
|
||||||
@@ -127,7 +145,119 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
|||||||
|
|
||||||
// we always have an extra space
|
// we always have an extra space
|
||||||
out[out_i] = 0;
|
out[out_i] = 0;
|
||||||
return out[0..out_i :0];
|
return processResolved(allocator, out[0..out_i :0], opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processResolved(allocator: Allocator, url: [:0]const u8, comptime opts: ResolveOpts) ![:0]const u8 {
|
||||||
|
if (!comptime opts.encode) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return ensureEncoded(allocator, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensureEncoded(allocator: Allocator, url: [:0]const u8) ![:0]const u8 {
|
||||||
|
const scheme_end = std.mem.indexOf(u8, url, "://");
|
||||||
|
const authority_start = if (scheme_end) |end| end + 3 else 0;
|
||||||
|
const path_start = std.mem.indexOfScalarPos(u8, url, authority_start, '/') orelse return url;
|
||||||
|
|
||||||
|
const query_start = std.mem.indexOfScalarPos(u8, url, path_start, '?');
|
||||||
|
const fragment_start = std.mem.indexOfScalarPos(u8, url, query_start orelse path_start, '#');
|
||||||
|
|
||||||
|
const path_end = query_start orelse fragment_start orelse url.len;
|
||||||
|
const query_end = if (query_start) |_| (fragment_start orelse url.len) else path_end;
|
||||||
|
|
||||||
|
const path_to_encode = url[path_start..path_end];
|
||||||
|
const encoded_path = try percentEncodeSegment(allocator, path_to_encode, true);
|
||||||
|
|
||||||
|
const encoded_query = if (query_start) |qs| blk: {
|
||||||
|
const query_to_encode = url[qs + 1 .. query_end];
|
||||||
|
const encoded = try percentEncodeSegment(allocator, query_to_encode, false);
|
||||||
|
break :blk encoded;
|
||||||
|
} else null;
|
||||||
|
|
||||||
|
const encoded_fragment = if (fragment_start) |fs| blk: {
|
||||||
|
const fragment_to_encode = url[fs + 1 ..];
|
||||||
|
const encoded = try percentEncodeSegment(allocator, fragment_to_encode, false);
|
||||||
|
break :blk encoded;
|
||||||
|
} else null;
|
||||||
|
|
||||||
|
if (encoded_path.ptr == path_to_encode.ptr and
|
||||||
|
(encoded_query == null or encoded_query.?.ptr == url[query_start.? + 1 .. query_end].ptr) and
|
||||||
|
(encoded_fragment == null or encoded_fragment.?.ptr == url[fragment_start.? + 1 ..].ptr))
|
||||||
|
{
|
||||||
|
// nothing has changed
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf = try std.ArrayList(u8).initCapacity(allocator, url.len + 20);
|
||||||
|
try buf.appendSlice(allocator, url[0..path_start]);
|
||||||
|
try buf.appendSlice(allocator, encoded_path);
|
||||||
|
if (encoded_query) |eq| {
|
||||||
|
try buf.append(allocator, '?');
|
||||||
|
try buf.appendSlice(allocator, eq);
|
||||||
|
}
|
||||||
|
if (encoded_fragment) |ef| {
|
||||||
|
try buf.append(allocator, '#');
|
||||||
|
try buf.appendSlice(allocator, ef);
|
||||||
|
}
|
||||||
|
try buf.append(allocator, 0);
|
||||||
|
return buf.items[0 .. buf.items.len - 1 :0];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime is_path: bool) ![]const u8 {
|
||||||
|
// Check if encoding is needed
|
||||||
|
var needs_encoding = false;
|
||||||
|
for (segment) |c| {
|
||||||
|
if (shouldPercentEncode(c, is_path)) {
|
||||||
|
needs_encoding = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!needs_encoding) {
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf = try std.ArrayList(u8).initCapacity(allocator, segment.len + 10);
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < segment.len) : (i += 1) {
|
||||||
|
const c = segment[i];
|
||||||
|
|
||||||
|
// Check if this is an already-encoded sequence (%XX)
|
||||||
|
if (c == '%' and i + 2 < segment.len) {
|
||||||
|
const end = i + 2;
|
||||||
|
const h1 = segment[i + 1];
|
||||||
|
const h2 = segment[end];
|
||||||
|
if (std.ascii.isHex(h1) and std.ascii.isHex(h2)) {
|
||||||
|
try buf.appendSlice(allocator, segment[i .. end + 1]);
|
||||||
|
i = end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldPercentEncode(c, is_path)) {
|
||||||
|
try buf.writer(allocator).print("%{X:0>2}", .{c});
|
||||||
|
} else {
|
||||||
|
try buf.append(allocator, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shouldPercentEncode(c: u8, comptime is_path: bool) bool {
|
||||||
|
return switch (c) {
|
||||||
|
// Unreserved characters (RFC 3986)
|
||||||
|
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => false,
|
||||||
|
// sub-delims allowed in both path and query
|
||||||
|
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => false,
|
||||||
|
// Separators allowed in both path and query
|
||||||
|
'/', ':', '@' => false,
|
||||||
|
// Query-specific: '?' is allowed in queries but not in paths
|
||||||
|
'?' => comptime is_path,
|
||||||
|
// Everything else needs encoding (including space)
|
||||||
|
else => true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isNullTerminated(comptime value: type) bool {
|
fn isNullTerminated(comptime value: type) bool {
|
||||||
@@ -512,6 +642,33 @@ pub fn getRobotsUrl(arena: Allocator, url: [:0]const u8) ![:0]const u8 {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn unescape(arena: Allocator, input: []const u8) ![]const u8 {
|
||||||
|
if (std.mem.indexOfScalar(u8, input, '%') == null) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = try std.ArrayList(u8).initCapacity(arena, input.len);
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < input.len) {
|
||||||
|
if (input[i] == '%' and i + 2 < input.len) {
|
||||||
|
const hex = input[i + 1 .. i + 3];
|
||||||
|
const byte = std.fmt.parseInt(u8, hex, 16) catch {
|
||||||
|
result.appendAssumeCapacity(input[i]);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
result.appendAssumeCapacity(byte);
|
||||||
|
i += 3;
|
||||||
|
} else {
|
||||||
|
result.appendAssumeCapacity(input[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.items;
|
||||||
|
}
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
test "URL: isCompleteHTTPUrl" {
|
test "URL: isCompleteHTTPUrl" {
|
||||||
try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about"));
|
try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about"));
|
||||||
@@ -691,6 +848,293 @@ test "URL: resolve" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "URL: ensureEncoded" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const Case = struct {
|
||||||
|
url: [:0]const u8,
|
||||||
|
expected: [:0]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cases = [_]Case{
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/over 9000!",
|
||||||
|
.expected = "https://example.com/over%209000!",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "http://example.com/hello world.html",
|
||||||
|
.expected = "http://example.com/hello%20world.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/file[1].html",
|
||||||
|
.expected = "https://example.com/file%5B1%5D.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/file{name}.html",
|
||||||
|
.expected = "https://example.com/file%7Bname%7D.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page?query=hello world",
|
||||||
|
.expected = "https://example.com/page?query=hello%20world",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page?a=1&b=value with spaces",
|
||||||
|
.expected = "https://example.com/page?a=1&b=value%20with%20spaces",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page#section one",
|
||||||
|
.expected = "https://example.com/page#section%20one",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/my path?query=my value#my anchor",
|
||||||
|
.expected = "https://example.com/my%20path?query=my%20value#my%20anchor",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/already%20encoded",
|
||||||
|
.expected = "https://example.com/already%20encoded",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/file%5B1%5D.html",
|
||||||
|
.expected = "https://example.com/file%5B1%5D.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/caf%C3%A9",
|
||||||
|
.expected = "https://example.com/caf%C3%A9",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page?query=already%20encoded",
|
||||||
|
.expected = "https://example.com/page?query=already%20encoded",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page?a=1&b=value%20here",
|
||||||
|
.expected = "https://example.com/page?a=1&b=value%20here",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page#section%20one",
|
||||||
|
.expected = "https://example.com/page#section%20one",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/part%20encoded and not",
|
||||||
|
.expected = "https://example.com/part%20encoded%20and%20not",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/page?a=encoded%20value&b=not encoded",
|
||||||
|
.expected = "https://example.com/page?a=encoded%20value&b=not%20encoded",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/my%20path?query=not encoded#encoded%20anchor",
|
||||||
|
.expected = "https://example.com/my%20path?query=not%20encoded#encoded%20anchor",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/fully%20encoded?query=also%20encoded#and%20this",
|
||||||
|
.expected = "https://example.com/fully%20encoded?query=also%20encoded#and%20this",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/path-with_under~tilde",
|
||||||
|
.expected = "https://example.com/path-with_under~tilde",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/sub-delims!$&'()*+,;=",
|
||||||
|
.expected = "https://example.com/sub-delims!$&'()*+,;=",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com",
|
||||||
|
.expected = "https://example.com",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com?query=value",
|
||||||
|
.expected = "https://example.com?query=value",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/clean/path",
|
||||||
|
.expected = "https://example.com/clean/path",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/path?clean=query#clean-fragment",
|
||||||
|
.expected = "https://example.com/path?clean=query#clean-fragment",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/100% complete",
|
||||||
|
.expected = "https://example.com/100%25%20complete",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.url = "https://example.com/path?value=100% done",
|
||||||
|
.expected = "https://example.com/path?value=100%25%20done",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (cases) |case| {
|
||||||
|
const result = try ensureEncoded(testing.arena_allocator, case.url);
|
||||||
|
try testing.expectString(case.expected, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "URL: resolve with encoding" {
|
||||||
|
defer testing.reset();
|
||||||
|
|
||||||
|
const Case = struct {
|
||||||
|
base: [:0]const u8,
|
||||||
|
path: [:0]const u8,
|
||||||
|
expected: [:0]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cases = [_]Case{
|
||||||
|
// Spaces should be encoded as %20, but ! is allowed
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/dir/",
|
||||||
|
.path = "over 9000!",
|
||||||
|
.expected = "https://example.com/dir/over%209000!",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "hello world.html",
|
||||||
|
.expected = "https://example.com/hello%20world.html",
|
||||||
|
},
|
||||||
|
// Multiple spaces
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "path with multiple spaces",
|
||||||
|
.expected = "https://example.com/path%20with%20%20multiple%20%20%20spaces",
|
||||||
|
},
|
||||||
|
// Special characters that need encoding
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file[1].html",
|
||||||
|
.expected = "https://example.com/file%5B1%5D.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file{name}.html",
|
||||||
|
.expected = "https://example.com/file%7Bname%7D.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file<test>.html",
|
||||||
|
.expected = "https://example.com/file%3Ctest%3E.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file\"quote\".html",
|
||||||
|
.expected = "https://example.com/file%22quote%22.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file|pipe.html",
|
||||||
|
.expected = "https://example.com/file%7Cpipe.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file\\backslash.html",
|
||||||
|
.expected = "https://example.com/file%5Cbackslash.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file^caret.html",
|
||||||
|
.expected = "https://example.com/file%5Ecaret.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file`backtick`.html",
|
||||||
|
.expected = "https://example.com/file%60backtick%60.html",
|
||||||
|
},
|
||||||
|
// Characters that should NOT be encoded
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "path-with_under~tilde.html",
|
||||||
|
.expected = "https://example.com/path-with_under~tilde.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "path/with/slashes",
|
||||||
|
.expected = "https://example.com/path/with/slashes",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "sub-delims!$&'()*+,;=.html",
|
||||||
|
.expected = "https://example.com/sub-delims!$&'()*+,;=.html",
|
||||||
|
},
|
||||||
|
// Already encoded characters should not be double-encoded
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "already%20encoded",
|
||||||
|
.expected = "https://example.com/already%20encoded",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file%5B1%5D.html",
|
||||||
|
.expected = "https://example.com/file%5B1%5D.html",
|
||||||
|
},
|
||||||
|
// Mix of encoded and unencoded
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "part%20encoded and not",
|
||||||
|
.expected = "https://example.com/part%20encoded%20and%20not",
|
||||||
|
},
|
||||||
|
// Query strings and fragments ARE encoded
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file name.html?query=value with spaces",
|
||||||
|
.expected = "https://example.com/file%20name.html?query=value%20with%20spaces",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file name.html#anchor with spaces",
|
||||||
|
.expected = "https://example.com/file%20name.html#anchor%20with%20spaces",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file.html?hello=world !",
|
||||||
|
.expected = "https://example.com/file.html?hello=world%20!",
|
||||||
|
},
|
||||||
|
// Query structural characters should NOT be encoded
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "file.html?a=1&b=2",
|
||||||
|
.expected = "https://example.com/file.html?a=1&b=2",
|
||||||
|
},
|
||||||
|
// Relative paths with encoding
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/dir/page.html",
|
||||||
|
.path = "../other dir/file.html",
|
||||||
|
.expected = "https://example.com/other%20dir/file.html",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/dir/",
|
||||||
|
.path = "./sub dir/file.html",
|
||||||
|
.expected = "https://example.com/dir/sub%20dir/file.html",
|
||||||
|
},
|
||||||
|
// Absolute paths with encoding
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/some/path",
|
||||||
|
.path = "/absolute path/file.html",
|
||||||
|
.expected = "https://example.com/absolute%20path/file.html",
|
||||||
|
},
|
||||||
|
// Unicode/high bytes (though ideally these should be UTF-8 encoded first)
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "café",
|
||||||
|
.expected = "https://example.com/caf%C3%A9",
|
||||||
|
},
|
||||||
|
// Empty path
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "",
|
||||||
|
.expected = "https://example.com/",
|
||||||
|
},
|
||||||
|
// Complete URL as path (should not be encoded)
|
||||||
|
.{
|
||||||
|
.base = "https://example.com/",
|
||||||
|
.path = "https://other.com/path with spaces",
|
||||||
|
.expected = "https://other.com/path%20with%20spaces",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (cases) |case| {
|
||||||
|
const result = try resolve(testing.arena_allocator, case.base, case.path, .{ .encode = true });
|
||||||
|
try testing.expectString(case.expected, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "URL: eqlDocument" {
|
test "URL: eqlDocument" {
|
||||||
defer testing.reset();
|
defer testing.reset();
|
||||||
{
|
{
|
||||||
@@ -816,3 +1260,68 @@ test "URL: getRobotsUrl" {
|
|||||||
try testing.expectString("https://example.com/robots.txt", url);
|
try testing.expectString("https://example.com/robots.txt", url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "URL: unescape" {
|
||||||
|
defer testing.reset();
|
||||||
|
const arena = testing.arena_allocator;
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "hello world");
|
||||||
|
try testing.expectEqual("hello world", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "hello%20world");
|
||||||
|
try testing.expectEqual("hello world", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "%48%65%6c%6c%6f");
|
||||||
|
try testing.expectEqual("Hello", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "%48%65%6C%6C%6F");
|
||||||
|
try testing.expectEqual("Hello", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "a%3Db");
|
||||||
|
try testing.expectEqual("a=b", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "a%3DB");
|
||||||
|
try testing.expectEqual("a=B", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "ZDIgPSAndHdvJzs%3D");
|
||||||
|
try testing.expectEqual("ZDIgPSAndHdvJzs=", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "%5a%44%4d%67%50%53%41%6e%64%47%68%79%5a%57%55%6e%4f%77%3D%3D");
|
||||||
|
try testing.expectEqual("ZDMgPSAndGhyZWUnOw==", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "hello%2world");
|
||||||
|
try testing.expectEqual("hello%2world", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "hello%ZZworld");
|
||||||
|
try testing.expectEqual("hello%ZZworld", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "hello%");
|
||||||
|
try testing.expectEqual("hello%", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = try unescape(arena, "hello%2");
|
||||||
|
try testing.expectEqual("hello%2", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ fn consumeNumeric(self: *Tokenizer) Token {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.advance(2);
|
self.advance(2);
|
||||||
} else if (self.hasAtLeast(1) and std.ascii.isDigit(self.byteAt(2))) {
|
} else if (self.hasAtLeast(2) and std.ascii.isDigit(self.byteAt(2))) {
|
||||||
self.advance(1);
|
self.advance(1);
|
||||||
} else {
|
} else {
|
||||||
break :blk;
|
break :blk;
|
||||||
|
|||||||
@@ -20,16 +20,15 @@ const std = @import("std");
|
|||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const Node = @import("webapi/Node.zig");
|
const Node = @import("webapi/Node.zig");
|
||||||
const Slot = @import("webapi/element/html/Slot.zig");
|
const Slot = @import("webapi/element/html/Slot.zig");
|
||||||
|
const IFrame = @import("webapi/element/html/IFrame.zig");
|
||||||
|
|
||||||
pub const RootOpts = struct {
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
with_base: bool = false,
|
|
||||||
strip: Opts.Strip = .{},
|
|
||||||
shadow: Opts.Shadow = .rendered,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Opts = struct {
|
pub const Opts = struct {
|
||||||
strip: Strip = .{},
|
with_base: bool = false,
|
||||||
shadow: Shadow = .rendered,
|
with_frames: bool = false,
|
||||||
|
strip: Opts.Strip = .{},
|
||||||
|
shadow: Opts.Shadow = .rendered,
|
||||||
|
|
||||||
pub const Strip = struct {
|
pub const Strip = struct {
|
||||||
js: bool = false,
|
js: bool = false,
|
||||||
@@ -49,7 +48,7 @@ pub const Opts = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn root(doc: *Node.Document, opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void {
|
pub fn root(doc: *Node.Document, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
|
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
|
||||||
blk: {
|
blk: {
|
||||||
// Ideally we just render the doctype which is part of the document
|
// Ideally we just render the doctype which is part of the document
|
||||||
@@ -71,7 +70,7 @@ pub fn root(doc: *Node.Document, opts: RootOpts, writer: *std.Io.Writer, page: *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return deep(doc.asNode(), .{ .strip = opts.strip, .shadow = opts.shadow }, writer, page);
|
return deep(doc.asNode(), opts, writer, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
||||||
@@ -83,19 +82,19 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
|||||||
.cdata => |cd| {
|
.cdata => |cd| {
|
||||||
if (node.is(Node.CData.Comment)) |_| {
|
if (node.is(Node.CData.Comment)) |_| {
|
||||||
try writer.writeAll("<!--");
|
try writer.writeAll("<!--");
|
||||||
try writer.writeAll(cd.getData());
|
try writer.writeAll(cd.getData().str());
|
||||||
try writer.writeAll("-->");
|
try writer.writeAll("-->");
|
||||||
} else if (node.is(Node.CData.ProcessingInstruction)) |pi| {
|
} else if (node.is(Node.CData.ProcessingInstruction)) |pi| {
|
||||||
try writer.writeAll("<?");
|
try writer.writeAll("<?");
|
||||||
try writer.writeAll(pi._target);
|
try writer.writeAll(pi._target);
|
||||||
try writer.writeAll(" ");
|
try writer.writeAll(" ");
|
||||||
try writer.writeAll(cd.getData());
|
try writer.writeAll(cd.getData().str());
|
||||||
try writer.writeAll("?>");
|
try writer.writeAll("?>");
|
||||||
} else {
|
} else {
|
||||||
if (shouldEscapeText(node._parent)) {
|
if (shouldEscapeText(node._parent)) {
|
||||||
try writeEscapedText(cd.getData(), writer);
|
try writeEscapedText(cd.getData().str(), writer);
|
||||||
} else {
|
} else {
|
||||||
try writer.writeAll(cd.getData());
|
try writer.writeAll(cd.getData().str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -140,7 +139,24 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try children(node, opts, writer, page);
|
if (opts.with_frames and el.is(IFrame) != null) {
|
||||||
|
const frame = el.as(IFrame);
|
||||||
|
if (frame.getContentDocument()) |doc| {
|
||||||
|
// A frame's document should always ahave a page, but
|
||||||
|
// I'm not willing to crash a release build on that assertion.
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(doc._page != null);
|
||||||
|
}
|
||||||
|
if (doc._page) |frame_page| {
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
root(doc, opts, writer, frame_page) catch return error.WriteFailed;
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try children(node, opts, writer, page);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isVoidElement(el)) {
|
if (!isVoidElement(el)) {
|
||||||
try writer.writeAll("</");
|
try writer.writeAll("</");
|
||||||
try writer.writeAll(el.getTagNameDump());
|
try writer.writeAll(el.getTagNameDump());
|
||||||
@@ -172,7 +188,11 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
|||||||
try writer.writeAll(">\n");
|
try writer.writeAll(">\n");
|
||||||
},
|
},
|
||||||
.document_fragment => try children(node, opts, writer, page),
|
.document_fragment => try children(node, opts, writer, page),
|
||||||
.attribute => unreachable,
|
.attribute => {
|
||||||
|
// Not called normally, but can be called via XMLSerializer.serializeToString
|
||||||
|
// in which case it should return an empty string
|
||||||
|
try writer.writeAll("");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +314,12 @@ fn shouldEscapeText(node_: ?*Node) bool {
|
|||||||
if (node.is(Node.Element.Html.Script) != null) {
|
if (node.is(Node.Element.Html.Script) != null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// When scripting is enabled, <noscript> is a raw text element per the HTML spec
|
||||||
|
// (https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments).
|
||||||
|
// Its text content must not be HTML-escaped during serialization.
|
||||||
|
if (node.is(Node.Element.Html.Generic)) |generic| {
|
||||||
|
if (generic._tag == .noscript) return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
fn writeEscapedText(text: []const u8, writer: *std.Io.Writer) !void {
|
fn writeEscapedText(text: []const u8, writer: *std.Io.Writer) !void {
|
||||||
|
|||||||
@@ -251,7 +251,33 @@ fn _deleteNamedIndex(comptime T: type, local: *const Local, func: anytype, name:
|
|||||||
return handleIndexedReturn(T, F, false, local, ret, info, opts);
|
return handleIndexedReturn(T, F, false, local, ret, info, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handleIndexedReturn(comptime T: type, comptime F: type, comptime getter: bool, local: *const Local, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
pub fn getEnumerator(self: *Caller, comptime T: type, func: anytype, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||||
|
const local = &self.local;
|
||||||
|
|
||||||
|
var hs: js.HandleScope = undefined;
|
||||||
|
hs.init(local.isolate);
|
||||||
|
defer hs.deinit();
|
||||||
|
|
||||||
|
const info = PropertyCallbackInfo{ .handle = handle };
|
||||||
|
return _getEnumerator(T, local, func, info, opts) catch |err| {
|
||||||
|
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||||
|
// not intercepted
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _getEnumerator(comptime T: type, local: *const Local, func: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
|
const F = @TypeOf(func);
|
||||||
|
var args: ParameterTypes(F) = undefined;
|
||||||
|
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||||
|
if (@typeInfo(F).@"fn".params.len == 2) {
|
||||||
|
@field(args, "1") = local.ctx.page;
|
||||||
|
}
|
||||||
|
const ret = @call(.auto, func, args);
|
||||||
|
return handleIndexedReturn(T, F, true, local, ret, info, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleIndexedReturn(comptime T: type, comptime F: type, comptime with_value: bool, local: *const Local, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||||
// need to unwrap this error immediately for when opts.null_as_undefined == true
|
// need to unwrap this error immediately for when opts.null_as_undefined == true
|
||||||
// and we need to compare it to null;
|
// and we need to compare it to null;
|
||||||
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
|
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
|
||||||
@@ -274,7 +300,7 @@ fn handleIndexedReturn(comptime T: type, comptime F: type, comptime getter: bool
|
|||||||
else => ret,
|
else => ret,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (comptime getter) {
|
if (comptime with_value) {
|
||||||
info.getReturnValue().set(try local.zigValueToJs(non_error_ret, opts));
|
info.getReturnValue().set(try local.zigValueToJs(non_error_ret, opts));
|
||||||
}
|
}
|
||||||
// intercepted
|
// intercepted
|
||||||
@@ -463,6 +489,7 @@ const ReturnValue = struct {
|
|||||||
|
|
||||||
pub const Function = struct {
|
pub const Function = struct {
|
||||||
pub const Opts = struct {
|
pub const Opts = struct {
|
||||||
|
noop: bool = false,
|
||||||
static: bool = false,
|
static: bool = false,
|
||||||
dom_exception: bool = false,
|
dom_exception: bool = false,
|
||||||
as_typed_array: bool = false,
|
as_typed_array: bool = false,
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ env: *Env,
|
|||||||
page: *Page,
|
page: *Page,
|
||||||
isolate: js.Isolate,
|
isolate: js.Isolate,
|
||||||
|
|
||||||
|
// Per-context microtask queue for isolation between contexts
|
||||||
|
microtask_queue: *v8.MicrotaskQueue,
|
||||||
|
|
||||||
// The v8::Global<v8::Context>. When necessary, we can create a v8::Local<<v8::Context>>
|
// The v8::Global<v8::Context>. When necessary, we can create a v8::Local<<v8::Context>>
|
||||||
// from this, and we can free it when the context is done.
|
// from this, and we can free it when the context is done.
|
||||||
handle: v8.Global,
|
handle: v8.Global,
|
||||||
@@ -121,10 +124,6 @@ script_manager: ?*ScriptManager,
|
|||||||
// Our macrotasks
|
// Our macrotasks
|
||||||
scheduler: Scheduler,
|
scheduler: Scheduler,
|
||||||
|
|
||||||
// Prevents us from enqueuing a microtask for this context while we're shutting
|
|
||||||
// down.
|
|
||||||
shutting_down: bool = false,
|
|
||||||
|
|
||||||
unknown_properties: (if (IS_DEBUG) std.StringHashMapUnmanaged(UnknownPropertyStat) else void) = if (IS_DEBUG) .{} else {},
|
unknown_properties: (if (IS_DEBUG) std.StringHashMapUnmanaged(UnknownPropertyStat) else void) = if (IS_DEBUG) .{} else {},
|
||||||
|
|
||||||
const ModuleEntry = struct {
|
const ModuleEntry = struct {
|
||||||
@@ -146,16 +145,11 @@ const ModuleEntry = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn fromC(c_context: *const v8.Context) *Context {
|
pub fn fromC(c_context: *const v8.Context) *Context {
|
||||||
const data = v8.v8__Context__GetEmbedderData(c_context, 1).?;
|
return @ptrCast(@alignCast(v8.v8__Context__GetAlignedPointerFromEmbedderData(c_context, 1)));
|
||||||
const big_int = js.BigInt{ .handle = @ptrCast(data) };
|
|
||||||
return @ptrFromInt(big_int.getUint64());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fromIsolate(isolate: js.Isolate) *Context {
|
pub fn fromIsolate(isolate: js.Isolate) *Context {
|
||||||
const v8_context = v8.v8__Isolate__GetCurrentContext(isolate.handle).?;
|
return fromC(v8.v8__Isolate__GetCurrentContext(isolate.handle).?);
|
||||||
const data = v8.v8__Context__GetEmbedderData(v8_context, 1).?;
|
|
||||||
const big_int = js.BigInt{ .handle = @ptrCast(data) };
|
|
||||||
return @ptrFromInt(big_int.getUint64());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Context) void {
|
pub fn deinit(self: *Context) void {
|
||||||
@@ -169,21 +163,15 @@ pub fn deinit(self: *Context) void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer self.env.app.arena_pool.release(self.arena);
|
|
||||||
|
const env = self.env;
|
||||||
|
defer env.app.arena_pool.release(self.arena);
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
const entered = self.enter(&hs);
|
const entered = self.enter(&hs);
|
||||||
defer entered.exit();
|
defer entered.exit();
|
||||||
|
|
||||||
// We might have microtasks in the isolate that refence this context. The
|
// this can release objects
|
||||||
// only option we have is to run them. But a microtask could queue another
|
|
||||||
// microtask, so we set the shutting_down flag, so that any such microtask
|
|
||||||
// will be a noop (this isn't automatic, when v8 calls our microtask callback
|
|
||||||
// the first thing we'll check is if self.shutting_down == true).
|
|
||||||
self.shutting_down = true;
|
|
||||||
self.env.runMicrotasks();
|
|
||||||
|
|
||||||
// can release objects
|
|
||||||
self.scheduler.deinit();
|
self.scheduler.deinit();
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -244,7 +232,13 @@ pub fn deinit(self: *Context) void {
|
|||||||
v8.v8__Global__Reset(global);
|
v8.v8__Global__Reset(global);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
v8.v8__Global__Reset(&self.handle);
|
v8.v8__Global__Reset(&self.handle);
|
||||||
|
env.isolate.notifyContextDisposed();
|
||||||
|
// There can be other tasks associated with this context that we need to
|
||||||
|
// purge while the context is still alive.
|
||||||
|
_ = env.pumpMessageLoop();
|
||||||
|
v8.v8__MicrotaskQueue__DELETE(self.microtask_queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn weakRef(self: *Context, obj: anytype) void {
|
pub fn weakRef(self: *Context, obj: anytype) void {
|
||||||
@@ -606,9 +600,18 @@ pub fn dynamicModuleCallback(
|
|||||||
.isolate = self.isolate,
|
.isolate = self.isolate,
|
||||||
};
|
};
|
||||||
|
|
||||||
const resource = js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
|
const resource = blk: {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
|
const resource_value = js.Value{ .handle = resource_name.?, .local = &local };
|
||||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
if (resource_value.isNullOrUndefined()) {
|
||||||
|
// will only be null / undefined in extreme cases (e.g. WPT tests)
|
||||||
|
// where you're
|
||||||
|
break :blk self.page.base();
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
|
||||||
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
|
||||||
|
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
|
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
|
||||||
@@ -992,13 +995,10 @@ pub fn queueSlotchangeDelivery(self: *Context) !void {
|
|||||||
// But for these Context microtasks, we want to (a) make sure the context isn't
|
// But for these Context microtasks, we want to (a) make sure the context isn't
|
||||||
// being shut down and (b) that it's entered.
|
// being shut down and (b) that it's entered.
|
||||||
fn enqueueMicrotask(self: *Context, callback: anytype) void {
|
fn enqueueMicrotask(self: *Context, callback: anytype) void {
|
||||||
self.isolate.enqueueMicrotask(struct {
|
// Use context-specific microtask queue instead of isolate queue
|
||||||
|
v8.v8__MicrotaskQueue__EnqueueMicrotask(self.microtask_queue, self.isolate.handle, struct {
|
||||||
fn run(data: ?*anyopaque) callconv(.c) void {
|
fn run(data: ?*anyopaque) callconv(.c) void {
|
||||||
const ctx: *Context = @ptrCast(@alignCast(data.?));
|
const ctx: *Context = @ptrCast(@alignCast(data.?));
|
||||||
if (ctx.shutting_down) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
const entered = ctx.enter(&hs);
|
const entered = ctx.enter(&hs);
|
||||||
defer entered.exit();
|
defer entered.exit();
|
||||||
@@ -1008,10 +1008,11 @@ fn enqueueMicrotask(self: *Context, callback: anytype) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {
|
pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {
|
||||||
self.isolate.enqueueMicrotaskFunc(cb);
|
// Use context-specific microtask queue instead of isolate queue
|
||||||
|
v8.v8__MicrotaskQueue__EnqueueMicrotaskFunc(self.microtask_queue, self.isolate.handle, cb.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createFinalizerCallback(self: *Context, global: v8.Global, ptr: *anyopaque, finalizerFn: *const fn (ptr: *anyopaque) void) !*FinalizerCallback {
|
pub fn createFinalizerCallback(self: *Context, global: v8.Global, ptr: *anyopaque, finalizerFn: *const fn (ptr: *anyopaque, page: *Page) void) !*FinalizerCallback {
|
||||||
const fc = try self.finalizer_callback_pool.create();
|
const fc = try self.finalizer_callback_pool.create();
|
||||||
fc.* = .{
|
fc.* = .{
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
@@ -1031,10 +1032,10 @@ pub const FinalizerCallback = struct {
|
|||||||
ctx: *Context,
|
ctx: *Context,
|
||||||
ptr: *anyopaque,
|
ptr: *anyopaque,
|
||||||
global: v8.Global,
|
global: v8.Global,
|
||||||
finalizerFn: *const fn (ptr: *anyopaque) void,
|
finalizerFn: *const fn (ptr: *anyopaque, page: *Page) void,
|
||||||
|
|
||||||
pub fn deinit(self: *FinalizerCallback) void {
|
pub fn deinit(self: *FinalizerCallback) void {
|
||||||
self.finalizerFn(self.ptr);
|
self.finalizerFn(self.ptr, self.ctx.page);
|
||||||
self.ctx.finalizer_callback_pool.destroy(self);
|
self.ctx.finalizer_callback_pool.destroy(self);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ platform: *const Platform,
|
|||||||
// the global isolate
|
// the global isolate
|
||||||
isolate: js.Isolate,
|
isolate: js.Isolate,
|
||||||
|
|
||||||
contexts: std.ArrayList(*js.Context),
|
contexts: [64]*Context,
|
||||||
|
context_count: usize,
|
||||||
|
|
||||||
// just kept around because we need to free it on deinit
|
// just kept around because we need to free it on deinit
|
||||||
isolate_params: *v8.CreateParams,
|
isolate_params: *v8.CreateParams,
|
||||||
@@ -85,6 +86,8 @@ inspector: ?*Inspector,
|
|||||||
// which an be created once per isolaet.
|
// which an be created once per isolaet.
|
||||||
private_symbols: PrivateSymbols,
|
private_symbols: PrivateSymbols,
|
||||||
|
|
||||||
|
microtask_queues_are_running: bool,
|
||||||
|
|
||||||
pub const InitOpts = struct {
|
pub const InitOpts = struct {
|
||||||
with_inspector: bool = false,
|
with_inspector: bool = false,
|
||||||
};
|
};
|
||||||
@@ -203,7 +206,8 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
|||||||
return .{
|
return .{
|
||||||
.app = app,
|
.app = app,
|
||||||
.context_id = 0,
|
.context_id = 0,
|
||||||
.contexts = .empty,
|
.contexts = undefined,
|
||||||
|
.context_count = 0,
|
||||||
.isolate = isolate,
|
.isolate = isolate,
|
||||||
.platform = &app.platform,
|
.platform = &app.platform,
|
||||||
.templates = templates,
|
.templates = templates,
|
||||||
@@ -211,15 +215,16 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
|||||||
.inspector = inspector,
|
.inspector = inspector,
|
||||||
.global_template = global_eternal,
|
.global_template = global_eternal,
|
||||||
.private_symbols = private_symbols,
|
.private_symbols = private_symbols,
|
||||||
|
.microtask_queues_are_running = false,
|
||||||
.eternal_function_templates = eternal_function_templates,
|
.eternal_function_templates = eternal_function_templates,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Env) void {
|
pub fn deinit(self: *Env) void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(self.contexts.items.len == 0);
|
std.debug.assert(self.context_count == 0);
|
||||||
}
|
}
|
||||||
for (self.contexts.items) |ctx| {
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
ctx.deinit();
|
ctx.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,8 +233,6 @@ pub fn deinit(self: *Env) void {
|
|||||||
i.deinit(allocator);
|
i.deinit(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.contexts.deinit(allocator);
|
|
||||||
|
|
||||||
allocator.free(self.templates);
|
allocator.free(self.templates);
|
||||||
allocator.free(self.eternal_function_templates);
|
allocator.free(self.eternal_function_templates);
|
||||||
self.private_symbols.deinit();
|
self.private_symbols.deinit();
|
||||||
@@ -249,10 +252,19 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
|
|||||||
hs.init(isolate);
|
hs.init(isolate);
|
||||||
defer hs.deinit();
|
defer hs.deinit();
|
||||||
|
|
||||||
|
// Create a per-context microtask queue for isolation
|
||||||
|
const microtask_queue = v8.v8__MicrotaskQueue__New(isolate.handle, v8.kExplicit).?;
|
||||||
|
errdefer v8.v8__MicrotaskQueue__DELETE(microtask_queue);
|
||||||
|
|
||||||
// Get the global template that was created once per isolate
|
// Get the global template that was created once per isolate
|
||||||
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?));
|
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?));
|
||||||
v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime Snapshot.countInternalFields(Window.JsApi));
|
v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime Snapshot.countInternalFields(Window.JsApi));
|
||||||
const v8_context = v8.v8__Context__New(isolate.handle, global_template, null).?;
|
|
||||||
|
const v8_context = v8.v8__Context__New__Config(isolate.handle, &.{
|
||||||
|
.global_template = global_template,
|
||||||
|
.global_object = null,
|
||||||
|
.microtask_queue = microtask_queue,
|
||||||
|
}).?;
|
||||||
|
|
||||||
// Create the v8::Context and wrap it in a v8::Global
|
// Create the v8::Context and wrap it in a v8::Global
|
||||||
var context_global: v8.Global = undefined;
|
var context_global: v8.Global = undefined;
|
||||||
@@ -292,6 +304,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
|
|||||||
.handle = context_global,
|
.handle = context_global,
|
||||||
.templates = self.templates,
|
.templates = self.templates,
|
||||||
.call_arena = page.call_arena,
|
.call_arena = page.call_arena,
|
||||||
|
.microtask_queue = microtask_queue,
|
||||||
.script_manager = &page._script_manager,
|
.script_manager = &page._script_manager,
|
||||||
.scheduler = .init(context_arena),
|
.scheduler = .init(context_arena),
|
||||||
.finalizer_callback_pool = std.heap.MemoryPool(Context.FinalizerCallback).init(self.app.allocator),
|
.finalizer_callback_pool = std.heap.MemoryPool(Context.FinalizerCallback).init(self.app.allocator),
|
||||||
@@ -300,17 +313,24 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
|
|||||||
|
|
||||||
// Store a pointer to our context inside the v8 context so that, given
|
// Store a pointer to our context inside the v8 context so that, given
|
||||||
// a v8 context, we can get our context out
|
// a v8 context, we can get our context out
|
||||||
const data = isolate.initBigInt(@intFromPtr(context));
|
v8.v8__Context__SetAlignedPointerInEmbedderData(v8_context, 1, @ptrCast(context));
|
||||||
v8.v8__Context__SetEmbedderData(v8_context, 1, @ptrCast(data.handle));
|
|
||||||
|
const count = self.context_count;
|
||||||
|
if (count >= self.contexts.len) {
|
||||||
|
return error.TooManyContexts;
|
||||||
|
}
|
||||||
|
self.contexts[count] = context;
|
||||||
|
self.context_count = count + 1;
|
||||||
|
|
||||||
try self.contexts.append(self.app.allocator, context);
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroyContext(self: *Env, context: *Context) void {
|
pub fn destroyContext(self: *Env, context: *Context) void {
|
||||||
for (self.contexts.items, 0..) |ctx, i| {
|
for (self.contexts[0..self.context_count], 0..) |ctx, i| {
|
||||||
if (ctx == context) {
|
if (ctx == context) {
|
||||||
_ = self.contexts.swapRemove(i);
|
// Swap with last element and decrement count
|
||||||
|
self.context_count -= 1;
|
||||||
|
self.contexts[i] = self.contexts[self.context_count];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -328,16 +348,26 @@ pub fn destroyContext(self: *Env, context: *Context) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.deinit();
|
context.deinit();
|
||||||
isolate.notifyContextDisposed();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMicrotasks(self: *const Env) void {
|
pub fn runMicrotasks(self: *Env) void {
|
||||||
self.isolate.performMicrotasksCheckpoint();
|
if (self.microtask_queues_are_running == false) {
|
||||||
|
const v8_isolate = self.isolate.handle;
|
||||||
|
|
||||||
|
self.microtask_queues_are_running = true;
|
||||||
|
defer self.microtask_queues_are_running = false;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < self.context_count) : (i += 1) {
|
||||||
|
const ctx = self.contexts[i];
|
||||||
|
v8.v8__MicrotaskQueue__PerformCheckpoint(ctx.microtask_queue, v8_isolate);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMacrotasks(self: *Env) !?u64 {
|
pub fn runMacrotasks(self: *Env) !?u64 {
|
||||||
var ms_to_next_task: ?u64 = null;
|
var ms_to_next_task: ?u64 = null;
|
||||||
for (self.contexts.items) |ctx| {
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
if (comptime builtin.is_test == false) {
|
if (comptime builtin.is_test == false) {
|
||||||
// I hate this comptime check as much as you do. But we have tests
|
// I hate this comptime check as much as you do. But we have tests
|
||||||
// which rely on short execution before shutdown. In real world, it's
|
// which rely on short execution before shutdown. In real world, it's
|
||||||
@@ -360,12 +390,31 @@ pub fn runMacrotasks(self: *Env) !?u64 {
|
|||||||
return ms_to_next_task;
|
return ms_to_next_task;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pumpMessageLoop(self: *const Env) bool {
|
pub fn pumpMessageLoop(self: *const Env) void {
|
||||||
var hs: v8.HandleScope = undefined;
|
var hs: v8.HandleScope = undefined;
|
||||||
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);
|
||||||
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
return v8.v8__Platform__PumpMessageLoop(self.platform.handle, self.isolate.handle, false);
|
const isolate = self.isolate.handle;
|
||||||
|
const platform = self.platform.handle;
|
||||||
|
while (v8.v8__Platform__PumpMessageLoop(platform, isolate, false)) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hasBackgroundTasks(self: *const Env) bool {
|
||||||
|
return v8.v8__Isolate__HasPendingBackgroundTasks(self.isolate.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn waitForBackgroundTasks(self: *Env) void {
|
||||||
|
var hs: v8.HandleScope = undefined;
|
||||||
|
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);
|
||||||
|
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||||
|
|
||||||
|
const isolate = self.isolate.handle;
|
||||||
|
const platform = self.platform.handle;
|
||||||
|
while (v8.v8__Isolate__HasPendingBackgroundTasks(isolate)) {
|
||||||
|
_ = v8.v8__Platform__PumpMessageLoop(platform, isolate, true);
|
||||||
|
self.runMicrotasks();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runIdleTasks(self: *const Env) void {
|
pub fn runIdleTasks(self: *const Env) void {
|
||||||
|
|||||||
@@ -241,8 +241,6 @@ pub const Session = struct {
|
|||||||
msg.ptr,
|
msg.ptr,
|
||||||
msg.len,
|
msg.len,
|
||||||
);
|
);
|
||||||
|
|
||||||
v8.v8__Isolate__PerformMicrotaskCheckpoint(isolate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets a value by object ID regardless of which context it is in.
|
// Gets a value by object ID regardless of which context it is in.
|
||||||
|
|||||||
@@ -41,18 +41,6 @@ pub fn exit(self: Isolate) void {
|
|||||||
v8.v8__Isolate__Exit(self.handle);
|
v8.v8__Isolate__Exit(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn performMicrotasksCheckpoint(self: Isolate) void {
|
|
||||||
v8.v8__Isolate__PerformMicrotaskCheckpoint(self.handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enqueueMicrotask(self: Isolate, callback: anytype, data: anytype) void {
|
|
||||||
v8.v8__Isolate__EnqueueMicrotask(self.handle, callback, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enqueueMicrotaskFunc(self: Isolate, function: js.Function) void {
|
|
||||||
v8.v8__Isolate__EnqueueMicrotaskFunc(self.handle, function.handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lowMemoryNotification(self: Isolate) void {
|
pub fn lowMemoryNotification(self: Isolate) void {
|
||||||
v8.v8__Isolate__LowMemoryNotification(self.handle);
|
v8.v8__Isolate__LowMemoryNotification(self.handle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 Page = @import("../Page.zig");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const string = @import("../../string.zig");
|
const string = @import("../../string.zig");
|
||||||
|
|
||||||
@@ -81,8 +82,14 @@ pub fn createTypedArray(self: *const Local, comptime array_type: js.ArrayType, s
|
|||||||
return .init(self, size);
|
return .init(self, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn runMacrotasks(self: *const Local) void {
|
||||||
|
const env = self.ctx.env;
|
||||||
|
env.pumpMessageLoop();
|
||||||
|
env.runMicrotasks(); // macrotasks can cause microtasks to queue
|
||||||
|
}
|
||||||
|
|
||||||
pub fn runMicrotasks(self: *const Local) void {
|
pub fn runMicrotasks(self: *const Local) void {
|
||||||
self.isolate.performMicrotasksCheckpoint();
|
self.ctx.env.runMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
// == Executors ==
|
// == Executors ==
|
||||||
@@ -216,7 +223,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
|
|||||||
try ctx.finalizer_callbacks.put(ctx.arena, @intFromPtr(resolved.ptr), fc);
|
try ctx.finalizer_callbacks.put(ctx.arena, @intFromPtr(resolved.ptr), fc);
|
||||||
}
|
}
|
||||||
|
|
||||||
conditionallyFlagHandoff(value);
|
conditionallyReference(value);
|
||||||
if (@hasDecl(JsApi.Meta, "weak")) {
|
if (@hasDecl(JsApi.Meta, "weak")) {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(JsApi.Meta.weak == true);
|
std.debug.assert(JsApi.Meta.weak == true);
|
||||||
@@ -322,6 +329,7 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
|
|||||||
},
|
},
|
||||||
|
|
||||||
inline
|
inline
|
||||||
|
js.Array,
|
||||||
js.Function,
|
js.Function,
|
||||||
js.Object,
|
js.Object,
|
||||||
js.Promise,
|
js.Promise,
|
||||||
@@ -1061,7 +1069,7 @@ const Resolved = struct {
|
|||||||
class_id: u16,
|
class_id: u16,
|
||||||
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry,
|
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry,
|
||||||
finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null,
|
finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null,
|
||||||
finalizer_from_zig: ?*const fn (ptr: *anyopaque) void = null,
|
finalizer_from_zig: ?*const fn (ptr: *anyopaque, page: *Page) void = null,
|
||||||
};
|
};
|
||||||
pub fn resolveValue(value: anytype) Resolved {
|
pub fn resolveValue(value: anytype) Resolved {
|
||||||
const T = bridge.Struct(@TypeOf(value));
|
const T = bridge.Struct(@TypeOf(value));
|
||||||
@@ -1100,14 +1108,14 @@ fn resolveT(comptime T: type, value: *anyopaque) Resolved {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn conditionallyFlagHandoff(value: anytype) void {
|
fn conditionallyReference(value: anytype) void {
|
||||||
const T = bridge.Struct(@TypeOf(value));
|
const T = bridge.Struct(@TypeOf(value));
|
||||||
if (@hasField(T, "_v8_handoff")) {
|
if (@hasDecl(T, "acquireRef")) {
|
||||||
value._v8_handoff = true;
|
value.acquireRef();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (@hasField(T, "_proto")) {
|
if (@hasField(T, "_proto")) {
|
||||||
conditionallyFlagHandoff(value._proto);
|
conditionallyReference(value._proto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1208,13 +1216,20 @@ fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnman
|
|||||||
gop.value_ptr.* = {};
|
gop.value_ptr.* = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const names_arr = js_obj.getOwnPropertyNames();
|
|
||||||
const len = names_arr.len();
|
|
||||||
|
|
||||||
if (depth > 20) {
|
if (depth > 20) {
|
||||||
return writer.writeAll("...deeply nested object...");
|
return writer.writeAll("...deeply nested object...");
|
||||||
}
|
}
|
||||||
const own_len = js_obj.getOwnPropertyNames().len();
|
|
||||||
|
const names_arr = js_obj.getOwnPropertyNames() catch {
|
||||||
|
return writer.writeAll("...invalid object...");
|
||||||
|
};
|
||||||
|
const len = names_arr.len();
|
||||||
|
|
||||||
|
const own_len = blk: {
|
||||||
|
const own_names = js_obj.getOwnPropertyNames() catch break :blk 0;
|
||||||
|
break :blk own_names.len();
|
||||||
|
};
|
||||||
|
|
||||||
if (own_len == 0) {
|
if (own_len == 0) {
|
||||||
const js_val_str = try js_val.toStringSlice();
|
const js_val_str = try js_val.toStringSlice();
|
||||||
if (js_val_str.len > 2000) {
|
if (js_val_str.len > 2000) {
|
||||||
|
|||||||
@@ -129,8 +129,14 @@ pub fn isNullOrUndefined(self: Object) bool {
|
|||||||
return v8.v8__Value__IsNullOrUndefined(@ptrCast(self.handle));
|
return v8.v8__Value__IsNullOrUndefined(@ptrCast(self.handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOwnPropertyNames(self: Object) js.Array {
|
pub fn getOwnPropertyNames(self: Object) !js.Array {
|
||||||
const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.local.handle).?;
|
const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.local.handle) orelse {
|
||||||
|
// This is almost always a fatal error case. Either we're in some exception
|
||||||
|
// and things are messy, or we're shutting down, or someone has messed up
|
||||||
|
// the object (like some WPT tests do).
|
||||||
|
return error.TypeError;
|
||||||
|
};
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.local = self.local,
|
.local = self.local,
|
||||||
.handle = handle,
|
.handle = handle,
|
||||||
@@ -145,8 +151,11 @@ pub fn getPropertyNames(self: Object) js.Array {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nameIterator(self: Object) NameIterator {
|
pub fn nameIterator(self: Object) !NameIterator {
|
||||||
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?;
|
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle) orelse {
|
||||||
|
// see getOwnPropertyNames above
|
||||||
|
return error.TypeError;
|
||||||
|
};
|
||||||
const count = v8.v8__Array__Length(handle);
|
const count = v8.v8__Array__Length(handle);
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
|
|||||||
@@ -282,9 +282,13 @@ fn hasNamedIndexedGetter(comptime JsApi: type) bool {
|
|||||||
fn countExternalReferences() comptime_int {
|
fn countExternalReferences() comptime_int {
|
||||||
@setEvalBranchQuota(100_000);
|
@setEvalBranchQuota(100_000);
|
||||||
|
|
||||||
// +1 for the illegal constructor callback
|
var count: comptime_int = 0;
|
||||||
var count: comptime_int = 1;
|
|
||||||
var has_non_template_property: bool = false;
|
// +1 for the illegal constructor callback shared by various types
|
||||||
|
count += 1;
|
||||||
|
|
||||||
|
// +1 for the noop function shared by various types
|
||||||
|
count += 1;
|
||||||
|
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
// Constructor (only if explicit)
|
// Constructor (only if explicit)
|
||||||
@@ -304,17 +308,18 @@ fn countExternalReferences() comptime_int {
|
|||||||
const T = @TypeOf(value);
|
const T = @TypeOf(value);
|
||||||
if (T == bridge.Accessor) {
|
if (T == bridge.Accessor) {
|
||||||
count += 1; // getter
|
count += 1; // getter
|
||||||
if (value.setter != null) count += 1; // setter
|
if (value.setter != null) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
} else if (T == bridge.Function) {
|
} else if (T == bridge.Function) {
|
||||||
count += 1;
|
count += 1;
|
||||||
} else if (T == bridge.Property) {
|
|
||||||
if (value.template == false) {
|
|
||||||
has_non_template_property = true;
|
|
||||||
}
|
|
||||||
} else if (T == bridge.Iterator) {
|
} else if (T == bridge.Iterator) {
|
||||||
count += 1;
|
count += 1;
|
||||||
} else if (T == bridge.Indexed) {
|
} else if (T == bridge.Indexed) {
|
||||||
count += 1;
|
count += 1;
|
||||||
|
if (value.enumerator != null) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
} else if (T == bridge.NamedIndexed) {
|
} else if (T == bridge.NamedIndexed) {
|
||||||
count += 1; // getter
|
count += 1; // getter
|
||||||
if (value.setter != null) count += 1;
|
if (value.setter != null) count += 1;
|
||||||
@@ -323,10 +328,6 @@ fn countExternalReferences() comptime_int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (has_non_template_property) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In debug mode, add unknown property callbacks for types without NamedIndexed
|
// In debug mode, add unknown property callbacks for types without NamedIndexed
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
@@ -346,7 +347,8 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
|
references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
|
||||||
var has_non_template_property = false;
|
references[idx] = @bitCast(@intFromPtr(&bridge.Function.noopFunction));
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
if (@hasDecl(JsApi, "constructor")) {
|
if (@hasDecl(JsApi, "constructor")) {
|
||||||
@@ -373,16 +375,16 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
} else if (T == bridge.Function) {
|
} else if (T == bridge.Function) {
|
||||||
references[idx] = @bitCast(@intFromPtr(value.func));
|
references[idx] = @bitCast(@intFromPtr(value.func));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
} else if (T == bridge.Property) {
|
|
||||||
if (value.template == false) {
|
|
||||||
has_non_template_property = true;
|
|
||||||
}
|
|
||||||
} else if (T == bridge.Iterator) {
|
} else if (T == bridge.Iterator) {
|
||||||
references[idx] = @bitCast(@intFromPtr(value.func));
|
references[idx] = @bitCast(@intFromPtr(value.func));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
} else if (T == bridge.Indexed) {
|
} else if (T == bridge.Indexed) {
|
||||||
references[idx] = @bitCast(@intFromPtr(value.getter));
|
references[idx] = @bitCast(@intFromPtr(value.getter));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
if (value.enumerator) |enumerator| {
|
||||||
|
references[idx] = @bitCast(@intFromPtr(enumerator));
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
} else if (T == bridge.NamedIndexed) {
|
} else if (T == bridge.NamedIndexed) {
|
||||||
references[idx] = @bitCast(@intFromPtr(value.getter));
|
references[idx] = @bitCast(@intFromPtr(value.getter));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
@@ -398,11 +400,6 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (has_non_template_property) {
|
|
||||||
references[idx] = @bitCast(@intFromPtr(&bridge.Property.getter));
|
|
||||||
idx += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In debug mode, collect unknown property callbacks for types without NamedIndexed
|
// In debug mode, collect unknown property callbacks for types without NamedIndexed
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
inline for (JsApis) |JsApi| {
|
inline for (JsApis) |JsApi| {
|
||||||
@@ -486,8 +483,8 @@ pub fn countInternalFields(comptime JsApi: type) u8 {
|
|||||||
|
|
||||||
// Attaches JsApi members to the prototype template (normal case)
|
// Attaches JsApi members to the prototype template (normal case)
|
||||||
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void {
|
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void {
|
||||||
const target = v8.v8__FunctionTemplate__PrototypeTemplate(template);
|
|
||||||
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||||
|
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);
|
||||||
|
|
||||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||||
var has_named_index_getter = false;
|
var has_named_index_getter = false;
|
||||||
@@ -505,14 +502,14 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
if (value.static) {
|
if (value.static) {
|
||||||
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
|
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
|
||||||
} else {
|
} else {
|
||||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(target, js_name, getter_callback);
|
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(prototype, js_name, getter_callback);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(value.static == false);
|
std.debug.assert(value.static == false);
|
||||||
}
|
}
|
||||||
const setter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.setter.? }).?);
|
const setter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.setter.? }).?);
|
||||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(target, js_name, getter_callback, setter_callback);
|
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(prototype, js_name, getter_callback, setter_callback);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Function => {
|
bridge.Function => {
|
||||||
@@ -521,16 +518,16 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
if (value.static) {
|
if (value.static) {
|
||||||
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
||||||
} else {
|
} else {
|
||||||
v8.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.None);
|
v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.None);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Indexed => {
|
bridge.Indexed => {
|
||||||
var configuration: v8.IndexedPropertyHandlerConfiguration = .{
|
var configuration: v8.IndexedPropertyHandlerConfiguration = .{
|
||||||
.getter = value.getter,
|
.getter = value.getter,
|
||||||
|
.enumerator = value.enumerator,
|
||||||
.setter = null,
|
.setter = null,
|
||||||
.query = null,
|
.query = null,
|
||||||
.deleter = null,
|
.deleter = null,
|
||||||
.enumerator = null,
|
|
||||||
.definer = null,
|
.definer = null,
|
||||||
.descriptor = null,
|
.descriptor = null,
|
||||||
.data = null,
|
.data = null,
|
||||||
@@ -559,7 +556,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
v8.v8__Symbol__GetAsyncIterator(isolate)
|
v8.v8__Symbol__GetAsyncIterator(isolate)
|
||||||
else
|
else
|
||||||
v8.v8__Symbol__GetIterator(isolate);
|
v8.v8__Symbol__GetIterator(isolate);
|
||||||
v8.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.None);
|
v8.v8__Template__Set(@ptrCast(prototype), js_name, @ptrCast(function_template), v8.None);
|
||||||
},
|
},
|
||||||
bridge.Property => {
|
bridge.Property => {
|
||||||
const js_value = switch (value.value) {
|
const js_value = switch (value.value) {
|
||||||
@@ -568,22 +565,14 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
};
|
};
|
||||||
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
|
|
||||||
if (value.template == false) {
|
{
|
||||||
// not defined on the template, only on the instance. This
|
const flags = if (value.readonly) v8.ReadOnly + v8.DontDelete else 0;
|
||||||
// is like an Accessor, but because the value is known at
|
v8.v8__Template__Set(@ptrCast(prototype), js_name, js_value, flags);
|
||||||
// compile time, we skip _a lot_ of code and quickly return
|
}
|
||||||
// the hard-coded value
|
|
||||||
const getter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
if (value.template) {
|
||||||
.callback = bridge.Property.getter,
|
// apply it both to the type itself (e.g. Node.Elem)
|
||||||
.data = js_value,
|
|
||||||
.side_effect_type = v8.kSideEffectType_HasSideEffectToReceiver,
|
|
||||||
}));
|
|
||||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(target, js_name, getter_callback);
|
|
||||||
} else {
|
|
||||||
// apply it both to the type itself
|
|
||||||
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||||
// and to instances of the type
|
|
||||||
v8.v8__Template__Set(@ptrCast(target), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Constructor => {}, // already handled in generateConstructor
|
bridge.Constructor => {}, // already handled in generateConstructor
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ pub fn Builder(comptime T: type) type {
|
|||||||
return Function.init(T, func, opts);
|
return Function.init(T, func, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn indexed(comptime getter_func: anytype, comptime opts: Indexed.Opts) Indexed {
|
pub fn indexed(comptime getter_func: anytype, comptime enumerator_func: anytype, comptime opts: Indexed.Opts) Indexed {
|
||||||
return Indexed.init(T, getter_func, opts);
|
return Indexed.init(T, getter_func, enumerator_func, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn namedIndexed(comptime getter_func: anytype, setter_func: anytype, deleter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed {
|
pub fn namedIndexed(comptime getter_func: anytype, setter_func: anytype, deleter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed {
|
||||||
@@ -104,11 +104,11 @@ pub fn Builder(comptime T: type) type {
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool) void) Finalizer {
|
pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool, page: *Page) void) Finalizer {
|
||||||
return .{
|
return .{
|
||||||
.from_zig = struct {
|
.from_zig = struct {
|
||||||
fn wrap(ptr: *anyopaque) void {
|
fn wrap(ptr: *anyopaque, page: *Page) void {
|
||||||
func(@ptrCast(@alignCast(ptr)), true);
|
func(@ptrCast(@alignCast(ptr)), true, page);
|
||||||
}
|
}
|
||||||
}.wrap,
|
}.wrap,
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ pub fn Builder(comptime T: type) type {
|
|||||||
const ctx = fc.ctx;
|
const ctx = fc.ctx;
|
||||||
const value_ptr = fc.ptr;
|
const value_ptr = fc.ptr;
|
||||||
if (ctx.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
if (ctx.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
||||||
func(@ptrCast(@alignCast(value_ptr)), false);
|
func(@ptrCast(@alignCast(value_ptr)), false, ctx.page);
|
||||||
ctx.release(value_ptr);
|
ctx.release(value_ptr);
|
||||||
} else {
|
} else {
|
||||||
// A bit weird, but v8 _requires_ that we release it
|
// A bit weird, but v8 _requires_ that we release it
|
||||||
@@ -160,6 +160,7 @@ pub const Constructor = struct {
|
|||||||
pub const Function = struct {
|
pub const Function = struct {
|
||||||
static: bool,
|
static: bool,
|
||||||
arity: usize,
|
arity: usize,
|
||||||
|
noop: bool = false,
|
||||||
cache: ?Caller.Function.Opts.Caching = null,
|
cache: ?Caller.Function.Opts.Caching = null,
|
||||||
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||||
|
|
||||||
@@ -168,7 +169,7 @@ pub const Function = struct {
|
|||||||
.cache = opts.cache,
|
.cache = opts.cache,
|
||||||
.static = opts.static,
|
.static = opts.static,
|
||||||
.arity = getArity(@TypeOf(func)),
|
.arity = getArity(@TypeOf(func)),
|
||||||
.func = struct {
|
.func = if (opts.noop) noopFunction else struct {
|
||||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||||
Caller.Function.call(T, handle.?, func, opts);
|
Caller.Function.call(T, handle.?, func, opts);
|
||||||
}
|
}
|
||||||
@@ -176,6 +177,8 @@ pub const Function = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn noopFunction(_: ?*const v8.FunctionCallbackInfo) callconv(.c) void {}
|
||||||
|
|
||||||
fn getArity(comptime T: type) usize {
|
fn getArity(comptime T: type) usize {
|
||||||
var count: usize = 0;
|
var count: usize = 0;
|
||||||
var params = @typeInfo(T).@"fn".params;
|
var params = @typeInfo(T).@"fn".params;
|
||||||
@@ -227,26 +230,44 @@ pub const Accessor = struct {
|
|||||||
|
|
||||||
pub const Indexed = struct {
|
pub const Indexed = struct {
|
||||||
getter: *const fn (idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
getter: *const fn (idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||||
|
enumerator: ?*const fn (handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
as_typed_array: bool = false,
|
as_typed_array: bool = false,
|
||||||
null_as_undefined: bool = false,
|
null_as_undefined: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) Indexed {
|
fn init(comptime T: type, comptime getter: anytype, comptime enumerator: anytype, comptime opts: Opts) Indexed {
|
||||||
return .{ .getter = struct {
|
var indexed = Indexed{
|
||||||
fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
.enumerator = null,
|
||||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
.getter = struct {
|
||||||
var caller: Caller = undefined;
|
fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
caller.init(v8_isolate);
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
defer caller.deinit();
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
|
defer caller.deinit();
|
||||||
|
|
||||||
return caller.getIndex(T, getter, idx, handle.?, .{
|
return caller.getIndex(T, getter, idx, handle.?, .{
|
||||||
.as_typed_array = opts.as_typed_array,
|
.as_typed_array = opts.as_typed_array,
|
||||||
.null_as_undefined = opts.null_as_undefined,
|
.null_as_undefined = opts.null_as_undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}.wrap };
|
}.wrap,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (@typeInfo(@TypeOf(enumerator)) != .null) {
|
||||||
|
indexed.enumerator = struct {
|
||||||
|
fn wrap(handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||||
|
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||||
|
var caller: Caller = undefined;
|
||||||
|
caller.init(v8_isolate);
|
||||||
|
defer caller.deinit();
|
||||||
|
return caller.getEnumerator(T, enumerator, handle.?, .{});
|
||||||
|
}
|
||||||
|
}.wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexed;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -367,6 +388,7 @@ pub const Callable = struct {
|
|||||||
pub const Property = struct {
|
pub const Property = struct {
|
||||||
value: Value,
|
value: Value,
|
||||||
template: bool,
|
template: bool,
|
||||||
|
readonly: bool,
|
||||||
|
|
||||||
const Value = union(enum) {
|
const Value = union(enum) {
|
||||||
null,
|
null,
|
||||||
@@ -378,27 +400,22 @@ pub const Property = struct {
|
|||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
template: bool,
|
template: bool,
|
||||||
|
readonly: bool = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn init(value: Value, opts: Opts) Property {
|
fn init(value: Value, opts: Opts) Property {
|
||||||
return .{
|
return .{
|
||||||
.value = value,
|
.value = value,
|
||||||
.template = opts.template,
|
.template = opts.template,
|
||||||
|
.readonly = opts.readonly,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getter(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
|
||||||
const value = v8.v8__FunctionCallbackInfo__Data(handle.?);
|
|
||||||
var rv: v8.ReturnValue = undefined;
|
|
||||||
v8.v8__FunctionCallbackInfo__GetReturnValue(handle.?, &rv);
|
|
||||||
v8.v8__ReturnValue__Set(rv, value);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Finalizer = struct {
|
const Finalizer = struct {
|
||||||
// The finalizer wrapper when called fro Zig. This is only called on
|
// The finalizer wrapper when called fro Zig. This is only called on
|
||||||
// Context.deinit
|
// Context.deinit
|
||||||
from_zig: *const fn (ctx: *anyopaque) void,
|
from_zig: *const fn (ctx: *anyopaque, page: *Page) void,
|
||||||
|
|
||||||
// The finalizer wrapper when called from V8. This may never be called
|
// The finalizer wrapper when called from V8. This may never be called
|
||||||
// (hence why we fallback to calling in Context.denit). If it is called,
|
// (hence why we fallback to calling in Context.denit). If it is called,
|
||||||
@@ -713,6 +730,7 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/css/CSSStyleRule.zig"),
|
@import("../webapi/css/CSSStyleRule.zig"),
|
||||||
@import("../webapi/css/CSSStyleSheet.zig"),
|
@import("../webapi/css/CSSStyleSheet.zig"),
|
||||||
@import("../webapi/css/CSSStyleProperties.zig"),
|
@import("../webapi/css/CSSStyleProperties.zig"),
|
||||||
|
@import("../webapi/css/FontFaceSet.zig"),
|
||||||
@import("../webapi/css/MediaQueryList.zig"),
|
@import("../webapi/css/MediaQueryList.zig"),
|
||||||
@import("../webapi/css/StyleSheetList.zig"),
|
@import("../webapi/css/StyleSheetList.zig"),
|
||||||
@import("../webapi/Document.zig"),
|
@import("../webapi/Document.zig"),
|
||||||
@@ -857,6 +875,7 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/IdleDeadline.zig"),
|
@import("../webapi/IdleDeadline.zig"),
|
||||||
@import("../webapi/Blob.zig"),
|
@import("../webapi/Blob.zig"),
|
||||||
@import("../webapi/File.zig"),
|
@import("../webapi/File.zig"),
|
||||||
|
@import("../webapi/FileReader.zig"),
|
||||||
@import("../webapi/Screen.zig"),
|
@import("../webapi/Screen.zig"),
|
||||||
@import("../webapi/VisualViewport.zig"),
|
@import("../webapi/VisualViewport.zig"),
|
||||||
@import("../webapi/PerformanceObserver.zig"),
|
@import("../webapi/PerformanceObserver.zig"),
|
||||||
@@ -865,6 +884,8 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/navigation/NavigationActivation.zig"),
|
@import("../webapi/navigation/NavigationActivation.zig"),
|
||||||
@import("../webapi/canvas/CanvasRenderingContext2D.zig"),
|
@import("../webapi/canvas/CanvasRenderingContext2D.zig"),
|
||||||
@import("../webapi/canvas/WebGLRenderingContext.zig"),
|
@import("../webapi/canvas/WebGLRenderingContext.zig"),
|
||||||
|
@import("../webapi/canvas/OffscreenCanvas.zig"),
|
||||||
|
@import("../webapi/canvas/OffscreenCanvasRenderingContext2D.zig"),
|
||||||
@import("../webapi/SubtleCrypto.zig"),
|
@import("../webapi/SubtleCrypto.zig"),
|
||||||
@import("../webapi/Selection.zig"),
|
@import("../webapi/Selection.zig"),
|
||||||
@import("../webapi/ImageData.zig"),
|
@import("../webapi/ImageData.zig"),
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ fn render(node: *Node, state: *State, writer: *std.Io.Writer, page: *Page) error
|
|||||||
},
|
},
|
||||||
.cdata => |cd| {
|
.cdata => |cd| {
|
||||||
if (node.is(Node.CData.Text)) |_| {
|
if (node.is(Node.CData.Text)) |_| {
|
||||||
var text = cd.getData();
|
var text = cd.getData().str();
|
||||||
if (state.pre_node) |pre| {
|
if (state.pre_node) |pre| {
|
||||||
if (node.parentNode() == pre and node.nextSibling() == null) {
|
if (node.parentNode() == pre and node.nextSibling() == null) {
|
||||||
text = std.mem.trimRight(u8, text, " \t\r\n");
|
text = std.mem.trimRight(u8, text, " \t\r\n");
|
||||||
|
|||||||
@@ -88,3 +88,15 @@
|
|||||||
ctx.putImageData(imageData, 0, 0, 0, 0, 5, 5);
|
ctx.putImageData(imageData, 0, 0, 0, 0, 5, 5);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script id="getter">
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
testing.expectEqual('10px sans-serif', ctx.font);
|
||||||
|
ctx.font = 'bold 48px serif'
|
||||||
|
testing.expectEqual('bold 48px serif', ctx.font);
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
64
src/browser/tests/canvas/offscreen_canvas.html
Normal file
64
src/browser/tests/canvas/offscreen_canvas.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(256, 256);
|
||||||
|
testing.expectEqual(true, canvas instanceof OffscreenCanvas);
|
||||||
|
testing.expectEqual(canvas.width, 256);
|
||||||
|
testing.expectEqual(canvas.height, 256);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas#width>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(100, 200);
|
||||||
|
testing.expectEqual(canvas.width, 100);
|
||||||
|
canvas.width = 300;
|
||||||
|
testing.expectEqual(canvas.width, 300);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas#height>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(100, 200);
|
||||||
|
testing.expectEqual(canvas.height, 200);
|
||||||
|
canvas.height = 400;
|
||||||
|
testing.expectEqual(canvas.height, 400);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas#getContext>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(64, 64);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
testing.expectEqual(true, ctx instanceof OffscreenCanvasRenderingContext2D);
|
||||||
|
// We can't really test rendering but let's try to call it at least.
|
||||||
|
ctx.fillRect(0, 0, 10, 10);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=OffscreenCanvas#convertToBlob>
|
||||||
|
{
|
||||||
|
const canvas = new OffscreenCanvas(64, 64);
|
||||||
|
const promise = canvas.convertToBlob();
|
||||||
|
testing.expectEqual(true, promise instanceof Promise);
|
||||||
|
// The promise should resolve to a Blob (even if empty)
|
||||||
|
promise.then(blob => {
|
||||||
|
testing.expectEqual(true, blob instanceof Blob);
|
||||||
|
testing.expectEqual(blob.size, 0); // Empty since no rendering
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=HTMLCanvasElement#transferControlToOffscreen>
|
||||||
|
{
|
||||||
|
const htmlCanvas = document.createElement("canvas");
|
||||||
|
htmlCanvas.width = 128;
|
||||||
|
htmlCanvas.height = 96;
|
||||||
|
const offscreen = htmlCanvas.transferControlToOffscreen();
|
||||||
|
testing.expectEqual(true, offscreen instanceof OffscreenCanvas);
|
||||||
|
testing.expectEqual(offscreen.width, 128);
|
||||||
|
testing.expectEqual(offscreen.height, 96);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
58
src/browser/tests/css/font_face_set.html
Normal file
58
src/browser/tests/css/font_face_set.html
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id="document_fonts_exists">
|
||||||
|
{
|
||||||
|
testing.expectTrue(document.fonts !== undefined);
|
||||||
|
testing.expectTrue(document.fonts !== null);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_same_instance">
|
||||||
|
{
|
||||||
|
// Should return same instance each time
|
||||||
|
const f1 = document.fonts;
|
||||||
|
const f2 = document.fonts;
|
||||||
|
testing.expectTrue(f1 === f2);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_status">
|
||||||
|
{
|
||||||
|
testing.expectEqual('loaded', document.fonts.status);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_size">
|
||||||
|
{
|
||||||
|
testing.expectEqual(0, document.fonts.size);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_ready_is_promise">
|
||||||
|
{
|
||||||
|
const ready = document.fonts.ready;
|
||||||
|
testing.expectTrue(ready instanceof Promise);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_ready_resolves">
|
||||||
|
{
|
||||||
|
let resolved = false;
|
||||||
|
document.fonts.ready.then(() => { resolved = true; });
|
||||||
|
// Promise resolution is async; just confirm .then() does not throw
|
||||||
|
testing.expectTrue(typeof document.fonts.ready.then === 'function');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_check">
|
||||||
|
{
|
||||||
|
testing.expectTrue(document.fonts.check('16px sans-serif'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="document_fonts_constructor_name">
|
||||||
|
{
|
||||||
|
testing.expectEqual('FontFaceSet', document.fonts.constructor.name);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
testing.expectEqual(10, document.childNodes[0].nodeType);
|
testing.expectEqual(10, document.childNodes[0].nodeType);
|
||||||
testing.expectEqual(null, document.parentNode);
|
testing.expectEqual(null, document.parentNode);
|
||||||
testing.expectEqual(undefined, document.getCurrentScript);
|
testing.expectEqual(undefined, document.getCurrentScript);
|
||||||
testing.expectEqual("http://127.0.0.1:9582/src/browser/tests/document/document.html", document.URL);
|
testing.expectEqual(testing.BASE_URL + 'document/document.html', document.URL);
|
||||||
testing.expectEqual(window, document.defaultView);
|
testing.expectEqual(window, document.defaultView);
|
||||||
testing.expectEqual(false, document.hidden);
|
testing.expectEqual(false, document.hidden);
|
||||||
testing.expectEqual("visible", document.visibilityState);
|
testing.expectEqual("visible", document.visibilityState);
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
testing.expectEqual('CSS1Compat', document.compatMode);
|
testing.expectEqual('CSS1Compat', document.compatMode);
|
||||||
testing.expectEqual(document.URL, document.documentURI);
|
testing.expectEqual(document.URL, document.documentURI);
|
||||||
testing.expectEqual('', document.referrer);
|
testing.expectEqual('', document.referrer);
|
||||||
testing.expectEqual('127.0.0.1', document.domain);
|
testing.expectEqual(testing.HOST, document.domain);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=programmatic_document_metadata>
|
<script id=programmatic_document_metadata>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
testing.expectEqual('CSS1Compat', doc.compatMode);
|
testing.expectEqual('CSS1Compat', doc.compatMode);
|
||||||
testing.expectEqual('', doc.referrer);
|
testing.expectEqual('', doc.referrer);
|
||||||
// Programmatic document should have empty domain (no URL/origin)
|
// Programmatic document should have empty domain (no URL/origin)
|
||||||
testing.expectEqual('127.0.0.1', doc.domain);
|
testing.expectEqual(testing.HOST, doc.domain);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Test anchors and links -->
|
<!-- Test anchors and links -->
|
||||||
|
|||||||
@@ -27,7 +27,9 @@
|
|||||||
testing.expectEqual(expected.length, result.length);
|
testing.expectEqual(expected.length, result.length);
|
||||||
testing.expectEqual(expected, Array.from(result).map((e) => e.textContent));
|
testing.expectEqual(expected, Array.from(result).map((e) => e.textContent));
|
||||||
testing.expectEqual(expected, Array.from(result.values()).map((e) => e.textContent));
|
testing.expectEqual(expected, Array.from(result.values()).map((e) => e.textContent));
|
||||||
|
|
||||||
testing.expectEqual(expected.map((e, i) => i), Array.from(result.keys()));
|
testing.expectEqual(expected.map((e, i) => i), Array.from(result.keys()));
|
||||||
|
testing.expectEqual(expected.map((e, i) => i.toString()), Object.keys(result));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -440,3 +442,29 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=iterator_list_lifetime>
|
||||||
|
// This test is intended to ensure that a list remains alive as long as it
|
||||||
|
// must, i.e. as long as any iterator referencing the list is alive.
|
||||||
|
// This test depends on being able to force the v8 GC to cleanup, which
|
||||||
|
// we have no way of controlling. At worst, the test will pass without
|
||||||
|
// actually testing correct lifetime. But it was at least manually verified
|
||||||
|
// for me that this triggers plenty of GCs.
|
||||||
|
const expected = Array.from(document.querySelectorAll('*')).length;
|
||||||
|
{
|
||||||
|
let keys = [];
|
||||||
|
|
||||||
|
// Phase 1: Create many lists+iterators to fill up the arena pool
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
let list = document.querySelectorAll('*');
|
||||||
|
keys.push(list.keys());
|
||||||
|
|
||||||
|
// Create an Event every iteration to compete for arenas
|
||||||
|
new Event('arena_compete');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let k of keys) {
|
||||||
|
const result = Array.from(k);
|
||||||
|
testing.expectEqual(expected, result.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -154,3 +154,11 @@
|
|||||||
testing.expectEqual(true, typeof div.style.getPropertyPriority === 'function');
|
testing.expectEqual(true, typeof div.style.getPropertyPriority === 'function');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div id=crash1 style="background-position: 5% .1em"></div>
|
||||||
|
<script id="crash_case_1">
|
||||||
|
{
|
||||||
|
testing.expectEqual('5% .1em', $('#crash1').style.backgroundPosition);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -11,11 +11,11 @@
|
|||||||
<script id=empty_href>
|
<script id=empty_href>
|
||||||
testing.expectEqual('', $('#a0').href);
|
testing.expectEqual('', $('#a0').href);
|
||||||
|
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/anchor1.html', $('#a1').href);
|
testing.expectEqual(testing.BASE_URL + 'element/anchor1.html', $('#a1').href);
|
||||||
testing.expectEqual('http://127.0.0.1:9582/hello/world/anchor2.html', $('#a2').href);
|
testing.expectEqual(testing.ORIGIN + 'hello/world/anchor2.html', $('#a2').href);
|
||||||
testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href);
|
testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href);
|
||||||
|
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/foo', $('#link').href);
|
testing.expectEqual(testing.BASE_URL + 'element/html/foo', $('#link').href);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=dynamic_anchor_defaults>
|
<script id=dynamic_anchor_defaults>
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
|
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
|
||||||
|
|
||||||
link.href = 'foo';
|
link.href = 'foo';
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/foo', link.href);
|
testing.expectEqual(testing.BASE_URL + 'element/html/foo', link.href);
|
||||||
|
|
||||||
testing.expectEqual('', link.type);
|
testing.expectEqual('', link.type);
|
||||||
link.type = 'text/html';
|
link.type = 'text/html';
|
||||||
@@ -245,3 +245,11 @@
|
|||||||
testing.expectEqual('', b.toString());
|
testing.expectEqual('', b.toString());
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=url_encode>
|
||||||
|
{
|
||||||
|
let a = document.createElement('a');
|
||||||
|
a.href = 'over 9000!';
|
||||||
|
testing.expectEqual(testing.BASE_URL + 'element/html/over%209000!', a.href);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -143,6 +143,29 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="js_setter_null_clears_listener">
|
||||||
|
{
|
||||||
|
// Setting an event handler property to null must silently clear it (not throw).
|
||||||
|
// Browsers also accept undefined and non-function values without throwing.
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.onload = () => 42;
|
||||||
|
testing.expectEqual('function', typeof div.onload);
|
||||||
|
|
||||||
|
// Setting to null removes the listener; getter returns null
|
||||||
|
div.onload = null;
|
||||||
|
testing.expectEqual(null, div.onload);
|
||||||
|
|
||||||
|
div.onerror = () => {};
|
||||||
|
div.onerror = null;
|
||||||
|
testing.expectEqual(null, div.onerror);
|
||||||
|
|
||||||
|
div.onclick = () => {};
|
||||||
|
div.onclick = null;
|
||||||
|
testing.expectEqual(null, div.onclick);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id="different_event_types_independent">
|
<script id="different_event_types_independent">
|
||||||
{
|
{
|
||||||
// Test that different event types are stored independently
|
// Test that different event types are stored independently
|
||||||
|
|||||||
@@ -32,12 +32,12 @@
|
|||||||
|
|
||||||
img.src = 'test.png';
|
img.src = 'test.png';
|
||||||
// src property returns resolved absolute URL
|
// src property returns resolved absolute URL
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/test.png', img.src);
|
testing.expectEqual(testing.BASE_URL + 'element/html/test.png', img.src);
|
||||||
// getAttribute returns the raw attribute value
|
// getAttribute returns the raw attribute value
|
||||||
testing.expectEqual('test.png', img.getAttribute('src'));
|
testing.expectEqual('test.png', img.getAttribute('src'));
|
||||||
|
|
||||||
img.src = '/absolute/path.png';
|
img.src = '/absolute/path.png';
|
||||||
testing.expectEqual('http://127.0.0.1:9582/absolute/path.png', img.src);
|
testing.expectEqual(testing.ORIGIN + 'absolute/path.png', img.src);
|
||||||
testing.expectEqual('/absolute/path.png', img.getAttribute('src'));
|
testing.expectEqual('/absolute/path.png', img.getAttribute('src'));
|
||||||
|
|
||||||
img.src = 'https://example.com/image.png';
|
img.src = 'https://example.com/image.png';
|
||||||
@@ -114,48 +114,15 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id="load-trigger-event">
|
<body></body>
|
||||||
|
|
||||||
|
<script id="img-load-event">
|
||||||
{
|
{
|
||||||
|
// An img fires a load event when src is set.
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
let count = 0;
|
let result = false;
|
||||||
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
|
||||||
testing.expectEqual(true, count < 3);
|
|
||||||
count++;
|
|
||||||
|
|
||||||
testing.expectEqual(false, bubbles);
|
|
||||||
testing.expectEqual(false, cancelBubble);
|
|
||||||
testing.expectEqual(false, cancelable);
|
|
||||||
testing.expectEqual(false, composed);
|
|
||||||
testing.expectEqual(true, isTrusted);
|
|
||||||
testing.expectEqual(img, target);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
|
|
||||||
testing.expectEqual("https://cdn.lightpanda.io/website/assets/images/docs/hn.png", img.src);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure count is incremented asynchronously.
|
|
||||||
testing.expectEqual(0, count);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<img
|
|
||||||
id="inline-img"
|
|
||||||
src="https://cdn.lightpanda.io/website/assets/images/docs/hn.png"
|
|
||||||
onload="(() => testing.expectEqual(true, true))()"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<script id="inline-on-load">
|
|
||||||
{
|
|
||||||
const img = document.getElementById("inline-img");
|
|
||||||
testing.expectEqual(true, img.onload instanceof Function);
|
|
||||||
// Also call inline to double check.
|
|
||||||
img.onload();
|
|
||||||
|
|
||||||
// Make sure ones attached with `addEventListener` also executed.
|
|
||||||
testing.async(async () => {
|
testing.async(async () => {
|
||||||
const result = await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
||||||
testing.expectEqual(false, bubbles);
|
testing.expectEqual(false, bubbles);
|
||||||
testing.expectEqual(false, cancelBubble);
|
testing.expectEqual(false, cancelBubble);
|
||||||
@@ -163,12 +130,46 @@
|
|||||||
testing.expectEqual(false, composed);
|
testing.expectEqual(false, composed);
|
||||||
testing.expectEqual(true, isTrusted);
|
testing.expectEqual(true, isTrusted);
|
||||||
testing.expectEqual(img, target);
|
testing.expectEqual(img, target);
|
||||||
|
result = true;
|
||||||
return resolve(true);
|
return resolve();
|
||||||
});
|
});
|
||||||
|
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.expectEqual(true, result);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="img-no-load-without-src">
|
||||||
|
{
|
||||||
|
// An img without src should not fire a load event.
|
||||||
|
let fired = false;
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.addEventListener("load", () => { fired = true; });
|
||||||
|
document.body.appendChild(img);
|
||||||
|
testing.eventually(() => testing.expectEqual(false, fired));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="lazy-src-set">
|
||||||
|
{
|
||||||
|
// Append to DOM first, then set src — load should still fire.
|
||||||
|
const img = document.createElement("img");
|
||||||
|
let result = false;
|
||||||
|
img.onload = () => result = true;
|
||||||
|
document.body.appendChild(img);
|
||||||
|
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
|
||||||
|
|
||||||
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=url_encode>
|
||||||
|
{
|
||||||
|
let img = document.createElement('img');
|
||||||
|
img.src = 'over 9000!?hello=world !';
|
||||||
|
testing.expectEqual('over 9000!?hello=world !', img.getAttribute('src'));
|
||||||
|
testing.expectEqual(testing.BASE_URL + 'element/html/over%209000!?hello=world%20!', img.src);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -57,9 +57,9 @@
|
|||||||
|
|
||||||
testing.expectEqual('', input.src);
|
testing.expectEqual('', input.src);
|
||||||
input.src = 'foo'
|
input.src = 'foo'
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/foo', input.src);
|
testing.expectEqual(testing.BASE_URL + 'element/html/foo', input.src);
|
||||||
input.src = '-3'
|
input.src = '-3'
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/-3', input.src);
|
testing.expectEqual(testing.BASE_URL + 'element/html/-3', input.src);
|
||||||
input.src = ''
|
input.src = ''
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);
|
testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);
|
||||||
|
|
||||||
l2.href = '/over/9000';
|
l2.href = '/over/9000';
|
||||||
testing.expectEqual('http://127.0.0.1:9582/over/9000', l2.href);
|
testing.expectEqual(testing.ORIGIN + 'over/9000', l2.href);
|
||||||
|
|
||||||
l2.crossOrigin = 'nope';
|
l2.crossOrigin = 'nope';
|
||||||
testing.expectEqual('anonymous', l2.crossOrigin);
|
testing.expectEqual('anonymous', l2.crossOrigin);
|
||||||
@@ -19,3 +19,68 @@
|
|||||||
l2.crossOrigin = '';
|
l2.crossOrigin = '';
|
||||||
testing.expectEqual('anonymous', l2.crossOrigin);
|
testing.expectEqual('anonymous', l2.crossOrigin);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="link-load-event">
|
||||||
|
{
|
||||||
|
// A link with rel=stylesheet and a non-empty href fires a load event when appended to the DOM
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = 'https://lightpanda.io/opensource-browser/15';
|
||||||
|
|
||||||
|
testing.async(async () => {
|
||||||
|
const result = await new Promise(resolve => {
|
||||||
|
link.addEventListener('load', ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
||||||
|
testing.expectEqual(false, bubbles);
|
||||||
|
testing.expectEqual(false, cancelBubble);
|
||||||
|
testing.expectEqual(false, cancelable);
|
||||||
|
testing.expectEqual(false, composed);
|
||||||
|
testing.expectEqual(true, isTrusted);
|
||||||
|
testing.expectEqual(link, target);
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
document.head.appendChild(link);
|
||||||
|
});
|
||||||
|
testing.expectEqual(true, result);
|
||||||
|
});
|
||||||
|
testing.expectEqual(true, true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="link-no-load-without-href">
|
||||||
|
{
|
||||||
|
// A link with rel=stylesheet but no href should not fire a load event
|
||||||
|
let fired = false;
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.addEventListener('load', () => { fired = true; });
|
||||||
|
document.head.appendChild(link);
|
||||||
|
testing.eventually(() => testing.expectEqual(false, fired));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="link-no-load-wrong-rel">
|
||||||
|
{
|
||||||
|
// A link without rel=stylesheet should not fire a load event
|
||||||
|
let fired = false;
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.href = 'https://lightpanda.io/opensource-browser/15';
|
||||||
|
link.addEventListener('load', () => { fired = true; });
|
||||||
|
document.head.appendChild(link);
|
||||||
|
testing.eventually(() => testing.expectEqual(false, fired));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="lazy-href-set">
|
||||||
|
{
|
||||||
|
let result = false;
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.onload = () => result = true;
|
||||||
|
// Append to DOM,
|
||||||
|
document.head.appendChild(link);
|
||||||
|
// then set href.
|
||||||
|
link.href = 'https://lightpanda.io/opensource-browser/15';
|
||||||
|
|
||||||
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -238,7 +238,7 @@
|
|||||||
testing.expectEqual('', audio.src);
|
testing.expectEqual('', audio.src);
|
||||||
|
|
||||||
audio.src = 'test.mp3';
|
audio.src = 'test.mp3';
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/test.mp3', audio.src);
|
testing.expectEqual(testing.BASE_URL + 'element/html/test.mp3', audio.src);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@
|
|||||||
testing.expectEqual('', video.poster);
|
testing.expectEqual('', video.poster);
|
||||||
|
|
||||||
video.poster = 'poster.jpg';
|
video.poster = 'poster.jpg';
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/poster.jpg', video.poster);
|
testing.expectEqual(testing.BASE_URL + 'element/html/poster.jpg', video.poster);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,12 @@
|
|||||||
testing.expectEqual('Text 3', $('#opt3').text)
|
testing.expectEqual('Text 3', $('#opt3').text)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="text_set">
|
||||||
|
$('#opt1').text = 'New Text 1'
|
||||||
|
testing.expectEqual('New Text 1', $('#opt1').text)
|
||||||
|
testing.expectEqual('New Text 1', $('#opt1').textContent)
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id="selected">
|
<script id="selected">
|
||||||
testing.expectEqual(false, $('#opt1').selected)
|
testing.expectEqual(false, $('#opt1').selected)
|
||||||
testing.expectEqual(true, $('#opt2').selected)
|
testing.expectEqual(true, $('#opt2').selected)
|
||||||
|
|||||||
0
src/browser/tests/element/html/script/empty.js
Normal file
0
src/browser/tests/element/html/script/empty.js
Normal file
@@ -2,11 +2,28 @@
|
|||||||
<script src="../../../testing.js"></script>
|
<script src="../../../testing.js"></script>
|
||||||
|
|
||||||
<script id="script">
|
<script id="script">
|
||||||
{
|
{
|
||||||
let s = document.createElement('script');
|
let dom_load = false;
|
||||||
testing.expectEqual('', s.src);
|
let attribute_load = false;
|
||||||
|
|
||||||
s.src = '/over.9000.js';
|
let s = document.createElement('script');
|
||||||
testing.expectEqual('http://127.0.0.1:9582/over.9000.js', s.src);
|
document.documentElement.addEventListener('load', (e) => {
|
||||||
|
testing.expectEqual(s, e.target);
|
||||||
|
dom_load = true;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
testing.expectEqual('', s.src);
|
||||||
|
s.onload = function(e) {
|
||||||
|
testing.expectEqual(s, e.target);
|
||||||
|
attribute_load = true;
|
||||||
}
|
}
|
||||||
|
s.src = 'empty.js';
|
||||||
|
testing.expectEqual(testing.BASE_URL + 'element/html/script/empty.js', s.src);
|
||||||
|
document.head.appendChild(s);
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(true, dom_load);
|
||||||
|
testing.expectEqual(true, attribute_load);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -106,3 +106,28 @@
|
|||||||
testing.expectEqual(true, style.disabled);
|
testing.expectEqual(true, style.disabled);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="style-load-event">
|
||||||
|
{
|
||||||
|
// A style element fires a load event when appended to the DOM.
|
||||||
|
const style = document.createElement("style");
|
||||||
|
let result = false;
|
||||||
|
testing.async(async () => {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
style.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
||||||
|
testing.expectEqual(false, bubbles);
|
||||||
|
testing.expectEqual(false, cancelBubble);
|
||||||
|
testing.expectEqual(false, cancelable);
|
||||||
|
testing.expectEqual(false, composed);
|
||||||
|
testing.expectEqual(true, isTrusted);
|
||||||
|
testing.expectEqual(style, target);
|
||||||
|
result = true;
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
document.head.appendChild(style);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -45,7 +45,6 @@
|
|||||||
testing.expectEqual('hi', $('#link').innerText);
|
testing.expectEqual('hi', $('#link').innerText);
|
||||||
|
|
||||||
d1.innerHTML = '';
|
d1.innerHTML = '';
|
||||||
testing.todo(null, $('#link'));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=attributeSerialization>
|
<script id=attributeSerialization>
|
||||||
|
|||||||
@@ -103,3 +103,16 @@
|
|||||||
document.dispatchEvent(new KeyboardEvent('keytest', {key: 'b'}));
|
document.dispatchEvent(new KeyboardEvent('keytest', {key: 'b'}));
|
||||||
testing.expectEqual(false, keyIsTrusted);
|
testing.expectEqual(false, keyIsTrusted);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=non_keyboard_keydown>
|
||||||
|
// this used to crash
|
||||||
|
{
|
||||||
|
let called = false;
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.addEventListener('keydown', () => {
|
||||||
|
called = true;
|
||||||
|
});
|
||||||
|
div.dispatchEvent(new Event('keydown'));
|
||||||
|
testing.expectEqual(true, called);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
197
src/browser/tests/file_reader.html
Normal file
197
src/browser/tests/file_reader.html
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<script src="./testing.js"></script>
|
||||||
|
|
||||||
|
<script id=basic>
|
||||||
|
{
|
||||||
|
const reader = new FileReader();
|
||||||
|
testing.expectEqual(0, reader.readyState);
|
||||||
|
testing.expectEqual(null, reader.result);
|
||||||
|
testing.expectEqual(null, reader.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
testing.expectEqual(0, FileReader.EMPTY);
|
||||||
|
testing.expectEqual(1, FileReader.LOADING);
|
||||||
|
testing.expectEqual(2, FileReader.DONE);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readAsText>
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob = new Blob(["Hello, World!"], { type: "text/plain" });
|
||||||
|
|
||||||
|
let loadstartFired = false;
|
||||||
|
let progressFired = false;
|
||||||
|
let loadFired = false;
|
||||||
|
let loadendFired = false;
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
reader.onloadstart = function(e) {
|
||||||
|
loadstartFired = true;
|
||||||
|
testing.expectEqual("loadstart", e.type);
|
||||||
|
testing.expectEqual(1, reader.readyState);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onprogress = function(e) {
|
||||||
|
progressFired = true;
|
||||||
|
testing.expectEqual(13, e.loaded);
|
||||||
|
testing.expectEqual(13, e.total);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onload = function(e) {
|
||||||
|
loadFired = true;
|
||||||
|
testing.expectEqual(2, reader.readyState);
|
||||||
|
testing.expectEqual("Hello, World!", reader.result);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onloadend = function(e) {
|
||||||
|
loadendFired = true;
|
||||||
|
testing.expectEqual(true, loadstartFired);
|
||||||
|
testing.expectEqual(true, progressFired);
|
||||||
|
testing.expectEqual(true, loadFired);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsText(blob);
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readAsDataURL>
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob = new Blob(["test"], { type: "text/plain" });
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
testing.expectEqual("data:text/plain;base64,dGVzdA==", reader.result);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Empty MIME type
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob = new Blob(["test"]);
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
testing.expectEqual("data:application/octet-stream;base64,dGVzdA==", reader.result);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readAsArrayBuffer>
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob = new Blob([new Uint8Array([65, 66, 67])]);
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
const result = reader.result;
|
||||||
|
testing.expectEqual(true, result instanceof ArrayBuffer);
|
||||||
|
testing.expectEqual(3, result.byteLength);
|
||||||
|
|
||||||
|
const view = new Uint8Array(result);
|
||||||
|
testing.expectEqual(65, view[0]);
|
||||||
|
testing.expectEqual(66, view[1]);
|
||||||
|
testing.expectEqual(67, view[2]);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(blob);
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readAsBinaryString>
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob = new Blob(["ABC"]);
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
testing.expectEqual("ABC", reader.result);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsBinaryString(blob);
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=abort>
|
||||||
|
// Test aborting when not loading (should do nothing)
|
||||||
|
{
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.abort(); // Should not throw
|
||||||
|
testing.expectEqual(0, reader.readyState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Testing abort during read is implementation-dependent.
|
||||||
|
// In synchronous implementations (like ours), the read completes before abort can be called.
|
||||||
|
// In async implementations (like Firefox), you can abort during the read.
|
||||||
|
// We test that abort() at least doesn't throw and maintains correct state.
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=multipleReads>
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob1 = new Blob(["first"]);
|
||||||
|
const blob2 = new Blob(["second"]);
|
||||||
|
|
||||||
|
// First read
|
||||||
|
const promise1 = new Promise((resolve) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
testing.expectEqual("first", reader.result);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
reader.readAsText(blob1);
|
||||||
|
await promise1;
|
||||||
|
|
||||||
|
// Second read - should work after first completes
|
||||||
|
const promise2 = new Promise((resolve) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
testing.expectEqual("second", reader.result);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
reader.readAsText(blob2);
|
||||||
|
await promise2;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=addEventListener>
|
||||||
|
testing.async(async () => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const blob = new Blob(["test"]);
|
||||||
|
|
||||||
|
let loadFired = false;
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
reader.addEventListener("load", function() {
|
||||||
|
loadFired = true;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsText(blob);
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
testing.expectEqual(true, loadFired);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<iframe id=f1 onload="frame1Onload" src="support/sub1.html"></iframe>
|
<iframe id=f1 onload="frame1Onload" src="support/sub 1.html"></iframe>
|
||||||
<iframe id=f2 src="support/sub2.html"></iframe>
|
<iframe id=f2 src="support/sub2.html"></iframe>
|
||||||
|
|
||||||
<script id="basic">
|
<script id="basic">
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
testing.expectEqual(0, $('#f1').childNodes.length);
|
testing.expectEqual(0, $('#f1').childNodes.length);
|
||||||
|
|
||||||
|
testing.expectEqual(testing.BASE_URL + 'frames/support/sub%201.html', $('#f1').src);
|
||||||
testing.expectEqual(window[0], $('#f1').contentWindow);
|
testing.expectEqual(window[0], $('#f1').contentWindow);
|
||||||
testing.expectEqual(window[1], $('#f2').contentWindow);
|
testing.expectEqual(window[1], $('#f2').contentWindow);
|
||||||
|
|
||||||
|
|||||||
@@ -19,3 +19,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=datauri src="data:text/plain;charset=utf-8;base64,dGVzdGluZy5leHBlY3RFcXVhbCh0cnVlLCB0cnVlKTs="></script>
|
<script id=datauri src="data:text/plain;charset=utf-8;base64,dGVzdGluZy5leHBlY3RFcXVhbCh0cnVlLCB0cnVlKTs="></script>
|
||||||
|
|
||||||
|
<script id=datauri_url_encoded_text src="data:text/javascript,testing.expectEqual(3, 3);"></script>
|
||||||
|
|
||||||
|
<script id=datauri_encoded_padding src="data:text/javascript;base64,dGVzdGluZy5leHBlY3RFcXVhbCgxLCAxKTs%3D"></script>
|
||||||
|
|
||||||
|
<script id=datauri_fully_encoded src="data:text/javascript;base64,%64%47%56%7a%64%47%6c%75%5a%79%35%6c%65%48%42%6c%59%33%52%46%63%58%56%68%62%43%67%79%4c%43%41%79%4b%54%73%3d"></script>
|
||||||
|
|
||||||
|
<script id=datauri_with_whitespace src="data:text/javascript;base64,%20ZD%20Qg%0D%0APS%20An%20Zm91cic%0D%0A%207%20"></script>
|
||||||
|
|
||||||
|
<script id=datauri_url_encoded_unicode src="data:text/javascript,testing.expectEqual(4%2C%204)%3B"></script>
|
||||||
|
|||||||
@@ -180,6 +180,13 @@
|
|||||||
|
|
||||||
testing.expectEqual(true, response.body !== null);
|
testing.expectEqual(true, response.body !== null);
|
||||||
testing.expectEqual(true, response.body instanceof ReadableStream);
|
testing.expectEqual(true, response.body instanceof ReadableStream);
|
||||||
|
|
||||||
|
const buf = await response.arrayBuffer()
|
||||||
|
restore();
|
||||||
|
|
||||||
|
const uint8array = new Uint8Array(buf);
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
testing.expectEqual('1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890', decoder.decode(uint8array));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
<script id=legacy>
|
<script id=legacy>
|
||||||
{
|
{
|
||||||
let request = new Request("flower.png");
|
let request = new Request("flower.png");
|
||||||
testing.expectEqual("http://127.0.0.1:9582/src/browser/tests/net/flower.png", request.url);
|
testing.expectEqual(testing.BASE_URL + 'net/flower.png', request.url);
|
||||||
testing.expectEqual("GET", request.method);
|
testing.expectEqual("GET", request.method);
|
||||||
|
|
||||||
let request2 = new Request("https://google.com", {
|
let request2 = new Request("https://google.com", {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<a href="foo" id="foo">foo</a>
|
<a href="foo" id="foo">foo</a>
|
||||||
|
|
||||||
<script id=baseURI>
|
<script id=baseURI>
|
||||||
testing.expectEqual("http://127.0.0.1:9582/src/browser/tests/node/base_uri.html", document.URL);
|
testing.expectEqual(testing.BASE_URL + 'node/base_uri.html', document.URL);
|
||||||
testing.expectEqual("https://example.com/", document.baseURI);
|
testing.expectEqual("https://example.com/", document.baseURI);
|
||||||
|
|
||||||
const link = $('#foo');
|
const link = $('#foo');
|
||||||
|
|||||||
24
src/browser/tests/node/noscript_serialization.html
Normal file
24
src/browser/tests/node/noscript_serialization.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<!-- When scripting is enabled the HTML parser treats <noscript> as a raw text
|
||||||
|
element: its entire content is stored as a single text node containing the
|
||||||
|
raw markup. Serializing it back (outerHTML / innerHTML) must output that
|
||||||
|
markup as-is without HTML-escaping the angle brackets. -->
|
||||||
|
<noscript id="ns1"><h1>Hello</h1><p>World</p></noscript>
|
||||||
|
<noscript id="ns2"><div id="bsky_post_summary"><h3>Post</h3><p id="bsky_display_name">Henri Helvetica</p></div></noscript>
|
||||||
|
|
||||||
|
<script id="noscript-outerHTML">
|
||||||
|
const ns1 = document.getElementById('ns1');
|
||||||
|
testing.expectEqual('<noscript id="ns1"><h1>Hello</h1><p>World</p></noscript>', ns1.outerHTML);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="noscript-innerHTML">
|
||||||
|
const ns2 = document.getElementById('ns2');
|
||||||
|
testing.expectEqual('<div id="bsky_post_summary"><h3>Post</h3><p id="bsky_display_name">Henri Helvetica</p></div>', ns2.innerHTML);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="noscript-textContent">
|
||||||
|
// The raw text node content must be the literal HTML markup (not parsed DOM)
|
||||||
|
testing.expectEqual('<h1>Hello</h1><p>World</p>', ns1.firstChild.nodeValue);
|
||||||
|
</script>
|
||||||
@@ -252,6 +252,72 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=measure_with_navigation_timing_marks>
|
||||||
|
{
|
||||||
|
// performance.measure() must accept PerformanceTiming attribute names
|
||||||
|
// (e.g. "fetchStart") as start/end marks per the W3C User Timing Level 2 spec.
|
||||||
|
// https://www.w3.org/TR/user-timing/#dom-performance-measure
|
||||||
|
performance.clearMarks();
|
||||||
|
performance.clearMeasures();
|
||||||
|
|
||||||
|
performance.mark("mark-dataComplete");
|
||||||
|
let m = performance.measure("dataComplete", "fetchStart", "mark-dataComplete");
|
||||||
|
testing.expectEqual('dataComplete', m.name);
|
||||||
|
testing.expectEqual('measure', m.entryType);
|
||||||
|
testing.expectEqual(true, m.duration >= 0);
|
||||||
|
|
||||||
|
// navigationStart is also a valid mark name (was already supported)
|
||||||
|
performance.mark("mark-end");
|
||||||
|
let m2 = performance.measure("fromNav", "navigationStart", "mark-end");
|
||||||
|
testing.expectEqual('fromNav', m2.name);
|
||||||
|
testing.expectEqual(true, m2.duration >= 0);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=performance_timing_exists>
|
||||||
|
{
|
||||||
|
// performance.timing must not be undefined (used by sites like Bing)
|
||||||
|
testing.expectEqual(true, performance.timing !== undefined);
|
||||||
|
testing.expectEqual(true, performance.timing !== null);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=performance_timing_navigationStart>
|
||||||
|
{
|
||||||
|
// navigationStart must be a number (sites access performance.timing.navigationStart)
|
||||||
|
const timing = performance.timing;
|
||||||
|
testing.expectEqual('number', typeof timing.navigationStart);
|
||||||
|
testing.expectEqual(0, timing.navigationStart);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=performance_timing_all_properties>
|
||||||
|
{
|
||||||
|
// All PerformanceTiming properties must be accessible and return numbers
|
||||||
|
const timing = performance.timing;
|
||||||
|
const props = [
|
||||||
|
'navigationStart', 'unloadEventStart', 'unloadEventEnd',
|
||||||
|
'redirectStart', 'redirectEnd', 'fetchStart',
|
||||||
|
'domainLookupStart', 'domainLookupEnd',
|
||||||
|
'connectStart', 'connectEnd', 'secureConnectionStart',
|
||||||
|
'requestStart', 'responseStart', 'responseEnd',
|
||||||
|
'domLoading', 'domInteractive',
|
||||||
|
'domContentLoadedEventStart', 'domContentLoadedEventEnd',
|
||||||
|
'domComplete', 'loadEventStart', 'loadEventEnd',
|
||||||
|
];
|
||||||
|
for (const prop of props) {
|
||||||
|
testing.expectEqual('number', typeof timing[prop]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=performance_timing_same_object>
|
||||||
|
{
|
||||||
|
// performance.timing should return the same object on each access
|
||||||
|
testing.expectEqual(true, performance.timing === performance.timing);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id=mixed_marks_and_measures>
|
<script id=mixed_marks_and_measures>
|
||||||
{
|
{
|
||||||
performance.clearMarks();
|
performance.clearMarks();
|
||||||
@@ -280,3 +346,50 @@
|
|||||||
testing.expectEqual(0, remainingMarks.length);
|
testing.expectEqual(0, remainingMarks.length);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=performance_timing_exists>
|
||||||
|
{
|
||||||
|
// Navigation Timing Level 1: performance.timing must be an object, not undefined
|
||||||
|
testing.expectEqual('object', typeof performance.timing);
|
||||||
|
testing.expectEqual(false, performance.timing === null);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=performance_timing_navigationStart>
|
||||||
|
{
|
||||||
|
// The most commonly used property — must be a number (not undefined)
|
||||||
|
testing.expectEqual('number', typeof performance.timing.navigationStart);
|
||||||
|
testing.expectEqual(0, performance.timing.navigationStart);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=performance_navigation_exists>
|
||||||
|
{
|
||||||
|
// Navigation Timing Level 1: performance.navigation must be an object, not undefined
|
||||||
|
testing.expectEqual('object', typeof performance.navigation);
|
||||||
|
testing.expectEqual(false, performance.navigation === null);
|
||||||
|
testing.expectEqual('number', typeof performance.navigation.type);
|
||||||
|
testing.expectEqual('number', typeof performance.navigation.redirectCount);
|
||||||
|
testing.expectEqual(0, performance.navigation.type);
|
||||||
|
testing.expectEqual(0, performance.navigation.redirectCount);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=performance_timing_all_properties>
|
||||||
|
{
|
||||||
|
const t = performance.timing;
|
||||||
|
const props = [
|
||||||
|
'navigationStart', 'unloadEventStart', 'unloadEventEnd',
|
||||||
|
'redirectStart', 'redirectEnd', 'fetchStart',
|
||||||
|
'domainLookupStart', 'domainLookupEnd',
|
||||||
|
'connectStart', 'connectEnd', 'secureConnectionStart',
|
||||||
|
'requestStart', 'responseStart', 'responseEnd',
|
||||||
|
'domLoading', 'domInteractive',
|
||||||
|
'domContentLoadedEventStart', 'domContentLoadedEventEnd',
|
||||||
|
'domComplete', 'loadEventStart', 'loadEventEnd',
|
||||||
|
];
|
||||||
|
for (const prop of props) {
|
||||||
|
testing.expectEqual('number', typeof t[prop]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -546,14 +546,14 @@
|
|||||||
{
|
{
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
|
|
||||||
let eventCount = 0;
|
let eventCount = 0;
|
||||||
let lastEvent = null;
|
let lastEvent = null;
|
||||||
|
|
||||||
document.addEventListener('selectionchange', (e) => {
|
const listener = (e) => {
|
||||||
eventCount++;
|
eventCount++;
|
||||||
lastEvent = e;
|
lastEvent = e;
|
||||||
});
|
};
|
||||||
|
document.addEventListener('selectionchange', listener);
|
||||||
|
|
||||||
const p1 = document.getElementById("p1");
|
const p1 = document.getElementById("p1");
|
||||||
const textNode = p1.firstChild;
|
const textNode = p1.firstChild;
|
||||||
@@ -563,27 +563,25 @@
|
|||||||
sel.extend(textNode, 10);
|
sel.extend(textNode, 10);
|
||||||
sel.collapseToStart();
|
sel.collapseToStart();
|
||||||
sel.collapseToEnd();
|
sel.collapseToEnd();
|
||||||
|
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.setStart(textNode, 4);
|
range.setStart(textNode, 4);
|
||||||
range.setEnd(textNode, 15);
|
range.setEnd(textNode, 15);
|
||||||
sel.addRange(range);
|
sel.addRange(range);
|
||||||
|
|
||||||
sel.removeRange(range);
|
sel.removeRange(range);
|
||||||
|
|
||||||
const newRange = document.createRange();
|
const newRange = document.createRange();
|
||||||
newRange.selectNodeContents(p1);
|
newRange.selectNodeContents(p1);
|
||||||
sel.addRange(newRange);
|
sel.addRange(newRange);
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
|
|
||||||
sel.selectAllChildren(nested);
|
sel.selectAllChildren(nested);
|
||||||
sel.setBaseAndExtent(textNode, 4, textNode, 15);
|
sel.setBaseAndExtent(textNode, 4, textNode, 15);
|
||||||
|
|
||||||
sel.collapse(textNode, 5);
|
sel.collapse(textNode, 5);
|
||||||
sel.extend(textNode, 10);
|
sel.extend(textNode, 10);
|
||||||
sel.deleteFromDocument();
|
sel.deleteFromDocument();
|
||||||
|
|
||||||
|
document.removeEventListener('selectionchange', listener);
|
||||||
|
textNode.textContent = "The quick brown fox";
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.eventually(() => {
|
||||||
testing.expectEqual(14, eventCount);
|
testing.expectEqual(14, eventCount);
|
||||||
testing.expectEqual('selectionchange', lastEvent.type);
|
testing.expectEqual('selectionchange', lastEvent.type);
|
||||||
@@ -593,3 +591,149 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=modifyCharacterForward>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const textNode = p1.firstChild; // "The quick brown fox"
|
||||||
|
|
||||||
|
// Collapse to position 4 (after "The ")
|
||||||
|
sel.collapse(textNode, 4);
|
||||||
|
testing.expectEqual(4, sel.anchorOffset);
|
||||||
|
|
||||||
|
// Move forward one character
|
||||||
|
sel.modify("move", "forward", "character");
|
||||||
|
testing.expectEqual(5, sel.anchorOffset);
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
testing.expectEqual("none", sel.direction);
|
||||||
|
|
||||||
|
// Move forward again
|
||||||
|
sel.modify("move", "forward", "character");
|
||||||
|
testing.expectEqual(6, sel.anchorOffset);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=modifyWordForward>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const textNode = p1.firstChild; // "The quick brown fox"
|
||||||
|
|
||||||
|
// Collapse to start
|
||||||
|
sel.collapse(textNode, 0);
|
||||||
|
|
||||||
|
// Move forward one word - should land at end of "The"
|
||||||
|
sel.modify("move", "forward", "word");
|
||||||
|
testing.expectEqual(3, sel.anchorOffset);
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
|
||||||
|
// Move forward again - should skip space and land at end of "quick"
|
||||||
|
sel.modify("move", "forward", "word");
|
||||||
|
testing.expectEqual(9, sel.anchorOffset);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=modifyCharacterBackward>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const textNode = p1.firstChild; // "The quick brown fox"
|
||||||
|
|
||||||
|
// Collapse to position 6
|
||||||
|
sel.collapse(textNode, 6);
|
||||||
|
testing.expectEqual(6, sel.anchorOffset);
|
||||||
|
|
||||||
|
// Move backward one character
|
||||||
|
sel.modify("move", "backward", "character");
|
||||||
|
testing.expectEqual(5, sel.anchorOffset);
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
testing.expectEqual("none", sel.direction);
|
||||||
|
|
||||||
|
// Move backward again
|
||||||
|
sel.modify("move", "backward", "character");
|
||||||
|
testing.expectEqual(4, sel.anchorOffset);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=modifyWordBackward>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
const textNode = p1.firstChild; // "The quick brown fox"
|
||||||
|
|
||||||
|
// Collapse to end of "quick" (offset 9)
|
||||||
|
sel.collapse(textNode, 9);
|
||||||
|
|
||||||
|
// Move backward one word - should land at start of "quick"
|
||||||
|
sel.modify("move", "backward", "word");
|
||||||
|
testing.expectEqual(4, sel.anchorOffset);
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
|
||||||
|
// Move backward again - should land at start of "The"
|
||||||
|
sel.modify("move", "backward", "word");
|
||||||
|
testing.expectEqual(0, sel.anchorOffset);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=modifyCharacterForwardFromElementNode>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
sel.collapse(p1, 1);
|
||||||
|
|
||||||
|
testing.expectEqual(p1, sel.anchorNode);
|
||||||
|
testing.expectEqual(1, sel.anchorOffset);
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
|
||||||
|
sel.modify("move", "forward", "character");
|
||||||
|
|
||||||
|
testing.expectEqual(3, sel.anchorNode.nodeType);
|
||||||
|
testing.expectEqual(true, sel.anchorNode !== p1.firstChild);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=modifyCharacterForwardFromElementNodeMidChildren>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const nested = document.getElementById("nested");
|
||||||
|
|
||||||
|
sel.collapse(nested, nested.childNodes.length);
|
||||||
|
|
||||||
|
testing.expectEqual(nested, sel.anchorNode);
|
||||||
|
testing.expectEqual(nested.childNodes.length, sel.anchorOffset);
|
||||||
|
|
||||||
|
sel.modify("move", "forward", "character");
|
||||||
|
|
||||||
|
// Must land on a text node strictly after #nested
|
||||||
|
testing.expectEqual(3, sel.anchorNode.nodeType);
|
||||||
|
testing.expectEqual(false, nested.contains(sel.anchorNode));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=modifyWordForwardFromElementNode>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
sel.collapse(p1, 1);
|
||||||
|
|
||||||
|
sel.modify("move", "forward", "word");
|
||||||
|
|
||||||
|
// Must land on a text node strictly after p1
|
||||||
|
testing.expectEqual(3, sel.anchorNode.nodeType);
|
||||||
|
testing.expectEqual(false, p1.contains(sel.anchorNode));
|
||||||
|
testing.expectEqual(true, sel.isCollapsed);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=modifyCharacterForwardNewNodeOffsetNotElement>
|
||||||
|
{
|
||||||
|
const sel = window.getSelection();
|
||||||
|
const p1 = document.getElementById("p1");
|
||||||
|
|
||||||
|
sel.collapse(p1, 1);
|
||||||
|
sel.modify("move", "forward", "character");
|
||||||
|
|
||||||
|
testing.expectEqual(3, sel.anchorNode.nodeType);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -88,3 +88,12 @@
|
|||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
testing.expectEqual(0, localStorage.length)
|
testing.expectEqual(0, localStorage.length)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="localstorage_limits">
|
||||||
|
localStorage.clear();
|
||||||
|
for (i = 0; i < 5; i++) {
|
||||||
|
const v = "v".repeat(1024 * 1024);
|
||||||
|
localStorage.setItem(v, v);
|
||||||
|
}
|
||||||
|
testing.expectError("QuotaExceededError", () => localStorage.setItem("last", "v"));
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -99,6 +99,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// our test runner sets this to true
|
||||||
|
const IS_TEST_RUNNER = window._lightpanda_skip_auto_assert === true;
|
||||||
|
|
||||||
window.testing = {
|
window.testing = {
|
||||||
fail: fail,
|
fail: fail,
|
||||||
async: async,
|
async: async,
|
||||||
@@ -109,9 +112,24 @@
|
|||||||
expectError: expectError,
|
expectError: expectError,
|
||||||
withError: withError,
|
withError: withError,
|
||||||
eventually: eventually,
|
eventually: eventually,
|
||||||
todo: function(){},
|
IS_TEST_RUNNER: IS_TEST_RUNNER,
|
||||||
|
HOST: '127.0.0.1',
|
||||||
|
ORIGIN: 'http://127.0.0.1:9582/',
|
||||||
|
BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!IS_TEST_RUNNER) {
|
||||||
|
// The page is running in a different browser. Probably a developer making sure
|
||||||
|
// a test is correct. There are a few tweaks we need to do to make this a
|
||||||
|
// seemless, namely around adapting paths/urls.
|
||||||
|
console.warn(`The page is not being executed in the test runner, certain behavior has been adjusted`);
|
||||||
|
window.testing.HOST = location.hostname;
|
||||||
|
window.testing.ORIGIN = location.origin + '/';
|
||||||
|
window.testing.BASE_URL = location.origin + '/src/browser/tests/';
|
||||||
|
window.addEventListener('load', testing.assertOk);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
window.$ = function(sel) {
|
window.$ = function(sel) {
|
||||||
return document.querySelector(sel);
|
return document.querySelector(sel);
|
||||||
}
|
}
|
||||||
@@ -220,8 +238,4 @@
|
|||||||
return val;
|
return val;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window._lightpanda_skip_auto_assert !== true) {
|
|
||||||
window.addEventListener('load', testing.assertOk);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -2,24 +2,24 @@
|
|||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
<script id=location>
|
<script id=location>
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/window/location.html', window.location.href);
|
testing.expectEqual(testing.BASE_URL + 'window/location.html', window.location.href);
|
||||||
testing.expectEqual(document.location, window.location);
|
testing.expectEqual(document.location, window.location);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=location_hash>
|
<script id=location_hash>
|
||||||
location.hash = "";
|
location.hash = "";
|
||||||
testing.expectEqual("", location.hash);
|
testing.expectEqual("", location.hash);
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/window/location.html', location.href);
|
testing.expectEqual(testing.BASE_URL + 'window/location.html', location.href);
|
||||||
|
|
||||||
location.hash = "#abcdef";
|
location.hash = "#abcdef";
|
||||||
testing.expectEqual("#abcdef", location.hash);
|
testing.expectEqual("#abcdef", location.hash);
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/window/location.html#abcdef', location.href);
|
testing.expectEqual(testing.BASE_URL + 'window/location.html#abcdef', location.href);
|
||||||
|
|
||||||
location.hash = "xyzxyz";
|
location.hash = "xyzxyz";
|
||||||
testing.expectEqual("#xyzxyz", location.hash);
|
testing.expectEqual("#xyzxyz", location.hash);
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/window/location.html#xyzxyz', location.href);
|
testing.expectEqual(testing.BASE_URL + 'window/location.html#xyzxyz', location.href);
|
||||||
|
|
||||||
location.hash = "";
|
location.hash = "";
|
||||||
testing.expectEqual("", location.hash);
|
testing.expectEqual("", location.hash);
|
||||||
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/window/location.html', location.href);
|
testing.expectEqual(testing.BASE_URL + 'window/location.html', location.href);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
|
|
||||||
<script id=screen>
|
<script id=screen>
|
||||||
let screen = window.screen;
|
let screen = window.screen;
|
||||||
testing.expectEqual(1920, screen.width);
|
if (testing.IS_TEST_RUNNER) {
|
||||||
testing.expectEqual(1080, screen.height);
|
testing.expectEqual(1920, screen.width);
|
||||||
|
testing.expectEqual(1080, screen.height);
|
||||||
|
}
|
||||||
|
|
||||||
let orientation = screen.orientation;
|
let orientation = screen.orientation;
|
||||||
testing.expectEqual(0, orientation.angle);
|
testing.expectEqual(0, orientation.angle);
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<script id=setTimeout>
|
<script id=setTimeout>
|
||||||
testing.expectEqual(1, window.setTimeout.length);
|
testing.expectEqual(1, window.setTimeout.length);
|
||||||
let wst2 = 1;
|
let wst2 = 1;
|
||||||
@@ -32,3 +31,11 @@
|
|||||||
}, 1, 2, 3);
|
}, 1, 2, 3);
|
||||||
testing.eventually(() => testing.expectEqual(5, wst2));
|
testing.eventually(() => testing.expectEqual(5, wst2));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=invalid-timer-clear>
|
||||||
|
// Surprisingly, these don't fail but silently ignored.
|
||||||
|
clearTimeout(-1);
|
||||||
|
clearInterval(-2);
|
||||||
|
clearImmediate(-3);
|
||||||
|
testing.expectEqual(true, true);
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -75,6 +75,16 @@
|
|||||||
testing.expectEqual('abc', atob('YWJj'));
|
testing.expectEqual('abc', atob('YWJj'));
|
||||||
testing.expectEqual('0123456789', atob('MDEyMzQ1Njc4OQ=='));
|
testing.expectEqual('0123456789', atob('MDEyMzQ1Njc4OQ=='));
|
||||||
testing.expectEqual('The quick brown fox jumps over the lazy dog', atob('VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw=='));
|
testing.expectEqual('The quick brown fox jumps over the lazy dog', atob('VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw=='));
|
||||||
|
|
||||||
|
// atob must accept unpadded base64 (forgiving-base64 decode per HTML spec)
|
||||||
|
testing.expectEqual('a', atob('YQ')); // 2 chars, len%4==2, needs '=='
|
||||||
|
testing.expectEqual('ab', atob('YWI')); // 3 chars, len%4==3, needs '='
|
||||||
|
testing.expectEqual('ceil', atob('Y2VpbA')); // 6 chars, len%4==2, needs '=='
|
||||||
|
|
||||||
|
// length % 4 == 1 must still throw
|
||||||
|
testing.expectError('Error: InvalidCharacterError', () => {
|
||||||
|
atob('Y');
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=btoa_atob_roundtrip>
|
<script id=btoa_atob_roundtrip>
|
||||||
|
|||||||
36
src/browser/tests/window_scroll.html
Normal file
36
src/browser/tests/window_scroll.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="testing.js"></script>
|
||||||
|
|
||||||
|
<script id=scrollBy_exists>
|
||||||
|
testing.expectEqual('function', typeof window.scrollBy);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=scrollBy_xy>
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
testing.expectEqual(0, window.scrollX);
|
||||||
|
testing.expectEqual(0, window.scrollY);
|
||||||
|
window.scrollBy(100, 200);
|
||||||
|
testing.expectEqual(100, window.scrollX);
|
||||||
|
testing.expectEqual(200, window.scrollY);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=scrollBy_relative>
|
||||||
|
window.scrollTo(100, 100);
|
||||||
|
window.scrollBy(50, 50);
|
||||||
|
testing.expectEqual(150, window.scrollX);
|
||||||
|
testing.expectEqual(150, window.scrollY);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=scrollBy_opts>
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
window.scrollBy({ left: 30, top: 40 });
|
||||||
|
testing.expectEqual(30, window.scrollX);
|
||||||
|
testing.expectEqual(40, window.scrollY);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=scrollBy_negative_clamp>
|
||||||
|
window.scrollTo(10, 10);
|
||||||
|
window.scrollBy(-100, -100);
|
||||||
|
testing.expectEqual(0, window.scrollX);
|
||||||
|
testing.expectEqual(0, window.scrollY);
|
||||||
|
</script>
|
||||||
@@ -129,3 +129,14 @@
|
|||||||
testing.expectEqual(original, serialized);
|
testing.expectEqual(original, serialized);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=serializeAttribute>
|
||||||
|
{
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.setAttribute('over', '9000');
|
||||||
|
|
||||||
|
const serializer = new XMLSerializer();
|
||||||
|
const serialized = serializer.serializeToString(div.getAttributeNode('over'));
|
||||||
|
testing.expectEqual('', serialized);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -77,8 +77,6 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, local: *const js.Local, page:
|
|||||||
|
|
||||||
// Dispatch abort event
|
// Dispatch abort event
|
||||||
const event = try Event.initTrusted(comptime .wrap("abort"), .{}, page);
|
const event = try Event.initTrusted(comptime .wrap("abort"), .{}, page);
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
|
|
||||||
try page._event_manager.dispatchWithFunction(
|
try page._event_manager.dispatchWithFunction(
|
||||||
self.asEventTarget(),
|
self.asEventTarget(),
|
||||||
event,
|
event,
|
||||||
|
|||||||
@@ -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 String = @import("../../string.zig").String;
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
@@ -31,7 +32,7 @@ const CData = @This();
|
|||||||
|
|
||||||
_type: Type,
|
_type: Type,
|
||||||
_proto: *Node,
|
_proto: *Node,
|
||||||
_data: []const u8 = "",
|
_data: String = .empty,
|
||||||
|
|
||||||
/// Count UTF-16 code units in a UTF-8 string.
|
/// Count UTF-16 code units in a UTF-8 string.
|
||||||
/// 4-byte UTF-8 sequences (codepoints >= U+10000) produce 2 UTF-16 code units (surrogate pair),
|
/// 4-byte UTF-8 sequences (codepoints >= U+10000) produce 2 UTF-16 code units (surrogate pair),
|
||||||
@@ -157,7 +158,7 @@ pub fn is(self: *CData, comptime T: type) ?*T {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getData(self: *const CData) []const u8 {
|
pub fn getData(self: *const CData) String {
|
||||||
return self._data;
|
return self._data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +173,7 @@ pub fn render(self: *const CData, writer: *std.io.Writer, opts: RenderOpts) !boo
|
|||||||
var start: usize = 0;
|
var start: usize = 0;
|
||||||
var prev_w: ?bool = null;
|
var prev_w: ?bool = null;
|
||||||
var is_w: bool = undefined;
|
var is_w: bool = undefined;
|
||||||
const s = self._data;
|
const s = self._data.str();
|
||||||
|
|
||||||
for (s, 0..) |c, i| {
|
for (s, 0..) |c, i| {
|
||||||
is_w = std.ascii.isWhitespace(c);
|
is_w = std.ascii.isWhitespace(c);
|
||||||
@@ -222,9 +223,9 @@ pub fn setData(self: *CData, value: ?[]const u8, page: *Page) !void {
|
|||||||
const old_value = self._data;
|
const old_value = self._data;
|
||||||
|
|
||||||
if (value) |v| {
|
if (value) |v| {
|
||||||
self._data = try page.dupeString(v);
|
self._data = try page.dupeSSO(v);
|
||||||
} else {
|
} else {
|
||||||
self._data = "";
|
self._data = .empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
page.characterDataChange(self.asNode(), old_value);
|
page.characterDataChange(self.asNode(), old_value);
|
||||||
@@ -243,15 +244,15 @@ pub fn _setData(self: *CData, value: js.Value, page: *Page) !void {
|
|||||||
|
|
||||||
pub fn format(self: *const CData, writer: *std.io.Writer) !void {
|
pub fn format(self: *const CData, writer: *std.io.Writer) !void {
|
||||||
return switch (self._type) {
|
return switch (self._type) {
|
||||||
.text => writer.print("<text>{s}</text>", .{self._data}),
|
.text => writer.print("<text>{f}</text>", .{self._data}),
|
||||||
.comment => writer.print("<!-- {s} -->", .{self._data}),
|
.comment => writer.print("<!-- {f} -->", .{self._data}),
|
||||||
.cdata_section => writer.print("<![CDATA[{s}]]>", .{self._data}),
|
.cdata_section => writer.print("<![CDATA[{f}]]>", .{self._data}),
|
||||||
.processing_instruction => |pi| writer.print("<?{s} {s}?>", .{ pi._target, self._data }),
|
.processing_instruction => |pi| writer.print("<?{s} {f}?>", .{ pi._target, self._data }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getLength(self: *const CData) usize {
|
pub fn getLength(self: *const CData) usize {
|
||||||
return utf16Len(self._data);
|
return utf16Len(self._data.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isEqualNode(self: *const CData, other: *const CData) bool {
|
pub fn isEqualNode(self: *const CData, other: *const CData) bool {
|
||||||
@@ -267,58 +268,64 @@ pub fn isEqualNode(self: *const CData, other: *const CData) bool {
|
|||||||
// if the _targets are equal, we still want to compare the data
|
// if the _targets are equal, we still want to compare the data
|
||||||
}
|
}
|
||||||
|
|
||||||
return std.mem.eql(u8, self.getData(), other.getData());
|
return self._data.eql(other._data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn appendData(self: *CData, data: []const u8, page: *Page) !void {
|
pub fn appendData(self: *CData, data: []const u8, page: *Page) !void {
|
||||||
const new_data = try std.mem.concat(page.arena, u8, &.{ self._data, data });
|
const old_value = self._data;
|
||||||
try self.setData(new_data, page);
|
self._data = try String.concat(page.arena, &.{ self._data.str(), data });
|
||||||
|
page.characterDataChange(self.asNode(), old_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deleteData(self: *CData, offset: usize, count: usize, page: *Page) !void {
|
pub fn deleteData(self: *CData, offset: usize, count: usize, page: *Page) !void {
|
||||||
const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);
|
const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);
|
||||||
const range = try utf16RangeToUtf8(self._data, offset, end_utf16);
|
const range = try utf16RangeToUtf8(self._data.str(), offset, end_utf16);
|
||||||
|
|
||||||
// Just slice - original data stays in arena
|
const old_data = self._data;
|
||||||
const old_value = self._data;
|
const old_value = old_data.str();
|
||||||
if (range.start == 0) {
|
if (range.start == 0) {
|
||||||
self._data = self._data[range.end..];
|
self._data = try page.dupeSSO(old_value[range.end..]);
|
||||||
} else if (range.end >= self._data.len) {
|
} else if (range.end >= old_value.len) {
|
||||||
self._data = self._data[0..range.start];
|
self._data = try page.dupeSSO(old_value[0..range.start]);
|
||||||
} else {
|
} else {
|
||||||
self._data = try std.mem.concat(page.arena, u8, &.{
|
// Deleting from middle - concat prefix and suffix
|
||||||
self._data[0..range.start],
|
self._data = try String.concat(page.arena, &.{
|
||||||
self._data[range.end..],
|
old_value[0..range.start],
|
||||||
|
old_value[range.end..],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
page.characterDataChange(self.asNode(), old_value);
|
page.characterDataChange(self.asNode(), old_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insertData(self: *CData, offset: usize, data: []const u8, page: *Page) !void {
|
pub fn insertData(self: *CData, offset: usize, data: []const u8, page: *Page) !void {
|
||||||
const byte_offset = try utf16OffsetToUtf8(self._data, offset);
|
const byte_offset = try utf16OffsetToUtf8(self._data.str(), offset);
|
||||||
const new_data = try std.mem.concat(page.arena, u8, &.{
|
const old_value = self._data;
|
||||||
self._data[0..byte_offset],
|
const existing = old_value.str();
|
||||||
|
self._data = try String.concat(page.arena, &.{
|
||||||
|
existing[0..byte_offset],
|
||||||
data,
|
data,
|
||||||
self._data[byte_offset..],
|
existing[byte_offset..],
|
||||||
});
|
});
|
||||||
try self.setData(new_data, page);
|
page.characterDataChange(self.asNode(), old_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, page: *Page) !void {
|
pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, page: *Page) !void {
|
||||||
const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);
|
const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);
|
||||||
const range = try utf16RangeToUtf8(self._data, offset, end_utf16);
|
const range = try utf16RangeToUtf8(self._data.str(), offset, end_utf16);
|
||||||
const new_data = try std.mem.concat(page.arena, u8, &.{
|
const old_value = self._data;
|
||||||
self._data[0..range.start],
|
const existing = old_value.str();
|
||||||
|
self._data = try String.concat(page.arena, &.{
|
||||||
|
existing[0..range.start],
|
||||||
data,
|
data,
|
||||||
self._data[range.end..],
|
existing[range.end..],
|
||||||
});
|
});
|
||||||
try self.setData(new_data, page);
|
page.characterDataChange(self.asNode(), old_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn substringData(self: *const CData, offset: usize, count: usize) ![]const u8 {
|
pub fn substringData(self: *const CData, offset: usize, count: usize) ![]const u8 {
|
||||||
const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);
|
const end_utf16 = std.math.add(usize, offset, count) catch std.math.maxInt(usize);
|
||||||
const range = try utf16RangeToUtf8(self._data, offset, end_utf16);
|
const range = try utf16RangeToUtf8(self._data.str(), offset, end_utf16);
|
||||||
return self._data[range.start..range.end];
|
return self._data.str()[range.start..range.end];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(self: *CData, page: *Page) !void {
|
pub fn remove(self: *CData, page: *Page) !void {
|
||||||
@@ -451,7 +458,7 @@ test "WebApi: CData.render" {
|
|||||||
const cdata = CData{
|
const cdata = CData{
|
||||||
._type = .{ .text = undefined },
|
._type = .{ .text = undefined },
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
._data = test_case.value,
|
._data = .wrap(test_case.value),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = try cdata.render(&buffer.writer, test_case.opts);
|
const result = try cdata.render(&buffer.writer, test_case.opts);
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ pub const JsApi = struct {
|
|||||||
pub const info = bridge.function(Console.info, .{});
|
pub const info = bridge.function(Console.info, .{});
|
||||||
pub const log = bridge.function(Console.log, .{});
|
pub const log = bridge.function(Console.log, .{});
|
||||||
pub const warn = bridge.function(Console.warn, .{});
|
pub const warn = bridge.function(Console.warn, .{});
|
||||||
pub const clear = bridge.function(Console.clear, .{});
|
pub const clear = bridge.function(Console.clear, .{ .noop = true });
|
||||||
pub const assert = bridge.function(Console.assert, .{});
|
pub const assert = bridge.function(Console.assert, .{});
|
||||||
pub const @"error" = bridge.function(Console.@"error", .{});
|
pub const @"error" = bridge.function(Console.@"error", .{});
|
||||||
pub const exception = bridge.function(Console.@"error", .{});
|
pub const exception = bridge.function(Console.@"error", .{});
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub fn getRandomValues(_: *const Crypto, js_obj: js.Object) !js.Object {
|
|||||||
var into = try js_obj.toZig(RandomValues);
|
var into = try js_obj.toZig(RandomValues);
|
||||||
const buf = into.asBuffer();
|
const buf = into.asBuffer();
|
||||||
if (buf.len > 65_536) {
|
if (buf.len > 65_536) {
|
||||||
return error.QuotaExceededError;
|
return error.QuotaExceeded;
|
||||||
}
|
}
|
||||||
std.crypto.random.bytes(buf);
|
std.crypto.random.bytes(buf);
|
||||||
return js_obj;
|
return js_obj;
|
||||||
@@ -82,7 +82,7 @@ pub const JsApi = struct {
|
|||||||
pub const empty_with_no_proto = true;
|
pub const empty_with_no_proto = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const getRandomValues = bridge.function(Crypto.getRandomValues, .{});
|
pub const getRandomValues = bridge.function(Crypto.getRandomValues, .{ .dom_exception = true });
|
||||||
pub const randomUUID = bridge.function(Crypto.randomUUID, .{});
|
pub const randomUUID = bridge.function(Crypto.randomUUID, .{});
|
||||||
pub const subtle = bridge.accessor(Crypto.getSubtle, null, .{});
|
pub const subtle = bridge.accessor(Crypto.getSubtle, null, .{});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const Page = @import("../Page.zig");
|
|||||||
|
|
||||||
const Node = @import("Node.zig");
|
const Node = @import("Node.zig");
|
||||||
const Element = @import("Element.zig");
|
const Element = @import("Element.zig");
|
||||||
|
const DOMException = @import("DOMException.zig");
|
||||||
const Custom = @import("element/html/Custom.zig");
|
const Custom = @import("element/html/Custom.zig");
|
||||||
const CustomElementDefinition = @import("CustomElementDefinition.zig");
|
const CustomElementDefinition = @import("CustomElementDefinition.zig");
|
||||||
|
|
||||||
@@ -124,6 +125,10 @@ pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page)
|
|||||||
return local.resolvePromise(definition.constructor);
|
return local.resolvePromise(definition.constructor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateName(name) catch |err| {
|
||||||
|
return local.rejectPromise(DOMException.fromError(err) orelse unreachable);
|
||||||
|
};
|
||||||
|
|
||||||
const gop = try self._when_defined.getOrPut(page.arena, name);
|
const gop = try self._when_defined.getOrPut(page.arena, name);
|
||||||
if (gop.found_existing) {
|
if (gop.found_existing) {
|
||||||
return local.toLocal(gop.value_ptr.*).promise();
|
return local.toLocal(gop.value_ptr.*).promise();
|
||||||
@@ -200,15 +205,15 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
|
|||||||
|
|
||||||
fn validateName(name: []const u8) !void {
|
fn validateName(name: []const u8) !void {
|
||||||
if (name.len == 0) {
|
if (name.len == 0) {
|
||||||
return error.InvalidCustomElementName;
|
return error.SyntaxError;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.indexOf(u8, name, "-") == null) {
|
if (std.mem.indexOf(u8, name, "-") == null) {
|
||||||
return error.InvalidCustomElementName;
|
return error.SyntaxError;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name[0] < 'a' or name[0] > 'z') {
|
if (name[0] < 'a' or name[0] > 'z') {
|
||||||
return error.InvalidCustomElementName;
|
return error.SyntaxError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reserved_names = [_][]const u8{
|
const reserved_names = [_][]const u8{
|
||||||
@@ -224,16 +229,20 @@ fn validateName(name: []const u8) !void {
|
|||||||
|
|
||||||
for (reserved_names) |reserved| {
|
for (reserved_names) |reserved| {
|
||||||
if (std.mem.eql(u8, name, reserved)) {
|
if (std.mem.eql(u8, name, reserved)) {
|
||||||
return error.InvalidCustomElementName;
|
return error.SyntaxError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (name) |c| {
|
for (name) |c| {
|
||||||
const valid = (c >= 'a' and c <= 'z') or
|
if (c >= 'A' and c <= 'Z') {
|
||||||
(c >= '0' and c <= '9') or
|
return error.SyntaxError;
|
||||||
c == '-';
|
}
|
||||||
if (!valid) {
|
|
||||||
return error.InvalidCustomElementName;
|
// Reject control characters and specific invalid characters
|
||||||
|
// per elementLocalNameRegex: [^\0\t\n\f\r\u0020/>]*
|
||||||
|
switch (c) {
|
||||||
|
0, '\t', '\n', '\r', 0x0C, ' ', '/', '>' => return error.SyntaxError,
|
||||||
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,7 +259,7 @@ pub const JsApi = struct {
|
|||||||
pub const define = bridge.function(CustomElementRegistry.define, .{ .dom_exception = true });
|
pub const define = bridge.function(CustomElementRegistry.define, .{ .dom_exception = true });
|
||||||
pub const get = bridge.function(CustomElementRegistry.get, .{ .null_as_undefined = true });
|
pub const get = bridge.function(CustomElementRegistry.get, .{ .null_as_undefined = true });
|
||||||
pub const upgrade = bridge.function(CustomElementRegistry.upgrade, .{});
|
pub const upgrade = bridge.function(CustomElementRegistry.upgrade, .{});
|
||||||
pub const whenDefined = bridge.function(CustomElementRegistry.whenDefined, .{});
|
pub const whenDefined = bridge.function(CustomElementRegistry.whenDefined, .{ .dom_exception = true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ const Document = @import("Document.zig");
|
|||||||
|
|
||||||
const DOMParser = @This();
|
const DOMParser = @This();
|
||||||
|
|
||||||
|
// Padding to avoid zero-size struct, which causes identity_map pointer collisions.
|
||||||
|
_pad: bool = false,
|
||||||
|
|
||||||
pub fn init() DOMParser {
|
pub fn init() DOMParser {
|
||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const DOMTreeWalker = @import("DOMTreeWalker.zig");
|
|||||||
const DOMNodeIterator = @import("DOMNodeIterator.zig");
|
const DOMNodeIterator = @import("DOMNodeIterator.zig");
|
||||||
const DOMImplementation = @import("DOMImplementation.zig");
|
const DOMImplementation = @import("DOMImplementation.zig");
|
||||||
const StyleSheetList = @import("css/StyleSheetList.zig");
|
const StyleSheetList = @import("css/StyleSheetList.zig");
|
||||||
|
const FontFaceSet = @import("css/FontFaceSet.zig");
|
||||||
const Selection = @import("Selection.zig");
|
const Selection = @import("Selection.zig");
|
||||||
|
|
||||||
pub const XMLDocument = @import("XMLDocument.zig");
|
pub const XMLDocument = @import("XMLDocument.zig");
|
||||||
@@ -43,6 +44,7 @@ const Document = @This();
|
|||||||
|
|
||||||
_type: Type,
|
_type: Type,
|
||||||
_proto: *Node,
|
_proto: *Node,
|
||||||
|
_page: ?*Page = null,
|
||||||
_location: ?*Location = null,
|
_location: ?*Location = null,
|
||||||
_url: ?[:0]const u8 = null, // URL for documents created via DOMImplementation (about:blank)
|
_url: ?[:0]const u8 = null, // URL for documents created via DOMImplementation (about:blank)
|
||||||
_ready_state: ReadyState = .loading,
|
_ready_state: ReadyState = .loading,
|
||||||
@@ -52,6 +54,8 @@ _elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty,
|
|||||||
_removed_ids: std.StringHashMapUnmanaged(void) = .empty,
|
_removed_ids: std.StringHashMapUnmanaged(void) = .empty,
|
||||||
_active_element: ?*Element = null,
|
_active_element: ?*Element = null,
|
||||||
_style_sheets: ?*StyleSheetList = null,
|
_style_sheets: ?*StyleSheetList = null,
|
||||||
|
_implementation: ?*DOMImplementation = null,
|
||||||
|
_fonts: ?*FontFaceSet = null,
|
||||||
_write_insertion_point: ?*Node = null,
|
_write_insertion_point: ?*Node = null,
|
||||||
_script_created_parser: ?Parser.Streaming = null,
|
_script_created_parser: ?Parser.Streaming = null,
|
||||||
_adopted_style_sheets: ?js.Object.Global = null,
|
_adopted_style_sheets: ?js.Object.Global = null,
|
||||||
@@ -272,8 +276,11 @@ pub fn querySelectorAll(self: *Document, input: String, page: *Page) !*Selector.
|
|||||||
return Selector.querySelectorAll(self.asNode(), input.str(), page);
|
return Selector.querySelectorAll(self.asNode(), input.str(), page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getImplementation(_: *const Document) DOMImplementation {
|
pub fn getImplementation(self: *Document, page: *Page) !*DOMImplementation {
|
||||||
return .{};
|
if (self._implementation) |impl| return impl;
|
||||||
|
const impl = try page._factory.create(DOMImplementation{});
|
||||||
|
self._implementation = impl;
|
||||||
|
return impl;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createDocumentFragment(self: *Document, page: *Page) !*Node.DocumentFragment {
|
pub fn createDocumentFragment(self: *Document, page: *Page) !*Node.DocumentFragment {
|
||||||
@@ -430,6 +437,15 @@ pub fn getStyleSheets(self: *Document, page: *Page) !*StyleSheetList {
|
|||||||
return sheets;
|
return sheets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getFonts(self: *Document, page: *Page) !*FontFaceSet {
|
||||||
|
if (self._fonts) |fonts| {
|
||||||
|
return fonts;
|
||||||
|
}
|
||||||
|
const fonts = try FontFaceSet.init(page);
|
||||||
|
self._fonts = fonts;
|
||||||
|
return fonts;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn adoptNode(_: *const Document, node: *Node, page: *Page) !*Node {
|
pub fn adoptNode(_: *const Document, node: *Node, page: *Page) !*Node {
|
||||||
if (node._type == .document) {
|
if (node._type == .document) {
|
||||||
return error.NotSupported;
|
return error.NotSupported;
|
||||||
@@ -637,11 +653,13 @@ pub fn write(self: *Document, text: []const []const u8, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (html.len > 0) {
|
if (html.len > 0) {
|
||||||
self._script_created_parser.?.read(html) catch |err| {
|
if (self._script_created_parser) |*parser| {
|
||||||
log.warn(.dom, "document.write parser error", .{ .err = err });
|
parser.read(html) catch |err| {
|
||||||
// was alrady closed
|
log.warn(.dom, "document.write parser error", .{ .err = err });
|
||||||
self._script_created_parser = null;
|
// was alrady closed
|
||||||
};
|
self._script_created_parser = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -726,6 +744,7 @@ pub fn open(self: *Document, page: *Page) !*Document {
|
|||||||
self._elements_by_id.clearAndFree(page.arena);
|
self._elements_by_id.clearAndFree(page.arena);
|
||||||
self._active_element = null;
|
self._active_element = null;
|
||||||
self._style_sheets = null;
|
self._style_sheets = null;
|
||||||
|
self._implementation = null;
|
||||||
self._ready_state = .loading;
|
self._ready_state = .loading;
|
||||||
|
|
||||||
self._script_created_parser = Parser.Streaming.init(page.arena, doc_node, page);
|
self._script_created_parser = Parser.Streaming.init(page.arena, doc_node, page);
|
||||||
@@ -805,17 +824,6 @@ pub fn setAdoptedStyleSheets(self: *Document, sheets: js.Object) !void {
|
|||||||
self._adopted_style_sheets = try sheets.persist();
|
self._adopted_style_sheets = try sheets.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getHidden(_: *const Document) bool {
|
|
||||||
// it's hidden when, for example, the decive is locked, or user is on a
|
|
||||||
// a different tab.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getVisibilityState(_: *const Document) []const u8 {
|
|
||||||
// See getHidden above, possible options are "visible" or "hidden"
|
|
||||||
return "visible";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validates that nodes can be inserted into a Document, respecting Document constraints:
|
// Validates that nodes can be inserted into a Document, respecting Document constraints:
|
||||||
// - At most one Element child
|
// - At most one Element child
|
||||||
// - At most one DocumentType child
|
// - At most one DocumentType child
|
||||||
@@ -963,6 +971,7 @@ pub const JsApi = struct {
|
|||||||
pub const implementation = bridge.accessor(Document.getImplementation, null, .{});
|
pub const implementation = bridge.accessor(Document.getImplementation, null, .{});
|
||||||
pub const activeElement = bridge.accessor(Document.getActiveElement, null, .{});
|
pub const activeElement = bridge.accessor(Document.getActiveElement, null, .{});
|
||||||
pub const styleSheets = bridge.accessor(Document.getStyleSheets, null, .{});
|
pub const styleSheets = bridge.accessor(Document.getStyleSheets, null, .{});
|
||||||
|
pub const fonts = bridge.accessor(Document.getFonts, null, .{});
|
||||||
pub const contentType = bridge.accessor(Document.getContentType, null, .{});
|
pub const contentType = bridge.accessor(Document.getContentType, null, .{});
|
||||||
pub const domain = bridge.accessor(Document.getDomain, null, .{});
|
pub const domain = bridge.accessor(Document.getDomain, null, .{});
|
||||||
pub const createElement = bridge.function(Document.createElement, .{ .dom_exception = true });
|
pub const createElement = bridge.function(Document.createElement, .{ .dom_exception = true });
|
||||||
@@ -1011,8 +1020,8 @@ pub const JsApi = struct {
|
|||||||
pub const lastElementChild = bridge.accessor(Document.getLastElementChild, null, .{});
|
pub const lastElementChild = bridge.accessor(Document.getLastElementChild, null, .{});
|
||||||
pub const childElementCount = bridge.accessor(Document.getChildElementCount, null, .{});
|
pub const childElementCount = bridge.accessor(Document.getChildElementCount, null, .{});
|
||||||
pub const adoptedStyleSheets = bridge.accessor(Document.getAdoptedStyleSheets, Document.setAdoptedStyleSheets, .{});
|
pub const adoptedStyleSheets = bridge.accessor(Document.getAdoptedStyleSheets, Document.setAdoptedStyleSheets, .{});
|
||||||
pub const hidden = bridge.accessor(Document.getHidden, null, .{});
|
pub const hidden = bridge.property(false, .{ .template = false, .readonly = true });
|
||||||
pub const visibilityState = bridge.accessor(Document.getVisibilityState, null, .{});
|
pub const visibilityState = bridge.property("visible", .{ .template = false, .readonly = true });
|
||||||
pub const defaultView = bridge.accessor(struct {
|
pub const defaultView = bridge.accessor(struct {
|
||||||
fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") {
|
fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") {
|
||||||
return page.window;
|
return page.window;
|
||||||
|
|||||||
@@ -868,12 +868,10 @@ pub fn focus(self: *Element, page: *Page) !void {
|
|||||||
|
|
||||||
// Dispatch blur on old element (no bubble, composed)
|
// Dispatch blur on old element (no bubble, composed)
|
||||||
const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true, .relatedTarget = new_target }, page);
|
const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true, .relatedTarget = new_target }, page);
|
||||||
defer if (!blur_event.asEvent()._v8_handoff) blur_event.deinit(false);
|
|
||||||
try page._event_manager.dispatch(old_target, blur_event.asEvent());
|
try page._event_manager.dispatch(old_target, blur_event.asEvent());
|
||||||
|
|
||||||
// Dispatch focusout on old element (bubbles, composed)
|
// Dispatch focusout on old element (bubbles, composed)
|
||||||
const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true, .relatedTarget = new_target }, page);
|
const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true, .relatedTarget = new_target }, page);
|
||||||
defer if (!focusout_event.asEvent()._v8_handoff) focusout_event.deinit(false);
|
|
||||||
try page._event_manager.dispatch(old_target, focusout_event.asEvent());
|
try page._event_manager.dispatch(old_target, focusout_event.asEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,12 +879,10 @@ pub fn focus(self: *Element, page: *Page) !void {
|
|||||||
|
|
||||||
// Dispatch focus on new element (no bubble, composed)
|
// Dispatch focus on new element (no bubble, composed)
|
||||||
const focus_event = try FocusEvent.initTrusted(comptime .wrap("focus"), .{ .composed = true, .relatedTarget = old_related }, page);
|
const focus_event = try FocusEvent.initTrusted(comptime .wrap("focus"), .{ .composed = true, .relatedTarget = old_related }, page);
|
||||||
defer if (!focus_event.asEvent()._v8_handoff) focus_event.deinit(false);
|
|
||||||
try page._event_manager.dispatch(new_target, focus_event.asEvent());
|
try page._event_manager.dispatch(new_target, focus_event.asEvent());
|
||||||
|
|
||||||
// Dispatch focusin on new element (bubbles, composed)
|
// Dispatch focusin on new element (bubbles, composed)
|
||||||
const focusin_event = try FocusEvent.initTrusted(comptime .wrap("focusin"), .{ .bubbles = true, .composed = true, .relatedTarget = old_related }, page);
|
const focusin_event = try FocusEvent.initTrusted(comptime .wrap("focusin"), .{ .bubbles = true, .composed = true, .relatedTarget = old_related }, page);
|
||||||
defer if (!focusin_event.asEvent()._v8_handoff) focusin_event.deinit(false);
|
|
||||||
try page._event_manager.dispatch(new_target, focusin_event.asEvent());
|
try page._event_manager.dispatch(new_target, focusin_event.asEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -900,12 +896,10 @@ pub fn blur(self: *Element, page: *Page) !void {
|
|||||||
|
|
||||||
// Dispatch blur (no bubble, composed)
|
// Dispatch blur (no bubble, composed)
|
||||||
const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true }, page);
|
const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true }, page);
|
||||||
defer if (!blur_event.asEvent()._v8_handoff) blur_event.deinit(false);
|
|
||||||
try page._event_manager.dispatch(old_target, blur_event.asEvent());
|
try page._event_manager.dispatch(old_target, blur_event.asEvent());
|
||||||
|
|
||||||
// Dispatch focusout (bubbles, composed)
|
// Dispatch focusout (bubbles, composed)
|
||||||
const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true }, page);
|
const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true }, page);
|
||||||
defer if (!focusout_event.asEvent()._v8_handoff) focusout_event.deinit(false);
|
|
||||||
try page._event_manager.dispatch(old_target, focusout_event.asEvent());
|
try page._event_manager.dispatch(old_target, focusout_event.asEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ const Node = @import("Node.zig");
|
|||||||
const String = @import("../../string.zig").String;
|
const String = @import("../../string.zig").String;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
pub const Event = @This();
|
pub const Event = @This();
|
||||||
|
|
||||||
pub const _prototype_root = true;
|
pub const _prototype_root = true;
|
||||||
_type: Type,
|
_type: Type,
|
||||||
_page: *Page,
|
|
||||||
_arena: Allocator,
|
_arena: Allocator,
|
||||||
_bubbles: bool = false,
|
_bubbles: bool = false,
|
||||||
_cancelable: bool = false,
|
_cancelable: bool = false,
|
||||||
@@ -45,13 +45,16 @@ _stop_immediate_propagation: bool = false,
|
|||||||
_event_phase: EventPhase = .none,
|
_event_phase: EventPhase = .none,
|
||||||
_time_stamp: u64,
|
_time_stamp: u64,
|
||||||
_needs_retargeting: bool = false,
|
_needs_retargeting: bool = false,
|
||||||
_isTrusted: bool = false,
|
_is_trusted: bool = false,
|
||||||
|
|
||||||
// There's a period of time between creating an event and handing it off to v8
|
// There's a period of time between creating an event and handing it off to v8
|
||||||
// where things can fail. If it does fail, we need to deinit the event. This flag
|
// where things can fail. If it does fail, we need to deinit the event. The timing
|
||||||
// when true, tells us the event is registered in the js.Contxt and thus, at
|
// window can be difficult to capture, so we use a reference count.
|
||||||
// the very least, will be finalized on context shutdown.
|
// should be 0, 1, or 2. 0
|
||||||
_v8_handoff: bool = false,
|
// - 0: no reference, always a transient state going to either 1 or about to be deinit'd
|
||||||
|
// - 1: either zig or v8 have a reference
|
||||||
|
// - 2: both zig and v8 have a reference
|
||||||
|
_rc: u8 = 0,
|
||||||
|
|
||||||
pub const EventPhase = enum(u8) {
|
pub const EventPhase = enum(u8) {
|
||||||
none = 0,
|
none = 0,
|
||||||
@@ -84,16 +87,16 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
|
|||||||
const arena = try page.getArena(.{ .debug = "Event" });
|
const arena = try page.getArena(.{ .debug = "Event" });
|
||||||
errdefer page.releaseArena(arena);
|
errdefer page.releaseArena(arena);
|
||||||
const str = try String.init(arena, typ, .{});
|
const str = try String.init(arena, typ, .{});
|
||||||
return initWithTrusted(arena, str, opts_, false, page);
|
return initWithTrusted(arena, str, opts_, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*Event {
|
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*Event {
|
||||||
const arena = try page.getArena(.{ .debug = "Event.trusted" });
|
const arena = try page.getArena(.{ .debug = "Event.trusted" });
|
||||||
errdefer page.releaseArena(arena);
|
errdefer page.releaseArena(arena);
|
||||||
return initWithTrusted(arena, typ, opts_, true, page);
|
return initWithTrusted(arena, typ, opts_, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*Event {
|
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, comptime trusted: bool) !*Event {
|
||||||
const opts = opts_ orelse Options{};
|
const opts = opts_ orelse Options{};
|
||||||
|
|
||||||
// Round to 2ms for privacy (browsers do this)
|
// Round to 2ms for privacy (browsers do this)
|
||||||
@@ -102,7 +105,6 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
|
|||||||
|
|
||||||
const event = try arena.create(Event);
|
const event = try arena.create(Event);
|
||||||
event.* = .{
|
event.* = .{
|
||||||
._page = page,
|
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
._type = .generic,
|
._type = .generic,
|
||||||
._bubbles = opts.bubbles,
|
._bubbles = opts.bubbles,
|
||||||
@@ -110,7 +112,7 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
|
|||||||
._cancelable = opts.cancelable,
|
._cancelable = opts.cancelable,
|
||||||
._composed = opts.composed,
|
._composed = opts.composed,
|
||||||
._type_string = typ,
|
._type_string = typ,
|
||||||
._isTrusted = trusted,
|
._is_trusted = trusted,
|
||||||
};
|
};
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
@@ -133,9 +135,26 @@ pub fn initEvent(
|
|||||||
self._prevent_default = false;
|
self._prevent_default = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Event, shutdown: bool) void {
|
pub fn acquireRef(self: *Event) void {
|
||||||
_ = shutdown;
|
self._rc += 1;
|
||||||
self._page.releaseArena(self._arena);
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Event, shutdown: bool, page: *Page) void {
|
||||||
|
if (shutdown) {
|
||||||
|
page.releaseArena(self._arena);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rc = self._rc;
|
||||||
|
if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(rc != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rc == 1) {
|
||||||
|
page.releaseArena(self._arena);
|
||||||
|
} else {
|
||||||
|
self._rc = rc - 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as(self: *Event, comptime T: type) *T {
|
pub fn as(self: *Event, comptime T: type) *T {
|
||||||
@@ -237,15 +256,15 @@ pub fn getTimeStamp(self: *const Event) u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn setTrusted(self: *Event) void {
|
pub fn setTrusted(self: *Event) void {
|
||||||
self._isTrusted = true;
|
self._is_trusted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setUntrusted(self: *Event) void {
|
pub fn setUntrusted(self: *Event) void {
|
||||||
self._isTrusted = false;
|
self._is_trusted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getIsTrusted(self: *const Event) bool {
|
pub fn getIsTrusted(self: *const Event) bool {
|
||||||
return self._isTrusted;
|
return self._is_trusted;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget {
|
pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget {
|
||||||
@@ -403,8 +422,8 @@ pub fn populatePrototypes(self: anytype, opts: anytype, trusted: bool) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set isTrusted at the Event level (base of prototype chain)
|
// Set isTrusted at the Event level (base of prototype chain)
|
||||||
if (T == Event or @hasField(T, "_isTrusted")) {
|
if (T == Event or @hasField(T, "is_trusted")) {
|
||||||
self._isTrusted = trusted;
|
self._is_trusted = trusted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ pub const Type = union(enum) {
|
|||||||
screen: *@import("Screen.zig"),
|
screen: *@import("Screen.zig"),
|
||||||
screen_orientation: *@import("Screen.zig").Orientation,
|
screen_orientation: *@import("Screen.zig").Orientation,
|
||||||
visual_viewport: *@import("VisualViewport.zig"),
|
visual_viewport: *@import("VisualViewport.zig"),
|
||||||
|
file_reader: *@import("FileReader.zig"),
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(page: *Page) !*EventTarget {
|
pub fn init(page: *Page) !*EventTarget {
|
||||||
@@ -55,7 +56,10 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool {
|
|||||||
if (event._event_phase != .none) {
|
if (event._event_phase != .none) {
|
||||||
return error.InvalidStateError;
|
return error.InvalidStateError;
|
||||||
}
|
}
|
||||||
event._isTrusted = false;
|
event._is_trusted = false;
|
||||||
|
|
||||||
|
event.acquireRef();
|
||||||
|
defer event.deinit(false, page);
|
||||||
try page._event_manager.dispatch(self, event);
|
try page._event_manager.dispatch(self, event);
|
||||||
return !event._cancelable or !event._prevent_default;
|
return !event._cancelable or !event._prevent_default;
|
||||||
}
|
}
|
||||||
@@ -151,6 +155,7 @@ pub fn toString(self: *EventTarget) []const u8 {
|
|||||||
.screen => return "[object Screen]",
|
.screen => return "[object Screen]",
|
||||||
.screen_orientation => return "[object ScreenOrientation]",
|
.screen_orientation => return "[object ScreenOrientation]",
|
||||||
.visual_viewport => return "[object VisualViewport]",
|
.visual_viewport => return "[object VisualViewport]",
|
||||||
|
.file_reader => return "[object FileReader]",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
358
src/browser/webapi/FileReader.zig
Normal file
358
src/browser/webapi/FileReader.zig
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
// Copyright (C) 2023-2026 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 Page = @import("../Page.zig");
|
||||||
|
const EventTarget = @import("EventTarget.zig");
|
||||||
|
const ProgressEvent = @import("event/ProgressEvent.zig");
|
||||||
|
const Blob = @import("Blob.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
/// https://w3c.github.io/FileAPI/#dfn-filereader
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Web/API/FileReader
|
||||||
|
const FileReader = @This();
|
||||||
|
|
||||||
|
_page: *Page,
|
||||||
|
_proto: *EventTarget,
|
||||||
|
_arena: Allocator,
|
||||||
|
|
||||||
|
_ready_state: ReadyState = .empty,
|
||||||
|
_result: ?Result = null,
|
||||||
|
_error: ?[]const u8 = null,
|
||||||
|
|
||||||
|
_on_abort: ?js.Function.Temp = null,
|
||||||
|
_on_error: ?js.Function.Temp = null,
|
||||||
|
_on_load: ?js.Function.Temp = null,
|
||||||
|
_on_load_end: ?js.Function.Temp = null,
|
||||||
|
_on_load_start: ?js.Function.Temp = null,
|
||||||
|
_on_progress: ?js.Function.Temp = null,
|
||||||
|
|
||||||
|
_aborted: bool = false,
|
||||||
|
|
||||||
|
const ReadyState = enum(u8) {
|
||||||
|
empty = 0,
|
||||||
|
loading = 1,
|
||||||
|
done = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Result = union(enum) {
|
||||||
|
string: []const u8,
|
||||||
|
arraybuffer: js.ArrayBuffer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(page: *Page) !*FileReader {
|
||||||
|
const arena = try page.getArena(.{ .debug = "FileReader" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
|
||||||
|
return page._factory.eventTargetWithAllocator(arena, FileReader{
|
||||||
|
._page = page,
|
||||||
|
._arena = arena,
|
||||||
|
._proto = undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *FileReader, _: bool, page: *Page) void {
|
||||||
|
const js_ctx = page.js;
|
||||||
|
|
||||||
|
if (self._on_abort) |func| js_ctx.release(func);
|
||||||
|
if (self._on_error) |func| js_ctx.release(func);
|
||||||
|
if (self._on_load) |func| js_ctx.release(func);
|
||||||
|
if (self._on_load_end) |func| js_ctx.release(func);
|
||||||
|
if (self._on_load_start) |func| js_ctx.release(func);
|
||||||
|
if (self._on_progress) |func| js_ctx.release(func);
|
||||||
|
|
||||||
|
page.releaseArena(self._arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn asEventTarget(self: *FileReader) *EventTarget {
|
||||||
|
return self._proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnAbort(self: *const FileReader) ?js.Function.Temp {
|
||||||
|
return self._on_abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnAbort(self: *FileReader, cb: ?js.Function.Temp) !void {
|
||||||
|
self._on_abort = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnError(self: *const FileReader) ?js.Function.Temp {
|
||||||
|
return self._on_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnError(self: *FileReader, cb: ?js.Function.Temp) !void {
|
||||||
|
self._on_error = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnLoad(self: *const FileReader) ?js.Function.Temp {
|
||||||
|
return self._on_load;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnLoad(self: *FileReader, cb: ?js.Function.Temp) !void {
|
||||||
|
self._on_load = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnLoadEnd(self: *const FileReader) ?js.Function.Temp {
|
||||||
|
return self._on_load_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnLoadEnd(self: *FileReader, cb: ?js.Function.Temp) !void {
|
||||||
|
self._on_load_end = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnLoadStart(self: *const FileReader) ?js.Function.Temp {
|
||||||
|
return self._on_load_start;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnLoadStart(self: *FileReader, cb: ?js.Function.Temp) !void {
|
||||||
|
self._on_load_start = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnProgress(self: *const FileReader) ?js.Function.Temp {
|
||||||
|
return self._on_progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnProgress(self: *FileReader, cb: ?js.Function.Temp) !void {
|
||||||
|
self._on_progress = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getReadyState(self: *const FileReader) u8 {
|
||||||
|
return @intFromEnum(self._ready_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getResult(self: *const FileReader) ?Result {
|
||||||
|
return self._result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getError(self: *const FileReader) ?[]const u8 {
|
||||||
|
return self._error;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readAsArrayBuffer(self: *FileReader, blob: *Blob) !void {
|
||||||
|
try self.readInternal(blob, .arraybuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readAsBinaryString(self: *FileReader, blob: *Blob) !void {
|
||||||
|
try self.readInternal(blob, .binary_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readAsText(self: *FileReader, blob: *Blob, encoding_: ?[]const u8) !void {
|
||||||
|
_ = encoding_; // TODO: Handle encoding properly
|
||||||
|
try self.readInternal(blob, .text);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readAsDataURL(self: *FileReader, blob: *Blob) !void {
|
||||||
|
try self.readInternal(blob, .data_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReadType = enum {
|
||||||
|
arraybuffer,
|
||||||
|
binary_string,
|
||||||
|
text,
|
||||||
|
data_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn readInternal(self: *FileReader, blob: *Blob, read_type: ReadType) !void {
|
||||||
|
if (self._ready_state == .loading) {
|
||||||
|
return error.InvalidStateError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
self._ready_state = .loading;
|
||||||
|
self._result = null;
|
||||||
|
self._error = null;
|
||||||
|
self._aborted = false;
|
||||||
|
|
||||||
|
const page = self._page;
|
||||||
|
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
const local = &ls.local;
|
||||||
|
|
||||||
|
try self.dispatch(.load_start, .{ .loaded = 0, .total = blob.getSize() }, local, page);
|
||||||
|
if (self._aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the read (synchronous since data is in memory)
|
||||||
|
const data = blob._slice;
|
||||||
|
const size = data.len;
|
||||||
|
try self.dispatch(.progress, .{ .loaded = size, .total = size }, local, page);
|
||||||
|
if (self._aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the data based on read type
|
||||||
|
self._result = switch (read_type) {
|
||||||
|
.arraybuffer => .{ .arraybuffer = .{ .values = data } },
|
||||||
|
.binary_string => .{ .string = data },
|
||||||
|
.text => .{ .string = data },
|
||||||
|
.data_url => blk: {
|
||||||
|
// Create data URL with base64 encoding
|
||||||
|
const mime = if (blob._mime.len > 0) blob._mime else "application/octet-stream";
|
||||||
|
const data_url = try encodeDataURL(self._arena, mime, data);
|
||||||
|
break :blk .{ .string = data_url };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
self._ready_state = .done;
|
||||||
|
|
||||||
|
try self.dispatch(.load, .{ .loaded = size, .total = size }, local, page);
|
||||||
|
try self.dispatch(.load_end, .{ .loaded = size, .total = size }, local, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn abort(self: *FileReader) !void {
|
||||||
|
if (self._ready_state != .loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self._aborted = true;
|
||||||
|
self._ready_state = .done;
|
||||||
|
self._result = null;
|
||||||
|
|
||||||
|
const page = self._page;
|
||||||
|
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
const local = &ls.local;
|
||||||
|
|
||||||
|
try self.dispatch(.abort, null, local, page);
|
||||||
|
|
||||||
|
try self.dispatch(.load_end, null, local, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch(self: *FileReader, comptime event_type: DispatchType, progress_: ?Progress, local: *const js.Local, page: *Page) !void {
|
||||||
|
const field, const typ = comptime blk: {
|
||||||
|
break :blk switch (event_type) {
|
||||||
|
.abort => .{ "_on_abort", "abort" },
|
||||||
|
.err => .{ "_on_error", "error" },
|
||||||
|
.load => .{ "_on_load", "load" },
|
||||||
|
.load_end => .{ "_on_load_end", "loadend" },
|
||||||
|
.load_start => .{ "_on_load_start", "loadstart" },
|
||||||
|
.progress => .{ "_on_progress", "progress" },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const progress = progress_ orelse Progress{};
|
||||||
|
const event = (try ProgressEvent.initTrusted(
|
||||||
|
comptime .wrap(typ),
|
||||||
|
.{ .total = progress.total, .loaded = progress.loaded },
|
||||||
|
page,
|
||||||
|
)).asEvent();
|
||||||
|
|
||||||
|
return page._event_manager.dispatchWithFunction(
|
||||||
|
self.asEventTarget(),
|
||||||
|
event,
|
||||||
|
local.toLocal(@field(self, field)),
|
||||||
|
.{ .context = "FileReader " ++ typ },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DispatchType = enum {
|
||||||
|
abort,
|
||||||
|
err,
|
||||||
|
load,
|
||||||
|
load_end,
|
||||||
|
load_start,
|
||||||
|
progress,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Progress = struct {
|
||||||
|
loaded: usize = 0,
|
||||||
|
total: usize = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Encodes binary data as a data URL with base64 encoding.
|
||||||
|
/// Format: data:[<mediatype>][;base64],<data>
|
||||||
|
fn encodeDataURL(arena: Allocator, mime: []const u8, data: []const u8) ![]const u8 {
|
||||||
|
const base64 = std.base64.standard.Encoder;
|
||||||
|
|
||||||
|
// Calculate size needed for base64 encoding
|
||||||
|
const encoded_size = base64.calcSize(data.len);
|
||||||
|
|
||||||
|
// Allocate buffer for the full data URL
|
||||||
|
// Format: "data:" + mime + ";base64," + encoded_data
|
||||||
|
const prefix = "data:";
|
||||||
|
const suffix = ";base64,";
|
||||||
|
const total_size = prefix.len + mime.len + suffix.len + encoded_size;
|
||||||
|
|
||||||
|
var pos: usize = 0;
|
||||||
|
const buf = try arena.alloc(u8, total_size);
|
||||||
|
|
||||||
|
@memcpy(buf[pos..][0..prefix.len], prefix);
|
||||||
|
pos += prefix.len;
|
||||||
|
|
||||||
|
@memcpy(buf[pos..][0..mime.len], mime);
|
||||||
|
pos += mime.len;
|
||||||
|
|
||||||
|
@memcpy(buf[pos..][0..suffix.len], suffix);
|
||||||
|
pos += suffix.len;
|
||||||
|
|
||||||
|
_ = base64.encode(buf[pos..], data);
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(FileReader);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "FileReader";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const weak = true;
|
||||||
|
pub const finalizer = bridge.finalizer(FileReader.deinit);
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const constructor = bridge.constructor(FileReader.init, .{});
|
||||||
|
|
||||||
|
// State constants
|
||||||
|
pub const EMPTY = bridge.property(@intFromEnum(FileReader.ReadyState.empty), .{ .template = true });
|
||||||
|
pub const LOADING = bridge.property(@intFromEnum(FileReader.ReadyState.loading), .{ .template = true });
|
||||||
|
pub const DONE = bridge.property(@intFromEnum(FileReader.ReadyState.done), .{ .template = true });
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
pub const readyState = bridge.accessor(FileReader.getReadyState, null, .{});
|
||||||
|
pub const result = bridge.accessor(FileReader.getResult, null, .{});
|
||||||
|
pub const @"error" = bridge.accessor(FileReader.getError, null, .{});
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
pub const onabort = bridge.accessor(FileReader.getOnAbort, FileReader.setOnAbort, .{});
|
||||||
|
pub const onerror = bridge.accessor(FileReader.getOnError, FileReader.setOnError, .{});
|
||||||
|
pub const onload = bridge.accessor(FileReader.getOnLoad, FileReader.setOnLoad, .{});
|
||||||
|
pub const onloadend = bridge.accessor(FileReader.getOnLoadEnd, FileReader.setOnLoadEnd, .{});
|
||||||
|
pub const onloadstart = bridge.accessor(FileReader.getOnLoadStart, FileReader.setOnLoadStart, .{});
|
||||||
|
pub const onprogress = bridge.accessor(FileReader.getOnProgress, FileReader.setOnProgress, .{});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
pub const readAsArrayBuffer = bridge.function(FileReader.readAsArrayBuffer, .{ .dom_exception = true });
|
||||||
|
pub const readAsBinaryString = bridge.function(FileReader.readAsBinaryString, .{ .dom_exception = true });
|
||||||
|
pub const readAsText = bridge.function(FileReader.readAsText, .{ .dom_exception = true });
|
||||||
|
pub const readAsDataURL = bridge.function(FileReader.readAsDataURL, .{ .dom_exception = true });
|
||||||
|
pub const abort = bridge.function(FileReader.abort, .{});
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "WebApi: FileReader" {
|
||||||
|
try testing.htmlRunner("file_reader.html", .{});
|
||||||
|
}
|
||||||
@@ -80,8 +80,6 @@ fn goInner(delta: i32, page: *Page) !void {
|
|||||||
if (entry._url) |url| {
|
if (entry._url) |url| {
|
||||||
if (try page.isSameOrigin(url)) {
|
if (try page.isSameOrigin(url)) {
|
||||||
const event = (try PopStateEvent.initTrusted(comptime .wrap("popstate"), .{ .state = entry._state.value }, page)).asEvent();
|
const event = (try PopStateEvent.initTrusted(comptime .wrap("popstate"), .{ .state = entry._state.value }, page)).asEvent();
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
|
|
||||||
try page._event_manager.dispatchWithFunction(
|
try page._event_manager.dispatchWithFunction(
|
||||||
page.window.asEventTarget(),
|
page.window.asEventTarget(),
|
||||||
event,
|
event,
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ const std = @import("std");
|
|||||||
|
|
||||||
const IdleDeadline = @This();
|
const IdleDeadline = @This();
|
||||||
|
|
||||||
|
// Padding to avoid zero-size struct, which causes identity_map pointer collisions.
|
||||||
|
_pad: bool = false,
|
||||||
|
|
||||||
pub fn init() IdleDeadline {
|
pub fn init() IdleDeadline {
|
||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,14 +93,6 @@ pub fn getHeight(self: *const ImageData) u32 {
|
|||||||
return self._height;
|
return self._height;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getPixelFormat(_: *const ImageData) String {
|
|
||||||
return comptime .wrap("rgba-unorm8");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getColorSpace(_: *const ImageData) String {
|
|
||||||
return comptime .wrap("srgb");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getData(self: *const ImageData) js.ArrayBufferRef(.uint8_clamped).Global {
|
pub fn getData(self: *const ImageData) js.ArrayBufferRef(.uint8_clamped).Global {
|
||||||
return self._data;
|
return self._data;
|
||||||
}
|
}
|
||||||
@@ -116,11 +108,12 @@ pub const JsApi = struct {
|
|||||||
|
|
||||||
pub const constructor = bridge.constructor(ImageData.constructor, .{ .dom_exception = true });
|
pub const constructor = bridge.constructor(ImageData.constructor, .{ .dom_exception = true });
|
||||||
|
|
||||||
|
pub const colorSpace = bridge.property("srgb", .{ .template = false, .readonly = true });
|
||||||
|
pub const pixelFormat = bridge.property("rgba-unorm8", .{ .template = false, .readonly = true });
|
||||||
|
|
||||||
|
pub const data = bridge.accessor(ImageData.getData, null, .{});
|
||||||
pub const width = bridge.accessor(ImageData.getWidth, null, .{});
|
pub const width = bridge.accessor(ImageData.getWidth, null, .{});
|
||||||
pub const height = bridge.accessor(ImageData.getHeight, null, .{});
|
pub const height = bridge.accessor(ImageData.getHeight, null, .{});
|
||||||
pub const pixelFormat = bridge.accessor(ImageData.getPixelFormat, null, .{});
|
|
||||||
pub const colorSpace = bridge.accessor(ImageData.getColorSpace, null, .{});
|
|
||||||
pub const data = bridge.accessor(ImageData.getData, null, .{});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ pub fn registerTypes() []const type {
|
|||||||
|
|
||||||
const IntersectionObserver = @This();
|
const IntersectionObserver = @This();
|
||||||
|
|
||||||
_page: *Page,
|
|
||||||
_arena: Allocator,
|
_arena: Allocator,
|
||||||
_callback: js.Function.Temp,
|
_callback: js.Function.Temp,
|
||||||
_observing: std.ArrayList(*Element) = .{},
|
_observing: std.ArrayList(*Element) = .{},
|
||||||
@@ -83,7 +82,6 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I
|
|||||||
|
|
||||||
const self = try arena.create(IntersectionObserver);
|
const self = try arena.create(IntersectionObserver);
|
||||||
self.* = .{
|
self.* = .{
|
||||||
._page = page,
|
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
._callback = callback,
|
._callback = callback,
|
||||||
._root = opts.root,
|
._root = opts.root,
|
||||||
@@ -93,8 +91,7 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *IntersectionObserver, shutdown: bool) void {
|
pub fn deinit(self: *IntersectionObserver, shutdown: bool, page: *Page) void {
|
||||||
const page = self._page;
|
|
||||||
page.js.release(self._callback);
|
page.js.release(self._callback);
|
||||||
if ((comptime IS_DEBUG) and !shutdown) {
|
if ((comptime IS_DEBUG) and !shutdown) {
|
||||||
std.debug.assert(self._observing.items.len == 0);
|
std.debug.assert(self._observing.items.len == 0);
|
||||||
@@ -140,7 +137,7 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi
|
|||||||
while (j < self._pending_entries.items.len) {
|
while (j < self._pending_entries.items.len) {
|
||||||
if (self._pending_entries.items[j]._target == target) {
|
if (self._pending_entries.items[j]._target == target) {
|
||||||
const entry = self._pending_entries.swapRemove(j);
|
const entry = self._pending_entries.swapRemove(j);
|
||||||
entry.deinit(false);
|
entry.deinit(false, page);
|
||||||
} else {
|
} else {
|
||||||
j += 1;
|
j += 1;
|
||||||
}
|
}
|
||||||
@@ -160,7 +157,7 @@ pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
|
|||||||
self._previous_states.clearRetainingCapacity();
|
self._previous_states.clearRetainingCapacity();
|
||||||
|
|
||||||
for (self._pending_entries.items) |entry| {
|
for (self._pending_entries.items) |entry| {
|
||||||
entry.deinit(false);
|
entry.deinit(false, page);
|
||||||
}
|
}
|
||||||
self._pending_entries.clearRetainingCapacity();
|
self._pending_entries.clearRetainingCapacity();
|
||||||
page.js.safeWeakRef(self);
|
page.js.safeWeakRef(self);
|
||||||
@@ -245,7 +242,6 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
|
|||||||
|
|
||||||
const entry = try arena.create(IntersectionObserverEntry);
|
const entry = try arena.create(IntersectionObserverEntry);
|
||||||
entry.* = .{
|
entry.* = .{
|
||||||
._page = page,
|
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
._target = target,
|
._target = target,
|
||||||
._time = page.window._performance.now(),
|
._time = page.window._performance.now(),
|
||||||
@@ -297,7 +293,6 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const IntersectionObserverEntry = struct {
|
pub const IntersectionObserverEntry = struct {
|
||||||
_page: *Page,
|
|
||||||
_arena: Allocator,
|
_arena: Allocator,
|
||||||
_time: f64,
|
_time: f64,
|
||||||
_target: *Element,
|
_target: *Element,
|
||||||
@@ -307,8 +302,8 @@ pub const IntersectionObserverEntry = struct {
|
|||||||
_intersection_ratio: f64,
|
_intersection_ratio: f64,
|
||||||
_is_intersecting: bool,
|
_is_intersecting: bool,
|
||||||
|
|
||||||
pub fn deinit(self: *const IntersectionObserverEntry, _: bool) void {
|
pub fn deinit(self: *const IntersectionObserverEntry, _: bool, page: *Page) void {
|
||||||
self._page.releaseArena(self._arena);
|
page.releaseArena(self._arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getTarget(self: *const IntersectionObserverEntry) *Element {
|
pub fn getTarget(self: *const IntersectionObserverEntry) *Element {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ pub fn copy(arena: Allocator, original: KeyValueList) !KeyValueList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn fromJsObject(arena: Allocator, js_obj: js.Object, comptime normalizer: ?Normalizer, page: *Page) !KeyValueList {
|
pub fn fromJsObject(arena: Allocator, js_obj: js.Object, comptime normalizer: ?Normalizer, page: *Page) !KeyValueList {
|
||||||
var it = js_obj.nameIterator();
|
var it = try js_obj.nameIterator();
|
||||||
var list = KeyValueList.init();
|
var list = KeyValueList.init();
|
||||||
try list.ensureTotalCapacity(arena, it.count);
|
try list.ensureTotalCapacity(arena, it.count);
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,6 @@ const PostMessageCallback = struct {
|
|||||||
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
||||||
return null;
|
return null;
|
||||||
}).asEvent();
|
}).asEvent();
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
|
|
||||||
var ls: js.Local.Scope = undefined;
|
var ls: js.Local.Scope = undefined;
|
||||||
page.js.localScope(&ls);
|
page.js.localScope(&ls);
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ pub fn registerTypes() []const type {
|
|||||||
|
|
||||||
const MutationObserver = @This();
|
const MutationObserver = @This();
|
||||||
|
|
||||||
_page: *Page,
|
|
||||||
_arena: Allocator,
|
_arena: Allocator,
|
||||||
_callback: js.Function.Temp,
|
_callback: js.Function.Temp,
|
||||||
_observing: std.ArrayList(Observing) = .{},
|
_observing: std.ArrayList(Observing) = .{},
|
||||||
@@ -79,15 +78,13 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
|
|||||||
|
|
||||||
const self = try arena.create(MutationObserver);
|
const self = try arena.create(MutationObserver);
|
||||||
self.* = .{
|
self.* = .{
|
||||||
._page = page,
|
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
._callback = callback,
|
._callback = callback,
|
||||||
};
|
};
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *MutationObserver, shutdown: bool) void {
|
pub fn deinit(self: *MutationObserver, shutdown: bool, page: *Page) void {
|
||||||
const page = self._page;
|
|
||||||
page.js.release(self._callback);
|
page.js.release(self._callback);
|
||||||
if ((comptime IS_DEBUG) and !shutdown) {
|
if ((comptime IS_DEBUG) and !shutdown) {
|
||||||
std.debug.assert(self._observing.items.len == 0);
|
std.debug.assert(self._observing.items.len == 0);
|
||||||
@@ -174,7 +171,7 @@ pub fn disconnect(self: *MutationObserver, page: *Page) void {
|
|||||||
page.unregisterMutationObserver(self);
|
page.unregisterMutationObserver(self);
|
||||||
self._observing.clearRetainingCapacity();
|
self._observing.clearRetainingCapacity();
|
||||||
for (self._pending_records.items) |record| {
|
for (self._pending_records.items) |record| {
|
||||||
record.deinit(false);
|
record.deinit(false, page);
|
||||||
}
|
}
|
||||||
self._pending_records.clearRetainingCapacity();
|
self._pending_records.clearRetainingCapacity();
|
||||||
page.js.safeWeakRef(self);
|
page.js.safeWeakRef(self);
|
||||||
@@ -218,10 +215,9 @@ pub fn notifyAttributeChange(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const arena = try self._page.getArena(.{ .debug = "MutationRecord" });
|
const arena = try page.getArena(.{ .debug = "MutationRecord" });
|
||||||
const record = try arena.create(MutationRecord);
|
const record = try arena.create(MutationRecord);
|
||||||
record.* = .{
|
record.* = .{
|
||||||
._page = page,
|
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
._type = .attributes,
|
._type = .attributes,
|
||||||
._target = target_node,
|
._target = target_node,
|
||||||
@@ -247,7 +243,7 @@ pub fn notifyAttributeChange(
|
|||||||
pub fn notifyCharacterDataChange(
|
pub fn notifyCharacterDataChange(
|
||||||
self: *MutationObserver,
|
self: *MutationObserver,
|
||||||
target: *Node,
|
target: *Node,
|
||||||
old_value: ?[]const u8,
|
old_value: ?String,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !void {
|
) !void {
|
||||||
for (self._observing.items) |obs| {
|
for (self._observing.items) |obs| {
|
||||||
@@ -263,16 +259,15 @@ pub fn notifyCharacterDataChange(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const arena = try self._page.getArena(.{ .debug = "MutationRecord" });
|
const arena = try page.getArena(.{ .debug = "MutationRecord" });
|
||||||
const record = try arena.create(MutationRecord);
|
const record = try arena.create(MutationRecord);
|
||||||
record.* = .{
|
record.* = .{
|
||||||
._page = page,
|
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
._type = .characterData,
|
._type = .characterData,
|
||||||
._target = target,
|
._target = target,
|
||||||
._attribute_name = null,
|
._attribute_name = null,
|
||||||
._old_value = if (obs.options.characterDataOldValue and old_value != null)
|
._old_value = if (obs.options.characterDataOldValue and old_value != null)
|
||||||
try arena.dupe(u8, old_value.?)
|
try arena.dupe(u8, old_value.?.str())
|
||||||
else
|
else
|
||||||
null,
|
null,
|
||||||
._added_nodes = &.{},
|
._added_nodes = &.{},
|
||||||
@@ -311,10 +306,9 @@ pub fn notifyChildListChange(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const arena = try self._page.getArena(.{ .debug = "MutationRecord" });
|
const arena = try page.getArena(.{ .debug = "MutationRecord" });
|
||||||
const record = try arena.create(MutationRecord);
|
const record = try arena.create(MutationRecord);
|
||||||
record.* = .{
|
record.* = .{
|
||||||
._page = page,
|
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
._type = .childList,
|
._type = .childList,
|
||||||
._target = target,
|
._target = target,
|
||||||
@@ -354,7 +348,6 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
|
|||||||
|
|
||||||
pub const MutationRecord = struct {
|
pub const MutationRecord = struct {
|
||||||
_type: Type,
|
_type: Type,
|
||||||
_page: *Page,
|
|
||||||
_target: *Node,
|
_target: *Node,
|
||||||
_arena: Allocator,
|
_arena: Allocator,
|
||||||
_attribute_name: ?[]const u8,
|
_attribute_name: ?[]const u8,
|
||||||
@@ -370,8 +363,8 @@ pub const MutationRecord = struct {
|
|||||||
characterData,
|
characterData,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn deinit(self: *const MutationRecord, _: bool) void {
|
pub fn deinit(self: *const MutationRecord, _: bool, page: *Page) void {
|
||||||
self._page.releaseArena(self._arena);
|
page.releaseArena(self._arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getType(self: *const MutationRecord) []const u8 {
|
pub fn getType(self: *const MutationRecord) []const u8 {
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ pub fn getTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!vo
|
|||||||
try child.getTextContent(writer);
|
try child.getTextContent(writer);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.cdata => |c| try writer.writeAll(c.getData()),
|
.cdata => |c| try writer.writeAll(c._data.str()),
|
||||||
.document => {},
|
.document => {},
|
||||||
.document_type => {},
|
.document_type => {},
|
||||||
.attribute => |attr| try writer.writeAll(attr._value.str()),
|
.attribute => |attr| try writer.writeAll(attr._value.str()),
|
||||||
@@ -293,7 +293,7 @@ pub fn setTextContent(self: *Node, data: []const u8, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
return el.replaceChildren(&.{.{ .text = data }}, page);
|
return el.replaceChildren(&.{.{ .text = data }}, page);
|
||||||
},
|
},
|
||||||
.cdata => |c| c._data = try page.arena.dupe(u8, data),
|
.cdata => |c| c._data = try page.dupeSSO(data),
|
||||||
.document => {},
|
.document => {},
|
||||||
.document_type => {},
|
.document_type => {},
|
||||||
.document_fragment => |frag| {
|
.document_fragment => |frag| {
|
||||||
@@ -599,10 +599,10 @@ pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, page: *Page
|
|||||||
return old_child;
|
return old_child;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getNodeValue(self: *const Node) ?[]const u8 {
|
pub fn getNodeValue(self: *const Node) ?String {
|
||||||
return switch (self._type) {
|
return switch (self._type) {
|
||||||
.cdata => |c| c.getData(),
|
.cdata => |c| c.getData(),
|
||||||
.attribute => |attr| attr._value.str(),
|
.attribute => |attr| attr._value,
|
||||||
.element => null,
|
.element => null,
|
||||||
.document => null,
|
.document => null,
|
||||||
.document_type => null,
|
.document_type => null,
|
||||||
@@ -694,10 +694,10 @@ pub fn getChildAt(self: *Node, index: u32) ?*Node {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getData(self: *const Node) []const u8 {
|
pub fn getData(self: *const Node) String {
|
||||||
return switch (self._type) {
|
return switch (self._type) {
|
||||||
.cdata => |c| c.getData(),
|
.cdata => |c| c.getData(),
|
||||||
else => "",
|
else => .empty,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,11 +713,23 @@ pub fn normalize(self: *Node, page: *Page) !void {
|
|||||||
return self._normalize(page.call_arena, &buffer, page);
|
return self._normalize(page.call_arena, &buffer, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) error{ OutOfMemory, StringTooLarge, NotSupported, NotImplemented, InvalidCharacterError, CloneError, IFrameLoadError }!*Node {
|
const CloneError = error{
|
||||||
|
OutOfMemory,
|
||||||
|
StringTooLarge,
|
||||||
|
NotSupported,
|
||||||
|
NotImplemented,
|
||||||
|
InvalidCharacterError,
|
||||||
|
CloneError,
|
||||||
|
IFrameLoadError,
|
||||||
|
TooManyContexts,
|
||||||
|
LinkLoadError,
|
||||||
|
StyleLoadError,
|
||||||
|
};
|
||||||
|
pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node {
|
||||||
const deep = deep_ orelse false;
|
const deep = deep_ orelse false;
|
||||||
switch (self._type) {
|
switch (self._type) {
|
||||||
.cdata => |cd| {
|
.cdata => |cd| {
|
||||||
const data = cd.getData();
|
const data = cd.getData().str();
|
||||||
return switch (cd._type) {
|
return switch (cd._type) {
|
||||||
.text => page.createTextNode(data),
|
.text => page.createTextNode(data),
|
||||||
.cdata_section => page.createCDATASection(data),
|
.cdata_section => page.createCDATASection(data),
|
||||||
@@ -872,7 +884,7 @@ fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), pag
|
|||||||
next_node = node_to_merge.nextSibling();
|
next_node = node_to_merge.nextSibling();
|
||||||
page.removeNode(self, to_remove, .{ .will_be_reconnected = false });
|
page.removeNode(self, to_remove, .{ .will_be_reconnected = false });
|
||||||
}
|
}
|
||||||
text_node._proto._data = try page.dupeString(buffer.items);
|
text_node._proto._data = try page.dupeSSO(buffer.items);
|
||||||
buffer.clearRetainingCapacity();
|
buffer.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1016,7 +1028,7 @@ pub const JsApi = struct {
|
|||||||
try self.getTextContent(&buf.writer);
|
try self.getTextContent(&buf.writer);
|
||||||
return buf.written();
|
return buf.written();
|
||||||
},
|
},
|
||||||
.cdata => |cdata| return cdata.getData(),
|
.cdata => |cdata| return cdata._data.str(),
|
||||||
.attribute => |attr| return attr._value.str(),
|
.attribute => |attr| return attr._value.str(),
|
||||||
.document => return null,
|
.document => return null,
|
||||||
.document_type => return null,
|
.document_type => return null,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const Page = @import("../Page.zig");
|
|||||||
const datetime = @import("../../datetime.zig");
|
const datetime = @import("../../datetime.zig");
|
||||||
|
|
||||||
pub fn registerTypes() []const type {
|
pub fn registerTypes() []const type {
|
||||||
return &.{ Performance, Entry, Mark, Measure };
|
return &.{ Performance, Entry, Mark, Measure, PerformanceTiming, PerformanceNavigation };
|
||||||
}
|
}
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
@@ -12,6 +12,8 @@ const Performance = @This();
|
|||||||
|
|
||||||
_time_origin: u64,
|
_time_origin: u64,
|
||||||
_entries: std.ArrayList(*Entry) = .{},
|
_entries: std.ArrayList(*Entry) = .{},
|
||||||
|
_timing: PerformanceTiming = .{},
|
||||||
|
_navigation: PerformanceNavigation = .{},
|
||||||
|
|
||||||
/// Get high-resolution timestamp in microseconds, rounded to 5μs increments
|
/// Get high-resolution timestamp in microseconds, rounded to 5μs increments
|
||||||
/// to match browser behavior (prevents fingerprinting)
|
/// to match browser behavior (prevents fingerprinting)
|
||||||
@@ -27,9 +29,15 @@ pub fn init() Performance {
|
|||||||
return .{
|
return .{
|
||||||
._time_origin = highResTimestamp(),
|
._time_origin = highResTimestamp(),
|
||||||
._entries = .{},
|
._entries = .{},
|
||||||
|
._timing = .{},
|
||||||
|
._navigation = .{},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getTiming(self: *Performance) *PerformanceTiming {
|
||||||
|
return &self._timing;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn now(self: *const Performance) f64 {
|
pub fn now(self: *const Performance) f64 {
|
||||||
const current = highResTimestamp();
|
const current = highResTimestamp();
|
||||||
const elapsed = current - self._time_origin;
|
const elapsed = current - self._time_origin;
|
||||||
@@ -42,6 +50,10 @@ pub fn getTimeOrigin(self: *const Performance) f64 {
|
|||||||
return @as(f64, @floatFromInt(self._time_origin)) / 1000.0;
|
return @as(f64, @floatFromInt(self._time_origin)) / 1000.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getNavigation(self: *Performance) *PerformanceNavigation {
|
||||||
|
return &self._navigation;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mark(
|
pub fn mark(
|
||||||
self: *Performance,
|
self: *Performance,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
@@ -208,9 +220,37 @@ fn getMarkTime(self: *const Performance, mark_name: []const u8) !f64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recognized mark names by browsers. `navigationStart` is an equivalent
|
// PerformanceTiming attribute names are valid start/end marks per the
|
||||||
// to 0. Others are dependant to request arrival, end of request etc.
|
// W3C User Timing Level 2 spec. All are relative to navigationStart (= 0).
|
||||||
if (std.mem.eql(u8, "navigationStart", mark_name)) {
|
// https://www.w3.org/TR/user-timing/#dom-performance-measure
|
||||||
|
//
|
||||||
|
// `navigationStart` is an equivalent to 0.
|
||||||
|
// Others are dependant to request arrival, end of request etc, but we
|
||||||
|
// return a dummy 0 value for now.
|
||||||
|
const navigation_timing_marks = std.StaticStringMap(void).initComptime(.{
|
||||||
|
.{ "navigationStart", {} },
|
||||||
|
.{ "unloadEventStart", {} },
|
||||||
|
.{ "unloadEventEnd", {} },
|
||||||
|
.{ "redirectStart", {} },
|
||||||
|
.{ "redirectEnd", {} },
|
||||||
|
.{ "fetchStart", {} },
|
||||||
|
.{ "domainLookupStart", {} },
|
||||||
|
.{ "domainLookupEnd", {} },
|
||||||
|
.{ "connectStart", {} },
|
||||||
|
.{ "connectEnd", {} },
|
||||||
|
.{ "secureConnectionStart", {} },
|
||||||
|
.{ "requestStart", {} },
|
||||||
|
.{ "responseStart", {} },
|
||||||
|
.{ "responseEnd", {} },
|
||||||
|
.{ "domLoading", {} },
|
||||||
|
.{ "domInteractive", {} },
|
||||||
|
.{ "domContentLoadedEventStart", {} },
|
||||||
|
.{ "domContentLoadedEventEnd", {} },
|
||||||
|
.{ "domComplete", {} },
|
||||||
|
.{ "loadEventStart", {} },
|
||||||
|
.{ "loadEventEnd", {} },
|
||||||
|
});
|
||||||
|
if (navigation_timing_marks.has(mark_name)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +275,8 @@ pub const JsApi = struct {
|
|||||||
pub const getEntriesByType = bridge.function(Performance.getEntriesByType, .{});
|
pub const getEntriesByType = bridge.function(Performance.getEntriesByType, .{});
|
||||||
pub const getEntriesByName = bridge.function(Performance.getEntriesByName, .{});
|
pub const getEntriesByName = bridge.function(Performance.getEntriesByName, .{});
|
||||||
pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{});
|
pub const timeOrigin = bridge.accessor(Performance.getTimeOrigin, null, .{});
|
||||||
|
pub const timing = bridge.accessor(Performance.getTiming, null, .{});
|
||||||
|
pub const navigation = bridge.accessor(Performance.getNavigation, null, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Entry = struct {
|
pub const Entry = struct {
|
||||||
@@ -421,6 +463,70 @@ pub const Measure = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// PerformanceTiming — Navigation Timing Level 1 (legacy, but widely used).
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming
|
||||||
|
/// All properties return 0 as stub values; the object must not be undefined
|
||||||
|
/// so that scripts accessing performance.timing.navigationStart don't crash.
|
||||||
|
pub const PerformanceTiming = struct {
|
||||||
|
// Padding to avoid zero-size struct, which causes identity_map pointer collisions.
|
||||||
|
_pad: bool = false,
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(PerformanceTiming);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "PerformanceTiming";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const empty_with_no_proto = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const navigationStart = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const unloadEventStart = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const unloadEventEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const redirectStart = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const redirectEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const fetchStart = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const domainLookupStart = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const domainLookupEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const connectStart = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const connectEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const secureConnectionStart = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const requestStart = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const responseStart = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const responseEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const domLoading = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const domInteractive = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const domContentLoadedEventStart = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const domContentLoadedEventEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const domComplete = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const loadEventStart = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const loadEventEnd = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// PerformanceNavigation implements the Navigation Timing Level 1 API.
|
||||||
|
// https://www.w3.org/TR/navigation-timing/#sec-navigation-navigation-timing-interface
|
||||||
|
// Stub implementation — returns 0 for type (TYPE_NAVIGATE) and 0 for redirectCount.
|
||||||
|
pub const PerformanceNavigation = struct {
|
||||||
|
// Padding to avoid zero-size struct, which causes identity_map pointer collisions.
|
||||||
|
_pad: bool = false,
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(PerformanceNavigation);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "PerformanceNavigation";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const empty_with_no_proto = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const @"type" = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
pub const redirectCount = bridge.property(0.0, .{ .template = false, .readonly = true });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "WebApi: Performance" {
|
test "WebApi: Performance" {
|
||||||
try testing.htmlRunner("performance.html", .{});
|
try testing.htmlRunner("performance.html", .{});
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ pub const JsApi = struct {
|
|||||||
|
|
||||||
pub const length = bridge.property(0, .{ .template = false });
|
pub const length = bridge.property(0, .{ .template = false });
|
||||||
pub const refresh = bridge.function(PluginArray.refresh, .{});
|
pub const refresh = bridge.function(PluginArray.refresh, .{});
|
||||||
pub const @"[int]" = bridge.indexed(PluginArray.getAtIndex, .{ .null_as_undefined = true });
|
pub const @"[int]" = bridge.indexed(PluginArray.getAtIndex, null, .{ .null_as_undefined = true });
|
||||||
pub const @"[str]" = bridge.namedIndexed(PluginArray.getByName, null, null, .{ .null_as_undefined = true });
|
pub const @"[str]" = bridge.namedIndexed(PluginArray.getByName, null, null, .{ .null_as_undefined = true });
|
||||||
pub const item = bridge.function(_item, .{});
|
pub const item = bridge.function(_item, .{});
|
||||||
fn _item(self: *const PluginArray, index: i32) ?*Plugin {
|
fn _item(self: *const PluginArray, index: i32) ?*Plugin {
|
||||||
|
|||||||
@@ -17,9 +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 String = @import("../../string.zig").String;
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
|
|
||||||
const Node = @import("Node.zig");
|
const Node = @import("Node.zig");
|
||||||
const DocumentFragment = @import("DocumentFragment.zig");
|
const DocumentFragment = @import("DocumentFragment.zig");
|
||||||
const AbstractRange = @import("AbstractRange.zig");
|
const AbstractRange = @import("AbstractRange.zig");
|
||||||
@@ -326,7 +328,7 @@ pub fn insertNode(self: *Range, node: *Node, page: *Page) !void {
|
|||||||
if (offset == 0) {
|
if (offset == 0) {
|
||||||
_ = try parent.insertBefore(node, container, page);
|
_ = try parent.insertBefore(node, container, page);
|
||||||
} else {
|
} else {
|
||||||
const text_data = container.getData();
|
const text_data = container.getData().str();
|
||||||
if (offset >= text_data.len) {
|
if (offset >= text_data.len) {
|
||||||
_ = try parent.insertBefore(node, container.nextSibling(), page);
|
_ = try parent.insertBefore(node, container.nextSibling(), page);
|
||||||
} else {
|
} else {
|
||||||
@@ -362,15 +364,15 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
|
|||||||
|
|
||||||
// Simple case: same container
|
// Simple case: same container
|
||||||
if (self._proto._start_container == self._proto._end_container) {
|
if (self._proto._start_container == self._proto._end_container) {
|
||||||
if (self._proto._start_container.is(Node.CData)) |_| {
|
if (self._proto._start_container.is(Node.CData)) |cdata| {
|
||||||
// Delete part of text node
|
// Delete part of text node
|
||||||
const text_data = self._proto._start_container.getData();
|
const old_value = cdata.getData();
|
||||||
const new_text = try std.mem.concat(
|
const text_data = old_value.str();
|
||||||
|
cdata._data = try String.concat(
|
||||||
page.arena,
|
page.arena,
|
||||||
u8,
|
|
||||||
&.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] },
|
&.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] },
|
||||||
);
|
);
|
||||||
try self._proto._start_container.setData(new_text, page);
|
page.characterDataChange(self._proto._start_container, old_value);
|
||||||
} else {
|
} else {
|
||||||
// Delete child nodes in range
|
// Delete child nodes in range
|
||||||
var offset = self._proto._start_offset;
|
var offset = self._proto._start_offset;
|
||||||
@@ -386,8 +388,8 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
|
|||||||
|
|
||||||
// Complex case: different containers
|
// Complex case: different containers
|
||||||
// Handle start container - if it's a text node, truncate it
|
// Handle start container - if it's a text node, truncate it
|
||||||
if (self._proto._start_container.is(Node.CData)) |_| {
|
if (self._proto._start_container.is(Node.CData)) |cdata| {
|
||||||
const text_data = self._proto._start_container.getData();
|
const text_data = cdata._data.str();
|
||||||
if (self._proto._start_offset < text_data.len) {
|
if (self._proto._start_offset < text_data.len) {
|
||||||
// Keep only the part before start_offset
|
// Keep only the part before start_offset
|
||||||
const new_text = text_data[0..self._proto._start_offset];
|
const new_text = text_data[0..self._proto._start_offset];
|
||||||
@@ -396,8 +398,8 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle end container - if it's a text node, truncate it
|
// Handle end container - if it's a text node, truncate it
|
||||||
if (self._proto._end_container.is(Node.CData)) |_| {
|
if (self._proto._end_container.is(Node.CData)) |cdata| {
|
||||||
const text_data = self._proto._end_container.getData();
|
const text_data = cdata._data.str();
|
||||||
if (self._proto._end_offset < text_data.len) {
|
if (self._proto._end_offset < text_data.len) {
|
||||||
// Keep only the part from end_offset onwards
|
// Keep only the part from end_offset onwards
|
||||||
const new_text = text_data[self._proto._end_offset..];
|
const new_text = text_data[self._proto._end_offset..];
|
||||||
@@ -433,7 +435,7 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
|
|||||||
if (self._proto._start_container == self._proto._end_container) {
|
if (self._proto._start_container == self._proto._end_container) {
|
||||||
if (self._proto._start_container.is(Node.CData)) |_| {
|
if (self._proto._start_container.is(Node.CData)) |_| {
|
||||||
// Clone part of text node
|
// Clone part of text node
|
||||||
const text_data = self._proto._start_container.getData();
|
const text_data = self._proto._start_container.getData().str();
|
||||||
if (self._proto._start_offset < text_data.len and self._proto._end_offset <= text_data.len) {
|
if (self._proto._start_offset < text_data.len and self._proto._end_offset <= text_data.len) {
|
||||||
const cloned_text = text_data[self._proto._start_offset..self._proto._end_offset];
|
const cloned_text = text_data[self._proto._start_offset..self._proto._end_offset];
|
||||||
const text_node = try page.createTextNode(cloned_text);
|
const text_node = try page.createTextNode(cloned_text);
|
||||||
@@ -453,7 +455,7 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
|
|||||||
// Complex case: different containers
|
// Complex case: different containers
|
||||||
// Clone partial start container
|
// Clone partial start container
|
||||||
if (self._proto._start_container.is(Node.CData)) |_| {
|
if (self._proto._start_container.is(Node.CData)) |_| {
|
||||||
const text_data = self._proto._start_container.getData();
|
const text_data = self._proto._start_container.getData().str();
|
||||||
if (self._proto._start_offset < text_data.len) {
|
if (self._proto._start_offset < text_data.len) {
|
||||||
// Clone from start_offset to end of text
|
// Clone from start_offset to end of text
|
||||||
const cloned_text = text_data[self._proto._start_offset..];
|
const cloned_text = text_data[self._proto._start_offset..];
|
||||||
@@ -474,7 +476,7 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
|
|||||||
|
|
||||||
// Clone partial end container
|
// Clone partial end container
|
||||||
if (self._proto._end_container.is(Node.CData)) |_| {
|
if (self._proto._end_container.is(Node.CData)) |_| {
|
||||||
const text_data = self._proto._end_container.getData();
|
const text_data = self._proto._end_container.getData().str();
|
||||||
if (self._proto._end_offset > 0 and self._proto._end_offset <= text_data.len) {
|
if (self._proto._end_offset > 0 and self._proto._end_offset <= text_data.len) {
|
||||||
// Clone from start to end_offset
|
// Clone from start to end_offset
|
||||||
const cloned_text = text_data[0..self._proto._end_offset];
|
const cloned_text = text_data[0..self._proto._end_offset];
|
||||||
@@ -560,7 +562,7 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
|||||||
if (start_node == end_node) {
|
if (start_node == end_node) {
|
||||||
if (start_node.is(Node.CData)) |cdata| {
|
if (start_node.is(Node.CData)) |cdata| {
|
||||||
if (!isCommentOrPI(cdata)) {
|
if (!isCommentOrPI(cdata)) {
|
||||||
const data = cdata.getData();
|
const data = cdata.getData().str();
|
||||||
const s = @min(start_offset, data.len);
|
const s = @min(start_offset, data.len);
|
||||||
const e = @min(end_offset, data.len);
|
const e = @min(end_offset, data.len);
|
||||||
try writer.writeAll(data[s..e]);
|
try writer.writeAll(data[s..e]);
|
||||||
@@ -574,7 +576,7 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
|||||||
// Partial start: if start container is a text node, write from offset to end
|
// Partial start: if start container is a text node, write from offset to end
|
||||||
if (start_node.is(Node.CData)) |cdata| {
|
if (start_node.is(Node.CData)) |cdata| {
|
||||||
if (!isCommentOrPI(cdata)) {
|
if (!isCommentOrPI(cdata)) {
|
||||||
const data = cdata.getData();
|
const data = cdata.getData().str();
|
||||||
const s = @min(start_offset, data.len);
|
const s = @min(start_offset, data.len);
|
||||||
try writer.writeAll(data[s..]);
|
try writer.writeAll(data[s..]);
|
||||||
}
|
}
|
||||||
@@ -601,7 +603,7 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
|||||||
}
|
}
|
||||||
if (n.is(Node.CData)) |cdata| {
|
if (n.is(Node.CData)) |cdata| {
|
||||||
if (!isCommentOrPI(cdata)) {
|
if (!isCommentOrPI(cdata)) {
|
||||||
try writer.writeAll(cdata.getData());
|
try writer.writeAll(cdata.getData().str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
current = nextInTreeOrder(n, root);
|
current = nextInTreeOrder(n, root);
|
||||||
@@ -612,7 +614,7 @@ fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
|||||||
if (start_node != end_node) {
|
if (start_node != end_node) {
|
||||||
if (end_node.is(Node.CData)) |cdata| {
|
if (end_node.is(Node.CData)) |cdata| {
|
||||||
if (!isCommentOrPI(cdata)) {
|
if (!isCommentOrPI(cdata)) {
|
||||||
const data = cdata.getData();
|
const data = cdata.getData().str();
|
||||||
const e = @min(end_offset, data.len);
|
const e = @min(end_offset, data.len);
|
||||||
try writer.writeAll(data[0..e]);
|
try writer.writeAll(data[0..e]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const Element = @import("Element.zig");
|
|||||||
|
|
||||||
pub const ResizeObserver = @This();
|
pub const ResizeObserver = @This();
|
||||||
|
|
||||||
|
// Padding to avoid zero-size struct, which causes identity_map pointer collisions.
|
||||||
|
_pad: bool = false,
|
||||||
|
|
||||||
fn init(cbk: js.Function) ResizeObserver {
|
fn init(cbk: js.Function) ResizeObserver {
|
||||||
_ = cbk;
|
_ = cbk;
|
||||||
return .{};
|
return .{};
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ pub const init: Selection = .{};
|
|||||||
|
|
||||||
fn dispatchSelectionChangeEvent(page: *Page) !void {
|
fn dispatchSelectionChangeEvent(page: *Page) !void {
|
||||||
const event = try Event.init("selectionchange", .{}, page);
|
const event = try Event.init("selectionchange", .{}, page);
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
try page._event_manager.dispatch(page.document.asEventTarget(), event);
|
try page._event_manager.dispatch(page.document.asEventTarget(), event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,16 +290,9 @@ const ModifyDirection = enum {
|
|||||||
const ModifyGranularity = enum {
|
const ModifyGranularity = enum {
|
||||||
character,
|
character,
|
||||||
word,
|
word,
|
||||||
line,
|
// The rest are either:
|
||||||
paragraph,
|
// 1. Layout dependent.
|
||||||
lineboundary,
|
// 2. Not widely supported across browsers.
|
||||||
// Firefox doesn't implement:
|
|
||||||
// - sentence
|
|
||||||
// - paragraph
|
|
||||||
// - sentenceboundary
|
|
||||||
// - paragraphboundary
|
|
||||||
// - documentboundary
|
|
||||||
// so we won't either for now.
|
|
||||||
|
|
||||||
pub fn fromString(str: []const u8) ?ModifyGranularity {
|
pub fn fromString(str: []const u8) ?ModifyGranularity {
|
||||||
return std.meta.stringToEnum(ModifyGranularity, str);
|
return std.meta.stringToEnum(ModifyGranularity, str);
|
||||||
@@ -312,18 +304,268 @@ pub fn modify(
|
|||||||
alter_str: []const u8,
|
alter_str: []const u8,
|
||||||
direction_str: []const u8,
|
direction_str: []const u8,
|
||||||
granularity_str: []const u8,
|
granularity_str: []const u8,
|
||||||
|
page: *Page,
|
||||||
) !void {
|
) !void {
|
||||||
const alter = ModifyAlter.fromString(alter_str) orelse return error.InvalidParams;
|
const alter = ModifyAlter.fromString(alter_str) orelse return;
|
||||||
const direction = ModifyDirection.fromString(direction_str) orelse return error.InvalidParams;
|
const direction = ModifyDirection.fromString(direction_str) orelse return;
|
||||||
const granularity = ModifyGranularity.fromString(granularity_str) orelse return error.InvalidParams;
|
const granularity = ModifyGranularity.fromString(granularity_str) orelse return;
|
||||||
|
|
||||||
_ = self._range orelse return;
|
const range = self._range orelse return;
|
||||||
|
|
||||||
log.warn(.not_implemented, "Selection.modify", .{
|
const is_forward = switch (direction) {
|
||||||
.alter = alter,
|
.forward, .right => true,
|
||||||
.direction = direction,
|
.backward, .left => false,
|
||||||
.granularity = granularity,
|
};
|
||||||
});
|
|
||||||
|
switch (granularity) {
|
||||||
|
.character => try self.modifyByCharacter(alter, is_forward, range, page),
|
||||||
|
.word => try self.modifyByWord(alter, is_forward, range, page),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isTextNode(node: *const Node) bool {
|
||||||
|
return switch (node._type) {
|
||||||
|
.cdata => |cd| cd._type == .text,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nextTextNode(node: *Node) ?*Node {
|
||||||
|
var current = node;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (current.firstChild()) |child| {
|
||||||
|
current = child;
|
||||||
|
} else if (current.nextSibling()) |sib| {
|
||||||
|
current = sib;
|
||||||
|
} else {
|
||||||
|
while (true) {
|
||||||
|
const parent = current.parentNode() orelse return null;
|
||||||
|
if (parent.nextSibling()) |uncle| {
|
||||||
|
current = uncle;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTextNode(current)) return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nextTextNodeAfter(node: *Node) ?*Node {
|
||||||
|
var current = node;
|
||||||
|
while (true) {
|
||||||
|
if (current.nextSibling()) |sib| {
|
||||||
|
current = sib;
|
||||||
|
} else {
|
||||||
|
while (true) {
|
||||||
|
const parent = current.parentNode() orelse return null;
|
||||||
|
if (parent.nextSibling()) |uncle| {
|
||||||
|
current = uncle;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var descend = current;
|
||||||
|
while (true) {
|
||||||
|
if (isTextNode(descend)) return descend;
|
||||||
|
descend = descend.firstChild() orelse break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prevTextNode(node: *Node) ?*Node {
|
||||||
|
var current = node;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (current.previousSibling()) |sib| {
|
||||||
|
current = sib;
|
||||||
|
while (current.lastChild()) |child| {
|
||||||
|
current = child;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current = current.parentNode() orelse return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTextNode(current)) return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modifyByCharacter(self: *Selection, alter: ModifyAlter, forward: bool, range: *Range, page: *Page) !void {
|
||||||
|
const abstract = range.asAbstractRange();
|
||||||
|
|
||||||
|
const focus_node = switch (self._direction) {
|
||||||
|
.backward => abstract.getStartContainer(),
|
||||||
|
.forward, .none => abstract.getEndContainer(),
|
||||||
|
};
|
||||||
|
const focus_offset = switch (self._direction) {
|
||||||
|
.backward => abstract.getStartOffset(),
|
||||||
|
.forward, .none => abstract.getEndOffset(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var new_node = focus_node;
|
||||||
|
var new_offset = focus_offset;
|
||||||
|
|
||||||
|
if (isTextNode(focus_node)) {
|
||||||
|
if (forward) {
|
||||||
|
const len = focus_node.getLength();
|
||||||
|
if (focus_offset < len) {
|
||||||
|
new_offset += 1;
|
||||||
|
} else if (nextTextNode(focus_node)) |next| {
|
||||||
|
new_node = next;
|
||||||
|
new_offset = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (focus_offset > 0) {
|
||||||
|
new_offset -= 1;
|
||||||
|
} else if (prevTextNode(focus_node)) |prev| {
|
||||||
|
new_node = prev;
|
||||||
|
new_offset = prev.getLength();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (forward) {
|
||||||
|
if (focus_node.getChildAt(focus_offset)) |child| {
|
||||||
|
if (isTextNode(child)) {
|
||||||
|
new_node = child;
|
||||||
|
new_offset = 0;
|
||||||
|
} else if (nextTextNode(child)) |t| {
|
||||||
|
new_node = t;
|
||||||
|
new_offset = 0;
|
||||||
|
}
|
||||||
|
} else if (nextTextNodeAfter(focus_node)) |next| {
|
||||||
|
new_node = next;
|
||||||
|
new_offset = 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// backward element-node case
|
||||||
|
var idx = focus_offset;
|
||||||
|
while (idx > 0) {
|
||||||
|
idx -= 1;
|
||||||
|
const child = focus_node.getChildAt(idx) orelse break;
|
||||||
|
var bottom = child;
|
||||||
|
while (bottom.lastChild()) |c| bottom = c;
|
||||||
|
if (isTextNode(bottom)) {
|
||||||
|
new_node = bottom;
|
||||||
|
new_offset = bottom.getLength();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.applyModify(alter, new_node, new_offset, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isWordChar(c: u8) bool {
|
||||||
|
return std.ascii.isAlphanumeric(c) or c == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nextWordEnd(text: []const u8, offset: u32) u32 {
|
||||||
|
var i = offset;
|
||||||
|
// consumes whitespace till next word
|
||||||
|
while (i < text.len and !isWordChar(text[i])) : (i += 1) {}
|
||||||
|
// consumes next word
|
||||||
|
while (i < text.len and isWordChar(text[i])) : (i += 1) {}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prevWordStart(text: []const u8, offset: u32) u32 {
|
||||||
|
var i = offset;
|
||||||
|
if (i > 0) i -= 1;
|
||||||
|
// consumes the white space
|
||||||
|
while (i > 0 and !isWordChar(text[i])) : (i -= 1) {}
|
||||||
|
// consumes the last word
|
||||||
|
while (i > 0 and isWordChar(text[i - 1])) : (i -= 1) {}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modifyByWord(self: *Selection, alter: ModifyAlter, forward: bool, range: *Range, page: *Page) !void {
|
||||||
|
const abstract = range.asAbstractRange();
|
||||||
|
|
||||||
|
const focus_node = switch (self._direction) {
|
||||||
|
.backward => abstract.getStartContainer(),
|
||||||
|
.forward, .none => abstract.getEndContainer(),
|
||||||
|
};
|
||||||
|
const focus_offset = switch (self._direction) {
|
||||||
|
.backward => abstract.getStartOffset(),
|
||||||
|
.forward, .none => abstract.getEndOffset(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var new_node = focus_node;
|
||||||
|
var new_offset = focus_offset;
|
||||||
|
|
||||||
|
if (isTextNode(focus_node)) {
|
||||||
|
if (forward) {
|
||||||
|
const i = nextWordEnd(new_node.getData().str(), new_offset);
|
||||||
|
if (i > new_offset) {
|
||||||
|
new_offset = i;
|
||||||
|
} else if (nextTextNode(focus_node)) |next| {
|
||||||
|
new_node = next;
|
||||||
|
new_offset = nextWordEnd(next.getData().str(), 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const i = prevWordStart(new_node.getData().str(), new_offset);
|
||||||
|
if (i < new_offset) {
|
||||||
|
new_offset = i;
|
||||||
|
} else if (prevTextNode(focus_node)) |prev| {
|
||||||
|
new_node = prev;
|
||||||
|
new_offset = prevWordStart(prev.getData().str(), @intCast(prev.getData().len));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Search and apply rules on the next Text Node.
|
||||||
|
// This is either next (on forward) or previous (on backward).
|
||||||
|
|
||||||
|
if (forward) {
|
||||||
|
const child = focus_node.getChildAt(focus_offset) orelse {
|
||||||
|
if (nextTextNodeAfter(focus_node)) |next| {
|
||||||
|
new_node = next;
|
||||||
|
new_offset = nextWordEnd(next.getData().str(), 0);
|
||||||
|
}
|
||||||
|
return self.applyModify(alter, new_node, new_offset, page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const t = if (isTextNode(child)) child else nextTextNode(child) orelse {
|
||||||
|
return self.applyModify(alter, new_node, new_offset, page);
|
||||||
|
};
|
||||||
|
|
||||||
|
new_node = t;
|
||||||
|
new_offset = nextWordEnd(t.getData().str(), 0);
|
||||||
|
} else {
|
||||||
|
var idx = focus_offset;
|
||||||
|
while (idx > 0) {
|
||||||
|
idx -= 1;
|
||||||
|
const child = focus_node.getChildAt(idx) orelse break;
|
||||||
|
var bottom = child;
|
||||||
|
while (bottom.lastChild()) |c| bottom = c;
|
||||||
|
if (isTextNode(bottom)) {
|
||||||
|
new_node = bottom;
|
||||||
|
new_offset = prevWordStart(bottom.getData().str(), bottom.getLength());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.applyModify(alter, new_node, new_offset, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn applyModify(self: *Selection, alter: ModifyAlter, new_node: *Node, new_offset: u32, page: *Page) !void {
|
||||||
|
switch (alter) {
|
||||||
|
.move => {
|
||||||
|
const new_range = try Range.init(page);
|
||||||
|
try new_range.setStart(new_node, new_offset);
|
||||||
|
try new_range.setEnd(new_node, new_offset);
|
||||||
|
self._range = new_range;
|
||||||
|
self._direction = .none;
|
||||||
|
try dispatchSelectionChangeEvent(page);
|
||||||
|
},
|
||||||
|
.extend => try self.extend(new_node, new_offset, page),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selectAllChildren(self: *Selection, parent: *Node, page: *Page) !void {
|
pub fn selectAllChildren(self: *Selection, parent: *Node, page: *Page) !void {
|
||||||
|
|||||||
@@ -308,12 +308,10 @@ pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
|
|||||||
.cancelable = true,
|
.cancelable = true,
|
||||||
}, page);
|
}, page);
|
||||||
|
|
||||||
const event = error_event.asEvent();
|
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
|
|
||||||
// Invoke window.onerror callback if set (per WHATWG spec, this is called
|
// Invoke window.onerror callback if set (per WHATWG spec, this is called
|
||||||
// with 5 arguments: message, source, lineno, colno, error)
|
// with 5 arguments: message, source, lineno, colno, error)
|
||||||
// If it returns true, the event is cancelled.
|
// If it returns true, the event is cancelled.
|
||||||
|
var prevent_default = false;
|
||||||
if (self._on_error) |on_error| {
|
if (self._on_error) |on_error| {
|
||||||
var ls: js.Local.Scope = undefined;
|
var ls: js.Local.Scope = undefined;
|
||||||
page.js.localScope(&ls);
|
page.js.localScope(&ls);
|
||||||
@@ -330,12 +328,12 @@ pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
|
|||||||
|
|
||||||
// Per spec: returning true from onerror cancels the event
|
// Per spec: returning true from onerror cancels the event
|
||||||
if (result) |r| {
|
if (result) |r| {
|
||||||
if (r.isTrue()) {
|
prevent_default = r.isTrue();
|
||||||
event._prevent_default = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const event = error_event.asEvent();
|
||||||
|
event._prevent_default = prevent_default;
|
||||||
try page._event_manager.dispatch(self.asEventTarget(), event);
|
try page._event_manager.dispatch(self.asEventTarget(), event);
|
||||||
|
|
||||||
if (comptime builtin.is_test == false) {
|
if (comptime builtin.is_test == false) {
|
||||||
@@ -398,9 +396,19 @@ pub fn btoa(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
|
|||||||
|
|
||||||
pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
|
pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 {
|
||||||
const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace);
|
const trimmed = std.mem.trim(u8, input, &std.ascii.whitespace);
|
||||||
const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(trimmed) catch return error.InvalidCharacterError;
|
// Forgiving base64 decode per WHATWG spec:
|
||||||
|
// https://infra.spec.whatwg.org/#forgiving-base64-decode
|
||||||
|
// Remove trailing padding to use standard_no_pad decoder
|
||||||
|
const unpadded = std.mem.trimRight(u8, trimmed, "=");
|
||||||
|
|
||||||
|
// Length % 4 == 1 is invalid (can't represent valid base64)
|
||||||
|
if (unpadded.len % 4 == 1) {
|
||||||
|
return error.InvalidCharacterError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded_len = std.base64.standard_no_pad.Decoder.calcSizeForSlice(unpadded) catch return error.InvalidCharacterError;
|
||||||
const decoded = try page.call_arena.alloc(u8, decoded_len);
|
const decoded = try page.call_arena.alloc(u8, decoded_len);
|
||||||
std.base64.standard.Decoder.decode(decoded, trimmed) catch return error.InvalidCharacterError;
|
std.base64.standard_no_pad.Decoder.decode(decoded, unpadded) catch return error.InvalidCharacterError;
|
||||||
return decoded;
|
return decoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +486,6 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, p);
|
const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, p);
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
try p._event_manager.dispatch(p.document.asEventTarget(), event);
|
try p._event_manager.dispatch(p.document.asEventTarget(), event);
|
||||||
|
|
||||||
pos.state = .end;
|
pos.state = .end;
|
||||||
@@ -506,7 +513,6 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
|
|||||||
.done => return null,
|
.done => return null,
|
||||||
}
|
}
|
||||||
const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, p);
|
const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, p);
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
try p._event_manager.dispatch(p.document.asEventTarget(), event);
|
try p._event_manager.dispatch(p.document.asEventTarget(), event);
|
||||||
|
|
||||||
pos.state = .done;
|
pos.state = .done;
|
||||||
@@ -519,6 +525,24 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scrollBy(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
|
||||||
|
// The scroll is relative to the current position. So compute to new
|
||||||
|
// absolute position.
|
||||||
|
var absx: i32 = undefined;
|
||||||
|
var absy: i32 = undefined;
|
||||||
|
switch (opts) {
|
||||||
|
.x => |x| {
|
||||||
|
absx = @as(i32, @intCast(self._scroll_pos.x)) + x;
|
||||||
|
absy = @as(i32, @intCast(self._scroll_pos.y)) + (y orelse 0);
|
||||||
|
},
|
||||||
|
.opts => |o| {
|
||||||
|
absx = @as(i32, @intCast(self._scroll_pos.x)) + o.left;
|
||||||
|
absy = @as(i32, @intCast(self._scroll_pos.y)) + o.top;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return self.scrollTo(.{ .x = absx }, absy, page);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection, page: *Page) !void {
|
pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection, page: *Page) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.js, "unhandled rejection", .{
|
log.debug(.js, "unhandled rejection", .{
|
||||||
@@ -527,11 +551,10 @@ pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var event = (try @import("event/PromiseRejectionEvent.zig").init("unhandledrejection", .{
|
const event = (try @import("event/PromiseRejectionEvent.zig").init("unhandledrejection", .{
|
||||||
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
||||||
.promise = try rejection.promise().temp(),
|
.promise = try rejection.promise().temp(),
|
||||||
}, page)).asEvent();
|
}, page)).asEvent();
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
|
|
||||||
try page._event_manager.dispatchWithFunction(
|
try page._event_manager.dispatchWithFunction(
|
||||||
self.asEventTarget(),
|
self.asEventTarget(),
|
||||||
@@ -705,7 +728,6 @@ const PostMessageCallback = struct {
|
|||||||
.bubbles = false,
|
.bubbles = false,
|
||||||
.cancelable = false,
|
.cancelable = false,
|
||||||
}, page)).asEvent();
|
}, page)).asEvent();
|
||||||
defer if (!event._v8_handoff) event.deinit(false);
|
|
||||||
try page._event_manager.dispatch(window.asEventTarget(), event);
|
try page._event_manager.dispatch(window.asEventTarget(), event);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -782,7 +804,7 @@ pub const JsApi = struct {
|
|||||||
pub const getSelection = bridge.function(Window.getSelection, .{});
|
pub const getSelection = bridge.function(Window.getSelection, .{});
|
||||||
|
|
||||||
pub const frames = bridge.accessor(Window.getWindow, null, .{});
|
pub const frames = bridge.accessor(Window.getWindow, null, .{});
|
||||||
pub const index = bridge.indexed(Window.getFrame, .{ .null_as_undefined = true });
|
pub const index = bridge.indexed(Window.getFrame, null, .{ .null_as_undefined = true });
|
||||||
pub const length = bridge.accessor(Window.getFramesLength, null, .{});
|
pub const length = bridge.accessor(Window.getFramesLength, null, .{});
|
||||||
pub const scrollX = bridge.accessor(Window.getScrollX, null, .{});
|
pub const scrollX = bridge.accessor(Window.getScrollX, null, .{});
|
||||||
pub const scrollY = bridge.accessor(Window.getScrollY, null, .{});
|
pub const scrollY = bridge.accessor(Window.getScrollY, null, .{});
|
||||||
@@ -790,6 +812,7 @@ pub const JsApi = struct {
|
|||||||
pub const pageYOffset = bridge.accessor(Window.getScrollY, null, .{});
|
pub const pageYOffset = bridge.accessor(Window.getScrollY, null, .{});
|
||||||
pub const scrollTo = bridge.function(Window.scrollTo, .{});
|
pub const scrollTo = bridge.function(Window.scrollTo, .{});
|
||||||
pub const scroll = bridge.function(Window.scrollTo, .{});
|
pub const scroll = bridge.function(Window.scrollTo, .{});
|
||||||
|
pub const scrollBy = bridge.function(Window.scrollBy, .{});
|
||||||
|
|
||||||
// Return false since we don't have secure-context-only APIs implemented
|
// Return false since we don't have secure-context-only APIs implemented
|
||||||
// (webcam, geolocation, clipboard, etc.)
|
// (webcam, geolocation, clipboard, etc.)
|
||||||
@@ -807,7 +830,7 @@ pub const JsApi = struct {
|
|||||||
|
|
||||||
pub const alert = bridge.function(struct {
|
pub const alert = bridge.function(struct {
|
||||||
fn alert(_: *const Window, _: ?[]const u8) void {}
|
fn alert(_: *const Window, _: ?[]const u8) void {}
|
||||||
}.alert, .{});
|
}.alert, .{ .noop = true });
|
||||||
pub const confirm = bridge.function(struct {
|
pub const confirm = bridge.function(struct {
|
||||||
fn confirm(_: *const Window, _: ?[]const u8) bool {
|
fn confirm(_: *const Window, _: ?[]const u8) bool {
|
||||||
return false;
|
return false;
|
||||||
@@ -824,3 +847,7 @@ const testing = @import("../../testing.zig");
|
|||||||
test "WebApi: Window" {
|
test "WebApi: Window" {
|
||||||
try testing.htmlRunner("window", .{});
|
try testing.htmlRunner("window", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "WebApi: Window scroll" {
|
||||||
|
try testing.htmlRunner("window_scroll.html", .{});
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ const dump = @import("../dump.zig");
|
|||||||
|
|
||||||
const XMLSerializer = @This();
|
const XMLSerializer = @This();
|
||||||
|
|
||||||
|
// Padding to avoid zero-size struct, which causes identity_map pointer collisions.
|
||||||
|
_pad: bool = false,
|
||||||
|
|
||||||
pub fn init() XMLSerializer {
|
pub fn init() XMLSerializer {
|
||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ pub fn init(page: *Page) !*Animation {
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Animation, _: bool) void {
|
pub fn deinit(self: *Animation, _: bool, page: *Page) void {
|
||||||
self._page.releaseArena(self._arena);
|
page.releaseArena(self._arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play(self: *Animation, page: *Page) !void {
|
pub fn play(self: *Animation, page: *Page) !void {
|
||||||
|
|||||||
@@ -47,46 +47,6 @@ pub fn setFillStyle(
|
|||||||
self._fill_style = color.RGBA.parse(value) catch self._fill_style;
|
self._fill_style = color.RGBA.parse(value) catch self._fill_style;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getGlobalAlpha(_: *const CanvasRenderingContext2D) f64 {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getGlobalCompositeOperation(_: *const CanvasRenderingContext2D) []const u8 {
|
|
||||||
return "source-over";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getStrokeStyle(_: *const CanvasRenderingContext2D) []const u8 {
|
|
||||||
return "#000000";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getLineWidth(_: *const CanvasRenderingContext2D) f64 {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getLineCap(_: *const CanvasRenderingContext2D) []const u8 {
|
|
||||||
return "butt";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getLineJoin(_: *const CanvasRenderingContext2D) []const u8 {
|
|
||||||
return "miter";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getMiterLimit(_: *const CanvasRenderingContext2D) f64 {
|
|
||||||
return 10.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getFont(_: *const CanvasRenderingContext2D) []const u8 {
|
|
||||||
return "10px sans-serif";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getTextAlign(_: *const CanvasRenderingContext2D) []const u8 {
|
|
||||||
return "start";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getTextBaseline(_: *const CanvasRenderingContext2D) []const u8 {
|
|
||||||
return "alphabetic";
|
|
||||||
}
|
|
||||||
|
|
||||||
const WidthOrImageData = union(enum) {
|
const WidthOrImageData = union(enum) {
|
||||||
width: u32,
|
width: u32,
|
||||||
image_data: *ImageData,
|
image_data: *ImageData,
|
||||||
@@ -113,7 +73,6 @@ pub fn createImageData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn putImageData(_: *const CanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}
|
pub fn putImageData(_: *const CanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}
|
||||||
|
|
||||||
pub fn save(_: *CanvasRenderingContext2D) void {}
|
pub fn save(_: *CanvasRenderingContext2D) void {}
|
||||||
pub fn restore(_: *CanvasRenderingContext2D) void {}
|
pub fn restore(_: *CanvasRenderingContext2D) void {}
|
||||||
pub fn scale(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}
|
pub fn scale(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}
|
||||||
@@ -122,13 +81,7 @@ pub fn translate(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}
|
|||||||
pub fn transform(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
|
pub fn transform(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
pub fn setTransform(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
|
pub fn setTransform(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
pub fn resetTransform(_: *CanvasRenderingContext2D) void {}
|
pub fn resetTransform(_: *CanvasRenderingContext2D) void {}
|
||||||
pub fn setGlobalAlpha(_: *CanvasRenderingContext2D, _: f64) void {}
|
|
||||||
pub fn setGlobalCompositeOperation(_: *CanvasRenderingContext2D, _: []const u8) void {}
|
|
||||||
pub fn setStrokeStyle(_: *CanvasRenderingContext2D, _: []const u8) void {}
|
pub fn setStrokeStyle(_: *CanvasRenderingContext2D, _: []const u8) void {}
|
||||||
pub fn setLineWidth(_: *CanvasRenderingContext2D, _: f64) void {}
|
|
||||||
pub fn setLineCap(_: *CanvasRenderingContext2D, _: []const u8) void {}
|
|
||||||
pub fn setLineJoin(_: *CanvasRenderingContext2D, _: []const u8) void {}
|
|
||||||
pub fn setMiterLimit(_: *CanvasRenderingContext2D, _: f64) void {}
|
|
||||||
pub fn clearRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
|
pub fn clearRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
pub fn fillRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
|
pub fn fillRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
pub fn strokeRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
|
pub fn strokeRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
@@ -144,9 +97,6 @@ pub fn rect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {
|
|||||||
pub fn fill(_: *CanvasRenderingContext2D) void {}
|
pub fn fill(_: *CanvasRenderingContext2D) void {}
|
||||||
pub fn stroke(_: *CanvasRenderingContext2D) void {}
|
pub fn stroke(_: *CanvasRenderingContext2D) void {}
|
||||||
pub fn clip(_: *CanvasRenderingContext2D) void {}
|
pub fn clip(_: *CanvasRenderingContext2D) void {}
|
||||||
pub fn setFont(_: *CanvasRenderingContext2D, _: []const u8) void {}
|
|
||||||
pub fn setTextAlign(_: *CanvasRenderingContext2D, _: []const u8) void {}
|
|
||||||
pub fn setTextBaseline(_: *CanvasRenderingContext2D, _: []const u8) void {}
|
|
||||||
pub fn fillText(_: *CanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
|
pub fn fillText(_: *CanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
|
||||||
pub fn strokeText(_: *CanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
|
pub fn strokeText(_: *CanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
|
||||||
|
|
||||||
@@ -160,53 +110,46 @@ pub const JsApi = struct {
|
|||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
|
pub const font = bridge.property("10px sans-serif", .{ .template = false, .readonly = false });
|
||||||
pub const putImageData = bridge.function(CanvasRenderingContext2D.putImageData, .{});
|
pub const globalAlpha = bridge.property(1.0, .{ .template = false, .readonly = false });
|
||||||
|
pub const globalCompositeOperation = bridge.property("source-over", .{ .template = false, .readonly = false });
|
||||||
pub const save = bridge.function(CanvasRenderingContext2D.save, .{});
|
pub const strokeStyle = bridge.property("#000000", .{ .template = false, .readonly = false });
|
||||||
pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{});
|
pub const lineWidth = bridge.property(1.0, .{ .template = false, .readonly = false });
|
||||||
|
pub const lineCap = bridge.property("butt", .{ .template = false, .readonly = false });
|
||||||
pub const scale = bridge.function(CanvasRenderingContext2D.scale, .{});
|
pub const lineJoin = bridge.property("miter", .{ .template = false, .readonly = false });
|
||||||
pub const rotate = bridge.function(CanvasRenderingContext2D.rotate, .{});
|
pub const miterLimit = bridge.property(10.0, .{ .template = false, .readonly = false });
|
||||||
pub const translate = bridge.function(CanvasRenderingContext2D.translate, .{});
|
pub const textAlign = bridge.property("start", .{ .template = false, .readonly = false });
|
||||||
pub const transform = bridge.function(CanvasRenderingContext2D.transform, .{});
|
pub const textBaseline = bridge.property("alphabetic", .{ .template = false, .readonly = false });
|
||||||
pub const setTransform = bridge.function(CanvasRenderingContext2D.setTransform, .{});
|
|
||||||
pub const resetTransform = bridge.function(CanvasRenderingContext2D.resetTransform, .{});
|
|
||||||
|
|
||||||
pub const globalAlpha = bridge.accessor(CanvasRenderingContext2D.getGlobalAlpha, CanvasRenderingContext2D.setGlobalAlpha, .{});
|
|
||||||
pub const globalCompositeOperation = bridge.accessor(CanvasRenderingContext2D.getGlobalCompositeOperation, CanvasRenderingContext2D.setGlobalCompositeOperation, .{});
|
|
||||||
|
|
||||||
pub const fillStyle = bridge.accessor(CanvasRenderingContext2D.getFillStyle, CanvasRenderingContext2D.setFillStyle, .{});
|
pub const fillStyle = bridge.accessor(CanvasRenderingContext2D.getFillStyle, CanvasRenderingContext2D.setFillStyle, .{});
|
||||||
pub const strokeStyle = bridge.accessor(CanvasRenderingContext2D.getStrokeStyle, CanvasRenderingContext2D.setStrokeStyle, .{});
|
pub const createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
|
||||||
|
|
||||||
pub const lineWidth = bridge.accessor(CanvasRenderingContext2D.getLineWidth, CanvasRenderingContext2D.setLineWidth, .{});
|
pub const putImageData = bridge.function(CanvasRenderingContext2D.putImageData, .{ .noop = true });
|
||||||
pub const lineCap = bridge.accessor(CanvasRenderingContext2D.getLineCap, CanvasRenderingContext2D.setLineCap, .{});
|
pub const save = bridge.function(CanvasRenderingContext2D.save, .{ .noop = true });
|
||||||
pub const lineJoin = bridge.accessor(CanvasRenderingContext2D.getLineJoin, CanvasRenderingContext2D.setLineJoin, .{});
|
pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{ .noop = true });
|
||||||
pub const miterLimit = bridge.accessor(CanvasRenderingContext2D.getMiterLimit, CanvasRenderingContext2D.setMiterLimit, .{});
|
pub const scale = bridge.function(CanvasRenderingContext2D.scale, .{ .noop = true });
|
||||||
|
pub const rotate = bridge.function(CanvasRenderingContext2D.rotate, .{ .noop = true });
|
||||||
pub const clearRect = bridge.function(CanvasRenderingContext2D.clearRect, .{});
|
pub const translate = bridge.function(CanvasRenderingContext2D.translate, .{ .noop = true });
|
||||||
pub const fillRect = bridge.function(CanvasRenderingContext2D.fillRect, .{});
|
pub const transform = bridge.function(CanvasRenderingContext2D.transform, .{ .noop = true });
|
||||||
pub const strokeRect = bridge.function(CanvasRenderingContext2D.strokeRect, .{});
|
pub const setTransform = bridge.function(CanvasRenderingContext2D.setTransform, .{ .noop = true });
|
||||||
|
pub const resetTransform = bridge.function(CanvasRenderingContext2D.resetTransform, .{ .noop = true });
|
||||||
pub const beginPath = bridge.function(CanvasRenderingContext2D.beginPath, .{});
|
pub const clearRect = bridge.function(CanvasRenderingContext2D.clearRect, .{ .noop = true });
|
||||||
pub const closePath = bridge.function(CanvasRenderingContext2D.closePath, .{});
|
pub const fillRect = bridge.function(CanvasRenderingContext2D.fillRect, .{ .noop = true });
|
||||||
pub const moveTo = bridge.function(CanvasRenderingContext2D.moveTo, .{});
|
pub const strokeRect = bridge.function(CanvasRenderingContext2D.strokeRect, .{ .noop = true });
|
||||||
pub const lineTo = bridge.function(CanvasRenderingContext2D.lineTo, .{});
|
pub const beginPath = bridge.function(CanvasRenderingContext2D.beginPath, .{ .noop = true });
|
||||||
pub const quadraticCurveTo = bridge.function(CanvasRenderingContext2D.quadraticCurveTo, .{});
|
pub const closePath = bridge.function(CanvasRenderingContext2D.closePath, .{ .noop = true });
|
||||||
pub const bezierCurveTo = bridge.function(CanvasRenderingContext2D.bezierCurveTo, .{});
|
pub const moveTo = bridge.function(CanvasRenderingContext2D.moveTo, .{ .noop = true });
|
||||||
pub const arc = bridge.function(CanvasRenderingContext2D.arc, .{});
|
pub const lineTo = bridge.function(CanvasRenderingContext2D.lineTo, .{ .noop = true });
|
||||||
pub const arcTo = bridge.function(CanvasRenderingContext2D.arcTo, .{});
|
pub const quadraticCurveTo = bridge.function(CanvasRenderingContext2D.quadraticCurveTo, .{ .noop = true });
|
||||||
pub const rect = bridge.function(CanvasRenderingContext2D.rect, .{});
|
pub const bezierCurveTo = bridge.function(CanvasRenderingContext2D.bezierCurveTo, .{ .noop = true });
|
||||||
|
pub const arc = bridge.function(CanvasRenderingContext2D.arc, .{ .noop = true });
|
||||||
pub const fill = bridge.function(CanvasRenderingContext2D.fill, .{});
|
pub const arcTo = bridge.function(CanvasRenderingContext2D.arcTo, .{ .noop = true });
|
||||||
pub const stroke = bridge.function(CanvasRenderingContext2D.stroke, .{});
|
pub const rect = bridge.function(CanvasRenderingContext2D.rect, .{ .noop = true });
|
||||||
pub const clip = bridge.function(CanvasRenderingContext2D.clip, .{});
|
pub const fill = bridge.function(CanvasRenderingContext2D.fill, .{ .noop = true });
|
||||||
|
pub const stroke = bridge.function(CanvasRenderingContext2D.stroke, .{ .noop = true });
|
||||||
pub const font = bridge.accessor(CanvasRenderingContext2D.getFont, CanvasRenderingContext2D.setFont, .{});
|
pub const clip = bridge.function(CanvasRenderingContext2D.clip, .{ .noop = true });
|
||||||
pub const textAlign = bridge.accessor(CanvasRenderingContext2D.getTextAlign, CanvasRenderingContext2D.setTextAlign, .{});
|
pub const fillText = bridge.function(CanvasRenderingContext2D.fillText, .{ .noop = true });
|
||||||
pub const textBaseline = bridge.accessor(CanvasRenderingContext2D.getTextBaseline, CanvasRenderingContext2D.setTextBaseline, .{});
|
pub const strokeText = bridge.function(CanvasRenderingContext2D.strokeText, .{ .noop = true });
|
||||||
pub const fillText = bridge.function(CanvasRenderingContext2D.fillText, .{});
|
|
||||||
pub const strokeText = bridge.function(CanvasRenderingContext2D.strokeText, .{});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../../testing.zig");
|
const testing = @import("../../../testing.zig");
|
||||||
|
|||||||
105
src/browser/webapi/canvas/OffscreenCanvas.zig
Normal file
105
src/browser/webapi/canvas/OffscreenCanvas.zig
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// Copyright (C) 2023-2026 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 Page = @import("../../Page.zig");
|
||||||
|
|
||||||
|
const Blob = @import("../Blob.zig");
|
||||||
|
const OffscreenCanvasRenderingContext2D = @import("OffscreenCanvasRenderingContext2D.zig");
|
||||||
|
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
|
||||||
|
const OffscreenCanvas = @This();
|
||||||
|
|
||||||
|
pub const _prototype_root = true;
|
||||||
|
|
||||||
|
_width: u32,
|
||||||
|
_height: u32,
|
||||||
|
|
||||||
|
/// Since there's no base class rendering contextes inherit from,
|
||||||
|
/// we're using tagged union.
|
||||||
|
const DrawingContext = union(enum) {
|
||||||
|
@"2d": *OffscreenCanvasRenderingContext2D,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn constructor(width: u32, height: u32, page: *Page) !*OffscreenCanvas {
|
||||||
|
return page._factory.create(OffscreenCanvas{
|
||||||
|
._width = width,
|
||||||
|
._height = height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getWidth(self: *const OffscreenCanvas) u32 {
|
||||||
|
return self._width;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setWidth(self: *OffscreenCanvas, value: u32) void {
|
||||||
|
self._width = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getHeight(self: *const OffscreenCanvas) u32 {
|
||||||
|
return self._height;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setHeight(self: *OffscreenCanvas, value: u32) void {
|
||||||
|
self._height = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, page: *Page) !?DrawingContext {
|
||||||
|
if (std.mem.eql(u8, context_type, "2d")) {
|
||||||
|
const ctx = try page._factory.create(OffscreenCanvasRenderingContext2D{});
|
||||||
|
return .{ .@"2d" = ctx };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a Promise that resolves to a Blob containing the image.
|
||||||
|
/// Since we have no actual rendering, this returns an empty blob.
|
||||||
|
pub fn convertToBlob(_: *OffscreenCanvas, page: *Page) !js.Promise {
|
||||||
|
const blob = try Blob.init(null, null, page);
|
||||||
|
return page.js.local.?.resolvePromise(blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an ImageBitmap with the rendered content (stub).
|
||||||
|
pub fn transferToImageBitmap(_: *OffscreenCanvas) ?void {
|
||||||
|
// ImageBitmap not implemented yet, return null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(OffscreenCanvas);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "OffscreenCanvas";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const constructor = bridge.constructor(OffscreenCanvas.constructor, .{});
|
||||||
|
pub const width = bridge.accessor(OffscreenCanvas.getWidth, OffscreenCanvas.setWidth, .{});
|
||||||
|
pub const height = bridge.accessor(OffscreenCanvas.getHeight, OffscreenCanvas.setHeight, .{});
|
||||||
|
pub const getContext = bridge.function(OffscreenCanvas.getContext, .{});
|
||||||
|
pub const convertToBlob = bridge.function(OffscreenCanvas.convertToBlob, .{});
|
||||||
|
pub const transferToImageBitmap = bridge.function(OffscreenCanvas.transferToImageBitmap, .{});
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../../testing.zig");
|
||||||
|
test "WebApi: OffscreenCanvas" {
|
||||||
|
try testing.htmlRunner("canvas/offscreen_canvas.html", .{});
|
||||||
|
}
|
||||||
152
src/browser/webapi/canvas/OffscreenCanvasRenderingContext2D.zig
Normal file
152
src/browser/webapi/canvas/OffscreenCanvasRenderingContext2D.zig
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
// 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 color = @import("../../color.zig");
|
||||||
|
const Page = @import("../../Page.zig");
|
||||||
|
|
||||||
|
const ImageData = @import("../ImageData.zig");
|
||||||
|
|
||||||
|
/// This class doesn't implement a `constructor`.
|
||||||
|
/// It can be obtained with a call to `OffscreenCanvas#getContext`.
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvasRenderingContext2D
|
||||||
|
const OffscreenCanvasRenderingContext2D = @This();
|
||||||
|
/// Fill color.
|
||||||
|
/// TODO: Add support for `CanvasGradient` and `CanvasPattern`.
|
||||||
|
_fill_style: color.RGBA = color.RGBA.Named.black,
|
||||||
|
|
||||||
|
pub fn getFillStyle(self: *const OffscreenCanvasRenderingContext2D, page: *Page) ![]const u8 {
|
||||||
|
var w = std.Io.Writer.Allocating.init(page.call_arena);
|
||||||
|
try self._fill_style.format(&w.writer);
|
||||||
|
return w.written();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setFillStyle(
|
||||||
|
self: *OffscreenCanvasRenderingContext2D,
|
||||||
|
value: []const u8,
|
||||||
|
) !void {
|
||||||
|
// Prefer the same fill_style if fails.
|
||||||
|
self._fill_style = color.RGBA.parse(value) catch self._fill_style;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WidthOrImageData = union(enum) {
|
||||||
|
width: u32,
|
||||||
|
image_data: *ImageData,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn createImageData(
|
||||||
|
_: *const OffscreenCanvasRenderingContext2D,
|
||||||
|
width_or_image_data: WidthOrImageData,
|
||||||
|
/// If `ImageData` variant preferred, this is null.
|
||||||
|
maybe_height: ?u32,
|
||||||
|
/// Can be used if width and height provided.
|
||||||
|
maybe_settings: ?ImageData.ConstructorSettings,
|
||||||
|
page: *Page,
|
||||||
|
) !*ImageData {
|
||||||
|
switch (width_or_image_data) {
|
||||||
|
.width => |width| {
|
||||||
|
const height = maybe_height orelse return error.TypeError;
|
||||||
|
return ImageData.constructor(width, height, maybe_settings, page);
|
||||||
|
},
|
||||||
|
.image_data => |image_data| {
|
||||||
|
return ImageData.constructor(image_data._width, image_data._height, null, page);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn putImageData(_: *const OffscreenCanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}
|
||||||
|
pub fn save(_: *OffscreenCanvasRenderingContext2D) void {}
|
||||||
|
pub fn restore(_: *OffscreenCanvasRenderingContext2D) void {}
|
||||||
|
pub fn scale(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {}
|
||||||
|
pub fn rotate(_: *OffscreenCanvasRenderingContext2D, _: f64) void {}
|
||||||
|
pub fn translate(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {}
|
||||||
|
pub fn transform(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
|
pub fn setTransform(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
|
pub fn resetTransform(_: *OffscreenCanvasRenderingContext2D) void {}
|
||||||
|
pub fn setStrokeStyle(_: *OffscreenCanvasRenderingContext2D, _: []const u8) void {}
|
||||||
|
pub fn clearRect(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
|
pub fn fillRect(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
|
pub fn strokeRect(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
|
pub fn beginPath(_: *OffscreenCanvasRenderingContext2D) void {}
|
||||||
|
pub fn closePath(_: *OffscreenCanvasRenderingContext2D) void {}
|
||||||
|
pub fn moveTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {}
|
||||||
|
pub fn lineTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {}
|
||||||
|
pub fn quadraticCurveTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
|
pub fn bezierCurveTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
|
pub fn arc(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: ?bool) void {}
|
||||||
|
pub fn arcTo(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
|
pub fn rect(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
|
||||||
|
pub fn fill(_: *OffscreenCanvasRenderingContext2D) void {}
|
||||||
|
pub fn stroke(_: *OffscreenCanvasRenderingContext2D) void {}
|
||||||
|
pub fn clip(_: *OffscreenCanvasRenderingContext2D) void {}
|
||||||
|
pub fn fillText(_: *OffscreenCanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
|
||||||
|
pub fn strokeText(_: *OffscreenCanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(OffscreenCanvasRenderingContext2D);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "OffscreenCanvasRenderingContext2D";
|
||||||
|
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const font = bridge.property("10px sans-serif", .{ .template = false, .readonly = false });
|
||||||
|
pub const globalAlpha = bridge.property(1.0, .{ .template = false, .readonly = false });
|
||||||
|
pub const globalCompositeOperation = bridge.property("source-over", .{ .template = false, .readonly = false });
|
||||||
|
pub const strokeStyle = bridge.property("#000000", .{ .template = false, .readonly = false });
|
||||||
|
pub const lineWidth = bridge.property(1.0, .{ .template = false, .readonly = false });
|
||||||
|
pub const lineCap = bridge.property("butt", .{ .template = false, .readonly = false });
|
||||||
|
pub const lineJoin = bridge.property("miter", .{ .template = false, .readonly = false });
|
||||||
|
pub const miterLimit = bridge.property(10.0, .{ .template = false, .readonly = false });
|
||||||
|
pub const textAlign = bridge.property("start", .{ .template = false, .readonly = false });
|
||||||
|
pub const textBaseline = bridge.property("alphabetic", .{ .template = false, .readonly = false });
|
||||||
|
|
||||||
|
pub const fillStyle = bridge.accessor(OffscreenCanvasRenderingContext2D.getFillStyle, OffscreenCanvasRenderingContext2D.setFillStyle, .{});
|
||||||
|
pub const createImageData = bridge.function(OffscreenCanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
|
||||||
|
|
||||||
|
pub const putImageData = bridge.function(OffscreenCanvasRenderingContext2D.putImageData, .{ .noop = true });
|
||||||
|
pub const save = bridge.function(OffscreenCanvasRenderingContext2D.save, .{ .noop = true });
|
||||||
|
pub const restore = bridge.function(OffscreenCanvasRenderingContext2D.restore, .{ .noop = true });
|
||||||
|
pub const scale = bridge.function(OffscreenCanvasRenderingContext2D.scale, .{ .noop = true });
|
||||||
|
pub const rotate = bridge.function(OffscreenCanvasRenderingContext2D.rotate, .{ .noop = true });
|
||||||
|
pub const translate = bridge.function(OffscreenCanvasRenderingContext2D.translate, .{ .noop = true });
|
||||||
|
pub const transform = bridge.function(OffscreenCanvasRenderingContext2D.transform, .{ .noop = true });
|
||||||
|
pub const setTransform = bridge.function(OffscreenCanvasRenderingContext2D.setTransform, .{ .noop = true });
|
||||||
|
pub const resetTransform = bridge.function(OffscreenCanvasRenderingContext2D.resetTransform, .{ .noop = true });
|
||||||
|
pub const clearRect = bridge.function(OffscreenCanvasRenderingContext2D.clearRect, .{ .noop = true });
|
||||||
|
pub const fillRect = bridge.function(OffscreenCanvasRenderingContext2D.fillRect, .{ .noop = true });
|
||||||
|
pub const strokeRect = bridge.function(OffscreenCanvasRenderingContext2D.strokeRect, .{ .noop = true });
|
||||||
|
pub const beginPath = bridge.function(OffscreenCanvasRenderingContext2D.beginPath, .{ .noop = true });
|
||||||
|
pub const closePath = bridge.function(OffscreenCanvasRenderingContext2D.closePath, .{ .noop = true });
|
||||||
|
pub const moveTo = bridge.function(OffscreenCanvasRenderingContext2D.moveTo, .{ .noop = true });
|
||||||
|
pub const lineTo = bridge.function(OffscreenCanvasRenderingContext2D.lineTo, .{ .noop = true });
|
||||||
|
pub const quadraticCurveTo = bridge.function(OffscreenCanvasRenderingContext2D.quadraticCurveTo, .{ .noop = true });
|
||||||
|
pub const bezierCurveTo = bridge.function(OffscreenCanvasRenderingContext2D.bezierCurveTo, .{ .noop = true });
|
||||||
|
pub const arc = bridge.function(OffscreenCanvasRenderingContext2D.arc, .{ .noop = true });
|
||||||
|
pub const arcTo = bridge.function(OffscreenCanvasRenderingContext2D.arcTo, .{ .noop = true });
|
||||||
|
pub const rect = bridge.function(OffscreenCanvasRenderingContext2D.rect, .{ .noop = true });
|
||||||
|
pub const fill = bridge.function(OffscreenCanvasRenderingContext2D.fill, .{ .noop = true });
|
||||||
|
pub const stroke = bridge.function(OffscreenCanvasRenderingContext2D.stroke, .{ .noop = true });
|
||||||
|
pub const clip = bridge.function(OffscreenCanvasRenderingContext2D.clip, .{ .noop = true });
|
||||||
|
pub const fillText = bridge.function(OffscreenCanvasRenderingContext2D.fillText, .{ .noop = true });
|
||||||
|
pub const strokeText = bridge.function(OffscreenCanvasRenderingContext2D.strokeText, .{ .noop = true });
|
||||||
|
};
|
||||||
@@ -122,14 +122,6 @@ pub const Extension = union(enum) {
|
|||||||
pub const UNMASKED_VENDOR_WEBGL: u64 = 0x9245;
|
pub const UNMASKED_VENDOR_WEBGL: u64 = 0x9245;
|
||||||
pub const UNMASKED_RENDERER_WEBGL: u64 = 0x9246;
|
pub const UNMASKED_RENDERER_WEBGL: u64 = 0x9246;
|
||||||
|
|
||||||
pub fn getUnmaskedVendorWebGL(_: *const WEBGL_debug_renderer_info) u64 {
|
|
||||||
return UNMASKED_VENDOR_WEBGL;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getUnmaskedRendererWebGL(_: *const WEBGL_debug_renderer_info) u64 {
|
|
||||||
return UNMASKED_RENDERER_WEBGL;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(WEBGL_debug_renderer_info);
|
pub const bridge = js.Bridge(WEBGL_debug_renderer_info);
|
||||||
|
|
||||||
@@ -140,8 +132,8 @@ pub const Extension = union(enum) {
|
|||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const UNMASKED_VENDOR_WEBGL = bridge.accessor(WEBGL_debug_renderer_info.getUnmaskedVendorWebGL, null, .{});
|
pub const UNMASKED_VENDOR_WEBGL = bridge.property(WEBGL_debug_renderer_info.UNMASKED_VENDOR_WEBGL, .{ .template = false, .readonly = true });
|
||||||
pub const UNMASKED_RENDERER_WEBGL = bridge.accessor(WEBGL_debug_renderer_info.getUnmaskedRendererWebGL, null, .{});
|
pub const UNMASKED_RENDERER_WEBGL = bridge.property(WEBGL_debug_renderer_info.UNMASKED_RENDERER_WEBGL, .{ .template = false, .readonly = true });
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -160,8 +152,8 @@ pub const Extension = union(enum) {
|
|||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const loseContext = bridge.function(WEBGL_lose_context.loseContext, .{});
|
pub const loseContext = bridge.function(WEBGL_lose_context.loseContext, .{ .noop = true });
|
||||||
pub const restoreContext = bridge.function(WEBGL_lose_context.restoreContext, .{});
|
pub const restoreContext = bridge.function(WEBGL_lose_context.restoreContext, .{ .noop = true });
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +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 String = @import("../../../string.zig").String;
|
||||||
const js = @import("../../js/js.zig");
|
const js = @import("../../js/js.zig");
|
||||||
const Page = @import("../../Page.zig");
|
const Page = @import("../../Page.zig");
|
||||||
const CData = @import("../CData.zig");
|
const CData = @import("../CData.zig");
|
||||||
@@ -30,11 +31,11 @@ pub fn init(str: ?js.NullableString, page: *Page) !*Text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getWholeText(self: *Text) []const u8 {
|
pub fn getWholeText(self: *Text) []const u8 {
|
||||||
return self._proto._data;
|
return self._proto._data.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn splitText(self: *Text, offset: usize, page: *Page) !*Text {
|
pub fn splitText(self: *Text, offset: usize, page: *Page) !*Text {
|
||||||
const data = self._proto._data;
|
const data = self._proto._data.str();
|
||||||
|
|
||||||
const byte_offset = CData.utf16OffsetToUtf8(data, offset) catch return error.IndexSizeError;
|
const byte_offset = CData.utf16OffsetToUtf8(data, offset) catch return error.IndexSizeError;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user