mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 15:40:04 +00:00
Compare commits
1 Commits
v0.2.3
...
zigdom-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6e8aff2c9 |
14
.github/actions/install/action.yml
vendored
14
.github/actions/install/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.2.8'
|
||||
default: 'v0.1.37'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
@@ -22,10 +22,6 @@ inputs:
|
||||
description: 'cache dir to use'
|
||||
required: false
|
||||
default: '~/.cache'
|
||||
debug:
|
||||
description: 'enable v8 pre-built debug version, only available for linux x86_64'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -36,7 +32,7 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y wget xz-utils ca-certificates clang make git
|
||||
sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
|
||||
|
||||
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
||||
- uses: mlugg/setup-zig@v2
|
||||
@@ -51,17 +47,17 @@ runs:
|
||||
cache-name: cache-v8
|
||||
with:
|
||||
path: ${{ inputs.cache-dir }}/v8
|
||||
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}.a
|
||||
|
||||
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ${{ inputs.cache-dir }}/v8
|
||||
|
||||
wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||
wget -O ${{ inputs.cache-dir }}/v8/libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${{ inputs.zig-v8 }}/libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a
|
||||
|
||||
- name: install v8
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p v8
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8${{inputs.debug == 'true' && '_debug' || '' }}.a
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8.a
|
||||
|
||||
48
.github/workflows/build.yml
vendored
48
.github/workflows/build.yml
vendored
@@ -5,12 +5,8 @@ env:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
|
||||
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
||||
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
schedule:
|
||||
- cron: "2 2 * * *"
|
||||
|
||||
@@ -27,7 +23,7 @@ jobs:
|
||||
OS: linux
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -40,12 +36,10 @@ jobs:
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: v8 snapshot
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||
mode: 'release'
|
||||
|
||||
- name: zig build
|
||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
@@ -60,8 +54,7 @@ jobs:
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: ${{ env.RELEASE }}
|
||||
makeLatest: true
|
||||
tag: nightly
|
||||
|
||||
build-linux-aarch64:
|
||||
env:
|
||||
@@ -69,7 +62,7 @@ jobs:
|
||||
OS: linux
|
||||
|
||||
runs-on: ubuntu-22.04-arm
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -82,12 +75,10 @@ jobs:
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: v8 snapshot
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||
mode: 'release'
|
||||
|
||||
- name: zig build
|
||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
@@ -102,8 +93,7 @@ jobs:
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: ${{ env.RELEASE }}
|
||||
makeLatest: true
|
||||
tag: nightly
|
||||
|
||||
build-macos-aarch64:
|
||||
env:
|
||||
@@ -113,7 +103,7 @@ jobs:
|
||||
# macos-14 runs on arm CPU. see
|
||||
# https://github.com/actions/runner-images?tab=readme-ov-file
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -126,12 +116,10 @@ jobs:
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: v8 snapshot
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||
mode: 'release'
|
||||
|
||||
- name: zig build
|
||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
@@ -146,8 +134,7 @@ jobs:
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: ${{ env.RELEASE }}
|
||||
makeLatest: true
|
||||
tag: nightly
|
||||
|
||||
build-macos-x86_64:
|
||||
env:
|
||||
@@ -155,7 +142,7 @@ jobs:
|
||||
OS: macos
|
||||
|
||||
runs-on: macos-14-large
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -168,12 +155,10 @@ jobs:
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: v8 snapshot
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||
mode: 'release'
|
||||
|
||||
- name: zig build
|
||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
@@ -188,5 +173,4 @@ jobs:
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: ${{ env.RELEASE }}
|
||||
makeLatest: true
|
||||
tag: nightly
|
||||
|
||||
22
.github/workflows/e2e-test.yml
vendored
22
.github/workflows/e2e-test.yml
vendored
@@ -56,6 +56,8 @@ jobs:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
with:
|
||||
mode: 'release'
|
||||
|
||||
- name: zig build release
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
@@ -122,8 +124,8 @@ jobs:
|
||||
needs: zig-build-release
|
||||
|
||||
env:
|
||||
MAX_MEMORY: 26000
|
||||
MAX_AVG_DURATION: 17
|
||||
MAX_MEMORY: 28000
|
||||
MAX_AVG_DURATION: 23
|
||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||
|
||||
# use a self host runner.
|
||||
@@ -230,19 +232,3 @@ jobs:
|
||||
|
||||
- name: format and send json result
|
||||
run: /perf-fmt hyperfine ${{ github.sha }} hyperfine.json
|
||||
|
||||
browser-fetch:
|
||||
name: browser fetch
|
||||
needs: zig-build-release
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: lightpanda-build-release
|
||||
|
||||
- run: chmod a+x ./lightpanda
|
||||
|
||||
- run: ./lightpanda fetch https://demo-browser.lightpanda.io/campfire-commerce/
|
||||
|
||||
7
.github/workflows/wpt.yml
vendored
7
.github/workflows/wpt.yml
vendored
@@ -30,11 +30,8 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- name: build wpt
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -- version
|
||||
|
||||
- name: run test with json output
|
||||
run: zig-out/bin/lightpanda-wpt --json > wpt.json
|
||||
- name: json output
|
||||
run: zig build wpt -- --json > wpt.json
|
||||
|
||||
- name: write commit
|
||||
run: |
|
||||
|
||||
49
.github/workflows/zig-test.yml
vendored
49
.github/workflows/zig-test.yml
vendored
@@ -12,7 +12,8 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- "build.zig"
|
||||
- "src/**"
|
||||
- "src/**/*.zig"
|
||||
- "src/*.zig"
|
||||
- "vendor/zig-js-runtime"
|
||||
- ".github/**"
|
||||
- "vendor/**"
|
||||
@@ -37,25 +38,51 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
zig-test-debug:
|
||||
name: zig test using v8 in debug mode
|
||||
timeout-minutes: 15
|
||||
zig-build-dev:
|
||||
name: zig build dev
|
||||
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
with:
|
||||
debug: true
|
||||
|
||||
- name: zig build test
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8_debug.a -Dtsan=true test
|
||||
- name: zig build debug
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lightpanda-build-dev
|
||||
path: |
|
||||
zig-out/bin/lightpanda
|
||||
retention-days: 1
|
||||
|
||||
browser-fetch:
|
||||
name: browser fetch
|
||||
needs: zig-build-dev
|
||||
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: lightpanda-build-dev
|
||||
|
||||
- run: chmod a+x ./lightpanda
|
||||
|
||||
- run: ./lightpanda fetch https://httpbin.io/xhr/get
|
||||
|
||||
zig-test:
|
||||
name: zig test
|
||||
@@ -76,7 +103,7 @@ jobs:
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- name: zig build test
|
||||
run: METRICS=true zig build -Dprebuilt_v8_path=v8/libc_v8.a test > bench.json
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a test -- --json > bench.json
|
||||
|
||||
- name: write commit
|
||||
run: |
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@@ -3,12 +3,11 @@ FROM debian:stable-slim
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG V8=14.0.365.4
|
||||
ARG ZIG_V8=v0.2.8
|
||||
ARG ZIG_V8=v0.1.37
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
apt-get install -yq xz-utils ca-certificates \
|
||||
pkg-config libglib2.0-dev \
|
||||
clang make curl git
|
||||
|
||||
# Get Rust
|
||||
@@ -49,16 +48,8 @@ RUN case $TARGETPLATFORM in \
|
||||
mkdir -p v8/ && \
|
||||
mv libc_v8.a v8/libc_v8.a
|
||||
|
||||
# build v8 snapshot
|
||||
RUN zig build -Doptimize=ReleaseFast \
|
||||
-Dprebuilt_v8_path=v8/libc_v8.a \
|
||||
snapshot_creator -- src/snapshot.bin
|
||||
|
||||
# build release
|
||||
RUN zig build -Doptimize=ReleaseFast \
|
||||
-Dsnapshot_path=../../snapshot.bin \
|
||||
-Dprebuilt_v8_path=v8/libc_v8.a \
|
||||
-Dgit_commit=$(git rev-parse --short HEAD)
|
||||
RUN zig build -Doptimize=ReleaseFast -Dprebuilt_v8_path=v8/libc_v8.a -Dgit_commit=$(git rev-parse --short HEAD)
|
||||
|
||||
FROM debian:stable-slim
|
||||
|
||||
|
||||
14
Makefile
14
Makefile
@@ -47,18 +47,12 @@ help:
|
||||
|
||||
# $(ZIG) commands
|
||||
# ------------
|
||||
.PHONY: build build-v8-snapshot build-dev run run-release shell test bench wpt data end2end
|
||||
.PHONY: build build-dev run run-release shell test bench wpt data end2end
|
||||
|
||||
## Build v8 snapshot
|
||||
build-v8-snapshot:
|
||||
@printf "\033[36mBuilding v8 snapshot (release safe)...\033[0m\n"
|
||||
@$(ZIG) build -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||
@printf "\033[33mBuild OK\033[0m\n"
|
||||
|
||||
## Build in release-fast mode
|
||||
build: build-v8-snapshot
|
||||
## Build in release-safe mode
|
||||
build:
|
||||
@printf "\033[36mBuilding (release safe)...\033[0m\n"
|
||||
@$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||
@$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||
@printf "\033[33mBuild OK\033[0m\n"
|
||||
|
||||
## Build in debug mode
|
||||
|
||||
61
README.md
61
README.md
@@ -78,49 +78,23 @@ docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
|
||||
### Dump a URL
|
||||
|
||||
```console
|
||||
./lightpanda fetch --obey_robots --log_format pretty --log_level info https://demo-browser.lightpanda.io/campfire-commerce/
|
||||
./lightpanda fetch --dump https://lightpanda.io
|
||||
```
|
||||
```console
|
||||
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||
disabled = false
|
||||
|
||||
INFO page : navigate . . . . . . . . . . . . . . . . . . . . [+6ms]
|
||||
url = https://demo-browser.lightpanda.io/campfire-commerce/
|
||||
method = GET
|
||||
reason = address_bar
|
||||
body = false
|
||||
req_id = 1
|
||||
|
||||
INFO browser : executing script . . . . . . . . . . . . . . [+118ms]
|
||||
src = https://demo-browser.lightpanda.io/campfire-commerce/script.js
|
||||
kind = javascript
|
||||
cacheable = true
|
||||
|
||||
INFO http : request complete . . . . . . . . . . . . . . . . [+140ms]
|
||||
source = xhr
|
||||
url = https://demo-browser.lightpanda.io/campfire-commerce/json/product.json
|
||||
status = 200
|
||||
len = 4770
|
||||
|
||||
INFO http : request complete . . . . . . . . . . . . . . . . [+141ms]
|
||||
source = fetch
|
||||
url = https://demo-browser.lightpanda.io/campfire-commerce/json/reviews.json
|
||||
status = 200
|
||||
len = 1615
|
||||
info(browser): GET https://lightpanda.io/ http.Status.ok
|
||||
info(browser): fetch script https://api.website.lightpanda.io/js/script.js: http.Status.ok
|
||||
info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeError: Cannot read properties of undefined (reading 'pushState')
|
||||
<!DOCTYPE html>
|
||||
```
|
||||
|
||||
### Start a CDP server
|
||||
|
||||
```console
|
||||
./lightpanda serve --obey_robots --log_format pretty --log_level info --host 127.0.0.1 --port 9222
|
||||
./lightpanda serve --host 127.0.0.1 --port 9222
|
||||
```
|
||||
```console
|
||||
INFO telemetry : telemetry status . . . . . . . . . . . . . [+0ms]
|
||||
disabled = false
|
||||
|
||||
INFO app : server running . . . . . . . . . . . . . . . . . [+0ms]
|
||||
address = 127.0.0.1:9222
|
||||
info(websocket): starting blocking worker to listen on 127.0.0.1:9222
|
||||
info(server): accepting new conn...
|
||||
```
|
||||
|
||||
Once the CDP server started, you can run a Puppeteer script by configuring the
|
||||
@@ -141,7 +115,7 @@ const context = await browser.createBrowserContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
// Dump all the links from the page.
|
||||
await page.goto('https://demo-browser.lightpanda.io/amiibo/', {waitUntil: "networkidle0"});
|
||||
await page.goto('https://wikipedia.com/');
|
||||
|
||||
const links = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll('a')).map(row => {
|
||||
@@ -182,7 +156,6 @@ Here are the key features we have implemented:
|
||||
- [x] Custom HTTP headers
|
||||
- [x] Proxy support
|
||||
- [x] Network interception
|
||||
- [x] Respect `robots.txt` with option `--obey_robots`
|
||||
|
||||
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||
|
||||
@@ -205,7 +178,6 @@ For **Debian/Ubuntu based Linux**:
|
||||
|
||||
```
|
||||
sudo apt install xz-utils ca-certificates \
|
||||
pkg-config libglib2.0-dev \
|
||||
clang make curl git
|
||||
```
|
||||
You also need to [install Rust](https://rust-lang.org/tools/install/).
|
||||
@@ -239,23 +211,6 @@ env.
|
||||
|
||||
But you can directly use the zig command: `zig build run`.
|
||||
|
||||
#### Embed v8 snapshot
|
||||
|
||||
Lighpanda uses v8 snapshot. By default, it is created on startup but you can
|
||||
embed it by using the following commands:
|
||||
|
||||
Generate the snapshot.
|
||||
```
|
||||
zig build snapshot_creator -- src/snapshot.bin
|
||||
```
|
||||
|
||||
Build using the snapshot binary.
|
||||
```
|
||||
zig build -Dsnapshot_path=../../snapshot.bin
|
||||
```
|
||||
|
||||
See [#1279](https://github.com/lightpanda-io/browser/pull/1279) for more details.
|
||||
|
||||
## Test
|
||||
|
||||
### Unit Tests
|
||||
|
||||
21
build.zig
21
build.zig
@@ -35,8 +35,7 @@ pub fn build(b: *Build) !void {
|
||||
opts.addOption([]const u8, "git_commit", git_commit orelse "dev");
|
||||
opts.addOption(?[]const u8, "snapshot_path", snapshot_path);
|
||||
|
||||
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer") orelse false;
|
||||
const enable_asan = b.option(bool, "asan", "Enable Address Sanitizer") orelse false;
|
||||
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer");
|
||||
const enable_csan = b.option(std.zig.SanitizeC, "csan", "Enable C Sanitizers");
|
||||
|
||||
const lightpanda_module = blk: {
|
||||
@@ -49,9 +48,8 @@ pub fn build(b: *Build) !void {
|
||||
.sanitize_c = enable_csan,
|
||||
.sanitize_thread = enable_tsan,
|
||||
});
|
||||
mod.addImport("lightpanda", mod); // allow circular "lightpanda" import
|
||||
|
||||
try addDependencies(b, mod, opts, enable_asan, enable_tsan, prebuilt_v8_path);
|
||||
try addDependencies(b, mod, opts, prebuilt_v8_path);
|
||||
|
||||
break :blk mod;
|
||||
};
|
||||
@@ -119,6 +117,7 @@ pub fn build(b: *Build) !void {
|
||||
}
|
||||
|
||||
{
|
||||
// ZIGDOM
|
||||
// browser
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "legacy_test",
|
||||
@@ -171,25 +170,15 @@ pub fn build(b: *Build) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn addDependencies(
|
||||
b: *Build,
|
||||
mod: *Build.Module,
|
||||
opts: *Build.Step.Options,
|
||||
is_asan: bool,
|
||||
is_tsan: bool,
|
||||
prebuilt_v8_path: ?[]const u8,
|
||||
) !void {
|
||||
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, prebuilt_v8_path: ?[]const u8) !void {
|
||||
mod.addImport("build_config", opts.createModule());
|
||||
|
||||
const target = mod.resolved_target.?;
|
||||
const dep_opts = .{
|
||||
.target = target,
|
||||
.optimize = mod.optimize.?,
|
||||
.cache_root = b.pathFromRoot(".lp-cache"),
|
||||
.prebuilt_v8_path = prebuilt_v8_path,
|
||||
.is_asan = is_asan,
|
||||
.is_tsan = is_tsan,
|
||||
.v8_enable_sandbox = is_tsan,
|
||||
.cache_root = b.pathFromRoot(".lp-cache"),
|
||||
};
|
||||
|
||||
mod.addIncludePath(b.path("vendor/lightpanda"));
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.2.8.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH63lfBADJ7UE2zAJ8nJIBnxoSiimXSaX6Q_M_7DS3",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/0d64a3d5b36ac94067df3e13fddbf715caa6f391.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH65sfBAC8o3q41YxhOms5uY2fvMzBrsgN8IeCXZgE",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
.@"boringssl-zig" = .{
|
||||
|
||||
24
flake.lock
generated
24
flake.lock
generated
@@ -8,11 +8,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770708269,
|
||||
"narHash": "sha256-OnZW86app7hHJJoB5lC9GNXY5QBBIESJB+sIdwEyld0=",
|
||||
"lastModified": 1763016383,
|
||||
"narHash": "sha256-eYmo7FNvm3q08iROzwIi8i9dWuUbJJl3uLR3OLnSmdI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "6b5325a017a9a9fe7e6252ccac3680cc7181cd63",
|
||||
"rev": "0fad5c0e5c531358e7174cd666af4608f08bc3ba",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -96,11 +96,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1768649915,
|
||||
"narHash": "sha256-jc21hKogFnxU7KXSVTRmxC7u5D4RHwm9BAvDf5/Z1Uo=",
|
||||
"lastModified": 1763043403,
|
||||
"narHash": "sha256-DgCTbHdIpzbXSlQlOZEWj8oPt2lrRMlSk03oIstvkVQ=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3e3f3c7f9977dc123c23ee21e8085ed63daf8c37",
|
||||
"rev": "75e04ecd084f93d4105ce68c07dac7656291fe2e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -122,11 +122,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1770668050,
|
||||
"narHash": "sha256-Q05yaIZtQrBKHpyWaPmyJmDRj0lojnVf8nUFE0vydcY=",
|
||||
"lastModified": 1762860488,
|
||||
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "9efc1f709f3c8134c3acac5d3592a8e4c184a0c6",
|
||||
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -175,11 +175,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770598090,
|
||||
"narHash": "sha256-k+82IDgTd9o5sxHIqGlvfwseKln3Ejx1edGtDltuPXo=",
|
||||
"lastModified": 1762907712,
|
||||
"narHash": "sha256-VNW/+VYIg6N4b9Iq+F0YZmm22n74IdFS7hsPLblWuOY=",
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "142495696982c88edddc8e17e4da90d8164acadf",
|
||||
"rev": "d16453ee78765e49527c56d23386cead799b6b53",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
66
src/App.zig
66
src/App.zig
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -21,53 +21,80 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("log.zig");
|
||||
const Config = @import("Config.zig");
|
||||
const Http = @import("http/Http.zig");
|
||||
const Snapshot = @import("browser/js/Snapshot.zig");
|
||||
const Platform = @import("browser/js/Platform.zig");
|
||||
|
||||
const Notification = @import("Notification.zig");
|
||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||
const RobotStore = @import("browser/Robots.zig").RobotStore;
|
||||
|
||||
pub const Http = @import("http/Http.zig");
|
||||
pub const ArenaPool = @import("ArenaPool.zig");
|
||||
|
||||
// Container for global state / objects that various parts of the system
|
||||
// might need.
|
||||
const App = @This();
|
||||
|
||||
http: Http,
|
||||
config: *const Config,
|
||||
config: Config,
|
||||
platform: Platform,
|
||||
snapshot: Snapshot,
|
||||
telemetry: Telemetry,
|
||||
allocator: Allocator,
|
||||
arena_pool: ArenaPool,
|
||||
robots: RobotStore,
|
||||
app_dir_path: ?[]const u8,
|
||||
notification: *Notification,
|
||||
shutdown: bool = false,
|
||||
|
||||
pub fn init(allocator: Allocator, config: *const Config) !*App {
|
||||
pub const RunMode = enum {
|
||||
help,
|
||||
fetch,
|
||||
serve,
|
||||
version,
|
||||
};
|
||||
|
||||
pub const Config = struct {
|
||||
run_mode: RunMode,
|
||||
tls_verify_host: bool = true,
|
||||
http_proxy: ?[:0]const u8 = null,
|
||||
proxy_bearer_token: ?[:0]const u8 = null,
|
||||
http_timeout_ms: ?u31 = null,
|
||||
http_connect_timeout_ms: ?u31 = null,
|
||||
http_max_host_open: ?u8 = null,
|
||||
http_max_concurrent: ?u8 = null,
|
||||
user_agent: [:0]const u8,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator, config: Config) !*App {
|
||||
const app = try allocator.create(App);
|
||||
errdefer allocator.destroy(app);
|
||||
|
||||
app.config = config;
|
||||
app.allocator = allocator;
|
||||
|
||||
app.robots = RobotStore.init(allocator);
|
||||
app.notification = try Notification.init(allocator, null);
|
||||
errdefer app.notification.deinit();
|
||||
|
||||
app.http = try Http.init(allocator, &app.robots, config);
|
||||
app.http = try Http.init(allocator, .{
|
||||
.max_host_open = config.http_max_host_open orelse 4,
|
||||
.max_concurrent = config.http_max_concurrent orelse 10,
|
||||
.timeout_ms = config.http_timeout_ms orelse 5000,
|
||||
.connect_timeout_ms = config.http_connect_timeout_ms orelse 0,
|
||||
.http_proxy = config.http_proxy,
|
||||
.tls_verify_host = config.tls_verify_host,
|
||||
.proxy_bearer_token = config.proxy_bearer_token,
|
||||
.user_agent = config.user_agent,
|
||||
});
|
||||
errdefer app.http.deinit();
|
||||
|
||||
app.platform = try Platform.init();
|
||||
errdefer app.platform.deinit();
|
||||
|
||||
app.snapshot = try Snapshot.load();
|
||||
errdefer app.snapshot.deinit();
|
||||
app.snapshot = try Snapshot.load(allocator);
|
||||
errdefer app.snapshot.deinit(allocator);
|
||||
|
||||
app.app_dir_path = getAndMakeAppDir(allocator);
|
||||
|
||||
app.telemetry = try Telemetry.init(app, config.mode);
|
||||
app.telemetry = try Telemetry.init(app, config.run_mode);
|
||||
errdefer app.telemetry.deinit();
|
||||
|
||||
app.arena_pool = ArenaPool.init(allocator);
|
||||
errdefer app.arena_pool.deinit();
|
||||
try app.telemetry.register(app.notification);
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -83,11 +110,10 @@ pub fn deinit(self: *App) void {
|
||||
self.app_dir_path = null;
|
||||
}
|
||||
self.telemetry.deinit();
|
||||
self.robots.deinit();
|
||||
self.notification.deinit();
|
||||
self.http.deinit();
|
||||
self.snapshot.deinit();
|
||||
self.snapshot.deinit(allocator);
|
||||
self.platform.deinit();
|
||||
self.arena_pool.deinit();
|
||||
|
||||
allocator.destroy(self);
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
// 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 Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const ArenaPool = @This();
|
||||
|
||||
allocator: Allocator,
|
||||
retain_bytes: usize,
|
||||
free_list_len: u16 = 0,
|
||||
free_list: ?*Entry = null,
|
||||
free_list_max: u16,
|
||||
entry_pool: std.heap.MemoryPool(Entry),
|
||||
|
||||
const Entry = struct {
|
||||
next: ?*Entry,
|
||||
arena: ArenaAllocator,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator) ArenaPool {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.free_list_max = 512, // TODO make configurable
|
||||
.retain_bytes = 1024 * 16, // TODO make configurable
|
||||
.entry_pool = std.heap.MemoryPool(Entry).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ArenaPool) void {
|
||||
var entry = self.free_list;
|
||||
while (entry) |e| {
|
||||
entry = e.next;
|
||||
e.arena.deinit();
|
||||
}
|
||||
self.entry_pool.deinit();
|
||||
}
|
||||
|
||||
pub fn acquire(self: *ArenaPool) !Allocator {
|
||||
if (self.free_list) |entry| {
|
||||
self.free_list = entry.next;
|
||||
self.free_list_len -= 1;
|
||||
return entry.arena.allocator();
|
||||
}
|
||||
|
||||
const entry = try self.entry_pool.create();
|
||||
entry.* = .{
|
||||
.next = null,
|
||||
.arena = ArenaAllocator.init(self.allocator),
|
||||
};
|
||||
|
||||
return entry.arena.allocator();
|
||||
}
|
||||
|
||||
pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
||||
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||
const entry: *Entry = @fieldParentPtr("arena", arena);
|
||||
|
||||
const free_list_len = self.free_list_len;
|
||||
if (free_list_len == self.free_list_max) {
|
||||
arena.deinit();
|
||||
self.entry_pool.destroy(entry);
|
||||
return;
|
||||
}
|
||||
|
||||
_ = arena.reset(.{ .retain_with_limit = self.retain_bytes });
|
||||
entry.next = self.free_list;
|
||||
self.free_list_len = free_list_len + 1;
|
||||
self.free_list = entry;
|
||||
}
|
||||
800
src/Config.zig
800
src/Config.zig
@@ -1,800 +0,0 @@
|
||||
// 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 builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("log.zig");
|
||||
const dump = @import("browser/dump.zig");
|
||||
|
||||
pub const RunMode = enum {
|
||||
help,
|
||||
fetch,
|
||||
serve,
|
||||
version,
|
||||
};
|
||||
|
||||
mode: Mode,
|
||||
exec_name: []const u8,
|
||||
http_headers: HttpHeaders,
|
||||
|
||||
const Config = @This();
|
||||
|
||||
pub fn init(allocator: Allocator, exec_name: []const u8, mode: Mode) !Config {
|
||||
var config = Config{
|
||||
.mode = mode,
|
||||
.exec_name = exec_name,
|
||||
.http_headers = undefined,
|
||||
};
|
||||
config.http_headers = try HttpHeaders.init(allocator, &config);
|
||||
return config;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Config, allocator: Allocator) void {
|
||||
self.http_headers.deinit(allocator);
|
||||
}
|
||||
|
||||
pub fn tlsVerifyHost(self: *const Config) bool {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.tls_verify_host,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn obeyRobots(self: *const Config) bool {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.obey_robots,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpProxy(self: *const Config) ?[:0]const u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.http_proxy,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn proxyBearerToken(self: *const Config) ?[:0]const u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.proxy_bearer_token,
|
||||
.help, .version => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpMaxConcurrent(self: *const Config) u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.http_max_concurrent orelse 10,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpMaxHostOpen(self: *const Config) u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.http_max_host_open orelse 4,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpConnectTimeout(self: *const Config) u31 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.http_connect_timeout orelse 0,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpTimeout(self: *const Config) u31 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.http_timeout orelse 5000,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpMaxRedirects(_: *const Config) u8 {
|
||||
return 10;
|
||||
}
|
||||
|
||||
pub fn httpMaxResponseSize(self: *const Config) ?usize {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.http_max_response_size,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logLevel(self: *const Config) ?log.Level {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.log_level,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logFormat(self: *const Config) ?log.Format {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.log_format,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logFilterScopes(self: *const Config) ?[]const log.Scope {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.log_filter_scopes,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.user_agent_suffix,
|
||||
.help, .version => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Mode = union(RunMode) {
|
||||
help: bool, // false when being printed because of an error
|
||||
fetch: Fetch,
|
||||
serve: Serve,
|
||||
version: void,
|
||||
};
|
||||
|
||||
pub const Serve = struct {
|
||||
host: []const u8 = "127.0.0.1",
|
||||
port: u16 = 9222,
|
||||
timeout: u31 = 10,
|
||||
max_connections: u16 = 16,
|
||||
max_tabs_per_connection: u16 = 8,
|
||||
max_memory_per_tab: u64 = 512 * 1024 * 1024,
|
||||
max_pending_connections: u16 = 128,
|
||||
common: Common = .{},
|
||||
};
|
||||
|
||||
pub const Fetch = struct {
|
||||
url: [:0]const u8,
|
||||
dump: bool = false,
|
||||
common: Common = .{},
|
||||
withbase: bool = false,
|
||||
strip: dump.Opts.Strip = .{},
|
||||
};
|
||||
|
||||
pub const Common = struct {
|
||||
obey_robots: bool = false,
|
||||
proxy_bearer_token: ?[:0]const u8 = null,
|
||||
http_proxy: ?[:0]const u8 = null,
|
||||
http_max_concurrent: ?u8 = null,
|
||||
http_max_host_open: ?u8 = null,
|
||||
http_timeout: ?u31 = null,
|
||||
http_connect_timeout: ?u31 = null,
|
||||
http_max_response_size: ?usize = null,
|
||||
tls_verify_host: bool = true,
|
||||
log_level: ?log.Level = null,
|
||||
log_format: ?log.Format = null,
|
||||
log_filter_scopes: ?[]log.Scope = null,
|
||||
user_agent_suffix: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// Pre-formatted HTTP headers for reuse across Http and Client.
|
||||
/// Must be initialized with an allocator that outlives all HTTP connections.
|
||||
pub const HttpHeaders = struct {
|
||||
const user_agent_base: [:0]const u8 = "Lightpanda/1.0";
|
||||
|
||||
user_agent: [:0]const u8, // User agent value (e.g. "Lightpanda/1.0")
|
||||
user_agent_header: [:0]const u8,
|
||||
|
||||
proxy_bearer_header: ?[:0]const u8,
|
||||
|
||||
pub fn init(allocator: Allocator, config: *const Config) !HttpHeaders {
|
||||
const user_agent: [:0]const u8 = if (config.userAgentSuffix()) |suffix|
|
||||
try std.fmt.allocPrintSentinel(allocator, "{s} {s}", .{ user_agent_base, suffix }, 0)
|
||||
else
|
||||
user_agent_base;
|
||||
errdefer if (config.userAgentSuffix() != null) allocator.free(user_agent);
|
||||
|
||||
const user_agent_header = try std.fmt.allocPrintSentinel(allocator, "User-Agent: {s}", .{user_agent}, 0);
|
||||
errdefer allocator.free(user_agent_header);
|
||||
|
||||
const proxy_bearer_header: ?[:0]const u8 = if (config.proxyBearerToken()) |token|
|
||||
try std.fmt.allocPrintSentinel(allocator, "Proxy-Authorization: Bearer {s}", .{token}, 0)
|
||||
else
|
||||
null;
|
||||
|
||||
return .{
|
||||
.user_agent = user_agent,
|
||||
.user_agent_header = user_agent_header,
|
||||
.proxy_bearer_header = proxy_bearer_header,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const HttpHeaders, allocator: Allocator) void {
|
||||
if (self.proxy_bearer_header) |hdr| {
|
||||
allocator.free(hdr);
|
||||
}
|
||||
allocator.free(self.user_agent_header);
|
||||
if (self.user_agent.ptr != user_agent_base.ptr) {
|
||||
allocator.free(self.user_agent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
// MAX_HELP_LEN|
|
||||
const common_options =
|
||||
\\
|
||||
\\--insecure_disable_tls_host_verification
|
||||
\\ Disables host verification on all HTTP requests. This is an
|
||||
\\ advanced option which should only be set if you understand
|
||||
\\ and accept the risk of disabling host verification.
|
||||
\\
|
||||
\\--obey_robots
|
||||
\\ Fetches and obeys the robots.txt (if available) of the web pages
|
||||
\\ we make requests towards.
|
||||
\\ Defaults to false.
|
||||
\\
|
||||
\\--http_proxy The HTTP proxy to use for all HTTP requests.
|
||||
\\ A username:password can be included for basic authentication.
|
||||
\\ Defaults to none.
|
||||
\\
|
||||
\\--proxy_bearer_token
|
||||
\\ The <token> to send for bearer authentication with the proxy
|
||||
\\ Proxy-Authorization: Bearer <token>
|
||||
\\
|
||||
\\--http_max_concurrent
|
||||
\\ The maximum number of concurrent HTTP requests.
|
||||
\\ Defaults to 10.
|
||||
\\
|
||||
\\--http_max_host_open
|
||||
\\ The maximum number of open connection to a given host:port.
|
||||
\\ Defaults to 4.
|
||||
\\
|
||||
\\--http_connect_timeout
|
||||
\\ The time, in milliseconds, for establishing an HTTP connection
|
||||
\\ before timing out. 0 means it never times out.
|
||||
\\ Defaults to 0.
|
||||
\\
|
||||
\\--http_timeout
|
||||
\\ The maximum time, in milliseconds, the transfer is allowed
|
||||
\\ to complete. 0 means it never times out.
|
||||
\\ Defaults to 10000.
|
||||
\\
|
||||
\\--http_max_response_size
|
||||
\\ Limits the acceptable response size for any request
|
||||
\\ (e.g. XHR, fetch, script loading, ...).
|
||||
\\ Defaults to no limit.
|
||||
\\
|
||||
\\--log_level The log level: debug, info, warn, error or fatal.
|
||||
\\ Defaults to
|
||||
++ (if (builtin.mode == .Debug) " info." else "warn.") ++
|
||||
\\
|
||||
\\
|
||||
\\--log_format The log format: pretty or logfmt.
|
||||
\\ Defaults to
|
||||
++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++
|
||||
\\
|
||||
\\
|
||||
\\--log_filter_scopes
|
||||
\\ Filter out too verbose logs per scope:
|
||||
\\ http, unknown_prop, event, ...
|
||||
\\
|
||||
\\--user_agent_suffix
|
||||
\\ Suffix to append to the Lightpanda/X.Y User-Agent
|
||||
\\
|
||||
;
|
||||
|
||||
// MAX_HELP_LEN|
|
||||
const usage =
|
||||
\\usage: {s} command [options] [URL]
|
||||
\\
|
||||
\\Command can be either 'fetch', 'serve' or 'help'
|
||||
\\
|
||||
\\fetch command
|
||||
\\Fetches the specified URL
|
||||
\\Example: {s} fetch --dump https://lightpanda.io/
|
||||
\\
|
||||
\\Options:
|
||||
\\--dump Dumps document to stdout.
|
||||
\\ Defaults to false.
|
||||
\\
|
||||
\\--strip_mode Comma separated list of tag groups to remove from dump
|
||||
\\ the dump. e.g. --strip_mode js,css
|
||||
\\ - "js" script and link[as=script, rel=preload]
|
||||
\\ - "ui" includes img, picture, video, css and svg
|
||||
\\ - "css" includes style and link[rel=stylesheet]
|
||||
\\ - "full" includes js, ui and css
|
||||
\\
|
||||
\\--with_base Add a <base> tag in dump. Defaults to false.
|
||||
\\
|
||||
++ common_options ++
|
||||
\\
|
||||
\\serve command
|
||||
\\Starts a websocket CDP server
|
||||
\\Example: {s} serve --host 127.0.0.1 --port 9222
|
||||
\\
|
||||
\\Options:
|
||||
\\--host Host of the CDP server
|
||||
\\ Defaults to "127.0.0.1"
|
||||
\\
|
||||
\\--port Port of the CDP server
|
||||
\\ Defaults to 9222
|
||||
\\
|
||||
\\--timeout Inactivity timeout in seconds before disconnecting clients
|
||||
\\ Defaults to 10 (seconds). Limited to 604800 (1 week).
|
||||
\\
|
||||
\\--max_connections
|
||||
\\ Maximum number of simultaneous CDP connections.
|
||||
\\ Defaults to 16.
|
||||
\\
|
||||
\\--max_tabs Maximum number of tabs per CDP connection.
|
||||
\\ Defaults to 8.
|
||||
\\
|
||||
\\--max_tab_memory
|
||||
\\ Maximum memory per tab in bytes.
|
||||
\\ Defaults to 536870912 (512 MB).
|
||||
\\
|
||||
\\--max_pending_connections
|
||||
\\ Maximum pending connections in the accept queue.
|
||||
\\ Defaults to 128.
|
||||
\\
|
||||
++ common_options ++
|
||||
\\
|
||||
\\version command
|
||||
\\Displays the version of {s}
|
||||
\\
|
||||
\\help command
|
||||
\\Displays this message
|
||||
\\
|
||||
;
|
||||
std.debug.print(usage, .{ self.exec_name, self.exec_name, self.exec_name, self.exec_name });
|
||||
if (success) {
|
||||
return std.process.cleanExit();
|
||||
}
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
pub fn parseArgs(allocator: Allocator) !Config {
|
||||
var args = try std.process.argsWithAllocator(allocator);
|
||||
defer args.deinit();
|
||||
|
||||
const exec_name = try allocator.dupe(u8, std.fs.path.basename(args.next().?));
|
||||
|
||||
const mode_string = args.next() orelse "";
|
||||
const run_mode = std.meta.stringToEnum(RunMode, mode_string) orelse blk: {
|
||||
const inferred_mode = inferMode(mode_string) orelse
|
||||
return init(allocator, exec_name, .{ .help = false });
|
||||
// "command" wasn't a command but an option. We can't reset args, but
|
||||
// we can create a new one. Not great, but this fallback is temporary
|
||||
// as we transition to this command mode approach.
|
||||
args.deinit();
|
||||
|
||||
args = try std.process.argsWithAllocator(allocator);
|
||||
// skip the exec_name
|
||||
_ = args.skip();
|
||||
|
||||
break :blk inferred_mode;
|
||||
};
|
||||
|
||||
const mode: Mode = switch (run_mode) {
|
||||
.help => .{ .help = true },
|
||||
.serve => .{ .serve = parseServeArgs(allocator, &args) catch
|
||||
return init(allocator, exec_name, .{ .help = false }) },
|
||||
.fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch
|
||||
return init(allocator, exec_name, .{ .help = false }) },
|
||||
.version => .{ .version = {} },
|
||||
};
|
||||
return init(allocator, exec_name, mode);
|
||||
}
|
||||
|
||||
fn inferMode(opt: []const u8) ?RunMode {
|
||||
if (opt.len == 0) {
|
||||
return .serve;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, opt, "--") == false) {
|
||||
return .fetch;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--dump")) {
|
||||
return .fetch;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--noscript")) {
|
||||
return .fetch;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--strip_mode")) {
|
||||
return .fetch;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--with_base")) {
|
||||
return .fetch;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--host")) {
|
||||
return .serve;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--port")) {
|
||||
return .serve;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, opt, "--timeout")) {
|
||||
return .serve;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn parseServeArgs(
|
||||
allocator: Allocator,
|
||||
args: *std.process.ArgIterator,
|
||||
) !Serve {
|
||||
var serve: Serve = .{};
|
||||
|
||||
while (args.next()) |opt| {
|
||||
if (std.mem.eql(u8, "--host", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--host" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
serve.host = try allocator.dupe(u8, str);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--port", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--port" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.port = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--port", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--timeout", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--timeout" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--timeout", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--max_connections", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_connections" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.max_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_connections", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--max_tabs", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_tabs" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.max_tabs_per_connection = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_tabs", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--max_tab_memory", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_tab_memory" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.max_memory_per_tab = std.fmt.parseInt(u64, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_tab_memory", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--max_pending_connections", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_pending_connections" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.max_pending_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_pending_connections", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (try parseCommonArg(allocator, opt, args, &serve.common)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.fatal(.app, "unknown argument", .{ .mode = "serve", .arg = opt });
|
||||
return error.UnkownOption;
|
||||
}
|
||||
|
||||
return serve;
|
||||
}
|
||||
|
||||
fn parseFetchArgs(
|
||||
allocator: Allocator,
|
||||
args: *std.process.ArgIterator,
|
||||
) !Fetch {
|
||||
var fetch_dump: bool = false;
|
||||
var withbase: bool = false;
|
||||
var url: ?[:0]const u8 = null;
|
||||
var common: Common = .{};
|
||||
var strip: dump.Opts.Strip = .{};
|
||||
|
||||
while (args.next()) |opt| {
|
||||
if (std.mem.eql(u8, "--dump", opt)) {
|
||||
fetch_dump = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--noscript", opt)) {
|
||||
log.warn(.app, "deprecation warning", .{
|
||||
.feature = "--noscript argument",
|
||||
.hint = "use '--strip_mode js' instead",
|
||||
});
|
||||
strip.js = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--with_base", opt)) {
|
||||
withbase = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--strip_mode", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--strip_mode" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
var it = std.mem.splitScalar(u8, str, ',');
|
||||
while (it.next()) |part| {
|
||||
const trimmed = std.mem.trim(u8, part, &std.ascii.whitespace);
|
||||
if (std.mem.eql(u8, trimmed, "js")) {
|
||||
strip.js = true;
|
||||
} else if (std.mem.eql(u8, trimmed, "ui")) {
|
||||
strip.ui = true;
|
||||
} else if (std.mem.eql(u8, trimmed, "css")) {
|
||||
strip.css = true;
|
||||
} else if (std.mem.eql(u8, trimmed, "full")) {
|
||||
strip.js = true;
|
||||
strip.ui = true;
|
||||
strip.css = true;
|
||||
} else {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--strip_mode", .value = trimmed });
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (try parseCommonArg(allocator, opt, args, &common)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, opt, "--")) {
|
||||
log.fatal(.app, "unknown argument", .{ .mode = "fetch", .arg = opt });
|
||||
return error.UnkownOption;
|
||||
}
|
||||
|
||||
if (url != null) {
|
||||
log.fatal(.app, "duplicate fetch url", .{ .help = "only 1 URL can be specified" });
|
||||
return error.TooManyURLs;
|
||||
}
|
||||
url = try allocator.dupeZ(u8, opt);
|
||||
}
|
||||
|
||||
if (url == null) {
|
||||
log.fatal(.app, "missing fetch url", .{ .help = "URL to fetch must be provided" });
|
||||
return error.MissingURL;
|
||||
}
|
||||
|
||||
return .{
|
||||
.url = url.?,
|
||||
.dump = fetch_dump,
|
||||
.strip = strip,
|
||||
.common = common,
|
||||
.withbase = withbase,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseCommonArg(
|
||||
allocator: Allocator,
|
||||
opt: []const u8,
|
||||
args: *std.process.ArgIterator,
|
||||
common: *Common,
|
||||
) !bool {
|
||||
if (std.mem.eql(u8, "--insecure_disable_tls_host_verification", opt)) {
|
||||
common.tls_verify_host = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--obey_robots", opt)) {
|
||||
common.obey_robots = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--http_proxy", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_proxy" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
common.http_proxy = try allocator.dupeZ(u8, str);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--proxy_bearer_token", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_bearer_token" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
common.proxy_bearer_token = try allocator.dupeZ(u8, str);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--http_max_concurrent", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_max_concurrent" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.http_max_concurrent = std.fmt.parseInt(u8, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_max_concurrent", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--http_max_host_open", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_max_host_open" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.http_max_host_open = std.fmt.parseInt(u8, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_max_host_open", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--http_connect_timeout", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_connect_timeout" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.http_connect_timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_connect_timeout", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--http_timeout", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_timeout" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.http_timeout = std.fmt.parseInt(u31, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_timeout", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--http_max_response_size", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--http_max_response_size" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.http_max_response_size = std.fmt.parseInt(usize, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--http_max_response_size", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--log_level", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--log_level" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.log_level = std.meta.stringToEnum(log.Level, str) orelse blk: {
|
||||
if (std.mem.eql(u8, str, "error")) {
|
||||
break :blk .err;
|
||||
}
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log_level", .value = str });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--log_format", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--log_format" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
common.log_format = std.meta.stringToEnum(log.Format, str) orelse {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log_format", .value = str });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--log_filter_scopes", opt)) {
|
||||
if (builtin.mode != .Debug) {
|
||||
log.fatal(.app, "experimental", .{ .help = "log scope filtering is only available in debug builds" });
|
||||
return false;
|
||||
}
|
||||
|
||||
const str = args.next() orelse {
|
||||
// disables the default filters
|
||||
common.log_filter_scopes = &.{};
|
||||
return true;
|
||||
};
|
||||
|
||||
var arr: std.ArrayList(log.Scope) = .empty;
|
||||
|
||||
var it = std.mem.splitScalar(u8, str, ',');
|
||||
while (it.next()) |part| {
|
||||
try arr.append(allocator, std.meta.stringToEnum(log.Scope, part) orelse {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log_filter_scopes", .value = part });
|
||||
return false;
|
||||
});
|
||||
}
|
||||
common.log_filter_scopes = arr.items;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--user_agent_suffix", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--user_agent_suffix" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
for (str) |c| {
|
||||
if (!std.ascii.isPrint(c)) {
|
||||
log.fatal(.app, "not printable character", .{ .arg = "--user_agent_suffix" });
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
}
|
||||
common.user_agent_suffix = try allocator.dupe(u8, str);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const log = @import("log.zig");
|
||||
const Page = @import("browser/Page.zig");
|
||||
@@ -39,9 +38,10 @@ const List = std.DoublyLinkedList;
|
||||
// CDP code registers for the "network_bytes_sent" event, because it needs to
|
||||
// send messages to the client when this happens. Our HTTP client could then
|
||||
// emit a "network_bytes_sent" message. It would be easy, and it would work.
|
||||
// That is, it would work until multiple CDP clients connect, and because
|
||||
// everything's just one big global, events from one CDP session would be sent
|
||||
// to all CDP clients.
|
||||
// That is, it would work until the Telemetry code makes an HTTP request, and
|
||||
// because everything's just one big global, that gets picked up by the
|
||||
// registered CDP listener, and the telemetry network activity gets sent to the
|
||||
// CDP client.
|
||||
//
|
||||
// To avoid this, one way or another, we need scoping. We could still have
|
||||
// a global registry but every "register" and every "emit" has some type of
|
||||
@@ -49,10 +49,14 @@ const List = std.DoublyLinkedList;
|
||||
// between components to share a common scope.
|
||||
//
|
||||
// Instead, the approach that we take is to have a notification instance per
|
||||
// CDP connection (BrowserContext). Each CDP connection has its own notification
|
||||
// that is shared across all Sessions (tabs) within that connection. This ensures
|
||||
// proper isolation between different CDP clients while allowing a single client
|
||||
// to receive events from all its tabs.
|
||||
// scope. This makes some things harder, but we only plan on having 2
|
||||
// notification instances at a given time: one in a Browser and one in the App.
|
||||
// What about something like Telemetry, which lives outside of a Browser but
|
||||
// still cares about Browser-events (like .page_navigate)? When the Browser
|
||||
// notification is created, a `notification_created` event is raised in the
|
||||
// App's notification, which Telemetry is registered for. This allows Telemetry
|
||||
// to register for events in the Browser notification. See the Telemetry's
|
||||
// register function.
|
||||
const Notification = @This();
|
||||
// Every event type (which are hard-coded), has a list of Listeners.
|
||||
// When the event happens, we dispatch to those listener.
|
||||
@@ -61,7 +65,7 @@ event_listeners: EventListeners,
|
||||
// list of listeners for a specified receiver
|
||||
// @intFromPtr(receiver) -> [listener1, listener2, ...]
|
||||
// Used when `unregisterAll` is called.
|
||||
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayList(*Listener)),
|
||||
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayListUnmanaged(*Listener)),
|
||||
|
||||
allocator: Allocator,
|
||||
mem_pool: std.heap.MemoryPool(Listener),
|
||||
@@ -80,6 +84,7 @@ const EventListeners = struct {
|
||||
http_request_auth_required: List = .{},
|
||||
http_response_data: List = .{},
|
||||
http_response_header_done: List = .{},
|
||||
notification_created: List = .{},
|
||||
};
|
||||
|
||||
const Events = union(enum) {
|
||||
@@ -96,6 +101,7 @@ const Events = union(enum) {
|
||||
http_request_done: *const RequestDone,
|
||||
http_response_data: *const ResponseData,
|
||||
http_response_header_done: *const ResponseHeaderDone,
|
||||
notification_created: *Notification,
|
||||
};
|
||||
const EventType = std.meta.FieldEnum(Events);
|
||||
|
||||
@@ -155,7 +161,12 @@ pub const RequestFail = struct {
|
||||
err: anyerror,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator) !*Notification {
|
||||
pub fn init(allocator: Allocator, parent: ?*Notification) !*Notification {
|
||||
|
||||
// This is put on the heap because we want to raise a .notification_created
|
||||
// event, so that, something like Telemetry, can receive the
|
||||
// .page_navigate event on all notification instances. That can only work
|
||||
// if we dispatch .notification_created with a *Notification.
|
||||
const notification = try allocator.create(Notification);
|
||||
errdefer allocator.destroy(notification);
|
||||
|
||||
@@ -166,6 +177,10 @@ pub fn init(allocator: Allocator) !*Notification {
|
||||
.mem_pool = std.heap.MemoryPool(Listener).init(allocator),
|
||||
};
|
||||
|
||||
if (parent) |pn| {
|
||||
pn.dispatch(.notification_created, notification);
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
@@ -226,7 +241,7 @@ pub fn unregister(self: *Notification, comptime event: EventType, receiver: anyt
|
||||
if (listeners.items.len == 0) {
|
||||
listeners.deinit(self.allocator);
|
||||
const removed = self.listeners.remove(@intFromPtr(receiver));
|
||||
lp.assert(removed == true, "Notification.unregister", .{ .type = event });
|
||||
std.debug.assert(removed == true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,9 +255,6 @@ pub fn unregisterAll(self: *Notification, receiver: *anyopaque) void {
|
||||
}
|
||||
|
||||
pub fn dispatch(self: *Notification, comptime event: EventType, data: ArgType(event)) void {
|
||||
if (self.listeners.count() == 0) {
|
||||
return;
|
||||
}
|
||||
const list = &@field(self.event_listeners, @tagName(event));
|
||||
|
||||
var node = list.first;
|
||||
@@ -300,7 +312,7 @@ const Listener = struct {
|
||||
|
||||
const testing = std.testing;
|
||||
test "Notification" {
|
||||
var notifier = try Notification.init(testing.allocator);
|
||||
var notifier = try Notification.init(testing.allocator, null);
|
||||
defer notifier.deinit();
|
||||
|
||||
// noop
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const net = std.net;
|
||||
@@ -158,7 +157,7 @@ fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void {
|
||||
});
|
||||
defer http.removeCDPClient();
|
||||
|
||||
lp.assert(client.mode == .http, "Server.readLoop invalid mode", .{});
|
||||
std.debug.assert(client.mode == .http);
|
||||
while (true) {
|
||||
if (http.poll(timeout_ms) != .cdp_socket) {
|
||||
log.info(.app, "CDP timeout", .{});
|
||||
@@ -205,6 +204,7 @@ fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void {
|
||||
}
|
||||
ms_remaining -= @intCast(elapsed);
|
||||
},
|
||||
.navigate => unreachable, // must have been handled by the session
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,7 +236,7 @@ pub const Client = struct {
|
||||
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", .{});
|
||||
std.debug.assert(socket_flags & nonblocking == nonblocking);
|
||||
|
||||
var reader = try Reader(true).init(server.allocator);
|
||||
errdefer reader.deinit();
|
||||
@@ -311,7 +311,7 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
fn processHTTPRequest(self: *Client) !bool {
|
||||
lp.assert(self.reader.pos == 0, "Client.HTTP pos", .{ .pos = self.reader.pos });
|
||||
std.debug.assert(self.reader.pos == 0);
|
||||
const request = self.reader.buf[0..self.reader.len];
|
||||
|
||||
if (request.len > MAX_HTTP_REQUEST_SIZE) {
|
||||
@@ -560,7 +560,7 @@ pub const Client = struct {
|
||||
|
||||
pub fn sendJSONRaw(
|
||||
self: *Client,
|
||||
buf: std.ArrayList(u8),
|
||||
buf: std.ArrayListUnmanaged(u8),
|
||||
) !void {
|
||||
// Dangerous API!. We assume the caller has reserved the first 10
|
||||
// bytes in `buf`.
|
||||
@@ -592,7 +592,8 @@ pub const Client = struct {
|
||||
// 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", .{});
|
||||
std.debug.assert(changed_to_blocking == false);
|
||||
log.debug(.app, "CDP write would 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;
|
||||
@@ -820,7 +821,7 @@ fn Reader(comptime EXPECT_MASK: bool) type {
|
||||
const pos = self.pos;
|
||||
const len = self.len;
|
||||
|
||||
lp.assert(pos <= len, "Client.Reader.compact precondition", .{ .pos = pos, .len = len });
|
||||
std.debug.assert(pos <= len);
|
||||
|
||||
// how many (if any) partial bytes do we have
|
||||
const partial_bytes = len - pos;
|
||||
@@ -841,7 +842,7 @@ fn Reader(comptime EXPECT_MASK: bool) type {
|
||||
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 });
|
||||
std.debug.assert(next_message_len > partial_bytes);
|
||||
|
||||
const missing_bytes = next_message_len - partial_bytes;
|
||||
|
||||
@@ -882,7 +883,7 @@ fn growBuffer(allocator: Allocator, buf: []u8, required_capacity: usize) ![]u8 {
|
||||
|
||||
const Fragments = struct {
|
||||
type: Message.Type,
|
||||
message: std.ArrayList(u8),
|
||||
message: std.ArrayListUnmanaged(u8),
|
||||
};
|
||||
|
||||
const Message = struct {
|
||||
@@ -906,7 +907,7 @@ const OpCode = enum(u8) {
|
||||
pong = 128 | 10,
|
||||
};
|
||||
|
||||
fn fillWebsocketHeader(buf: std.ArrayList(u8)) []const u8 {
|
||||
fn fillWebsocketHeader(buf: std.ArrayListUnmanaged(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:
|
||||
@@ -928,7 +929,7 @@ fn fillWebsocketHeader(buf: std.ArrayList(u8)) []const u8 {
|
||||
// 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 });
|
||||
std.debug.assert(buf.len == 10);
|
||||
|
||||
const len = payload_len;
|
||||
buf[0] = 128 | @intFromEnum(op_code); // fin | opcode
|
||||
@@ -1341,7 +1342,7 @@ fn assertWebSocketMessage(expected: []const u8, input: []const u8) !void {
|
||||
}
|
||||
|
||||
const MockCDP = struct {
|
||||
messages: std.ArrayList([]const u8) = .{},
|
||||
messages: std.ArrayListUnmanaged([]const u8) = .{},
|
||||
|
||||
allocator: Allocator = testing.allocator,
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
||||
|
||||
const TestHTTPServer = @This();
|
||||
|
||||
shutdown: std.atomic.Value(bool),
|
||||
shutdown: bool,
|
||||
listener: ?std.net.Server,
|
||||
handler: Handler,
|
||||
|
||||
@@ -28,23 +28,16 @@ const Handler = *const fn (req: *std.http.Server.Request) anyerror!void;
|
||||
|
||||
pub fn init(handler: Handler) TestHTTPServer {
|
||||
return .{
|
||||
.shutdown = .init(true),
|
||||
.shutdown = true,
|
||||
.listener = null,
|
||||
.handler = handler,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TestHTTPServer) void {
|
||||
self.listener = null;
|
||||
}
|
||||
|
||||
pub fn stop(self: *TestHTTPServer) void {
|
||||
self.shutdown.store(true, .release);
|
||||
self.shutdown = true;
|
||||
if (self.listener) |*listener| {
|
||||
switch (@import("builtin").target.os.tag) {
|
||||
.linux => std.posix.shutdown(listener.stream.handle, .recv) catch {},
|
||||
else => std.posix.close(listener.stream.handle),
|
||||
}
|
||||
listener.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,13 +46,12 @@ pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {
|
||||
|
||||
self.listener = try address.listen(.{ .reuse_address = true });
|
||||
var listener = &self.listener.?;
|
||||
self.shutdown.store(false, .release);
|
||||
|
||||
wg.finish();
|
||||
|
||||
while (true) {
|
||||
const conn = listener.accept() catch |err| {
|
||||
if (self.shutdown.load(.acquire) or err == error.SocketNotListening) {
|
||||
if (self.shutdown) {
|
||||
return;
|
||||
}
|
||||
return err;
|
||||
|
||||
@@ -24,14 +24,12 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const js = @import("js/js.zig");
|
||||
const log = @import("../log.zig");
|
||||
const App = @import("../App.zig");
|
||||
|
||||
const ArenaPool = App.ArenaPool;
|
||||
const HttpClient = App.Http.Client;
|
||||
const HttpClient = @import("../http/Client.zig");
|
||||
const Notification = @import("../Notification.zig");
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Session = @import("Session.zig");
|
||||
const Notification = @import("../Notification.zig");
|
||||
|
||||
// Browser is an instance of the browser.
|
||||
// You can create multiple browser instances.
|
||||
@@ -42,29 +40,30 @@ env: js.Env,
|
||||
app: *App,
|
||||
session: ?Session,
|
||||
allocator: Allocator,
|
||||
arena_pool: *ArenaPool,
|
||||
http_client: *HttpClient,
|
||||
call_arena: ArenaAllocator,
|
||||
page_arena: ArenaAllocator,
|
||||
session_arena: ArenaAllocator,
|
||||
transfer_arena: ArenaAllocator,
|
||||
notification: *Notification,
|
||||
|
||||
const InitOpts = struct {
|
||||
env: js.Env.InitOpts = .{},
|
||||
};
|
||||
|
||||
pub fn init(app: *App, opts: InitOpts) !Browser {
|
||||
pub fn init(app: *App) !Browser {
|
||||
const allocator = app.allocator;
|
||||
|
||||
var env = try js.Env.init(app, opts.env);
|
||||
var env = try js.Env.init(allocator, &app.platform, &app.snapshot);
|
||||
errdefer env.deinit();
|
||||
|
||||
const notification = try Notification.init(allocator, app.notification);
|
||||
app.http.client.notification = notification;
|
||||
app.http.client.next_request_id = 0; // Should we track ids in CDP only?
|
||||
errdefer notification.deinit();
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.env = env,
|
||||
.session = null,
|
||||
.allocator = allocator,
|
||||
.arena_pool = &app.arena_pool,
|
||||
.notification = notification,
|
||||
.http_client = app.http.client,
|
||||
.call_arena = ArenaAllocator.init(allocator),
|
||||
.page_arena = ArenaAllocator.init(allocator),
|
||||
@@ -80,13 +79,15 @@ pub fn deinit(self: *Browser) void {
|
||||
self.page_arena.deinit();
|
||||
self.session_arena.deinit();
|
||||
self.transfer_arena.deinit();
|
||||
self.http_client.notification = null;
|
||||
self.notification.deinit();
|
||||
}
|
||||
|
||||
pub fn newSession(self: *Browser, notification: *Notification) !*Session {
|
||||
pub fn newSession(self: *Browser) !*Session {
|
||||
self.closeSession();
|
||||
self.session = @as(Session, undefined);
|
||||
const session = &self.session.?;
|
||||
try Session.init(session, self, notification);
|
||||
try Session.init(session, self);
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -95,7 +96,7 @@ pub fn closeSession(self: *Browser) void {
|
||||
session.deinit();
|
||||
self.session = null;
|
||||
_ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
self.env.memoryPressureNotification(.critical);
|
||||
self.env.lowMemoryNotification();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,10 +104,6 @@ pub fn runMicrotasks(self: *const Browser) void {
|
||||
self.env.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn runMacrotasks(self: *Browser) !?u64 {
|
||||
return try self.env.runMacrotasks();
|
||||
}
|
||||
|
||||
pub fn runMessageLoop(self: *const Browser) void {
|
||||
while (self.env.pumpMessageLoop()) {
|
||||
if (comptime IS_DEBUG) {
|
||||
|
||||
@@ -33,38 +33,14 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
const EventKey = struct {
|
||||
event_target: usize,
|
||||
type_string: String,
|
||||
};
|
||||
|
||||
const EventKeyContext = struct {
|
||||
pub fn hash(_: @This(), key: EventKey) u64 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
hasher.update(std.mem.asBytes(&key.event_target));
|
||||
hasher.update(key.type_string.str());
|
||||
return hasher.final();
|
||||
}
|
||||
|
||||
pub fn eql(_: @This(), a: EventKey, b: EventKey) bool {
|
||||
return a.event_target == b.event_target and a.type_string.eql(b.type_string);
|
||||
}
|
||||
};
|
||||
|
||||
pub const EventManager = @This();
|
||||
|
||||
page: *Page,
|
||||
arena: Allocator,
|
||||
listener_pool: std.heap.MemoryPool(Listener),
|
||||
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
||||
lookup: std.HashMapUnmanaged(
|
||||
EventKey,
|
||||
*std.DoublyLinkedList,
|
||||
EventKeyContext,
|
||||
std.hash_map.default_max_load_percentage,
|
||||
),
|
||||
dispatch_depth: usize,
|
||||
deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),
|
||||
lookup: std.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList),
|
||||
dispatch_depth: u32 = 0,
|
||||
|
||||
pub fn init(page: *Page) EventManager {
|
||||
return .{
|
||||
@@ -74,7 +50,6 @@ pub fn init(page: *Page) EventManager {
|
||||
.list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena),
|
||||
.listener_pool = std.heap.MemoryPool(Listener).init(page.arena),
|
||||
.dispatch_depth = 0,
|
||||
.deferred_removals = .{},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,7 +67,7 @@ pub const Callback = union(enum) {
|
||||
|
||||
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once, .target = target.toString() });
|
||||
log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once, .target = target });
|
||||
}
|
||||
|
||||
// If a signal is provided and already aborted, don't register the listener
|
||||
@@ -102,24 +77,20 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate the type string we'll use in both listener and key
|
||||
const type_string = try String.init(self.arena, typ, .{});
|
||||
|
||||
const gop = try self.lookup.getOrPut(self.arena, .{
|
||||
.type_string = type_string,
|
||||
.event_target = @intFromPtr(target),
|
||||
});
|
||||
const gop = try self.lookup.getOrPut(self.arena, @intFromPtr(target));
|
||||
if (gop.found_existing) {
|
||||
// check for duplicate callbacks already registered
|
||||
var node = gop.value_ptr.*.first;
|
||||
while (node) |n| {
|
||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||
const is_duplicate = switch (callback) {
|
||||
.object => |obj| listener.function.eqlObject(obj),
|
||||
.function => |func| listener.function.eqlFunction(func),
|
||||
};
|
||||
if (is_duplicate and listener.capture == opts.capture) {
|
||||
return;
|
||||
if (listener.typ.eqlSlice(typ)) {
|
||||
const is_duplicate = switch (callback) {
|
||||
.object => |obj| listener.function.eqlObject(obj),
|
||||
.function => |func| listener.function.eqlFunction(func),
|
||||
};
|
||||
if (is_duplicate and listener.capture == opts.capture) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
node = n.next;
|
||||
}
|
||||
@@ -129,8 +100,8 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
||||
}
|
||||
|
||||
const func = switch (callback) {
|
||||
.function => |f| Function{ .value = try f.persist() },
|
||||
.object => |o| Function{ .object = try o.persist() },
|
||||
.function => |f| Function{ .value = f },
|
||||
.object => |o| Function{ .object = o },
|
||||
};
|
||||
|
||||
const listener = try self.listener_pool.create();
|
||||
@@ -141,34 +112,23 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
||||
.passive = opts.passive,
|
||||
.function = func,
|
||||
.signal = opts.signal,
|
||||
.typ = type_string,
|
||||
.typ = try String.init(self.arena, typ, .{}),
|
||||
};
|
||||
// append the listener to the list of listeners for this target
|
||||
gop.value_ptr.*.append(&listener.node);
|
||||
}
|
||||
|
||||
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
|
||||
const list = self.lookup.get(.{
|
||||
.type_string = .wrap(typ),
|
||||
.event_target = @intFromPtr(target),
|
||||
}) orelse return;
|
||||
if (findListener(list, callback, use_capture)) |listener| {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.event, "eventManager.remove", .{ .type = typ, .capture = use_capture, .target = target });
|
||||
}
|
||||
const list = self.lookup.get(@intFromPtr(target)) orelse return;
|
||||
if (findListener(list, typ, callback, use_capture)) |listener| {
|
||||
self.removeListener(list, listener);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// inferred error.
|
||||
const DispatchError = error{
|
||||
OutOfMemory,
|
||||
StringTooLarge,
|
||||
JSExecCallback,
|
||||
CompilationError,
|
||||
ExecutionError,
|
||||
JsException,
|
||||
};
|
||||
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!void {
|
||||
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
|
||||
}
|
||||
@@ -178,10 +138,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
|
||||
var was_handled = false;
|
||||
|
||||
defer if (was_handled) {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
self.page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
ls.local.runMicrotasks();
|
||||
self.page.js.runMicrotasks();
|
||||
};
|
||||
|
||||
switch (target._type) {
|
||||
@@ -195,13 +152,9 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
|
||||
.navigation,
|
||||
.screen,
|
||||
.screen_orientation,
|
||||
.visual_viewport,
|
||||
.generic,
|
||||
=> {
|
||||
const list = self.lookup.get(.{
|
||||
.event_target = @intFromPtr(target),
|
||||
.type_string = event._type_string,
|
||||
}) orelse return;
|
||||
const list = self.lookup.get(@intFromPtr(target)) orelse return;
|
||||
try self.dispatchAll(list, target, event, &was_handled);
|
||||
},
|
||||
}
|
||||
@@ -228,15 +181,12 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
||||
|
||||
var was_dispatched = false;
|
||||
defer if (was_dispatched) {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
self.page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
ls.local.runMicrotasks();
|
||||
self.page.js.runMicrotasks();
|
||||
};
|
||||
|
||||
if (function_) |func| {
|
||||
event._current_target = target;
|
||||
if (func.callWithThis(void, target, .{event})) {
|
||||
if (func.call(void, .{event})) {
|
||||
was_dispatched = true;
|
||||
} else |err| {
|
||||
// a non-JS error
|
||||
@@ -244,10 +194,7 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
||||
}
|
||||
}
|
||||
|
||||
const list = self.lookup.get(.{
|
||||
.event_target = @intFromPtr(target),
|
||||
.type_string = event._type_string,
|
||||
}) orelse return;
|
||||
const list = self.lookup.get(@intFromPtr(target)) orelse return;
|
||||
try self.dispatchAll(list, target, event, &was_dispatched);
|
||||
}
|
||||
|
||||
@@ -315,10 +262,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
while (i > 1) {
|
||||
i -= 1;
|
||||
const current_target = path[i];
|
||||
if (self.lookup.get(.{
|
||||
.event_target = @intFromPtr(current_target),
|
||||
.type_string = event._type_string,
|
||||
})) |list| {
|
||||
if (self.lookup.get(@intFromPtr(current_target))) |list| {
|
||||
try self.dispatchPhase(list, current_target, event, was_handled, true);
|
||||
if (event._stop_propagation) {
|
||||
return;
|
||||
@@ -329,10 +273,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
// Phase 2: At target
|
||||
event._event_phase = .at_target;
|
||||
const target_et = target.asEventTarget();
|
||||
if (self.lookup.get(.{
|
||||
.type_string = event._type_string,
|
||||
.event_target = @intFromPtr(target_et),
|
||||
})) |list| {
|
||||
if (self.lookup.get(@intFromPtr(target_et))) |list| {
|
||||
try self.dispatchPhase(list, target_et, event, was_handled, null);
|
||||
if (event._stop_propagation) {
|
||||
return;
|
||||
@@ -344,10 +285,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
if (event._bubbles) {
|
||||
event._event_phase = .bubbling_phase;
|
||||
for (path[1..]) |current_target| {
|
||||
if (self.lookup.get(.{
|
||||
.type_string = event._type_string,
|
||||
.event_target = @intFromPtr(current_target),
|
||||
})) |list| {
|
||||
if (self.lookup.get(@intFromPtr(current_target))) |list| {
|
||||
try self.dispatchPhase(list, current_target, event, was_handled, false);
|
||||
if (event._stop_propagation) {
|
||||
break;
|
||||
@@ -359,51 +297,40 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
|
||||
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime capture_only: ?bool) !void {
|
||||
const page = self.page;
|
||||
const typ = event._type_string;
|
||||
|
||||
// Track dispatch depth for deferred removal
|
||||
// Track that we're dispatching to prevent immediate removal
|
||||
self.dispatch_depth += 1;
|
||||
defer {
|
||||
const dispatch_depth = self.dispatch_depth;
|
||||
// Only destroy deferred listeners when we exit the outermost dispatch
|
||||
if (dispatch_depth == 1) {
|
||||
for (self.deferred_removals.items) |removal| {
|
||||
removal.list.remove(&removal.listener.node);
|
||||
self.listener_pool.destroy(removal.listener);
|
||||
}
|
||||
self.deferred_removals.clearRetainingCapacity();
|
||||
} else {
|
||||
self.dispatch_depth = dispatch_depth - 1;
|
||||
}
|
||||
self.dispatch_depth -= 1;
|
||||
// Clean up any marked listeners in this target's list after this phase
|
||||
// We do this regardless of depth to handle cross-target removals correctly
|
||||
self.cleanupMarkedListeners(list);
|
||||
}
|
||||
|
||||
// Use the last listener in the list as sentinel - listeners added during dispatch will be after it
|
||||
const last_node = list.last orelse return;
|
||||
const last_listener: *Listener = @alignCast(@fieldParentPtr("node", last_node));
|
||||
|
||||
// Iterate through the list, stopping after we've encountered the last_listener
|
||||
var node = list.first;
|
||||
var is_done = false;
|
||||
while (node) |n| {
|
||||
if (is_done) {
|
||||
break;
|
||||
}
|
||||
|
||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||
is_done = (listener == last_listener);
|
||||
// do this now, in case we need to remove n (once: true or aborted signal)
|
||||
node = n.next;
|
||||
|
||||
// Skip non-matching listeners
|
||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||
|
||||
// Skip listeners that were marked for removal
|
||||
if (listener.marked_for_removal) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!listener.typ.eql(typ)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Can be null when dispatching to the target itself
|
||||
if (comptime capture_only) |capture| {
|
||||
if (listener.capture != capture) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip removed listeners
|
||||
if (listener.removed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the listener has an aborted signal, remove it and skip
|
||||
if (listener.signal) |signal| {
|
||||
if (signal.getAborted()) {
|
||||
@@ -412,11 +339,6 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
}
|
||||
}
|
||||
|
||||
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
|
||||
if (listener.once) {
|
||||
self.removeListener(list, listener);
|
||||
}
|
||||
|
||||
was_handled.* = true;
|
||||
event._current_target = current_target;
|
||||
|
||||
@@ -426,18 +348,13 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
event._target = getAdjustedTarget(original_target, current_target);
|
||||
}
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
switch (listener.function) {
|
||||
.value => |value| try ls.toLocal(value).callWithThis(void, current_target, .{event}),
|
||||
.value => |value| try value.call(void, .{event}),
|
||||
.string => |string| {
|
||||
const str = try page.call_arena.dupeZ(u8, string.str());
|
||||
try ls.local.eval(str, null);
|
||||
try self.page.js.eval(str, null);
|
||||
},
|
||||
.object => |obj_global| {
|
||||
const obj = ls.toLocal(obj_global);
|
||||
.object => |obj| {
|
||||
if (try obj.getFunction("handleEvent")) |handleEvent| {
|
||||
try handleEvent.callWithThis(void, obj, .{event});
|
||||
}
|
||||
@@ -449,6 +366,10 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
event._target = original_target;
|
||||
}
|
||||
|
||||
if (listener.once) {
|
||||
self.removeListener(list, listener);
|
||||
}
|
||||
|
||||
if (event._stop_immediate_propagation) {
|
||||
return;
|
||||
}
|
||||
@@ -461,18 +382,30 @@ fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target:
|
||||
}
|
||||
|
||||
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {
|
||||
// If we're in a dispatch, defer removal to avoid invalidating iteration
|
||||
if (self.dispatch_depth > 0) {
|
||||
listener.removed = true;
|
||||
self.deferred_removals.append(self.arena, .{ .list = list, .listener = listener }) catch unreachable;
|
||||
// We're in the middle of dispatching, just mark for removal
|
||||
// This prevents invalidating the linked list during iteration
|
||||
listener.marked_for_removal = true;
|
||||
} else {
|
||||
// Outside dispatch, remove immediately
|
||||
// Safe to remove immediately
|
||||
list.remove(&listener.node);
|
||||
self.listener_pool.destroy(listener);
|
||||
}
|
||||
}
|
||||
|
||||
fn findListener(list: *const std.DoublyLinkedList, callback: Callback, capture: bool) ?*Listener {
|
||||
fn cleanupMarkedListeners(self: *EventManager, list: *std.DoublyLinkedList) void {
|
||||
var node = list.first;
|
||||
while (node) |n| {
|
||||
node = n.next;
|
||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||
if (listener.marked_for_removal) {
|
||||
list.remove(&listener.node);
|
||||
self.listener_pool.destroy(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Callback, capture: bool) ?*Listener {
|
||||
var node = list.first;
|
||||
while (node) |n| {
|
||||
node = n.next;
|
||||
@@ -487,6 +420,9 @@ fn findListener(list: *const std.DoublyLinkedList, callback: Callback, capture:
|
||||
if (listener.capture != capture) {
|
||||
continue;
|
||||
}
|
||||
if (!listener.typ.eqlSlice(typ)) {
|
||||
continue;
|
||||
}
|
||||
return listener;
|
||||
}
|
||||
return null;
|
||||
@@ -500,24 +436,24 @@ const Listener = struct {
|
||||
function: Function,
|
||||
signal: ?*@import("webapi/AbortSignal.zig") = null,
|
||||
node: std.DoublyLinkedList.Node,
|
||||
removed: bool = false,
|
||||
marked_for_removal: bool = false,
|
||||
};
|
||||
|
||||
const Function = union(enum) {
|
||||
value: js.Function.Global,
|
||||
value: js.Function,
|
||||
string: String,
|
||||
object: js.Object.Global,
|
||||
object: js.Object,
|
||||
|
||||
fn eqlFunction(self: Function, func: js.Function) bool {
|
||||
return switch (self) {
|
||||
.value => |v| v.isEqual(func),
|
||||
.value => |v| return v.id == func.id,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn eqlObject(self: Function, obj: js.Object) bool {
|
||||
return switch (self) {
|
||||
.object => |o| return o.isEqual(obj),
|
||||
.object => |o| return o.getId() == obj.getId(),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -17,8 +17,10 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const builtin = @import("builtin");
|
||||
const reflect = @import("reflect.zig");
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
const log = @import("../log.zig");
|
||||
const String = @import("../string.zig").String;
|
||||
@@ -29,7 +31,6 @@ const Page = @import("Page.zig");
|
||||
const Node = @import("webapi/Node.zig");
|
||||
const Event = @import("webapi/Event.zig");
|
||||
const UIEvent = @import("webapi/event/UIEvent.zig");
|
||||
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
const Document = @import("webapi/Document.zig");
|
||||
const EventTarget = @import("webapi/EventTarget.zig");
|
||||
@@ -37,11 +38,6 @@ const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.
|
||||
const Blob = @import("webapi/Blob.zig");
|
||||
const AbstractRange = @import("webapi/AbstractRange.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const Factory = @This();
|
||||
_page: *Page,
|
||||
_slab: SlabAllocator,
|
||||
@@ -173,68 +169,43 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
}
|
||||
|
||||
// this is a root object
|
||||
pub fn event(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
|
||||
const chain = try PrototypeChain(
|
||||
&.{ Event, @TypeOf(child) },
|
||||
).allocate(arena);
|
||||
).allocate(allocator);
|
||||
|
||||
// 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));
|
||||
event_ptr.* = .{
|
||||
._type = unionInit(Event.Type, chain.get(1)),
|
||||
._type_string = try String.init(self._page.arena, typ, .{}),
|
||||
};
|
||||
chain.setLeaf(1, child);
|
||||
|
||||
return chain.get(1);
|
||||
}
|
||||
|
||||
pub fn uiEvent(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||
pub fn uiEvent(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
|
||||
const chain = try PrototypeChain(
|
||||
&.{ Event, UIEvent, @TypeOf(child) },
|
||||
).allocate(arena);
|
||||
).allocate(allocator);
|
||||
|
||||
// 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));
|
||||
event_ptr.* = .{
|
||||
._type = unionInit(Event.Type, chain.get(1)),
|
||||
._type_string = try String.init(self._page.arena, typ, .{}),
|
||||
};
|
||||
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)
|
||||
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
|
||||
const time_stamp = (raw_timestamp / 2) * 2;
|
||||
|
||||
return .{
|
||||
._arena = arena,
|
||||
._page = self._page,
|
||||
._type = unionInit(Event.Type, value),
|
||||
._type_string = typ,
|
||||
._time_stamp = time_stamp,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
|
||||
@@ -342,7 +313,9 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO
|
||||
return chain.get(4);
|
||||
}
|
||||
|
||||
pub fn xhrEventTarget(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {
|
||||
pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
const allocator = self._slab.allocator();
|
||||
|
||||
return try AutoPrototypeChain(
|
||||
&.{ EventTarget, XMLHttpRequestEventTarget, @TypeOf(child) },
|
||||
).create(allocator, child);
|
||||
@@ -357,6 +330,32 @@ pub fn textTrackCue(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
).create(allocator, child);
|
||||
}
|
||||
|
||||
fn hasChainRoot(comptime T: type) bool {
|
||||
// Check if this is a root
|
||||
if (@hasDecl(T, "_prototype_root")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no _proto field, we're at the top but not a recognized root
|
||||
if (!@hasField(T, "_proto")) return false;
|
||||
|
||||
// Get the _proto field's type and recurse
|
||||
const fields = @typeInfo(T).@"struct".fields;
|
||||
inline for (fields) |field| {
|
||||
if (std.mem.eql(u8, field.name, "_proto")) {
|
||||
const ProtoType = reflect.Struct(field.type);
|
||||
return hasChainRoot(ProtoType);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn isChainType(comptime T: type) bool {
|
||||
if (@hasField(T, "_proto")) return false;
|
||||
return comptime hasChainRoot(T);
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Factory, value: anytype) void {
|
||||
const S = reflect.Struct(@TypeOf(value));
|
||||
|
||||
@@ -373,7 +372,7 @@ pub fn destroy(self: *Factory, value: anytype) void {
|
||||
}
|
||||
}
|
||||
|
||||
if (comptime @hasField(S, "_proto")) {
|
||||
if (comptime isChainType(S)) {
|
||||
self.destroyChain(value, true, 0, std.mem.Alignment.@"1");
|
||||
} else {
|
||||
self.destroyStandalone(value);
|
||||
@@ -381,7 +380,20 @@ pub fn destroy(self: *Factory, value: anytype) void {
|
||||
}
|
||||
|
||||
pub fn destroyStandalone(self: *Factory, value: anytype) void {
|
||||
const S = reflect.Struct(@TypeOf(value));
|
||||
assert(!@hasDecl(S, "_prototype_root"));
|
||||
|
||||
const allocator = self._slab.allocator();
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
allocator.destroy(value);
|
||||
}
|
||||
|
||||
@@ -397,8 +409,10 @@ fn destroyChain(
|
||||
|
||||
// aligns the old size to the alignment of this element
|
||||
const current_size = std.mem.alignForward(usize, old_size, @alignOf(S));
|
||||
const alignment = std.mem.Alignment.fromByteUnits(@alignOf(S));
|
||||
|
||||
const new_align = std.mem.Alignment.max(old_align, alignment);
|
||||
const new_size = current_size + @sizeOf(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
|
||||
@@ -417,15 +431,20 @@ fn destroyChain(
|
||||
|
||||
if (@hasField(S, "_proto")) {
|
||||
self.destroyChain(value._proto, false, new_size, new_align);
|
||||
} else if (@hasDecl(S, "JsApi")) {
|
||||
// Doesn't have a _proto, but has a JsApi.
|
||||
if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| {
|
||||
allocator.destroy(tagged);
|
||||
}
|
||||
} else {
|
||||
// no proto so this is the head of the chain.
|
||||
// we use this as the ptr to the start of the chain.
|
||||
// and we have summed up the length.
|
||||
assert(@hasDecl(S, "_prototype_root"));
|
||||
|
||||
const memory_ptr: [*]u8 = @ptrCast(@constCast(value));
|
||||
const memory_ptr: [*]const u8 = @ptrCast(value);
|
||||
const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits());
|
||||
allocator.rawFree(memory_ptr[0..len], new_align, @returnAddress());
|
||||
allocator.free(memory_ptr[0..len]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1910
src/browser/Page.zig
1910
src/browser/Page.zig
File diff suppressed because it is too large
Load Diff
@@ -1,884 +0,0 @@
|
||||
// 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 log = @import("../log.zig");
|
||||
|
||||
pub const Rule = union(enum) {
|
||||
allow: []const u8,
|
||||
disallow: []const u8,
|
||||
};
|
||||
|
||||
pub const Key = enum {
|
||||
@"user-agent",
|
||||
allow,
|
||||
disallow,
|
||||
};
|
||||
|
||||
/// https://www.rfc-editor.org/rfc/rfc9309.html
|
||||
pub const Robots = @This();
|
||||
pub const empty: Robots = .{ .rules = &.{} };
|
||||
|
||||
pub const RobotStore = struct {
|
||||
const RobotsEntry = union(enum) {
|
||||
present: Robots,
|
||||
absent,
|
||||
};
|
||||
|
||||
pub const RobotsMap = std.HashMapUnmanaged([]const u8, RobotsEntry, struct {
|
||||
const Context = @This();
|
||||
|
||||
pub fn hash(_: Context, value: []const u8) u32 {
|
||||
var hasher = std.hash.Wyhash.init(value.len);
|
||||
for (value) |c| {
|
||||
std.hash.autoHash(&hasher, std.ascii.toLower(c));
|
||||
}
|
||||
return @truncate(hasher.final());
|
||||
}
|
||||
|
||||
pub fn eql(_: Context, a: []const u8, b: []const u8) bool {
|
||||
return std.ascii.eqlIgnoreCase(a, b);
|
||||
}
|
||||
}, 80);
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
map: RobotsMap,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) RobotStore {
|
||||
return .{ .allocator = allocator, .map = .empty };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *RobotStore) void {
|
||||
var iter = self.map.iterator();
|
||||
|
||||
while (iter.next()) |entry| {
|
||||
self.allocator.free(entry.key_ptr.*);
|
||||
|
||||
switch (entry.value_ptr.*) {
|
||||
.present => |*robots| robots.deinit(self.allocator),
|
||||
.absent => {},
|
||||
}
|
||||
}
|
||||
|
||||
self.map.deinit(self.allocator);
|
||||
}
|
||||
|
||||
pub fn get(self: *RobotStore, url: []const u8) ?RobotsEntry {
|
||||
return self.map.get(url);
|
||||
}
|
||||
|
||||
pub fn robotsFromBytes(self: *RobotStore, user_agent: []const u8, bytes: []const u8) !Robots {
|
||||
return try Robots.fromBytes(self.allocator, user_agent, bytes);
|
||||
}
|
||||
|
||||
pub fn put(self: *RobotStore, url: []const u8, robots: Robots) !void {
|
||||
const duped = try self.allocator.dupe(u8, url);
|
||||
try self.map.put(self.allocator, duped, .{ .present = robots });
|
||||
}
|
||||
|
||||
pub fn putAbsent(self: *RobotStore, url: []const u8) !void {
|
||||
const duped = try self.allocator.dupe(u8, url);
|
||||
try self.map.put(self.allocator, duped, .absent);
|
||||
}
|
||||
};
|
||||
|
||||
rules: []const Rule,
|
||||
|
||||
const State = struct {
|
||||
entry: enum {
|
||||
not_in_entry,
|
||||
in_other_entry,
|
||||
in_our_entry,
|
||||
in_wildcard_entry,
|
||||
},
|
||||
has_rules: bool = false,
|
||||
};
|
||||
|
||||
fn freeRulesInList(allocator: std.mem.Allocator, rules: []const Rule) void {
|
||||
for (rules) |rule| {
|
||||
switch (rule) {
|
||||
.allow => |value| allocator.free(value),
|
||||
.disallow => |value| allocator.free(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parseRulesWithUserAgent(
|
||||
allocator: std.mem.Allocator,
|
||||
user_agent: []const u8,
|
||||
raw_bytes: []const u8,
|
||||
) ![]const Rule {
|
||||
var rules: std.ArrayList(Rule) = .empty;
|
||||
defer rules.deinit(allocator);
|
||||
|
||||
var wildcard_rules: std.ArrayList(Rule) = .empty;
|
||||
defer wildcard_rules.deinit(allocator);
|
||||
|
||||
var state: State = .{ .entry = .not_in_entry, .has_rules = false };
|
||||
|
||||
// https://en.wikipedia.org/wiki/Byte_order_mark
|
||||
const UTF8_BOM: []const u8 = &.{ 0xEF, 0xBB, 0xBF };
|
||||
|
||||
// Strip UTF8 BOM
|
||||
const bytes = if (std.mem.startsWith(u8, raw_bytes, UTF8_BOM))
|
||||
raw_bytes[3..]
|
||||
else
|
||||
raw_bytes;
|
||||
|
||||
var iter = std.mem.splitScalar(u8, bytes, '\n');
|
||||
while (iter.next()) |line| {
|
||||
const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace);
|
||||
|
||||
// Skip all comment lines.
|
||||
if (std.mem.startsWith(u8, trimmed, "#")) continue;
|
||||
|
||||
// Remove end of line comment.
|
||||
const true_line = if (std.mem.indexOfScalar(u8, trimmed, '#')) |pos|
|
||||
std.mem.trimRight(u8, trimmed[0..pos], &std.ascii.whitespace)
|
||||
else
|
||||
trimmed;
|
||||
|
||||
if (true_line.len == 0) continue;
|
||||
|
||||
const colon_idx = std.mem.indexOfScalar(u8, true_line, ':') orelse {
|
||||
log.warn(.browser, "robots line missing colon", .{ .line = line });
|
||||
continue;
|
||||
};
|
||||
const key_str = try std.ascii.allocLowerString(allocator, true_line[0..colon_idx]);
|
||||
defer allocator.free(key_str);
|
||||
|
||||
const key = std.meta.stringToEnum(Key, key_str) orelse continue;
|
||||
const value = std.mem.trim(u8, true_line[colon_idx + 1 ..], &std.ascii.whitespace);
|
||||
|
||||
switch (key) {
|
||||
.@"user-agent" => {
|
||||
if (state.has_rules) {
|
||||
state = .{ .entry = .not_in_entry, .has_rules = false };
|
||||
}
|
||||
|
||||
switch (state.entry) {
|
||||
.in_other_entry => {
|
||||
if (std.ascii.eqlIgnoreCase(user_agent, value)) {
|
||||
state.entry = .in_our_entry;
|
||||
}
|
||||
},
|
||||
.in_our_entry => {},
|
||||
.in_wildcard_entry => {
|
||||
if (std.ascii.eqlIgnoreCase(user_agent, value)) {
|
||||
state.entry = .in_our_entry;
|
||||
}
|
||||
},
|
||||
.not_in_entry => {
|
||||
if (std.ascii.eqlIgnoreCase(user_agent, value)) {
|
||||
state.entry = .in_our_entry;
|
||||
} else if (std.mem.eql(u8, "*", value)) {
|
||||
state.entry = .in_wildcard_entry;
|
||||
} else {
|
||||
state.entry = .in_other_entry;
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
.allow => {
|
||||
defer state.has_rules = true;
|
||||
|
||||
switch (state.entry) {
|
||||
.in_our_entry => {
|
||||
const duped_value = try allocator.dupe(u8, value);
|
||||
errdefer allocator.free(duped_value);
|
||||
try rules.append(allocator, .{ .allow = duped_value });
|
||||
},
|
||||
.in_other_entry => {},
|
||||
.in_wildcard_entry => {
|
||||
const duped_value = try allocator.dupe(u8, value);
|
||||
errdefer allocator.free(duped_value);
|
||||
try wildcard_rules.append(allocator, .{ .allow = duped_value });
|
||||
},
|
||||
.not_in_entry => {
|
||||
log.warn(.browser, "robots unexpected rule", .{ .rule = "allow" });
|
||||
continue;
|
||||
},
|
||||
}
|
||||
},
|
||||
.disallow => {
|
||||
defer state.has_rules = true;
|
||||
|
||||
switch (state.entry) {
|
||||
.in_our_entry => {
|
||||
const duped_value = try allocator.dupe(u8, value);
|
||||
errdefer allocator.free(duped_value);
|
||||
try rules.append(allocator, .{ .disallow = duped_value });
|
||||
},
|
||||
.in_other_entry => {},
|
||||
.in_wildcard_entry => {
|
||||
const duped_value = try allocator.dupe(u8, value);
|
||||
errdefer allocator.free(duped_value);
|
||||
try wildcard_rules.append(allocator, .{ .disallow = duped_value });
|
||||
},
|
||||
.not_in_entry => {
|
||||
log.warn(.browser, "robots unexpected rule", .{ .rule = "disallow" });
|
||||
continue;
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// If we have rules for our specific User-Agent, we will use those rules.
|
||||
// If we don't have any rules, we fallback to using the wildcard ("*") rules.
|
||||
if (rules.items.len > 0) {
|
||||
freeRulesInList(allocator, wildcard_rules.items);
|
||||
return try rules.toOwnedSlice(allocator);
|
||||
} else {
|
||||
freeRulesInList(allocator, rules.items);
|
||||
return try wildcard_rules.toOwnedSlice(allocator);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fromBytes(allocator: std.mem.Allocator, user_agent: []const u8, bytes: []const u8) !Robots {
|
||||
const rules = try parseRulesWithUserAgent(allocator, user_agent, bytes);
|
||||
return .{ .rules = rules };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Robots, allocator: std.mem.Allocator) void {
|
||||
freeRulesInList(allocator, self.rules);
|
||||
allocator.free(self.rules);
|
||||
}
|
||||
|
||||
fn matchPatternRecursive(pattern: []const u8, path: []const u8, exact_match: bool) bool {
|
||||
if (pattern.len == 0) return true;
|
||||
|
||||
const star_pos = std.mem.indexOfScalar(u8, pattern, '*') orelse {
|
||||
if (exact_match) {
|
||||
// If we end in '$', we must be exactly equal.
|
||||
return std.mem.eql(u8, path, pattern);
|
||||
} else {
|
||||
// Otherwise, we are just a prefix.
|
||||
return std.mem.startsWith(u8, path, pattern);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure the prefix before the '*' matches.
|
||||
if (!std.mem.startsWith(u8, path, pattern[0..star_pos])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const suffix_pattern = pattern[star_pos + 1 ..];
|
||||
if (suffix_pattern.len == 0) return true;
|
||||
|
||||
var i: usize = star_pos;
|
||||
while (i <= path.len) : (i += 1) {
|
||||
if (matchPatternRecursive(suffix_pattern, path[i..], exact_match)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// There are rules for how the pattern in robots.txt should be matched.
|
||||
///
|
||||
/// * should match 0 or more of any character.
|
||||
/// $ should signify the end of a path, making it exact.
|
||||
/// otherwise, it is a prefix path.
|
||||
fn matchPattern(pattern: []const u8, path: []const u8) ?usize {
|
||||
if (pattern.len == 0) return 0;
|
||||
const exact_match = pattern[pattern.len - 1] == '$';
|
||||
const inner_pattern = if (exact_match) pattern[0 .. pattern.len - 1] else pattern;
|
||||
|
||||
if (matchPatternRecursive(
|
||||
inner_pattern,
|
||||
path,
|
||||
exact_match,
|
||||
)) return pattern.len else return null;
|
||||
}
|
||||
|
||||
pub fn isAllowed(self: *const Robots, path: []const u8) bool {
|
||||
const rules = self.rules;
|
||||
|
||||
var longest_match_len: usize = 0;
|
||||
var is_allowed_result = true;
|
||||
|
||||
for (rules) |rule| {
|
||||
switch (rule) {
|
||||
.allow => |pattern| {
|
||||
if (matchPattern(pattern, path)) |len| {
|
||||
// Longest or Last Wins.
|
||||
if (len >= longest_match_len) {
|
||||
longest_match_len = len;
|
||||
is_allowed_result = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
.disallow => |pattern| {
|
||||
if (pattern.len == 0) continue;
|
||||
|
||||
if (matchPattern(pattern, path)) |len| {
|
||||
// Longest or Last Wins.
|
||||
if (len >= longest_match_len) {
|
||||
longest_match_len = len;
|
||||
is_allowed_result = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return is_allowed_result;
|
||||
}
|
||||
|
||||
test "Robots: simple robots.txt" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const file =
|
||||
\\User-agent: *
|
||||
\\Disallow: /private/
|
||||
\\Allow: /public/
|
||||
\\
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
;
|
||||
|
||||
const rules = try parseRulesWithUserAgent(allocator, "GoogleBot", file);
|
||||
defer {
|
||||
freeRulesInList(allocator, rules);
|
||||
allocator.free(rules);
|
||||
}
|
||||
|
||||
try std.testing.expectEqual(1, rules.len);
|
||||
try std.testing.expectEqualStrings("/admin/", rules[0].disallow);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - simple prefix" {
|
||||
try std.testing.expect(matchPattern("/admin", "/admin/page") != null);
|
||||
try std.testing.expect(matchPattern("/admin", "/admin") != null);
|
||||
try std.testing.expect(matchPattern("/admin", "/other") == null);
|
||||
try std.testing.expect(matchPattern("/admin/page", "/admin") == null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - single wildcard" {
|
||||
try std.testing.expect(matchPattern("/admin/*", "/admin/") != null);
|
||||
try std.testing.expect(matchPattern("/admin/*", "/admin/page") != null);
|
||||
try std.testing.expect(matchPattern("/admin/*", "/admin/page/subpage") != null);
|
||||
try std.testing.expect(matchPattern("/admin/*", "/other/page") == null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - wildcard in middle" {
|
||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/abc/def/xyz") != null);
|
||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/abc/def/ghi/xyz") != null);
|
||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/abc/def") == null);
|
||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/other/def/xyz") == null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - complex wildcard case" {
|
||||
try std.testing.expect(matchPattern("/abc/*/def/xyz", "/abc/def/def/xyz") != null);
|
||||
try std.testing.expect(matchPattern("/abc/*/def/xyz", "/abc/ANYTHING/def/xyz") != null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - multiple wildcards" {
|
||||
try std.testing.expect(matchPattern("/a/*/b/*/c", "/a/x/b/y/c") != null);
|
||||
try std.testing.expect(matchPattern("/a/*/b/*/c", "/a/x/y/b/z/w/c") != null);
|
||||
try std.testing.expect(matchPattern("/*.php", "/index.php") != null);
|
||||
try std.testing.expect(matchPattern("/*.php", "/admin/index.php") != null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - end anchor" {
|
||||
try std.testing.expect(matchPattern("/*.php$", "/index.php") != null);
|
||||
try std.testing.expect(matchPattern("/*.php$", "/index.php?param=value") == null);
|
||||
try std.testing.expect(matchPattern("/admin$", "/admin") != null);
|
||||
try std.testing.expect(matchPattern("/admin$", "/admin/") == null);
|
||||
try std.testing.expect(matchPattern("/fish$", "/fish") != null);
|
||||
try std.testing.expect(matchPattern("/fish$", "/fishheads") == null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - wildcard with extension" {
|
||||
try std.testing.expect(matchPattern("/fish*.php", "/fish.php") != null);
|
||||
try std.testing.expect(matchPattern("/fish*.php", "/fishheads.php") != null);
|
||||
try std.testing.expect(matchPattern("/fish*.php", "/fish/salmon.php") != null);
|
||||
try std.testing.expect(matchPattern("/fish*.php", "/fish.asp") == null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - empty and edge cases" {
|
||||
try std.testing.expect(matchPattern("", "/anything") != null);
|
||||
try std.testing.expect(matchPattern("/", "/") != null);
|
||||
try std.testing.expect(matchPattern("*", "/anything") != null);
|
||||
try std.testing.expect(matchPattern("/*", "/anything") != null);
|
||||
try std.testing.expect(matchPattern("$", "") != null);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - real world examples" {
|
||||
try std.testing.expect(matchPattern("/", "/anything") != null);
|
||||
|
||||
try std.testing.expect(matchPattern("/admin/", "/admin/page") != null);
|
||||
try std.testing.expect(matchPattern("/admin/", "/public/page") == null);
|
||||
|
||||
try std.testing.expect(matchPattern("/*.pdf$", "/document.pdf") != null);
|
||||
try std.testing.expect(matchPattern("/*.pdf$", "/document.pdf.bak") == null);
|
||||
|
||||
try std.testing.expect(matchPattern("/*?", "/page?param=value") != null);
|
||||
try std.testing.expect(matchPattern("/*?", "/page") == null);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - basic allow/disallow" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "MyBot",
|
||||
\\User-agent: MyBot
|
||||
\\Disallow: /admin/
|
||||
\\Allow: /public/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/") == true);
|
||||
try std.testing.expect(robots.isAllowed("/public/page") == true);
|
||||
try std.testing.expect(robots.isAllowed("/admin/secret") == false);
|
||||
try std.testing.expect(robots.isAllowed("/other/page") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - longest match wins" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "TestBot",
|
||||
\\User-agent: TestBot
|
||||
\\Disallow: /admin/
|
||||
\\Allow: /admin/public/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/admin/secret") == false);
|
||||
try std.testing.expect(robots.isAllowed("/admin/public/page") == true);
|
||||
try std.testing.expect(robots.isAllowed("/admin/public/") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - specific user-agent vs wildcard" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots1 = try Robots.fromBytes(allocator, "Googlebot",
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
\\User-agent: *
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
);
|
||||
defer robots1.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots1.isAllowed("/private/page") == false);
|
||||
try std.testing.expect(robots1.isAllowed("/admin/page") == true);
|
||||
|
||||
// Test with other bot (should use wildcard)
|
||||
var robots2 = try Robots.fromBytes(allocator, "OtherBot",
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
\\User-agent: *
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
);
|
||||
defer robots2.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots2.isAllowed("/private/page") == true);
|
||||
try std.testing.expect(robots2.isAllowed("/admin/page") == false);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - case insensitive user-agent" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots1 = try Robots.fromBytes(allocator, "googlebot",
|
||||
\\User-agent: GoogleBot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots1.deinit(allocator);
|
||||
try std.testing.expect(robots1.isAllowed("/private/") == false);
|
||||
|
||||
var robots2 = try Robots.fromBytes(allocator, "GOOGLEBOT",
|
||||
\\User-agent: GoogleBot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots2.deinit(allocator);
|
||||
try std.testing.expect(robots2.isAllowed("/private/") == false);
|
||||
|
||||
var robots3 = try Robots.fromBytes(allocator, "GoOgLeBoT",
|
||||
\\User-agent: GoogleBot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots3.deinit(allocator);
|
||||
try std.testing.expect(robots3.isAllowed("/private/") == false);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - merged rules for same agent" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Googlebot",
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/admin/page") == false);
|
||||
try std.testing.expect(robots.isAllowed("/private/page") == false);
|
||||
try std.testing.expect(robots.isAllowed("/public/page") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - wildcards in patterns" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Bot",
|
||||
\\User-agent: Bot
|
||||
\\Disallow: /*.php$
|
||||
\\Allow: /index.php$
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/page.php") == false);
|
||||
try std.testing.expect(robots.isAllowed("/index.php") == true);
|
||||
try std.testing.expect(robots.isAllowed("/page.php?param=1") == true);
|
||||
try std.testing.expect(robots.isAllowed("/page.html") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - empty disallow allows everything" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Bot",
|
||||
\\User-agent: Bot
|
||||
\\Disallow:
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/anything") == true);
|
||||
try std.testing.expect(robots.isAllowed("/") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - no rules" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Bot", "");
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/anything") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - disallow all" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Bot",
|
||||
\\User-agent: Bot
|
||||
\\Disallow: /
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/") == false);
|
||||
try std.testing.expect(robots.isAllowed("/anything") == false);
|
||||
try std.testing.expect(robots.isAllowed("/admin/page") == false);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - multiple user-agents in same entry" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots1 = try Robots.fromBytes(allocator, "Googlebot",
|
||||
\\User-agent: Googlebot
|
||||
\\User-agent: Bingbot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots1.deinit(allocator);
|
||||
try std.testing.expect(robots1.isAllowed("/private/") == false);
|
||||
|
||||
var robots2 = try Robots.fromBytes(allocator, "Bingbot",
|
||||
\\User-agent: Googlebot
|
||||
\\User-agent: Bingbot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots2.deinit(allocator);
|
||||
try std.testing.expect(robots2.isAllowed("/private/") == false);
|
||||
|
||||
var robots3 = try Robots.fromBytes(allocator, "OtherBot",
|
||||
\\User-agent: Googlebot
|
||||
\\User-agent: Bingbot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots3.deinit(allocator);
|
||||
try std.testing.expect(robots3.isAllowed("/private/") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - wildcard fallback" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "UnknownBot",
|
||||
\\User-agent: *
|
||||
\\Disallow: /admin/
|
||||
\\Allow: /admin/public/
|
||||
\\
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/admin/secret") == false);
|
||||
try std.testing.expect(robots.isAllowed("/admin/public/page") == true);
|
||||
try std.testing.expect(robots.isAllowed("/private/") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - complex real-world example" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "MyBot",
|
||||
\\User-agent: *
|
||||
\\Disallow: /cgi-bin/
|
||||
\\Disallow: /tmp/
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
\\User-agent: MyBot
|
||||
\\Disallow: /admin/
|
||||
\\Disallow: /*.pdf$
|
||||
\\Allow: /public/*.pdf$
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/") == true);
|
||||
try std.testing.expect(robots.isAllowed("/admin/dashboard") == false);
|
||||
try std.testing.expect(robots.isAllowed("/docs/guide.pdf") == false);
|
||||
try std.testing.expect(robots.isAllowed("/public/manual.pdf") == true);
|
||||
try std.testing.expect(robots.isAllowed("/page.html") == true);
|
||||
try std.testing.expect(robots.isAllowed("/cgi-bin/script.sh") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - order doesn't matter for same length" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Bot",
|
||||
\\User-agent: Bot
|
||||
\\ # WOW!!
|
||||
\\Allow: /page
|
||||
\\Disallow: /page
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/page") == false);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - empty file uses wildcard defaults" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "MyBot",
|
||||
\\User-agent: * # ABCDEF!!!
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/admin/") == false);
|
||||
try std.testing.expect(robots.isAllowed("/public/") == true);
|
||||
}
|
||||
test "Robots: isAllowed - wildcard entry with multiple user-agents including specific" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Googlebot",
|
||||
\\User-agent: *
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /shared/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/shared/") == false);
|
||||
try std.testing.expect(robots.isAllowed("/other/") == true);
|
||||
|
||||
var robots2 = try Robots.fromBytes(allocator, "Bingbot",
|
||||
\\User-agent: *
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /shared/
|
||||
\\
|
||||
);
|
||||
defer robots2.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots2.isAllowed("/shared/") == false);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - specific agent appears after wildcard in entry" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "MyBot",
|
||||
\\User-agent: *
|
||||
\\User-agent: MyBot
|
||||
\\User-agent: Bingbot
|
||||
\\Disallow: /admin/
|
||||
\\Allow: /admin/public/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/admin/secret") == false);
|
||||
try std.testing.expect(robots.isAllowed("/admin/public/page") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - wildcard should not override specific entry" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Googlebot",
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
\\User-agent: *
|
||||
\\User-agent: Googlebot
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/private/") == false);
|
||||
try std.testing.expect(robots.isAllowed("/admin/") == false);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - Google's real robots.txt" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Simplified version of google.com/robots.txt
|
||||
const google_robots =
|
||||
\\User-agent: *
|
||||
\\User-agent: Yandex
|
||||
\\Disallow: /search
|
||||
\\Allow: /search/about
|
||||
\\Allow: /search/howsearchworks
|
||||
\\Disallow: /imgres
|
||||
\\Disallow: /m?
|
||||
\\Disallow: /m/
|
||||
\\Allow: /m/finance
|
||||
\\Disallow: /maps/
|
||||
\\Allow: /maps/$
|
||||
\\Allow: /maps/@
|
||||
\\Allow: /maps/dir/
|
||||
\\Disallow: /shopping?
|
||||
\\Allow: /shopping?udm=28$
|
||||
\\
|
||||
\\User-agent: AdsBot-Google
|
||||
\\Disallow: /maps/api/js/
|
||||
\\Allow: /maps/api/js
|
||||
\\Disallow: /maps/api/staticmap
|
||||
\\
|
||||
\\User-agent: Yandex
|
||||
\\Disallow: /about/careers/applications/jobs/results
|
||||
\\
|
||||
\\User-agent: facebookexternalhit
|
||||
\\User-agent: Twitterbot
|
||||
\\Allow: /imgres
|
||||
\\Allow: /search
|
||||
\\Disallow: /groups
|
||||
\\Disallow: /m/
|
||||
\\
|
||||
;
|
||||
|
||||
var regular_bot = try Robots.fromBytes(allocator, "Googlebot", google_robots);
|
||||
defer regular_bot.deinit(allocator);
|
||||
|
||||
try std.testing.expect(regular_bot.isAllowed("/") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/search") == false);
|
||||
try std.testing.expect(regular_bot.isAllowed("/search/about") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/search/howsearchworks") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/imgres") == false);
|
||||
try std.testing.expect(regular_bot.isAllowed("/m/finance") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/m/other") == false);
|
||||
try std.testing.expect(regular_bot.isAllowed("/maps/") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/maps/@") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/shopping?udm=28") == true);
|
||||
try std.testing.expect(regular_bot.isAllowed("/shopping?udm=28&extra") == false);
|
||||
|
||||
var adsbot = try Robots.fromBytes(allocator, "AdsBot-Google", google_robots);
|
||||
defer adsbot.deinit(allocator);
|
||||
|
||||
try std.testing.expect(adsbot.isAllowed("/maps/api/js") == true);
|
||||
try std.testing.expect(adsbot.isAllowed("/maps/api/js/") == false);
|
||||
try std.testing.expect(adsbot.isAllowed("/maps/api/staticmap") == false);
|
||||
|
||||
var twitterbot = try Robots.fromBytes(allocator, "Twitterbot", google_robots);
|
||||
defer twitterbot.deinit(allocator);
|
||||
|
||||
try std.testing.expect(twitterbot.isAllowed("/imgres") == true);
|
||||
try std.testing.expect(twitterbot.isAllowed("/search") == true);
|
||||
try std.testing.expect(twitterbot.isAllowed("/groups") == false);
|
||||
try std.testing.expect(twitterbot.isAllowed("/m/") == false);
|
||||
}
|
||||
|
||||
test "Robots: user-agent after rules starts new entry" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const file =
|
||||
\\User-agent: Bot1
|
||||
\\User-agent: Bot2
|
||||
\\Disallow: /admin/
|
||||
\\Allow: /public/
|
||||
\\User-agent: Bot3
|
||||
\\Disallow: /private/
|
||||
\\
|
||||
;
|
||||
|
||||
var robots1 = try Robots.fromBytes(allocator, "Bot1", file);
|
||||
defer robots1.deinit(allocator);
|
||||
try std.testing.expect(robots1.isAllowed("/admin/") == false);
|
||||
try std.testing.expect(robots1.isAllowed("/public/") == true);
|
||||
try std.testing.expect(robots1.isAllowed("/private/") == true);
|
||||
|
||||
var robots2 = try Robots.fromBytes(allocator, "Bot2", file);
|
||||
defer robots2.deinit(allocator);
|
||||
try std.testing.expect(robots2.isAllowed("/admin/") == false);
|
||||
try std.testing.expect(robots2.isAllowed("/public/") == true);
|
||||
try std.testing.expect(robots2.isAllowed("/private/") == true);
|
||||
|
||||
var robots3 = try Robots.fromBytes(allocator, "Bot3", file);
|
||||
defer robots3.deinit(allocator);
|
||||
try std.testing.expect(robots3.isAllowed("/admin/") == true);
|
||||
try std.testing.expect(robots3.isAllowed("/public/") == true);
|
||||
try std.testing.expect(robots3.isAllowed("/private/") == false);
|
||||
}
|
||||
|
||||
test "Robots: blank lines don't end entries" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const file =
|
||||
\\User-agent: MyBot
|
||||
\\Disallow: /admin/
|
||||
\\
|
||||
\\
|
||||
\\Allow: /public/
|
||||
\\
|
||||
;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "MyBot", file);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/admin/") == false);
|
||||
try std.testing.expect(robots.isAllowed("/public/") == true);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -19,8 +19,8 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
|
||||
const log = @import("../log.zig");
|
||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
||||
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
@@ -47,15 +47,9 @@ pub fn init(allocator: std.mem.Allocator) Scheduler {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Scheduler) void {
|
||||
finalizeTasks(&self.low_priority);
|
||||
finalizeTasks(&self.high_priority);
|
||||
}
|
||||
|
||||
const AddOpts = struct {
|
||||
name: []const u8 = "",
|
||||
low_priority: bool = false,
|
||||
finalizer: ?Finalizer = null,
|
||||
};
|
||||
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
@@ -69,7 +63,6 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts
|
||||
.callback = cb,
|
||||
.sequence = seq,
|
||||
.name = opts.name,
|
||||
.finalizer = opts.finalizer,
|
||||
.run_at = milliTimestamp(.monotonic) + run_in_ms,
|
||||
});
|
||||
}
|
||||
@@ -79,11 +72,6 @@ pub fn run(self: *Scheduler) !?u64 {
|
||||
return self.runQueue(&self.high_priority);
|
||||
}
|
||||
|
||||
pub fn hasReadyTasks(self: *Scheduler) bool {
|
||||
const now = milliTimestamp(.monotonic);
|
||||
return queueuHasReadyTask(&self.low_priority, now) or queueuHasReadyTask(&self.high_priority, now);
|
||||
}
|
||||
|
||||
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
||||
if (queue.count() == 0) {
|
||||
return null;
|
||||
@@ -107,9 +95,7 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
||||
|
||||
if (repeat_in_ms) |ms| {
|
||||
// Task cannot be repeated immediately, and they should know that
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(ms != 0);
|
||||
}
|
||||
std.debug.assert(ms != 0);
|
||||
task.run_at = now + ms;
|
||||
try self.low_priority.add(task);
|
||||
}
|
||||
@@ -117,28 +103,12 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
||||
return null;
|
||||
}
|
||||
|
||||
fn queueuHasReadyTask(queue: *Queue, now: u64) bool {
|
||||
const task = queue.peek() orelse return false;
|
||||
return task.run_at <= now;
|
||||
}
|
||||
|
||||
fn finalizeTasks(queue: *Queue) void {
|
||||
var it = queue.iterator();
|
||||
while (it.next()) |t| {
|
||||
if (t.finalizer) |func| {
|
||||
func(t.ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Task = struct {
|
||||
run_at: u64,
|
||||
sequence: u64,
|
||||
ctx: *anyopaque,
|
||||
name: []const u8,
|
||||
callback: Callback,
|
||||
finalizer: ?Finalizer,
|
||||
};
|
||||
|
||||
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;
|
||||
const Finalizer = *const fn (ctx: *anyopaque) void;
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const js = @import("js/js.zig");
|
||||
@@ -31,7 +30,7 @@ const Http = @import("../http/Http.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArrayList = std.ArrayList;
|
||||
const ArrayListUnmanaged = std.ArrayListUnmanaged;
|
||||
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
@@ -138,12 +137,6 @@ fn clearList(list: *std.DoublyLinkedList) void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getHeaders(self: *ScriptManager, url: [:0]const u8) !Http.Headers {
|
||||
var headers = try self.client.newHeaders();
|
||||
try self.page.headersForRequest(self.page.arena, url, &headers);
|
||||
return headers;
|
||||
}
|
||||
|
||||
pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_element: *Element.Html.Script, comptime ctx: []const u8) !void {
|
||||
if (script_element._executed) {
|
||||
// If a script tag gets dynamically created and added to the dom:
|
||||
@@ -158,14 +151,14 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
script_element._executed = true;
|
||||
|
||||
const element = script_element.asElement();
|
||||
if (element.getAttributeSafe(comptime .wrap("nomodule")) != null) {
|
||||
if (element.getAttributeSafe("nomodule") != null) {
|
||||
// these scripts should only be loaded if we don't support modules
|
||||
// but since we do support modules, we can just skip them.
|
||||
return;
|
||||
}
|
||||
|
||||
const kind: Script.Kind = blk: {
|
||||
const script_type = element.getAttributeSafe(comptime .wrap("type")) orelse break :blk .javascript;
|
||||
const script_type = element.getAttributeSafe("type") orelse break :blk .javascript;
|
||||
if (script_type.len == 0) {
|
||||
break :blk .javascript;
|
||||
}
|
||||
@@ -192,7 +185,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
var source: Script.Source = undefined;
|
||||
var remote_url: ?[:0]const u8 = null;
|
||||
const base_url = page.base();
|
||||
if (element.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||
if (element.getAttributeSafe("src")) |src| {
|
||||
if (try parseDataURI(page.arena, src)) |data_uri| {
|
||||
source = .{ .@"inline" = data_uri };
|
||||
} else {
|
||||
@@ -223,12 +216,12 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
break :blk if (kind == .module) .@"defer" else .normal;
|
||||
}
|
||||
|
||||
if (element.getAttributeSafe(comptime .wrap("async")) != null) {
|
||||
if (element.getAttributeSafe("async") != null) {
|
||||
break :blk .async;
|
||||
}
|
||||
|
||||
// Check for defer or module (before checking dynamic script default)
|
||||
if (kind == .module or element.getAttributeSafe(comptime .wrap("defer")) != null) {
|
||||
if (kind == .module or element.getAttributeSafe("defer") != null) {
|
||||
break :blk .@"defer";
|
||||
}
|
||||
|
||||
@@ -246,27 +239,20 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
};
|
||||
|
||||
const is_blocking = script.mode == .normal;
|
||||
if (is_blocking == false) {
|
||||
self.scriptList(script).append(&script.node);
|
||||
}
|
||||
|
||||
if (remote_url) |url| {
|
||||
errdefer {
|
||||
if (is_blocking == false) {
|
||||
self.scriptList(script).remove(&script.node);
|
||||
}
|
||||
script.deinit(true);
|
||||
}
|
||||
errdefer script.deinit(true);
|
||||
|
||||
var headers = try self.client.newHeaders();
|
||||
try page.requestCookie(.{}).headersForRequest(page.arena, url, &headers);
|
||||
|
||||
try self.client.request(.{
|
||||
.url = url,
|
||||
.ctx = script,
|
||||
.method = .GET,
|
||||
.headers = try self.getHeaders(url),
|
||||
.headers = headers,
|
||||
.blocking = is_blocking,
|
||||
.cookie_jar = &page._session.cookie_jar,
|
||||
.resource_type = .script,
|
||||
.notification = page._session.notification,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||
.header_callback = Script.headerCallback,
|
||||
.data_callback = Script.dataCallback,
|
||||
@@ -275,20 +261,18 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
});
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
log.debug(.http, "script queue", .{
|
||||
.ctx = ctx,
|
||||
.url = remote_url.?,
|
||||
.element = element,
|
||||
.stack = ls.local.stackTrace() catch "???",
|
||||
.stack = page.js.stackTrace() catch "???",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (is_blocking == false) {
|
||||
const list = self.scriptList(script);
|
||||
list.append(&script.node);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -361,16 +345,15 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
||||
.manager = self,
|
||||
};
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
self.page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
var headers = try self.client.newHeaders();
|
||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.http, "script queue", .{
|
||||
.url = url,
|
||||
.ctx = "module",
|
||||
.referrer = referrer,
|
||||
.stack = ls.local.stackTrace() catch "???",
|
||||
.stack = self.page.js.stackTrace() catch "???",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -378,10 +361,9 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
||||
.url = url,
|
||||
.ctx = script,
|
||||
.method = .GET,
|
||||
.headers = try self.getHeaders(url),
|
||||
.headers = headers,
|
||||
.cookie_jar = &self.page._session.cookie_jar,
|
||||
.resource_type = .script,
|
||||
.notification = self.page._session.notification,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||
.header_callback = Script.headerCallback,
|
||||
.data_callback = Script.dataCallback,
|
||||
@@ -454,16 +436,15 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
||||
} },
|
||||
};
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
self.page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
var headers = try self.client.newHeaders();
|
||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.http, "script queue", .{
|
||||
.url = url,
|
||||
.ctx = "dynamic module",
|
||||
.referrer = referrer,
|
||||
.stack = ls.local.stackTrace() catch "???",
|
||||
.stack = self.page.js.stackTrace() catch "???",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -479,11 +460,10 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
||||
try self.client.request(.{
|
||||
.url = url,
|
||||
.method = .GET,
|
||||
.headers = try self.getHeaders(url),
|
||||
.headers = headers,
|
||||
.ctx = script,
|
||||
.resource_type = .script,
|
||||
.cookie_jar = &self.page._session.cookie_jar,
|
||||
.notification = self.page._session.notification,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||
.header_callback = Script.headerCallback,
|
||||
.data_callback = Script.dataCallback,
|
||||
@@ -497,7 +477,7 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
||||
// Called from the Page to let us know it's done parsing the HTML. Necessary that
|
||||
// we know this so that we know that we can start evaluating deferred scripts.
|
||||
pub fn staticScriptsDone(self: *ScriptManager) void {
|
||||
lp.assert(self.static_scripts_done == false, "ScriptManager.staticScriptsDone", .{});
|
||||
std.debug.assert(self.static_scripts_done == false);
|
||||
self.static_scripts_done = true;
|
||||
self.evaluate();
|
||||
}
|
||||
@@ -634,7 +614,7 @@ pub const Script = struct {
|
||||
|
||||
const Source = union(enum) {
|
||||
@"inline": []const u8,
|
||||
remote: std.ArrayList(u8),
|
||||
remote: std.ArrayListUnmanaged(u8),
|
||||
|
||||
fn content(self: Source) []const u8 {
|
||||
return switch (self) {
|
||||
@@ -663,7 +643,7 @@ pub const Script = struct {
|
||||
log.debug(.http, "script fetch start", .{ .req = transfer });
|
||||
}
|
||||
|
||||
fn headerCallback(transfer: *Http.Transfer) !bool {
|
||||
fn headerCallback(transfer: *Http.Transfer) !void {
|
||||
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
|
||||
const header = &transfer.response_header.?;
|
||||
self.status = header.status;
|
||||
@@ -673,7 +653,7 @@ pub const Script = struct {
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
@@ -684,13 +664,16 @@ pub const Script = struct {
|
||||
});
|
||||
}
|
||||
|
||||
lp.assert(self.source.remote.capacity == 0, "ScriptManager.HeaderCallback", .{ .capacity = self.source.remote.capacity });
|
||||
// If this isn't true, then we'll likely leak memory. If you don't
|
||||
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
|
||||
// will fail. This assertion exists to catch incorrect assumptions about
|
||||
// how libcurl works, or about how we've configured it.
|
||||
std.debug.assert(self.source.remote.capacity == 0);
|
||||
var buffer = self.manager.buffer_pool.get();
|
||||
if (transfer.getContentLength()) |cl| {
|
||||
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
|
||||
}
|
||||
self.source = .{ .remote = buffer };
|
||||
return true;
|
||||
}
|
||||
|
||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
@@ -730,7 +713,7 @@ pub const Script = struct {
|
||||
log.warn(.http, "script fetch error", .{
|
||||
.err = err,
|
||||
.req = self.url,
|
||||
.mode = std.meta.activeTag(self.mode),
|
||||
.mode = self.mode,
|
||||
.kind = self.kind,
|
||||
.status = self.status,
|
||||
});
|
||||
@@ -750,13 +733,9 @@ pub const Script = struct {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (self.mode) {
|
||||
.import_async => |ia| ia.callback(ia.data, error.FailedToLoad),
|
||||
.import => {
|
||||
const entry = manager.imported_modules.getPtr(self.url).?;
|
||||
entry.state = .err;
|
||||
},
|
||||
else => {},
|
||||
if (self.mode == .import) {
|
||||
const entry = self.manager.imported_modules.getPtr(self.url).?;
|
||||
entry.state = .err;
|
||||
}
|
||||
self.deinit(true);
|
||||
manager.evaluate();
|
||||
@@ -764,12 +743,10 @@ pub const Script = struct {
|
||||
|
||||
fn eval(self: *Script, page: *Page) void {
|
||||
// never evaluated, source is passed back to v8, via callbacks.
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.mode != .import_async);
|
||||
std.debug.assert(self.mode != .import_async);
|
||||
|
||||
// never evaluated, source is passed back to v8 when asked for it.
|
||||
std.debug.assert(self.mode != .import);
|
||||
}
|
||||
// never evaluated, source is passed back to v8 when asked for it.
|
||||
std.debug.assert(self.mode != .import);
|
||||
|
||||
if (page.isGoingAway()) {
|
||||
// don't evaluate scripts for a dying page.
|
||||
@@ -798,12 +775,6 @@ pub const Script = struct {
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
const local = &ls.local;
|
||||
|
||||
// Handle importmap special case here: the content is a JSON containing
|
||||
// imports.
|
||||
if (self.kind == .importmap) {
|
||||
@@ -814,24 +785,25 @@ pub const Script = struct {
|
||||
.kind = self.kind,
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
self.executeCallback("error", local.toLocal(script_element._on_error), page);
|
||||
self.executeCallback("error", script_element._on_error, page);
|
||||
return;
|
||||
};
|
||||
self.executeCallback("load", local.toLocal(script_element._on_load), page);
|
||||
self.executeCallback("load", script_element._on_load, page);
|
||||
return;
|
||||
}
|
||||
|
||||
const js_context = page.js;
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(local);
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const success = blk: {
|
||||
const content = self.source.content();
|
||||
switch (self.kind) {
|
||||
.javascript => _ = local.eval(content, url) catch break :blk false,
|
||||
.javascript => _ = js_context.eval(content, url) catch break :blk false,
|
||||
.module => {
|
||||
// We don't care about waiting for the evaluation here.
|
||||
page.js.module(false, local, content, url, cacheable) catch break :blk false;
|
||||
js_context.module(false, content, url, cacheable) catch break :blk false;
|
||||
},
|
||||
.importmap => unreachable, // handled before the try/catch.
|
||||
}
|
||||
@@ -844,32 +816,34 @@ pub const Script = struct {
|
||||
|
||||
defer {
|
||||
// We should run microtasks even if script execution fails.
|
||||
local.runMicrotasks();
|
||||
_ = page.js.scheduler.run() catch |err| {
|
||||
page.js.runMicrotasks();
|
||||
_ = page.scheduler.run() catch |err| {
|
||||
log.err(.page, "scheduler", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
if (success) {
|
||||
self.executeCallback("load", local.toLocal(script_element._on_load), page);
|
||||
self.executeCallback("load", script_element._on_load, page);
|
||||
return;
|
||||
}
|
||||
|
||||
const caught = try_catch.caughtOrError(page.call_arena, error.Unknown);
|
||||
const msg = try_catch.err(page.arena) catch |err| @errorName(err) orelse "unknown";
|
||||
log.warn(.js, "eval script", .{
|
||||
.url = url,
|
||||
.caught = caught,
|
||||
.err = msg,
|
||||
.stack = try_catch.stack(page.call_arena) catch null,
|
||||
.line = try_catch.sourceLineNumber() orelse 0,
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
|
||||
self.executeCallback("error", local.toLocal(script_element._on_error), page);
|
||||
self.executeCallback("error", script_element._on_error, page);
|
||||
}
|
||||
|
||||
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void {
|
||||
const cb = cb_ orelse return;
|
||||
|
||||
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", .{
|
||||
.url = self.url,
|
||||
.type = typ,
|
||||
@@ -877,14 +851,14 @@ pub const Script = struct {
|
||||
});
|
||||
return;
|
||||
};
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
cb.tryCall(void, .{event}, &caught) catch {
|
||||
var result: js.Function.Result = undefined;
|
||||
cb.tryCall(void, .{event}, &result) catch {
|
||||
log.warn(.js, "script callback", .{
|
||||
.url = self.url,
|
||||
.type = typ,
|
||||
.caught = caught,
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -901,7 +875,7 @@ const BufferPool = struct {
|
||||
|
||||
const Container = struct {
|
||||
node: List.Node,
|
||||
buf: std.ArrayList(u8),
|
||||
buf: std.ArrayListUnmanaged(u8),
|
||||
};
|
||||
|
||||
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
|
||||
@@ -926,7 +900,7 @@ const BufferPool = struct {
|
||||
self.mem_pool.deinit();
|
||||
}
|
||||
|
||||
fn get(self: *BufferPool) std.ArrayList(u8) {
|
||||
fn get(self: *BufferPool) std.ArrayListUnmanaged(u8) {
|
||||
const node = self.available.popFirst() orelse {
|
||||
// return a new buffer
|
||||
return .{};
|
||||
@@ -938,7 +912,7 @@ const BufferPool = struct {
|
||||
return container.buf;
|
||||
}
|
||||
|
||||
fn release(self: *BufferPool, buffer: ArrayList(u8)) void {
|
||||
fn release(self: *BufferPool, buffer: ArrayListUnmanaged(u8)) void {
|
||||
// create mutable copy
|
||||
var b = buffer;
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const log = @import("../log.zig");
|
||||
|
||||
@@ -28,7 +27,6 @@ const History = @import("webapi/History.zig");
|
||||
|
||||
const Page = @import("Page.zig");
|
||||
const Browser = @import("Browser.zig");
|
||||
const Notification = @import("../Notification.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
@@ -40,7 +38,6 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
const Session = @This();
|
||||
|
||||
browser: *Browser,
|
||||
notification: *Notification,
|
||||
|
||||
// Used to create our Inspector and in the BrowserContext.
|
||||
arena: Allocator,
|
||||
@@ -55,27 +52,30 @@ arena: Allocator,
|
||||
// page and start another.
|
||||
transfer_arena: Allocator,
|
||||
|
||||
executor: js.ExecutionWorld,
|
||||
cookie_jar: storage.Cookie.Jar,
|
||||
storage_shed: storage.Shed,
|
||||
|
||||
history: History,
|
||||
navigation: Navigation,
|
||||
|
||||
page: ?Page,
|
||||
page: ?*Page = null,
|
||||
|
||||
pub fn init(self: *Session, browser: *Browser) !void {
|
||||
var executor = try browser.env.newExecutionWorld();
|
||||
errdefer executor.deinit();
|
||||
|
||||
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
|
||||
const allocator = browser.app.allocator;
|
||||
const session_allocator = browser.session_arena.allocator();
|
||||
|
||||
self.* = .{
|
||||
.page = null,
|
||||
.history = .{},
|
||||
.navigation = .{},
|
||||
.storage_shed = .{},
|
||||
.browser = browser,
|
||||
.notification = notification,
|
||||
.executor = executor,
|
||||
.storage_shed = .{},
|
||||
.arena = session_allocator,
|
||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||
.navigation = .{},
|
||||
.history = .{},
|
||||
.transfer_arena = browser.transfer_arena.allocator(),
|
||||
};
|
||||
}
|
||||
@@ -86,18 +86,19 @@ pub fn deinit(self: *Session) void {
|
||||
}
|
||||
self.cookie_jar.deinit();
|
||||
self.storage_shed.deinit(self.browser.app.allocator);
|
||||
self.executor.deinit();
|
||||
}
|
||||
|
||||
// NOTE: the caller is not the owner of the returned value,
|
||||
// the pointer on Page is just returned as a convenience
|
||||
pub fn createPage(self: *Session) !*Page {
|
||||
lp.assert(self.page == null, "Session.createPage - page not null", .{});
|
||||
std.debug.assert(self.page == null);
|
||||
|
||||
_ = self.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
const page_arena = &self.browser.page_arena;
|
||||
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, self);
|
||||
self.page = try Page.init(page_arena.allocator(), self.browser.call_arena.allocator(), self);
|
||||
const page = self.page.?;
|
||||
|
||||
// Creates a new NavigationEventTarget for this page.
|
||||
try self.navigation.onNewPage(page);
|
||||
@@ -107,15 +108,16 @@ pub fn createPage(self: *Session) !*Page {
|
||||
}
|
||||
// start JS env
|
||||
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||
self.notification.dispatch(.page_created, page);
|
||||
self.browser.notification.dispatch(.page_created, page);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
pub fn removePage(self: *Session) void {
|
||||
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
|
||||
self.notification.dispatch(.page_remove, .{});
|
||||
lp.assert(self.page != null, "Session.removePage - page is null", .{});
|
||||
self.browser.notification.dispatch(.page_remove, .{});
|
||||
|
||||
std.debug.assert(self.page != null);
|
||||
|
||||
self.page.?.deinit();
|
||||
self.page = null;
|
||||
@@ -128,29 +130,22 @@ pub fn removePage(self: *Session) void {
|
||||
}
|
||||
|
||||
pub fn currentPage(self: *Session) ?*Page {
|
||||
return &(self.page orelse return null);
|
||||
return self.page orelse return null;
|
||||
}
|
||||
|
||||
pub const WaitResult = enum {
|
||||
done,
|
||||
no_page,
|
||||
cdp_socket,
|
||||
navigate,
|
||||
};
|
||||
|
||||
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
||||
while (true) {
|
||||
if (self.page) |*page| {
|
||||
switch (page.wait(wait_ms)) {
|
||||
.done => {
|
||||
if (page._queued_navigation == null) {
|
||||
return .done;
|
||||
}
|
||||
self.processScheduledNavigation() catch return .done;
|
||||
},
|
||||
else => |result| return result,
|
||||
}
|
||||
} else {
|
||||
return .no_page;
|
||||
const page = self.page orelse return .no_page;
|
||||
switch (page.wait(wait_ms)) {
|
||||
.navigate => self.processScheduledNavigation() catch return .done,
|
||||
else => |result| return result,
|
||||
}
|
||||
// if we've successfull navigated, we'll give the new page another
|
||||
// page.wait(wait_ms)
|
||||
@@ -158,32 +153,24 @@ pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
||||
}
|
||||
|
||||
fn processScheduledNavigation(self: *Session) !void {
|
||||
const qn = self.page.?._queued_navigation.?;
|
||||
defer _ = self.browser.transfer_arena.reset(.{ .retain_with_limit = 8 * 1024 });
|
||||
const url, const opts = blk: {
|
||||
const qn = self.page.?._queued_navigation.?;
|
||||
// qn might not be safe to use after self.removePage is called, hence
|
||||
// this block;
|
||||
const url = qn.url;
|
||||
const opts = qn.opts;
|
||||
|
||||
// This was already aborted on the page, but it would be pretty
|
||||
// bad if old requests went to the new page, so let's make double sure
|
||||
self.browser.http_client.abort();
|
||||
self.removePage();
|
||||
|
||||
break :blk .{ url, opts };
|
||||
};
|
||||
// This was already aborted on the page, but it would be pretty
|
||||
// bad if old requests went to the new page, so let's make double sure
|
||||
self.browser.http_client.abort();
|
||||
self.removePage();
|
||||
|
||||
const page = self.createPage() catch |err| {
|
||||
log.err(.browser, "queued navigation page error", .{
|
||||
.err = err,
|
||||
.url = url,
|
||||
.url = qn.url,
|
||||
});
|
||||
return err;
|
||||
};
|
||||
|
||||
page.navigate(url, opts) catch |err| {
|
||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = url });
|
||||
page.navigate(qn.url, qn.opts) catch |err| {
|
||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -76,9 +76,8 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
||||
}
|
||||
|
||||
// trailing space so that we always have space to append the null terminator
|
||||
// and so that we can compare the next two characters without needing to length check
|
||||
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
|
||||
const end = out.len - 2;
|
||||
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
|
||||
const end = out.len - 1;
|
||||
|
||||
const path_marker = path_start + 1;
|
||||
|
||||
@@ -88,39 +87,33 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
||||
var in_i: usize = 0;
|
||||
var out_i: usize = 0;
|
||||
while (in_i < end) {
|
||||
if (out[in_i] == '.' and (out_i == 0 or out[out_i - 1] == '/')) {
|
||||
if (out[in_i + 1] == '/') { // always safe, because we added a whitespace
|
||||
// /./
|
||||
in_i += 2;
|
||||
continue;
|
||||
}
|
||||
if (out[in_i + 1] == '.' and out[in_i + 2] == '/') { // always safe, because we added two whitespaces
|
||||
// /../
|
||||
if (out_i > path_marker) {
|
||||
// go back before the /
|
||||
out_i -= 2;
|
||||
while (out_i > 1 and out[out_i - 1] != '/') {
|
||||
out_i -= 1;
|
||||
}
|
||||
} else {
|
||||
// if out_i == path_marker, than we've reached the start of
|
||||
// the path. We can't ../ any more. E.g.:
|
||||
// http://www.example.com/../hello.
|
||||
// You might think that's an error, but, at least with
|
||||
// new URL('../hello', 'http://www.example.com/')
|
||||
// it just ignores the extra ../
|
||||
}
|
||||
in_i += 3;
|
||||
continue;
|
||||
}
|
||||
if (in_i == end - 1) {
|
||||
// ignore trailing dot
|
||||
break;
|
||||
}
|
||||
if (std.mem.startsWith(u8, out[in_i..], "./")) {
|
||||
in_i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
const c = out[in_i];
|
||||
out[out_i] = c;
|
||||
if (std.mem.startsWith(u8, out[in_i..], "../")) {
|
||||
std.debug.assert(out[out_i - 1] == '/');
|
||||
|
||||
if (out_i > path_marker) {
|
||||
// go back before the /
|
||||
out_i -= 2;
|
||||
while (out_i > 1 and out[out_i - 1] != '/') {
|
||||
out_i -= 1;
|
||||
}
|
||||
} else {
|
||||
// if out_i == path_marker, than we've reached the start of
|
||||
// the path. We can't ../ any more. E.g.:
|
||||
// http://www.example.com/../hello.
|
||||
// You might think that's an error, but, at least with
|
||||
// new URL('../hello', 'http://www.example.com/')
|
||||
// it just ignores the extra ../
|
||||
}
|
||||
in_i += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
out[out_i] = out[in_i];
|
||||
in_i += 1;
|
||||
out_i += 1;
|
||||
}
|
||||
@@ -502,16 +495,6 @@ pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []cons
|
||||
return buf.items[0 .. buf.items.len - 1 :0];
|
||||
}
|
||||
|
||||
pub fn getRobotsUrl(arena: Allocator, url: [:0]const u8) ![:0]const u8 {
|
||||
const origin = try getOrigin(arena, url) orelse return error.NoOrigin;
|
||||
return try std.fmt.allocPrintSentinel(
|
||||
arena,
|
||||
"{s}/robots.txt",
|
||||
.{origin},
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "URL: isCompleteHTTPUrl" {
|
||||
try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about"));
|
||||
@@ -558,21 +541,6 @@ test "URL: resolve" {
|
||||
};
|
||||
|
||||
const cases = [_]Case{
|
||||
.{
|
||||
.base = "https://example/dir",
|
||||
.path = "abc../test",
|
||||
.expected = "https://example/abc../test",
|
||||
},
|
||||
.{
|
||||
.base = "https://example/dir",
|
||||
.path = "abc.",
|
||||
.expected = "https://example/abc.",
|
||||
},
|
||||
.{
|
||||
.base = "https://example/dir",
|
||||
.path = "abc/.",
|
||||
.expected = "https://example/abc/",
|
||||
},
|
||||
.{
|
||||
.base = "https://example/xyz/abc/123",
|
||||
.path = "something.js",
|
||||
@@ -788,31 +756,3 @@ test "URL: concatQueryString" {
|
||||
try testing.expectEqual("https://www.lightpanda.io/index?1=2&a=b", url);
|
||||
}
|
||||
}
|
||||
|
||||
test "URL: getRobotsUrl" {
|
||||
defer testing.reset();
|
||||
const arena = testing.arena_allocator;
|
||||
|
||||
{
|
||||
const url = try getRobotsUrl(arena, "https://www.lightpanda.io");
|
||||
try testing.expectEqual("https://www.lightpanda.io/robots.txt", url);
|
||||
}
|
||||
|
||||
{
|
||||
const url = try getRobotsUrl(arena, "https://www.lightpanda.io/some/path");
|
||||
try testing.expectString("https://www.lightpanda.io/robots.txt", url);
|
||||
}
|
||||
|
||||
{
|
||||
const url = try getRobotsUrl(arena, "https://www.lightpanda.io:8080/page");
|
||||
try testing.expectString("https://www.lightpanda.io:8080/robots.txt", url);
|
||||
}
|
||||
{
|
||||
const url = try getRobotsUrl(arena, "http://example.com/deep/nested/path?query=value#fragment");
|
||||
try testing.expectString("http://example.com/robots.txt", url);
|
||||
}
|
||||
{
|
||||
const url = try getRobotsUrl(arena, "https://user:pass@example.com/page");
|
||||
try testing.expectString("https://example.com/robots.txt", url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
|
||||
pub fn isHexColor(value: []const u8) bool {
|
||||
if (value.len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value[0] != '#') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hex_part = value[1..];
|
||||
switch (hex_part.len) {
|
||||
3, 4, 6, 8 => for (hex_part) |c| if (!std.ascii.isHex(c)) return false,
|
||||
else => return false,
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub const RGBA = packed struct(u32) {
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
/// Opaque by default.
|
||||
a: u8 = std.math.maxInt(u8),
|
||||
|
||||
pub const Named = struct {
|
||||
// Basic colors (CSS Level 1)
|
||||
pub const black: RGBA = .init(0, 0, 0, 1);
|
||||
pub const silver: RGBA = .init(192, 192, 192, 1);
|
||||
pub const gray: RGBA = .init(128, 128, 128, 1);
|
||||
pub const white: RGBA = .init(255, 255, 255, 1);
|
||||
pub const maroon: RGBA = .init(128, 0, 0, 1);
|
||||
pub const red: RGBA = .init(255, 0, 0, 1);
|
||||
pub const purple: RGBA = .init(128, 0, 128, 1);
|
||||
pub const fuchsia: RGBA = .init(255, 0, 255, 1);
|
||||
pub const green: RGBA = .init(0, 128, 0, 1);
|
||||
pub const lime: RGBA = .init(0, 255, 0, 1);
|
||||
pub const olive: RGBA = .init(128, 128, 0, 1);
|
||||
pub const yellow: RGBA = .init(255, 255, 0, 1);
|
||||
pub const navy: RGBA = .init(0, 0, 128, 1);
|
||||
pub const blue: RGBA = .init(0, 0, 255, 1);
|
||||
pub const teal: RGBA = .init(0, 128, 128, 1);
|
||||
pub const aqua: RGBA = .init(0, 255, 255, 1);
|
||||
|
||||
// Extended colors (CSS Level 2+)
|
||||
pub const aliceblue: RGBA = .init(240, 248, 255, 1);
|
||||
pub const antiquewhite: RGBA = .init(250, 235, 215, 1);
|
||||
pub const aquamarine: RGBA = .init(127, 255, 212, 1);
|
||||
pub const azure: RGBA = .init(240, 255, 255, 1);
|
||||
pub const beige: RGBA = .init(245, 245, 220, 1);
|
||||
pub const bisque: RGBA = .init(255, 228, 196, 1);
|
||||
pub const blanchedalmond: RGBA = .init(255, 235, 205, 1);
|
||||
pub const blueviolet: RGBA = .init(138, 43, 226, 1);
|
||||
pub const brown: RGBA = .init(165, 42, 42, 1);
|
||||
pub const burlywood: RGBA = .init(222, 184, 135, 1);
|
||||
pub const cadetblue: RGBA = .init(95, 158, 160, 1);
|
||||
pub const chartreuse: RGBA = .init(127, 255, 0, 1);
|
||||
pub const chocolate: RGBA = .init(210, 105, 30, 1);
|
||||
pub const coral: RGBA = .init(255, 127, 80, 1);
|
||||
pub const cornflowerblue: RGBA = .init(100, 149, 237, 1);
|
||||
pub const cornsilk: RGBA = .init(255, 248, 220, 1);
|
||||
pub const crimson: RGBA = .init(220, 20, 60, 1);
|
||||
pub const cyan: RGBA = .init(0, 255, 255, 1); // Synonym of aqua
|
||||
pub const darkblue: RGBA = .init(0, 0, 139, 1);
|
||||
pub const darkcyan: RGBA = .init(0, 139, 139, 1);
|
||||
pub const darkgoldenrod: RGBA = .init(184, 134, 11, 1);
|
||||
pub const darkgray: RGBA = .init(169, 169, 169, 1);
|
||||
pub const darkgreen: RGBA = .init(0, 100, 0, 1);
|
||||
pub const darkgrey: RGBA = .init(169, 169, 169, 1); // Synonym of darkgray
|
||||
pub const darkkhaki: RGBA = .init(189, 183, 107, 1);
|
||||
pub const darkmagenta: RGBA = .init(139, 0, 139, 1);
|
||||
pub const darkolivegreen: RGBA = .init(85, 107, 47, 1);
|
||||
pub const darkorange: RGBA = .init(255, 140, 0, 1);
|
||||
pub const darkorchid: RGBA = .init(153, 50, 204, 1);
|
||||
pub const darkred: RGBA = .init(139, 0, 0, 1);
|
||||
pub const darksalmon: RGBA = .init(233, 150, 122, 1);
|
||||
pub const darkseagreen: RGBA = .init(143, 188, 143, 1);
|
||||
pub const darkslateblue: RGBA = .init(72, 61, 139, 1);
|
||||
pub const darkslategray: RGBA = .init(47, 79, 79, 1);
|
||||
pub const darkslategrey: RGBA = .init(47, 79, 79, 1); // Synonym of darkslategray
|
||||
pub const darkturquoise: RGBA = .init(0, 206, 209, 1);
|
||||
pub const darkviolet: RGBA = .init(148, 0, 211, 1);
|
||||
pub const deeppink: RGBA = .init(255, 20, 147, 1);
|
||||
pub const deepskyblue: RGBA = .init(0, 191, 255, 1);
|
||||
pub const dimgray: RGBA = .init(105, 105, 105, 1);
|
||||
pub const dimgrey: RGBA = .init(105, 105, 105, 1); // Synonym of dimgray
|
||||
pub const dodgerblue: RGBA = .init(30, 144, 255, 1);
|
||||
pub const firebrick: RGBA = .init(178, 34, 34, 1);
|
||||
pub const floralwhite: RGBA = .init(255, 250, 240, 1);
|
||||
pub const forestgreen: RGBA = .init(34, 139, 34, 1);
|
||||
pub const gainsboro: RGBA = .init(220, 220, 220, 1);
|
||||
pub const ghostwhite: RGBA = .init(248, 248, 255, 1);
|
||||
pub const gold: RGBA = .init(255, 215, 0, 1);
|
||||
pub const goldenrod: RGBA = .init(218, 165, 32, 1);
|
||||
pub const greenyellow: RGBA = .init(173, 255, 47, 1);
|
||||
pub const grey: RGBA = .init(128, 128, 128, 1); // Synonym of gray
|
||||
pub const honeydew: RGBA = .init(240, 255, 240, 1);
|
||||
pub const hotpink: RGBA = .init(255, 105, 180, 1);
|
||||
pub const indianred: RGBA = .init(205, 92, 92, 1);
|
||||
pub const indigo: RGBA = .init(75, 0, 130, 1);
|
||||
pub const ivory: RGBA = .init(255, 255, 240, 1);
|
||||
pub const khaki: RGBA = .init(240, 230, 140, 1);
|
||||
pub const lavender: RGBA = .init(230, 230, 250, 1);
|
||||
pub const lavenderblush: RGBA = .init(255, 240, 245, 1);
|
||||
pub const lawngreen: RGBA = .init(124, 252, 0, 1);
|
||||
pub const lemonchiffon: RGBA = .init(255, 250, 205, 1);
|
||||
pub const lightblue: RGBA = .init(173, 216, 230, 1);
|
||||
pub const lightcoral: RGBA = .init(240, 128, 128, 1);
|
||||
pub const lightcyan: RGBA = .init(224, 255, 255, 1);
|
||||
pub const lightgoldenrodyellow: RGBA = .init(250, 250, 210, 1);
|
||||
pub const lightgray: RGBA = .init(211, 211, 211, 1);
|
||||
pub const lightgreen: RGBA = .init(144, 238, 144, 1);
|
||||
pub const lightgrey: RGBA = .init(211, 211, 211, 1); // Synonym of lightgray
|
||||
pub const lightpink: RGBA = .init(255, 182, 193, 1);
|
||||
pub const lightsalmon: RGBA = .init(255, 160, 122, 1);
|
||||
pub const lightseagreen: RGBA = .init(32, 178, 170, 1);
|
||||
pub const lightskyblue: RGBA = .init(135, 206, 250, 1);
|
||||
pub const lightslategray: RGBA = .init(119, 136, 153, 1);
|
||||
pub const lightslategrey: RGBA = .init(119, 136, 153, 1); // Synonym of lightslategray
|
||||
pub const lightsteelblue: RGBA = .init(176, 196, 222, 1);
|
||||
pub const lightyellow: RGBA = .init(255, 255, 224, 1);
|
||||
pub const limegreen: RGBA = .init(50, 205, 50, 1);
|
||||
pub const linen: RGBA = .init(250, 240, 230, 1);
|
||||
pub const magenta: RGBA = .init(255, 0, 255, 1); // Synonym of fuchsia
|
||||
pub const mediumaquamarine: RGBA = .init(102, 205, 170, 1);
|
||||
pub const mediumblue: RGBA = .init(0, 0, 205, 1);
|
||||
pub const mediumorchid: RGBA = .init(186, 85, 211, 1);
|
||||
pub const mediumpurple: RGBA = .init(147, 112, 219, 1);
|
||||
pub const mediumseagreen: RGBA = .init(60, 179, 113, 1);
|
||||
pub const mediumslateblue: RGBA = .init(123, 104, 238, 1);
|
||||
pub const mediumspringgreen: RGBA = .init(0, 250, 154, 1);
|
||||
pub const mediumturquoise: RGBA = .init(72, 209, 204, 1);
|
||||
pub const mediumvioletred: RGBA = .init(199, 21, 133, 1);
|
||||
pub const midnightblue: RGBA = .init(25, 25, 112, 1);
|
||||
pub const mintcream: RGBA = .init(245, 255, 250, 1);
|
||||
pub const mistyrose: RGBA = .init(255, 228, 225, 1);
|
||||
pub const moccasin: RGBA = .init(255, 228, 181, 1);
|
||||
pub const navajowhite: RGBA = .init(255, 222, 173, 1);
|
||||
pub const oldlace: RGBA = .init(253, 245, 230, 1);
|
||||
pub const olivedrab: RGBA = .init(107, 142, 35, 1);
|
||||
pub const orange: RGBA = .init(255, 165, 0, 1);
|
||||
pub const orangered: RGBA = .init(255, 69, 0, 1);
|
||||
pub const orchid: RGBA = .init(218, 112, 214, 1);
|
||||
pub const palegoldenrod: RGBA = .init(238, 232, 170, 1);
|
||||
pub const palegreen: RGBA = .init(152, 251, 152, 1);
|
||||
pub const paleturquoise: RGBA = .init(175, 238, 238, 1);
|
||||
pub const palevioletred: RGBA = .init(219, 112, 147, 1);
|
||||
pub const papayawhip: RGBA = .init(255, 239, 213, 1);
|
||||
pub const peachpuff: RGBA = .init(255, 218, 185, 1);
|
||||
pub const peru: RGBA = .init(205, 133, 63, 1);
|
||||
pub const pink: RGBA = .init(255, 192, 203, 1);
|
||||
pub const plum: RGBA = .init(221, 160, 221, 1);
|
||||
pub const powderblue: RGBA = .init(176, 224, 230, 1);
|
||||
pub const rebeccapurple: RGBA = .init(102, 51, 153, 1);
|
||||
pub const rosybrown: RGBA = .init(188, 143, 143, 1);
|
||||
pub const royalblue: RGBA = .init(65, 105, 225, 1);
|
||||
pub const saddlebrown: RGBA = .init(139, 69, 19, 1);
|
||||
pub const salmon: RGBA = .init(250, 128, 114, 1);
|
||||
pub const sandybrown: RGBA = .init(244, 164, 96, 1);
|
||||
pub const seagreen: RGBA = .init(46, 139, 87, 1);
|
||||
pub const seashell: RGBA = .init(255, 245, 238, 1);
|
||||
pub const sienna: RGBA = .init(160, 82, 45, 1);
|
||||
pub const skyblue: RGBA = .init(135, 206, 235, 1);
|
||||
pub const slateblue: RGBA = .init(106, 90, 205, 1);
|
||||
pub const slategray: RGBA = .init(112, 128, 144, 1);
|
||||
pub const slategrey: RGBA = .init(112, 128, 144, 1); // Synonym of slategray
|
||||
pub const snow: RGBA = .init(255, 250, 250, 1);
|
||||
pub const springgreen: RGBA = .init(0, 255, 127, 1);
|
||||
pub const steelblue: RGBA = .init(70, 130, 180, 1);
|
||||
pub const tan: RGBA = .init(210, 180, 140, 1);
|
||||
pub const thistle: RGBA = .init(216, 191, 216, 1);
|
||||
pub const tomato: RGBA = .init(255, 99, 71, 1);
|
||||
pub const transparent: RGBA = .init(0, 0, 0, 0);
|
||||
pub const turquoise: RGBA = .init(64, 224, 208, 1);
|
||||
pub const violet: RGBA = .init(238, 130, 238, 1);
|
||||
pub const wheat: RGBA = .init(245, 222, 179, 1);
|
||||
pub const whitesmoke: RGBA = .init(245, 245, 245, 1);
|
||||
pub const yellowgreen: RGBA = .init(154, 205, 50, 1);
|
||||
};
|
||||
|
||||
pub fn init(r: u8, g: u8, b: u8, a: f32) RGBA {
|
||||
const clamped = std.math.clamp(a, 0, 1);
|
||||
return .{ .r = r, .g = g, .b = b, .a = @intFromFloat(clamped * 255) };
|
||||
}
|
||||
|
||||
/// Finds a color by its name.
|
||||
pub fn find(name: []const u8) ?RGBA {
|
||||
const match = std.meta.stringToEnum(std.meta.DeclEnum(Named), name) orelse return null;
|
||||
|
||||
return switch (match) {
|
||||
inline else => |comptime_enum| @field(Named, @tagName(comptime_enum)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses the given color.
|
||||
/// Currently we only parse hex colors and named colors; other variants
|
||||
/// require CSS evaluation.
|
||||
pub fn parse(input: []const u8) !RGBA {
|
||||
if (!isHexColor(input)) {
|
||||
// Try named colors.
|
||||
return find(input) orelse return error.Invalid;
|
||||
}
|
||||
|
||||
const slice = input[1..];
|
||||
switch (slice.len) {
|
||||
// This means the digit for a color is repeated.
|
||||
// Given HEX is #f0c, its interpreted the same as #FF00CC.
|
||||
3 => {
|
||||
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
|
||||
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
|
||||
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
|
||||
return .{ .r = r, .g = g, .b = b, .a = 255 };
|
||||
},
|
||||
4 => {
|
||||
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
|
||||
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
|
||||
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
|
||||
const a = try std.fmt.parseInt(u8, &.{ slice[3], slice[3] }, 16);
|
||||
return .{ .r = r, .g = g, .b = b, .a = a };
|
||||
},
|
||||
// Regular HEX format.
|
||||
6 => {
|
||||
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
|
||||
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
|
||||
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
|
||||
return .{ .r = r, .g = g, .b = b, .a = 255 };
|
||||
},
|
||||
8 => {
|
||||
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
|
||||
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
|
||||
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
|
||||
const a = try std.fmt.parseInt(u8, slice[6..8], 16);
|
||||
return .{ .r = r, .g = g, .b = b, .a = a };
|
||||
},
|
||||
else => return error.Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// By default, browsers prefer lowercase formatting.
|
||||
const format_upper = false;
|
||||
|
||||
/// Formats the `Color` according to web expectations.
|
||||
/// If color is opaque, HEX is preferred; RGBA otherwise.
|
||||
pub fn format(self: *const RGBA, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
if (self.isOpaque()) {
|
||||
// Convert RGB to HEX.
|
||||
// https://gristle.tripod.com/hexconv.html
|
||||
// Hexadecimal characters up to 15.
|
||||
const char: []const u8 = "0123456789" ++ if (format_upper) "ABCDEF" else "abcdef";
|
||||
// This variant always prefers 6 digit format, +1 is for hash char.
|
||||
const buffer = [7]u8{
|
||||
'#',
|
||||
char[self.r >> 4],
|
||||
char[self.r & 15],
|
||||
char[self.g >> 4],
|
||||
char[self.g & 15],
|
||||
char[self.b >> 4],
|
||||
char[self.b & 15],
|
||||
};
|
||||
|
||||
return writer.writeAll(&buffer);
|
||||
}
|
||||
|
||||
// Prefer RGBA format for everything else.
|
||||
return writer.print("rgba({d}, {d}, {d}, {d:.2})", .{ self.r, self.g, self.b, self.normalizedAlpha() });
|
||||
}
|
||||
|
||||
/// Returns true if `Color` is opaque.
|
||||
pub inline fn isOpaque(self: *const RGBA) bool {
|
||||
return self.a == std.math.maxInt(u8);
|
||||
}
|
||||
|
||||
/// Returns the normalized alpha value.
|
||||
pub inline fn normalizedAlpha(self: *const RGBA) f32 {
|
||||
return @as(f32, @floatFromInt(self.a)) / 255;
|
||||
}
|
||||
};
|
||||
@@ -1,295 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Tokenizer = @import("Tokenizer.zig");
|
||||
|
||||
pub const Declaration = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
important: bool,
|
||||
};
|
||||
|
||||
const TokenSpan = struct {
|
||||
token: Tokenizer.Token,
|
||||
start: usize,
|
||||
end: usize,
|
||||
};
|
||||
|
||||
const TokenStream = struct {
|
||||
tokenizer: Tokenizer,
|
||||
peeked: ?TokenSpan = null,
|
||||
|
||||
fn init(input: []const u8) TokenStream {
|
||||
return .{ .tokenizer = .{ .input = input } };
|
||||
}
|
||||
|
||||
fn nextRaw(self: *TokenStream) ?TokenSpan {
|
||||
const start = self.tokenizer.position;
|
||||
const token = self.tokenizer.next() orelse return null;
|
||||
const end = self.tokenizer.position;
|
||||
return .{ .token = token, .start = start, .end = end };
|
||||
}
|
||||
|
||||
fn next(self: *TokenStream) ?TokenSpan {
|
||||
if (self.peeked) |token| {
|
||||
self.peeked = null;
|
||||
return token;
|
||||
}
|
||||
return self.nextRaw();
|
||||
}
|
||||
|
||||
fn peek(self: *TokenStream) ?TokenSpan {
|
||||
if (self.peeked == null) {
|
||||
self.peeked = self.nextRaw();
|
||||
}
|
||||
return self.peeked;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn parseDeclarationsList(input: []const u8) DeclarationsIterator {
|
||||
return DeclarationsIterator.init(input);
|
||||
}
|
||||
|
||||
pub const DeclarationsIterator = struct {
|
||||
input: []const u8,
|
||||
stream: TokenStream,
|
||||
|
||||
pub fn init(input: []const u8) DeclarationsIterator {
|
||||
return .{
|
||||
.input = input,
|
||||
.stream = TokenStream.init(input),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn next(self: *DeclarationsIterator) ?Declaration {
|
||||
while (true) {
|
||||
self.skipTriviaAndSemicolons();
|
||||
const peeked = self.stream.peek() orelse return null;
|
||||
|
||||
switch (peeked.token) {
|
||||
.at_keyword => {
|
||||
_ = self.stream.next();
|
||||
self.skipAtRule();
|
||||
},
|
||||
.ident => |name| {
|
||||
_ = self.stream.next();
|
||||
if (self.consumeDeclaration(name)) |declaration| {
|
||||
return declaration;
|
||||
}
|
||||
},
|
||||
else => {
|
||||
_ = self.stream.next();
|
||||
self.skipInvalidDeclaration();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn consumeDeclaration(self: *DeclarationsIterator, name: []const u8) ?Declaration {
|
||||
self.skipTrivia();
|
||||
|
||||
const colon = self.stream.next() orelse return null;
|
||||
if (!isColon(colon.token)) {
|
||||
self.skipInvalidDeclaration();
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = self.consumeValue() orelse return null;
|
||||
return .{
|
||||
.name = name,
|
||||
.value = value.value,
|
||||
.important = value.important,
|
||||
};
|
||||
}
|
||||
|
||||
const ValueResult = struct {
|
||||
value: []const u8,
|
||||
important: bool,
|
||||
};
|
||||
|
||||
fn consumeValue(self: *DeclarationsIterator) ?ValueResult {
|
||||
self.skipTrivia();
|
||||
|
||||
var depth: usize = 0;
|
||||
var start: ?usize = null;
|
||||
var last_sig: ?TokenSpan = null;
|
||||
var prev_sig: ?TokenSpan = null;
|
||||
|
||||
while (true) {
|
||||
const peeked = self.stream.peek() orelse break;
|
||||
if (isSemicolon(peeked.token) and depth == 0) {
|
||||
_ = self.stream.next();
|
||||
break;
|
||||
}
|
||||
|
||||
const span = self.stream.next() orelse break;
|
||||
if (isWhitespaceOrComment(span.token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (start == null) start = span.start;
|
||||
prev_sig = last_sig;
|
||||
last_sig = span;
|
||||
updateDepth(span.token, &depth);
|
||||
}
|
||||
|
||||
const value_start = start orelse return null;
|
||||
const last = last_sig orelse return null;
|
||||
|
||||
var important = false;
|
||||
var end_pos = last.end;
|
||||
|
||||
if (isImportantPair(prev_sig, last)) {
|
||||
important = true;
|
||||
const bang = prev_sig orelse return null;
|
||||
if (value_start >= bang.start) return null;
|
||||
end_pos = bang.start;
|
||||
}
|
||||
|
||||
var value_slice = self.input[value_start..end_pos];
|
||||
value_slice = std.mem.trim(u8, value_slice, &std.ascii.whitespace);
|
||||
if (value_slice.len == 0) return null;
|
||||
|
||||
return .{ .value = value_slice, .important = important };
|
||||
}
|
||||
|
||||
fn skipTrivia(self: *DeclarationsIterator) void {
|
||||
while (self.stream.peek()) |peeked| {
|
||||
if (!isWhitespaceOrComment(peeked.token)) break;
|
||||
_ = self.stream.next();
|
||||
}
|
||||
}
|
||||
|
||||
fn skipTriviaAndSemicolons(self: *DeclarationsIterator) void {
|
||||
while (self.stream.peek()) |peeked| {
|
||||
if (isWhitespaceOrComment(peeked.token) or isSemicolon(peeked.token)) {
|
||||
_ = self.stream.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn skipAtRule(self: *DeclarationsIterator) void {
|
||||
var depth: usize = 0;
|
||||
var saw_block = false;
|
||||
|
||||
while (true) {
|
||||
const peeked = self.stream.peek() orelse return;
|
||||
if (!saw_block and isSemicolon(peeked.token) and depth == 0) {
|
||||
_ = self.stream.next();
|
||||
return;
|
||||
}
|
||||
|
||||
const span = self.stream.next() orelse return;
|
||||
if (isWhitespaceOrComment(span.token)) continue;
|
||||
|
||||
if (isBlockStart(span.token)) {
|
||||
depth += 1;
|
||||
saw_block = true;
|
||||
} else if (isBlockEnd(span.token)) {
|
||||
if (depth > 0) depth -= 1;
|
||||
if (saw_block and depth == 0) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn skipInvalidDeclaration(self: *DeclarationsIterator) void {
|
||||
var depth: usize = 0;
|
||||
|
||||
while (self.stream.peek()) |peeked| {
|
||||
if (isSemicolon(peeked.token) and depth == 0) {
|
||||
_ = self.stream.next();
|
||||
return;
|
||||
}
|
||||
|
||||
const span = self.stream.next() orelse return;
|
||||
if (isWhitespaceOrComment(span.token)) continue;
|
||||
updateDepth(span.token, &depth);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn isWhitespaceOrComment(token: Tokenizer.Token) bool {
|
||||
return switch (token) {
|
||||
.white_space, .comment => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn isSemicolon(token: Tokenizer.Token) bool {
|
||||
return switch (token) {
|
||||
.semicolon => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn isColon(token: Tokenizer.Token) bool {
|
||||
return switch (token) {
|
||||
.colon => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn isBlockStart(token: Tokenizer.Token) bool {
|
||||
return switch (token) {
|
||||
.curly_bracket_block, .square_bracket_block, .parenthesis_block, .function => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn isBlockEnd(token: Tokenizer.Token) bool {
|
||||
return switch (token) {
|
||||
.close_curly_bracket, .close_parenthesis, .close_square_bracket => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn updateDepth(token: Tokenizer.Token, depth: *usize) void {
|
||||
if (isBlockStart(token)) {
|
||||
depth.* += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBlockEnd(token)) {
|
||||
if (depth.* > 0) depth.* -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn isImportantPair(prev_sig: ?TokenSpan, last_sig: TokenSpan) bool {
|
||||
if (!isIdentImportant(last_sig.token)) return false;
|
||||
const prev = prev_sig orelse return false;
|
||||
return isBang(prev.token);
|
||||
}
|
||||
|
||||
fn isIdentImportant(token: Tokenizer.Token) bool {
|
||||
return switch (token) {
|
||||
.ident => |name| std.ascii.eqlIgnoreCase(name, "important"),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn isBang(token: Tokenizer.Token) bool {
|
||||
return switch (token) {
|
||||
.delim => |c| c == '!',
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
@@ -644,10 +644,8 @@ fn consumeNumeric(self: *Tokenizer) Token {
|
||||
fn consumeUnquotedUrl(self: *Tokenizer) ?Token {
|
||||
// TODO: true url parser
|
||||
if (self.nextByte()) |it| {
|
||||
return self.consumeString(it == '\'');
|
||||
self.consumeString(it == '\'');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn consumeIdentLike(self: *Tokenizer) Token {
|
||||
|
||||
@@ -51,22 +51,11 @@ pub const Opts = struct {
|
||||
|
||||
pub fn root(doc: *Node.Document, opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void {
|
||||
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
|
||||
blk: {
|
||||
// Ideally we just render the doctype which is part of the document
|
||||
if (doc.asNode().firstChild()) |first| {
|
||||
if (first._type == .document_type) {
|
||||
break :blk;
|
||||
}
|
||||
}
|
||||
// But if the doc has no child, or the first child isn't a doctype
|
||||
// well force it.
|
||||
try writer.writeAll("<!DOCTYPE html>");
|
||||
}
|
||||
|
||||
try writer.writeAll("<!DOCTYPE html>");
|
||||
if (opts.with_base) {
|
||||
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
|
||||
const base = try doc.createElement("base", null, page);
|
||||
try base.setAttributeSafe(comptime .wrap("base"), .wrap(page.base()), page);
|
||||
try base.setAttributeSafe("base", page.base(), page);
|
||||
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);
|
||||
}
|
||||
}
|
||||
@@ -110,7 +99,7 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
|
||||
// to render that "active" content, so when we're trying to render
|
||||
// it, we don't want to skip it.
|
||||
if ((comptime force_slot == false) and opts.shadow == .rendered) {
|
||||
if (el.getAttributeSafe(comptime .wrap("slot"))) |_| {
|
||||
if (el.getAttributeSafe("slot")) |_| {
|
||||
// Skip - will be rendered by the Slot if it's the active container
|
||||
return;
|
||||
}
|
||||
@@ -253,12 +242,12 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
|
||||
if (std.mem.eql(u8, tag_name, "noscript")) return true;
|
||||
|
||||
if (std.mem.eql(u8, tag_name, "link")) {
|
||||
if (el.getAttributeSafe(comptime .wrap("as"))) |as| {
|
||||
if (el.getAttributeSafe("as")) |as| {
|
||||
if (std.mem.eql(u8, as, "script")) return true;
|
||||
}
|
||||
if (el.getAttributeSafe(comptime .wrap("rel"))) |rel| {
|
||||
if (el.getAttributeSafe("rel")) |rel| {
|
||||
if (std.mem.eql(u8, rel, "modulepreload") or std.mem.eql(u8, rel, "preload")) {
|
||||
if (el.getAttributeSafe(comptime .wrap("as"))) |as| {
|
||||
if (el.getAttributeSafe("as")) |as| {
|
||||
if (std.mem.eql(u8, as, "script")) return true;
|
||||
}
|
||||
}
|
||||
@@ -270,7 +259,7 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
|
||||
if (std.mem.eql(u8, tag_name, "style")) return true;
|
||||
|
||||
if (std.mem.eql(u8, tag_name, "link")) {
|
||||
if (el.getAttributeSafe(comptime .wrap("rel"))) |rel| {
|
||||
if (el.getAttributeSafe("rel")) |rel| {
|
||||
if (std.mem.eql(u8, rel, "stylesheet")) return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,46 +21,18 @@ const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Array = @This();
|
||||
|
||||
local: *const js.Local,
|
||||
handle: *const v8.Array,
|
||||
js_arr: v8.Array,
|
||||
context: *js.Context,
|
||||
|
||||
pub fn len(self: Array) usize {
|
||||
return v8.v8__Array__Length(self.handle);
|
||||
return @intCast(self.js_arr.length());
|
||||
}
|
||||
|
||||
pub fn get(self: Array, index: u32) !js.Value {
|
||||
const ctx = self.local.ctx;
|
||||
|
||||
const idx = js.Integer.init(ctx.isolate.handle, index);
|
||||
const handle = v8.v8__Object__Get(@ptrCast(self.handle), self.local.handle, idx.handle) orelse {
|
||||
return error.JsException;
|
||||
};
|
||||
|
||||
pub fn get(self: Array, index: usize) !js.Value {
|
||||
const idx_key = v8.Integer.initU32(self.context.isolate, @intCast(index));
|
||||
const js_obj = self.js_arr.castTo(v8.Object);
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = handle,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
|
||||
const js_value = try self.local.zigValueToJs(value, opts);
|
||||
|
||||
var out: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__SetAtIndex(@ptrCast(self.handle), self.local.handle, index, js_value.handle, &out);
|
||||
return out.has_value;
|
||||
}
|
||||
|
||||
pub fn toObject(self: Array) js.Object {
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = @ptrCast(self.handle),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toValue(self: Array) js.Value {
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = @ptrCast(self.handle),
|
||||
.context = self.context,
|
||||
.js_val = try js_obj.getValue(self.context.v8_context, idx_key.toValue()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const BigInt = @This();
|
||||
|
||||
handle: *const v8.Integer,
|
||||
|
||||
pub fn init(isolate: *v8.Isolate, val: anytype) BigInt {
|
||||
const handle = switch (@TypeOf(val)) {
|
||||
i8, i16, i32, i64, isize => v8.v8__BigInt__New(isolate, val).?,
|
||||
u8, u16, u32, u64, usize => v8.v8__BigInt__NewFromUnsigned(isolate, val).?,
|
||||
else => |T| @compileError("cannot create v8::BigInt from: " ++ @typeName(T)),
|
||||
};
|
||||
return .{ .handle = handle };
|
||||
}
|
||||
|
||||
pub fn getInt64(self: BigInt) i64 {
|
||||
return v8.v8__BigInt__Int64Value(self.handle, null);
|
||||
}
|
||||
|
||||
pub fn getUint64(self: BigInt) u64 {
|
||||
return v8.v8__BigInt__Uint64Value(self.handle, null);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -17,52 +17,51 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const string = @import("../../string.zig");
|
||||
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Context = @import("Context.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const js = @import("js.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const TaggedOpaque = @import("TaggedOpaque.zig");
|
||||
|
||||
const v8 = js.v8;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const CALL_ARENA_RETAIN = 1024 * 16;
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
// Responsible for calling Zig functions from JS invocations. This could
|
||||
// probably just contained in ExecutionWorld, but having this specific logic, which
|
||||
// is somewhat repetitive between constructors, functions, getters, etc contained
|
||||
// here does feel like it makes it cleaner.
|
||||
const Caller = @This();
|
||||
local: js.Local,
|
||||
prev_local: ?*const js.Local,
|
||||
prev_context: *Context,
|
||||
context: *Context,
|
||||
v8_context: v8.Context,
|
||||
isolate: v8.Isolate,
|
||||
call_arena: Allocator,
|
||||
|
||||
// Takes the raw v8 isolate and extracts the context from it.
|
||||
pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
|
||||
const v8_context_handle = v8.v8__Isolate__GetCurrentContext(v8_isolate);
|
||||
// info is a v8.PropertyCallbackInfo or a v8.FunctionCallback
|
||||
// All we really want from it is the isolate.
|
||||
// executor = Isolate -> getCurrentContext -> getEmbedderData()
|
||||
pub fn init(info: anytype) Caller {
|
||||
const isolate = info.getIsolate();
|
||||
const v8_context = isolate.getCurrentContext();
|
||||
const context: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
|
||||
|
||||
const embedder_data = v8.v8__Context__GetEmbedderData(v8_context_handle, 1);
|
||||
var lossless: bool = undefined;
|
||||
const ctx: *Context = @ptrFromInt(v8.v8__BigInt__Uint64Value(embedder_data, &lossless));
|
||||
|
||||
ctx.call_depth += 1;
|
||||
self.* = Caller{
|
||||
.local = .{
|
||||
.ctx = ctx,
|
||||
.handle = v8_context_handle.?,
|
||||
.call_arena = ctx.call_arena,
|
||||
.isolate = .{ .handle = v8_isolate },
|
||||
},
|
||||
.prev_local = ctx.local,
|
||||
.prev_context = ctx.page.js,
|
||||
context.call_depth += 1;
|
||||
return .{
|
||||
.context = context,
|
||||
.isolate = isolate,
|
||||
.v8_context = v8_context,
|
||||
.call_arena = context.call_arena,
|
||||
};
|
||||
ctx.page.js = ctx;
|
||||
ctx.local = &self.local;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Caller) void {
|
||||
const ctx = self.local.ctx;
|
||||
const call_depth = ctx.call_depth - 1;
|
||||
const context = self.context;
|
||||
const call_depth = context.call_depth - 1;
|
||||
|
||||
// Because of callbacks, calls can be nested. Because of this, we
|
||||
// can't clear the call_arena after _every_ call. Imagine we have
|
||||
@@ -76,13 +75,11 @@ pub fn deinit(self: *Caller) void {
|
||||
// Therefore, we keep a call_depth, and only reset the call_arena
|
||||
// when a top-level (call_depth == 0) function ends.
|
||||
if (call_depth == 0) {
|
||||
const arena: *ArenaAllocator = @ptrCast(@alignCast(ctx.call_arena.ptr));
|
||||
const arena: *ArenaAllocator = @ptrCast(@alignCast(context.call_arena.ptr));
|
||||
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
|
||||
}
|
||||
|
||||
ctx.call_depth = call_depth;
|
||||
ctx.local = self.prev_local;
|
||||
ctx.page.js = self.prev_context;
|
||||
context.call_depth = call_depth;
|
||||
}
|
||||
|
||||
pub const CallOpts = struct {
|
||||
@@ -91,24 +88,12 @@ pub const CallOpts = struct {
|
||||
as_typed_array: bool = false,
|
||||
};
|
||||
|
||||
pub fn constructor(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = FunctionCallbackInfo{ .handle = handle };
|
||||
|
||||
if (!info.isConstructCall()) {
|
||||
self.handleError(T, @TypeOf(func), error.InvalidArgument, info, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn constructor(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||
self._constructor(func, info) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
};
|
||||
}
|
||||
|
||||
fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void {
|
||||
pub fn _constructor(self: *Caller, func: anytype, info: v8.FunctionCallbackInfo) !void {
|
||||
const F = @TypeOf(func);
|
||||
const args = try self.getArgs(F, 0, info);
|
||||
const res = @call(.auto, func, args);
|
||||
@@ -117,167 +102,129 @@ fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void
|
||||
@compileError(@typeName(F) ++ " has a constructor without a return type");
|
||||
};
|
||||
|
||||
const new_this_handle = info.getThis();
|
||||
var this = js.Object{ .local = &self.local, .handle = new_this_handle };
|
||||
const new_this = info.getThis();
|
||||
var this = new_this;
|
||||
if (@typeInfo(ReturnType) == .error_union) {
|
||||
const non_error_res = res catch |err| return err;
|
||||
this = try self.local.mapZigInstanceToJs(new_this_handle, non_error_res);
|
||||
this = (try self.context.mapZigInstanceToJs(this, non_error_res)).castToObject();
|
||||
} else {
|
||||
this = try self.local.mapZigInstanceToJs(new_this_handle, res);
|
||||
this = (try self.context.mapZigInstanceToJs(this, res)).castToObject();
|
||||
}
|
||||
|
||||
// If we got back a different object (existing wrapper), copy the prototype
|
||||
// from new object. (this happens when we're upgrading an CustomElement)
|
||||
if (this.handle != new_this_handle) {
|
||||
const prototype_handle = v8.v8__Object__GetPrototype(new_this_handle).?;
|
||||
var out: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__SetPrototype(this.handle, self.local.handle, prototype_handle, &out);
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(out.has_value and out.value);
|
||||
}
|
||||
if (this.handle != new_this.handle) {
|
||||
const new_prototype = new_this.getPrototype();
|
||||
_ = this.setPrototype(self.context.v8_context, new_prototype.castTo(v8.Object));
|
||||
}
|
||||
|
||||
info.getReturnValue().set(this.handle);
|
||||
info.getReturnValue().set(this);
|
||||
}
|
||||
|
||||
pub fn method(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = FunctionCallbackInfo{ .handle = handle };
|
||||
pub fn method(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||
self._method(T, func, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
};
|
||||
}
|
||||
|
||||
fn _method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
|
||||
pub fn _method(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) !void {
|
||||
const F = @TypeOf(func);
|
||||
var handle_scope: v8.HandleScope = undefined;
|
||||
handle_scope.init(self.isolate);
|
||||
defer handle_scope.deinit();
|
||||
|
||||
var args = try self.getArgs(F, 1, info);
|
||||
|
||||
const js_this = info.getThis();
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, js_this);
|
||||
|
||||
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
|
||||
const res = @call(.auto, func, args);
|
||||
|
||||
const mapped = try self.local.zigValueToJs(res, opts);
|
||||
const return_value = info.getReturnValue();
|
||||
return_value.set(mapped);
|
||||
info.getReturnValue().set(try self.context.zigValueToJs(res, opts));
|
||||
}
|
||||
|
||||
pub fn function(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = FunctionCallbackInfo{ .handle = handle };
|
||||
pub fn function(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||
self._function(func, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
};
|
||||
}
|
||||
|
||||
fn _function(self: *Caller, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
|
||||
pub fn _function(self: *Caller, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) !void {
|
||||
const F = @TypeOf(func);
|
||||
const context = self.context;
|
||||
const args = try self.getArgs(F, 0, info);
|
||||
const res = @call(.auto, func, args);
|
||||
info.getReturnValue().set(try self.local.zigValueToJs(res, opts));
|
||||
info.getReturnValue().set(try context.zigValueToJs(res, opts));
|
||||
}
|
||||
|
||||
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
return self._getIndex(T, func, idx, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
return v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
|
||||
fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
pub fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
const F = @TypeOf(func);
|
||||
var args = try self.getArgs(F, 2, info);
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
|
||||
@field(args, "1") = idx;
|
||||
const ret = @call(.auto, func, args);
|
||||
return self.handleIndexedReturn(T, F, true, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
return self._getNamedIndex(T, func, name, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
return v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
|
||||
fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
pub fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
const F = @TypeOf(func);
|
||||
var args = try self.getArgs(F, 2, info);
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
|
||||
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
const ret = @call(.auto, func, args);
|
||||
return self.handleIndexedReturn(T, F, true, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return self._setNamedIndex(T, func, name, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| {
|
||||
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
return self._setNamedIndex(T, func, name, js_value, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
return v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
|
||||
fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
pub fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
|
||||
@field(args, "2") = try self.local.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
|
||||
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = try self.context.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
|
||||
if (@typeInfo(F).@"fn".params.len == 4) {
|
||||
@field(args, "3") = self.local.ctx.page;
|
||||
@field(args, "3") = self.context.page;
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return self.handleIndexedReturn(T, F, false, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
return self._deleteNamedIndex(T, func, name, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
return 0;
|
||||
return v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
|
||||
fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
pub fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
|
||||
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
if (@typeInfo(F).@"fn".params.len == 3) {
|
||||
@field(args, "2") = self.local.ctx.page;
|
||||
@field(args, "2") = self.context.page;
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return self.handleIndexedReturn(T, F, false, ret, info, opts);
|
||||
}
|
||||
|
||||
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
// need to unwrap this error immediately for when opts.null_as_undefined == true
|
||||
// and we need to compare it to null;
|
||||
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
|
||||
@@ -288,23 +235,20 @@ fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, compti
|
||||
// if error.NotHandled is part of the error set.
|
||||
if (isInErrorSet(error.NotHandled, eu.error_set)) {
|
||||
if (err == error.NotHandled) {
|
||||
// not intercepted
|
||||
return 0;
|
||||
return v8.Intercepted.No;
|
||||
}
|
||||
}
|
||||
self.handleError(T, F, err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
return v8.Intercepted.No;
|
||||
};
|
||||
},
|
||||
else => ret,
|
||||
};
|
||||
|
||||
if (comptime getter) {
|
||||
info.getReturnValue().set(try self.local.zigValueToJs(non_error_ret, opts));
|
||||
info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts));
|
||||
}
|
||||
// intercepted
|
||||
return 1;
|
||||
return v8.Intercepted.Yes;
|
||||
}
|
||||
|
||||
fn isInErrorSet(err: anyerror, comptime T: type) bool {
|
||||
@@ -314,45 +258,86 @@ fn isInErrorSet(err: anyerror, comptime T: type) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn nameToString(self: *const Caller, comptime T: type, name: *const v8.Name) !T {
|
||||
const handle = @as(*const v8.String, @ptrCast(name));
|
||||
if (T == string.String) {
|
||||
return js.String.toSSO(.{ .local = &self.local, .handle = handle }, false);
|
||||
fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 {
|
||||
if (@typeInfo(@TypeOf(res)) == .error_union) {
|
||||
_ = try res;
|
||||
}
|
||||
if (T == string.Global) {
|
||||
return js.String.toSSO(.{ .local = &self.local, .handle = handle }, true);
|
||||
if (has_value == false) {
|
||||
return v8.Intercepted.No;
|
||||
}
|
||||
return try js.String.toSlice(.{ .local = &self.local, .handle = handle });
|
||||
return v8.Intercepted.Yes;
|
||||
}
|
||||
|
||||
fn nameToString(self: *Caller, name: v8.Name) ![]const u8 {
|
||||
return self.context.valueToString(.{ .handle = name.handle }, .{});
|
||||
}
|
||||
|
||||
fn isSelfReceiver(comptime T: type, comptime F: type) bool {
|
||||
return checkSelfReceiver(T, F, false);
|
||||
}
|
||||
fn assertSelfReceiver(comptime T: type, comptime F: type) void {
|
||||
_ = checkSelfReceiver(T, F, true);
|
||||
}
|
||||
fn checkSelfReceiver(comptime T: type, comptime F: type, comptime fail: bool) bool {
|
||||
const params = @typeInfo(F).@"fn".params;
|
||||
if (params.len == 0) {
|
||||
if (fail) {
|
||||
@compileError(@typeName(F) ++ " must have a self parameter");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const first_param = params[0].type.?;
|
||||
if (first_param != *T and first_param != *const T) {
|
||||
if (fail) {
|
||||
@compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{
|
||||
@typeName(F),
|
||||
@typeName(T),
|
||||
@typeName(T),
|
||||
@typeName(first_param),
|
||||
}));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn assertIsPageArg(comptime T: type, comptime F: type, index: comptime_int) void {
|
||||
const param = @typeInfo(F).@"fn".params[index].type.?;
|
||||
if (isPage(param)) {
|
||||
return;
|
||||
}
|
||||
@compileError(std.fmt.comptimePrint("The {d} parameter of {s}.{s} must be a *Page or *const Page. Got: {s}", .{ index, @typeName(T), @typeName(F), @typeName(param) }));
|
||||
}
|
||||
|
||||
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {
|
||||
const isolate = self.local.isolate;
|
||||
const isolate = self.isolate;
|
||||
|
||||
if (comptime @import("builtin").mode == .Debug and @TypeOf(info) == FunctionCallbackInfo) {
|
||||
if (comptime @import("builtin").mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
|
||||
if (log.enabled(.js, .warn)) {
|
||||
self.logFunctionCallError(@typeName(T), @typeName(F), err, info);
|
||||
}
|
||||
}
|
||||
|
||||
const js_err: *const v8.Value = switch (err) {
|
||||
error.TryCatchRethrow => return,
|
||||
error.InvalidArgument => isolate.createTypeError("invalid argument"),
|
||||
error.OutOfMemory => isolate.createError("out of memory"),
|
||||
error.IllegalConstructor => isolate.createError("Illegal Contructor"),
|
||||
var js_err: ?v8.Value = switch (err) {
|
||||
error.InvalidArgument => createTypeException(isolate, "invalid argument"),
|
||||
error.OutOfMemory => js._createException(isolate, "out of memory"),
|
||||
error.IllegalConstructor => js._createException(isolate, "Illegal Contructor"),
|
||||
else => blk: {
|
||||
if (comptime opts.dom_exception) {
|
||||
const DOMException = @import("../webapi/DOMException.zig");
|
||||
if (DOMException.fromError(err)) |ex| {
|
||||
const value = self.local.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error");
|
||||
break :blk value.handle;
|
||||
}
|
||||
if (!comptime opts.dom_exception) {
|
||||
break :blk null;
|
||||
}
|
||||
break :blk isolate.createError(@errorName(err));
|
||||
const DOMException = @import("../webapi/DOMException.zig");
|
||||
const ex = DOMException.fromError(err) orelse break :blk null;
|
||||
break :blk self.context.zigValueToJs(ex, .{}) catch js._createException(isolate, "internal error");
|
||||
},
|
||||
};
|
||||
|
||||
const js_exception = isolate.throwException(js_err);
|
||||
info.getReturnValue().setValueHandle(js_exception);
|
||||
if (js_err == null) {
|
||||
js_err = js._createException(isolate, @errorName(err));
|
||||
}
|
||||
const js_exception = isolate.throwException(js_err.?);
|
||||
info.getReturnValue().setValueHandle(js_exception.handle);
|
||||
}
|
||||
|
||||
// If we call a method in javascript: cat.lives('nine');
|
||||
@@ -369,7 +354,7 @@ fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror,
|
||||
// the last parameter in Zig is an array, we'll try to slurp the additional
|
||||
// parameters into the array.
|
||||
fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info: anytype) !ParameterTypes(F) {
|
||||
const local = &self.local;
|
||||
const context = self.context;
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
|
||||
const params = @typeInfo(F).@"fn".params[offset..];
|
||||
@@ -384,7 +369,25 @@ fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info:
|
||||
// from our params slice, because we don't want to bind it to
|
||||
// a JS argument
|
||||
if (comptime isPage(params[params.len - 1].type.?)) {
|
||||
@field(args, tupleFieldName(params.len - 1 + offset)) = local.ctx.page;
|
||||
@field(args, tupleFieldName(params.len - 1 + offset)) = self.context.page;
|
||||
break :blk params[0 .. params.len - 1];
|
||||
}
|
||||
|
||||
// If the last parameter is a special JsThis, set it, and exclude it
|
||||
// from our params slice, because we don't want to bind it to
|
||||
// a JS argument
|
||||
if (comptime params[params.len - 1].type.? == js.This) {
|
||||
@field(args, tupleFieldName(params.len - 1 + offset)) = .{ .obj = .{
|
||||
.context = context,
|
||||
.js_obj = info.getThis(),
|
||||
} };
|
||||
|
||||
// AND the 2nd last parameter is state
|
||||
if (params.len > 1 and comptime isPage(params[params.len - 2].type.?)) {
|
||||
@field(args, tupleFieldName(params.len - 2 + offset)) = self.context.page;
|
||||
break :blk params[0 .. params.len - 2];
|
||||
}
|
||||
|
||||
break :blk params[0 .. params.len - 1];
|
||||
}
|
||||
|
||||
@@ -410,15 +413,16 @@ fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info:
|
||||
const last_parameter_type_info = @typeInfo(last_parameter_type);
|
||||
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
|
||||
const slice_type = last_parameter_type_info.pointer.child;
|
||||
const corresponding_js_value = info.getArg(@intCast(last_js_parameter), local);
|
||||
const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter)));
|
||||
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
|
||||
is_variadic = true;
|
||||
if (js_parameter_count == 0) {
|
||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||
} else if (js_parameter_count >= params_to_map.len) {
|
||||
const arr = try local.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
|
||||
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
|
||||
for (arr, last_js_parameter..) |*a, i| {
|
||||
a.* = try local.jsValueToZig(slice_type, info.getArg(@intCast(i), local));
|
||||
const js_value = info.getArg(@as(u32, @intCast(i)));
|
||||
a.* = try context.jsValueToZig(slice_type, js_value);
|
||||
}
|
||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
|
||||
} else {
|
||||
@@ -438,14 +442,16 @@ fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info:
|
||||
|
||||
if (comptime isPage(param.type.?)) {
|
||||
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
|
||||
} else if (comptime param.type.? == js.This) {
|
||||
@compileError("JsThis must be the last parameter: " ++ @typeName(F));
|
||||
} else if (i >= js_parameter_count) {
|
||||
if (@typeInfo(param.type.?) != .optional) {
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
@field(args, tupleFieldName(field_index)) = null;
|
||||
} else {
|
||||
const js_val = info.getArg(@intCast(i), local);
|
||||
@field(args, tupleFieldName(field_index)) = local.jsValueToZig(param.type.?, js_val) catch {
|
||||
const js_value = info.getArg(@as(u32, @intCast(i)));
|
||||
@field(args, tupleFieldName(field_index)) = context.jsValueToZig(param.type.?, js_value) catch {
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
}
|
||||
@@ -456,26 +462,25 @@ fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info:
|
||||
|
||||
// This is extracted to speed up compilation. When left inlined in handleError,
|
||||
// this can add as much as 10 seconds of compilation time.
|
||||
fn logFunctionCallError(self: *Caller, type_name: []const u8, func: []const u8, err: anyerror, info: FunctionCallbackInfo) void {
|
||||
fn logFunctionCallError(self: *Caller, type_name: []const u8, func: []const u8, err: anyerror, info: v8.FunctionCallbackInfo) void {
|
||||
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
|
||||
log.info(.js, "function call error", .{
|
||||
.type = type_name,
|
||||
.func = func,
|
||||
.err = err,
|
||||
.args = args_dump,
|
||||
.stack = self.local.stackTrace() catch |err1| @errorName(err1),
|
||||
.stack = self.context.stackTrace() catch |err1| @errorName(err1),
|
||||
});
|
||||
}
|
||||
|
||||
fn serializeFunctionArgs(self: *Caller, info: FunctionCallbackInfo) ![]const u8 {
|
||||
const local = &self.local;
|
||||
var buf = std.Io.Writer.Allocating.init(local.call_arena);
|
||||
fn serializeFunctionArgs(self: *Caller, info: v8.FunctionCallbackInfo) ![]const u8 {
|
||||
const context = self.context;
|
||||
var buf = std.Io.Writer.Allocating.init(context.call_arena);
|
||||
|
||||
const separator = log.separator();
|
||||
for (0..info.length()) |i| {
|
||||
try buf.writer.print("{s}{d} - ", .{ separator, i + 1 });
|
||||
const js_value = info.getArg(@intCast(i), local);
|
||||
try local.debugValue(js_value, &buf.writer);
|
||||
try context.debugValue(info.getArg(@intCast(i)), &buf.writer);
|
||||
}
|
||||
return buf.written();
|
||||
}
|
||||
@@ -524,64 +529,6 @@ fn isPage(comptime T: type) bool {
|
||||
return T == *Page or T == *const Page;
|
||||
}
|
||||
|
||||
// These wrap the raw v8 C API to provide a cleaner interface.
|
||||
pub const FunctionCallbackInfo = struct {
|
||||
handle: *const v8.FunctionCallbackInfo,
|
||||
|
||||
pub fn length(self: FunctionCallbackInfo) u32 {
|
||||
return @intCast(v8.v8__FunctionCallbackInfo__Length(self.handle));
|
||||
}
|
||||
|
||||
pub fn getArg(self: FunctionCallbackInfo, index: u32, local: *const js.Local) js.Value {
|
||||
return .{ .local = local, .handle = v8.v8__FunctionCallbackInfo__INDEX(self.handle, @intCast(index)).? };
|
||||
}
|
||||
|
||||
pub fn getThis(self: FunctionCallbackInfo) *const v8.Object {
|
||||
return v8.v8__FunctionCallbackInfo__This(self.handle).?;
|
||||
}
|
||||
|
||||
pub fn getReturnValue(self: FunctionCallbackInfo) ReturnValue {
|
||||
var rv: v8.ReturnValue = undefined;
|
||||
v8.v8__FunctionCallbackInfo__GetReturnValue(self.handle, &rv);
|
||||
return .{ .handle = rv };
|
||||
}
|
||||
|
||||
fn isConstructCall(self: FunctionCallbackInfo) bool {
|
||||
return v8.v8__FunctionCallbackInfo__IsConstructCall(self.handle);
|
||||
}
|
||||
};
|
||||
|
||||
pub const PropertyCallbackInfo = struct {
|
||||
handle: *const v8.PropertyCallbackInfo,
|
||||
|
||||
pub fn getThis(self: PropertyCallbackInfo) *const v8.Object {
|
||||
return v8.v8__PropertyCallbackInfo__This(self.handle).?;
|
||||
}
|
||||
|
||||
pub fn getReturnValue(self: PropertyCallbackInfo) ReturnValue {
|
||||
var rv: v8.ReturnValue = undefined;
|
||||
v8.v8__PropertyCallbackInfo__GetReturnValue(self.handle, &rv);
|
||||
return .{ .handle = rv };
|
||||
}
|
||||
};
|
||||
|
||||
const ReturnValue = struct {
|
||||
handle: v8.ReturnValue,
|
||||
|
||||
pub fn set(self: ReturnValue, value: anytype) void {
|
||||
const T = @TypeOf(value);
|
||||
if (T == *const v8.Object) {
|
||||
self.setValueHandle(@ptrCast(value));
|
||||
} else if (T == *const v8.Value) {
|
||||
self.setValueHandle(value);
|
||||
} else if (T == js.Value) {
|
||||
self.setValueHandle(value.handle);
|
||||
} else {
|
||||
@compileError("Unsupported type for ReturnValue.set: " ++ @typeName(T));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setValueHandle(self: ReturnValue, handle: *const v8.Value) void {
|
||||
v8.v8__ReturnValue__Set(self.handle, handle);
|
||||
}
|
||||
};
|
||||
fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value {
|
||||
return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,26 +18,20 @@
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const v8 = js.v8;
|
||||
|
||||
const App = @import("../../App.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const bridge = @import("bridge.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const Isolate = @import("Isolate.zig");
|
||||
const Platform = @import("Platform.zig");
|
||||
const Snapshot = @import("Snapshot.zig");
|
||||
const Inspector = @import("Inspector.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const Window = @import("../webapi/Window.zig");
|
||||
const ExecutionWorld = @import("ExecutionWorld.zig");
|
||||
|
||||
const JsApis = bridge.JsApis;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
// The Env maps to a V8 isolate, which represents a isolated sandbox for
|
||||
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
||||
@@ -47,308 +41,118 @@ const IS_DEBUG = builtin.mode == .Debug;
|
||||
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
|
||||
const Env = @This();
|
||||
|
||||
app: *App,
|
||||
allocator: Allocator,
|
||||
|
||||
platform: *const Platform,
|
||||
|
||||
// the global isolate
|
||||
isolate: js.Isolate,
|
||||
|
||||
contexts: std.ArrayList(*js.Context),
|
||||
isolate: v8.Isolate,
|
||||
|
||||
// just kept around because we need to free it on deinit
|
||||
isolate_params: *v8.CreateParams,
|
||||
|
||||
context_id: usize,
|
||||
|
||||
// Global handles that need to be freed on deinit
|
||||
eternal_function_templates: []v8.Eternal,
|
||||
|
||||
// Dynamic slice to avoid circular dependency on JsApis.len at comptime
|
||||
templates: []*const v8.FunctionTemplate,
|
||||
|
||||
// Global template created once per isolate and reused across all contexts
|
||||
global_template: v8.Eternal,
|
||||
|
||||
// Inspector associated with the Isolate. Exists when CDP is being used.
|
||||
inspector: ?*Inspector,
|
||||
|
||||
pub const InitOpts = struct {
|
||||
with_inspector: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(app: *App, opts: InitOpts) !Env {
|
||||
const allocator = app.allocator;
|
||||
const snapshot = &app.snapshot;
|
||||
templates: []v8.FunctionTemplate,
|
||||
|
||||
pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot) !Env {
|
||||
var params = try allocator.create(v8.CreateParams);
|
||||
errdefer allocator.destroy(params);
|
||||
v8.v8__Isolate__CreateParams__CONSTRUCT(params);
|
||||
v8.c.v8__Isolate__CreateParams__CONSTRUCT(params);
|
||||
params.snapshot_blob = @ptrCast(&snapshot.startup_data);
|
||||
|
||||
params.array_buffer_allocator = v8.v8__ArrayBuffer__Allocator__NewDefaultAllocator().?;
|
||||
errdefer v8.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);
|
||||
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
|
||||
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
|
||||
|
||||
params.external_references = &snapshot.external_references;
|
||||
|
||||
var isolate = js.Isolate.init(params);
|
||||
var isolate = v8.Isolate.init(params);
|
||||
errdefer isolate.deinit();
|
||||
const isolate_handle = isolate.handle;
|
||||
|
||||
v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate_handle, Context.dynamicModuleCallback);
|
||||
v8.v8__Isolate__SetPromiseRejectCallback(isolate_handle, promiseRejectCallback);
|
||||
v8.v8__Isolate__SetMicrotasksPolicy(isolate_handle, v8.kExplicit);
|
||||
v8.v8__Isolate__SetFatalErrorHandler(isolate_handle, fatalCallback);
|
||||
v8.v8__Isolate__SetOOMErrorHandler(isolate_handle, oomCallback);
|
||||
// This is the callback that runs whenever a module is dynamically imported.
|
||||
isolate.setHostImportModuleDynamicallyCallback(Context.dynamicModuleCallback);
|
||||
isolate.setPromiseRejectCallback(promiseRejectCallback);
|
||||
isolate.setMicrotasksPolicy(v8.c.kExplicit);
|
||||
|
||||
isolate.enter();
|
||||
errdefer isolate.exit();
|
||||
|
||||
v8.v8__Isolate__SetHostInitializeImportMetaObjectCallback(isolate_handle, Context.metaObjectCallback);
|
||||
isolate.setHostInitializeImportMetaObjectCallback(Context.metaObjectCallback);
|
||||
|
||||
// Allocate arrays dynamically to avoid comptime dependency on JsApis.len
|
||||
const eternal_function_templates = try allocator.alloc(v8.Eternal, JsApis.len);
|
||||
errdefer allocator.free(eternal_function_templates);
|
||||
|
||||
const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len);
|
||||
// Allocate templates array dynamically to avoid comptime dependency on JsApis.len
|
||||
const templates = try allocator.alloc(v8.FunctionTemplate, JsApis.len);
|
||||
errdefer allocator.free(templates);
|
||||
|
||||
var global_eternal: v8.Eternal = undefined;
|
||||
{
|
||||
var temp_scope: js.HandleScope = undefined;
|
||||
temp_scope.init(isolate);
|
||||
var temp_scope: v8.HandleScope = undefined;
|
||||
v8.HandleScope.init(&temp_scope, isolate);
|
||||
defer temp_scope.deinit();
|
||||
const context = v8.Context.init(isolate, null, null);
|
||||
|
||||
context.enter();
|
||||
defer context.exit();
|
||||
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
JsApi.Meta.class_id = i;
|
||||
const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate_handle, snapshot.data_start + i);
|
||||
const function_handle: *const v8.FunctionTemplate = @ptrCast(data);
|
||||
// Make function template eternal
|
||||
v8.v8__Eternal__New(isolate_handle, @ptrCast(function_handle), &eternal_function_templates[i]);
|
||||
|
||||
// Extract the local handle from the global for easy access
|
||||
const eternal_ptr = v8.v8__Eternal__Get(&eternal_function_templates[i], isolate_handle);
|
||||
templates[i] = @ptrCast(@alignCast(eternal_ptr.?));
|
||||
const data = context.getDataFromSnapshotOnce(snapshot.data_start + i);
|
||||
const function = v8.FunctionTemplate{ .handle = @ptrCast(data) };
|
||||
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, function).castToFunctionTemplate();
|
||||
}
|
||||
|
||||
// Create global template once per isolate
|
||||
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate_handle);
|
||||
const window_name = v8.v8__String__NewFromUtf8(isolate_handle, "Window", v8.kNormal, 6);
|
||||
v8.v8__FunctionTemplate__SetClassName(js_global, window_name);
|
||||
|
||||
// Find Window in JsApis by name (avoids circular import)
|
||||
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
|
||||
v8.v8__FunctionTemplate__Inherit(js_global, templates[window_index]);
|
||||
|
||||
const global_template_local = v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;
|
||||
v8.v8__ObjectTemplate__SetNamedHandler(global_template_local, &.{
|
||||
.getter = bridge.unknownWindowPropertyCallback,
|
||||
.setter = null,
|
||||
.query = null,
|
||||
.deleter = null,
|
||||
.enumerator = null,
|
||||
.definer = null,
|
||||
.descriptor = null,
|
||||
.data = null,
|
||||
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||
});
|
||||
v8.v8__Eternal__New(isolate_handle, @ptrCast(global_template_local), &global_eternal);
|
||||
}
|
||||
|
||||
var inspector: ?*js.Inspector = null;
|
||||
if (opts.with_inspector) {
|
||||
inspector = try Inspector.init(allocator, isolate_handle);
|
||||
}
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.context_id = 0,
|
||||
.contexts = .empty,
|
||||
.isolate = isolate,
|
||||
.platform = &app.platform,
|
||||
.platform = platform,
|
||||
.allocator = allocator,
|
||||
.templates = templates,
|
||||
.isolate_params = params,
|
||||
.inspector = inspector,
|
||||
.eternal_function_templates = eternal_function_templates,
|
||||
.global_template = global_eternal,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Env) void {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.contexts.items.len == 0);
|
||||
}
|
||||
for (self.contexts.items) |ctx| {
|
||||
ctx.deinit();
|
||||
}
|
||||
|
||||
const allocator = self.app.allocator;
|
||||
if (self.inspector) |i| {
|
||||
i.deinit(allocator);
|
||||
}
|
||||
|
||||
self.contexts.deinit(allocator);
|
||||
|
||||
allocator.free(self.templates);
|
||||
allocator.free(self.eternal_function_templates);
|
||||
|
||||
self.isolate.exit();
|
||||
self.isolate.deinit();
|
||||
v8.v8__ArrayBuffer__Allocator__DELETE(self.isolate_params.array_buffer_allocator.?);
|
||||
allocator.destroy(self.isolate_params);
|
||||
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
|
||||
self.allocator.destroy(self.isolate_params);
|
||||
self.allocator.free(self.templates);
|
||||
}
|
||||
|
||||
pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
|
||||
const context_arena = try self.app.arena_pool.acquire();
|
||||
errdefer self.app.arena_pool.release(context_arena);
|
||||
|
||||
const isolate = self.isolate;
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
// 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 v8_context = v8.v8__Context__New(isolate.handle, global_template, null).?;
|
||||
|
||||
// Create the v8::Context and wrap it in a v8::Global
|
||||
var context_global: v8.Global = undefined;
|
||||
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
|
||||
|
||||
// our window wrapped in a v8::Global
|
||||
const global_obj = v8.v8__Context__Global(v8_context).?;
|
||||
var global_global: v8.Global = undefined;
|
||||
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
|
||||
|
||||
if (enter) {
|
||||
v8.v8__Context__Enter(v8_context);
|
||||
}
|
||||
errdefer if (enter) {
|
||||
v8.v8__Context__Exit(v8_context);
|
||||
};
|
||||
|
||||
const context_id = self.context_id;
|
||||
self.context_id = context_id + 1;
|
||||
|
||||
const context = try context_arena.create(Context);
|
||||
context.* = .{
|
||||
.env = self,
|
||||
.page = page,
|
||||
.id = context_id,
|
||||
.entered = enter,
|
||||
.isolate = isolate,
|
||||
.arena = context_arena,
|
||||
.handle = context_global,
|
||||
.templates = self.templates,
|
||||
.call_arena = page.call_arena,
|
||||
.script_manager = &page._script_manager,
|
||||
.scheduler = .init(context_arena),
|
||||
.finalizer_callback_pool = std.heap.MemoryPool(Context.FinalizerCallback).init(self.app.allocator),
|
||||
};
|
||||
try context.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global);
|
||||
|
||||
// Store a pointer to our context inside the v8 context so that, given
|
||||
// a v8 context, we can get our context out
|
||||
const data = isolate.initBigInt(@intFromPtr(context));
|
||||
v8.v8__Context__SetEmbedderData(v8_context, 1, @ptrCast(data.handle));
|
||||
|
||||
try self.contexts.append(self.app.allocator, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
pub fn destroyContext(self: *Env, context: *Context) void {
|
||||
for (self.contexts.items, 0..) |ctx, i| {
|
||||
if (ctx == context) {
|
||||
_ = self.contexts.swapRemove(i);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (comptime IS_DEBUG) {
|
||||
@panic("Tried to remove unknown context");
|
||||
}
|
||||
}
|
||||
|
||||
const isolate = self.isolate;
|
||||
if (self.inspector) |inspector| {
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(isolate);
|
||||
defer hs.deinit();
|
||||
inspector.contextDestroyed(@ptrCast(v8.v8__Global__Get(&context.handle, isolate.handle)));
|
||||
}
|
||||
|
||||
context.deinit();
|
||||
isolate.notifyContextDisposed();
|
||||
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector {
|
||||
return Inspector.init(arena, self.isolate, ctx);
|
||||
}
|
||||
|
||||
pub fn runMicrotasks(self: *const Env) void {
|
||||
self.isolate.performMicrotasksCheckpoint();
|
||||
}
|
||||
|
||||
pub fn runMacrotasks(self: *Env) !?u64 {
|
||||
var ms_to_next_task: ?u64 = null;
|
||||
for (self.contexts.items) |ctx| {
|
||||
if (comptime builtin.is_test == false) {
|
||||
// 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
|
||||
// underterministic whether a timer will or won't run before the
|
||||
// page shutsdown. But for tests, we need to run them to their end.
|
||||
if (ctx.scheduler.hasReadyTasks() == false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
const entered = ctx.enter(&hs);
|
||||
defer entered.exit();
|
||||
|
||||
const ms = (try ctx.scheduler.run()) orelse continue;
|
||||
if (ms_to_next_task == null or ms < ms_to_next_task.?) {
|
||||
ms_to_next_task = ms;
|
||||
}
|
||||
}
|
||||
return ms_to_next_task;
|
||||
}
|
||||
|
||||
pub fn pumpMessageLoop(self: *const Env) bool {
|
||||
var hs: v8.HandleScope = undefined;
|
||||
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);
|
||||
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||
|
||||
return v8.v8__Platform__PumpMessageLoop(self.platform.handle, self.isolate.handle, false);
|
||||
return self.platform.inner.pumpMessageLoop(self.isolate, false);
|
||||
}
|
||||
|
||||
pub fn runIdleTasks(self: *const Env) void {
|
||||
v8.v8__Platform__RunIdleTasks(self.platform.handle, self.isolate.handle, 1);
|
||||
return self.platform.inner.runIdleTasks(self.isolate, 1);
|
||||
}
|
||||
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
|
||||
return .{
|
||||
.env = self,
|
||||
.context = null,
|
||||
.context_arena = ArenaAllocator.init(self.allocator),
|
||||
};
|
||||
}
|
||||
|
||||
// V8 doesn't immediately free memory associated with
|
||||
// a Context, it's managed by the garbage collector. We use the
|
||||
// `lowMemoryNotification` call on the isolate to encourage v8 to free
|
||||
// any contexts which have been freed.
|
||||
// This GC is very aggressive. Use memoryPressureNotification for less
|
||||
// aggressive GC passes.
|
||||
pub fn lowMemoryNotification(self: *Env) void {
|
||||
var handle_scope: js.HandleScope = undefined;
|
||||
handle_scope.init(self.isolate);
|
||||
var handle_scope: v8.HandleScope = undefined;
|
||||
v8.HandleScope.init(&handle_scope, self.isolate);
|
||||
defer handle_scope.deinit();
|
||||
self.isolate.lowMemoryNotification();
|
||||
}
|
||||
|
||||
// V8 doesn't immediately free memory associated with
|
||||
// a Context, it's managed by the garbage collector. We use the
|
||||
// `memoryPressureNotification` call on the isolate to encourage v8 to free
|
||||
// any contexts which have been freed.
|
||||
// The level indicates the aggressivity of the GC required:
|
||||
// moderate speeds up incremental GC
|
||||
// critical runs one full GC
|
||||
// For a more aggressive GC, use lowMemoryNotification.
|
||||
pub fn memoryPressureNotification(self: *Env, level: Isolate.MemoryPressureLevel) void {
|
||||
var handle_scope: js.HandleScope = undefined;
|
||||
handle_scope.init(self.isolate);
|
||||
defer handle_scope.deinit();
|
||||
self.isolate.memoryPressureNotification(level);
|
||||
}
|
||||
|
||||
pub fn dumpMemoryStats(self: *Env) void {
|
||||
const stats = self.isolate.getHeapStatistics();
|
||||
std.debug.print(
|
||||
@@ -370,38 +174,20 @@ pub fn dumpMemoryStats(self: *Env) void {
|
||||
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
|
||||
}
|
||||
|
||||
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
||||
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
||||
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
||||
const js_isolate = js.Isolate{ .handle = v8_isolate };
|
||||
const ctx = Context.fromIsolate(js_isolate);
|
||||
fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
|
||||
const msg = v8.PromiseRejectMessage.initFromC(v8_msg);
|
||||
const isolate = msg.getPromise().toObject().getIsolate();
|
||||
const context = Context.fromIsolate(isolate);
|
||||
|
||||
const local = js.Local{
|
||||
.ctx = ctx,
|
||||
.isolate = js_isolate,
|
||||
.handle = v8.v8__Isolate__GetCurrentContext(v8_isolate).?,
|
||||
.call_arena = ctx.call_arena,
|
||||
};
|
||||
const value =
|
||||
if (msg.getValue()) |v8_value|
|
||||
context.valueToString(v8_value, .{}) catch |err| @errorName(err)
|
||||
else
|
||||
"no value";
|
||||
|
||||
const page = ctx.page;
|
||||
page.window.unhandledPromiseRejection(.{
|
||||
.local = &local,
|
||||
.handle = &message_handle,
|
||||
}, page) catch |err| {
|
||||
log.warn(.browser, "unhandled rejection handler", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
fn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void {
|
||||
const location = std.mem.span(c_location);
|
||||
const message = std.mem.span(c_message);
|
||||
log.fatal(.app, "V8 fatal callback", .{ .location = location, .message = message });
|
||||
@import("../../crash_handler.zig").crash("Fatal V8 Error", .{ .location = location, .message = message }, @returnAddress());
|
||||
}
|
||||
|
||||
fn oomCallback(c_location: [*c]const u8, details: ?*const v8.OOMDetails) callconv(.c) void {
|
||||
const location = std.mem.span(c_location);
|
||||
const detail = if (details) |d| std.mem.span(d.detail) else "";
|
||||
log.fatal(.app, "V8 OOM", .{ .location = location, .detail = detail });
|
||||
@import("../../crash_handler.zig").crash("V8 OOM", .{ .location = location, .detail = detail }, @returnAddress());
|
||||
log.debug(.js, "unhandled rejection", .{
|
||||
.value = value,
|
||||
.stack = context.stackTrace() catch |err| @errorName(err) orelse "???",
|
||||
.note = "This should be updated to call window.unhandledrejection",
|
||||
});
|
||||
}
|
||||
|
||||
197
src/browser/js/ExecutionWorld.zig
Normal file
197
src/browser/js/ExecutionWorld.zig
Normal file
@@ -0,0 +1,197 @@
|
||||
// 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 IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Env = @import("Env.zig");
|
||||
const Context = @import("Context.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const CONTEXT_ARENA_RETAIN = 1024 * 64;
|
||||
|
||||
// ExecutionWorld closely models a JS World.
|
||||
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#World
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
|
||||
const ExecutionWorld = @This();
|
||||
|
||||
env: *Env,
|
||||
|
||||
// Arena whose lifetime is for a single page load. Where
|
||||
// the call_arena lives for a single function call, the context_arena
|
||||
// lives for the lifetime of the entire page. The allocator will be
|
||||
// owned by the Context, but the arena itself is owned by the ExecutionWorld
|
||||
// so that we can re-use it from context to context.
|
||||
context_arena: ArenaAllocator,
|
||||
|
||||
// Currently a context maps to a Browser's Page. Here though, it's only a
|
||||
// mechanism to organization page-specific memory. The ExecutionWorld
|
||||
// does all the work, but having all page-specific data structures
|
||||
// grouped together helps keep things clean.
|
||||
context: ?Context = null,
|
||||
|
||||
// no init, must be initialized via env.newExecutionWorld()
|
||||
|
||||
pub fn deinit(self: *ExecutionWorld) void {
|
||||
if (self.context != null) {
|
||||
self.removeContext();
|
||||
}
|
||||
|
||||
self.context_arena.deinit();
|
||||
}
|
||||
|
||||
// Only the top Context in the Main ExecutionWorld should hold a handle_scope.
|
||||
// A v8.HandleScope is like an arena. Once created, any "Local" that
|
||||
// v8 creates will be released (or at least, releasable by the v8 GC)
|
||||
// when the handle_scope is freed.
|
||||
// We also maintain our own "context_arena" which allows us to have
|
||||
// all page related memory easily managed.
|
||||
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context {
|
||||
std.debug.assert(self.context == null);
|
||||
|
||||
const env = self.env;
|
||||
const isolate = env.isolate;
|
||||
|
||||
var v8_context: v8.Context = blk: {
|
||||
var temp_scope: v8.HandleScope = undefined;
|
||||
v8.HandleScope.init(&temp_scope, isolate);
|
||||
defer temp_scope.deinit();
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
// Getting this into the snapshot is tricky (anything involving the
|
||||
// global is tricky). Easier to do here, and in debug mode, we're
|
||||
// fine with paying the small perf hit.
|
||||
const js_global = v8.FunctionTemplate.initDefault(isolate);
|
||||
const global_template = js_global.getInstanceTemplate();
|
||||
|
||||
global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{
|
||||
.getter = unknownPropertyCallback,
|
||||
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
|
||||
}, null);
|
||||
}
|
||||
|
||||
const context_local = v8.Context.init(isolate, null, null);
|
||||
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
|
||||
break :blk v8_context;
|
||||
};
|
||||
|
||||
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
|
||||
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
|
||||
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
|
||||
var handle_scope: ?v8.HandleScope = null;
|
||||
if (enter) {
|
||||
handle_scope = @as(v8.HandleScope, undefined);
|
||||
v8.HandleScope.init(&handle_scope.?, isolate);
|
||||
v8_context.enter();
|
||||
}
|
||||
errdefer if (enter) {
|
||||
v8_context.exit();
|
||||
handle_scope.?.deinit();
|
||||
};
|
||||
|
||||
const context_id = env.context_id;
|
||||
env.context_id = context_id + 1;
|
||||
|
||||
self.context = Context{
|
||||
.page = page,
|
||||
.id = context_id,
|
||||
.isolate = isolate,
|
||||
.v8_context = v8_context,
|
||||
.templates = env.templates,
|
||||
.handle_scope = handle_scope,
|
||||
.script_manager = &page._script_manager,
|
||||
.call_arena = page.call_arena,
|
||||
.arena = self.context_arena.allocator(),
|
||||
};
|
||||
|
||||
var context = &self.context.?;
|
||||
// Store a pointer to our context inside the v8 context so that, given
|
||||
// a v8 context, we can get our context out
|
||||
const data = isolate.initBigIntU64(@intCast(@intFromPtr(context)));
|
||||
v8_context.setEmbedderData(1, data);
|
||||
|
||||
try context.setupGlobal();
|
||||
return context;
|
||||
}
|
||||
|
||||
pub fn removeContext(self: *ExecutionWorld) void {
|
||||
// Force running the micro task to drain the queue before reseting the
|
||||
// context arena.
|
||||
// Tasks in the queue are relying to the arena memory could be present in
|
||||
// the queue. Running them later could lead to invalid memory accesses.
|
||||
self.env.runMicrotasks();
|
||||
|
||||
self.context.?.deinit();
|
||||
self.context = null;
|
||||
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
|
||||
}
|
||||
|
||||
pub fn terminateExecution(self: *const ExecutionWorld) void {
|
||||
self.env.isolate.terminateExecution();
|
||||
}
|
||||
|
||||
pub fn resumeExecution(self: *const ExecutionWorld) void {
|
||||
self.env.isolate.cancelTerminateExecution();
|
||||
}
|
||||
|
||||
pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
const context = Context.fromIsolate(info.getIsolate());
|
||||
|
||||
const property = context.valueToString(.{ .handle = c_name.? }, .{}) catch "???";
|
||||
|
||||
const ignored = std.StaticStringMap(void).initComptime(.{
|
||||
.{ "process", {} },
|
||||
.{ "ShadyDOM", {} },
|
||||
.{ "ShadyCSS", {} },
|
||||
|
||||
.{ "litNonce", {} },
|
||||
.{ "litHtmlVersions", {} },
|
||||
.{ "litElementVersions", {} },
|
||||
.{ "litHtmlPolyfillSupport", {} },
|
||||
.{ "litElementHydrateSupport", {} },
|
||||
.{ "litElementPolyfillSupport", {} },
|
||||
.{ "reactiveElementVersions", {} },
|
||||
|
||||
.{ "recaptcha", {} },
|
||||
.{ "grecaptcha", {} },
|
||||
.{ "___grecaptcha_cfg", {} },
|
||||
.{ "__recaptcha_api", {} },
|
||||
.{ "__google_recaptcha_client", {} },
|
||||
|
||||
.{ "CLOSURE_FLAGS", {} },
|
||||
});
|
||||
|
||||
if (!ignored.has(property)) {
|
||||
log.debug(.unknown_prop, "unkown global property", .{
|
||||
.info = "but the property can exist in pure JS",
|
||||
.stack = context.stackTrace() catch "???",
|
||||
.property = property,
|
||||
});
|
||||
}
|
||||
|
||||
return v8.Intercepted.No;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -20,237 +20,148 @@ const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const PersistentFunction = v8.Persistent(v8.Function);
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Function = @This();
|
||||
|
||||
local: *const js.Local,
|
||||
this: ?*const v8.Object = null,
|
||||
handle: *const v8.Function,
|
||||
id: usize,
|
||||
context: *js.Context,
|
||||
this: ?v8.Object = null,
|
||||
func: PersistentFunction,
|
||||
|
||||
pub const Result = struct {
|
||||
stack: ?[]const u8,
|
||||
exception: []const u8,
|
||||
};
|
||||
|
||||
pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 {
|
||||
const name = self.func.castToFunction().getName();
|
||||
return self.context.valueToString(name, .{ .allocator = allocator });
|
||||
}
|
||||
|
||||
pub fn setName(self: *const Function, name: []const u8) void {
|
||||
const v8_name = v8.String.initUtf8(self.context.isolate, name);
|
||||
self.func.castToFunction().setName(v8_name);
|
||||
}
|
||||
|
||||
pub fn withThis(self: *const Function, value: anytype) !Function {
|
||||
const local = self.local;
|
||||
const this_obj = if (@TypeOf(value) == js.Object)
|
||||
value.handle
|
||||
value.js_obj
|
||||
else
|
||||
(try local.zigValueToJs(value, .{})).handle;
|
||||
(try self.context.zigValueToJs(value, .{})).castTo(v8.Object);
|
||||
|
||||
return .{
|
||||
.local = local,
|
||||
.id = self.id,
|
||||
.this = this_obj,
|
||||
.handle = self.handle,
|
||||
.func = self.func,
|
||||
.context = self.context,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object {
|
||||
const local = self.local;
|
||||
pub fn newInstance(self: *const Function, result: *Result) !js.Object {
|
||||
const context = self.context;
|
||||
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(local);
|
||||
try_catch.init(context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
// This creates a new instance using this Function as a constructor.
|
||||
// const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
|
||||
const handle = v8.v8__Function__NewInstance(self.handle, local.handle, 0, null) orelse {
|
||||
caught.* = try_catch.caughtOrError(local.call_arena, error.Unknown);
|
||||
// This returns a generic Object
|
||||
const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse {
|
||||
if (try_catch.hasCaught()) {
|
||||
const allocator = context.call_arena;
|
||||
result.stack = try_catch.stack(allocator) catch null;
|
||||
result.exception = (try_catch.exception(allocator) catch "???") orelse "???";
|
||||
} else {
|
||||
result.stack = null;
|
||||
result.exception = "???";
|
||||
}
|
||||
return error.JsConstructorFailed;
|
||||
};
|
||||
|
||||
return .{
|
||||
.local = local,
|
||||
.handle = handle,
|
||||
.context = context,
|
||||
.js_obj = js_obj,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
return self._tryCallWithThis(T, self.getThis(), args, &caught, .{}) catch |err| {
|
||||
log.warn(.js, "call caught", .{ .err = err, .caught = caught });
|
||||
return err;
|
||||
};
|
||||
return self.callWithThis(T, self.getThis(), args);
|
||||
}
|
||||
|
||||
pub fn callRethrow(self: *const Function, comptime T: type, args: anytype) !T {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
return self._tryCallWithThis(T, self.getThis(), args, &caught, .{ .rethrow = true }) catch |err| {
|
||||
log.warn(.js, "call caught", .{ .err = err, .caught = caught });
|
||||
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T {
|
||||
return self.tryCallWithThis(T, self.getThis(), args, result);
|
||||
}
|
||||
|
||||
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !T {
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(self.context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
return self.callWithThis(T, this, args) catch |err| {
|
||||
if (try_catch.hasCaught()) {
|
||||
const allocator = self.context.call_arena;
|
||||
result.stack = try_catch.stack(allocator) catch null;
|
||||
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
|
||||
} else {
|
||||
result.stack = null;
|
||||
result.exception = @errorName(err);
|
||||
}
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
return self._tryCallWithThis(T, this, args, &caught, .{}) catch |err| {
|
||||
log.warn(.js, "callWithThis caught", .{ .err = err, .caught = caught });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
const context = self.context;
|
||||
|
||||
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T {
|
||||
return self._tryCallWithThis(T, self.getThis(), args, caught, .{});
|
||||
}
|
||||
|
||||
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
|
||||
return self._tryCallWithThis(T, this, args, caught, .{});
|
||||
}
|
||||
|
||||
const CallOpts = struct {
|
||||
rethrow: bool = false,
|
||||
};
|
||||
fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught, comptime opts: CallOpts) !T {
|
||||
caught.* = .{};
|
||||
const local = self.local;
|
||||
|
||||
// When we're calling a function from within JavaScript itself, this isn't
|
||||
// necessary. We're within a Caller instantiation, which will already have
|
||||
// incremented the call_depth and it won't decrement it until the Caller is
|
||||
// done.
|
||||
// But some JS functions are initiated from Zig code, and not v8. For
|
||||
// example, Observers, some event and window callbacks. In those cases, we
|
||||
// need to increase the call_depth so that the call_arena remains valid for
|
||||
// the duration of the function call. If we don't do this, the call_arena
|
||||
// will be reset after each statement of the function which executes Zig code.
|
||||
const ctx = local.ctx;
|
||||
const call_depth = ctx.call_depth;
|
||||
ctx.call_depth = call_depth + 1;
|
||||
defer ctx.call_depth = call_depth;
|
||||
|
||||
const js_this = blk: {
|
||||
if (@TypeOf(this) == js.Object) {
|
||||
break :blk this;
|
||||
}
|
||||
break :blk try local.zigValueToJs(this, .{});
|
||||
};
|
||||
const js_this = try context.valueToExistingObject(this);
|
||||
|
||||
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
|
||||
|
||||
const js_args: []const *const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
|
||||
const js_args: []const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
|
||||
.@"struct" => |s| blk: {
|
||||
const fields = s.fields;
|
||||
var js_args: [fields.len]*const v8.Value = undefined;
|
||||
var js_args: [fields.len]v8.Value = undefined;
|
||||
inline for (fields, 0..) |f, i| {
|
||||
js_args[i] = (try local.zigValueToJs(@field(aargs, f.name), .{})).handle;
|
||||
js_args[i] = try context.zigValueToJs(@field(aargs, f.name), .{});
|
||||
}
|
||||
const cargs: [fields.len]*const v8.Value = js_args;
|
||||
const cargs: [fields.len]v8.Value = js_args;
|
||||
break :blk &cargs;
|
||||
},
|
||||
.pointer => blk: {
|
||||
var values = try local.call_arena.alloc(*const v8.Value, args.len);
|
||||
var values = try context.call_arena.alloc(v8.Value, args.len);
|
||||
for (args, 0..) |a, i| {
|
||||
values[i] = (try local.zigValueToJs(a, .{})).handle;
|
||||
values[i] = try context.zigValueToJs(a, .{});
|
||||
}
|
||||
break :blk values;
|
||||
},
|
||||
else => @compileError("JS Function called with invalid paremter type"),
|
||||
};
|
||||
|
||||
const c_args = @as(?[*]const ?*v8.Value, @ptrCast(js_args.ptr));
|
||||
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(local);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {
|
||||
if ((comptime opts.rethrow) and try_catch.hasCaught()) {
|
||||
try_catch.rethrow();
|
||||
return error.TryCatchRethrow;
|
||||
}
|
||||
caught.* = try_catch.caughtOrError(local.call_arena, error.JSExecCallback);
|
||||
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
|
||||
if (result == null) {
|
||||
// std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
|
||||
return error.JSExecCallback;
|
||||
};
|
||||
|
||||
if (@typeInfo(T) == .void) {
|
||||
return {};
|
||||
}
|
||||
return local.jsValueToZig(T, .{ .local = local, .handle = handle });
|
||||
|
||||
if (@typeInfo(T) == .void) return {};
|
||||
return context.jsValueToZig(T, result.?);
|
||||
}
|
||||
|
||||
fn getThis(self: *const Function) js.Object {
|
||||
const handle = if (self.this) |t| t else v8.v8__Context__Global(self.local.handle).?;
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = handle,
|
||||
};
|
||||
fn getThis(self: *const Function) v8.Object {
|
||||
return self.this orelse self.context.v8_context.getGlobal();
|
||||
}
|
||||
|
||||
pub fn src(self: *const Function) ![]const u8 {
|
||||
return self.local.valueToString(.{ .local = self.local, .handle = @ptrCast(self.handle) }, .{});
|
||||
const value = self.func.castToFunction().toValue();
|
||||
return self.context.valueToString(value, .{});
|
||||
}
|
||||
|
||||
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
|
||||
const local = self.local;
|
||||
const key = local.isolate.initStringHandle(name);
|
||||
const handle = v8.v8__Object__Get(self.handle, self.local.handle, key) orelse {
|
||||
return error.JsException;
|
||||
};
|
||||
|
||||
return .{
|
||||
.local = local,
|
||||
.handle = handle,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn persist(self: *const Function) !Global {
|
||||
return self._persist(true);
|
||||
}
|
||||
|
||||
pub fn temp(self: *const Function) !Temp {
|
||||
return self._persist(false);
|
||||
}
|
||||
|
||||
fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||
var ctx = self.local.ctx;
|
||||
|
||||
var global: v8.Global = undefined;
|
||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||
if (comptime is_global) {
|
||||
try ctx.global_functions.append(ctx.arena, global);
|
||||
} else {
|
||||
try ctx.global_functions_temp.put(ctx.arena, global.data_ptr, global);
|
||||
}
|
||||
return .{ .handle = global };
|
||||
}
|
||||
|
||||
pub fn tempWithThis(self: *const Function, value: anytype) !Temp {
|
||||
const with_this = try self.withThis(value);
|
||||
return with_this.temp();
|
||||
}
|
||||
|
||||
pub fn persistWithThis(self: *const Function, value: anytype) !Global {
|
||||
const with_this = try self.withThis(value);
|
||||
return with_this.persist();
|
||||
}
|
||||
|
||||
pub const Temp = G(0);
|
||||
pub const Global = G(1);
|
||||
|
||||
fn G(comptime discriminator: u8) type {
|
||||
return struct {
|
||||
handle: v8.Global,
|
||||
|
||||
// makes the types different (G(0) != G(1)), without taking up space
|
||||
comptime _: u8 = discriminator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
v8.v8__Global__Reset(&self.handle);
|
||||
}
|
||||
|
||||
pub fn local(self: *const Self, l: *const js.Local) Function {
|
||||
return .{
|
||||
.local = l,
|
||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isEqual(self: *const Self, other: Function) bool {
|
||||
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||
}
|
||||
};
|
||||
const func_obj = self.func.castToFunction().toObject();
|
||||
const key = v8.String.initUtf8(self.context.isolate, name);
|
||||
const value = func_obj.getValue(self.context.v8_context, key) catch return null;
|
||||
return self.context.createValue(value);
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const HandleScope = @This();
|
||||
|
||||
handle: v8.HandleScope,
|
||||
|
||||
// V8 takes an address of the value that's passed in, so it needs to be stable.
|
||||
// We can't create the v8.HandleScope here, pass it to v8 and then return the
|
||||
// value, as v8 will then have taken the address of the function-scopped (and no
|
||||
// longer valid) local.
|
||||
pub fn init(self: *HandleScope, isolate: js.Isolate) void {
|
||||
v8.v8__HandleScope__CONSTRUCT(&self.handle, isolate.handle);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *HandleScope) void {
|
||||
v8.v8__HandleScope__DESTRUCT(&self.handle);
|
||||
}
|
||||
@@ -20,79 +20,63 @@ const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const TaggedOpaque = @import("TaggedOpaque.zig");
|
||||
const Context = @import("Context.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const CONTEXT_GROUP_ID = 1;
|
||||
const CLIENT_TRUST_LEVEL = 1;
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
// Inspector exists for the lifetime of the Isolate/Env. 1 Isolate = 1 Inspector.
|
||||
// It combines the v8.Inspector and the v8.InspectorClientImpl. The v8.InspectorClientImpl
|
||||
// is our own implementation that fulfills the InspectorClient API, i.e. it's the
|
||||
// mechanism v8 provides to let us tweak how the inspector works. For example, it
|
||||
// Below, you'll find a few pub export fn v8_inspector__Client__IMPL__XYZ functions
|
||||
// which is our implementation of what the v8::Inspector requires of our Client
|
||||
// (not much at all)
|
||||
const Inspector = @This();
|
||||
|
||||
unique_id: i64,
|
||||
isolate: *v8.Isolate,
|
||||
handle: *v8.Inspector,
|
||||
client: *v8.InspectorClientImpl,
|
||||
default_context: ?v8.Global,
|
||||
session: ?Session,
|
||||
pub const RemoteObject = v8.RemoteObject;
|
||||
|
||||
pub fn init(allocator: Allocator, isolate: *v8.Isolate) !*Inspector {
|
||||
const self = try allocator.create(Inspector);
|
||||
errdefer allocator.destroy(self);
|
||||
isolate: v8.Isolate,
|
||||
inner: *v8.Inspector,
|
||||
session: v8.InspectorSession,
|
||||
|
||||
self.* = .{
|
||||
.unique_id = 1,
|
||||
.session = null,
|
||||
.isolate = isolate,
|
||||
.client = undefined,
|
||||
.handle = undefined,
|
||||
.default_context = null,
|
||||
// We expect allocator to be an arena
|
||||
pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector {
|
||||
const ContextT = @TypeOf(ctx);
|
||||
|
||||
const InspectorContainer = switch (@typeInfo(ContextT)) {
|
||||
.@"struct" => ContextT,
|
||||
.pointer => |ptr| ptr.child,
|
||||
.void => NoopInspector,
|
||||
else => @compileError("invalid context type"),
|
||||
};
|
||||
|
||||
self.client = v8.v8_inspector__Client__IMPL__CREATE();
|
||||
errdefer v8.v8_inspector__Client__IMPL__DELETE(self.client);
|
||||
v8.v8_inspector__Client__IMPL__SET_DATA(self.client, self);
|
||||
// If necessary, turn a void context into something we can safely ptrCast
|
||||
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
|
||||
|
||||
self.handle = v8.v8_inspector__Inspector__Create(isolate, self.client).?;
|
||||
errdefer v8.v8_inspector__Inspector__DELETE(self.handle);
|
||||
const channel = v8.InspectorChannel.init(
|
||||
safe_context,
|
||||
InspectorContainer.onInspectorResponse,
|
||||
InspectorContainer.onInspectorEvent,
|
||||
InspectorContainer.onRunMessageLoopOnPause,
|
||||
InspectorContainer.onQuitMessageLoopOnPause,
|
||||
isolate,
|
||||
);
|
||||
|
||||
return self;
|
||||
const client = v8.InspectorClient.init();
|
||||
|
||||
const inner = try allocator.create(v8.Inspector);
|
||||
v8.Inspector.init(inner, client, channel, isolate);
|
||||
return .{ .inner = inner, .isolate = isolate, .session = inner.connect() };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Inspector, allocator: Allocator) void {
|
||||
var hs: v8.HandleScope = undefined;
|
||||
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
|
||||
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||
|
||||
if (self.session) |*s| {
|
||||
s.deinit();
|
||||
}
|
||||
v8.v8_inspector__Client__IMPL__DELETE(self.client);
|
||||
v8.v8_inspector__Inspector__DELETE(self.handle);
|
||||
allocator.destroy(self);
|
||||
pub fn deinit(self: *const Inspector) void {
|
||||
self.session.deinit();
|
||||
self.inner.deinit();
|
||||
}
|
||||
|
||||
pub fn startSession(self: *Inspector, ctx: anytype) *Session {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.session == null);
|
||||
}
|
||||
pub fn send(self: *const Inspector, msg: []const u8) void {
|
||||
// Can't assume the main Context exists (with its HandleScope)
|
||||
// available when doing this. Pages (and thus the HandleScope)
|
||||
// comes and goes, but CDP can keep sending messages.
|
||||
const isolate = self.isolate;
|
||||
var temp_scope: v8.HandleScope = undefined;
|
||||
v8.HandleScope.init(&temp_scope, isolate);
|
||||
defer temp_scope.deinit();
|
||||
|
||||
self.session = @as(Session, undefined);
|
||||
Session.init(&self.session.?, self, ctx);
|
||||
return &self.session.?;
|
||||
}
|
||||
|
||||
pub fn stopSession(self: *Inspector) void {
|
||||
self.session.?.deinit();
|
||||
self.session = null;
|
||||
self.session.dispatchProtocolMessage(isolate, msg);
|
||||
}
|
||||
|
||||
// From CDP docs
|
||||
@@ -104,356 +88,75 @@ pub fn stopSession(self: *Inspector) void {
|
||||
// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
|
||||
// - is_default_context: Whether the execution context is default, should match the auxData
|
||||
pub fn contextCreated(
|
||||
self: *Inspector,
|
||||
local: *const js.Local,
|
||||
self: *const Inspector,
|
||||
context: *const Context,
|
||||
name: []const u8,
|
||||
origin: []const u8,
|
||||
aux_data: []const u8,
|
||||
aux_data: ?[]const u8,
|
||||
is_default_context: bool,
|
||||
) void {
|
||||
v8.v8_inspector__Inspector__ContextCreated(
|
||||
self.handle,
|
||||
name.ptr,
|
||||
name.len,
|
||||
origin.ptr,
|
||||
origin.len,
|
||||
aux_data.ptr,
|
||||
aux_data.len,
|
||||
CONTEXT_GROUP_ID,
|
||||
local.handle,
|
||||
self.inner.contextCreated(context.v8_context, name, origin, aux_data, is_default_context);
|
||||
}
|
||||
|
||||
// Retrieves the RemoteObject for a given value.
|
||||
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
|
||||
// just like a method return value. Therefore, if we've mapped this
|
||||
// value before, we'll get the existing JS PersistedObject and if not
|
||||
// we'll create it and track it for cleanup when the context ends.
|
||||
pub fn getRemoteObject(
|
||||
self: *const Inspector,
|
||||
context: *Context,
|
||||
group: []const u8,
|
||||
value: anytype,
|
||||
) !RemoteObject {
|
||||
const js_value = try context.zigValueToJs(value, .{});
|
||||
|
||||
// We do not want to expose this as a parameter for now
|
||||
const generate_preview = false;
|
||||
return self.session.wrapObject(
|
||||
context.isolate,
|
||||
context.v8_context,
|
||||
js_value,
|
||||
group,
|
||||
generate_preview,
|
||||
);
|
||||
|
||||
if (is_default_context) {
|
||||
self.default_context = local.ctx.handle;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contextDestroyed(self: *Inspector, context: *const v8.Context) void {
|
||||
v8.v8_inspector__Inspector__ContextDestroyed(self.handle, context);
|
||||
// Gets a value by object ID regardless of which context it is in.
|
||||
// Our TaggedAnyOpaque stores the "resolved" ptr value (the most specific _type,
|
||||
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
|
||||
// the pointer to the Node, so we need to use the same resolution mechanism which
|
||||
// is used when we're calling a function to turn the Div into a Node, which is
|
||||
// what Context.typeTaggedAnyOpaque does.
|
||||
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !*anyopaque {
|
||||
const unwrapped = try self.session.unwrapObject(allocator, object_id);
|
||||
// The values context and groupId are not used here
|
||||
const js_val = unwrapped.value;
|
||||
if (js_val.isObject() == false) {
|
||||
return error.ObjectIdIsNotANode;
|
||||
}
|
||||
const Node = @import("../webapi/Node.zig");
|
||||
return Context.typeTaggedAnyOpaque(*Node, js_val.castTo(v8.Object)) catch {
|
||||
return error.ObjectIdIsNotANode;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn resetContextGroup(self: *const Inspector) void {
|
||||
var hs: v8.HandleScope = undefined;
|
||||
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
|
||||
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||
|
||||
v8.v8_inspector__Inspector__ResetContextGroup(self.handle, CONTEXT_GROUP_ID);
|
||||
}
|
||||
|
||||
pub const RemoteObject = struct {
|
||||
handle: *v8.RemoteObject,
|
||||
|
||||
pub fn deinit(self: RemoteObject) void {
|
||||
v8.v8_inspector__RemoteObject__DELETE(self.handle);
|
||||
}
|
||||
|
||||
pub fn getType(self: RemoteObject, allocator: Allocator) ![]const u8 {
|
||||
var ctype_: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||
if (!v8.v8_inspector__RemoteObject__getType(self.handle, &allocator, &ctype_)) return error.V8AllocFailed;
|
||||
return cZigStringToString(ctype_) orelse return error.InvalidType;
|
||||
}
|
||||
|
||||
pub fn getSubtype(self: RemoteObject, allocator: Allocator) !?[]const u8 {
|
||||
if (!v8.v8_inspector__RemoteObject__hasSubtype(self.handle)) return null;
|
||||
|
||||
var csubtype: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||
if (!v8.v8_inspector__RemoteObject__getSubtype(self.handle, &allocator, &csubtype)) return error.V8AllocFailed;
|
||||
return cZigStringToString(csubtype);
|
||||
}
|
||||
|
||||
pub fn getClassName(self: RemoteObject, allocator: Allocator) !?[]const u8 {
|
||||
if (!v8.v8_inspector__RemoteObject__hasClassName(self.handle)) return null;
|
||||
|
||||
var cclass_name: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||
if (!v8.v8_inspector__RemoteObject__getClassName(self.handle, &allocator, &cclass_name)) return error.V8AllocFailed;
|
||||
return cZigStringToString(cclass_name);
|
||||
}
|
||||
|
||||
pub fn getDescription(self: RemoteObject, allocator: Allocator) !?[]const u8 {
|
||||
if (!v8.v8_inspector__RemoteObject__hasDescription(self.handle)) return null;
|
||||
|
||||
var description: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||
if (!v8.v8_inspector__RemoteObject__getDescription(self.handle, &allocator, &description)) return error.V8AllocFailed;
|
||||
return cZigStringToString(description);
|
||||
}
|
||||
|
||||
pub fn getObjectId(self: RemoteObject, allocator: Allocator) !?[]const u8 {
|
||||
if (!v8.v8_inspector__RemoteObject__hasObjectId(self.handle)) return null;
|
||||
|
||||
var cobject_id: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||
if (!v8.v8_inspector__RemoteObject__getObjectId(self.handle, &allocator, &cobject_id)) return error.V8AllocFailed;
|
||||
return cZigStringToString(cobject_id);
|
||||
}
|
||||
const NoopInspector = struct {
|
||||
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
||||
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
|
||||
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {}
|
||||
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
|
||||
};
|
||||
|
||||
// Combines a v8::InspectorSession and a v8::InspectorChannelImpl. The
|
||||
// InspectorSession is for zig -> v8 (sending messages to the inspector). The
|
||||
// Channel is for v8 -> zig, getting events from the Inspector (that we'll pass
|
||||
// back ot some opaque context, i.e the CDP BrowserContext).
|
||||
// The channel callbacks are defined below, as:
|
||||
// pub export fn v8_inspector__Channel__IMPL__XYZ
|
||||
pub const Session = struct {
|
||||
inspector: *Inspector,
|
||||
handle: *v8.InspectorSession,
|
||||
channel: *v8.InspectorChannelImpl,
|
||||
|
||||
// callbacks
|
||||
ctx: *anyopaque,
|
||||
onNotif: *const fn (ctx: *anyopaque, msg: []const u8) void,
|
||||
onResp: *const fn (ctx: *anyopaque, call_id: u32, msg: []const u8) void,
|
||||
|
||||
fn init(self: *Session, inspector: *Inspector, ctx: anytype) void {
|
||||
const Container = @typeInfo(@TypeOf(ctx)).pointer.child;
|
||||
|
||||
const channel = v8.v8_inspector__Channel__IMPL__CREATE(inspector.isolate);
|
||||
const handle = v8.v8_inspector__Inspector__Connect(
|
||||
inspector.handle,
|
||||
CONTEXT_GROUP_ID,
|
||||
channel,
|
||||
CLIENT_TRUST_LEVEL,
|
||||
).?;
|
||||
v8.v8_inspector__Channel__IMPL__SET_DATA(channel, self);
|
||||
|
||||
self.* = .{
|
||||
.ctx = ctx,
|
||||
.handle = handle,
|
||||
.channel = channel,
|
||||
.inspector = inspector,
|
||||
.onResp = Container.onInspectorResponse,
|
||||
.onNotif = Container.onInspectorEvent,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *const Session) void {
|
||||
v8.v8_inspector__Session__DELETE(self.handle);
|
||||
v8.v8_inspector__Channel__IMPL__DELETE(self.channel);
|
||||
}
|
||||
|
||||
pub fn send(self: *const Session, msg: []const u8) void {
|
||||
const isolate = self.inspector.isolate;
|
||||
var hs: v8.HandleScope = undefined;
|
||||
v8.v8__HandleScope__CONSTRUCT(&hs, isolate);
|
||||
defer v8.v8__HandleScope__DESTRUCT(&hs);
|
||||
|
||||
v8.v8_inspector__Session__dispatchProtocolMessage(
|
||||
self.handle,
|
||||
isolate,
|
||||
msg.ptr,
|
||||
msg.len,
|
||||
);
|
||||
|
||||
v8.v8__Isolate__PerformMicrotaskCheckpoint(isolate);
|
||||
}
|
||||
|
||||
// Gets a value by object ID regardless of which context it is in.
|
||||
// Our TaggedOpaque stores the "resolved" ptr value (the most specific _type,
|
||||
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
|
||||
// the pointer to the Node, so we need to use the same resolution mechanism which
|
||||
// is used when we're calling a function to turn the Div into a Node, which is
|
||||
// what TaggedOpaque.fromJS does.
|
||||
pub fn getNodePtr(self: *const Session, allocator: Allocator, object_id: []const u8, local: *js.Local) !*anyopaque {
|
||||
// just to indicate that the caller is responsible for ensuring there's a local environment
|
||||
_ = local;
|
||||
|
||||
const unwrapped = try self.unwrapObject(allocator, object_id);
|
||||
// The values context and groupId are not used here
|
||||
const js_val = unwrapped.value;
|
||||
if (!v8.v8__Value__IsObject(js_val)) {
|
||||
return error.ObjectIdIsNotANode;
|
||||
}
|
||||
|
||||
const Node = @import("../webapi/Node.zig");
|
||||
// Cast to *const v8.Object for typeTaggedAnyOpaque
|
||||
return TaggedOpaque.fromJS(*Node, @ptrCast(js_val)) catch return error.ObjectIdIsNotANode;
|
||||
}
|
||||
|
||||
// Retrieves the RemoteObject for a given value.
|
||||
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
|
||||
// just like a method return value. Therefore, if we've mapped this
|
||||
// value before, we'll get the existing js.Global(js.Object) and if not
|
||||
// we'll create it and track it for cleanup when the context ends.
|
||||
pub fn getRemoteObject(
|
||||
self: *const Session,
|
||||
local: *const js.Local,
|
||||
group: []const u8,
|
||||
value: anytype,
|
||||
) !RemoteObject {
|
||||
const js_val = try local.zigValueToJs(value, .{});
|
||||
|
||||
// We do not want to expose this as a parameter for now
|
||||
const generate_preview = false;
|
||||
return self.wrapObject(
|
||||
local.isolate.handle,
|
||||
local.handle,
|
||||
js_val.handle,
|
||||
group,
|
||||
generate_preview,
|
||||
);
|
||||
}
|
||||
|
||||
fn wrapObject(
|
||||
self: Session,
|
||||
isolate: *v8.Isolate,
|
||||
ctx: *const v8.Context,
|
||||
val: *const v8.Value,
|
||||
grpname: []const u8,
|
||||
generatepreview: bool,
|
||||
) !RemoteObject {
|
||||
const remote_object = v8.v8_inspector__Session__wrapObject(
|
||||
self.handle,
|
||||
isolate,
|
||||
ctx,
|
||||
val,
|
||||
grpname.ptr,
|
||||
grpname.len,
|
||||
generatepreview,
|
||||
).?;
|
||||
return .{ .handle = remote_object };
|
||||
}
|
||||
|
||||
fn unwrapObject(
|
||||
self: Session,
|
||||
allocator: Allocator,
|
||||
object_id: []const u8,
|
||||
) !UnwrappedObject {
|
||||
const in_object_id = v8.CZigString{
|
||||
.ptr = object_id.ptr,
|
||||
.len = object_id.len,
|
||||
};
|
||||
var out_error: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||
var out_value_handle: ?*v8.Value = null;
|
||||
var out_context_handle: ?*v8.Context = null;
|
||||
var out_object_group: v8.CZigString = .{ .ptr = null, .len = 0 };
|
||||
|
||||
const result = v8.v8_inspector__Session__unwrapObject(
|
||||
self.handle,
|
||||
&allocator,
|
||||
&out_error,
|
||||
in_object_id,
|
||||
&out_value_handle,
|
||||
&out_context_handle,
|
||||
&out_object_group,
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
const error_str = cZigStringToString(out_error) orelse return error.UnwrapFailed;
|
||||
std.log.err("unwrapObject failed: {s}", .{error_str});
|
||||
return error.UnwrapFailed;
|
||||
}
|
||||
|
||||
return .{
|
||||
.value = out_value_handle.?,
|
||||
.context = out_context_handle.?,
|
||||
.object_group = cZigStringToString(out_object_group),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const UnwrappedObject = struct {
|
||||
value: *const v8.Value,
|
||||
context: *const v8.Context,
|
||||
object_group: ?[]const u8,
|
||||
};
|
||||
|
||||
pub fn getTaggedOpaque(value: *const v8.Value) ?*TaggedOpaque {
|
||||
if (!v8.v8__Value__IsObject(value)) {
|
||||
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
|
||||
if (value.isObject() == false) {
|
||||
return null;
|
||||
}
|
||||
const internal_field_count = v8.v8__Object__InternalFieldCount(value);
|
||||
if (internal_field_count == 0) {
|
||||
const obj = value.castTo(v8.Object);
|
||||
if (obj.internalFieldCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const external_value = v8.v8__Object__GetInternalField(value, 0).?;
|
||||
const external_data = v8.v8__External__Value(external_value).?;
|
||||
const external_data = obj.getInternalField(0).castTo(v8.External).get().?;
|
||||
return @ptrCast(@alignCast(external_data));
|
||||
}
|
||||
|
||||
fn cZigStringToString(s: v8.CZigString) ?[]const u8 {
|
||||
if (s.ptr == null) return null;
|
||||
return s.ptr[0..s.len];
|
||||
}
|
||||
|
||||
// C export functions for Inspector callbacks
|
||||
pub export fn v8_inspector__Client__IMPL__generateUniqueId(
|
||||
_: *v8.InspectorClientImpl,
|
||||
data: *anyopaque,
|
||||
) callconv(.c) i64 {
|
||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
||||
const unique_id = inspector.unique_id + 1;
|
||||
inspector.unique_id = unique_id;
|
||||
return unique_id;
|
||||
}
|
||||
|
||||
pub export fn v8_inspector__Client__IMPL__runMessageLoopOnPause(
|
||||
_: *v8.InspectorClientImpl,
|
||||
data: *anyopaque,
|
||||
context_group_id: c_int,
|
||||
) callconv(.c) void {
|
||||
_ = data;
|
||||
_ = context_group_id;
|
||||
}
|
||||
|
||||
pub export fn v8_inspector__Client__IMPL__quitMessageLoopOnPause(
|
||||
_: *v8.InspectorClientImpl,
|
||||
data: *anyopaque,
|
||||
) callconv(.c) void {
|
||||
_ = data;
|
||||
}
|
||||
|
||||
pub export fn v8_inspector__Client__IMPL__runIfWaitingForDebugger(
|
||||
_: *v8.InspectorClientImpl,
|
||||
_: *anyopaque,
|
||||
_: c_int,
|
||||
) callconv(.c) void {
|
||||
// TODO
|
||||
}
|
||||
|
||||
pub export fn v8_inspector__Client__IMPL__consoleAPIMessage(
|
||||
_: *v8.InspectorClientImpl,
|
||||
_: *anyopaque,
|
||||
_: c_int,
|
||||
_: v8.MessageErrorLevel,
|
||||
_: *v8.StringView,
|
||||
_: *v8.StringView,
|
||||
_: c_uint,
|
||||
_: c_uint,
|
||||
_: *v8.StackTrace,
|
||||
) callconv(.c) void {}
|
||||
|
||||
pub export fn v8_inspector__Client__IMPL__ensureDefaultContextInGroup(
|
||||
_: *v8.InspectorClientImpl,
|
||||
data: *anyopaque,
|
||||
) callconv(.c) ?*const v8.Context {
|
||||
const inspector: *Inspector = @ptrCast(@alignCast(data));
|
||||
const global_handle = inspector.default_context orelse return null;
|
||||
return v8.v8__Global__Get(&global_handle, inspector.isolate);
|
||||
}
|
||||
|
||||
pub export fn v8_inspector__Channel__IMPL__sendResponse(
|
||||
_: *v8.InspectorChannelImpl,
|
||||
data: *anyopaque,
|
||||
call_id: c_int,
|
||||
msg: [*c]u8,
|
||||
length: usize,
|
||||
) callconv(.c) void {
|
||||
const session: *Session = @ptrCast(@alignCast(data));
|
||||
session.onResp(session.ctx, @intCast(call_id), msg[0..length]);
|
||||
}
|
||||
|
||||
pub export fn v8_inspector__Channel__IMPL__sendNotification(
|
||||
_: *v8.InspectorChannelImpl,
|
||||
data: *anyopaque,
|
||||
msg: [*c]u8,
|
||||
length: usize,
|
||||
) callconv(.c) void {
|
||||
const session: *Session = @ptrCast(@alignCast(data));
|
||||
session.onNotif(session.ctx, msg[0..length]);
|
||||
}
|
||||
|
||||
pub export fn v8_inspector__Channel__IMPL__flushProtocolNotifications(
|
||||
_: *v8.InspectorChannelImpl,
|
||||
_: *anyopaque,
|
||||
) callconv(.c) void {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
|
||||
const v8 = js.v8;
|
||||
|
||||
const Integer = @This();
|
||||
|
||||
handle: *const v8.Integer,
|
||||
|
||||
pub fn init(isolate: *v8.Isolate, value: anytype) Integer {
|
||||
const handle = switch (@TypeOf(value)) {
|
||||
i8, i16, i32 => v8.v8__Integer__New(isolate, value).?,
|
||||
u8, u16, u32 => v8.v8__Integer__NewFromUnsigned(isolate, value).?,
|
||||
else => |T| @compileError("cannot create v8::Integer from: " ++ @typeName(T)),
|
||||
};
|
||||
return .{ .handle = handle };
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Isolate = @This();
|
||||
|
||||
handle: *v8.Isolate,
|
||||
|
||||
pub fn init(params: *v8.CreateParams) Isolate {
|
||||
return .{
|
||||
.handle = v8.v8__Isolate__New(params).?,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Isolate) void {
|
||||
v8.v8__Isolate__Dispose(self.handle);
|
||||
}
|
||||
|
||||
pub fn enter(self: Isolate) void {
|
||||
v8.v8__Isolate__Enter(self.handle);
|
||||
}
|
||||
|
||||
pub fn exit(self: Isolate) void {
|
||||
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 {
|
||||
v8.v8__Isolate__LowMemoryNotification(self.handle);
|
||||
}
|
||||
|
||||
pub const MemoryPressureLevel = enum(u32) {
|
||||
none = v8.kNone,
|
||||
moderate = v8.kModerate,
|
||||
critical = v8.kCritical,
|
||||
};
|
||||
|
||||
pub fn memoryPressureNotification(self: Isolate, level: MemoryPressureLevel) void {
|
||||
v8.v8__Isolate__MemoryPressureNotification(self.handle, @intFromEnum(level));
|
||||
}
|
||||
|
||||
pub fn notifyContextDisposed(self: Isolate) void {
|
||||
_ = v8.v8__Isolate__ContextDisposedNotification(self.handle);
|
||||
}
|
||||
|
||||
pub fn getHeapStatistics(self: Isolate) v8.HeapStatistics {
|
||||
var res: v8.HeapStatistics = undefined;
|
||||
v8.v8__Isolate__GetHeapStatistics(self.handle, &res);
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn throwException(self: Isolate, value: *const v8.Value) *const v8.Value {
|
||||
return v8.v8__Isolate__ThrowException(self.handle, value).?;
|
||||
}
|
||||
|
||||
pub fn initStringHandle(self: Isolate, str: []const u8) *const v8.String {
|
||||
return v8.v8__String__NewFromUtf8(self.handle, str.ptr, v8.kNormal, @as(c_int, @intCast(str.len))).?;
|
||||
}
|
||||
|
||||
pub fn createError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||
const message = self.initStringHandle(msg);
|
||||
return v8.v8__Exception__Error(message).?;
|
||||
}
|
||||
|
||||
pub fn createTypeError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||
const message = self.initStringHandle(msg);
|
||||
return v8.v8__Exception__TypeError(message).?;
|
||||
}
|
||||
|
||||
pub fn initNull(self: Isolate) *const v8.Value {
|
||||
return v8.v8__Null(self.handle).?;
|
||||
}
|
||||
|
||||
pub fn initUndefined(self: Isolate) *const v8.Value {
|
||||
return v8.v8__Undefined(self.handle).?;
|
||||
}
|
||||
|
||||
pub fn initFalse(self: Isolate) *const v8.Value {
|
||||
return v8.v8__False(self.handle).?;
|
||||
}
|
||||
|
||||
pub fn initTrue(self: Isolate) *const v8.Value {
|
||||
return v8.v8__True(self.handle).?;
|
||||
}
|
||||
|
||||
pub fn initInteger(self: Isolate, val: anytype) js.Integer {
|
||||
return js.Integer.init(self.handle, val);
|
||||
}
|
||||
|
||||
pub fn initBigInt(self: Isolate, val: anytype) js.BigInt {
|
||||
return js.BigInt.init(self.handle, val);
|
||||
}
|
||||
|
||||
pub fn initNumber(self: Isolate, val: anytype) js.Number {
|
||||
return js.Number.init(self.handle, val);
|
||||
}
|
||||
|
||||
pub fn createExternal(self: Isolate, val: *anyopaque) *const v8.External {
|
||||
return v8.v8__External__New(self.handle, val).?;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,137 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Module = @This();
|
||||
|
||||
local: *const js.Local,
|
||||
handle: *const v8.Module,
|
||||
|
||||
pub const Status = enum(u32) {
|
||||
kUninstantiated = v8.kUninstantiated,
|
||||
kInstantiating = v8.kInstantiating,
|
||||
kInstantiated = v8.kInstantiated,
|
||||
kEvaluating = v8.kEvaluating,
|
||||
kEvaluated = v8.kEvaluated,
|
||||
kErrored = v8.kErrored,
|
||||
};
|
||||
|
||||
pub fn getStatus(self: Module) Status {
|
||||
return @enumFromInt(v8.v8__Module__GetStatus(self.handle));
|
||||
}
|
||||
|
||||
pub fn getException(self: Module) js.Value {
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = v8.v8__Module__GetException(self.handle).?,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getModuleRequests(self: Module) Requests {
|
||||
return .{
|
||||
.context_handle = self.local.handle,
|
||||
.handle = v8.v8__Module__GetModuleRequests(self.handle).?,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool {
|
||||
var out: v8.MaybeBool = undefined;
|
||||
v8.v8__Module__InstantiateModule(self.handle, self.local.handle, cb, &out);
|
||||
if (out.has_value) {
|
||||
return out.value;
|
||||
}
|
||||
return error.JsException;
|
||||
}
|
||||
|
||||
pub fn evaluate(self: Module) !js.Value {
|
||||
const res = v8.v8__Module__Evaluate(self.handle, self.local.handle) orelse return error.JsException;
|
||||
|
||||
if (self.getStatus() == .kErrored) {
|
||||
return error.JsException;
|
||||
}
|
||||
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = res,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getIdentityHash(self: Module) u32 {
|
||||
return @bitCast(v8.v8__Module__GetIdentityHash(self.handle));
|
||||
}
|
||||
|
||||
pub fn getModuleNamespace(self: Module) js.Value {
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = v8.v8__Module__GetModuleNamespace(self.handle).?,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getScriptId(self: Module) u32 {
|
||||
return @intCast(v8.v8__Module__ScriptId(self.handle));
|
||||
}
|
||||
|
||||
pub fn persist(self: Module) !Global {
|
||||
var ctx = self.local.ctx;
|
||||
var global: v8.Global = undefined;
|
||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||
try ctx.global_modules.append(ctx.arena, global);
|
||||
return .{ .handle = global };
|
||||
}
|
||||
|
||||
pub const Global = struct {
|
||||
handle: v8.Global,
|
||||
|
||||
pub fn deinit(self: *Global) void {
|
||||
v8.v8__Global__Reset(&self.handle);
|
||||
}
|
||||
|
||||
pub fn local(self: *const Global, l: *const js.Local) Module {
|
||||
return .{
|
||||
.local = l,
|
||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isEqual(self: *const Global, other: Module) bool {
|
||||
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||
}
|
||||
};
|
||||
|
||||
const Requests = struct {
|
||||
handle: *const v8.FixedArray,
|
||||
context_handle: *const v8.Context,
|
||||
|
||||
pub fn len(self: Requests) usize {
|
||||
return @intCast(v8.v8__FixedArray__Length(self.handle));
|
||||
}
|
||||
|
||||
pub fn get(self: Requests, idx: usize) Request {
|
||||
return .{ .handle = v8.v8__FixedArray__Get(self.handle, self.context_handle, @intCast(idx)).? };
|
||||
}
|
||||
};
|
||||
|
||||
const Request = struct {
|
||||
handle: *const v8.ModuleRequest,
|
||||
|
||||
pub fn specifier(self: Request, local: *const js.Local) js.String {
|
||||
return .{ .local = local, .handle = v8.v8__ModuleRequest__GetSpecifier(self.handle).? };
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -22,102 +22,103 @@ const v8 = js.v8;
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Context = @import("Context.zig");
|
||||
const PersistentObject = v8.Persistent(v8.Object);
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Object = @This();
|
||||
js_obj: v8.Object,
|
||||
context: *js.Context,
|
||||
|
||||
local: *const js.Local,
|
||||
handle: *const v8.Object,
|
||||
|
||||
pub fn has(self: Object, key: anytype) bool {
|
||||
const ctx = self.local.ctx;
|
||||
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
||||
|
||||
var out: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__Has(self.handle, self.local.handle, key_handle, &out);
|
||||
if (out.has_value) {
|
||||
return out.value;
|
||||
}
|
||||
return false;
|
||||
pub fn getId(self: Object) u32 {
|
||||
return self.js_obj.getIdentityHash();
|
||||
}
|
||||
|
||||
pub fn get(self: Object, key: anytype) !js.Value {
|
||||
const ctx = self.local.ctx;
|
||||
|
||||
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
||||
const js_val_handle = v8.v8__Object__Get(self.handle, self.local.handle, key_handle) orelse return error.JsException;
|
||||
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = js_val_handle,
|
||||
pub const SetOpts = packed struct(u32) {
|
||||
READ_ONLY: bool = false,
|
||||
DONT_ENUM: bool = false,
|
||||
DONT_DELETE: bool = false,
|
||||
_: u29 = 0,
|
||||
};
|
||||
pub fn setIndex(self: Object, index: u32, value: anytype, opts: SetOpts) !void {
|
||||
@setEvalBranchQuota(10000);
|
||||
const key = switch (index) {
|
||||
inline 0...20 => |i| std.fmt.comptimePrint("{d}", .{i}),
|
||||
else => try std.fmt.allocPrint(self.context.arena, "{d}", .{index}),
|
||||
};
|
||||
return self.set(key, value, opts);
|
||||
}
|
||||
|
||||
pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
|
||||
const ctx = self.local.ctx;
|
||||
pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{ FailedToSet, OutOfMemory }!void {
|
||||
const context = self.context;
|
||||
|
||||
const js_value = try self.local.zigValueToJs(value, opts);
|
||||
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
|
||||
const js_key = v8.String.initUtf8(context.isolate, key);
|
||||
const js_value = try context.zigValueToJs(value);
|
||||
|
||||
var out: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__Set(self.handle, self.local.handle, key_handle, js_value.handle, &out);
|
||||
return out.has_value;
|
||||
}
|
||||
|
||||
pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr: v8.PropertyAttribute) ?bool {
|
||||
const ctx = self.local.ctx;
|
||||
const name_handle = ctx.isolate.initStringHandle(name);
|
||||
|
||||
var out: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__DefineOwnProperty(self.handle, self.local.handle, @ptrCast(name_handle), value.handle, attr, &out);
|
||||
|
||||
if (out.has_value) {
|
||||
return out.value;
|
||||
} else {
|
||||
return null;
|
||||
const res = self.js_obj.defineOwnProperty(context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false;
|
||||
if (!res) {
|
||||
return error.FailedToSet;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toValue(self: Object) js.Value {
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = @ptrCast(self.handle),
|
||||
};
|
||||
pub fn get(self: Object, key: []const u8) !js.Value {
|
||||
const context = self.context;
|
||||
const js_key = v8.String.initUtf8(context.isolate, key);
|
||||
const js_val = try self.js_obj.getValue(context.v8_context, js_key);
|
||||
return context.createValue(js_val);
|
||||
}
|
||||
|
||||
pub fn isTruthy(self: Object) bool {
|
||||
const js_value = self.js_obj.toValue();
|
||||
return js_value.toBool(self.context.isolate);
|
||||
}
|
||||
|
||||
pub fn toString(self: Object) ![]const u8 {
|
||||
const js_value = self.js_obj.toValue();
|
||||
return self.context.valueToString(js_value, .{});
|
||||
}
|
||||
|
||||
pub fn format(self: Object, writer: *std.Io.Writer) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
return self.local.ctx.debugValue(self.toValue(), writer);
|
||||
return self.context.debugValue(self.js_obj.toValue(), writer);
|
||||
}
|
||||
const str = self.toString() catch return error.WriteFailed;
|
||||
return writer.writeAll(str);
|
||||
}
|
||||
|
||||
pub fn persist(self: Object) !Global {
|
||||
var ctx = self.local.ctx;
|
||||
pub fn toJson(self: Object, allocator: Allocator) ![]u8 {
|
||||
const json_string = try v8.Json.stringify(self.context.v8_context, self.js_obj.toValue(), null);
|
||||
const str = try self.context.jsStringToZig(json_string, .{ .allocator = allocator });
|
||||
return str;
|
||||
}
|
||||
|
||||
var global: v8.Global = undefined;
|
||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||
pub fn persist(self: Object) !Object {
|
||||
var context = self.context;
|
||||
const js_obj = self.js_obj;
|
||||
|
||||
try ctx.global_objects.append(ctx.arena, global);
|
||||
const persisted = PersistentObject.init(context.isolate, js_obj);
|
||||
try context.js_object_list.append(context.arena, persisted);
|
||||
|
||||
return .{ .handle = global };
|
||||
return .{
|
||||
.context = context,
|
||||
.js_obj = persisted.castToObject(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
|
||||
if (self.isNullOrUndefined()) {
|
||||
return null;
|
||||
}
|
||||
const local = self.local;
|
||||
const context = self.context;
|
||||
|
||||
const js_name = local.isolate.initStringHandle(name);
|
||||
const js_val_handle = v8.v8__Object__Get(self.handle, local.handle, js_name) orelse return error.JsException;
|
||||
const js_name = v8.String.initUtf8(context.isolate, name);
|
||||
|
||||
if (v8.v8__Value__IsFunction(js_val_handle) == false) {
|
||||
const js_value = try self.js_obj.getValue(context.v8_context, js_name.toName());
|
||||
if (!js_value.isFunction()) {
|
||||
return null;
|
||||
}
|
||||
return .{
|
||||
.local = local,
|
||||
.handle = @ptrCast(js_val_handle),
|
||||
};
|
||||
return try context.createFunction(js_value);
|
||||
}
|
||||
|
||||
pub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args: anytype) !T {
|
||||
@@ -125,66 +126,41 @@ pub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args:
|
||||
return func.callWithThis(T, self, args);
|
||||
}
|
||||
|
||||
pub fn isNull(self: Object) bool {
|
||||
return self.js_obj.toValue().isNull();
|
||||
}
|
||||
|
||||
pub fn isUndefined(self: Object) bool {
|
||||
return self.js_obj.toValue().isUndefined();
|
||||
}
|
||||
|
||||
pub fn isNullOrUndefined(self: Object) bool {
|
||||
return v8.v8__Value__IsNullOrUndefined(@ptrCast(self.handle));
|
||||
}
|
||||
|
||||
pub fn getOwnPropertyNames(self: Object) js.Array {
|
||||
const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.local.handle).?;
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = handle,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getPropertyNames(self: Object) js.Array {
|
||||
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?;
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = handle,
|
||||
};
|
||||
return self.js_obj.toValue().isNullOrUndefined();
|
||||
}
|
||||
|
||||
pub fn nameIterator(self: Object) NameIterator {
|
||||
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?;
|
||||
const count = v8.v8__Array__Length(handle);
|
||||
const context = self.context;
|
||||
const js_obj = self.js_obj;
|
||||
|
||||
const array = js_obj.getPropertyNames(context.v8_context);
|
||||
const count = array.length();
|
||||
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = handle,
|
||||
.count = count,
|
||||
.context = context,
|
||||
.js_obj = array.castTo(v8.Object),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toZig(self: Object, comptime T: type) !T {
|
||||
const js_value = js.Value{ .local = self.local, .handle = @ptrCast(self.handle) };
|
||||
return self.local.jsValueToZig(T, js_value);
|
||||
return self.context.jsValueToZig(T, self.js_obj.toValue());
|
||||
}
|
||||
|
||||
pub const Global = struct {
|
||||
handle: v8.Global,
|
||||
|
||||
pub fn deinit(self: *Global) void {
|
||||
v8.v8__Global__Reset(&self.handle);
|
||||
}
|
||||
|
||||
pub fn local(self: *const Global, l: *const js.Local) Object {
|
||||
return .{
|
||||
.local = l,
|
||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isEqual(self: *const Global, other: Object) bool {
|
||||
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||
}
|
||||
};
|
||||
|
||||
pub const NameIterator = struct {
|
||||
count: u32,
|
||||
idx: u32 = 0,
|
||||
local: *const js.Local,
|
||||
handle: *const v8.Array,
|
||||
js_obj: v8.Object,
|
||||
context: *const Context,
|
||||
|
||||
pub fn next(self: *NameIterator) !?[]const u8 {
|
||||
const idx = self.idx;
|
||||
@@ -193,8 +169,8 @@ pub const NameIterator = struct {
|
||||
}
|
||||
self.idx += 1;
|
||||
|
||||
const local = self.local;
|
||||
const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), local.handle, idx) orelse return error.JsException;
|
||||
return try js.Value.toStringSlice(.{ .local = local, .handle = js_val_handle });
|
||||
const context = self.context;
|
||||
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx);
|
||||
return try context.valueToString(js_val, .{});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,22 +20,20 @@ const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Platform = @This();
|
||||
handle: *v8.Platform,
|
||||
inner: v8.Platform,
|
||||
|
||||
pub fn init() !Platform {
|
||||
if (v8.v8__V8__InitializeICU() == false) {
|
||||
if (v8.initV8ICU() == false) {
|
||||
return error.FailedToInitializeICU;
|
||||
}
|
||||
// 0 - threadpool size, 0 == let v8 decide
|
||||
// 1 - idle_task_support, 1 == enabled
|
||||
const handle = v8.v8__Platform__NewDefaultPlatform(0, 1).?;
|
||||
v8.v8__V8__InitializePlatform(handle);
|
||||
v8.v8__V8__Initialize();
|
||||
return .{ .handle = handle };
|
||||
const platform = v8.Platform.initDefault(0, true);
|
||||
v8.initV8Platform(platform);
|
||||
v8.initV8();
|
||||
return .{ .inner = platform };
|
||||
}
|
||||
|
||||
pub fn deinit(self: Platform) void {
|
||||
_ = v8.v8__V8__Dispose();
|
||||
v8.v8__V8__DisposePlatform();
|
||||
v8.v8__Platform__DELETE(self.handle);
|
||||
_ = v8.deinitV8();
|
||||
v8.deinitV8Platform();
|
||||
self.inner.deinit();
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Promise = @This();
|
||||
|
||||
local: *const js.Local,
|
||||
handle: *const v8.Promise,
|
||||
|
||||
pub fn toObject(self: Promise) js.Object {
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = @ptrCast(self.handle),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toValue(self: Promise) js.Value {
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = @ptrCast(self.handle),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn thenAndCatch(self: Promise, on_fulfilled: js.Function, on_rejected: js.Function) !Promise {
|
||||
if (v8.v8__Promise__Then2(self.handle, self.local.handle, on_fulfilled.handle, on_rejected.handle)) |handle| {
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = handle,
|
||||
};
|
||||
}
|
||||
return error.PromiseChainFailed;
|
||||
}
|
||||
|
||||
pub fn persist(self: Promise) !Global {
|
||||
return self._persist(true);
|
||||
}
|
||||
|
||||
pub fn temp(self: Promise) !Temp {
|
||||
return self._persist(false);
|
||||
}
|
||||
|
||||
fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||
var ctx = self.local.ctx;
|
||||
|
||||
var global: v8.Global = undefined;
|
||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||
if (comptime is_global) {
|
||||
try ctx.global_promises.append(ctx.arena, global);
|
||||
} else {
|
||||
try ctx.global_promises_temp.put(ctx.arena, global.data_ptr, global);
|
||||
}
|
||||
return .{ .handle = global };
|
||||
}
|
||||
|
||||
pub const Temp = G(0);
|
||||
pub const Global = G(1);
|
||||
|
||||
fn G(comptime discriminator: u8) type {
|
||||
return struct {
|
||||
handle: v8.Global,
|
||||
|
||||
// makes the types different (G(0) != G(1)), without taking up space
|
||||
comptime _: u8 = discriminator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
v8.v8__Global__Reset(&self.handle);
|
||||
}
|
||||
|
||||
pub fn local(self: *const Self, l: *const js.Local) Promise {
|
||||
return .{
|
||||
.local = l,
|
||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// 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 js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const PromiseRejection = @This();
|
||||
|
||||
local: *const js.Local,
|
||||
handle: *const v8.PromiseRejectMessage,
|
||||
|
||||
pub fn promise(self: PromiseRejection) js.Promise {
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = v8.v8__PromiseRejectMessage__GetPromise(self.handle).?,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reason(self: PromiseRejection) ?js.Value {
|
||||
const value_handle = v8.v8__PromiseRejectMessage__GetValue(self.handle) orelse return null;
|
||||
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = value_handle,
|
||||
};
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const PromiseResolver = @This();
|
||||
|
||||
local: *const js.Local,
|
||||
handle: *const v8.PromiseResolver,
|
||||
|
||||
pub fn init(local: *const js.Local) PromiseResolver {
|
||||
return .{
|
||||
.local = local,
|
||||
.handle = v8.v8__Promise__Resolver__New(local.handle).?,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn promise(self: PromiseResolver) js.Promise {
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = v8.v8__Promise__Resolver__GetPromise(self.handle).?,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||
self._resolve(value) catch |err| {
|
||||
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = false });
|
||||
};
|
||||
}
|
||||
|
||||
fn _resolve(self: PromiseResolver, value: anytype) !void {
|
||||
const local = self.local;
|
||||
const js_val = try local.zigValueToJs(value, .{});
|
||||
|
||||
var out: v8.MaybeBool = undefined;
|
||||
v8.v8__Promise__Resolver__Resolve(self.handle, self.local.handle, js_val.handle, &out);
|
||||
if (!out.has_value or !out.value) {
|
||||
return error.FailedToResolvePromise;
|
||||
}
|
||||
local.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||
self._reject(value) catch |err| {
|
||||
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = false });
|
||||
};
|
||||
}
|
||||
|
||||
fn _reject(self: PromiseResolver, value: anytype) !void {
|
||||
const local = self.local;
|
||||
const js_val = try local.zigValueToJs(value, .{});
|
||||
|
||||
var out: v8.MaybeBool = undefined;
|
||||
v8.v8__Promise__Resolver__Reject(self.handle, local.handle, js_val.handle, &out);
|
||||
if (!out.has_value or !out.value) {
|
||||
return error.FailedToRejectPromise;
|
||||
}
|
||||
local.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn persist(self: PromiseResolver) !Global {
|
||||
var ctx = self.local.ctx;
|
||||
var global: v8.Global = undefined;
|
||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||
try ctx.global_promise_resolvers.append(ctx.arena, global);
|
||||
return .{ .handle = global };
|
||||
}
|
||||
|
||||
pub const Global = struct {
|
||||
handle: v8.Global,
|
||||
|
||||
pub fn deinit(self: *Global) void {
|
||||
v8.v8__Global__Reset(&self.handle);
|
||||
}
|
||||
|
||||
pub fn local(self: *const Global, l: *const js.Local) PromiseResolver {
|
||||
return .{
|
||||
.local = l,
|
||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -22,6 +22,7 @@ const bridge = @import("bridge.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
const Window = @import("../webapi/Window.zig");
|
||||
|
||||
const v8 = js.v8;
|
||||
const JsApis = bridge.JsApis;
|
||||
@@ -52,14 +53,14 @@ startup_data: v8.StartupData,
|
||||
external_references: [countExternalReferences()]isize,
|
||||
|
||||
// Track whether this snapshot owns its data (was created in-process)
|
||||
// If false, the data points into embedded_snapshot_blob and will not be freed
|
||||
// If false, the data points into embedded_snapshot_blob and should not be freed
|
||||
owns_data: bool = false,
|
||||
|
||||
pub fn load() !Snapshot {
|
||||
pub fn load(allocator: Allocator) !Snapshot {
|
||||
if (loadEmbedded()) |snapshot| {
|
||||
return snapshot;
|
||||
}
|
||||
return create();
|
||||
return create(allocator);
|
||||
}
|
||||
|
||||
fn loadEmbedded() ?Snapshot {
|
||||
@@ -74,7 +75,7 @@ fn loadEmbedded() ?Snapshot {
|
||||
const blob = embedded_snapshot_blob[@sizeOf(usize)..];
|
||||
|
||||
const startup_data = v8.StartupData{ .data = blob.ptr, .raw_size = @intCast(blob.len) };
|
||||
if (!v8.v8__StartupData__IsValid(startup_data)) {
|
||||
if (!v8.SnapshotCreator.startupDataIsValid(startup_data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -86,11 +87,10 @@ fn loadEmbedded() ?Snapshot {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Snapshot) void {
|
||||
pub fn deinit(self: Snapshot, allocator: Allocator) void {
|
||||
// Only free if we own the data (was created in-process)
|
||||
if (self.owns_data) {
|
||||
// V8 allocated this with `new char[]`, so we need to use the C++ delete[] operator
|
||||
v8.v8__StartupData__DELETE(self.startup_data.data);
|
||||
allocator.free(self.startup_data.data[0..@intCast(self.startup_data.raw_size)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,39 +105,39 @@ pub fn write(self: Snapshot, writer: *std.Io.Writer) !void {
|
||||
|
||||
pub fn fromEmbedded(self: Snapshot) bool {
|
||||
// if the snapshot comes from the embedFile, then it'll be flagged as not
|
||||
// owning (aka, not needing to free) the data.
|
||||
// owneing (aka, not needing to free) the data.
|
||||
return self.owns_data == false;
|
||||
}
|
||||
|
||||
fn isValid(self: Snapshot) bool {
|
||||
return v8.v8__StartupData__IsValid(self.startup_data);
|
||||
return v8.SnapshotCreator.startupDataIsValid(self.startup_data);
|
||||
}
|
||||
|
||||
pub fn create() !Snapshot {
|
||||
pub fn create(allocator: Allocator) !Snapshot {
|
||||
var external_references = collectExternalReferences();
|
||||
|
||||
var params: v8.CreateParams = undefined;
|
||||
v8.v8__Isolate__CreateParams__CONSTRUCT(¶ms);
|
||||
params.array_buffer_allocator = v8.v8__ArrayBuffer__Allocator__NewDefaultAllocator();
|
||||
defer v8.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);
|
||||
var params = v8.initCreateParams();
|
||||
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
|
||||
defer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
|
||||
params.external_references = @ptrCast(&external_references);
|
||||
|
||||
const snapshot_creator = v8.v8__SnapshotCreator__CREATE(¶ms);
|
||||
defer v8.v8__SnapshotCreator__DESTRUCT(snapshot_creator);
|
||||
var snapshot_creator: v8.SnapshotCreator = undefined;
|
||||
v8.SnapshotCreator.init(&snapshot_creator, ¶ms);
|
||||
defer snapshot_creator.deinit();
|
||||
|
||||
var data_start: usize = 0;
|
||||
const isolate = v8.v8__SnapshotCreator__getIsolate(snapshot_creator).?;
|
||||
const isolate = snapshot_creator.getIsolate();
|
||||
|
||||
{
|
||||
// CreateBlob, which we'll call once everything is setup, MUST NOT
|
||||
// be called from an active HandleScope. Hence we have this scope to
|
||||
// clean it up before we call CreateBlob
|
||||
var handle_scope: v8.HandleScope = undefined;
|
||||
v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate);
|
||||
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
|
||||
v8.HandleScope.init(&handle_scope, isolate);
|
||||
defer handle_scope.deinit();
|
||||
|
||||
// Create templates (constructors only) FIRST
|
||||
var templates: [JsApis.len]*v8.FunctionTemplate = undefined;
|
||||
var templates: [JsApis.len]v8.FunctionTemplate = undefined;
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
@setEvalBranchQuota(10_000);
|
||||
templates[i] = generateConstructor(JsApi, isolate);
|
||||
@@ -148,21 +148,30 @@ pub fn create() !Snapshot {
|
||||
// This must come before attachClass so inheritance is set up first
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
||||
v8.v8__FunctionTemplate__Inherit(templates[i], templates[proto_index]);
|
||||
templates[i].inherit(templates[proto_index]);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the global template to inherit from Window's template
|
||||
// This way the global object gets all Window properties through inheritance
|
||||
const context = v8.v8__Context__New(isolate, null, null);
|
||||
v8.v8__Context__Enter(context);
|
||||
defer v8.v8__Context__Exit(context);
|
||||
const js_global = v8.FunctionTemplate.initDefault(isolate);
|
||||
js_global.setClassName(v8.String.initUtf8(isolate, "Window"));
|
||||
|
||||
// Find Window in JsApis by name (avoids circular import)
|
||||
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
|
||||
js_global.inherit(templates[window_index]);
|
||||
|
||||
const global_template = js_global.getInstanceTemplate();
|
||||
|
||||
const context = v8.Context.init(isolate, global_template, null);
|
||||
context.enter();
|
||||
defer context.exit();
|
||||
|
||||
// Add templates to context snapshot
|
||||
var last_data_index: usize = 0;
|
||||
inline for (JsApis, 0..) |_, i| {
|
||||
@setEvalBranchQuota(10_000);
|
||||
const data_index = v8.v8__SnapshotCreator__AddData(snapshot_creator, @ptrCast(templates[i]));
|
||||
const data_index = snapshot_creator.addDataWithContext(context, @ptrCast(templates[i].handle));
|
||||
if (i == 0) {
|
||||
data_start = data_index;
|
||||
last_data_index = data_index;
|
||||
@@ -180,18 +189,16 @@ pub fn create() !Snapshot {
|
||||
}
|
||||
|
||||
// Realize all templates by getting their functions and attaching to global
|
||||
const global_obj = v8.v8__Context__Global(context);
|
||||
const global_obj = context.getGlobal();
|
||||
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
const func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
|
||||
const func = templates[i].getFunction(context);
|
||||
|
||||
// Attach to global if it has a name
|
||||
if (@hasDecl(JsApi.Meta, "name")) {
|
||||
if (@hasDecl(JsApi.Meta, "constructor_alias")) {
|
||||
const alias = JsApi.Meta.constructor_alias;
|
||||
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len));
|
||||
var maybe_result: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
|
||||
const v8_class_name = v8.String.initUtf8(isolate, JsApi.Meta.constructor_alias);
|
||||
_ = global_obj.setValue(context, v8_class_name, func);
|
||||
|
||||
// @TODO: This is wrong. This name should be registered with the
|
||||
// illegalConstructorCallback. I.e. new Image() is OK, but
|
||||
@@ -199,15 +206,11 @@ pub fn create() !Snapshot {
|
||||
// But we _have_ to register the name, i.e. HTMLImageElement
|
||||
// has to be registered so, for now, instead of creating another
|
||||
// template, we just hook it into the constructor.
|
||||
const name = JsApi.Meta.name;
|
||||
const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
var maybe_result2: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__Set(global_obj, context, illegal_class_name, func, &maybe_result2);
|
||||
const illegal_class_name = v8.String.initUtf8(isolate, JsApi.Meta.name);
|
||||
_ = global_obj.setValue(context, illegal_class_name, func);
|
||||
} else {
|
||||
const name = JsApi.Meta.name;
|
||||
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
var maybe_result: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
|
||||
const v8_class_name = v8.String.initUtf8(isolate, JsApi.Meta.name);
|
||||
_ = global_obj.setValue(context, v8_class_name, func);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,10 +218,8 @@ pub fn create() !Snapshot {
|
||||
{
|
||||
// If we want to overwrite the built-in console, we have to
|
||||
// delete the built-in one.
|
||||
const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7);
|
||||
var maybe_deleted: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted);
|
||||
if (maybe_deleted.value == false) {
|
||||
const console_key = v8.String.initUtf8(isolate, "console");
|
||||
if (global_obj.deleteValue(context, console_key) == false) {
|
||||
return error.ConsoleDeleteError;
|
||||
}
|
||||
}
|
||||
@@ -228,59 +229,39 @@ pub fn create() !Snapshot {
|
||||
// TODO: see if newer V8 engines have a way around this.
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
if (comptime protoIndexLookup(JsApi)) |proto_index| {
|
||||
const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context);
|
||||
const proto_obj: *const v8.Object = @ptrCast(proto_func);
|
||||
|
||||
const self_func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
|
||||
const self_obj: *const v8.Object = @ptrCast(self_func);
|
||||
|
||||
var maybe_result: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__SetPrototype(self_obj, context, proto_obj, &maybe_result);
|
||||
const proto_obj = templates[proto_index].getFunction(context).toObject();
|
||||
const self_obj = templates[i].getFunction(context).toObject();
|
||||
_ = self_obj.setPrototype(context, proto_obj);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Custom exception
|
||||
// TODO: this is an horrible hack, I can't figure out how to do this cleanly.
|
||||
const code_str = "DOMException.prototype.__proto__ = Error.prototype";
|
||||
const code = v8.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.kNormal, @intCast(code_str.len));
|
||||
const script = v8.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed;
|
||||
_ = v8.v8__Script__Run(script, context) orelse return error.ScriptRunFailed;
|
||||
const code = v8.String.initUtf8(isolate, "DOMException.prototype.__proto__ = Error.prototype");
|
||||
_ = try (try v8.Script.compile(context, code, null)).run(context);
|
||||
}
|
||||
|
||||
v8.v8__SnapshotCreator__setDefaultContext(snapshot_creator, context);
|
||||
snapshot_creator.setDefaultContext(context);
|
||||
}
|
||||
|
||||
const blob = v8.v8__SnapshotCreator__createBlob(snapshot_creator, v8.kKeep);
|
||||
const blob = snapshot_creator.createBlob(v8.FunctionCodeHandling.kKeep);
|
||||
const owned = try allocator.dupe(u8, blob.data[0..@intCast(blob.raw_size)]);
|
||||
|
||||
return .{
|
||||
.owns_data = true,
|
||||
.data_start = data_start,
|
||||
.external_references = external_references,
|
||||
.startup_data = blob,
|
||||
.startup_data = .{ .data = owned.ptr, .raw_size = @intCast(owned.len) },
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to check if a JsApi has a NamedIndexed handler
|
||||
fn hasNamedIndexedGetter(comptime JsApi: type) bool {
|
||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||
inline for (declarations) |d| {
|
||||
const value = @field(JsApi, d.name);
|
||||
const T = @TypeOf(value);
|
||||
if (T == bridge.NamedIndexed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Count total callbacks needed for external_references array
|
||||
fn countExternalReferences() comptime_int {
|
||||
@setEvalBranchQuota(100_000);
|
||||
|
||||
// +1 for the illegal constructor callback
|
||||
var count: comptime_int = 1;
|
||||
var has_non_template_property: bool = false;
|
||||
|
||||
inline for (JsApis) |JsApi| {
|
||||
// Constructor (only if explicit)
|
||||
@@ -303,10 +284,6 @@ fn countExternalReferences() comptime_int {
|
||||
if (value.setter != null) count += 1; // setter
|
||||
} else if (T == bridge.Function) {
|
||||
count += 1;
|
||||
} else if (T == bridge.Property) {
|
||||
if (value.template == false) {
|
||||
has_non_template_property = true;
|
||||
}
|
||||
} else if (T == bridge.Iterator) {
|
||||
count += 1;
|
||||
} else if (T == bridge.Indexed) {
|
||||
@@ -319,19 +296,6 @@ fn countExternalReferences() comptime_int {
|
||||
}
|
||||
}
|
||||
|
||||
if (has_non_template_property) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// In debug mode, add unknown property callbacks for types without NamedIndexed
|
||||
if (comptime IS_DEBUG) {
|
||||
inline for (JsApis) |JsApi| {
|
||||
if (!hasNamedIndexedGetter(JsApi)) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count + 1; // +1 for null terminator
|
||||
}
|
||||
|
||||
@@ -342,8 +306,6 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
||||
references[idx] = @bitCast(@intFromPtr(&illegalConstructorCallback));
|
||||
idx += 1;
|
||||
|
||||
var has_non_template_property = false;
|
||||
|
||||
inline for (JsApis) |JsApi| {
|
||||
if (@hasDecl(JsApi, "constructor")) {
|
||||
references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
|
||||
@@ -369,10 +331,6 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
||||
} else if (T == bridge.Function) {
|
||||
references[idx] = @bitCast(@intFromPtr(value.func));
|
||||
idx += 1;
|
||||
} else if (T == bridge.Property) {
|
||||
if (value.template == false) {
|
||||
has_non_template_property = true;
|
||||
}
|
||||
} else if (T == bridge.Iterator) {
|
||||
references[idx] = @bitCast(@intFromPtr(value.func));
|
||||
idx += 1;
|
||||
@@ -394,21 +352,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
|
||||
if (comptime IS_DEBUG) {
|
||||
inline for (JsApis) |JsApi| {
|
||||
if (!hasNamedIndexedGetter(JsApi)) {
|
||||
references[idx] = @bitCast(@intFromPtr(bridge.unknownObjectPropertyCallback(JsApi)));
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
@@ -418,7 +361,7 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
||||
// via `new ClassName()` - but they could, for example, be created in
|
||||
// Zig and returned from a function call, which is why we need the
|
||||
// FunctionTemplate.
|
||||
fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionTemplate {
|
||||
fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate {
|
||||
const callback = blk: {
|
||||
if (@hasDecl(JsApi, "constructor")) {
|
||||
break :blk JsApi.constructor.func;
|
||||
@@ -428,25 +371,19 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionT
|
||||
break :blk illegalConstructorCallback;
|
||||
};
|
||||
|
||||
const template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?);
|
||||
const template = v8.FunctionTemplate.initCallback(isolate, callback);
|
||||
if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
||||
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, 1);
|
||||
template.getInstanceTemplate().setInternalFieldCount(1);
|
||||
}
|
||||
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
|
||||
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
|
||||
v8.v8__FunctionTemplate__SetClassName(template, class_name);
|
||||
const class_name = v8.String.initUtf8(isolate, if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi));
|
||||
template.setClassName(class_name);
|
||||
return template;
|
||||
}
|
||||
|
||||
// Attaches JsApi members to the prototype template (normal case)
|
||||
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);
|
||||
|
||||
fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
|
||||
const target = template.getPrototypeTemplate();
|
||||
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||
var has_named_index_getter = false;
|
||||
|
||||
inline for (declarations) |d| {
|
||||
const name: [:0]const u8 = d.name;
|
||||
const value = @field(JsApi, name);
|
||||
@@ -454,92 +391,60 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
||||
|
||||
switch (definition) {
|
||||
bridge.Accessor => {
|
||||
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
const getter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.getter }).?);
|
||||
const js_name = v8.String.initUtf8(isolate, name).toName();
|
||||
const getter_callback = v8.FunctionTemplate.initCallback(isolate, value.getter);
|
||||
if (value.setter == null) {
|
||||
if (value.static) {
|
||||
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
|
||||
template.setAccessorGetter(js_name, getter_callback);
|
||||
} else {
|
||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(target, js_name, getter_callback);
|
||||
target.setAccessorGetter(js_name, getter_callback);
|
||||
}
|
||||
} else {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(value.static == false);
|
||||
}
|
||||
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);
|
||||
std.debug.assert(value.static == false);
|
||||
const setter_callback = v8.FunctionTemplate.initCallback(isolate, value.setter);
|
||||
target.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
|
||||
}
|
||||
},
|
||||
bridge.Function => {
|
||||
const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func, .length = value.arity }).?);
|
||||
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
|
||||
const js_name: v8.Name = v8.String.initUtf8(isolate, name).toName();
|
||||
if (value.static) {
|
||||
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
||||
template.set(js_name, function_template, v8.PropertyAttribute.None);
|
||||
} else {
|
||||
v8.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.None);
|
||||
target.set(js_name, function_template, v8.PropertyAttribute.None);
|
||||
}
|
||||
},
|
||||
bridge.Indexed => {
|
||||
var configuration: v8.IndexedPropertyHandlerConfiguration = .{
|
||||
const configuration = v8.IndexedPropertyHandlerConfiguration{
|
||||
.getter = value.getter,
|
||||
.setter = null,
|
||||
.query = null,
|
||||
.deleter = null,
|
||||
.enumerator = null,
|
||||
.definer = null,
|
||||
.descriptor = null,
|
||||
.data = null,
|
||||
.flags = 0,
|
||||
};
|
||||
v8.v8__ObjectTemplate__SetIndexedHandler(instance, &configuration);
|
||||
},
|
||||
bridge.NamedIndexed => {
|
||||
var configuration: v8.NamedPropertyHandlerConfiguration = .{
|
||||
.getter = value.getter,
|
||||
.setter = value.setter,
|
||||
.query = null,
|
||||
.deleter = value.deleter,
|
||||
.enumerator = null,
|
||||
.definer = null,
|
||||
.descriptor = null,
|
||||
.data = null,
|
||||
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||
};
|
||||
v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);
|
||||
has_named_index_getter = true;
|
||||
target.setIndexedProperty(configuration, null);
|
||||
},
|
||||
bridge.NamedIndexed => template.getInstanceTemplate().setNamedProperty(.{
|
||||
.getter = value.getter,
|
||||
.setter = value.setter,
|
||||
.deleter = value.deleter,
|
||||
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
|
||||
}, null),
|
||||
bridge.Iterator => {
|
||||
const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func }).?);
|
||||
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
|
||||
const js_name = if (value.async)
|
||||
v8.v8__Symbol__GetAsyncIterator(isolate)
|
||||
v8.Symbol.getAsyncIterator(isolate).toName()
|
||||
else
|
||||
v8.v8__Symbol__GetIterator(isolate);
|
||||
v8.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.None);
|
||||
v8.Symbol.getIterator(isolate).toName();
|
||||
target.set(js_name, function_template, v8.PropertyAttribute.None);
|
||||
},
|
||||
bridge.Property => {
|
||||
const js_value = switch (value.value) {
|
||||
.null => js.simpleZigValueToJs(.{ .handle = isolate }, null, true, false),
|
||||
inline .bool, .int, .float, .string => |v| js.simpleZigValueToJs(.{ .handle = isolate }, v, true, false),
|
||||
const js_value = switch (value) {
|
||||
.int => |v| js.simpleZigValueToJs(isolate, v, true, false),
|
||||
};
|
||||
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
|
||||
// is like an Accessor, but because the value is known at
|
||||
// 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, &.{
|
||||
.callback = bridge.Property.getter,
|
||||
.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);
|
||||
// and to instances of the type
|
||||
v8.v8__Template__Set(@ptrCast(target), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||
}
|
||||
const js_name = v8.String.initUtf8(isolate, name).toName();
|
||||
// apply it both to the type itself
|
||||
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
||||
|
||||
// and to instances of the type
|
||||
target.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
|
||||
},
|
||||
bridge.Constructor => {}, // already handled in generateConstructor
|
||||
else => {},
|
||||
@@ -547,31 +452,9 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
||||
}
|
||||
|
||||
if (@hasDecl(JsApi.Meta, "htmldda")) {
|
||||
v8.v8__ObjectTemplate__MarkAsUndetectable(instance);
|
||||
v8.v8__ObjectTemplate__SetCallAsFunctionHandler(instance, JsApi.Meta.callable.func);
|
||||
}
|
||||
|
||||
if (@hasDecl(JsApi.Meta, "name")) {
|
||||
const js_name = v8.v8__Symbol__GetToStringTag(isolate);
|
||||
const js_value = v8.v8__String__NewFromUtf8(isolate, JsApi.Meta.name.ptr, v8.kNormal, @intCast(JsApi.Meta.name.len));
|
||||
v8.v8__Template__Set(@ptrCast(instance), js_name, js_value, v8.ReadOnly + v8.DontDelete);
|
||||
}
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
if (!has_named_index_getter) {
|
||||
var configuration: v8.NamedPropertyHandlerConfiguration = .{
|
||||
.getter = bridge.unknownObjectPropertyCallback(JsApi),
|
||||
.setter = null,
|
||||
.query = null,
|
||||
.deleter = null,
|
||||
.enumerator = null,
|
||||
.definer = null,
|
||||
.descriptor = null,
|
||||
.data = null,
|
||||
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||
};
|
||||
v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);
|
||||
}
|
||||
const instance_template = template.getInstanceTemplate();
|
||||
instance_template.markAsUndetectable();
|
||||
instance_template.setCallAsFunctionHandler(JsApi.Meta.callable.func);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,15 +472,10 @@ fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
|
||||
}
|
||||
|
||||
// Shared illegal constructor callback for types without explicit constructors
|
||||
fn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info);
|
||||
fn illegalConstructorCallback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
const iso = info.getIsolate();
|
||||
log.warn(.js, "Illegal constructor call", .{});
|
||||
|
||||
const message = v8.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.kNormal, 19);
|
||||
const js_exception = v8.v8__Exception__TypeError(message);
|
||||
|
||||
_ = v8.v8__Isolate__ThrowException(isolate, js_exception);
|
||||
var return_value: v8.ReturnValue = undefined;
|
||||
v8.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value);
|
||||
v8.v8__ReturnValue__Set(return_value, js_exception);
|
||||
const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
|
||||
info.getReturnValue().set(js_exception);
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const SSO = @import("../../string.zig").String;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const v8 = js.v8;
|
||||
|
||||
const String = @This();
|
||||
|
||||
local: *const js.Local,
|
||||
handle: *const v8.String,
|
||||
|
||||
pub fn toSlice(self: String) ![]u8 {
|
||||
return self._toSlice(false, self.local.call_arena);
|
||||
}
|
||||
pub fn toSliceZ(self: String) ![:0]u8 {
|
||||
return self._toSlice(true, self.local.call_arena);
|
||||
}
|
||||
pub fn toSliceWithAlloc(self: String, allocator: Allocator) ![]u8 {
|
||||
return self._toSlice(false, allocator);
|
||||
}
|
||||
fn _toSlice(self: String, comptime null_terminate: bool, allocator: Allocator) !(if (null_terminate) [:0]u8 else []u8) {
|
||||
const local = self.local;
|
||||
const handle = self.handle;
|
||||
const isolate = local.isolate.handle;
|
||||
|
||||
const len = v8.v8__String__Utf8Length(handle, isolate);
|
||||
const buf = try (if (comptime null_terminate) allocator.allocSentinel(u8, @intCast(len), 0) else allocator.alloc(u8, @intCast(len)));
|
||||
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(n == len);
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
pub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) {
|
||||
if (comptime global) {
|
||||
return .{ .str = try self.toSSOWithAlloc(self.local.ctx.arena) };
|
||||
}
|
||||
return self.toSSOWithAlloc(self.local.call_arena);
|
||||
}
|
||||
pub fn toSSOWithAlloc(self: String, allocator: Allocator) !SSO {
|
||||
const handle = self.handle;
|
||||
const isolate = self.local.isolate.handle;
|
||||
|
||||
const len: usize = @intCast(v8.v8__String__Utf8Length(handle, isolate));
|
||||
|
||||
if (len <= 12) {
|
||||
var content: [12]u8 = undefined;
|
||||
const n = v8.v8__String__WriteUtf8(handle, isolate, &content[0], content.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(n == len);
|
||||
}
|
||||
// Weird that we do this _after_, but we have to..I've seen weird issues
|
||||
// in ReleaseMode where v8 won't write to content if it starts off zero
|
||||
// initiated
|
||||
@memset(content[len..], 0);
|
||||
return .{ .len = @intCast(len), .payload = .{ .content = content } };
|
||||
}
|
||||
|
||||
const buf = try allocator.alloc(u8, len);
|
||||
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(n == len);
|
||||
}
|
||||
|
||||
var prefix: [4]u8 = @splat(0);
|
||||
@memcpy(&prefix, buf[0..4]);
|
||||
|
||||
return .{
|
||||
.len = @intCast(len),
|
||||
.payload = .{ .heap = .{
|
||||
.prefix = prefix,
|
||||
.ptr = buf.ptr,
|
||||
} },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(self: String, writer: *std.Io.Writer) !void {
|
||||
const local = self.local;
|
||||
const handle = self.handle;
|
||||
const isolate = local.isolate.handle;
|
||||
|
||||
var small: [1024]u8 = undefined;
|
||||
const len = v8.v8__String__Utf8Length(handle, isolate);
|
||||
var buf = if (len < 1024) &small else local.call_arena.alloc(u8, @intCast(len)) catch return error.WriteFailed;
|
||||
|
||||
const n = v8.v8__String__WriteUtf8(handle, isolate, buf.ptr, buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||
return writer.writeAll(buf[0..n]);
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
// 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 js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
const bridge = js.bridge;
|
||||
|
||||
// When we return a Zig object to V8, we put it on the heap and pass it into
|
||||
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
|
||||
// function parameter, we know what type it _should_ be.
|
||||
//
|
||||
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
|
||||
// to the parameter type:
|
||||
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
|
||||
//
|
||||
// But there are 2 reasons we can't do that.
|
||||
//
|
||||
// == Reason 1 ==
|
||||
// The JS code might pass the wrong type:
|
||||
//
|
||||
// var cat = new Cat();
|
||||
// cat.setOwner(new Cat());
|
||||
//
|
||||
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
|
||||
// the JS code passed a *Cat.
|
||||
//
|
||||
// To solve this issue, we tag every returned value so that we can check what
|
||||
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
|
||||
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
|
||||
//
|
||||
// == Reason 2 ==
|
||||
// Because of prototype inheritance, even "correct" code can be a challenge. For
|
||||
// example, say the above JavaScript is fixed:
|
||||
//
|
||||
// var cat = new Cat();
|
||||
// cat.setOwner(new Owner("Leto"));
|
||||
//
|
||||
// The issue is that setOwner might not expect an *Owner, but rather a
|
||||
// *Person, which is the prototype for Owner. Now our Zig code is expecting
|
||||
// a *Person, but it was (correctly) given an *Owner.
|
||||
// For this reason, we also store the prototype chain.
|
||||
const TaggedOpaque = @This();
|
||||
|
||||
prototype_len: u16,
|
||||
prototype_chain: [*]const PrototypeChainEntry,
|
||||
|
||||
// Ptr to the Zig instance. Between the context where it's called (i.e.
|
||||
// we have the comptime parameter info for all functions), and the index field
|
||||
// we can figure out what type this is.
|
||||
value: *anyopaque,
|
||||
|
||||
// When we're asked to describe an object via the Inspector, we _must_ include
|
||||
// the proper subtype (and description) fields in the returned JSON.
|
||||
// V8 will give us a Value and ask us for the subtype. From the js.Value we
|
||||
// can get a js.Object, and from the js.Object, we can get out TaggedOpaque
|
||||
// which is where we store the subtype.
|
||||
subtype: ?bridge.SubType,
|
||||
|
||||
pub const PrototypeChainEntry = struct {
|
||||
index: bridge.JsApiLookup.BackingInt,
|
||||
offset: u16, // offset to the _proto field
|
||||
};
|
||||
|
||||
// Reverses the mapZigInstanceToJs, making sure that our TaggedOpaque
|
||||
// contains a ptr to the correct type.
|
||||
pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R {
|
||||
const ti = @typeInfo(R);
|
||||
if (ti != .pointer) {
|
||||
@compileError("non-pointer Zig parameter type: " ++ @typeName(R));
|
||||
}
|
||||
|
||||
const T = ti.pointer.child;
|
||||
const JsApi = bridge.Struct(T).JsApi;
|
||||
|
||||
if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
||||
// Empty structs aren't stored as TOAs and there's no data
|
||||
// stored in the JSObject's IntenrnalField. Why bother when
|
||||
// we can just return an empty struct here?
|
||||
return @constCast(@as(*const T, &.{}));
|
||||
}
|
||||
|
||||
const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle);
|
||||
// Special case for Window: the global object doesn't have internal fields
|
||||
// Window instance is stored in context.page.window instead
|
||||
if (internal_field_count == 0) {
|
||||
// Normally, this would be an error. All JsObject that map to a Zig type
|
||||
// are either `empty_with_no_proto` (handled above) or have an
|
||||
// interalFieldCount. The only exception to that is the Window...
|
||||
const isolate = v8.v8__Object__GetIsolate(js_obj_handle).?;
|
||||
const context = js.Context.fromIsolate(.{ .handle = isolate });
|
||||
|
||||
const Window = @import("../webapi/Window.zig");
|
||||
if (T == Window) {
|
||||
return context.page.window;
|
||||
}
|
||||
|
||||
// ... Or the window's prototype.
|
||||
// We could make this all comptime-fancy, but it's easier to hard-code
|
||||
// the EventTarget
|
||||
|
||||
const EventTarget = @import("../webapi/EventTarget.zig");
|
||||
if (T == EventTarget) {
|
||||
return context.page.window._proto;
|
||||
}
|
||||
|
||||
// Type not found in Window's prototype chain
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
|
||||
// if it isn't an empty struct, then the v8.Object should have an
|
||||
// InternalFieldCount > 0, since our toa pointer should be embedded
|
||||
// at index 0 of the internal field count.
|
||||
if (internal_field_count == 0) {
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
|
||||
if (!bridge.JsApiLookup.has(JsApi)) {
|
||||
@compileError("unknown Zig type: " ++ @typeName(R));
|
||||
}
|
||||
|
||||
const internal_field_handle = v8.v8__Object__GetInternalField(js_obj_handle, 0).?;
|
||||
const tao: *TaggedOpaque = @ptrCast(@alignCast(v8.v8__External__Value(internal_field_handle)));
|
||||
const expected_type_index = bridge.JsApiLookup.getId(JsApi);
|
||||
|
||||
const prototype_chain = tao.prototype_chain[0..tao.prototype_len];
|
||||
if (prototype_chain[0].index == expected_type_index) {
|
||||
return @ptrCast(@alignCast(tao.value));
|
||||
}
|
||||
|
||||
// Ok, let's walk up the chain
|
||||
var ptr = @intFromPtr(tao.value);
|
||||
for (prototype_chain[1..]) |proto| {
|
||||
ptr += proto.offset; // the offset to the _proto field
|
||||
const proto_ptr: **anyopaque = @ptrFromInt(ptr);
|
||||
if (proto.index == expected_type_index) {
|
||||
return @ptrCast(@alignCast(proto_ptr.*));
|
||||
}
|
||||
ptr = @intFromPtr(proto_ptr.*);
|
||||
}
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
@@ -19,13 +19,22 @@
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
|
||||
const v8 = js.v8;
|
||||
// This only exists so that we know whether a function wants the opaque
|
||||
// JS argument (js.Object), or if it wants the receiver as an opaque
|
||||
// value.
|
||||
// js.Object is normally used when a method wants an opaque JS object
|
||||
// that it'll pass into a callback.
|
||||
// This is used when the function wants to do advanced manipulation
|
||||
// of the v8.Object bound to the instance. For example, postAttach is an
|
||||
// example of using This.
|
||||
|
||||
const Number = @This();
|
||||
const This = @This();
|
||||
obj: js.Object,
|
||||
|
||||
handle: *const v8.Number,
|
||||
|
||||
pub fn init(isolate: *v8.Isolate, value: anytype) Number {
|
||||
const handle = v8.v8__Number__New(isolate, value).?;
|
||||
return .{ .handle = handle };
|
||||
pub fn setIndex(self: This, index: u32, value: anytype, opts: js.Object.SetOpts) !void {
|
||||
return self.obj.setIndex(index, value, opts);
|
||||
}
|
||||
|
||||
pub fn set(self: This, key: []const u8, value: anytype, opts: js.Object.SetOpts) !void {
|
||||
return self.obj.set(key, value, opts);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -20,118 +20,63 @@ const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const TryCatch = @This();
|
||||
|
||||
handle: v8.TryCatch,
|
||||
local: *const js.Local,
|
||||
inner: v8.TryCatch,
|
||||
context: *const js.Context,
|
||||
|
||||
pub fn init(self: *TryCatch, l: *const js.Local) void {
|
||||
self.local = l;
|
||||
v8.v8__TryCatch__CONSTRUCT(&self.handle, l.isolate.handle);
|
||||
pub fn init(self: *TryCatch, context: *const js.Context) void {
|
||||
self.context = context;
|
||||
self.inner.init(context.isolate);
|
||||
}
|
||||
|
||||
pub fn hasCaught(self: TryCatch) bool {
|
||||
return v8.v8__TryCatch__HasCaught(&self.handle);
|
||||
return self.inner.hasCaught();
|
||||
}
|
||||
|
||||
pub fn rethrow(self: *TryCatch) void {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.hasCaught());
|
||||
// the caller needs to deinit the string returned
|
||||
pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
||||
const msg = self.inner.getException() orelse return null;
|
||||
return try self.context.valueToString(msg, .{ .allocator = allocator });
|
||||
}
|
||||
|
||||
// the caller needs to deinit the string returned
|
||||
pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
||||
const context = self.context;
|
||||
const s = self.inner.getStackTrace(context.v8_context) orelse return null;
|
||||
return try context.valueToString(s, .{ .allocator = allocator });
|
||||
}
|
||||
|
||||
// the caller needs to deinit the string returned
|
||||
pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
||||
const context = self.context;
|
||||
const msg = self.inner.getMessage() orelse return null;
|
||||
const sl = msg.getSourceLine(context.v8_context) orelse return null;
|
||||
return try context.jsStringToZig(sl, .{ .allocator = allocator });
|
||||
}
|
||||
|
||||
pub fn sourceLineNumber(self: TryCatch) ?u32 {
|
||||
const context = self.context;
|
||||
const msg = self.inner.getMessage() orelse return null;
|
||||
return msg.getLineNumber(context.v8_context);
|
||||
}
|
||||
|
||||
// a shorthand method to return either the entire stack message
|
||||
// or just the exception message
|
||||
// - in Debug mode return the stack if available
|
||||
// - otherwise return the exception if available
|
||||
// the caller needs to deinit the string returned
|
||||
pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 {
|
||||
if (comptime @import("builtin").mode == .Debug) {
|
||||
if (try self.stack(allocator)) |msg| {
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
_ = v8.v8__TryCatch__ReThrow(&self.handle);
|
||||
}
|
||||
|
||||
pub fn caught(self: TryCatch, allocator: Allocator) ?Caught {
|
||||
if (self.hasCaught() == false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const l = self.local;
|
||||
const line: ?u32 = blk: {
|
||||
const handle = v8.v8__TryCatch__Message(&self.handle) orelse return null;
|
||||
const line = v8.v8__Message__GetLineNumber(handle, l.handle);
|
||||
break :blk if (line < 0) null else @intCast(line);
|
||||
};
|
||||
|
||||
const exception: ?[]const u8 = blk: {
|
||||
const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null;
|
||||
var js_val = js.Value{ .local = l, .handle = handle };
|
||||
|
||||
// If it's an Error object, try to get the message property
|
||||
if (js_val.isObject()) {
|
||||
const js_obj = js_val.toObject();
|
||||
if (js_obj.has("message")) {
|
||||
js_val = js_obj.get("message") catch break :blk null;
|
||||
}
|
||||
}
|
||||
|
||||
if (js_val.isString()) |js_str| {
|
||||
break :blk js_str.toSliceWithAlloc(allocator) catch |err| @errorName(err);
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
const stack: ?[]const u8 = blk: {
|
||||
const handle = v8.v8__TryCatch__StackTrace(&self.handle, l.handle) orelse break :blk null;
|
||||
var js_val = js.Value{ .local = l, .handle = handle };
|
||||
|
||||
// If it's an Error object, try to get the stack property
|
||||
if (js_val.isObject()) {
|
||||
const js_obj = js_val.toObject();
|
||||
if (js_obj.has("stack")) {
|
||||
js_val = js_obj.get("stack") catch break :blk null;
|
||||
}
|
||||
}
|
||||
|
||||
if (js_val.isString()) |js_str| {
|
||||
break :blk js_str.toSliceWithAlloc(allocator) catch |err| @errorName(err);
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
return .{
|
||||
.line = line,
|
||||
.stack = stack,
|
||||
.caught = true,
|
||||
.exception = exception,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn caughtOrError(self: TryCatch, allocator: Allocator, err: anyerror) Caught {
|
||||
return self.caught(allocator) orelse .{
|
||||
.caught = false,
|
||||
.line = null,
|
||||
.stack = null,
|
||||
.exception = @errorName(err),
|
||||
};
|
||||
return try self.exception(allocator);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TryCatch) void {
|
||||
v8.v8__TryCatch__DESTRUCT(&self.handle);
|
||||
self.inner.deinit();
|
||||
}
|
||||
|
||||
pub const Caught = struct {
|
||||
line: ?u32 = null,
|
||||
caught: bool = false,
|
||||
stack: ?[]const u8 = null,
|
||||
exception: ?[]const u8 = null,
|
||||
|
||||
pub fn format(self: Caught, writer: *std.Io.Writer) !void {
|
||||
const separator = @import("../../log.zig").separator();
|
||||
try writer.print("{s}exception: {?s}", .{ separator, self.exception });
|
||||
try writer.print("{s}stack: {?s}", .{ separator, self.stack });
|
||||
try writer.print("{s}line: {?d}", .{ separator, self.line });
|
||||
try writer.print("{s}caught: {any}", .{ separator, self.caught });
|
||||
}
|
||||
|
||||
pub fn logFmt(self: Caught, comptime prefix: []const u8, writer: anytype) !void {
|
||||
try writer.write(prefix ++ ".exception", self.exception orelse "???");
|
||||
try writer.write(prefix ++ ".stack", self.stack orelse "na");
|
||||
try writer.write(prefix ++ ".line", self.line);
|
||||
try writer.write(prefix ++ ".caught", self.caught);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,323 +18,75 @@
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const SSO = @import("../../string.zig").String;
|
||||
|
||||
const v8 = js.v8;
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Value = @This();
|
||||
const PersistentValue = v8.Persistent(v8.Value);
|
||||
|
||||
local: *const js.Local,
|
||||
handle: *const v8.Value,
|
||||
const Value = @This();
|
||||
js_val: v8.Value,
|
||||
context: *js.Context,
|
||||
|
||||
pub fn isObject(self: Value) bool {
|
||||
return v8.v8__Value__IsObject(self.handle);
|
||||
return self.js_val.isObject();
|
||||
}
|
||||
|
||||
pub fn isString(self: Value) ?js.String {
|
||||
const handle = self.handle;
|
||||
if (!v8.v8__Value__IsString(handle)) {
|
||||
return null;
|
||||
}
|
||||
return .{ .local = self.local, .handle = @ptrCast(handle) };
|
||||
pub fn isString(self: Value) bool {
|
||||
return self.js_val.isString();
|
||||
}
|
||||
|
||||
pub fn isArray(self: Value) bool {
|
||||
return v8.v8__Value__IsArray(self.handle);
|
||||
return self.js_val.isArray();
|
||||
}
|
||||
|
||||
pub fn isSymbol(self: Value) bool {
|
||||
return v8.v8__Value__IsSymbol(self.handle);
|
||||
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
|
||||
return self.context.valueToString(self.js_val, .{ .allocator = allocator });
|
||||
}
|
||||
|
||||
pub fn isFunction(self: Value) bool {
|
||||
return v8.v8__Value__IsFunction(self.handle);
|
||||
pub fn fromJson(ctx: *js.Context, json: []const u8) !Value {
|
||||
const json_string = v8.String.initUtf8(ctx.isolate, json);
|
||||
const value = try v8.Json.parse(ctx.v8_context, json_string);
|
||||
return Value{ .context = ctx, .js_val = value };
|
||||
}
|
||||
|
||||
pub fn isNull(self: Value) bool {
|
||||
return v8.v8__Value__IsNull(self.handle);
|
||||
}
|
||||
pub fn persist(self: Value) !Value {
|
||||
const js_val = self.js_val;
|
||||
var context = self.context;
|
||||
|
||||
pub fn isUndefined(self: Value) bool {
|
||||
return v8.v8__Value__IsUndefined(self.handle);
|
||||
}
|
||||
const persisted = PersistentValue.init(context.isolate, js_val);
|
||||
try context.js_value_list.append(context.arena, persisted);
|
||||
|
||||
pub fn isNullOrUndefined(self: Value) bool {
|
||||
return v8.v8__Value__IsNullOrUndefined(self.handle);
|
||||
}
|
||||
|
||||
pub fn isNumber(self: Value) bool {
|
||||
return v8.v8__Value__IsNumber(self.handle);
|
||||
}
|
||||
|
||||
pub fn isNumberObject(self: Value) bool {
|
||||
return v8.v8__Value__IsNumberObject(self.handle);
|
||||
}
|
||||
|
||||
pub fn isInt32(self: Value) bool {
|
||||
return v8.v8__Value__IsInt32(self.handle);
|
||||
}
|
||||
|
||||
pub fn isUint32(self: Value) bool {
|
||||
return v8.v8__Value__IsUint32(self.handle);
|
||||
}
|
||||
|
||||
pub fn isBigInt(self: Value) bool {
|
||||
return v8.v8__Value__IsBigInt(self.handle);
|
||||
}
|
||||
|
||||
pub fn isBigIntObject(self: Value) bool {
|
||||
return v8.v8__Value__IsBigIntObject(self.handle);
|
||||
}
|
||||
|
||||
pub fn isBoolean(self: Value) bool {
|
||||
return v8.v8__Value__IsBoolean(self.handle);
|
||||
}
|
||||
|
||||
pub fn isBooleanObject(self: Value) bool {
|
||||
return v8.v8__Value__IsBooleanObject(self.handle);
|
||||
}
|
||||
|
||||
pub fn isTrue(self: Value) bool {
|
||||
return v8.v8__Value__IsTrue(self.handle);
|
||||
}
|
||||
|
||||
pub fn isFalse(self: Value) bool {
|
||||
return v8.v8__Value__IsFalse(self.handle);
|
||||
}
|
||||
|
||||
pub fn isTypedArray(self: Value) bool {
|
||||
return v8.v8__Value__IsTypedArray(self.handle);
|
||||
}
|
||||
|
||||
pub fn isArrayBufferView(self: Value) bool {
|
||||
return v8.v8__Value__IsArrayBufferView(self.handle);
|
||||
}
|
||||
|
||||
pub fn isArrayBuffer(self: Value) bool {
|
||||
return v8.v8__Value__IsArrayBuffer(self.handle);
|
||||
}
|
||||
|
||||
pub fn isUint8Array(self: Value) bool {
|
||||
return v8.v8__Value__IsUint8Array(self.handle);
|
||||
}
|
||||
|
||||
pub fn isUint8ClampedArray(self: Value) bool {
|
||||
return v8.v8__Value__IsUint8ClampedArray(self.handle);
|
||||
}
|
||||
|
||||
pub fn isInt8Array(self: Value) bool {
|
||||
return v8.v8__Value__IsInt8Array(self.handle);
|
||||
}
|
||||
|
||||
pub fn isUint16Array(self: Value) bool {
|
||||
return v8.v8__Value__IsUint16Array(self.handle);
|
||||
}
|
||||
|
||||
pub fn isInt16Array(self: Value) bool {
|
||||
return v8.v8__Value__IsInt16Array(self.handle);
|
||||
}
|
||||
|
||||
pub fn isUint32Array(self: Value) bool {
|
||||
return v8.v8__Value__IsUint32Array(self.handle);
|
||||
}
|
||||
|
||||
pub fn isInt32Array(self: Value) bool {
|
||||
return v8.v8__Value__IsInt32Array(self.handle);
|
||||
}
|
||||
|
||||
pub fn isBigUint64Array(self: Value) bool {
|
||||
return v8.v8__Value__IsBigUint64Array(self.handle);
|
||||
}
|
||||
|
||||
pub fn isBigInt64Array(self: Value) bool {
|
||||
return v8.v8__Value__IsBigInt64Array(self.handle);
|
||||
}
|
||||
|
||||
pub fn isPromise(self: Value) bool {
|
||||
return v8.v8__Value__IsPromise(self.handle);
|
||||
}
|
||||
|
||||
pub fn toBool(self: Value) bool {
|
||||
return v8.v8__Value__BooleanValue(self.handle, self.local.isolate.handle);
|
||||
}
|
||||
|
||||
pub fn typeOf(self: Value) js.String {
|
||||
const str_handle = v8.v8__Value__TypeOf(self.handle, self.local.isolate.handle).?;
|
||||
return js.String{ .local = self.local, .handle = str_handle };
|
||||
}
|
||||
|
||||
pub fn toF32(self: Value) !f32 {
|
||||
return @floatCast(try self.toF64());
|
||||
}
|
||||
|
||||
pub fn toF64(self: Value) !f64 {
|
||||
var maybe: v8.MaybeF64 = undefined;
|
||||
v8.v8__Value__NumberValue(self.handle, self.local.handle, &maybe);
|
||||
if (!maybe.has_value) {
|
||||
return error.JsException;
|
||||
}
|
||||
return maybe.value;
|
||||
}
|
||||
|
||||
pub fn toI32(self: Value) !i32 {
|
||||
var maybe: v8.MaybeI32 = undefined;
|
||||
v8.v8__Value__Int32Value(self.handle, self.local.handle, &maybe);
|
||||
if (!maybe.has_value) {
|
||||
return error.JsException;
|
||||
}
|
||||
return maybe.value;
|
||||
}
|
||||
|
||||
pub fn toU32(self: Value) !u32 {
|
||||
var maybe: v8.MaybeU32 = undefined;
|
||||
v8.v8__Value__Uint32Value(self.handle, self.local.handle, &maybe);
|
||||
if (!maybe.has_value) {
|
||||
return error.JsException;
|
||||
}
|
||||
return maybe.value;
|
||||
}
|
||||
|
||||
pub fn toPromise(self: Value) js.Promise {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.isPromise());
|
||||
}
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = @ptrCast(self.handle),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toString(self: Value) !js.String {
|
||||
const l = self.local;
|
||||
const value_handle: *const v8.Value = blk: {
|
||||
if (self.isSymbol()) {
|
||||
break :blk @ptrCast(v8.v8__Symbol__Description(@ptrCast(self.handle), l.isolate.handle).?);
|
||||
}
|
||||
break :blk self.handle;
|
||||
};
|
||||
|
||||
const str_handle = v8.v8__Value__ToString(value_handle, l.handle) orelse return error.JsException;
|
||||
return .{ .local = self.local, .handle = str_handle };
|
||||
}
|
||||
|
||||
pub fn toSSO(self: Value, comptime global: bool) !(if (global) SSO.Global else SSO) {
|
||||
return (try self.toString()).toSSO(global);
|
||||
}
|
||||
pub fn toSSOWithAlloc(self: Value, allocator: Allocator) !SSO {
|
||||
return (try self.toString()).toSSOWithAlloc(allocator);
|
||||
}
|
||||
|
||||
pub fn toStringSlice(self: Value) ![]u8 {
|
||||
return (try self.toString()).toSlice();
|
||||
}
|
||||
pub fn toStringSliceZ(self: Value) ![:0]u8 {
|
||||
return (try self.toString()).toSliceZ();
|
||||
}
|
||||
pub fn toStringSliceWithAlloc(self: Value, allocator: Allocator) ![]u8 {
|
||||
return (try self.toString()).toSliceWithAlloc(allocator);
|
||||
}
|
||||
|
||||
pub fn toJson(self: Value, allocator: Allocator) ![]u8 {
|
||||
const local = self.local;
|
||||
const str_handle = v8.v8__JSON__Stringify(local.handle, self.handle, null) orelse return error.JsException;
|
||||
return js.String.toSliceWithAlloc(.{ .local = local, .handle = str_handle }, allocator);
|
||||
}
|
||||
|
||||
pub fn persist(self: Value) !Global {
|
||||
return self._persist(true);
|
||||
}
|
||||
|
||||
pub fn temp(self: Value) !Temp {
|
||||
return self._persist(false);
|
||||
}
|
||||
|
||||
fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Global else Temp) {
|
||||
var ctx = self.local.ctx;
|
||||
|
||||
var global: v8.Global = undefined;
|
||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||
if (comptime is_global) {
|
||||
try ctx.global_values.append(ctx.arena, global);
|
||||
} else {
|
||||
try ctx.global_values_temp.put(ctx.arena, global.data_ptr, global);
|
||||
}
|
||||
return .{ .handle = global };
|
||||
}
|
||||
|
||||
pub fn toZig(self: Value, comptime T: type) !T {
|
||||
return self.local.jsValueToZig(T, self);
|
||||
return Value{ .context = context, .js_val = persisted.toValue() };
|
||||
}
|
||||
|
||||
pub fn toObject(self: Value) js.Object {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.isObject());
|
||||
}
|
||||
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = @ptrCast(self.handle),
|
||||
.context = self.context,
|
||||
.js_obj = self.js_val.castTo(v8.Object),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toArray(self: Value) js.Array {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.isArray());
|
||||
}
|
||||
|
||||
return .{
|
||||
.local = self.local,
|
||||
.handle = @ptrCast(self.handle),
|
||||
.context = self.context,
|
||||
.js_arr = self.js_val.castTo(v8.Array),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toBigInt(self: Value) js.BigInt {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self.isBigInt());
|
||||
}
|
||||
// pub const Value = struct {
|
||||
// value: v8.Value,
|
||||
// context: *const Context,
|
||||
|
||||
return .{
|
||||
.handle = @ptrCast(self.handle),
|
||||
};
|
||||
}
|
||||
// // the caller needs to deinit the string returned
|
||||
// pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
|
||||
// return self.context.valueToString(self.value, .{ .allocator = allocator });
|
||||
// }
|
||||
|
||||
pub fn format(self: Value, writer: *std.Io.Writer) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
return self.local.debugValue(self, writer);
|
||||
}
|
||||
const js_str = self.toString() catch return error.WriteFailed;
|
||||
return js_str.format(writer);
|
||||
}
|
||||
|
||||
pub const Temp = G(0);
|
||||
pub const Global = G(1);
|
||||
|
||||
fn G(comptime discriminator: u8) type {
|
||||
return struct {
|
||||
handle: v8.Global,
|
||||
|
||||
// makes the types different (G(0) != G(1)), without taking up space
|
||||
comptime _: u8 = discriminator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
v8.v8__Global__Reset(&self.handle);
|
||||
}
|
||||
|
||||
pub fn local(self: *const Self, l: *const js.Local) Value {
|
||||
return .{
|
||||
.local = l,
|
||||
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isEqual(self: *const Self, other: Value) bool {
|
||||
return v8.v8__Global__IsEqual(&self.handle, other.handle);
|
||||
}
|
||||
};
|
||||
}
|
||||
// pub fn fromJson(ctx: *Context, json: []const u8) !Value {
|
||||
// const json_string = v8.String.initUtf8(ctx.isolate, json);
|
||||
// const value = try v8.Json.parse(ctx.v8_context, json_string);
|
||||
// return Value{ .context = ctx, .value = value };
|
||||
// }
|
||||
// };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -18,16 +18,11 @@
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const lp = @import("lightpanda");
|
||||
const log = @import("../../log.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const v8 = js.v8;
|
||||
|
||||
const Caller = @import("Caller.zig");
|
||||
const Context = @import("Context.zig");
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
pub fn Builder(comptime T: type) type {
|
||||
return struct {
|
||||
@@ -62,29 +57,16 @@ pub fn Builder(comptime T: type) type {
|
||||
return Callable.init(T, func, opts);
|
||||
}
|
||||
|
||||
pub fn property(value: anytype, opts: Property.Opts) Property {
|
||||
pub fn property(value: anytype) Property {
|
||||
switch (@typeInfo(@TypeOf(value))) {
|
||||
.bool => return Property.init(.{ .bool = value }, opts),
|
||||
.null => return Property.init(.null, opts),
|
||||
.comptime_int, .int => return Property.init(.{ .int = value }, opts),
|
||||
.comptime_float, .float => return Property.init(.{ .float = value }, opts),
|
||||
.pointer => |ptr| switch (ptr.size) {
|
||||
.one => {
|
||||
const one_info = @typeInfo(ptr.child);
|
||||
if (one_info == .array and one_info.array.child == u8) {
|
||||
return Property.init(.{ .string = value }, opts);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
},
|
||||
.comptime_int, .int => return .{ .int = value },
|
||||
else => {},
|
||||
}
|
||||
@compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet");
|
||||
}
|
||||
|
||||
const PrototypeChainEntry = @import("TaggedOpaque.zig").PrototypeChainEntry;
|
||||
pub fn prototypeChain() [prototypeChainLength(T)]PrototypeChainEntry {
|
||||
var entries: [prototypeChainLength(T)]PrototypeChainEntry = undefined;
|
||||
pub fn prototypeChain() [prototypeChainLength(T)]js.PrototypeChainEntry {
|
||||
var entries: [prototypeChainLength(T)]js.PrototypeChainEntry = undefined;
|
||||
|
||||
entries[0] = .{ .offset = 0, .index = JsApiLookup.getId(T.JsApi) };
|
||||
|
||||
@@ -103,39 +85,11 @@ pub fn Builder(comptime T: type) type {
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool) void) Finalizer {
|
||||
return .{
|
||||
.from_zig = struct {
|
||||
fn wrap(ptr: *anyopaque) void {
|
||||
func(@ptrCast(@alignCast(ptr)), true);
|
||||
}
|
||||
}.wrap,
|
||||
|
||||
.from_v8 = struct {
|
||||
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
|
||||
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
|
||||
const fc: *Context.FinalizerCallback = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const ctx = fc.ctx;
|
||||
const value_ptr = fc.ptr;
|
||||
if (ctx.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
|
||||
func(@ptrCast(@alignCast(value_ptr)), false);
|
||||
ctx.release(value_ptr);
|
||||
} else {
|
||||
// A bit weird, but v8 _requires_ that we release it
|
||||
// If we don't. We'll 100% crash.
|
||||
v8.v8__Global__Reset(&fc.global);
|
||||
}
|
||||
}
|
||||
}.wrap,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const Constructor = struct {
|
||||
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
||||
|
||||
const Opts = struct {
|
||||
dom_exception: bool = false,
|
||||
@@ -143,13 +97,12 @@ pub const Constructor = struct {
|
||||
|
||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor {
|
||||
return .{ .func = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
caller.constructor(T, func, handle.?, .{
|
||||
caller.constructor(T, func, info, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
});
|
||||
}
|
||||
@@ -159,8 +112,7 @@ pub const Constructor = struct {
|
||||
|
||||
pub const Function = struct {
|
||||
static: bool,
|
||||
arity: usize,
|
||||
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
||||
|
||||
const Opts = struct {
|
||||
static: bool = false,
|
||||
@@ -172,22 +124,20 @@ pub const Function = struct {
|
||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Function {
|
||||
return .{
|
||||
.static = opts.static,
|
||||
.arity = getArity(@TypeOf(func)),
|
||||
.func = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
if (comptime opts.static) {
|
||||
caller.function(T, func, handle.?, .{
|
||||
caller.function(T, func, info, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
} else {
|
||||
caller.method(T, func, handle.?, .{
|
||||
caller.method(T, func, info, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
@@ -197,34 +147,18 @@ pub const Function = struct {
|
||||
}.wrap,
|
||||
};
|
||||
}
|
||||
|
||||
fn getArity(comptime T: type) usize {
|
||||
var count: usize = 0;
|
||||
var params = @typeInfo(T).@"fn".params;
|
||||
for (params[1..]) |p| { // start at 1, skip self
|
||||
const PT = p.type.?;
|
||||
if (PT == *Page or PT == *const Page) {
|
||||
break;
|
||||
}
|
||||
if (@typeInfo(PT) == .optional) {
|
||||
break;
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Accessor = struct {
|
||||
static: bool = false,
|
||||
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
||||
setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
||||
getter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null,
|
||||
setter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null,
|
||||
|
||||
const Opts = struct {
|
||||
static: bool = false,
|
||||
cache: ?[]const u8 = null, // @ZIGDOM
|
||||
as_typed_array: bool = false,
|
||||
null_as_undefined: bool = false,
|
||||
dom_exception: bool = false,
|
||||
};
|
||||
|
||||
fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Opts) Accessor {
|
||||
@@ -234,39 +168,29 @@ pub const Accessor = struct {
|
||||
|
||||
if (@typeInfo(@TypeOf(getter)) != .null) {
|
||||
accessor.getter = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
if (comptime opts.static) {
|
||||
caller.function(T, getter, handle.?, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
} else {
|
||||
caller.method(T, getter, handle.?, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
}
|
||||
caller.method(T, getter, info, .{
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
}
|
||||
}.wrap;
|
||||
}
|
||||
|
||||
if (@typeInfo(@TypeOf(setter)) != .null) {
|
||||
accessor.setter = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
std.debug.assert(info.length() == 1);
|
||||
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
caller.method(T, setter, handle.?, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
caller.method(T, setter, info, .{
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
@@ -279,7 +203,7 @@ pub const Accessor = struct {
|
||||
};
|
||||
|
||||
pub const Indexed = struct {
|
||||
getter: *const fn (idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||
getter: *const fn (idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8,
|
||||
|
||||
const Opts = struct {
|
||||
as_typed_array: bool = false,
|
||||
@@ -288,13 +212,11 @@ pub const Indexed = struct {
|
||||
|
||||
fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) Indexed {
|
||||
return .{ .getter = struct {
|
||||
fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
fn wrap(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
return caller.getIndex(T, getter, idx, handle.?, .{
|
||||
return caller.getIndex(T, getter, idx, info, .{
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
@@ -304,9 +226,9 @@ pub const Indexed = struct {
|
||||
};
|
||||
|
||||
pub const NamedIndexed = struct {
|
||||
getter: *const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||
setter: ?*const fn (c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
|
||||
deleter: ?*const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
|
||||
getter: *const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8,
|
||||
setter: ?*const fn (c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null,
|
||||
deleter: ?*const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null,
|
||||
|
||||
const Opts = struct {
|
||||
as_typed_array: bool = false,
|
||||
@@ -315,13 +237,11 @@ pub const NamedIndexed = struct {
|
||||
|
||||
fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {
|
||||
const getter_fn = struct {
|
||||
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
return caller.getNamedIndex(T, getter, c_name.?, handle.?, .{
|
||||
return caller.getNamedIndex(T, getter, .{ .handle = c_name.? }, info, .{
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
@@ -329,13 +249,12 @@ pub const NamedIndexed = struct {
|
||||
}.wrap;
|
||||
|
||||
const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct {
|
||||
fn wrap(c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
fn wrap(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
return caller.setNamedIndex(T, setter, c_name.?, c_value.?, handle.?, .{
|
||||
return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
@@ -343,13 +262,12 @@ pub const NamedIndexed = struct {
|
||||
}.wrap;
|
||||
|
||||
const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct {
|
||||
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
return caller.deleteNamedIndex(T, deleter, c_name.?, handle.?, .{
|
||||
return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
@@ -365,7 +283,7 @@ pub const NamedIndexed = struct {
|
||||
};
|
||||
|
||||
pub const Iterator = struct {
|
||||
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
||||
async: bool,
|
||||
|
||||
const Opts = struct {
|
||||
@@ -378,8 +296,8 @@ pub const Iterator = struct {
|
||||
return .{
|
||||
.async = opts.async,
|
||||
.func = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = Caller.FunctionCallbackInfo{ .handle = handle.? };
|
||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
info.getReturnValue().set(info.getThis());
|
||||
}
|
||||
}.wrap,
|
||||
@@ -389,12 +307,11 @@ pub const Iterator = struct {
|
||||
return .{
|
||||
.async = opts.async,
|
||||
.func = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
caller.method(T, struct_or_func, handle.?, .{});
|
||||
caller.method(T, struct_or_func, info, .{});
|
||||
}
|
||||
}.wrap,
|
||||
};
|
||||
@@ -402,7 +319,7 @@ pub const Iterator = struct {
|
||||
};
|
||||
|
||||
pub const Callable = struct {
|
||||
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
||||
|
||||
const Opts = struct {
|
||||
null_as_undefined: bool = false,
|
||||
@@ -410,13 +327,11 @@ pub const Callable = struct {
|
||||
|
||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {
|
||||
return .{ .func = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
caller.method(T, func, handle.?, .{
|
||||
caller.method(T, func, info, .{
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
}
|
||||
@@ -424,195 +339,10 @@ pub const Callable = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const Property = struct {
|
||||
value: Value,
|
||||
template: bool,
|
||||
|
||||
const Value = union(enum) {
|
||||
null,
|
||||
int: i64,
|
||||
float: f64,
|
||||
bool: bool,
|
||||
string: []const u8,
|
||||
};
|
||||
|
||||
const Opts = struct {
|
||||
template: bool,
|
||||
};
|
||||
|
||||
fn init(value: Value, opts: Opts) Property {
|
||||
return .{
|
||||
.value = value,
|
||||
.template = opts.template,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
pub const Property = union(enum) {
|
||||
int: i64,
|
||||
};
|
||||
|
||||
const Finalizer = struct {
|
||||
// The finalizer wrapper when called fro Zig. This is only called on
|
||||
// Context.deinit
|
||||
from_zig: *const fn (ctx: *anyopaque) void,
|
||||
|
||||
// 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,
|
||||
// it is only ever called after we SetWeak on the Global.
|
||||
from_v8: *const fn (?*const v8.WeakCallbackInfo) callconv(.c) void,
|
||||
};
|
||||
|
||||
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, 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();
|
||||
|
||||
const local = &caller.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {
|
||||
return 0;
|
||||
};
|
||||
|
||||
const page = local.ctx.page;
|
||||
const document = page.document;
|
||||
|
||||
if (document.getElementById(property, page)) |el| {
|
||||
const js_val = local.zigValueToJs(el, .{}) catch return 0;
|
||||
var pc = Caller.PropertyCallbackInfo{ .handle = handle.? };
|
||||
pc.getReturnValue().set(js_val);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
if (std.mem.startsWith(u8, property, "__")) {
|
||||
// some frameworks will extend built-in types using a __ prefix
|
||||
// these should always be safe to ignore.
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ignored = std.StaticStringMap(void).initComptime(.{
|
||||
.{ "Deno", {} },
|
||||
.{ "process", {} },
|
||||
.{ "ShadyDOM", {} },
|
||||
.{ "ShadyCSS", {} },
|
||||
|
||||
// a lot of sites seem to like having their own window.config.
|
||||
.{ "config", {} },
|
||||
|
||||
.{ "litNonce", {} },
|
||||
.{ "litHtmlVersions", {} },
|
||||
.{ "litElementVersions", {} },
|
||||
.{ "litHtmlPolyfillSupport", {} },
|
||||
.{ "litElementHydrateSupport", {} },
|
||||
.{ "litElementPolyfillSupport", {} },
|
||||
.{ "reactiveElementVersions", {} },
|
||||
|
||||
.{ "recaptcha", {} },
|
||||
.{ "grecaptcha", {} },
|
||||
.{ "___grecaptcha_cfg", {} },
|
||||
.{ "__recaptcha_api", {} },
|
||||
.{ "__google_recaptcha_client", {} },
|
||||
|
||||
.{ "CLOSURE_FLAGS", {} },
|
||||
.{ "__REACT_DEVTOOLS_GLOBAL_HOOK__", {} },
|
||||
.{ "ApplePaySession", {} },
|
||||
});
|
||||
if (!ignored.has(property)) {
|
||||
const key = std.fmt.bufPrint(&local.ctx.page.buf, "Window:{s}", .{property}) catch return 0;
|
||||
logUnknownProperty(local, key) catch return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// not intercepted
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Only used for debugging
|
||||
pub fn unknownObjectPropertyCallback(comptime JsApi: type) *const fn (?*const v8.Name, ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
if (comptime !IS_DEBUG) {
|
||||
@compileError("unknownObjectPropertyCallback should only be used in debug builds");
|
||||
}
|
||||
|
||||
return struct {
|
||||
fn wrap(c_name: ?*const v8.Name, 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();
|
||||
|
||||
const local = &caller.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (std.mem.startsWith(u8, property, "__")) {
|
||||
// some frameworks will extend built-in types using a __ prefix
|
||||
// these should always be safe to ignore.
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, property, "jQuery")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (JsApi == @import("../webapi/cdata/Text.zig").JsApi or JsApi == @import("../webapi/cdata/Comment.zig").JsApi) {
|
||||
if (std.mem.eql(u8, property, "tagName")) {
|
||||
// knockout does this, a lot.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (JsApi == @import("../webapi/element/Html.zig").JsApi or JsApi == @import("../webapi/Element.zig").JsApi or JsApi == @import("../webapi/element/html/Custom.zig").JsApi) {
|
||||
// react ?
|
||||
if (std.mem.eql(u8, property, "props")) return 0;
|
||||
if (std.mem.eql(u8, property, "hydrated")) return 0;
|
||||
if (std.mem.eql(u8, property, "isHydrated")) return 0;
|
||||
}
|
||||
|
||||
if (JsApi == @import("../webapi/Console.zig").JsApi) {
|
||||
if (std.mem.eql(u8, property, "firebug")) return 0;
|
||||
}
|
||||
|
||||
const ignored = std.StaticStringMap(void).initComptime(.{});
|
||||
if (!ignored.has(property)) {
|
||||
const key = std.fmt.bufPrint(&local.ctx.page.buf, "{s}:{s}", .{ if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi), property }) catch return 0;
|
||||
logUnknownProperty(local, key) catch return 0;
|
||||
}
|
||||
// not intercepted
|
||||
return 0;
|
||||
}
|
||||
}.wrap;
|
||||
}
|
||||
|
||||
fn logUnknownProperty(local: *const js.Local, key: []const u8) !void {
|
||||
const ctx = local.ctx;
|
||||
const gop = try ctx.unknown_properties.getOrPut(ctx.arena, key);
|
||||
if (gop.found_existing) {
|
||||
gop.value_ptr.count += 1;
|
||||
} else {
|
||||
gop.key_ptr.* = try ctx.arena.dupe(u8, key);
|
||||
gop.value_ptr.* = .{
|
||||
.count = 1,
|
||||
.first_stack = try ctx.arena.dupe(u8, (try local.stackTrace()) orelse "???"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Given a Type, returns the length of the prototype chain, including self
|
||||
fn prototypeChainLength(comptime T: type) usize {
|
||||
var l: usize = 1;
|
||||
@@ -799,22 +529,16 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/element/Html.zig"),
|
||||
@import("../webapi/element/html/IFrame.zig"),
|
||||
@import("../webapi/element/html/Anchor.zig"),
|
||||
@import("../webapi/element/html/Area.zig"),
|
||||
@import("../webapi/element/html/Audio.zig"),
|
||||
@import("../webapi/element/html/Base.zig"),
|
||||
@import("../webapi/element/html/Body.zig"),
|
||||
@import("../webapi/element/html/BR.zig"),
|
||||
@import("../webapi/element/html/Button.zig"),
|
||||
@import("../webapi/element/html/Canvas.zig"),
|
||||
@import("../webapi/element/html/Custom.zig"),
|
||||
@import("../webapi/element/html/Data.zig"),
|
||||
@import("../webapi/element/html/DataList.zig"),
|
||||
@import("../webapi/element/html/Dialog.zig"),
|
||||
@import("../webapi/element/html/Directory.zig"),
|
||||
@import("../webapi/element/html/Div.zig"),
|
||||
@import("../webapi/element/html/Embed.zig"),
|
||||
@import("../webapi/element/html/FieldSet.zig"),
|
||||
@import("../webapi/element/html/Font.zig"),
|
||||
@import("../webapi/element/html/Form.zig"),
|
||||
@import("../webapi/element/html/Generic.zig"),
|
||||
@import("../webapi/element/html/Head.zig"),
|
||||
@@ -823,43 +547,20 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/element/html/Html.zig"),
|
||||
@import("../webapi/element/html/Image.zig"),
|
||||
@import("../webapi/element/html/Input.zig"),
|
||||
@import("../webapi/element/html/Label.zig"),
|
||||
@import("../webapi/element/html/Legend.zig"),
|
||||
@import("../webapi/element/html/LI.zig"),
|
||||
@import("../webapi/element/html/Link.zig"),
|
||||
@import("../webapi/element/html/Map.zig"),
|
||||
@import("../webapi/element/html/Media.zig"),
|
||||
@import("../webapi/element/html/Meta.zig"),
|
||||
@import("../webapi/element/html/Meter.zig"),
|
||||
@import("../webapi/element/html/Mod.zig"),
|
||||
@import("../webapi/element/html/Object.zig"),
|
||||
@import("../webapi/element/html/OL.zig"),
|
||||
@import("../webapi/element/html/OptGroup.zig"),
|
||||
@import("../webapi/element/html/Option.zig"),
|
||||
@import("../webapi/element/html/Output.zig"),
|
||||
@import("../webapi/element/html/Paragraph.zig"),
|
||||
@import("../webapi/element/html/Picture.zig"),
|
||||
@import("../webapi/element/html/Param.zig"),
|
||||
@import("../webapi/element/html/Pre.zig"),
|
||||
@import("../webapi/element/html/Progress.zig"),
|
||||
@import("../webapi/element/html/Quote.zig"),
|
||||
@import("../webapi/element/html/Script.zig"),
|
||||
@import("../webapi/element/html/Select.zig"),
|
||||
@import("../webapi/element/html/Slot.zig"),
|
||||
@import("../webapi/element/html/Source.zig"),
|
||||
@import("../webapi/element/html/Span.zig"),
|
||||
@import("../webapi/element/html/Style.zig"),
|
||||
@import("../webapi/element/html/Table.zig"),
|
||||
@import("../webapi/element/html/TableCaption.zig"),
|
||||
@import("../webapi/element/html/TableCell.zig"),
|
||||
@import("../webapi/element/html/TableCol.zig"),
|
||||
@import("../webapi/element/html/TableRow.zig"),
|
||||
@import("../webapi/element/html/TableSection.zig"),
|
||||
@import("../webapi/element/html/Template.zig"),
|
||||
@import("../webapi/element/html/TextArea.zig"),
|
||||
@import("../webapi/element/html/Time.zig"),
|
||||
@import("../webapi/element/html/Title.zig"),
|
||||
@import("../webapi/element/html/Track.zig"),
|
||||
@import("../webapi/element/html/Video.zig"),
|
||||
@import("../webapi/element/html/UL.zig"),
|
||||
@import("../webapi/element/html/Unknown.zig"),
|
||||
@@ -878,9 +579,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/event/PopStateEvent.zig"),
|
||||
@import("../webapi/event/UIEvent.zig"),
|
||||
@import("../webapi/event/MouseEvent.zig"),
|
||||
@import("../webapi/event/PointerEvent.zig"),
|
||||
@import("../webapi/event/KeyboardEvent.zig"),
|
||||
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
||||
@import("../webapi/MessageChannel.zig"),
|
||||
@import("../webapi/MessagePort.zig"),
|
||||
@import("../webapi/media/MediaError.zig"),
|
||||
@@ -905,7 +604,6 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/URL.zig"),
|
||||
@import("../webapi/Window.zig"),
|
||||
@import("../webapi/Performance.zig"),
|
||||
@import("../webapi/PluginArray.zig"),
|
||||
@import("../webapi/MutationObserver.zig"),
|
||||
@import("../webapi/IntersectionObserver.zig"),
|
||||
@import("../webapi/CustomElementRegistry.zig"),
|
||||
@@ -914,14 +612,9 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/Blob.zig"),
|
||||
@import("../webapi/File.zig"),
|
||||
@import("../webapi/Screen.zig"),
|
||||
@import("../webapi/VisualViewport.zig"),
|
||||
@import("../webapi/PerformanceObserver.zig"),
|
||||
@import("../webapi/navigation/Navigation.zig"),
|
||||
@import("../webapi/navigation/NavigationEventTarget.zig"),
|
||||
@import("../webapi/navigation/NavigationHistoryEntry.zig"),
|
||||
@import("../webapi/navigation/NavigationActivation.zig"),
|
||||
@import("../webapi/canvas/CanvasRenderingContext2D.zig"),
|
||||
@import("../webapi/canvas/WebGLRenderingContext.zig"),
|
||||
@import("../webapi/SubtleCrypto.zig"),
|
||||
@import("../webapi/Selection.zig"),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -17,34 +17,25 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
pub const v8 = @import("v8").c;
|
||||
pub const v8 = @import("v8");
|
||||
|
||||
const string = @import("../../string.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
pub const Env = @import("Env.zig");
|
||||
pub const bridge = @import("bridge.zig");
|
||||
pub const Caller = @import("Caller.zig");
|
||||
pub const ExecutionWorld = @import("ExecutionWorld.zig");
|
||||
pub const Context = @import("Context.zig");
|
||||
pub const Local = @import("Local.zig");
|
||||
pub const Inspector = @import("Inspector.zig");
|
||||
pub const Snapshot = @import("Snapshot.zig");
|
||||
pub const Platform = @import("Platform.zig");
|
||||
pub const Isolate = @import("Isolate.zig");
|
||||
pub const HandleScope = @import("HandleScope.zig");
|
||||
|
||||
// TODO: Is "This" really necessary?
|
||||
pub const This = @import("This.zig");
|
||||
pub const Value = @import("Value.zig");
|
||||
pub const Array = @import("Array.zig");
|
||||
pub const String = @import("String.zig");
|
||||
pub const Object = @import("Object.zig");
|
||||
pub const TryCatch = @import("TryCatch.zig");
|
||||
pub const Function = @import("Function.zig");
|
||||
pub const Promise = @import("Promise.zig");
|
||||
pub const Module = @import("Module.zig");
|
||||
pub const BigInt = @import("BigInt.zig");
|
||||
pub const Number = @import("Number.zig");
|
||||
pub const Integer = @import("Integer.zig");
|
||||
pub const PromiseResolver = @import("PromiseResolver.zig");
|
||||
pub const PromiseRejection = @import("PromiseRejection.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -77,43 +68,246 @@ pub const ArrayBuffer = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const Exception = struct {
|
||||
local: *const Local,
|
||||
handle: *const v8.Value,
|
||||
pub const PromiseResolver = struct {
|
||||
context: *Context,
|
||||
resolver: v8.PromiseResolver,
|
||||
|
||||
pub fn promise(self: PromiseResolver) Promise {
|
||||
return self.resolver.getPromise();
|
||||
}
|
||||
|
||||
pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||
self._resolve(value) catch |err| {
|
||||
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = false });
|
||||
};
|
||||
}
|
||||
fn _resolve(self: PromiseResolver, value: anytype) !void {
|
||||
const context = self.context;
|
||||
const js_value = try context.zigValueToJs(value, .{});
|
||||
|
||||
if (self.resolver.resolve(context.v8_context, js_value) == null) {
|
||||
return error.FailedToResolvePromise;
|
||||
}
|
||||
self.context.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||
self._reject(value) catch |err| {
|
||||
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = false });
|
||||
};
|
||||
}
|
||||
fn _reject(self: PromiseResolver, value: anytype) !void {
|
||||
const context = self.context;
|
||||
const js_value = try context.zigValueToJs(value);
|
||||
|
||||
if (self.resolver.reject(context.v8_context, js_value) == null) {
|
||||
return error.FailedToRejectPromise;
|
||||
}
|
||||
self.context.runMicrotasks();
|
||||
}
|
||||
};
|
||||
|
||||
pub const PersistentPromiseResolver = struct {
|
||||
context: *Context,
|
||||
resolver: v8.Persistent(v8.PromiseResolver),
|
||||
|
||||
pub fn deinit(self: *PersistentPromiseResolver) void {
|
||||
self.resolver.deinit();
|
||||
}
|
||||
|
||||
pub fn promise(self: PersistentPromiseResolver) Promise {
|
||||
return self.resolver.castToPromiseResolver().getPromise();
|
||||
}
|
||||
|
||||
pub fn resolve(self: PersistentPromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||
self._resolve(value) catch |err| {
|
||||
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = true });
|
||||
};
|
||||
}
|
||||
fn _resolve(self: PersistentPromiseResolver, value: anytype) !void {
|
||||
const context = self.context;
|
||||
const js_value = try context.zigValueToJs(value, .{});
|
||||
defer context.runMicrotasks();
|
||||
|
||||
if (self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) == null) {
|
||||
return error.FailedToResolvePromise;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reject(self: PersistentPromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||
self._reject(value) catch |err| {
|
||||
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = true });
|
||||
};
|
||||
}
|
||||
|
||||
fn _reject(self: PersistentPromiseResolver, value: anytype) !void {
|
||||
const context = self.context;
|
||||
const js_value = try context.zigValueToJs(value, .{});
|
||||
defer context.runMicrotasks();
|
||||
|
||||
// resolver.reject will return null if the promise isn't pending
|
||||
if (self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) == null) {
|
||||
return error.FailedToRejectPromise;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const Promise = v8.Promise;
|
||||
|
||||
// When doing jsValueToZig, string ([]const u8) are managed by the
|
||||
// call_arena. That means that if the API wants to persist the string
|
||||
// (which is relatively common), it needs to dupe it again.
|
||||
// If the parameter is an Env.String rather than a []const u8, then
|
||||
// the page's arena will be used (rather than the call arena).
|
||||
pub const String = struct {
|
||||
string: []const u8,
|
||||
};
|
||||
|
||||
pub const Exception = struct {
|
||||
inner: v8.Value,
|
||||
context: *const Context,
|
||||
|
||||
// the caller needs to deinit the string returned
|
||||
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
|
||||
return self.context.valueToString(self.inner, .{ .allocator = allocator });
|
||||
}
|
||||
};
|
||||
|
||||
pub fn UndefinedOr(comptime T: type) type {
|
||||
return union(enum) {
|
||||
undefined: void,
|
||||
value: T,
|
||||
};
|
||||
}
|
||||
|
||||
// An interface for types that want to have their jsScopeEnd function be
|
||||
// called when the call context ends
|
||||
const CallScopeEndCallback = struct {
|
||||
ptr: *anyopaque,
|
||||
callScopeEndFn: *const fn (ptr: *anyopaque) void,
|
||||
|
||||
fn init(ptr: anytype) CallScopeEndCallback {
|
||||
const T = @TypeOf(ptr);
|
||||
const ptr_info = @typeInfo(T);
|
||||
|
||||
const gen = struct {
|
||||
pub fn callScopeEnd(pointer: *anyopaque) void {
|
||||
const self: T = @ptrCast(@alignCast(pointer));
|
||||
return ptr_info.pointer.child.jsCallScopeEnd(self);
|
||||
}
|
||||
};
|
||||
|
||||
return .{
|
||||
.ptr = ptr,
|
||||
.callScopeEndFn = gen.callScopeEnd,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn callScopeEnd(self: CallScopeEndCallback) void {
|
||||
self.callScopeEndFn(self.ptr);
|
||||
}
|
||||
};
|
||||
|
||||
// Callback called on global's property missing.
|
||||
// Return true to intercept the execution or false to let the call
|
||||
// continue the chain.
|
||||
pub const GlobalMissingCallback = struct {
|
||||
ptr: *anyopaque,
|
||||
missingFn: *const fn (ptr: *anyopaque, name: []const u8, ctx: *Context) bool,
|
||||
|
||||
pub fn init(ptr: anytype) GlobalMissingCallback {
|
||||
const T = @TypeOf(ptr);
|
||||
const ptr_info = @typeInfo(T);
|
||||
|
||||
const gen = struct {
|
||||
pub fn missing(pointer: *anyopaque, name: []const u8, ctx: *Context) bool {
|
||||
const self: T = @ptrCast(@alignCast(pointer));
|
||||
return ptr_info.pointer.child.missing(self, name, ctx);
|
||||
}
|
||||
};
|
||||
|
||||
return .{
|
||||
.ptr = ptr,
|
||||
.missingFn = gen.missing,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn missing(self: GlobalMissingCallback, name: []const u8, ctx: *Context) bool {
|
||||
return self.missingFn(self.ptr, name, ctx);
|
||||
}
|
||||
};
|
||||
|
||||
// Attributes that return a primitive type are setup directly on the
|
||||
// FunctionTemplate when the Env is setup. More complex types need a v8.Context
|
||||
// and cannot be set directly on the FunctionTemplate.
|
||||
// We default to saying types are primitives because that's mostly what
|
||||
// we have. If we add a new complex type that isn't explictly handled here,
|
||||
// we'll get a compiler error in simpleZigValueToJs, and can then explicitly
|
||||
// add the type here.
|
||||
pub fn isComplexAttributeType(ti: std.builtin.Type) bool {
|
||||
return switch (ti) {
|
||||
.array => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
// These are simple types that we can convert to JS with only an isolate. This
|
||||
// is separated from the Caller's zigValueToJs to make it available when we
|
||||
// don't have a caller (i.e., when setting static attributes on types)
|
||||
pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool, comptime null_as_undefined: bool) if (fail) *const v8.Value else ?*const v8.Value {
|
||||
pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool, comptime null_as_undefined: bool) if (fail) v8.Value else ?v8.Value {
|
||||
switch (@typeInfo(@TypeOf(value))) {
|
||||
.void => return isolate.initUndefined(),
|
||||
.null => if (comptime null_as_undefined) return isolate.initUndefined() else return isolate.initNull(),
|
||||
.bool => return if (value) isolate.initTrue() else isolate.initFalse(),
|
||||
.int => |n| {
|
||||
if (comptime n.bits <= 32) {
|
||||
return @ptrCast(isolate.initInteger(value).handle);
|
||||
}
|
||||
if (value >= 0 and value <= 4_294_967_295) {
|
||||
return @ptrCast(isolate.initInteger(@as(u32, @intCast(value))).handle);
|
||||
}
|
||||
return @ptrCast(isolate.initBigInt(value).handle);
|
||||
.void => return v8.initUndefined(isolate).toValue(),
|
||||
.null => if (comptime null_as_undefined) return v8.initUndefined(isolate).toValue() else return v8.initNull(isolate).toValue(),
|
||||
.bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)),
|
||||
.int => |n| switch (n.signedness) {
|
||||
.signed => {
|
||||
if (value > 0 and value <= 4_294_967_295) {
|
||||
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
||||
}
|
||||
if (value >= -2_147_483_648 and value <= 2_147_483_647) {
|
||||
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
|
||||
}
|
||||
if (comptime n.bits <= 64) {
|
||||
return v8.getValue(v8.BigInt.initI64(isolate, @intCast(value)));
|
||||
}
|
||||
@compileError(@typeName(value) ++ " is not supported");
|
||||
},
|
||||
.unsigned => {
|
||||
if (value <= 4_294_967_295) {
|
||||
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
||||
}
|
||||
if (comptime n.bits <= 64) {
|
||||
return v8.getValue(v8.BigInt.initU64(isolate, @intCast(value)));
|
||||
}
|
||||
@compileError(@typeName(value) ++ " is not supported");
|
||||
},
|
||||
},
|
||||
.comptime_int => {
|
||||
if (value > -2_147_483_648 and value <= 4_294_967_295) {
|
||||
return @ptrCast(isolate.initInteger(value).handle);
|
||||
if (value >= 0) {
|
||||
if (value <= 4_294_967_295) {
|
||||
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
|
||||
}
|
||||
return v8.BigInt.initU64(isolate, @intCast(value)).toValue();
|
||||
}
|
||||
return @ptrCast(isolate.initBigInt(value).handle);
|
||||
if (value >= -2_147_483_648) {
|
||||
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
|
||||
}
|
||||
return v8.BigInt.initI64(isolate, @intCast(value)).toValue();
|
||||
},
|
||||
.comptime_float => return v8.Number.init(isolate, value).toValue(),
|
||||
.float => |f| switch (f.bits) {
|
||||
64 => return v8.Number.init(isolate, value).toValue(),
|
||||
32 => return v8.Number.init(isolate, @floatCast(value)).toValue(),
|
||||
else => @compileError(@typeName(value) ++ " is not supported"),
|
||||
},
|
||||
.float, .comptime_float => return @ptrCast(isolate.initNumber(value).handle),
|
||||
.pointer => |ptr| {
|
||||
if (ptr.size == .slice and ptr.child == u8) {
|
||||
return @ptrCast(isolate.initStringHandle(value));
|
||||
return v8.String.initUtf8(isolate, value).toValue();
|
||||
}
|
||||
if (ptr.size == .one) {
|
||||
const one_info = @typeInfo(ptr.child);
|
||||
if (one_info == .array and one_info.array.child == u8) {
|
||||
return @ptrCast(isolate.initStringHandle(value));
|
||||
return v8.String.initUtf8(isolate, value).toValue();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -123,21 +317,22 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
|
||||
return simpleZigValueToJs(isolate, v, fail, null_as_undefined);
|
||||
}
|
||||
if (comptime null_as_undefined) {
|
||||
return isolate.initUndefined();
|
||||
return v8.initUndefined(isolate).toValue();
|
||||
}
|
||||
return isolate.initNull();
|
||||
return v8.initNull(isolate).toValue();
|
||||
},
|
||||
.@"struct" => {
|
||||
switch (@TypeOf(value)) {
|
||||
string.String => return isolate.initStringHandle(value.str()),
|
||||
ArrayBuffer => {
|
||||
const values = value.values;
|
||||
const len = values.len;
|
||||
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, len);
|
||||
const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
|
||||
var array_buffer: v8.ArrayBuffer = undefined;
|
||||
const backing_store = v8.BackingStore.init(isolate, len);
|
||||
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
|
||||
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
|
||||
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
|
||||
return @ptrCast(v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?);
|
||||
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
||||
|
||||
return .{ .handle = array_buffer.handle };
|
||||
},
|
||||
// zig fmt: off
|
||||
TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64),
|
||||
@@ -154,38 +349,37 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
|
||||
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
|
||||
};
|
||||
|
||||
var array_buffer: *const v8.ArrayBuffer = undefined;
|
||||
var array_buffer: v8.ArrayBuffer = undefined;
|
||||
if (len == 0) {
|
||||
array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;
|
||||
array_buffer = v8.ArrayBuffer.init(isolate, 0);
|
||||
} else {
|
||||
const buffer_len = len * bits / 8;
|
||||
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, buffer_len).?;
|
||||
const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
|
||||
const backing_store = v8.BackingStore.init(isolate, buffer_len);
|
||||
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
|
||||
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
|
||||
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
|
||||
array_buffer = v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?;
|
||||
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
||||
}
|
||||
|
||||
switch (@typeInfo(value_type)) {
|
||||
.int => |n| switch (n.signedness) {
|
||||
.unsigned => switch (n.bits) {
|
||||
8 => return @ptrCast(v8.v8__Uint8Array__New(array_buffer, 0, len).?),
|
||||
16 => return @ptrCast(v8.v8__Uint16Array__New(array_buffer, 0, len).?),
|
||||
32 => return @ptrCast(v8.v8__Uint32Array__New(array_buffer, 0, len).?),
|
||||
64 => return @ptrCast(v8.v8__BigUint64Array__New(array_buffer, 0, len).?),
|
||||
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
|
||||
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
|
||||
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
|
||||
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
|
||||
else => {},
|
||||
},
|
||||
.signed => switch (n.bits) {
|
||||
8 => return @ptrCast(v8.v8__Int8Array__New(array_buffer, 0, len).?),
|
||||
16 => return @ptrCast(v8.v8__Int16Array__New(array_buffer, 0, len).?),
|
||||
32 => return @ptrCast(v8.v8__Int32Array__New(array_buffer, 0, len).?),
|
||||
64 => return @ptrCast(v8.v8__BigInt64Array__New(array_buffer, 0, len).?),
|
||||
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
|
||||
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
|
||||
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
|
||||
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
|
||||
else => {},
|
||||
},
|
||||
},
|
||||
.float => |f| switch (f.bits) {
|
||||
32 => return @ptrCast(v8.v8__Float32Array__New(array_buffer, 0, len).?),
|
||||
64 => return @ptrCast(v8.v8__Float64Array__New(array_buffer, 0, len).?),
|
||||
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
|
||||
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
@@ -194,7 +388,6 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
|
||||
// but this can never be valid.
|
||||
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
|
||||
},
|
||||
inline String, BigInt, Integer, Number, Value, Object => return value.handle,
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
@@ -213,6 +406,76 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _createException(isolate: v8.Isolate, msg: []const u8) v8.Value {
|
||||
return v8.Exception.initError(v8.String.initUtf8(isolate, msg));
|
||||
}
|
||||
|
||||
pub fn classNameForStruct(comptime Struct: type) []const u8 {
|
||||
if (@hasDecl(Struct, "js_name")) {
|
||||
return Struct.js_name;
|
||||
}
|
||||
@setEvalBranchQuota(10_000);
|
||||
const full_name = @typeName(Struct);
|
||||
const last = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse return full_name;
|
||||
return full_name[last + 1 ..];
|
||||
}
|
||||
|
||||
// When we return a Zig object to V8, we put it on the heap and pass it into
|
||||
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
|
||||
// function parameter, we know what type it _should_ be.
|
||||
//
|
||||
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
|
||||
// to the parameter type:
|
||||
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
|
||||
//
|
||||
// But there are 2 reasons we can't do that.
|
||||
//
|
||||
// == Reason 1 ==
|
||||
// The JS code might pass the wrong type:
|
||||
//
|
||||
// var cat = new Cat();
|
||||
// cat.setOwner(new Cat());
|
||||
//
|
||||
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
|
||||
// the JS code passed a *Cat.
|
||||
//
|
||||
// To solve this issue, we tag every returned value so that we can check what
|
||||
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
|
||||
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
|
||||
//
|
||||
// == Reason 2 ==
|
||||
// Because of prototype inheritance, even "correct" code can be a challenge. For
|
||||
// example, say the above JavaScript is fixed:
|
||||
//
|
||||
// var cat = new Cat();
|
||||
// cat.setOwner(new Owner("Leto"));
|
||||
//
|
||||
// The issue is that setOwner might not expect an *Owner, but rather a
|
||||
// *Person, which is the prototype for Owner. Now our Zig code is expecting
|
||||
// a *Person, but it was (correctly) given an *Owner.
|
||||
// For this reason, we also store the prototype chain.
|
||||
pub const TaggedAnyOpaque = struct {
|
||||
prototype_len: u16,
|
||||
prototype_chain: [*]const PrototypeChainEntry,
|
||||
|
||||
// Ptr to the Zig instance. Between the context where it's called (i.e.
|
||||
// we have the comptime parameter info for all functions), and the index field
|
||||
// we can figure out what type this is.
|
||||
value: *anyopaque,
|
||||
|
||||
// When we're asked to describe an object via the Inspector, we _must_ include
|
||||
// the proper subtype (and description) fields in the returned JSON.
|
||||
// V8 will give us a Value and ask us for the subtype. From the v8.Value we
|
||||
// can get a v8.Object, and from the v8.Object, we can get out TaggedAnyOpaque
|
||||
// which is where we store the subtype.
|
||||
subtype: ?bridge.SubType,
|
||||
};
|
||||
|
||||
pub const PrototypeChainEntry = struct {
|
||||
index: bridge.JsApiLookup.BackingInt,
|
||||
offset: u16, // offset to the _proto field
|
||||
};
|
||||
|
||||
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
|
||||
// included (e.g. in the wpt build).
|
||||
|
||||
@@ -220,10 +483,10 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
|
||||
// it'll call this function to gets its [optional] subtype - which, from V8's
|
||||
// point of view, is an arbitrary string.
|
||||
pub export fn v8_inspector__Client__IMPL__valueSubtype(
|
||||
_: *v8.InspectorClientImpl,
|
||||
c_value: *const v8.Value,
|
||||
_: *v8.c.InspectorClientImpl,
|
||||
c_value: *const v8.C_Value,
|
||||
) callconv(.c) [*c]const u8 {
|
||||
const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;
|
||||
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
|
||||
return if (external_entry.subtype) |st| @tagName(st) else null;
|
||||
}
|
||||
|
||||
@@ -232,19 +495,19 @@ pub export fn v8_inspector__Client__IMPL__valueSubtype(
|
||||
// present, even if it's empty. So if we have a subType for the value, we'll
|
||||
// put an empty description.
|
||||
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
|
||||
_: *v8.InspectorClientImpl,
|
||||
v8_context: *const v8.Context,
|
||||
c_value: *const v8.Value,
|
||||
_: *v8.c.InspectorClientImpl,
|
||||
v8_context: *const v8.C_Context,
|
||||
c_value: *const v8.C_Value,
|
||||
) callconv(.c) [*c]const u8 {
|
||||
_ = v8_context;
|
||||
|
||||
// We _must_ include a non-null description in order for the subtype value
|
||||
// to be included. Besides that, I don't know if the value has any meaning
|
||||
const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;
|
||||
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
|
||||
return if (external_entry.subtype == null) null else "";
|
||||
}
|
||||
|
||||
test "TaggedAnyOpaque" {
|
||||
// If we grow this, fine, but it should be a conscious decision
|
||||
try std.testing.expectEqual(24, @sizeOf(@import("TaggedOpaque.zig")));
|
||||
try std.testing.expectEqual(24, @sizeOf(TaggedAnyOpaque));
|
||||
}
|
||||
|
||||
@@ -17,14 +17,12 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const h5e = @import("html5ever.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const Node = @import("../webapi/Node.zig");
|
||||
const Element = @import("../webapi/Element.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
pub const ParsedNode = struct {
|
||||
node: *Node,
|
||||
@@ -100,29 +98,6 @@ pub fn parse(self: *Parser, html: []const u8) void {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn parseXML(self: *Parser, xml: []const u8) void {
|
||||
h5e.xml5ever_parse_document(
|
||||
xml.ptr,
|
||||
xml.len,
|
||||
&self.container,
|
||||
self,
|
||||
createXMLElementCallback,
|
||||
getDataCallback,
|
||||
appendCallback,
|
||||
parseErrorCallback,
|
||||
popCallback,
|
||||
createCommentCallback,
|
||||
createProcessingInstruction,
|
||||
appendDoctypeToDocument,
|
||||
addAttrsIfMissingCallback,
|
||||
getTemplateContentsCallback,
|
||||
removeFromParentCallback,
|
||||
reparentChildrenCallback,
|
||||
appendBeforeSiblingCallback,
|
||||
appendBasedOnParentNodeCallback,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn parseFragment(self: *Parser, html: []const u8) void {
|
||||
h5e.html5ever_parse_fragment(
|
||||
html.ptr,
|
||||
@@ -164,7 +139,7 @@ pub const Streaming = struct {
|
||||
}
|
||||
|
||||
pub fn start(self: *Streaming) !void {
|
||||
lp.assert(self.handle == null, "Parser.start non-null handle", .{});
|
||||
std.debug.assert(self.handle == null);
|
||||
|
||||
self.handle = h5e.html5ever_streaming_parser_create(
|
||||
&self.parser.container,
|
||||
@@ -227,26 +202,17 @@ fn _popCallback(self: *Parser, node: *Node) !void {
|
||||
}
|
||||
|
||||
fn createElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
|
||||
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .unknown);
|
||||
}
|
||||
|
||||
fn createXMLElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
|
||||
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .xml);
|
||||
}
|
||||
|
||||
fn _createElementCallbackWithDefaultnamespace(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) ?*anyopaque {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
return self._createElementCallback(data, qname, attributes, default_namespace) catch |err| {
|
||||
return self._createElementCallback(data, qname, attributes) catch |err| {
|
||||
self.err = .{ .err = err, .source = .create_element };
|
||||
return null;
|
||||
};
|
||||
}
|
||||
fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) !*anyopaque {
|
||||
fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) !*anyopaque {
|
||||
const page = self.page;
|
||||
const name = qname.local.slice();
|
||||
const namespace_string = qname.ns.slice();
|
||||
const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string);
|
||||
const node = try page.createElementNS(namespace, name, attributes);
|
||||
const namespace = qname.ns.slice();
|
||||
const node = try page.createElement(namespace, name, attributes);
|
||||
|
||||
const pn = try self.arena.create(ParsedNode);
|
||||
pn.* = .{
|
||||
@@ -359,7 +325,7 @@ fn getDataCallback(ctx: *anyopaque) callconv(.c) *anyopaque {
|
||||
const pn: *ParsedNode = @ptrCast(@alignCast(ctx));
|
||||
// For non-elements, data is null. But, we expect this to only ever
|
||||
// be called for elements.
|
||||
lp.assert(pn.data != null, "Parser.getDataCallback null data", .{});
|
||||
std.debug.assert(pn.data != null);
|
||||
return pn.data.?;
|
||||
}
|
||||
|
||||
@@ -374,17 +340,6 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !
|
||||
switch (node_or_text.toUnion()) {
|
||||
.node => |cpn| {
|
||||
const child = getNode(cpn);
|
||||
if (child._parent) |previous_parent| {
|
||||
// html5ever says this can't happen, but we might be screwing up
|
||||
// the node on our side. We shouldn't be, but we're seeing this
|
||||
// in the wild, and I'm not sure why. In debug, let's crash so
|
||||
// we can try to figure it out. In release, let's disconnect
|
||||
// the child first.
|
||||
if (comptime IS_DEBUG) {
|
||||
unreachable;
|
||||
}
|
||||
self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });
|
||||
}
|
||||
try self.page.appendNew(parent, .{ .node = child });
|
||||
},
|
||||
.text => |txt| try self.page.appendNew(parent, .{ .text = txt }),
|
||||
|
||||
@@ -171,24 +171,3 @@ pub const NodeOrText = extern struct {
|
||||
text: []const u8,
|
||||
};
|
||||
};
|
||||
|
||||
pub extern "c" fn xml5ever_parse_document(
|
||||
html: [*c]const u8,
|
||||
len: usize,
|
||||
doc: *anyopaque,
|
||||
ctx: *anyopaque,
|
||||
createElementCallback: *const fn (ctx: *anyopaque, data: *anyopaque, QualName, AttributeIterator) callconv(.c) ?*anyopaque,
|
||||
elemNameCallback: *const fn (node_ref: *anyopaque) callconv(.c) *anyopaque,
|
||||
appendCallback: *const fn (ctx: *anyopaque, parent_ref: *anyopaque, NodeOrText) callconv(.c) void,
|
||||
parseErrorCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) void,
|
||||
popCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void,
|
||||
createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque,
|
||||
createProcessingInstruction: *const fn (ctx: *anyopaque, StringSlice, StringSlice) callconv(.c) ?*anyopaque,
|
||||
appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void,
|
||||
addAttrsIfMissingCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque, AttributeIterator) callconv(.c) void,
|
||||
getTemplateContentsCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) ?*anyopaque,
|
||||
removeFromParentCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) void,
|
||||
reparentChildrenCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void,
|
||||
appendBeforeSiblingCallback: *const fn (ctx: *anyopaque, sibling_ref: *anyopaque, NodeOrText) callconv(.c) void,
|
||||
appendBasedOnParentNodeCallback: *const fn (ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, NodeOrText) callconv(.c) void,
|
||||
) void;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -16,6 +16,8 @@
|
||||
// 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");
|
||||
|
||||
// Gets the Parent of child.
|
||||
// HtmlElement.of(script) -> *HTMLElement
|
||||
pub fn Struct(comptime T: type) type {
|
||||
@@ -26,3 +28,37 @@ pub fn Struct(comptime T: type) type {
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
// Creates an enum of N enums. Doesn't perserve their underlying integer
|
||||
pub fn mergeEnums(comptime enums: []const type) type {
|
||||
const field_count = blk: {
|
||||
var count: usize = 0;
|
||||
inline for (enums) |e| {
|
||||
count += @typeInfo(e).@"enum".fields.len;
|
||||
}
|
||||
break :blk count;
|
||||
};
|
||||
|
||||
var i: usize = 0;
|
||||
var fields: [field_count]std.builtin.Type.EnumField = undefined;
|
||||
for (enums) |e| {
|
||||
for (@typeInfo(e).@"enum".fields) |f| {
|
||||
fields[i] = .{
|
||||
.name = f.name,
|
||||
.value = i,
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return @Type(.{ .@"enum" = .{
|
||||
.decls = &.{},
|
||||
.tag_type = blk: {
|
||||
if (field_count <= std.math.maxInt(u8)) break :blk u8;
|
||||
if (field_count <= std.math.maxInt(u16)) break :blk u16;
|
||||
unreachable;
|
||||
},
|
||||
.fields = &fields,
|
||||
.is_exhaustive = true,
|
||||
} });
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=CanvasRenderingContext2D>
|
||||
{
|
||||
const element = document.createElement("canvas");
|
||||
const ctx = element.getContext("2d");
|
||||
testing.expectEqual(true, ctx instanceof CanvasRenderingContext2D);
|
||||
// We can't really test this but let's try to call it at least.
|
||||
ctx.fillRect(0, 0, 0, 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=CanvasRenderingContext2D#fillStyle>
|
||||
{
|
||||
const element = document.createElement("canvas");
|
||||
const ctx = element.getContext("2d");
|
||||
|
||||
// Black by default.
|
||||
testing.expectEqual(ctx.fillStyle, "#000000");
|
||||
ctx.fillStyle = "red";
|
||||
testing.expectEqual(ctx.fillStyle, "#ff0000");
|
||||
ctx.fillStyle = "rebeccapurple";
|
||||
testing.expectEqual(ctx.fillStyle, "#663399");
|
||||
// No changes made if color is invalid.
|
||||
ctx.fillStyle = "invalid-color";
|
||||
testing.expectEqual(ctx.fillStyle, "#663399");
|
||||
ctx.fillStyle = "#fc0";
|
||||
testing.expectEqual(ctx.fillStyle, "#ffcc00");
|
||||
ctx.fillStyle = "#ff0000";
|
||||
testing.expectEqual(ctx.fillStyle, "#ff0000");
|
||||
ctx.fillStyle = "#fF00000F";
|
||||
testing.expectEqual(ctx.fillStyle, "rgba(255, 0, 0, 0.06)");
|
||||
}
|
||||
</script>
|
||||
@@ -1,87 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=WebGLRenderingContext#getSupportedExtensions>
|
||||
{
|
||||
const element = document.createElement("canvas");
|
||||
const ctx = element.getContext("webgl");
|
||||
testing.expectEqual(true, ctx instanceof WebGLRenderingContext);
|
||||
|
||||
const supportedExtensions = ctx.getSupportedExtensions();
|
||||
// The order Chrome prefer.
|
||||
const expectedExtensions = [
|
||||
"ANGLE_instanced_arrays",
|
||||
"EXT_blend_minmax",
|
||||
"EXT_clip_control",
|
||||
"EXT_color_buffer_half_float",
|
||||
"EXT_depth_clamp",
|
||||
"EXT_disjoint_timer_query",
|
||||
"EXT_float_blend",
|
||||
"EXT_frag_depth",
|
||||
"EXT_polygon_offset_clamp",
|
||||
"EXT_shader_texture_lod",
|
||||
"EXT_texture_compression_bptc",
|
||||
"EXT_texture_compression_rgtc",
|
||||
"EXT_texture_filter_anisotropic",
|
||||
"EXT_texture_mirror_clamp_to_edge",
|
||||
"EXT_sRGB",
|
||||
"KHR_parallel_shader_compile",
|
||||
"OES_element_index_uint",
|
||||
"OES_fbo_render_mipmap",
|
||||
"OES_standard_derivatives",
|
||||
"OES_texture_float",
|
||||
"OES_texture_float_linear",
|
||||
"OES_texture_half_float",
|
||||
"OES_texture_half_float_linear",
|
||||
"OES_vertex_array_object",
|
||||
"WEBGL_blend_func_extended",
|
||||
"WEBGL_color_buffer_float",
|
||||
"WEBGL_compressed_texture_astc",
|
||||
"WEBGL_compressed_texture_etc",
|
||||
"WEBGL_compressed_texture_etc1",
|
||||
"WEBGL_compressed_texture_pvrtc",
|
||||
"WEBGL_compressed_texture_s3tc",
|
||||
"WEBGL_compressed_texture_s3tc_srgb",
|
||||
"WEBGL_debug_renderer_info",
|
||||
"WEBGL_debug_shaders",
|
||||
"WEBGL_depth_texture",
|
||||
"WEBGL_draw_buffers",
|
||||
"WEBGL_lose_context",
|
||||
"WEBGL_multi_draw",
|
||||
"WEBGL_polygon_mode"
|
||||
];
|
||||
|
||||
testing.expectEqual(expectedExtensions.length, supportedExtensions.length);
|
||||
for (let i = 0; i < expectedExtensions.length; i++) {
|
||||
testing.expectEqual(expectedExtensions[i], supportedExtensions[i]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=WebGLRenderingCanvas#getExtension>
|
||||
// WEBGL_debug_renderer_info
|
||||
{
|
||||
const element = document.createElement("canvas");
|
||||
const ctx = element.getContext("webgl");
|
||||
const rendererInfo = ctx.getExtension("WEBGL_debug_renderer_info");
|
||||
testing.expectEqual(true, rendererInfo instanceof WEBGL_debug_renderer_info);
|
||||
|
||||
const { UNMASKED_VENDOR_WEBGL, UNMASKED_RENDERER_WEBGL } = rendererInfo;
|
||||
testing.expectEqual(UNMASKED_VENDOR_WEBGL, 0x9245);
|
||||
testing.expectEqual(UNMASKED_RENDERER_WEBGL, 0x9246);
|
||||
|
||||
testing.expectEqual("", ctx.getParameter(UNMASKED_VENDOR_WEBGL));
|
||||
testing.expectEqual("", ctx.getParameter(UNMASKED_RENDERER_WEBGL));
|
||||
}
|
||||
|
||||
// WEBGL_lose_context
|
||||
{
|
||||
const element = document.createElement("canvas");
|
||||
const ctx = element.getContext("webgl");
|
||||
const loseContext = ctx.getExtension("WEBGL_lose_context");
|
||||
testing.expectEqual(true, loseContext instanceof WEBGL_lose_context);
|
||||
|
||||
loseContext.loseContext();
|
||||
loseContext.restoreContext();
|
||||
}
|
||||
</script>
|
||||
@@ -201,8 +201,8 @@ cdataClassName<!DOCTYPE html>
|
||||
root.appendChild(cdata);
|
||||
root.appendChild(elem2);
|
||||
|
||||
testing.expectEqual('last', cdata.nextElementSibling.tagName);
|
||||
testing.expectEqual('first', cdata.previousElementSibling.tagName);
|
||||
testing.expectEqual('LAST', cdata.nextElementSibling.tagName);
|
||||
testing.expectEqual('FIRST', cdata.previousElementSibling.tagName);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Page</h1>
|
||||
<nav>
|
||||
<a href="/page1" id="link1">First Link</a>
|
||||
<a href="/page2" id="link2">Second Link</a>
|
||||
</nav>
|
||||
<form id="testForm" action="/submit" method="post">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" placeholder="Enter username">
|
||||
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" placeholder="Enter email">
|
||||
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password">
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,28 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id="time">
|
||||
// should not crash
|
||||
console.time();
|
||||
console.timeLog();
|
||||
console.timeEnd();
|
||||
|
||||
console.time("test");
|
||||
console.timeLog("test");
|
||||
console.timeEnd("test");
|
||||
|
||||
testing.expectEqual(true, true);
|
||||
</script>
|
||||
|
||||
<script id="count">
|
||||
// should not crash
|
||||
console.count();
|
||||
console.count();
|
||||
console.countReset();
|
||||
|
||||
console.count("test");
|
||||
console.count("test");
|
||||
console.countReset("test");
|
||||
|
||||
testing.expectEqual(true, true);
|
||||
</script>
|
||||
@@ -54,68 +54,3 @@
|
||||
const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
testing.expectEqual(true, regex.test(uuid));
|
||||
</script> -->
|
||||
|
||||
<script id=SubtleCrypto>
|
||||
testing.expectEqual(true, crypto.subtle instanceof SubtleCrypto);
|
||||
</script>
|
||||
|
||||
<script id=sign-and-verify-hmac>
|
||||
testing.async(async () => {
|
||||
let key = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: "HMAC",
|
||||
hash: { name: "SHA-512" },
|
||||
},
|
||||
true,
|
||||
["sign", "verify"],
|
||||
);
|
||||
|
||||
testing.expectEqual(true, key instanceof CryptoKey);
|
||||
|
||||
const raw = await crypto.subtle.exportKey("raw", key);
|
||||
testing.expectEqual(128, raw.byteLength);
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
key,
|
||||
encoder.encode("Hello, world!")
|
||||
);
|
||||
|
||||
testing.expectEqual(true, signature instanceof ArrayBuffer);
|
||||
|
||||
const result = await window.crypto.subtle.verify(
|
||||
{ name: "HMAC" },
|
||||
key,
|
||||
signature,
|
||||
encoder.encode("Hello, world!")
|
||||
);
|
||||
|
||||
testing.expectEqual(true, result);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=derive-shared-key-x25519>
|
||||
testing.async(async () => {
|
||||
const { privateKey, publicKey } = await crypto.subtle.generateKey(
|
||||
{ name: "X25519" },
|
||||
true,
|
||||
["deriveBits"],
|
||||
);
|
||||
|
||||
testing.expectEqual(true, privateKey instanceof CryptoKey);
|
||||
testing.expectEqual(true, publicKey instanceof CryptoKey);
|
||||
|
||||
const sharedKey = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: "X25519",
|
||||
public: publicKey,
|
||||
},
|
||||
privateKey,
|
||||
128,
|
||||
);
|
||||
|
||||
testing.expectEqual(16, sharedKey.byteLength);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -20,10 +20,8 @@
|
||||
{
|
||||
testing.expectEqual('\\30 abc', CSS.escape('0abc'));
|
||||
testing.expectEqual('\\31 23', CSS.escape('123'));
|
||||
testing.expectEqual('\\-', CSS.escape('-'));
|
||||
testing.expectEqual('-test', CSS.escape('-test'));
|
||||
testing.expectEqual('--test', CSS.escape('--test'));
|
||||
testing.expectEqual('-\\33 ', CSS.escape('-3'));
|
||||
testing.expectEqual('\\-test', CSS.escape('-test'));
|
||||
testing.expectEqual('\\--test', CSS.escape('--test'));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -27,329 +27,329 @@
|
||||
customElements.define('my-early', MyEarly);
|
||||
testing.expectEqual(true, early.upgraded);
|
||||
testing.expectEqual(1, constructorCalled);
|
||||
// testing.expectEqual(1, connectedCalled);
|
||||
testing.expectEqual(1, connectedCalled);
|
||||
}
|
||||
|
||||
// {
|
||||
// let order = [];
|
||||
{
|
||||
let order = [];
|
||||
|
||||
// class UpgradeParent extends HTMLElement {
|
||||
// constructor() {
|
||||
// super();
|
||||
// order.push('parent-constructor');
|
||||
// }
|
||||
|
||||
// connectedCallback() {
|
||||
// order.push('parent-connected');
|
||||
// }
|
||||
// }
|
||||
|
||||
// class UpgradeChild extends HTMLElement {
|
||||
// constructor() {
|
||||
// super();
|
||||
// order.push('child-constructor');
|
||||
// }
|
||||
|
||||
// connectedCallback() {
|
||||
// order.push('child-connected');
|
||||
// }
|
||||
// }
|
||||
class UpgradeParent extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
order.push('parent-constructor');
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
order.push('parent-connected');
|
||||
}
|
||||
}
|
||||
|
||||
class UpgradeChild extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
order.push('child-constructor');
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
order.push('child-connected');
|
||||
}
|
||||
}
|
||||
|
||||
// const container = document.createElement('div');
|
||||
// container.innerHTML = '<upgrade-parent><upgrade-child></upgrade-child></upgrade-parent>';
|
||||
// document.body.appendChild(container);
|
||||
|
||||
// testing.expectEqual(0, order.length);
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<upgrade-parent><upgrade-child></upgrade-child></upgrade-parent>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
testing.expectEqual(0, order.length);
|
||||
|
||||
// customElements.define('upgrade-parent', UpgradeParent);
|
||||
// testing.expectEqual(2, order.length);
|
||||
// testing.expectEqual('parent-constructor', order[0]);
|
||||
// testing.expectEqual('parent-connected', order[1]);
|
||||
|
||||
// customElements.define('upgrade-child', UpgradeChild);
|
||||
// testing.expectEqual(4, order.length);
|
||||
// testing.expectEqual('child-constructor', order[2]);
|
||||
// testing.expectEqual('child-connected', order[3]);
|
||||
// }
|
||||
customElements.define('upgrade-parent', UpgradeParent);
|
||||
testing.expectEqual(2, order.length);
|
||||
testing.expectEqual('parent-constructor', order[0]);
|
||||
testing.expectEqual('parent-connected', order[1]);
|
||||
|
||||
customElements.define('upgrade-child', UpgradeChild);
|
||||
testing.expectEqual(4, order.length);
|
||||
testing.expectEqual('child-constructor', order[2]);
|
||||
testing.expectEqual('child-connected', order[3]);
|
||||
}
|
||||
|
||||
// {
|
||||
// let connectedCalled = 0;
|
||||
{
|
||||
let connectedCalled = 0;
|
||||
|
||||
// class DetachedUpgrade extends HTMLElement {
|
||||
// connectedCallback() {
|
||||
// connectedCalled++;
|
||||
// }
|
||||
// }
|
||||
|
||||
// const container = document.createElement('div');
|
||||
// container.innerHTML = '<detached-upgrade></detached-upgrade>';
|
||||
|
||||
// testing.expectEqual(0, connectedCalled);
|
||||
|
||||
// customElements.define('detached-upgrade', DetachedUpgrade);
|
||||
// testing.expectEqual(0, connectedCalled);
|
||||
|
||||
// document.body.appendChild(container);
|
||||
// testing.expectEqual(1, connectedCalled);
|
||||
// }
|
||||
|
||||
// {
|
||||
// let constructorCalled = 0;
|
||||
// let connectedCalled = 0;
|
||||
|
||||
// class ManualUpgrade extends HTMLElement {
|
||||
// constructor() {
|
||||
// super();
|
||||
// constructorCalled++;
|
||||
// this.manuallyUpgraded = true;
|
||||
// }
|
||||
|
||||
// connectedCallback() {
|
||||
// connectedCalled++;
|
||||
// }
|
||||
// }
|
||||
class DetachedUpgrade extends HTMLElement {
|
||||
connectedCallback() {
|
||||
connectedCalled++;
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<detached-upgrade></detached-upgrade>';
|
||||
|
||||
testing.expectEqual(0, connectedCalled);
|
||||
|
||||
customElements.define('detached-upgrade', DetachedUpgrade);
|
||||
testing.expectEqual(0, connectedCalled);
|
||||
|
||||
document.body.appendChild(container);
|
||||
testing.expectEqual(1, connectedCalled);
|
||||
}
|
||||
|
||||
{
|
||||
let constructorCalled = 0;
|
||||
let connectedCalled = 0;
|
||||
|
||||
class ManualUpgrade extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
constructorCalled++;
|
||||
this.manuallyUpgraded = true;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
connectedCalled++;
|
||||
}
|
||||
}
|
||||
|
||||
// customElements.define('manual-upgrade', ManualUpgrade);
|
||||
customElements.define('manual-upgrade', ManualUpgrade);
|
||||
|
||||
// const container = document.createElement('div');
|
||||
// container.innerHTML = '<manual-upgrade id="m1"><manual-upgrade id="m2"></manual-upgrade></manual-upgrade>';
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<manual-upgrade id="m1"><manual-upgrade id="m2"></manual-upgrade></manual-upgrade>';
|
||||
|
||||
// testing.expectEqual(2, constructorCalled);
|
||||
// testing.expectEqual(0, connectedCalled);
|
||||
testing.expectEqual(2, constructorCalled);
|
||||
testing.expectEqual(0, connectedCalled);
|
||||
|
||||
// customElements.upgrade(container);
|
||||
customElements.upgrade(container);
|
||||
|
||||
// testing.expectEqual(2, constructorCalled);
|
||||
// testing.expectEqual(0, connectedCalled);
|
||||
|
||||
// const m1 = container.querySelector('#m1');
|
||||
// const m2 = container.querySelector('#m2');
|
||||
// testing.expectEqual(true, m1.manuallyUpgraded);
|
||||
// testing.expectEqual(true, m2.manuallyUpgraded);
|
||||
|
||||
// document.body.appendChild(container);
|
||||
// testing.expectEqual(2, connectedCalled);
|
||||
// }
|
||||
|
||||
// {
|
||||
// let alreadyUpgradedCalled = 0;
|
||||
|
||||
// class AlreadyUpgraded extends HTMLElement {
|
||||
// constructor() {
|
||||
// super();
|
||||
// alreadyUpgradedCalled++;
|
||||
// }
|
||||
// }
|
||||
testing.expectEqual(2, constructorCalled);
|
||||
testing.expectEqual(0, connectedCalled);
|
||||
|
||||
const m1 = container.querySelector('#m1');
|
||||
const m2 = container.querySelector('#m2');
|
||||
testing.expectEqual(true, m1.manuallyUpgraded);
|
||||
testing.expectEqual(true, m2.manuallyUpgraded);
|
||||
|
||||
document.body.appendChild(container);
|
||||
testing.expectEqual(2, connectedCalled);
|
||||
}
|
||||
|
||||
{
|
||||
let alreadyUpgradedCalled = 0;
|
||||
|
||||
class AlreadyUpgraded extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
alreadyUpgradedCalled++;
|
||||
}
|
||||
}
|
||||
|
||||
// const elem = document.createElement('div');
|
||||
// elem.innerHTML = '<already-upgraded></already-upgraded>';
|
||||
// document.body.appendChild(elem);
|
||||
const elem = document.createElement('div');
|
||||
elem.innerHTML = '<already-upgraded></already-upgraded>';
|
||||
document.body.appendChild(elem);
|
||||
|
||||
// customElements.define('already-upgraded', AlreadyUpgraded);
|
||||
// testing.expectEqual(1, alreadyUpgradedCalled);
|
||||
customElements.define('already-upgraded', AlreadyUpgraded);
|
||||
testing.expectEqual(1, alreadyUpgradedCalled);
|
||||
|
||||
// customElements.upgrade(elem);
|
||||
// testing.expectEqual(1, alreadyUpgradedCalled);
|
||||
// }
|
||||
customElements.upgrade(elem);
|
||||
testing.expectEqual(1, alreadyUpgradedCalled);
|
||||
}
|
||||
|
||||
// {
|
||||
// let attributeChangedCalls = [];
|
||||
{
|
||||
let attributeChangedCalls = [];
|
||||
|
||||
// class UpgradeWithAttrs extends HTMLElement {
|
||||
// static get observedAttributes() {
|
||||
// return ['data-foo', 'data-bar'];
|
||||
// }
|
||||
class UpgradeWithAttrs extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['data-foo', 'data-bar'];
|
||||
}
|
||||
|
||||
// attributeChangedCallback(name, oldValue, newValue) {
|
||||
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||
// }
|
||||
// }
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
// const container = document.createElement('div');
|
||||
// container.innerHTML = '<upgrade-with-attrs data-foo="hello" data-bar="world"></upgrade-with-attrs>';
|
||||
// document.body.appendChild(container);
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<upgrade-with-attrs data-foo="hello" data-bar="world"></upgrade-with-attrs>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// testing.expectEqual(0, attributeChangedCalls.length);
|
||||
testing.expectEqual(0, attributeChangedCalls.length);
|
||||
|
||||
// customElements.define('upgrade-with-attrs', UpgradeWithAttrs);
|
||||
customElements.define('upgrade-with-attrs', UpgradeWithAttrs);
|
||||
|
||||
// testing.expectEqual(2, attributeChangedCalls.length);
|
||||
// testing.expectEqual('data-foo', attributeChangedCalls[0].name);
|
||||
// testing.expectEqual(null, attributeChangedCalls[0].oldValue);
|
||||
// testing.expectEqual('hello', attributeChangedCalls[0].newValue);
|
||||
// testing.expectEqual('data-bar', attributeChangedCalls[1].name);
|
||||
// testing.expectEqual(null, attributeChangedCalls[1].oldValue);
|
||||
// testing.expectEqual('world', attributeChangedCalls[1].newValue);
|
||||
// }
|
||||
testing.expectEqual(2, attributeChangedCalls.length);
|
||||
testing.expectEqual('data-foo', attributeChangedCalls[0].name);
|
||||
testing.expectEqual(null, attributeChangedCalls[0].oldValue);
|
||||
testing.expectEqual('hello', attributeChangedCalls[0].newValue);
|
||||
testing.expectEqual('data-bar', attributeChangedCalls[1].name);
|
||||
testing.expectEqual(null, attributeChangedCalls[1].oldValue);
|
||||
testing.expectEqual('world', attributeChangedCalls[1].newValue);
|
||||
}
|
||||
|
||||
// {
|
||||
// let attributeChangedCalls = [];
|
||||
// let connectedCalls = 0;
|
||||
{
|
||||
let attributeChangedCalls = [];
|
||||
let connectedCalls = 0;
|
||||
|
||||
// class DetachedWithAttrs extends HTMLElement {
|
||||
// static get observedAttributes() {
|
||||
// return ['foo'];
|
||||
// }
|
||||
class DetachedWithAttrs extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['foo'];
|
||||
}
|
||||
|
||||
// attributeChangedCallback(name, oldValue, newValue) {
|
||||
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||
// }
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
|
||||
// connectedCallback() {
|
||||
// connectedCalls++;
|
||||
// }
|
||||
// }
|
||||
connectedCallback() {
|
||||
connectedCalls++;
|
||||
}
|
||||
}
|
||||
|
||||
// const container = document.createElement('div');
|
||||
// container.innerHTML = '<detached-with-attrs foo="bar"></detached-with-attrs>';
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<detached-with-attrs foo="bar"></detached-with-attrs>';
|
||||
|
||||
// testing.expectEqual(0, attributeChangedCalls.length);
|
||||
testing.expectEqual(0, attributeChangedCalls.length);
|
||||
|
||||
// customElements.define('detached-with-attrs', DetachedWithAttrs);
|
||||
customElements.define('detached-with-attrs', DetachedWithAttrs);
|
||||
|
||||
// testing.expectEqual(0, attributeChangedCalls.length);
|
||||
// testing.expectEqual(0, connectedCalls);
|
||||
testing.expectEqual(0, attributeChangedCalls.length);
|
||||
testing.expectEqual(0, connectedCalls);
|
||||
|
||||
// document.body.appendChild(container);
|
||||
document.body.appendChild(container);
|
||||
|
||||
// testing.expectEqual(1, attributeChangedCalls.length);
|
||||
// testing.expectEqual('foo', attributeChangedCalls[0].name);
|
||||
// testing.expectEqual(null, attributeChangedCalls[0].oldValue);
|
||||
// testing.expectEqual('bar', attributeChangedCalls[0].newValue);
|
||||
// testing.expectEqual(1, connectedCalls);
|
||||
// }
|
||||
testing.expectEqual(1, attributeChangedCalls.length);
|
||||
testing.expectEqual('foo', attributeChangedCalls[0].name);
|
||||
testing.expectEqual(null, attributeChangedCalls[0].oldValue);
|
||||
testing.expectEqual('bar', attributeChangedCalls[0].newValue);
|
||||
testing.expectEqual(1, connectedCalls);
|
||||
}
|
||||
|
||||
// {
|
||||
// let attributeChangedCalls = [];
|
||||
// let constructorCalled = 0;
|
||||
{
|
||||
let attributeChangedCalls = [];
|
||||
let constructorCalled = 0;
|
||||
|
||||
// class ManualUpgradeWithAttrs extends HTMLElement {
|
||||
// static get observedAttributes() {
|
||||
// return ['x', 'y'];
|
||||
// }
|
||||
class ManualUpgradeWithAttrs extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['x', 'y'];
|
||||
}
|
||||
|
||||
// constructor() {
|
||||
// super();
|
||||
// constructorCalled++;
|
||||
// }
|
||||
constructor() {
|
||||
super();
|
||||
constructorCalled++;
|
||||
}
|
||||
|
||||
// attributeChangedCallback(name, oldValue, newValue) {
|
||||
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||
// }
|
||||
// }
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
// customElements.define('manual-upgrade-with-attrs', ManualUpgradeWithAttrs);
|
||||
customElements.define('manual-upgrade-with-attrs', ManualUpgradeWithAttrs);
|
||||
|
||||
// const container = document.createElement('div');
|
||||
// container.innerHTML = '<manual-upgrade-with-attrs x="1" y="2"></manual-upgrade-with-attrs>';
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<manual-upgrade-with-attrs x="1" y="2"></manual-upgrade-with-attrs>';
|
||||
|
||||
// testing.expectEqual(1, constructorCalled);
|
||||
// testing.expectEqual(2, attributeChangedCalls.length);
|
||||
testing.expectEqual(1, constructorCalled);
|
||||
testing.expectEqual(2, attributeChangedCalls.length);
|
||||
|
||||
// const elem = container.querySelector('manual-upgrade-with-attrs');
|
||||
// elem.setAttribute('z', '3');
|
||||
const elem = container.querySelector('manual-upgrade-with-attrs');
|
||||
elem.setAttribute('z', '3');
|
||||
|
||||
// customElements.upgrade(container);
|
||||
customElements.upgrade(container);
|
||||
|
||||
// testing.expectEqual(1, constructorCalled);
|
||||
// testing.expectEqual(2, attributeChangedCalls.length);
|
||||
// }
|
||||
testing.expectEqual(1, constructorCalled);
|
||||
testing.expectEqual(2, attributeChangedCalls.length);
|
||||
}
|
||||
|
||||
// {
|
||||
// let attributeChangedCalls = [];
|
||||
{
|
||||
let attributeChangedCalls = [];
|
||||
|
||||
// class MixedAttrs extends HTMLElement {
|
||||
// static get observedAttributes() {
|
||||
// return ['watched'];
|
||||
// }
|
||||
class MixedAttrs extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['watched'];
|
||||
}
|
||||
|
||||
// attributeChangedCallback(name, oldValue, newValue) {
|
||||
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||
// }
|
||||
// }
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
// const container = document.createElement('div');
|
||||
// container.innerHTML = '<mixed-attrs watched="yes" ignored="no" also-ignored="maybe"></mixed-attrs>';
|
||||
// document.body.appendChild(container);
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<mixed-attrs watched="yes" ignored="no" also-ignored="maybe"></mixed-attrs>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// testing.expectEqual(0, attributeChangedCalls.length);
|
||||
testing.expectEqual(0, attributeChangedCalls.length);
|
||||
|
||||
// customElements.define('mixed-attrs', MixedAttrs);
|
||||
customElements.define('mixed-attrs', MixedAttrs);
|
||||
|
||||
// testing.expectEqual(1, attributeChangedCalls.length);
|
||||
// testing.expectEqual('watched', attributeChangedCalls[0].name);
|
||||
// testing.expectEqual('yes', attributeChangedCalls[0].newValue);
|
||||
// }
|
||||
testing.expectEqual(1, attributeChangedCalls.length);
|
||||
testing.expectEqual('watched', attributeChangedCalls[0].name);
|
||||
testing.expectEqual('yes', attributeChangedCalls[0].newValue);
|
||||
}
|
||||
|
||||
// {
|
||||
// let attributeChangedCalls = [];
|
||||
{
|
||||
let attributeChangedCalls = [];
|
||||
|
||||
// class EmptyAttr extends HTMLElement {
|
||||
// static get observedAttributes() {
|
||||
// return ['empty', 'non-empty'];
|
||||
// }
|
||||
class EmptyAttr extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['empty', 'non-empty'];
|
||||
}
|
||||
|
||||
// attributeChangedCallback(name, oldValue, newValue) {
|
||||
// attributeChangedCalls.push({ name, oldValue, newValue });
|
||||
// }
|
||||
// }
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
attributeChangedCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
// const container = document.createElement('div');
|
||||
// container.innerHTML = '<empty-attr empty="" non-empty="value"></empty-attr>';
|
||||
// document.body.appendChild(container);
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<empty-attr empty="" non-empty="value"></empty-attr>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// customElements.define('empty-attr', EmptyAttr);
|
||||
customElements.define('empty-attr', EmptyAttr);
|
||||
|
||||
// testing.expectEqual(2, attributeChangedCalls.length);
|
||||
// testing.expectEqual('empty', attributeChangedCalls[0].name);
|
||||
// testing.expectEqual('', attributeChangedCalls[0].newValue);
|
||||
// testing.expectEqual('non-empty', attributeChangedCalls[1].name);
|
||||
// testing.expectEqual('value', attributeChangedCalls[1].newValue);
|
||||
// }
|
||||
testing.expectEqual(2, attributeChangedCalls.length);
|
||||
testing.expectEqual('empty', attributeChangedCalls[0].name);
|
||||
testing.expectEqual('', attributeChangedCalls[0].newValue);
|
||||
testing.expectEqual('non-empty', attributeChangedCalls[1].name);
|
||||
testing.expectEqual('value', attributeChangedCalls[1].newValue);
|
||||
}
|
||||
|
||||
// {
|
||||
// let parentCalls = [];
|
||||
// let childCalls = [];
|
||||
{
|
||||
let parentCalls = [];
|
||||
let childCalls = [];
|
||||
|
||||
// class NestedParent extends HTMLElement {
|
||||
// static get observedAttributes() {
|
||||
// return ['parent-attr'];
|
||||
// }
|
||||
class NestedParent extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['parent-attr'];
|
||||
}
|
||||
|
||||
// attributeChangedCallback(name, oldValue, newValue) {
|
||||
// parentCalls.push({ name, oldValue, newValue });
|
||||
// }
|
||||
// }
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
parentCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
// class NestedChild extends HTMLElement {
|
||||
// static get observedAttributes() {
|
||||
// return ['child-attr'];
|
||||
// }
|
||||
class NestedChild extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['child-attr'];
|
||||
}
|
||||
|
||||
// attributeChangedCallback(name, oldValue, newValue) {
|
||||
// childCalls.push({ name, oldValue, newValue });
|
||||
// }
|
||||
// }
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
childCalls.push({ name, oldValue, newValue });
|
||||
}
|
||||
}
|
||||
|
||||
// const container = document.createElement('div');
|
||||
// container.innerHTML = '<nested-parent parent-attr="p"><nested-child child-attr="c"></nested-child></nested-parent>';
|
||||
// document.body.appendChild(container);
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<nested-parent parent-attr="p"><nested-child child-attr="c"></nested-child></nested-parent>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// testing.expectEqual(0, parentCalls.length);
|
||||
// testing.expectEqual(0, childCalls.length);
|
||||
testing.expectEqual(0, parentCalls.length);
|
||||
testing.expectEqual(0, childCalls.length);
|
||||
|
||||
// customElements.define('nested-parent', NestedParent);
|
||||
customElements.define('nested-parent', NestedParent);
|
||||
|
||||
// testing.expectEqual(1, parentCalls.length);
|
||||
// testing.expectEqual('parent-attr', parentCalls[0].name);
|
||||
// testing.expectEqual('p', parentCalls[0].newValue);
|
||||
// testing.expectEqual(0, childCalls.length);
|
||||
testing.expectEqual(1, parentCalls.length);
|
||||
testing.expectEqual('parent-attr', parentCalls[0].name);
|
||||
testing.expectEqual('p', parentCalls[0].newValue);
|
||||
testing.expectEqual(0, childCalls.length);
|
||||
|
||||
// customElements.define('nested-child', NestedChild);
|
||||
customElements.define('nested-child', NestedChild);
|
||||
|
||||
// testing.expectEqual(1, parentCalls.length);
|
||||
// testing.expectEqual(1, childCalls.length);
|
||||
// testing.expectEqual('child-attr', childCalls[0].name);
|
||||
// testing.expectEqual('c', childCalls[0].newValue);
|
||||
// }
|
||||
testing.expectEqual(1, parentCalls.length);
|
||||
testing.expectEqual(1, childCalls.length);
|
||||
testing.expectEqual('child-attr', childCalls[0].name);
|
||||
testing.expectEqual('c', childCalls[0].newValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,18 +2,12 @@
|
||||
<body></body>
|
||||
<script src="../testing.js"></script>
|
||||
<script id=createElement>
|
||||
testing.expectEqual(1, document.createElement.length);
|
||||
|
||||
const div1 = document.createElement('div');
|
||||
testing.expectEqual(true, div1 instanceof HTMLDivElement);
|
||||
testing.expectEqual("DIV", div1.tagName);
|
||||
div1.id = "hello";
|
||||
const div = document.createElement('div');
|
||||
testing.expectEqual("DIV", div.tagName);
|
||||
div.id = "hello";
|
||||
testing.expectEqual(null, $('#hello'));
|
||||
|
||||
const div2 = document.createElement('DIV');
|
||||
testing.expectEqual(true, div2 instanceof HTMLDivElement);
|
||||
|
||||
document.getElementsByTagName('body')[0].appendChild(div1);
|
||||
testing.expectEqual(div1, $('#hello'));
|
||||
document.getElementsByTagName('body')[0].appendChild(div);
|
||||
testing.expectEqual(div, $('#hello'));
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,15 +2,9 @@
|
||||
<body></body>
|
||||
<script src="../testing.js"></script>
|
||||
<script id=createElementNS>
|
||||
const htmlDiv1 = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
||||
testing.expectEqual('DIV', htmlDiv1.tagName);
|
||||
testing.expectEqual(true, htmlDiv1 instanceof HTMLDivElement);
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv1.namespaceURI);
|
||||
|
||||
const htmlDiv2 = document.createElementNS('http://www.w3.org/1999/xhtml', 'DIV');
|
||||
testing.expectEqual('DIV', htmlDiv2.tagName);
|
||||
testing.expectEqual(true, htmlDiv2 instanceof HTMLDivElement);
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv2.namespaceURI);
|
||||
const htmlDiv = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
||||
testing.expectEqual('DIV', htmlDiv.tagName);
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv.namespaceURI);
|
||||
|
||||
const svgRect = document.createElementNS('http://www.w3.org/2000/svg', 'RecT');
|
||||
testing.expectEqual('RecT', svgRect.tagName);
|
||||
@@ -25,13 +19,12 @@
|
||||
testing.expectEqual('http://www.w3.org/XML/1998/namespace', xmlElement.namespaceURI);
|
||||
|
||||
const nullNsElement = document.createElementNS(null, 'span');
|
||||
testing.expectEqual('span', nullNsElement.tagName);
|
||||
testing.expectEqual(null, nullNsElement.namespaceURI);
|
||||
testing.expectEqual('SPAN', nullNsElement.tagName);
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', nullNsElement.namespaceURI);
|
||||
|
||||
const unknownNsElement = document.createElementNS('http://example.com/unknown', 'custom');
|
||||
testing.expectEqual('custom', unknownNsElement.tagName);
|
||||
// Should be http://example.com/unknown
|
||||
testing.expectEqual('http://lightpanda.io/unsupported/namespace', unknownNsElement.namespaceURI);
|
||||
testing.expectEqual('CUSTOM', unknownNsElement.tagName);
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', unknownNsElement.namespaceURI);
|
||||
|
||||
const regularDiv = document.createElement('div');
|
||||
testing.expectEqual('DIV', regularDiv.tagName);
|
||||
@@ -43,5 +36,5 @@
|
||||
testing.expectEqual('te:ST', custom.tagName);
|
||||
testing.expectEqual('te', custom.prefix);
|
||||
testing.expectEqual('ST', custom.localName);
|
||||
testing.expectEqual('http://lightpanda.io/unsupported/namespace', custom.namespaceURI); // Should be test
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', custom.namespaceURI); // Should be test
|
||||
</script>
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
testing.expectEqual(undefined, document.getCurrentScript);
|
||||
testing.expectEqual("http://127.0.0.1:9582/src/browser/tests/document/document.html", document.URL);
|
||||
testing.expectEqual(window, document.defaultView);
|
||||
testing.expectEqual(false, document.hidden);
|
||||
testing.expectEqual("visible", document.visibilityState);
|
||||
testing.expectEqual(false, document.prerendering);
|
||||
testing.expectEqual(undefined, Document.prerendering);
|
||||
</script>
|
||||
|
||||
<script id=headAndbody>
|
||||
|
||||
@@ -41,53 +41,4 @@
|
||||
testing.expectEqual("DIV", newElement.tagName);
|
||||
testing.expectEqual("after begin", newElement.innerText);
|
||||
testing.expectEqual("afterbegin", newElement.className);
|
||||
|
||||
const fuzzWrapper = document.createElement("div");
|
||||
fuzzWrapper.id = "fuzz-wrapper";
|
||||
document.body.appendChild(fuzzWrapper);
|
||||
|
||||
const fuzzCases = [
|
||||
// These cases have no <body> element (or empty body), so nothing is inserted
|
||||
{ name: "empty string", html: "", expectElements: 0 },
|
||||
{ name: "comment only", html: "<!-- comment -->", expectElements: 0 },
|
||||
{ name: "doctype only", html: "<!DOCTYPE html>", expectElements: 0 },
|
||||
{ name: "full empty doc", html: "<!DOCTYPE html><html><head></head><body></body></html>", expectElements: 0 },
|
||||
|
||||
{ name: "whitespace only", html: " ", expectElements: 0 },
|
||||
{ name: "newlines only", html: "\n\n\n", expectElements: 0 },
|
||||
{ name: "just text", html: "plain text", expectElements: 0 },
|
||||
// Head-only elements: Extracted from <head> container
|
||||
{ name: "empty meta", html: "<meta>", expectElements: 1 },
|
||||
{ name: "empty title", html: "<title></title>", expectElements: 1 },
|
||||
{ name: "empty head", html: "<head></head>", expectElements: 0 }, // Container with no children
|
||||
{ name: "empty body", html: "<body></body>", expectElements: 0 }, // Container with no children
|
||||
{ name: "empty html", html: "<html></html>", expectElements: 0 }, // Container with no children
|
||||
{ name: "meta only", html: "<meta charset='utf-8'>", expectElements: 1 },
|
||||
{ name: "title only", html: "<title>Test</title>", expectElements: 1 },
|
||||
{ name: "link only", html: "<link rel='stylesheet' href='test.css'>", expectElements: 1 },
|
||||
{ name: "meta and title", html: "<meta charset='utf-8'><title>Test</title>", expectElements: 2 },
|
||||
{ name: "script only", html: "<script>console.log('hi')<\/script>", expectElements: 1 },
|
||||
{ name: "style only", html: "<style>body { color: red; }<\/style>", expectElements: 1 },
|
||||
{ name: "unclosed div", html: "<div>content", expectElements: 1 },
|
||||
{ name: "unclosed span", html: "<span>text", expectElements: 1 },
|
||||
{ name: "invalid tag", html: "<notarealtag>content</notarealtag>", expectElements: 1 },
|
||||
{ name: "malformed", html: "<<div>>test<</div>>", expectElements: 1 }, // Parser handles it
|
||||
{ name: "just closing tag", html: "</div>", expectElements: 0 },
|
||||
{ name: "nested empty", html: "<div><div></div></div>", expectElements: 1 },
|
||||
{ name: "multiple top-level", html: "<span>1</span><span>2</span><span>3</span>", expectElements: 3 },
|
||||
{ name: "mixed text and elements", html: "Text before<b>bold</b>Text after", expectElements: 1 },
|
||||
{ name: "deeply nested", html: "<div><div><div><span>deep</span></div></div></div>", expectElements: 1 },
|
||||
];
|
||||
|
||||
fuzzCases.forEach((tc, idx) => {
|
||||
fuzzWrapper.innerHTML = "";
|
||||
fuzzWrapper.insertAdjacentHTML("beforeend", tc.html);
|
||||
if (tc.expectElements !== fuzzWrapper.childElementCount) {
|
||||
console.warn(`Fuzz idx: ${idx}`);
|
||||
testing.expectEqual(tc.expectElements, fuzzWrapper.childElementCount);
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(fuzzWrapper);
|
||||
</script>
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
<main>Main content</main>
|
||||
|
||||
<script id=byId name="test1">
|
||||
testing.expectEqual(1, document.querySelector.length);
|
||||
testing.expectError("SyntaxError: Syntax Error", () => document.querySelector(''));
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual(12, err.code);
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<head>
|
||||
<title>document.replaceChildren Tests</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="test">Original content</div>
|
||||
</body>
|
||||
|
||||
<script id=error_multiple_elements>
|
||||
{
|
||||
// Test that we cannot have more than one Element child
|
||||
const doc = new Document();
|
||||
const div1 = doc.createElement('div');
|
||||
const div2 = doc.createElement('div');
|
||||
|
||||
testing.expectError('HierarchyRequest', () => {
|
||||
doc.replaceChildren(div1, div2);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=error_multiple_elements_via_fragment>
|
||||
{
|
||||
// Test that we cannot have more than one Element child via DocumentFragment
|
||||
const doc = new Document();
|
||||
const fragment = doc.createDocumentFragment();
|
||||
fragment.appendChild(doc.createElement('div'));
|
||||
fragment.appendChild(doc.createElement('span'));
|
||||
|
||||
testing.expectError('HierarchyRequest', () => {
|
||||
doc.replaceChildren(fragment);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=error_multiple_doctypes>
|
||||
{
|
||||
// Test that we cannot have more than one DocumentType child
|
||||
const doc = new Document();
|
||||
const doctype1 = doc.implementation.createDocumentType('html', '', '');
|
||||
const doctype2 = doc.implementation.createDocumentType('html', '', '');
|
||||
|
||||
testing.expectError('HierarchyRequest', () => {
|
||||
doc.replaceChildren(doctype1, doctype2);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=error_text_node>
|
||||
{
|
||||
// Test that we cannot insert Text nodes directly into Document
|
||||
const doc = new Document();
|
||||
|
||||
testing.expectError('HierarchyRequest', () => {
|
||||
doc.replaceChildren('Just text');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=error_text_with_element>
|
||||
{
|
||||
// Test that we cannot insert Text nodes even with valid Element
|
||||
const doc = new Document();
|
||||
const html = doc.createElement('html');
|
||||
|
||||
testing.expectError('HierarchyRequest', () => {
|
||||
doc.replaceChildren('Text 1', html, 'Text 2');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=error_append_multiple_elements>
|
||||
{
|
||||
// Test that append also validates
|
||||
const doc = new Document();
|
||||
doc.append(doc.createElement('html'));
|
||||
|
||||
const div = doc.createElement('div');
|
||||
testing.expectError('HierarchyRequest', () => {
|
||||
doc.append(div);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=error_prepend_multiple_elements>
|
||||
{
|
||||
// Test that prepend also validates
|
||||
const doc = new Document();
|
||||
doc.prepend(doc.createElement('html'));
|
||||
|
||||
const div = doc.createElement('div');
|
||||
testing.expectError('HierarchyRequest', () => {
|
||||
doc.prepend(div);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=error_append_text>
|
||||
{
|
||||
// Test that append rejects text nodes
|
||||
const doc = new Document();
|
||||
|
||||
testing.expectError('HierarchyRequest', () => {
|
||||
doc.append('text');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=error_prepend_text>
|
||||
{
|
||||
// Test that prepend rejects text nodes
|
||||
const doc = new Document();
|
||||
|
||||
testing.expectError('HierarchyRequest', () => {
|
||||
doc.prepend('text');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=replace_with_single_element>
|
||||
{
|
||||
const doc = new Document();
|
||||
const html = doc.createElement('html');
|
||||
html.id = 'replaced';
|
||||
html.textContent = 'New content';
|
||||
|
||||
doc.replaceChildren(html);
|
||||
|
||||
testing.expectEqual(1, doc.childNodes.length);
|
||||
testing.expectEqual(html, doc.firstChild);
|
||||
testing.expectEqual('replaced', doc.firstChild.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=replace_with_comments>
|
||||
{
|
||||
const doc = new Document();
|
||||
const comment1 = doc.createComment('Comment 1');
|
||||
const html = doc.createElement('html');
|
||||
const comment2 = doc.createComment('Comment 2');
|
||||
|
||||
doc.replaceChildren(comment1, html, comment2);
|
||||
|
||||
testing.expectEqual(3, doc.childNodes.length);
|
||||
testing.expectEqual('#comment', doc.firstChild.nodeName);
|
||||
testing.expectEqual('Comment 1', doc.firstChild.textContent);
|
||||
testing.expectEqual('html', doc.childNodes[1].nodeName);
|
||||
testing.expectEqual('#comment', doc.lastChild.nodeName);
|
||||
testing.expectEqual('Comment 2', doc.lastChild.textContent);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=replace_with_empty>
|
||||
{
|
||||
const doc = new Document();
|
||||
// First add some content
|
||||
const div = doc.createElement('div');
|
||||
doc.replaceChildren(div);
|
||||
testing.expectEqual(1, doc.childNodes.length);
|
||||
|
||||
// Now replace with nothing
|
||||
doc.replaceChildren();
|
||||
|
||||
testing.expectEqual(0, doc.childNodes.length);
|
||||
testing.expectEqual(null, doc.firstChild);
|
||||
testing.expectEqual(null, doc.lastChild);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=replace_removes_old_children>
|
||||
{
|
||||
const doc = new Document();
|
||||
const comment1 = doc.createComment('old');
|
||||
|
||||
doc.replaceChildren(comment1);
|
||||
testing.expectEqual(1, doc.childNodes.length);
|
||||
testing.expectEqual(doc, comment1.parentNode);
|
||||
|
||||
const html = doc.createElement('html');
|
||||
html.id = 'new';
|
||||
|
||||
doc.replaceChildren(html);
|
||||
|
||||
// Old child should be removed
|
||||
testing.expectEqual(null, comment1.parentNode);
|
||||
testing.expectEqual(1, doc.childNodes.length);
|
||||
testing.expectEqual('new', doc.firstChild.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=replace_with_document_fragment_valid>
|
||||
{
|
||||
const doc = new Document();
|
||||
const fragment = doc.createDocumentFragment();
|
||||
const html = doc.createElement('html');
|
||||
const comment = doc.createComment('comment');
|
||||
|
||||
fragment.appendChild(comment);
|
||||
fragment.appendChild(html);
|
||||
|
||||
doc.replaceChildren(fragment);
|
||||
|
||||
// Fragment contents should be moved
|
||||
testing.expectEqual(2, doc.childNodes.length);
|
||||
testing.expectEqual('#comment', doc.firstChild.nodeName);
|
||||
testing.expectEqual('html', doc.lastChild.nodeName);
|
||||
|
||||
// Fragment should be empty now
|
||||
testing.expectEqual(0, fragment.childNodes.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=replace_maintains_child_order>
|
||||
{
|
||||
const doc = new Document();
|
||||
const nodes = [];
|
||||
|
||||
// Document can have: comment, processing instruction, doctype, element
|
||||
nodes.push(doc.createComment('comment'));
|
||||
nodes.push(doc.createElement('html'));
|
||||
|
||||
doc.replaceChildren(...nodes);
|
||||
|
||||
testing.expectEqual(2, doc.childNodes.length);
|
||||
testing.expectEqual('#comment', doc.childNodes[0].nodeName);
|
||||
testing.expectEqual('html', doc.childNodes[1].nodeName);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=replace_with_nested_structure>
|
||||
{
|
||||
const doc = new Document();
|
||||
const outer = doc.createElement('html');
|
||||
outer.id = 'outer';
|
||||
const middle = doc.createElement('body');
|
||||
middle.id = 'middle';
|
||||
const inner = doc.createElement('span');
|
||||
inner.id = 'inner';
|
||||
inner.textContent = 'Nested';
|
||||
|
||||
middle.appendChild(inner);
|
||||
outer.appendChild(middle);
|
||||
|
||||
doc.replaceChildren(outer);
|
||||
|
||||
testing.expectEqual(1, doc.childNodes.length);
|
||||
testing.expectEqual('outer', doc.firstChild.id);
|
||||
|
||||
const foundInner = doc.getElementById('inner');
|
||||
testing.expectEqual(inner, foundInner);
|
||||
testing.expectEqual('Nested', foundInner.textContent);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=consecutive_replaces>
|
||||
{
|
||||
const doc = new Document();
|
||||
const html1 = doc.createElement('html');
|
||||
html1.id = 'first-replace';
|
||||
doc.replaceChildren(html1);
|
||||
testing.expectEqual('first-replace', doc.firstChild.id);
|
||||
|
||||
// Replace element with comments
|
||||
const comment = doc.createComment('in between');
|
||||
doc.replaceChildren(comment);
|
||||
testing.expectEqual(1, doc.childNodes.length);
|
||||
testing.expectEqual('#comment', doc.firstChild.nodeName);
|
||||
|
||||
// Replace comments with new element
|
||||
const html2 = doc.createElement('html');
|
||||
html2.id = 'second-replace';
|
||||
doc.replaceChildren(html2);
|
||||
testing.expectEqual('second-replace', doc.firstChild.id);
|
||||
testing.expectEqual(1, doc.childNodes.length);
|
||||
|
||||
// First element should no longer be in document
|
||||
testing.expectEqual(null, html1.parentNode);
|
||||
testing.expectEqual(null, comment.parentNode);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=replace_with_comments_only>
|
||||
{
|
||||
const doc = new Document();
|
||||
const comment1 = doc.createComment('First');
|
||||
const comment2 = doc.createComment('Second');
|
||||
|
||||
doc.replaceChildren(comment1, comment2);
|
||||
|
||||
testing.expectEqual(2, doc.childNodes.length);
|
||||
testing.expectEqual('#comment', doc.firstChild.nodeName);
|
||||
testing.expectEqual('First', doc.firstChild.textContent);
|
||||
testing.expectEqual('#comment', doc.lastChild.nodeName);
|
||||
testing.expectEqual('Second', doc.lastChild.textContent);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=error_fragment_with_text>
|
||||
{
|
||||
// DocumentFragment with text should fail when inserted into Document
|
||||
const doc = new Document();
|
||||
const fragment = doc.createDocumentFragment();
|
||||
fragment.appendChild(doc.createTextNode('text'));
|
||||
fragment.appendChild(doc.createElement('html'));
|
||||
|
||||
testing.expectError('HierarchyRequest', () => {
|
||||
doc.replaceChildren(fragment);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=append_valid_nodes>
|
||||
{
|
||||
const doc = new Document();
|
||||
const comment = doc.createComment('test');
|
||||
const html = doc.createElement('html');
|
||||
|
||||
doc.append(comment);
|
||||
testing.expectEqual(1, doc.childNodes.length);
|
||||
|
||||
doc.append(html);
|
||||
testing.expectEqual(2, doc.childNodes.length);
|
||||
testing.expectEqual('#comment', doc.firstChild.nodeName);
|
||||
testing.expectEqual('html', doc.lastChild.nodeName);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=prepend_valid_nodes>
|
||||
{
|
||||
const doc = new Document();
|
||||
const html = doc.createElement('html');
|
||||
const comment = doc.createComment('test');
|
||||
|
||||
doc.prepend(html);
|
||||
testing.expectEqual(1, doc.childNodes.length);
|
||||
|
||||
doc.prepend(comment);
|
||||
testing.expectEqual(2, doc.childNodes.length);
|
||||
testing.expectEqual('#comment', doc.firstChild.nodeName);
|
||||
testing.expectEqual('html', doc.lastChild.nodeName);
|
||||
}
|
||||
</script>
|
||||
@@ -168,7 +168,7 @@
|
||||
const root = doc.documentElement;
|
||||
testing.expectEqual(true, root !== null);
|
||||
// TODO: XML documents should preserve case, but we currently uppercase
|
||||
testing.expectEqual('root', root.tagName);
|
||||
testing.expectEqual('ROOT', root.tagName);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -206,9 +206,10 @@
|
||||
const doc = impl.createDocument('http://example.com', 'prefix:localName', null);
|
||||
|
||||
const root = doc.documentElement;
|
||||
testing.expectEqual('prefix:localName', root.tagName);
|
||||
// TODO: Custom namespaces are being replaced with an empty value
|
||||
testing.expectEqual('http://lightpanda.io/unsupported/namespace', root.namespaceURI);
|
||||
// TODO: XML documents should preserve case, but we currently uppercase
|
||||
testing.expectEqual('prefix:LOCALNAME', root.tagName);
|
||||
// TODO: Custom namespaces are being overridden to XHTML namespace
|
||||
testing.expectEqual('http://www.w3.org/1999/xhtml', root.namespaceURI);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -223,7 +224,8 @@
|
||||
doc.documentElement.appendChild(child);
|
||||
|
||||
testing.expectEqual(1, doc.documentElement.childNodes.length);
|
||||
testing.expectEqual('child', doc.documentElement.firstChild.tagName);
|
||||
// TODO: XML documents should preserve case, but we currently uppercase
|
||||
testing.expectEqual('CHILD', doc.documentElement.firstChild.tagName);
|
||||
testing.expectEqual('Test', doc.documentElement.firstChild.textContent);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -107,6 +107,19 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=unsupportedMimeType>
|
||||
{
|
||||
const parser = new DOMParser();
|
||||
|
||||
// Should throw an error for unsupported MIME types
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual('NotSupported', err.message);
|
||||
}, () => {
|
||||
parser.parseFromString('<div>test</div>', 'application/xml');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=getElementById>
|
||||
{
|
||||
const doc = new DOMParser().parseFromString('<div id="new-node">new-node</div>', 'text/html');
|
||||
@@ -231,161 +244,3 @@
|
||||
testing.expectEqual('<html><head></head><body>spice</body></html>', new DOMParser().parseFromString('spice', "text/html").documentElement.outerHTML);
|
||||
testing.expectEqual('<html><head></head><body></body></html>', new DOMParser().parseFromString('<html></html>', "text/html").documentElement.outerHTML);
|
||||
</script>
|
||||
|
||||
<script id=parse-xml>
|
||||
{
|
||||
const sampleXML = `<?xml version="1.0"?>
|
||||
<catalog>
|
||||
<book id="bk101">
|
||||
<author>Gambardella, Matthew</author>
|
||||
<title>XML Developer's Guide</title>
|
||||
<genre>Computer</genre>
|
||||
<price>44.95</price>
|
||||
<publish_date>2000-10-01</publish_date>
|
||||
<description>An in-depth look at creating applications
|
||||
with XML.</description>
|
||||
</book>
|
||||
<book id="bk102">
|
||||
<author>Ralls, Kim</author>
|
||||
<title>Midnight Rain</title>
|
||||
<genre>Fantasy</genre>
|
||||
<price>5.95</price>
|
||||
<publish_date>2000-12-16</publish_date>
|
||||
<description>A former architect battles corporate zombies,
|
||||
an evil sorceress, and her own childhood to become queen
|
||||
of the world.</description>
|
||||
</book>
|
||||
<book id="bk103">
|
||||
<author>Corets, Eva</author>
|
||||
<title>Maeve Ascendant</title>
|
||||
<genre>Fantasy</genre>
|
||||
<price>5.95</price>
|
||||
<publish_date>2000-11-17</publish_date>
|
||||
<description>After the collapse of a nanotechnology
|
||||
society in England, the young survivors lay the
|
||||
foundation for a new society.</description>
|
||||
</book>
|
||||
<book id="bk104">
|
||||
<author>Corets, Eva</author>
|
||||
<title>Oberon's Legacy</title>
|
||||
<genre>Fantasy</genre>
|
||||
<price>5.95</price>
|
||||
<publish_date>2001-03-10</publish_date>
|
||||
<description>In post-apocalypse England, the mysterious
|
||||
agent known only as Oberon helps to create a new life
|
||||
for the inhabitants of London. Sequel to Maeve
|
||||
Ascendant.</description>
|
||||
</book>
|
||||
<book id="bk105">
|
||||
<author>Corets, Eva</author>
|
||||
<title>The Sundered Grail</title>
|
||||
<genre>Fantasy</genre>
|
||||
<price>5.95</price>
|
||||
<publish_date>2001-09-10</publish_date>
|
||||
<description>The two daughters of Maeve, half-sisters,
|
||||
battle one another for control of England. Sequel to
|
||||
Oberon's Legacy.</description>
|
||||
</book>
|
||||
<book id="bk106">
|
||||
<author>Randall, Cynthia</author>
|
||||
<title>Lover Birds</title>
|
||||
<genre>Romance</genre>
|
||||
<price>4.95</price>
|
||||
<publish_date>2000-09-02</publish_date>
|
||||
<description>When Carla meets Paul at an ornithology
|
||||
conference, tempers fly as feathers get ruffled.</description>
|
||||
</book>
|
||||
<book id="bk107">
|
||||
<author>Thurman, Paula</author>
|
||||
<title>Splish Splash</title>
|
||||
<genre>Romance</genre>
|
||||
<price>4.95</price>
|
||||
<publish_date>2000-11-02</publish_date>
|
||||
<description>A deep sea diver finds true love twenty
|
||||
thousand leagues beneath the sea.</description>
|
||||
</book>
|
||||
<book id="bk108">
|
||||
<author>Knorr, Stefan</author>
|
||||
<title>Creepy Crawlies</title>
|
||||
<genre>Horror</genre>
|
||||
<price>4.95</price>
|
||||
<publish_date>2000-12-06</publish_date>
|
||||
<description>An anthology of horror stories about roaches,
|
||||
centipedes, scorpions and other insects.</description>
|
||||
</book>
|
||||
<book id="bk109">
|
||||
<author>Kress, Peter</author>
|
||||
<title>Paradox Lost</title>
|
||||
<genre>Science Fiction</genre>
|
||||
<price>6.95</price>
|
||||
<publish_date>2000-11-02</publish_date>
|
||||
<description>After an inadvertant trip through a Heisenberg
|
||||
Uncertainty Device, James Salway discovers the problems
|
||||
of being quantum.</description>
|
||||
</book>
|
||||
<book id="bk110">
|
||||
<author>O'Brien, Tim</author>
|
||||
<title>Microsoft .NET: The Programming Bible</title>
|
||||
<genre>Computer</genre>
|
||||
<price>36.95</price>
|
||||
<publish_date>2000-12-09</publish_date>
|
||||
<description>Microsoft's .NET initiative is explored in
|
||||
detail in this deep programmer's reference.</description>
|
||||
</book>
|
||||
<book id="bk111">
|
||||
<author>O'Brien, Tim</author>
|
||||
<title>MSXML3: A Comprehensive Guide</title>
|
||||
<genre>Computer</genre>
|
||||
<price>36.95</price>
|
||||
<publish_date>2000-12-01</publish_date>
|
||||
<description>The Microsoft MSXML3 parser is covered in
|
||||
detail, with attention to XML DOM interfaces, XSLT processing,
|
||||
SAX and more.</description>
|
||||
</book>
|
||||
<book id="bk112">
|
||||
<author>Galos, Mike</author>
|
||||
<title>Visual Studio 7: A Comprehensive Guide</title>
|
||||
<genre>Computer</genre>
|
||||
<price>49.95</price>
|
||||
<publish_date>2001-04-16</publish_date>
|
||||
<description>Microsoft Visual Studio 7 is explored in depth,
|
||||
looking at how Visual Basic, Visual C++, C#, and ASP+ are
|
||||
integrated into a comprehensive development
|
||||
environment.</description>
|
||||
</book>
|
||||
</catalog>`;
|
||||
|
||||
const parser = new DOMParser();
|
||||
const mimes = [
|
||||
"text/xml",
|
||||
"application/xml",
|
||||
"application/xhtml+xml",
|
||||
"image/svg+xml",
|
||||
];
|
||||
|
||||
for (const mime of mimes) {
|
||||
const doc = parser.parseFromString(sampleXML, mime);
|
||||
const { firstChild: { childNodes, children: collection, tagName }, children } = doc;
|
||||
// doc.
|
||||
testing.expectEqual(true, doc instanceof XMLDocument);
|
||||
testing.expectEqual(1, children.length);
|
||||
// firstChild.
|
||||
// TODO: Modern browsers expect this in lowercase.
|
||||
testing.expectEqual("catalog", tagName);
|
||||
testing.expectEqual(25, childNodes.length);
|
||||
testing.expectEqual(12, collection.length);
|
||||
// Check children of first child.
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
const {children: elements, id} = collection.item(i);
|
||||
testing.expectEqual("bk" + (100 + i + 1), id);
|
||||
// TODO: Modern browsers expect these in lowercase.
|
||||
testing.expectEqual("author", elements.item(0).tagName);
|
||||
testing.expectEqual("title", elements.item(1).tagName);
|
||||
testing.expectEqual("genre", elements.item(2).tagName);
|
||||
testing.expectEqual("price", elements.item(3).tagName);
|
||||
testing.expectEqual("publish_date", elements.item(4).tagName);
|
||||
testing.expectEqual("description", elements.item(5).tagName);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<a id=legacy></a>
|
||||
<div id=legacy></a>
|
||||
<script id=legacy>
|
||||
{
|
||||
let a = document.getElementById('legacy').attributes;
|
||||
@@ -266,19 +266,3 @@
|
||||
testing.expectEqual('abc123', a[0].value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="nsa"></div>
|
||||
<script id=non-string-attr>
|
||||
{
|
||||
let nsa = document.getElementById('nsa');
|
||||
|
||||
nsa.setAttribute('int', 1);
|
||||
testing.expectEqual('1', nsa.getAttribute('int'));
|
||||
|
||||
nsa.setAttribute('obj', {});
|
||||
testing.expectEqual('[object Object]', nsa.getAttribute('obj'));
|
||||
|
||||
nsa.setAttribute('arr', []);
|
||||
testing.expectEqual('', nsa.getAttribute('arr'));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<div id="test">first</div>
|
||||
<div id="test">second</div>
|
||||
|
||||
<script id=duplicateIds>
|
||||
const first = document.getElementById('test');
|
||||
testing.expectEqual('first', first.textContent);
|
||||
|
||||
first.remove();
|
||||
|
||||
const second = document.getElementById('test');
|
||||
testing.expectEqual('second', second.textContent);
|
||||
|
||||
// second.remove();
|
||||
|
||||
// testing.expectEqual(null, document.getElementById('test'));
|
||||
</script>
|
||||
@@ -1,514 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<!-- Test inline event listeners set via HTML attributes -->
|
||||
<div id="attr-click" onclick="window.x = 1"></div>
|
||||
<div id="attr-load" onload="window.x = 1"></div>
|
||||
<div id="attr-error" onerror="window.x = 1"></div>
|
||||
<div id="attr-focus" onfocus="window.x = 1"></div>
|
||||
<div id="attr-blur" onblur="window.x = 1"></div>
|
||||
<div id="attr-keydown" onkeydown="window.x = 1"></div>
|
||||
<div id="attr-mousedown" onmousedown="window.x = 1"></div>
|
||||
<div id="attr-submit" onsubmit="window.x = 1"></div>
|
||||
<div id="attr-wheel" onwheel="window.x = 1"></div>
|
||||
<div id="attr-scroll" onscroll="window.x = 1"></div>
|
||||
<div id="attr-contextmenu" oncontextmenu="window.x = 1"></div>
|
||||
<div id="no-listeners"></div>
|
||||
|
||||
<script id="attr_listener_returns_function">
|
||||
{
|
||||
// Inline listeners set via HTML attributes should return a function
|
||||
testing.expectEqual('function', typeof $('#attr-click').onclick);
|
||||
testing.expectEqual('function', typeof $('#attr-load').onload);
|
||||
testing.expectEqual('function', typeof $('#attr-error').onerror);
|
||||
testing.expectEqual('function', typeof $('#attr-focus').onfocus);
|
||||
testing.expectEqual('function', typeof $('#attr-blur').onblur);
|
||||
testing.expectEqual('function', typeof $('#attr-keydown').onkeydown);
|
||||
testing.expectEqual('function', typeof $('#attr-mousedown').onmousedown);
|
||||
testing.expectEqual('function', typeof $('#attr-submit').onsubmit);
|
||||
testing.expectEqual('function', typeof $('#attr-wheel').onwheel);
|
||||
testing.expectEqual('function', typeof $('#attr-scroll').onscroll);
|
||||
testing.expectEqual('function', typeof $('#attr-contextmenu').oncontextmenu);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="no_attr_listener_returns_null">
|
||||
{
|
||||
// Elements without inline listeners should return null
|
||||
const div = $('#no-listeners');
|
||||
testing.expectEqual(null, div.onclick);
|
||||
testing.expectEqual(null, div.onload);
|
||||
testing.expectEqual(null, div.onerror);
|
||||
testing.expectEqual(null, div.onfocus);
|
||||
testing.expectEqual(null, div.onblur);
|
||||
testing.expectEqual(null, div.onkeydown);
|
||||
testing.expectEqual(null, div.onmousedown);
|
||||
testing.expectEqual(null, div.onsubmit);
|
||||
testing.expectEqual(null, div.onwheel);
|
||||
testing.expectEqual(null, div.onscroll);
|
||||
testing.expectEqual(null, div.oncontextmenu);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="js_setter_getter">
|
||||
{
|
||||
// Test setting and getting listeners via JavaScript property
|
||||
const div = document.createElement('div');
|
||||
|
||||
// Initially null
|
||||
testing.expectEqual(null, div.onclick);
|
||||
testing.expectEqual(null, div.onload);
|
||||
testing.expectEqual(null, div.onerror);
|
||||
|
||||
// Set listeners
|
||||
const clickHandler = () => {};
|
||||
const loadHandler = () => {};
|
||||
const errorHandler = () => {};
|
||||
|
||||
div.onclick = clickHandler;
|
||||
div.onload = loadHandler;
|
||||
div.onerror = errorHandler;
|
||||
|
||||
// Verify they can be retrieved and are functions
|
||||
testing.expectEqual('function', typeof div.onclick);
|
||||
testing.expectEqual('function', typeof div.onload);
|
||||
testing.expectEqual('function', typeof div.onerror);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="js_listener_invoke">
|
||||
{
|
||||
// Test that JS-set listeners can be invoked directly
|
||||
const div = document.createElement('div');
|
||||
window.jsInvokeResult = 0;
|
||||
|
||||
div.onclick = () => { window.jsInvokeResult = 100; };
|
||||
div.onclick();
|
||||
testing.expectEqual(100, window.jsInvokeResult);
|
||||
|
||||
div.onload = () => { window.jsInvokeResult = 200; };
|
||||
div.onload();
|
||||
testing.expectEqual(200, window.jsInvokeResult);
|
||||
|
||||
div.onfocus = () => { window.jsInvokeResult = 300; };
|
||||
div.onfocus();
|
||||
testing.expectEqual(300, window.jsInvokeResult);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="js_listener_invoke_with_return">
|
||||
{
|
||||
// Test that JS-set listeners return values when invoked
|
||||
const div = document.createElement('div');
|
||||
|
||||
div.onclick = () => { return 'click-result'; };
|
||||
testing.expectEqual('click-result', div.onclick());
|
||||
|
||||
div.onload = () => { return 42; };
|
||||
testing.expectEqual(42, div.onload());
|
||||
|
||||
div.onfocus = () => { return { key: 'value' }; };
|
||||
testing.expectEqual('value', div.onfocus().key);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="js_listener_invoke_with_args">
|
||||
{
|
||||
// Test that JS-set listeners can receive arguments when invoked
|
||||
const div = document.createElement('div');
|
||||
|
||||
div.onclick = (a, b) => { return a + b; };
|
||||
testing.expectEqual(15, div.onclick(10, 5));
|
||||
|
||||
div.onload = (msg) => { return 'Hello, ' + msg; };
|
||||
testing.expectEqual('Hello, World', div.onload('World'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="js_setter_override">
|
||||
{
|
||||
// Test that setting a new listener overrides the old one
|
||||
const div = document.createElement('div');
|
||||
|
||||
const first = () => { return 1; };
|
||||
const second = () => { return 2; };
|
||||
|
||||
div.onclick = first;
|
||||
testing.expectEqual('function', typeof div.onclick);
|
||||
testing.expectEqual(1, div.onclick());
|
||||
|
||||
div.onclick = second;
|
||||
testing.expectEqual('function', typeof div.onclick);
|
||||
testing.expectEqual(2, div.onclick());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="different_event_types_independent">
|
||||
{
|
||||
// Test that different event types are stored independently
|
||||
const div = document.createElement('div');
|
||||
|
||||
const clickFn = () => {};
|
||||
const focusFn = () => {};
|
||||
const blurFn = () => {};
|
||||
|
||||
div.onclick = clickFn;
|
||||
testing.expectEqual('function', typeof div.onclick);
|
||||
testing.expectEqual(null, div.onfocus);
|
||||
testing.expectEqual(null, div.onblur);
|
||||
|
||||
div.onfocus = focusFn;
|
||||
testing.expectEqual('function', typeof div.onclick);
|
||||
testing.expectEqual('function', typeof div.onfocus);
|
||||
testing.expectEqual(null, div.onblur);
|
||||
|
||||
div.onblur = blurFn;
|
||||
testing.expectEqual('function', typeof div.onclick);
|
||||
testing.expectEqual('function', typeof div.onfocus);
|
||||
testing.expectEqual('function', typeof div.onblur);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="keyboard_event_listeners">
|
||||
{
|
||||
// Test keyboard event listener getters/setters
|
||||
const div = document.createElement('div');
|
||||
|
||||
testing.expectEqual(null, div.onkeydown);
|
||||
testing.expectEqual(null, div.onkeyup);
|
||||
testing.expectEqual(null, div.onkeypress);
|
||||
|
||||
div.onkeydown = () => {};
|
||||
div.onkeyup = () => {};
|
||||
div.onkeypress = () => {};
|
||||
|
||||
testing.expectEqual('function', typeof div.onkeydown);
|
||||
testing.expectEqual('function', typeof div.onkeyup);
|
||||
testing.expectEqual('function', typeof div.onkeypress);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="mouse_event_listeners">
|
||||
{
|
||||
// Test mouse event listener getters/setters
|
||||
const div = document.createElement('div');
|
||||
|
||||
testing.expectEqual(null, div.onmousedown);
|
||||
testing.expectEqual(null, div.onmouseup);
|
||||
testing.expectEqual(null, div.onmousemove);
|
||||
testing.expectEqual(null, div.onmouseover);
|
||||
testing.expectEqual(null, div.onmouseout);
|
||||
testing.expectEqual(null, div.ondblclick);
|
||||
|
||||
div.onmousedown = () => {};
|
||||
div.onmouseup = () => {};
|
||||
div.onmousemove = () => {};
|
||||
div.onmouseover = () => {};
|
||||
div.onmouseout = () => {};
|
||||
div.ondblclick = () => {};
|
||||
|
||||
testing.expectEqual('function', typeof div.onmousedown);
|
||||
testing.expectEqual('function', typeof div.onmouseup);
|
||||
testing.expectEqual('function', typeof div.onmousemove);
|
||||
testing.expectEqual('function', typeof div.onmouseover);
|
||||
testing.expectEqual('function', typeof div.onmouseout);
|
||||
testing.expectEqual('function', typeof div.ondblclick);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="pointer_event_listeners">
|
||||
{
|
||||
// Test pointer event listener getters/setters
|
||||
const div = document.createElement('div');
|
||||
|
||||
testing.expectEqual(null, div.onpointerdown);
|
||||
testing.expectEqual(null, div.onpointerup);
|
||||
testing.expectEqual(null, div.onpointermove);
|
||||
testing.expectEqual(null, div.onpointerover);
|
||||
testing.expectEqual(null, div.onpointerout);
|
||||
testing.expectEqual(null, div.onpointerenter);
|
||||
testing.expectEqual(null, div.onpointerleave);
|
||||
testing.expectEqual(null, div.onpointercancel);
|
||||
|
||||
div.onpointerdown = () => {};
|
||||
div.onpointerup = () => {};
|
||||
div.onpointermove = () => {};
|
||||
div.onpointerover = () => {};
|
||||
div.onpointerout = () => {};
|
||||
div.onpointerenter = () => {};
|
||||
div.onpointerleave = () => {};
|
||||
div.onpointercancel = () => {};
|
||||
|
||||
testing.expectEqual('function', typeof div.onpointerdown);
|
||||
testing.expectEqual('function', typeof div.onpointerup);
|
||||
testing.expectEqual('function', typeof div.onpointermove);
|
||||
testing.expectEqual('function', typeof div.onpointerover);
|
||||
testing.expectEqual('function', typeof div.onpointerout);
|
||||
testing.expectEqual('function', typeof div.onpointerenter);
|
||||
testing.expectEqual('function', typeof div.onpointerleave);
|
||||
testing.expectEqual('function', typeof div.onpointercancel);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="form_event_listeners">
|
||||
{
|
||||
// Test form event listener getters/setters
|
||||
const form = document.createElement('form');
|
||||
|
||||
testing.expectEqual(null, form.onsubmit);
|
||||
testing.expectEqual(null, form.onreset);
|
||||
testing.expectEqual(null, form.onchange);
|
||||
testing.expectEqual(null, form.oninput);
|
||||
testing.expectEqual(null, form.oninvalid);
|
||||
|
||||
form.onsubmit = () => {};
|
||||
form.onreset = () => {};
|
||||
form.onchange = () => {};
|
||||
form.oninput = () => {};
|
||||
form.oninvalid = () => {};
|
||||
|
||||
testing.expectEqual('function', typeof form.onsubmit);
|
||||
testing.expectEqual('function', typeof form.onreset);
|
||||
testing.expectEqual('function', typeof form.onchange);
|
||||
testing.expectEqual('function', typeof form.oninput);
|
||||
testing.expectEqual('function', typeof form.oninvalid);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="drag_event_listeners">
|
||||
{
|
||||
// Test drag event listener getters/setters
|
||||
const div = document.createElement('div');
|
||||
|
||||
testing.expectEqual(null, div.ondrag);
|
||||
testing.expectEqual(null, div.ondragstart);
|
||||
testing.expectEqual(null, div.ondragend);
|
||||
testing.expectEqual(null, div.ondragenter);
|
||||
testing.expectEqual(null, div.ondragleave);
|
||||
testing.expectEqual(null, div.ondragover);
|
||||
testing.expectEqual(null, div.ondrop);
|
||||
|
||||
div.ondrag = () => {};
|
||||
div.ondragstart = () => {};
|
||||
div.ondragend = () => {};
|
||||
div.ondragenter = () => {};
|
||||
div.ondragleave = () => {};
|
||||
div.ondragover = () => {};
|
||||
div.ondrop = () => {};
|
||||
|
||||
testing.expectEqual('function', typeof div.ondrag);
|
||||
testing.expectEqual('function', typeof div.ondragstart);
|
||||
testing.expectEqual('function', typeof div.ondragend);
|
||||
testing.expectEqual('function', typeof div.ondragenter);
|
||||
testing.expectEqual('function', typeof div.ondragleave);
|
||||
testing.expectEqual('function', typeof div.ondragover);
|
||||
testing.expectEqual('function', typeof div.ondrop);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="clipboard_event_listeners">
|
||||
{
|
||||
// Test clipboard event listener getters/setters
|
||||
const div = document.createElement('div');
|
||||
|
||||
testing.expectEqual(null, div.oncopy);
|
||||
testing.expectEqual(null, div.oncut);
|
||||
testing.expectEqual(null, div.onpaste);
|
||||
|
||||
div.oncopy = () => {};
|
||||
div.oncut = () => {};
|
||||
div.onpaste = () => {};
|
||||
|
||||
testing.expectEqual('function', typeof div.oncopy);
|
||||
testing.expectEqual('function', typeof div.oncut);
|
||||
testing.expectEqual('function', typeof div.onpaste);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="scroll_event_listeners">
|
||||
{
|
||||
// Test scroll event listener getters/setters
|
||||
const div = document.createElement('div');
|
||||
|
||||
testing.expectEqual(null, div.onscroll);
|
||||
testing.expectEqual(null, div.onscrollend);
|
||||
testing.expectEqual(null, div.onresize);
|
||||
|
||||
div.onscroll = () => {};
|
||||
div.onscrollend = () => {};
|
||||
div.onresize = () => {};
|
||||
|
||||
testing.expectEqual('function', typeof div.onscroll);
|
||||
testing.expectEqual('function', typeof div.onscrollend);
|
||||
testing.expectEqual('function', typeof div.onresize);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="animation_event_listeners">
|
||||
{
|
||||
// Test animation event listener getters/setters
|
||||
const div = document.createElement('div');
|
||||
|
||||
testing.expectEqual(null, div.onanimationstart);
|
||||
testing.expectEqual(null, div.onanimationend);
|
||||
testing.expectEqual(null, div.onanimationiteration);
|
||||
testing.expectEqual(null, div.onanimationcancel);
|
||||
|
||||
div.onanimationstart = () => {};
|
||||
div.onanimationend = () => {};
|
||||
div.onanimationiteration = () => {};
|
||||
div.onanimationcancel = () => {};
|
||||
|
||||
testing.expectEqual('function', typeof div.onanimationstart);
|
||||
testing.expectEqual('function', typeof div.onanimationend);
|
||||
testing.expectEqual('function', typeof div.onanimationiteration);
|
||||
testing.expectEqual('function', typeof div.onanimationcancel);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="transition_event_listeners">
|
||||
{
|
||||
// Test transition event listener getters/setters
|
||||
const div = document.createElement('div');
|
||||
|
||||
testing.expectEqual(null, div.ontransitionstart);
|
||||
testing.expectEqual(null, div.ontransitionend);
|
||||
testing.expectEqual(null, div.ontransitionrun);
|
||||
testing.expectEqual(null, div.ontransitioncancel);
|
||||
|
||||
div.ontransitionstart = () => {};
|
||||
div.ontransitionend = () => {};
|
||||
div.ontransitionrun = () => {};
|
||||
div.ontransitioncancel = () => {};
|
||||
|
||||
testing.expectEqual('function', typeof div.ontransitionstart);
|
||||
testing.expectEqual('function', typeof div.ontransitionend);
|
||||
testing.expectEqual('function', typeof div.ontransitionrun);
|
||||
testing.expectEqual('function', typeof div.ontransitioncancel);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="misc_event_listeners">
|
||||
{
|
||||
// Test miscellaneous event listener getters/setters
|
||||
const div = document.createElement('div');
|
||||
|
||||
testing.expectEqual(null, div.onwheel);
|
||||
testing.expectEqual(null, div.ontoggle);
|
||||
testing.expectEqual(null, div.oncontextmenu);
|
||||
testing.expectEqual(null, div.onselect);
|
||||
testing.expectEqual(null, div.onabort);
|
||||
testing.expectEqual(null, div.oncancel);
|
||||
testing.expectEqual(null, div.onclose);
|
||||
|
||||
div.onwheel = () => {};
|
||||
div.ontoggle = () => {};
|
||||
div.oncontextmenu = () => {};
|
||||
div.onselect = () => {};
|
||||
div.onabort = () => {};
|
||||
div.oncancel = () => {};
|
||||
div.onclose = () => {};
|
||||
|
||||
testing.expectEqual('function', typeof div.onwheel);
|
||||
testing.expectEqual('function', typeof div.ontoggle);
|
||||
testing.expectEqual('function', typeof div.oncontextmenu);
|
||||
testing.expectEqual('function', typeof div.onselect);
|
||||
testing.expectEqual('function', typeof div.onabort);
|
||||
testing.expectEqual('function', typeof div.oncancel);
|
||||
testing.expectEqual('function', typeof div.onclose);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="media_event_listeners">
|
||||
{
|
||||
// Test media event listener getters/setters
|
||||
const div = document.createElement('div');
|
||||
|
||||
testing.expectEqual(null, div.onplay);
|
||||
testing.expectEqual(null, div.onpause);
|
||||
testing.expectEqual(null, div.onplaying);
|
||||
testing.expectEqual(null, div.onended);
|
||||
testing.expectEqual(null, div.onvolumechange);
|
||||
testing.expectEqual(null, div.onwaiting);
|
||||
testing.expectEqual(null, div.onseeking);
|
||||
testing.expectEqual(null, div.onseeked);
|
||||
testing.expectEqual(null, div.ontimeupdate);
|
||||
testing.expectEqual(null, div.onloadstart);
|
||||
testing.expectEqual(null, div.onprogress);
|
||||
testing.expectEqual(null, div.onstalled);
|
||||
testing.expectEqual(null, div.onsuspend);
|
||||
testing.expectEqual(null, div.oncanplay);
|
||||
testing.expectEqual(null, div.oncanplaythrough);
|
||||
testing.expectEqual(null, div.ondurationchange);
|
||||
testing.expectEqual(null, div.onemptied);
|
||||
testing.expectEqual(null, div.onloadeddata);
|
||||
testing.expectEqual(null, div.onloadedmetadata);
|
||||
testing.expectEqual(null, div.onratechange);
|
||||
|
||||
div.onplay = () => {};
|
||||
div.onpause = () => {};
|
||||
div.onplaying = () => {};
|
||||
div.onended = () => {};
|
||||
div.onvolumechange = () => {};
|
||||
div.onwaiting = () => {};
|
||||
div.onseeking = () => {};
|
||||
div.onseeked = () => {};
|
||||
div.ontimeupdate = () => {};
|
||||
div.onloadstart = () => {};
|
||||
div.onprogress = () => {};
|
||||
div.onstalled = () => {};
|
||||
div.onsuspend = () => {};
|
||||
div.oncanplay = () => {};
|
||||
div.oncanplaythrough = () => {};
|
||||
div.ondurationchange = () => {};
|
||||
div.onemptied = () => {};
|
||||
div.onloadeddata = () => {};
|
||||
div.onloadedmetadata = () => {};
|
||||
div.onratechange = () => {};
|
||||
|
||||
testing.expectEqual('function', typeof div.onplay);
|
||||
testing.expectEqual('function', typeof div.onpause);
|
||||
testing.expectEqual('function', typeof div.onplaying);
|
||||
testing.expectEqual('function', typeof div.onended);
|
||||
testing.expectEqual('function', typeof div.onvolumechange);
|
||||
testing.expectEqual('function', typeof div.onwaiting);
|
||||
testing.expectEqual('function', typeof div.onseeking);
|
||||
testing.expectEqual('function', typeof div.onseeked);
|
||||
testing.expectEqual('function', typeof div.ontimeupdate);
|
||||
testing.expectEqual('function', typeof div.onloadstart);
|
||||
testing.expectEqual('function', typeof div.onprogress);
|
||||
testing.expectEqual('function', typeof div.onstalled);
|
||||
testing.expectEqual('function', typeof div.onsuspend);
|
||||
testing.expectEqual('function', typeof div.oncanplay);
|
||||
testing.expectEqual('function', typeof div.oncanplaythrough);
|
||||
testing.expectEqual('function', typeof div.ondurationchange);
|
||||
testing.expectEqual('function', typeof div.onemptied);
|
||||
testing.expectEqual('function', typeof div.onloadeddata);
|
||||
testing.expectEqual('function', typeof div.onloadedmetadata);
|
||||
testing.expectEqual('function', typeof div.onratechange);
|
||||
}
|
||||
</script>
|
||||
|
||||
<img src="https://cdn.lightpanda.io/website/assets/images/docs/hn.png" />
|
||||
|
||||
<script id="document-element-load">
|
||||
{
|
||||
let asyncBlockDispatched = false;
|
||||
const docElement = document.documentElement;
|
||||
|
||||
testing.async(async () => {
|
||||
const result = await new Promise(resolve => {
|
||||
// We should get this fired at capturing phase when a resource loaded.
|
||||
docElement.addEventListener("load", e => {
|
||||
testing.expectEqual(e.eventPhase, Event.CAPTURING_PHASE);
|
||||
return resolve(true);
|
||||
}, true);
|
||||
});
|
||||
|
||||
asyncBlockDispatched = true;
|
||||
testing.expectEqual(true, result);
|
||||
});
|
||||
|
||||
testing.eventually(() => testing.expectEqual(true, asyncBlockDispatched));
|
||||
}
|
||||
</script>
|
||||
@@ -97,62 +97,3 @@
|
||||
testing.expectEqual('lazy', img.getAttribute('loading'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="load-trigger-event">
|
||||
{
|
||||
const img = document.createElement("img");
|
||||
let count = 0;
|
||||
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 () => {
|
||||
const result = await new Promise(resolve => {
|
||||
img.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(img, target);
|
||||
|
||||
return resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
testing.expectEqual(true, result);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -183,45 +183,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="selectionchange_event">
|
||||
{
|
||||
const input = document.createElement('input');
|
||||
input.value = 'Hello World';
|
||||
document.body.appendChild(input);
|
||||
|
||||
let eventCount = 0;
|
||||
let lastEvent = null;
|
||||
|
||||
input.addEventListener('selectionchange', (e) => {
|
||||
eventCount++;
|
||||
lastEvent = e;
|
||||
});
|
||||
|
||||
testing.expectEqual(0, eventCount);
|
||||
|
||||
input.setSelectionRange(0, 5);
|
||||
input.select();
|
||||
input.selectionStart = 3;
|
||||
input.selectionEnd = 8;
|
||||
|
||||
let bubbledToBody = false;
|
||||
document.body.addEventListener('selectionchange', () => {
|
||||
bubbledToBody = true;
|
||||
});
|
||||
input.setSelectionRange(1, 4);
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(5, eventCount);
|
||||
testing.expectEqual('selectionchange', lastEvent.type);
|
||||
testing.expectEqual(input, lastEvent.target);
|
||||
testing.expectEqual(true, lastEvent.bubbles);
|
||||
testing.expectEqual(false, lastEvent.cancelable);
|
||||
testing.expectEqual(true, bubbledToBody);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- <script id="defaultChecked">
|
||||
<script id="defaultChecked">
|
||||
testing.expectEqual(true, $('#check1').defaultChecked)
|
||||
testing.expectEqual(false, $('#check2').defaultChecked)
|
||||
testing.expectEqual(true, $('#radio1').defaultChecked)
|
||||
@@ -493,4 +455,4 @@
|
||||
input_checked.defaultChecked = true;
|
||||
testing.expectEqual(false, input_checked.checked);
|
||||
}
|
||||
</script> -->
|
||||
</script>
|
||||
|
||||
@@ -238,15 +238,6 @@
|
||||
testing.expectEqual('[object HTMLAudioElement]', audio.toString());
|
||||
testing.expectEqual(true, audio.paused);
|
||||
}
|
||||
|
||||
// Create with `Audio` constructor.
|
||||
{
|
||||
const audio = new Audio();
|
||||
testing.expectEqual(true, audio instanceof HTMLAudioElement);
|
||||
testing.expectEqual("[object HTMLAudioElement]", audio.toString());
|
||||
testing.expectEqual(true, audio.paused);
|
||||
testing.expectEqual("auto", audio.getAttribute("preload"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="create_video_element">
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<!-- <script id="createElement">
|
||||
{
|
||||
const picture = document.createElement('picture');
|
||||
testing.expectEqual('PICTURE', picture.tagName);
|
||||
testing.expectEqual('[object HTMLPictureElement]', Object.prototype.toString.call(picture));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="constructor_type">
|
||||
{
|
||||
const picture = document.createElement('picture');
|
||||
testing.expectEqual(true, picture instanceof HTMLElement);
|
||||
testing.expectEqual(true, picture instanceof Element);
|
||||
testing.expectEqual(true, picture instanceof Node);
|
||||
}
|
||||
</script> -->
|
||||
|
||||
<picture id="inline-picture">
|
||||
<source media="(min-width: 800px)" srcset="large.jpg">
|
||||
<source media="(min-width: 400px)" srcset="medium.jpg">
|
||||
<img src="small.jpg" alt="Test image">
|
||||
</picture>
|
||||
|
||||
<script id="inline_picture">
|
||||
{
|
||||
const picture = document.getElementById('inline-picture');
|
||||
testing.expectEqual('PICTURE', picture.tagName);
|
||||
testing.expectEqual(3, picture.children.length);
|
||||
|
||||
const sources = picture.querySelectorAll('source');
|
||||
testing.expectEqual(2, sources.length);
|
||||
|
||||
// const img = picture.querySelector('img');
|
||||
// testing.expectEqual('IMG', img.tagName);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- <script id="appendChild">
|
||||
{
|
||||
const picture = document.createElement('picture');
|
||||
const source = document.createElement('source');
|
||||
const img = document.createElement('img');
|
||||
|
||||
picture.appendChild(source);
|
||||
picture.appendChild(img);
|
||||
|
||||
testing.expectEqual(2, picture.children.length);
|
||||
testing.expectEqual('SOURCE', picture.children[0].tagName);
|
||||
testing.expectEqual('IMG', picture.children[1].tagName);
|
||||
}
|
||||
</script>
|
||||
-->
|
||||
@@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../../testing.js"></script>
|
||||
|
||||
<script id="script">
|
||||
{
|
||||
let s = document.createElement('script');
|
||||
testing.expectEqual('', s.src);
|
||||
|
||||
s.src = '/over.9000.js';
|
||||
testing.expectEqual('http://127.0.0.1:9582/over.9000.js', s.src);
|
||||
}
|
||||
</script>
|
||||
@@ -42,34 +42,6 @@
|
||||
testing.expectEqual('initial text', $('#textarea1').defaultValue)
|
||||
</script>
|
||||
|
||||
<script id="defaultValue_set">
|
||||
{
|
||||
const textarea = document.createElement('textarea')
|
||||
testing.expectEqual('', textarea.defaultValue)
|
||||
testing.expectEqual('', textarea.value)
|
||||
|
||||
// Setting defaultValue should update the text content
|
||||
textarea.defaultValue = 'new default'
|
||||
testing.expectEqual('new default', textarea.defaultValue)
|
||||
testing.expectEqual('new default', textarea.value)
|
||||
testing.expectEqual('new default', textarea.textContent)
|
||||
|
||||
// Setting value should not affect defaultValue
|
||||
textarea.value = 'user input'
|
||||
testing.expectEqual('new default', textarea.defaultValue)
|
||||
testing.expectEqual('user input', textarea.value)
|
||||
|
||||
// Test setting defaultValue on element that already has content
|
||||
const textarea2 = document.createElement('textarea')
|
||||
textarea2.textContent = 'initial content'
|
||||
testing.expectEqual('initial content', textarea2.defaultValue)
|
||||
|
||||
textarea2.defaultValue = 'modified default'
|
||||
testing.expectEqual('modified default', textarea2.defaultValue)
|
||||
testing.expectEqual('modified default', textarea2.textContent)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="disabled_initial">
|
||||
testing.expectEqual(false, $('#textarea1').disabled)
|
||||
testing.expectEqual(true, $('#textarea3').disabled)
|
||||
@@ -177,93 +149,3 @@
|
||||
testing.expectFalse(textarea.outerHTML.includes('required'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="clone_basic">
|
||||
{
|
||||
const original = document.createElement('textarea')
|
||||
original.defaultValue = 'default text'
|
||||
testing.expectEqual('default text', original.value)
|
||||
|
||||
// Change the value
|
||||
original.value = 'user modified'
|
||||
testing.expectEqual('user modified', original.value)
|
||||
testing.expectEqual('default text', original.defaultValue)
|
||||
|
||||
// Clone the textarea
|
||||
const clone = original.cloneNode(true)
|
||||
|
||||
// Clone should have the runtime value copied
|
||||
testing.expectEqual('user modified', clone.value)
|
||||
testing.expectEqual('default text', clone.defaultValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="clone_preserves_user_changes">
|
||||
{
|
||||
// Create a fresh element to avoid interfering with other tests
|
||||
const original = document.createElement('textarea')
|
||||
original.textContent = 'initial text'
|
||||
testing.expectEqual('initial text', original.defaultValue)
|
||||
testing.expectEqual('initial text', original.value)
|
||||
|
||||
// User modifies the value
|
||||
original.value = 'user typed this'
|
||||
testing.expectEqual('user typed this', original.value)
|
||||
testing.expectEqual('initial text', original.defaultValue)
|
||||
|
||||
// Clone should preserve the user's changes
|
||||
const clone = original.cloneNode(true)
|
||||
testing.expectEqual('user typed this', clone.value)
|
||||
testing.expectEqual('initial text', clone.defaultValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="clone_empty_textarea">
|
||||
{
|
||||
const original = document.createElement('textarea')
|
||||
testing.expectEqual('', original.value)
|
||||
|
||||
original.value = 'some content'
|
||||
const clone = original.cloneNode(true)
|
||||
|
||||
testing.expectEqual('some content', clone.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="selectionchange_event">
|
||||
{
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = 'Hello World';
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
let eventCount = 0;
|
||||
let lastEvent = null;
|
||||
|
||||
textarea.addEventListener('selectionchange', (e) => {
|
||||
eventCount++;
|
||||
lastEvent = e;
|
||||
});
|
||||
|
||||
testing.expectEqual(0, eventCount);
|
||||
|
||||
textarea.setSelectionRange(0, 5);
|
||||
textarea.select();
|
||||
textarea.selectionStart = 3;
|
||||
textarea.selectionEnd = 8;
|
||||
|
||||
let bubbledToBody = false;
|
||||
document.body.addEventListener('selectionchange', () => {
|
||||
bubbledToBody = true;
|
||||
});
|
||||
textarea.setSelectionRange(1, 4);
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(5, eventCount);
|
||||
testing.expectEqual('selectionchange', lastEvent.type);
|
||||
testing.expectEqual(textarea, lastEvent.target);
|
||||
testing.expectEqual(true, lastEvent.bubbles);
|
||||
testing.expectEqual(false, lastEvent.cancelable);
|
||||
testing.expectEqual(true, bubbledToBody);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<div id=d1>hello <em>world</em></div>
|
||||
|
||||
<script id=outerHTML>
|
||||
const d1 = $('#d1');
|
||||
testing.expectEqual('<div id=\"d1\">hello <em>world</em></div>', d1.outerHTML);
|
||||
d1.outerHTML = '<p id=p1>spice</p>';
|
||||
// setting outerHTML doesn't update what d1 points to
|
||||
testing.expectEqual('<div id="d1">hello <em>world</em></div>', d1.outerHTML);
|
||||
|
||||
// but it does update the document
|
||||
testing.expectEqual(null, document.getElementById('d1'));
|
||||
testing.expectEqual(true, document.getElementById('p1') != null);
|
||||
testing.expectEqual('<p id="p1">spice</p>', document.getElementById('p1').outerHTML);
|
||||
// testing.expectEqual(true, document.body.outerHTML.replaceAll(/\n/g, '').startsWith('<body><p id="p1">spice</p><script id="outerHTML">'));
|
||||
|
||||
// document.getElementById('p1').outerHTML = '';
|
||||
// testing.expectEqual(null, document.getElementById('p1'));
|
||||
// testing.expectEqual(true, document.body.outerHTML.replaceAll(/\n/g, '').startsWith('<body><script id="outerHTML">'));
|
||||
</script>
|
||||
@@ -1,334 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<!-- Test 1: Basic single element replacement -->
|
||||
<div id="test1">
|
||||
<div id="parent1">
|
||||
<div id="old1">Old Content</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test1-basic-replacement">
|
||||
const old1 = $('#old1');
|
||||
const parent1 = $('#parent1');
|
||||
|
||||
testing.expectEqual(1, parent1.childElementCount);
|
||||
testing.expectEqual(old1, document.getElementById('old1'));
|
||||
|
||||
const new1 = document.createElement('div');
|
||||
new1.id = 'new1';
|
||||
new1.textContent = 'New Content';
|
||||
|
||||
old1.replaceWith(new1);
|
||||
|
||||
testing.expectEqual(1, parent1.childElementCount);
|
||||
testing.expectEqual(null, document.getElementById('old1'));
|
||||
testing.expectEqual(new1, document.getElementById('new1'));
|
||||
testing.expectEqual(parent1, new1.parentElement);
|
||||
</script>
|
||||
|
||||
<!-- Test 2: Replace with multiple elements -->
|
||||
<div id="test2">
|
||||
<div id="parent2">
|
||||
<div id="old2">Old</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test2-multiple-elements">
|
||||
const old2 = $('#old2');
|
||||
const parent2 = $('#parent2');
|
||||
|
||||
testing.expectEqual(1, parent2.childElementCount);
|
||||
|
||||
const new2a = document.createElement('div');
|
||||
new2a.id = 'new2a';
|
||||
const new2b = document.createElement('div');
|
||||
new2b.id = 'new2b';
|
||||
const new2c = document.createElement('div');
|
||||
new2c.id = 'new2c';
|
||||
|
||||
old2.replaceWith(new2a, new2b, new2c);
|
||||
|
||||
testing.expectEqual(3, parent2.childElementCount);
|
||||
testing.expectEqual(null, document.getElementById('old2'));
|
||||
testing.expectEqual(new2a, document.getElementById('new2a'));
|
||||
testing.expectEqual(new2b, document.getElementById('new2b'));
|
||||
testing.expectEqual(new2c, document.getElementById('new2c'));
|
||||
|
||||
// Check order
|
||||
testing.expectEqual(new2a, parent2.children[0]);
|
||||
testing.expectEqual(new2b, parent2.children[1]);
|
||||
testing.expectEqual(new2c, parent2.children[2]);
|
||||
</script>
|
||||
|
||||
<!-- Test 3: Replace with text nodes -->
|
||||
<div id="test3">
|
||||
<div id="parent3"><div id="old3">Old</div></div>
|
||||
</div>
|
||||
|
||||
<script id="test3-text-nodes">
|
||||
const old3 = $('#old3');
|
||||
const parent3 = $('#parent3');
|
||||
|
||||
old3.replaceWith('Text1', ' ', 'Text2');
|
||||
|
||||
testing.expectEqual(null, document.getElementById('old3'));
|
||||
testing.expectEqual('Text1 Text2', parent3.textContent);
|
||||
</script>
|
||||
|
||||
<!-- Test 4: Replace with mix of elements and text -->
|
||||
<div id="test4">
|
||||
<div id="parent4"><div id="old4">Old</div></div>
|
||||
</div>
|
||||
|
||||
<script id="test4-mixed">
|
||||
const old4 = $('#old4');
|
||||
const parent4 = $('#parent4');
|
||||
|
||||
const new4 = document.createElement('span');
|
||||
new4.id = 'new4';
|
||||
new4.textContent = 'Element';
|
||||
|
||||
old4.replaceWith('Before ', new4, ' After');
|
||||
|
||||
testing.expectEqual(null, document.getElementById('old4'));
|
||||
testing.expectEqual(new4, document.getElementById('new4'));
|
||||
testing.expectEqual('Before Element After', parent4.textContent);
|
||||
</script>
|
||||
|
||||
<!-- Test 5: Replace element not connected to document -->
|
||||
<script id="test5-not-connected">
|
||||
const disconnected = document.createElement('div');
|
||||
disconnected.id = 'disconnected5';
|
||||
|
||||
const replacement = document.createElement('div');
|
||||
replacement.id = 'replacement5';
|
||||
|
||||
// Should do nothing since element has no parent
|
||||
disconnected.replaceWith(replacement);
|
||||
|
||||
// Neither should be in the document
|
||||
testing.expectEqual(null, document.getElementById('disconnected5'));
|
||||
testing.expectEqual(null, document.getElementById('replacement5'));
|
||||
</script>
|
||||
|
||||
<!-- Test 6: Replace with nodes that already have a parent -->
|
||||
<div id="test6">
|
||||
<div id="parent6a">
|
||||
<div id="old6">Old</div>
|
||||
</div>
|
||||
<div id="parent6b">
|
||||
<div id="moving6a">Moving A</div>
|
||||
<div id="moving6b">Moving B</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test6-moving-nodes">
|
||||
const old6 = $('#old6');
|
||||
const parent6a = $('#parent6a');
|
||||
const parent6b = $('#parent6b');
|
||||
const moving6a = $('#moving6a');
|
||||
const moving6b = $('#moving6b');
|
||||
|
||||
testing.expectEqual(1, parent6a.childElementCount);
|
||||
testing.expectEqual(2, parent6b.childElementCount);
|
||||
|
||||
// Replace old6 with nodes that already have parent6b as parent
|
||||
old6.replaceWith(moving6a, moving6b);
|
||||
|
||||
// old6 should be gone
|
||||
testing.expectEqual(null, document.getElementById('old6'));
|
||||
|
||||
// parent6a should now have the moved elements
|
||||
testing.expectEqual(2, parent6a.childElementCount);
|
||||
testing.expectEqual(moving6a, parent6a.children[0]);
|
||||
testing.expectEqual(moving6b, parent6a.children[1]);
|
||||
|
||||
// parent6b should now be empty
|
||||
testing.expectEqual(0, parent6b.childElementCount);
|
||||
|
||||
// getElementById should still work
|
||||
testing.expectEqual(moving6a, document.getElementById('moving6a'));
|
||||
testing.expectEqual(moving6b, document.getElementById('moving6b'));
|
||||
testing.expectEqual(parent6a, moving6a.parentElement);
|
||||
testing.expectEqual(parent6a, moving6b.parentElement);
|
||||
</script>
|
||||
|
||||
<!-- Test 7: Replace with nested elements -->
|
||||
<div id="test7">
|
||||
<div id="parent7">
|
||||
<div id="old7">Old</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test7-nested">
|
||||
const old7 = $('#old7');
|
||||
const parent7 = $('#parent7');
|
||||
|
||||
const new7 = document.createElement('div');
|
||||
new7.id = 'new7';
|
||||
|
||||
const child7a = document.createElement('div');
|
||||
child7a.id = 'child7a';
|
||||
const child7b = document.createElement('div');
|
||||
child7b.id = 'child7b';
|
||||
|
||||
new7.appendChild(child7a);
|
||||
new7.appendChild(child7b);
|
||||
|
||||
old7.replaceWith(new7);
|
||||
|
||||
testing.expectEqual(null, document.getElementById('old7'));
|
||||
testing.expectEqual(new7, document.getElementById('new7'));
|
||||
testing.expectEqual(child7a, document.getElementById('child7a'));
|
||||
testing.expectEqual(child7b, document.getElementById('child7b'));
|
||||
testing.expectEqual(2, new7.childElementCount);
|
||||
</script>
|
||||
|
||||
<!-- Test 8: Replace maintains sibling order -->
|
||||
<div id="test8">
|
||||
<div id="parent8">
|
||||
<div id="before8">Before</div>
|
||||
<div id="old8">Old</div>
|
||||
<div id="after8">After</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test8-sibling-order">
|
||||
const old8 = $('#old8');
|
||||
const parent8 = $('#parent8');
|
||||
const before8 = $('#before8');
|
||||
const after8 = $('#after8');
|
||||
|
||||
testing.expectEqual(3, parent8.childElementCount);
|
||||
|
||||
const new8 = document.createElement('div');
|
||||
new8.id = 'new8';
|
||||
|
||||
old8.replaceWith(new8);
|
||||
|
||||
testing.expectEqual(3, parent8.childElementCount);
|
||||
testing.expectEqual(before8, parent8.children[0]);
|
||||
testing.expectEqual(new8, parent8.children[1]);
|
||||
testing.expectEqual(after8, parent8.children[2]);
|
||||
</script>
|
||||
|
||||
<!-- Test 9: Replace first child -->
|
||||
<div id="test9">
|
||||
<div id="parent9">
|
||||
<div id="first9">First</div>
|
||||
<div id="second9">Second</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test9-first-child">
|
||||
const first9 = $('#first9');
|
||||
const parent9 = $('#parent9');
|
||||
|
||||
const new9 = document.createElement('div');
|
||||
new9.id = 'new9';
|
||||
|
||||
first9.replaceWith(new9);
|
||||
|
||||
testing.expectEqual(null, document.getElementById('first9'));
|
||||
testing.expectEqual(new9, parent9.firstElementChild);
|
||||
testing.expectEqual(new9, parent9.children[0]);
|
||||
</script>
|
||||
|
||||
<!-- Test 10: Replace last child -->
|
||||
<div id="test10">
|
||||
<div id="parent10">
|
||||
<div id="first10">First</div>
|
||||
<div id="last10">Last</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test10-last-child">
|
||||
const last10 = $('#last10');
|
||||
const parent10 = $('#parent10');
|
||||
|
||||
const new10 = document.createElement('div');
|
||||
new10.id = 'new10';
|
||||
|
||||
last10.replaceWith(new10);
|
||||
|
||||
testing.expectEqual(null, document.getElementById('last10'));
|
||||
testing.expectEqual(new10, parent10.lastElementChild);
|
||||
testing.expectEqual(new10, parent10.children[1]);
|
||||
</script>
|
||||
|
||||
<!-- Test 11: Replace with empty (no arguments) - effectively removes the element -->
|
||||
<div id="test11">
|
||||
<div id="parent11">
|
||||
<div id="old11">Old</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test11-empty">
|
||||
const old11 = $('#old11');
|
||||
const parent11 = $('#parent11');
|
||||
|
||||
testing.expectEqual(1, parent11.childElementCount);
|
||||
|
||||
// Calling replaceWith() with no args should just remove the element
|
||||
old11.replaceWith();
|
||||
|
||||
// Element should be removed, leaving parent empty
|
||||
testing.expectEqual(0, parent11.childElementCount);
|
||||
testing.expectEqual(null, document.getElementById('old11'));
|
||||
testing.expectEqual(null, old11.parentElement);
|
||||
</script>
|
||||
|
||||
<!-- Test 12: Replace and check childElementCount updates -->
|
||||
<div id="test12">
|
||||
<div id="parent12">
|
||||
<div id="a12">A</div>
|
||||
<div id="b12">B</div>
|
||||
<div id="c12">C</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test12-child-count">
|
||||
const b12 = $('#b12');
|
||||
const parent12 = $('#parent12');
|
||||
|
||||
testing.expectEqual(3, parent12.childElementCount);
|
||||
|
||||
// Replace with 2 elements
|
||||
const new12a = document.createElement('div');
|
||||
new12a.id = 'new12a';
|
||||
const new12b = document.createElement('div');
|
||||
new12b.id = 'new12b';
|
||||
|
||||
b12.replaceWith(new12a, new12b);
|
||||
|
||||
testing.expectEqual(4, parent12.childElementCount);
|
||||
testing.expectEqual(null, document.getElementById('b12'));
|
||||
</script>
|
||||
|
||||
<!-- Test 13: Replace deeply nested element -->
|
||||
<div id="test13">
|
||||
<div id="l1">
|
||||
<div id="l2">
|
||||
<div id="l3">
|
||||
<div id="l4">
|
||||
<div id="old13">Deep</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="test13-deeply-nested">
|
||||
const old13 = $('#old13');
|
||||
const l4 = $('#l4');
|
||||
|
||||
const new13 = document.createElement('div');
|
||||
new13.id = 'new13';
|
||||
|
||||
old13.replaceWith(new13);
|
||||
|
||||
testing.expectEqual(null, document.getElementById('old13'));
|
||||
testing.expectEqual(new13, document.getElementById('new13'));
|
||||
testing.expectEqual(l4, new13.parentElement);
|
||||
</script>
|
||||
@@ -1,53 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<script id="removeListenerDuringDispatch">
|
||||
const target = document.createElement("div");
|
||||
|
||||
let listener1Called = 0;
|
||||
let listener2Called = 0;
|
||||
let listener3Called = 0;
|
||||
|
||||
function listener1() {
|
||||
listener1Called++;
|
||||
console.warn("listener1 called, removing listener2 and adding listener3");
|
||||
target.removeEventListener("foo", listener2);
|
||||
target.addEventListener("foo", listener3);
|
||||
}
|
||||
|
||||
function listener2() {
|
||||
listener2Called++;
|
||||
console.warn("listener2 called (SHOULD NOT HAPPEN)");
|
||||
}
|
||||
|
||||
function listener3() {
|
||||
listener3Called++;
|
||||
console.warn("listener3 called (SHOULD NOT HAPPEN IN FIRST DISPATCH)");
|
||||
}
|
||||
|
||||
target.addEventListener("foo", listener1);
|
||||
target.addEventListener("foo", listener2);
|
||||
|
||||
console.warn("Dispatching first event");
|
||||
target.dispatchEvent(new Event("foo"));
|
||||
|
||||
console.warn("After first dispatch:");
|
||||
console.warn(" listener1Called:", listener1Called);
|
||||
console.warn(" listener2Called:", listener2Called);
|
||||
console.warn(" listener3Called:", listener3Called);
|
||||
|
||||
testing.expectEqual(1, listener1Called);
|
||||
testing.expectEqual(0, listener2Called);
|
||||
testing.expectEqual(0, listener3Called);
|
||||
|
||||
console.warn("Dispatching second event");
|
||||
target.dispatchEvent(new Event("foo"));
|
||||
|
||||
console.warn("After second dispatch:");
|
||||
console.warn(" listener1Called:", listener1Called);
|
||||
console.warn(" listener2Called:", listener2Called);
|
||||
console.warn(" listener3Called:", listener3Called);
|
||||
|
||||
testing.expectEqual(2, listener1Called);
|
||||
testing.expectEqual(0, listener2Called);
|
||||
testing.expectEqual(1, listener3Called);
|
||||
</script>
|
||||
@@ -1,165 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<script id=default>
|
||||
{
|
||||
let event = new PointerEvent('pointerdown');
|
||||
testing.expectEqual('pointerdown', event.type);
|
||||
testing.expectEqual(true, event instanceof PointerEvent);
|
||||
testing.expectEqual(true, event instanceof MouseEvent);
|
||||
testing.expectEqual(true, event instanceof UIEvent);
|
||||
testing.expectEqual(true, event instanceof Event);
|
||||
testing.expectEqual(0, event.pointerId);
|
||||
testing.expectEqual('', event.pointerType);
|
||||
testing.expectEqual(1.0, event.width);
|
||||
testing.expectEqual(1.0, event.height);
|
||||
testing.expectEqual(0.0, event.pressure);
|
||||
testing.expectEqual(0.0, event.tangentialPressure);
|
||||
testing.expectEqual(0, event.tiltX);
|
||||
testing.expectEqual(0, event.tiltY);
|
||||
testing.expectEqual(0, event.twist);
|
||||
testing.expectEqual(false, event.isPrimary);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=parameters>
|
||||
{
|
||||
let new_event = new PointerEvent('pointerdown', {
|
||||
pointerId: 42,
|
||||
pointerType: 'pen',
|
||||
width: 10.5,
|
||||
height: 20.5,
|
||||
pressure: 0.75,
|
||||
tangentialPressure: -0.25,
|
||||
tiltX: 30,
|
||||
tiltY: 45,
|
||||
twist: 90,
|
||||
isPrimary: true,
|
||||
clientX: 100,
|
||||
clientY: 200,
|
||||
screenX: 300,
|
||||
screenY: 400
|
||||
});
|
||||
testing.expectEqual(42, new_event.pointerId);
|
||||
testing.expectEqual('pen', new_event.pointerType);
|
||||
testing.expectEqual(10.5, new_event.width);
|
||||
testing.expectEqual(20.5, new_event.height);
|
||||
testing.expectEqual(0.75, new_event.pressure);
|
||||
testing.expectEqual(-0.25, new_event.tangentialPressure);
|
||||
testing.expectEqual(30, new_event.tiltX);
|
||||
testing.expectEqual(45, new_event.tiltY);
|
||||
testing.expectEqual(90, new_event.twist);
|
||||
testing.expectEqual(true, new_event.isPrimary);
|
||||
testing.expectEqual(100, new_event.clientX);
|
||||
testing.expectEqual(200, new_event.clientY);
|
||||
testing.expectEqual(300, new_event.screenX);
|
||||
testing.expectEqual(400, new_event.screenY);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=mousePointerType>
|
||||
{
|
||||
let mouse_event = new PointerEvent('pointerdown', { pointerType: 'mouse' });
|
||||
testing.expectEqual('mouse', mouse_event.pointerType);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=touchPointerType>
|
||||
{
|
||||
let touch_event = new PointerEvent('pointerdown', { pointerType: 'touch', pointerId: 1, pressure: 0.5 });
|
||||
testing.expectEqual('touch', touch_event.pointerType);
|
||||
testing.expectEqual(1, touch_event.pointerId);
|
||||
testing.expectEqual(0.5, touch_event.pressure);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=listener>
|
||||
{
|
||||
let pe = new PointerEvent('pointerdown', { pointerId: 123 });
|
||||
testing.expectEqual(true, pe instanceof PointerEvent);
|
||||
testing.expectEqual(true, pe instanceof MouseEvent);
|
||||
testing.expectEqual(true, pe instanceof Event);
|
||||
|
||||
var evt = null;
|
||||
document.addEventListener('pointerdown', function (e) {
|
||||
evt = e;
|
||||
});
|
||||
document.dispatchEvent(pe);
|
||||
testing.expectEqual('pointerdown', evt.type);
|
||||
testing.expectEqual(true, evt instanceof PointerEvent);
|
||||
testing.expectEqual(123, evt.pointerId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=isTrusted>
|
||||
{
|
||||
let pointerEvent = new PointerEvent('pointerup');
|
||||
testing.expectEqual(false, pointerEvent.isTrusted);
|
||||
|
||||
let pointerIsTrusted = null;
|
||||
document.addEventListener('pointertest', (e) => {
|
||||
pointerIsTrusted = e.isTrusted;
|
||||
testing.expectEqual(true, e instanceof PointerEvent);
|
||||
});
|
||||
document.dispatchEvent(new PointerEvent('pointertest'));
|
||||
testing.expectEqual(false, pointerIsTrusted);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=eventTypes>
|
||||
{
|
||||
let down = new PointerEvent('pointerdown');
|
||||
testing.expectEqual('pointerdown', down.type);
|
||||
|
||||
let up = new PointerEvent('pointerup');
|
||||
testing.expectEqual('pointerup', up.type);
|
||||
|
||||
let move = new PointerEvent('pointermove');
|
||||
testing.expectEqual('pointermove', move.type);
|
||||
|
||||
let enter = new PointerEvent('pointerenter');
|
||||
testing.expectEqual('pointerenter', enter.type);
|
||||
|
||||
let leave = new PointerEvent('pointerleave');
|
||||
testing.expectEqual('pointerleave', leave.type);
|
||||
|
||||
let over = new PointerEvent('pointerover');
|
||||
testing.expectEqual('pointerover', over.type);
|
||||
|
||||
let out = new PointerEvent('pointerout');
|
||||
testing.expectEqual('pointerout', out.type);
|
||||
|
||||
let cancel = new PointerEvent('pointercancel');
|
||||
testing.expectEqual('pointercancel', cancel.type);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=inheritedMouseProperties>
|
||||
{
|
||||
let pe = new PointerEvent('pointerdown', {
|
||||
button: 2,
|
||||
buttons: 4,
|
||||
altKey: true,
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
metaKey: true
|
||||
});
|
||||
testing.expectEqual(2, pe.button);
|
||||
testing.expectEqual(true, pe.altKey);
|
||||
testing.expectEqual(true, pe.ctrlKey);
|
||||
testing.expectEqual(true, pe.shiftKey);
|
||||
testing.expectEqual(true, pe.metaKey);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=inheritedUIEventProperties>
|
||||
{
|
||||
let pe = new PointerEvent('pointerdown', {
|
||||
detail: 5,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
testing.expectEqual(5, pe.detail);
|
||||
testing.expectEqual(true, pe.bubbles);
|
||||
testing.expectEqual(true, pe.cancelable);
|
||||
}
|
||||
</script>
|
||||
@@ -1,22 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=project_rejection>
|
||||
{
|
||||
let e1 = new PromiseRejectionEvent("rejectionhandled");
|
||||
testing.expectEqual(true, e1 instanceof PromiseRejectionEvent);
|
||||
testing.expectEqual(true, e1 instanceof Event);
|
||||
|
||||
testing.expectEqual("rejectionhandled", e1.type);
|
||||
testing.expectEqual(null, e1.reason);
|
||||
testing.expectEqual(null, e1.promise);
|
||||
|
||||
let e2 = new PromiseRejectionEvent("rejectionhandled", {reason: ['tea']});
|
||||
testing.expectEqual(true, e2 instanceof PromiseRejectionEvent);
|
||||
testing.expectEqual(true, e2 instanceof Event);
|
||||
|
||||
testing.expectEqual("rejectionhandled", e2.type);
|
||||
testing.expectEqual(['tea'], e2.reason);
|
||||
testing.expectEqual(null, e2.promise);
|
||||
}
|
||||
</script>
|
||||
@@ -630,8 +630,3 @@
|
||||
let bubbledEvent = new Event('bubble', {bubbles: true});
|
||||
testing.expectEqual(false, bubbledEvent.isTrusted);
|
||||
</script>
|
||||
|
||||
<script id=emptyMessageEvent>
|
||||
// https://github.com/lightpanda-io/browser/pull/1316
|
||||
testing.expectError('TypeError', () => MessageEvent(''));
|
||||
</script>
|
||||
|
||||
@@ -30,9 +30,8 @@
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(true, popstateEventFired);
|
||||
testing.expectEqual({testInProgress: true }, popstateEventState);
|
||||
testing.expectEqual(state, popstateEventState);
|
||||
})
|
||||
|
||||
history.back();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="testing.js"></script>
|
||||
|
||||
<script id=history-after-nav>
|
||||
testing.expectEqual(true, history.state && history.state.testInProgress);
|
||||
</script>
|
||||
|
||||
@@ -106,17 +106,3 @@
|
||||
testing.expectEqual(req5, target);
|
||||
})
|
||||
</script>
|
||||
|
||||
<script id=xhr6 type=module>
|
||||
const req5 = new XMLHttpRequest()
|
||||
const promise5 = new Promise((resolve) => {
|
||||
req5.onload = resolve;
|
||||
req5.open('PROPFIND', 'http://127.0.0.1:9589/xhr')
|
||||
req5.send('foo')
|
||||
});
|
||||
|
||||
testing.async(promise5, () => {
|
||||
testing.expectEqual(200, req5.status);
|
||||
testing.expectEqual('OK', req5.statusText);
|
||||
testing.expectEqual(true, req5.responseText.length > 65);
|
||||
});
|
||||
|
||||
@@ -112,28 +112,3 @@
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id="microtask_access_to_records">
|
||||
testing.async(async () => {
|
||||
let savedRecords;
|
||||
const promise = new Promise((resolve) => {
|
||||
const element = document.createElement('div');
|
||||
const observer = new MutationObserver((records) => {
|
||||
// Save the records array itself
|
||||
savedRecords = records;
|
||||
resolve();
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(element, { attributes: true });
|
||||
element.setAttribute('test', 'value');
|
||||
});
|
||||
|
||||
await promise;
|
||||
// Force arena reset by making a Zig call
|
||||
document.getElementsByTagName('*');
|
||||
|
||||
testing.expectEqual(1, savedRecords.length);
|
||||
testing.expectEqual('attributes', savedRecords[0].type);
|
||||
testing.expectEqual('test', savedRecords[0].attributeName);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -130,10 +130,3 @@
|
||||
testing.expectEqual("you", request2.headers.get("target"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=propfind>
|
||||
{
|
||||
const req = new Request('https://example.com/api', { method: 'propfind' });
|
||||
testing.expectEqual('PROPFIND', req.method);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=response>
|
||||
// let response = new Response("Hello, World!");
|
||||
// testing.expectEqual(200, response.status);
|
||||
// testing.expectEqual("", response.statusText);
|
||||
// testing.expectEqual(true, response.ok);
|
||||
// testing.expectEqual("", response.url);
|
||||
// testing.expectEqual(false, response.redirected);
|
||||
let response = new Response("Hello, World!");
|
||||
testing.expectEqual(200, response.status);
|
||||
testing.expectEqual("", response.statusText);
|
||||
testing.expectEqual(true, response.ok);
|
||||
testing.expectEqual("", response.url);
|
||||
testing.expectEqual(false, response.redirected);
|
||||
|
||||
let response2 = new Response("Error occurred", {
|
||||
status: 404,
|
||||
@@ -18,29 +18,28 @@
|
||||
"Cache-Control": "no-cache"
|
||||
}
|
||||
});
|
||||
testing.expectEqual(true, true);
|
||||
// testing.expectEqual(404, response2.status);
|
||||
// testing.expectEqual("Not Found", response2.statusText);
|
||||
// testing.expectEqual(false, response2.ok);
|
||||
// testing.expectEqual("text/plain", response2.headers);
|
||||
// testing.expectEqual("test-value", response2.headers.get("X-Custom"));
|
||||
testing.expectEqual(404, response2.status);
|
||||
testing.expectEqual("Not Found", response2.statusText);
|
||||
testing.expectEqual(false, response2.ok);
|
||||
testing.expectEqual("text/plain", response2.headers.get("Content-Type"));
|
||||
testing.expectEqual("test-value", response2.headers.get("X-Custom"));
|
||||
testing.expectEqual("no-cache", response2.headers.get("cache-control"));
|
||||
|
||||
// let response3 = new Response("Created", { status: 201, statusText: "Created" });
|
||||
// testing.expectEqual("basic", response3.type);
|
||||
// testing.expectEqual(201, response3.status);
|
||||
// testing.expectEqual("Created", response3.statusText);
|
||||
// testing.expectEqual(true, response3.ok);
|
||||
let response3 = new Response("Created", { status: 201, statusText: "Created" });
|
||||
testing.expectEqual("basic", response3.type);
|
||||
testing.expectEqual(201, response3.status);
|
||||
testing.expectEqual("Created", response3.statusText);
|
||||
testing.expectEqual(true, response3.ok);
|
||||
|
||||
// let nullResponse = new Response(null);
|
||||
// testing.expectEqual(200, nullResponse.status);
|
||||
// testing.expectEqual("", nullResponse.statusText);
|
||||
let nullResponse = new Response(null);
|
||||
testing.expectEqual(200, nullResponse.status);
|
||||
testing.expectEqual("", nullResponse.statusText);
|
||||
|
||||
// let emptyResponse = new Response("");
|
||||
// testing.expectEqual(200, emptyResponse.status);
|
||||
let emptyResponse = new Response("");
|
||||
testing.expectEqual(200, emptyResponse.status);
|
||||
</script>
|
||||
|
||||
<!-- <script id=json>
|
||||
<script id=json>
|
||||
testing.async(async () => {
|
||||
const json = await new Promise((resolve) => {
|
||||
let response = new Response('[]');
|
||||
@@ -49,4 +48,3 @@
|
||||
testing.expectEqual([], json);
|
||||
});
|
||||
</script>
|
||||
-->
|
||||
|
||||
@@ -125,26 +125,6 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<script id=xhr6>
|
||||
const req6 = new XMLHttpRequest()
|
||||
testing.async(async (restore) => {
|
||||
await new Promise((resolve) => {
|
||||
req6.onload = resolve;
|
||||
req6.open('GET', 'http://127.0.0.1:9582/xhr/binary')
|
||||
req6.responseType ='arraybuffer'
|
||||
req6.send()
|
||||
});
|
||||
|
||||
restore();
|
||||
testing.expectEqual(200, req6.status);
|
||||
testing.expectEqual('OK', req6.statusText);
|
||||
testing.expectEqual(7, req6.response.byteLength);
|
||||
testing.expectEqual([0, 0, 1, 2, 0, 0, 9], new Int32Array(req6.response));
|
||||
testing.expectEqual('', typeof req6.response);
|
||||
testing.expectEqual('arraybuffer', req6.responseType);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=xhr_redirect>
|
||||
testing.async(async (restore) => {
|
||||
const req = new XMLHttpRequest();
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<body></body>
|
||||
<script id="adoptNode">
|
||||
const old = document.implementation.createHTMLDocument("");
|
||||
const div = old.createElement("div");
|
||||
div.appendChild(old.createTextNode("text"));
|
||||
|
||||
testing.expectEqual(old, div.ownerDocument);
|
||||
testing.expectEqual(old, div.firstChild.ownerDocument);
|
||||
|
||||
document.body.appendChild(div);
|
||||
|
||||
testing.expectEqual(document, div.ownerDocument);
|
||||
testing.expectEqual(document, div.firstChild.ownerDocument);
|
||||
</script>
|
||||
@@ -37,7 +37,4 @@
|
||||
testing.expectEqual(null, c2.parentNode);
|
||||
assertChildren([c3, c4], d1)
|
||||
assertChildren([], d2)
|
||||
|
||||
testing.expectEqual(c3, d1.replaceChild(c3, c3));
|
||||
assertChildren([c3, c4], d1)
|
||||
</script>
|
||||
|
||||
@@ -58,6 +58,3 @@
|
||||
testing.expectEqual(true, e.toString().includes("FailedToLoad"), {script_id: 'import-404'});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- this used to crash -->
|
||||
<script type=module src=modules/self_async.js></script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user