mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-30 17:18:57 +00:00
Merge branch 'main' into css-improvements
This commit is contained in:
4
.github/actions/install/action.yml
vendored
4
.github/actions/install/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
|||||||
zig-v8:
|
zig-v8:
|
||||||
description: 'zig v8 version to install'
|
description: 'zig v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
default: 'v0.3.3'
|
default: 'v0.3.4'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
@@ -46,7 +46,7 @@ runs:
|
|||||||
|
|
||||||
- name: Cache v8
|
- name: Cache v8
|
||||||
id: cache-v8
|
id: cache-v8
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
env:
|
env:
|
||||||
cache-name: cache-v8
|
cache-name: cache-v8
|
||||||
with:
|
with:
|
||||||
|
|||||||
10
.github/workflows/e2e-integration-test.yml
vendored
10
.github/workflows/e2e-integration-test.yml
vendored
@@ -20,11 +20,9 @@ jobs:
|
|||||||
if: github.event.pull_request.draft == false
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
@@ -32,7 +30,7 @@ jobs:
|
|||||||
run: zig build -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=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
path: |
|
path: |
|
||||||
@@ -47,7 +45,7 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -55,7 +53,7 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
|||||||
69
.github/workflows/e2e-test.yml
vendored
69
.github/workflows/e2e-test.yml
vendored
@@ -9,15 +9,13 @@ env:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [main]
|
||||||
- main
|
|
||||||
paths:
|
paths:
|
||||||
- "build.zig"
|
|
||||||
- "src/**/*.zig"
|
|
||||||
- "src/*.zig"
|
|
||||||
- "vendor/zig-js-runtime"
|
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
- "vendor/**"
|
- "src/**"
|
||||||
|
- "build.zig"
|
||||||
|
- "build.zig.zon"
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
# By default GH trigger on types opened, synchronize and reopened.
|
# By default GH trigger on types opened, synchronize and reopened.
|
||||||
@@ -29,12 +27,10 @@ on:
|
|||||||
|
|
||||||
paths:
|
paths:
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
|
- "src/**"
|
||||||
- "build.zig"
|
- "build.zig"
|
||||||
- "src/**/*.zig"
|
- "build.zig.zon"
|
||||||
- "src/*.zig"
|
|
||||||
- "vendor/**"
|
|
||||||
- ".github/**"
|
|
||||||
- "vendor/**"
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -52,8 +48,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
@@ -61,7 +55,7 @@ jobs:
|
|||||||
run: zig build -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=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
path: |
|
path: |
|
||||||
@@ -76,7 +70,7 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -84,7 +78,7 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
@@ -126,7 +120,7 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -134,7 +128,7 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
@@ -182,39 +176,41 @@ jobs:
|
|||||||
name: wba-test
|
name: wba-test
|
||||||
needs: zig-build-release
|
needs: zig-build-release
|
||||||
|
|
||||||
env:
|
|
||||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- run: echo "${{ secrets.WBA_PRIVATE_KEY_PEM }}" > private_key.pem
|
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
- run: chmod a+x ./lightpanda
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
# force a wakup of the auth server before requesting it w/ the test itself
|
||||||
|
- run: curl https://${{ vars.WBA_DOMAIN }}
|
||||||
|
|
||||||
- name: run wba test
|
- name: run wba test
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
node webbotauth/validator.js &
|
node webbotauth/validator.js &
|
||||||
VALIDATOR_PID=$!
|
VALIDATOR_PID=$!
|
||||||
sleep 2
|
sleep 5
|
||||||
|
|
||||||
./lightpanda fetch http://127.0.0.1:8989/ \
|
exec 3<<< "${{ secrets.WBA_PRIVATE_KEY_PEM }}"
|
||||||
--web_bot_auth_key_file private_key.pem \
|
|
||||||
|
./lightpanda fetch --dump http://127.0.0.1:8989/ \
|
||||||
|
--web_bot_auth_key_file /proc/self/fd/3 \
|
||||||
--web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \
|
--web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \
|
||||||
--web_bot_auth_domain ${{ vars.WBA_DOMAIN }}
|
--web_bot_auth_domain ${{ vars.WBA_DOMAIN }}
|
||||||
|
|
||||||
wait $VALIDATOR_PID
|
wait $VALIDATOR_PID
|
||||||
|
exec 3>&-
|
||||||
|
|
||||||
cdp-and-hyperfine-bench:
|
cdp-and-hyperfine-bench:
|
||||||
name: cdp-and-hyperfine-bench
|
name: cdp-and-hyperfine-bench
|
||||||
@@ -224,7 +220,6 @@ jobs:
|
|||||||
MAX_VmHWM: 28000 # 28MB (KB)
|
MAX_VmHWM: 28000 # 28MB (KB)
|
||||||
MAX_CG_PEAK: 8000 # 8MB (KB)
|
MAX_CG_PEAK: 8000 # 8MB (KB)
|
||||||
MAX_AVG_DURATION: 17
|
MAX_AVG_DURATION: 17
|
||||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
|
||||||
|
|
||||||
# How to give cgroups access to the user actions-runner on the host:
|
# How to give cgroups access to the user actions-runner on the host:
|
||||||
# $ sudo apt install cgroup-tools
|
# $ sudo apt install cgroup-tools
|
||||||
@@ -239,7 +234,7 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -247,7 +242,7 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
@@ -333,7 +328,7 @@ jobs:
|
|||||||
echo "${{github.sha}}" > commit.txt
|
echo "${{github.sha}}" > commit.txt
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
path: |
|
path: |
|
||||||
@@ -356,12 +351,12 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
||||||
credentials:
|
credentials:
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
|
|
||||||
@@ -379,7 +374,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ env:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
|
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
|
||||||
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
||||||
|
|
||||||
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
|
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
|
||||||
|
GIT_VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dgit_version={0}', github.ref_name) || '' }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -33,8 +35,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- 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 -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -72,11 +72,9 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
@@ -87,7 +85,7 @@ jobs:
|
|||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- 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 -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -116,11 +114,9 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
@@ -131,7 +127,7 @@ jobs:
|
|||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- 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 -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -158,11 +154,9 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
@@ -173,7 +167,7 @@ jobs:
|
|||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- 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 -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
42
.github/workflows/wpt.yml
vendored
42
.github/workflows/wpt.yml
vendored
@@ -10,7 +10,7 @@ env:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "23 2 * * *"
|
- cron: "21 2 * * *"
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -19,23 +19,31 @@ jobs:
|
|||||||
wpt-build-release:
|
wpt-build-release:
|
||||||
name: zig build release
|
name: zig build release
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
env:
|
||||||
timeout-minutes: 15
|
ARCH: aarch64
|
||||||
|
OS: linux
|
||||||
|
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
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
|
||||||
|
|
||||||
- name: zig build 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 }})
|
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 }})
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
path: |
|
path: |
|
||||||
@@ -45,7 +53,7 @@ jobs:
|
|||||||
wpt-build-runner:
|
wpt-build-runner:
|
||||||
name: build wpt runner
|
name: build wpt runner
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04-arm
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -59,7 +67,7 @@ jobs:
|
|||||||
CGO_ENABLED=0 go build
|
CGO_ENABLED=0 go build
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: wptrunner
|
name: wptrunner
|
||||||
path: |
|
path: |
|
||||||
@@ -73,8 +81,8 @@ jobs:
|
|||||||
- wpt-build-runner
|
- wpt-build-runner
|
||||||
|
|
||||||
# use a self host runner.
|
# use a self host runner.
|
||||||
runs-on: lpd-bench-hetzner
|
runs-on: lpd-wpt-aws
|
||||||
timeout-minutes: 180
|
timeout-minutes: 600
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -91,14 +99,14 @@ jobs:
|
|||||||
run: ./wpt manifest
|
run: ./wpt manifest
|
||||||
|
|
||||||
- name: download lightpanda release
|
- name: download lightpanda release
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
- run: chmod a+x ./lightpanda
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
- name: download wptrunner
|
- name: download wptrunner
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: wptrunner
|
name: wptrunner
|
||||||
|
|
||||||
@@ -107,8 +115,8 @@ jobs:
|
|||||||
- name: run test with json output
|
- name: run test with json output
|
||||||
run: |
|
run: |
|
||||||
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
||||||
sleep 10s
|
sleep 20s
|
||||||
./wptrunner -lpd-path ./lightpanda -json -concurrency 10 -pool 3 > wpt.json
|
./wptrunner -lpd-path ./lightpanda -json -concurrency 5 -pool 5 --mem-limit 400 > wpt.json
|
||||||
kill `cat WPT.pid`
|
kill `cat WPT.pid`
|
||||||
|
|
||||||
- name: write commit
|
- name: write commit
|
||||||
@@ -116,7 +124,7 @@ jobs:
|
|||||||
echo "${{github.sha}}" > commit.txt
|
echo "${{github.sha}}" > commit.txt
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: wpt-results
|
name: wpt-results
|
||||||
path: |
|
path: |
|
||||||
@@ -139,7 +147,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: wpt-results
|
name: wpt-results
|
||||||
|
|
||||||
|
|||||||
60
.github/workflows/zig-fmt.yml
vendored
60
.github/workflows/zig-fmt.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: zig-fmt
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
# By default GH trigger on types opened, synchronize and reopened.
|
|
||||||
# see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
|
||||||
# Since we skip the job when the PR is in draft state, we want to force CI
|
|
||||||
# running when the PR is marked ready_for_review w/o other change.
|
|
||||||
# see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
|
|
||||||
paths:
|
|
||||||
- ".github/**"
|
|
||||||
- "build.zig"
|
|
||||||
- "src/**/*.zig"
|
|
||||||
- "src/*.zig"
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
zig-fmt:
|
|
||||||
name: zig fmt
|
|
||||||
|
|
||||||
# Don't run the CI with draft PR.
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
|
||||||
- uses: mlugg/setup-zig@v2
|
|
||||||
|
|
||||||
- name: Run zig fmt
|
|
||||||
id: fmt
|
|
||||||
run: |
|
|
||||||
zig fmt --check ./*.zig ./**/*.zig 2> zig-fmt.err > zig-fmt.err2 || echo "Failed"
|
|
||||||
delimiter="$(openssl rand -hex 8)"
|
|
||||||
echo "zig_fmt_errs<<${delimiter}" >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
if [ -s zig-fmt.err ]; then
|
|
||||||
echo "// The following errors occurred:" >> "${GITHUB_OUTPUT}"
|
|
||||||
cat zig-fmt.err >> "${GITHUB_OUTPUT}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -s zig-fmt.err2 ]; then
|
|
||||||
echo "// The following files were not formatted:" >> "${GITHUB_OUTPUT}"
|
|
||||||
cat zig-fmt.err2 >> "${GITHUB_OUTPUT}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "${delimiter}" >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
- name: Fail the job
|
|
||||||
if: steps.fmt.outputs.zig_fmt_errs != ''
|
|
||||||
run: exit 1
|
|
||||||
98
.github/workflows/zig-test.yml
vendored
98
.github/workflows/zig-test.yml
vendored
@@ -5,19 +5,18 @@ env:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
||||||
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
||||||
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [main]
|
||||||
- main
|
|
||||||
paths:
|
paths:
|
||||||
- "build.zig"
|
|
||||||
- "src/**"
|
|
||||||
- "vendor/zig-js-runtime"
|
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
- "vendor/**"
|
- "src/**"
|
||||||
pull_request:
|
- "build.zig"
|
||||||
|
- "build.zig.zon"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
# By default GH trigger on types opened, synchronize and reopened.
|
# By default GH trigger on types opened, synchronize and reopened.
|
||||||
# see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
# see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||||
# Since we skip the job when the PR is in draft state, we want to force CI
|
# Since we skip the job when the PR is in draft state, we want to force CI
|
||||||
@@ -27,28 +26,63 @@ on:
|
|||||||
|
|
||||||
paths:
|
paths:
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
|
- "src/**"
|
||||||
- "build.zig"
|
- "build.zig"
|
||||||
- "src/**/*.zig"
|
- "build.zig.zon"
|
||||||
- "src/*.zig"
|
|
||||||
- "vendor/**"
|
|
||||||
- ".github/**"
|
|
||||||
- "vendor/**"
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
zig-test-debug:
|
zig-fmt:
|
||||||
name: zig test using v8 in debug mode
|
name: zig fmt
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
||||||
|
- uses: mlugg/setup-zig@v2
|
||||||
|
|
||||||
|
- name: Run zig fmt
|
||||||
|
id: fmt
|
||||||
|
run: |
|
||||||
|
zig fmt --check ./*.zig ./**/*.zig 2> zig-fmt.err > zig-fmt.err2 || echo "Failed"
|
||||||
|
delimiter="$(openssl rand -hex 8)"
|
||||||
|
echo "zig_fmt_errs<<${delimiter}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
if [ -s zig-fmt.err ]; then
|
||||||
|
echo "// The following errors occurred:" >> "${GITHUB_OUTPUT}"
|
||||||
|
cat zig-fmt.err >> "${GITHUB_OUTPUT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -s zig-fmt.err2 ]; then
|
||||||
|
echo "// The following files were not formatted:" >> "${GITHUB_OUTPUT}"
|
||||||
|
cat zig-fmt.err2 >> "${GITHUB_OUTPUT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${delimiter}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Fail the job
|
||||||
|
if: steps.fmt.outputs.zig_fmt_errs != ''
|
||||||
|
run: exit 1
|
||||||
|
|
||||||
|
zig-test-debug:
|
||||||
|
name: zig test using v8 in debug mode
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
@@ -57,21 +91,18 @@ jobs:
|
|||||||
- name: zig build test
|
- name: zig build test
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8_debug.a -Dtsan=true test
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8_debug.a -Dtsan=true test
|
||||||
|
|
||||||
zig-test:
|
zig-test-release:
|
||||||
name: zig test
|
name: zig test
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
# Don't run the CI with draft PR.
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
@@ -83,7 +114,7 @@ jobs:
|
|||||||
echo "${{github.sha}}" > commit.txt
|
echo "${{github.sha}}" > commit.txt
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
path: |
|
path: |
|
||||||
@@ -93,23 +124,22 @@ jobs:
|
|||||||
|
|
||||||
bench-fmt:
|
bench-fmt:
|
||||||
name: perf-fmt
|
name: perf-fmt
|
||||||
needs: zig-test
|
needs: zig-test-release
|
||||||
|
|
||||||
# Don't execute on PR
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
||||||
credentials:
|
credentials:
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ FROM debian:stable-slim
|
|||||||
ARG MINISIG=0.12
|
ARG MINISIG=0.12
|
||||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||||
ARG V8=14.0.365.4
|
ARG V8=14.0.365.4
|
||||||
ARG ZIG_V8=v0.3.3
|
ARG ZIG_V8=v0.3.4
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update -yq && \
|
RUN apt-get update -yq && \
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -47,7 +47,7 @@ help:
|
|||||||
|
|
||||||
# $(ZIG) commands
|
# $(ZIG) commands
|
||||||
# ------------
|
# ------------
|
||||||
.PHONY: build build-v8-snapshot build-dev run run-release shell test bench data end2end
|
.PHONY: build build-v8-snapshot build-dev run run-release test bench data end2end
|
||||||
|
|
||||||
## Build v8 snapshot
|
## Build v8 snapshot
|
||||||
build-v8-snapshot:
|
build-v8-snapshot:
|
||||||
@@ -77,11 +77,6 @@ run-debug: build-dev
|
|||||||
@printf "\033[36mRunning...\033[0m\n"
|
@printf "\033[36mRunning...\033[0m\n"
|
||||||
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
|
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
|
||||||
|
|
||||||
## Run a JS shell in debug mode
|
|
||||||
shell:
|
|
||||||
@printf "\033[36mBuilding shell...\033[0m\n"
|
|
||||||
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
|
||||||
|
|
||||||
## Test - `grep` is used to filter out the huge compile command on build
|
## Test - `grep` is used to filter out the huge compile command on build
|
||||||
ifeq ($(OS), macos)
|
ifeq ($(OS), macos)
|
||||||
test:
|
test:
|
||||||
@@ -106,4 +101,3 @@ install: build
|
|||||||
|
|
||||||
data:
|
data:
|
||||||
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
||||||
|
|
||||||
|
|||||||
@@ -190,8 +190,6 @@ Here are the key features we have implemented:
|
|||||||
|
|
||||||
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||||
|
|
||||||
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
|
|
||||||
|
|
||||||
## Build from sources
|
## Build from sources
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
@@ -200,10 +198,10 @@ Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to
|
|||||||
install it with the right version in order to build the project.
|
install it with the right version in order to build the project.
|
||||||
|
|
||||||
Lightpanda also depends on
|
Lightpanda also depends on
|
||||||
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
|
[v8](https://chromium.googlesource.com/v8/v8.git),
|
||||||
[Libcurl](https://curl.se/libcurl/) and [html5ever](https://github.com/servo/html5ever).
|
[Libcurl](https://curl.se/libcurl/) and [html5ever](https://github.com/servo/html5ever).
|
||||||
|
|
||||||
To be able to build the v8 engine for zig-js-runtime, you have to install some libs:
|
To be able to build the v8 engine, you have to install some libs:
|
||||||
|
|
||||||
For **Debian/Ubuntu based Linux**:
|
For **Debian/Ubuntu based Linux**:
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ pub fn build(b: *Build) !void {
|
|||||||
const manifest = Manifest.init(b);
|
const manifest = Manifest.init(b);
|
||||||
|
|
||||||
const git_commit = b.option([]const u8, "git_commit", "Current git commit");
|
const git_commit = b.option([]const u8, "git_commit", "Current git commit");
|
||||||
|
const git_version = b.option([]const u8, "git_version", "Current git version (from tag)");
|
||||||
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
|
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
|
||||||
const snapshot_path = b.option([]const u8, "snapshot_path", "Path to v8 snapshot");
|
const snapshot_path = b.option([]const u8, "snapshot_path", "Path to v8 snapshot");
|
||||||
|
|
||||||
var opts = b.addOptions();
|
var opts = b.addOptions();
|
||||||
opts.addOption([]const u8, "version", manifest.version);
|
opts.addOption([]const u8, "version", manifest.version);
|
||||||
opts.addOption([]const u8, "git_commit", git_commit orelse "dev");
|
opts.addOption([]const u8, "git_commit", git_commit orelse "dev");
|
||||||
|
opts.addOption(?[]const u8, "git_version", git_version orelse null);
|
||||||
opts.addOption(?[]const u8, "snapshot_path", snapshot_path);
|
opts.addOption(?[]const u8, "snapshot_path", snapshot_path);
|
||||||
|
|
||||||
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer") orelse false;
|
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer") orelse false;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
.minimum_zig_version = "0.15.2",
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.3.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.4.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH6yx3BAAGD9jSoq_ttt_bk9MectTU44s_HZxxE5LD",
|
.hash = "v8-0.0.0-xddH6_F3BAAiFvKY6R1H-gkuQlk19BkDQ0--uZuTrSup",
|
||||||
},
|
},
|
||||||
// .v8 = .{ .path = "../zig-v8-fork" },
|
// .v8 = .{ .path = "../zig-v8-fork" },
|
||||||
.brotli = .{
|
.brotli = .{
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ pub fn init(allocator: Allocator, config: *const Config) !*App {
|
|||||||
app.app_dir_path = getAndMakeAppDir(allocator);
|
app.app_dir_path = getAndMakeAppDir(allocator);
|
||||||
|
|
||||||
app.telemetry = try Telemetry.init(app, config.mode);
|
app.telemetry = try Telemetry.init(app, config.mode);
|
||||||
errdefer app.telemetry.deinit();
|
errdefer app.telemetry.deinit(allocator);
|
||||||
|
|
||||||
app.arena_pool = ArenaPool.init(allocator, 512, 1024 * 16);
|
app.arena_pool = ArenaPool.init(allocator, 512, 1024 * 16);
|
||||||
errdefer app.arena_pool.deinit();
|
errdefer app.arena_pool.deinit();
|
||||||
@@ -85,7 +85,7 @@ pub fn deinit(self: *App) void {
|
|||||||
allocator.free(app_dir_path);
|
allocator.free(app_dir_path);
|
||||||
self.app_dir_path = null;
|
self.app_dir_path = null;
|
||||||
}
|
}
|
||||||
self.telemetry.deinit();
|
self.telemetry.deinit(allocator);
|
||||||
self.network.deinit();
|
self.network.deinit();
|
||||||
self.snapshot.deinit();
|
self.snapshot.deinit();
|
||||||
self.platform.deinit();
|
self.platform.deinit();
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ dom_node: *Node,
|
|||||||
registry: *CDPNode.Registry,
|
registry: *CDPNode.Registry,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
arena: std.mem.Allocator,
|
arena: std.mem.Allocator,
|
||||||
prune: bool = false,
|
prune: bool = true,
|
||||||
|
interactive_only: bool = false,
|
||||||
|
max_depth: u32 = std.math.maxInt(u32) - 1,
|
||||||
|
|
||||||
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!void {
|
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!void {
|
||||||
var visitor = JsonVisitor{ .jw = jw, .tree = self };
|
var visitor = JsonVisitor{ .jw = jw, .tree = self };
|
||||||
@@ -47,7 +49,7 @@ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!
|
|||||||
};
|
};
|
||||||
var visibility_cache: Element.VisibilityCache = .empty;
|
var visibility_cache: Element.VisibilityCache = .empty;
|
||||||
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
||||||
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &visibility_cache, &pointer_events_cache) catch |err| {
|
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &visibility_cache, &pointer_events_cache, 0) catch |err| {
|
||||||
log.err(.app, "semantic tree json dump failed", .{ .err = err });
|
log.err(.app, "semantic tree json dump failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
@@ -62,7 +64,7 @@ pub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!v
|
|||||||
};
|
};
|
||||||
var visibility_cache: Element.VisibilityCache = .empty;
|
var visibility_cache: Element.VisibilityCache = .empty;
|
||||||
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
var pointer_events_cache: Element.PointerEventsCache = .empty;
|
||||||
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &visibility_cache, &pointer_events_cache) catch |err| {
|
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, &visibility_cache, &pointer_events_cache, 0) catch |err| {
|
||||||
log.err(.app, "semantic tree text dump failed", .{ .err = err });
|
log.err(.app, "semantic tree text dump failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
@@ -75,7 +77,7 @@ const OptionData = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NodeData = struct {
|
const NodeData = struct {
|
||||||
id: u32,
|
id: CDPNode.Id,
|
||||||
axn: AXNode,
|
axn: AXNode,
|
||||||
role: []const u8,
|
role: []const u8,
|
||||||
name: ?[]const u8,
|
name: ?[]const u8,
|
||||||
@@ -86,7 +88,9 @@ const NodeData = struct {
|
|||||||
node_name: []const u8,
|
node_name: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap, visibility_cache: ?*Element.VisibilityCache, pointer_events_cache: ?*Element.PointerEventsCache) !void {
|
fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap, visibility_cache: ?*Element.VisibilityCache, pointer_events_cache: ?*Element.PointerEventsCache, current_depth: u32) !void {
|
||||||
|
if (current_depth > self.max_depth) return;
|
||||||
|
|
||||||
// 1. Skip non-content nodes
|
// 1. Skip non-content nodes
|
||||||
if (node.is(Element)) |el| {
|
if (node.is(Element)) |el| {
|
||||||
const tag = el.getTag();
|
const tag = el.getTag();
|
||||||
@@ -178,7 +182,23 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
};
|
};
|
||||||
|
|
||||||
var should_visit = true;
|
var should_visit = true;
|
||||||
if (self.prune) {
|
if (self.interactive_only) {
|
||||||
|
var keep = false;
|
||||||
|
if (interactive.isInteractiveRole(role)) {
|
||||||
|
keep = true;
|
||||||
|
} else if (interactive.isContentRole(role)) {
|
||||||
|
if (name != null and name.?.len > 0) {
|
||||||
|
keep = true;
|
||||||
|
}
|
||||||
|
} else if (std.mem.eql(u8, role, "RootWebArea")) {
|
||||||
|
keep = true;
|
||||||
|
} else if (is_interactive) {
|
||||||
|
keep = true;
|
||||||
|
}
|
||||||
|
if (!keep) {
|
||||||
|
should_visit = false;
|
||||||
|
}
|
||||||
|
} else if (self.prune) {
|
||||||
if (structural and !is_interactive and !has_explicit_label) {
|
if (structural and !is_interactive and !has_explicit_label) {
|
||||||
should_visit = false;
|
should_visit = false;
|
||||||
}
|
}
|
||||||
@@ -217,7 +237,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
}
|
}
|
||||||
gop.value_ptr.* += 1;
|
gop.value_ptr.* += 1;
|
||||||
|
|
||||||
try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets, visibility_cache, pointer_events_cache);
|
try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets, visibility_cache, pointer_events_cache, current_depth + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,36 +413,45 @@ const TextVisitor = struct {
|
|||||||
depth: usize,
|
depth: usize,
|
||||||
|
|
||||||
pub fn visit(self: *TextVisitor, node: *Node, data: *NodeData) !bool {
|
pub fn visit(self: *TextVisitor, node: *Node, data: *NodeData) !bool {
|
||||||
// Format: " [12] link: Hacker News (value)"
|
for (0..self.depth) |_| {
|
||||||
for (0..(self.depth * 2)) |_| {
|
|
||||||
try self.writer.writeByte(' ');
|
try self.writer.writeByte(' ');
|
||||||
}
|
}
|
||||||
try self.writer.print("[{d}] {s}: ", .{ data.id, data.role });
|
|
||||||
|
|
||||||
|
var name_to_print: ?[]const u8 = null;
|
||||||
if (data.name) |n| {
|
if (data.name) |n| {
|
||||||
if (n.len > 0) {
|
if (n.len > 0) {
|
||||||
try self.writer.writeAll(n);
|
name_to_print = n;
|
||||||
}
|
}
|
||||||
} else if (node.is(CData.Text)) |text_node| {
|
} else if (node.is(CData.Text)) |text_node| {
|
||||||
const trimmed = std.mem.trim(u8, text_node.getWholeText(), " \t\r\n");
|
const trimmed = std.mem.trim(u8, text_node.getWholeText(), " \t\r\n");
|
||||||
if (trimmed.len > 0) {
|
if (trimmed.len > 0) {
|
||||||
try self.writer.writeAll(trimmed);
|
name_to_print = trimmed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const is_text_only = std.mem.eql(u8, data.role, "StaticText") or std.mem.eql(u8, data.role, "none") or std.mem.eql(u8, data.role, "generic");
|
||||||
|
|
||||||
|
try self.writer.print("{d}", .{data.id});
|
||||||
|
if (!is_text_only) {
|
||||||
|
try self.writer.print(" {s}", .{data.role});
|
||||||
|
}
|
||||||
|
if (name_to_print) |n| {
|
||||||
|
try self.writer.print(" '{s}'", .{n});
|
||||||
|
}
|
||||||
|
|
||||||
if (data.value) |v| {
|
if (data.value) |v| {
|
||||||
if (v.len > 0) {
|
if (v.len > 0) {
|
||||||
try self.writer.print(" (value: {s})", .{v});
|
try self.writer.print(" value='{s}'", .{v});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.options) |options| {
|
if (data.options) |options| {
|
||||||
try self.writer.writeAll(" options: [");
|
try self.writer.writeAll(" options=[");
|
||||||
for (options, 0..) |opt, i| {
|
for (options, 0..) |opt, i| {
|
||||||
if (i > 0) try self.writer.writeAll(", ");
|
if (i > 0) try self.writer.writeAll(",");
|
||||||
try self.writer.print("'{s}'", .{opt.value});
|
try self.writer.print("'{s}'", .{opt.value});
|
||||||
if (opt.selected) {
|
if (opt.selected) {
|
||||||
try self.writer.writeAll(" (selected)");
|
try self.writer.writeAll("*");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try self.writer.writeAll("]\n");
|
try self.writer.writeAll("]\n");
|
||||||
@@ -452,3 +481,56 @@ const TextVisitor = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testing = @import("testing.zig");
|
||||||
|
|
||||||
|
test "SemanticTree backendDOMNodeId" {
|
||||||
|
var registry: CDPNode.Registry = .init(testing.allocator);
|
||||||
|
defer registry.deinit();
|
||||||
|
|
||||||
|
var page = try testing.pageTest("cdp/registry1.html");
|
||||||
|
defer testing.reset();
|
||||||
|
defer page._session.removePage();
|
||||||
|
|
||||||
|
const st: Self = .{
|
||||||
|
.dom_node = page.window._document.asNode(),
|
||||||
|
.registry = ®istry,
|
||||||
|
.page = page,
|
||||||
|
.arena = testing.arena_allocator,
|
||||||
|
.prune = false,
|
||||||
|
.interactive_only = false,
|
||||||
|
.max_depth = std.math.maxInt(u32) - 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const json_str = try std.json.Stringify.valueAlloc(testing.allocator, st, .{});
|
||||||
|
defer testing.allocator.free(json_str);
|
||||||
|
|
||||||
|
try testing.expect(std.mem.indexOf(u8, json_str, "\"backendDOMNodeId\":") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "SemanticTree max_depth" {
|
||||||
|
var registry: CDPNode.Registry = .init(testing.allocator);
|
||||||
|
defer registry.deinit();
|
||||||
|
|
||||||
|
var page = try testing.pageTest("cdp/registry1.html");
|
||||||
|
defer testing.reset();
|
||||||
|
defer page._session.removePage();
|
||||||
|
|
||||||
|
const st: Self = .{
|
||||||
|
.dom_node = page.window._document.asNode(),
|
||||||
|
.registry = ®istry,
|
||||||
|
.page = page,
|
||||||
|
.arena = testing.arena_allocator,
|
||||||
|
.prune = false,
|
||||||
|
.interactive_only = false,
|
||||||
|
.max_depth = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||||
|
defer aw.deinit();
|
||||||
|
|
||||||
|
try st.textStringify(&aw.writer);
|
||||||
|
const text_str = aw.written();
|
||||||
|
|
||||||
|
try testing.expect(std.mem.indexOf(u8, text_str, "other") == null);
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,17 +64,17 @@ pub fn init(app: *App, address: net.Address) !*Server {
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Server) void {
|
pub fn shutdown(self: *Server) void {
|
||||||
// Stop all active clients
|
self.client_mutex.lock();
|
||||||
{
|
defer self.client_mutex.unlock();
|
||||||
self.client_mutex.lock();
|
|
||||||
defer self.client_mutex.unlock();
|
|
||||||
|
|
||||||
for (self.clients.items) |client| {
|
for (self.clients.items) |client| {
|
||||||
client.stop();
|
client.stop();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Server) void {
|
||||||
|
self.shutdown();
|
||||||
self.joinThreads();
|
self.joinThreads();
|
||||||
self.clients.deinit(self.allocator);
|
self.clients.deinit(self.allocator);
|
||||||
self.clients_pool.deinit();
|
self.clients_pool.deinit();
|
||||||
@@ -242,7 +242,10 @@ pub const Client = struct {
|
|||||||
fn stop(self: *Client) void {
|
fn stop(self: *Client) void {
|
||||||
switch (self.mode) {
|
switch (self.mode) {
|
||||||
.http => {},
|
.http => {},
|
||||||
.cdp => |*cdp| cdp.browser.env.terminate(),
|
.cdp => |*cdp| {
|
||||||
|
cdp.browser.env.terminate();
|
||||||
|
self.ws.sendClose();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
self.ws.shutdown();
|
self.ws.shutdown();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,25 +91,32 @@ pub fn runMicrotasks(self: *Browser) void {
|
|||||||
self.env.runMicrotasks();
|
self.env.runMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMacrotasks(self: *Browser) !?u64 {
|
pub fn runMacrotasks(self: *Browser) !void {
|
||||||
const env = &self.env;
|
const env = &self.env;
|
||||||
|
|
||||||
const time_to_next = try self.env.runMacrotasks();
|
try self.env.runMacrotasks();
|
||||||
env.pumpMessageLoop();
|
env.pumpMessageLoop();
|
||||||
|
|
||||||
// either of the above could have queued more microtasks
|
// either of the above could have queued more microtasks
|
||||||
env.runMicrotasks();
|
env.runMicrotasks();
|
||||||
|
|
||||||
return time_to_next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hasBackgroundTasks(self: *Browser) bool {
|
pub fn hasBackgroundTasks(self: *Browser) bool {
|
||||||
return self.env.hasBackgroundTasks();
|
return self.env.hasBackgroundTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn waitForBackgroundTasks(self: *Browser) void {
|
pub fn waitForBackgroundTasks(self: *Browser) void {
|
||||||
self.env.waitForBackgroundTasks();
|
self.env.waitForBackgroundTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn msToNextMacrotask(self: *Browser) ?u64 {
|
||||||
|
return self.env.msToNextMacrotask();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn msTo(self: *Browser) bool {
|
||||||
|
return self.env.hasBackgroundTasks();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn runIdleTasks(self: *const Browser) void {
|
pub fn runIdleTasks(self: *const Browser) void {
|
||||||
self.env.runIdleTasks();
|
self.env.runIdleTasks();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,6 +233,12 @@ const DispatchDirectOptions = struct {
|
|||||||
pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, handler: anytype, comptime opts: DispatchDirectOptions) !void {
|
pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, handler: anytype, comptime opts: DispatchDirectOptions) !void {
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
|
|
||||||
|
// Set window.event to the currently dispatching event (WHATWG spec)
|
||||||
|
const window = page.window;
|
||||||
|
const prev_event = window._current_event;
|
||||||
|
window._current_event = event;
|
||||||
|
defer window._current_event = prev_event;
|
||||||
|
|
||||||
event.acquireRef();
|
event.acquireRef();
|
||||||
defer event.deinit(false, page._session);
|
defer event.deinit(false, page._session);
|
||||||
|
|
||||||
@@ -398,6 +404,13 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
|||||||
}
|
}
|
||||||
|
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
|
|
||||||
|
// Set window.event to the currently dispatching event (WHATWG spec)
|
||||||
|
const window = page.window;
|
||||||
|
const prev_event = window._current_event;
|
||||||
|
window._current_event = event;
|
||||||
|
defer window._current_event = prev_event;
|
||||||
|
|
||||||
var was_handled = false;
|
var was_handled = false;
|
||||||
|
|
||||||
// Create a single scope for all event handlers in this dispatch.
|
// Create a single scope for all event handlers in this dispatch.
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ use_proxy: bool,
|
|||||||
// Current TLS verification state, applied per-connection in makeRequest.
|
// Current TLS verification state, applied per-connection in makeRequest.
|
||||||
tls_verify: bool = true,
|
tls_verify: bool = true,
|
||||||
|
|
||||||
|
obey_robots: bool,
|
||||||
|
|
||||||
cdp_client: ?CDPClient = null,
|
cdp_client: ?CDPClient = null,
|
||||||
|
|
||||||
// libcurl can monitor arbitrary sockets, this lets us use libcurl to poll
|
// libcurl can monitor arbitrary sockets, this lets us use libcurl to poll
|
||||||
@@ -154,6 +156,7 @@ pub fn init(allocator: Allocator, network: *Network) !*Client {
|
|||||||
.http_proxy = http_proxy,
|
.http_proxy = http_proxy,
|
||||||
.use_proxy = http_proxy != null,
|
.use_proxy = http_proxy != null,
|
||||||
.tls_verify = network.config.tlsVerifyHost(),
|
.tls_verify = network.config.tlsVerifyHost(),
|
||||||
|
.obey_robots = network.config.obeyRobots(),
|
||||||
.transfer_pool = transfer_pool,
|
.transfer_pool = transfer_pool,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -257,34 +260,33 @@ pub fn tick(self: *Client, timeout_ms: u32) !PerformStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn request(self: *Client, req: Request) !void {
|
pub fn request(self: *Client, req: Request) !void {
|
||||||
if (self.network.config.obeyRobots()) {
|
if (self.obey_robots == false) {
|
||||||
const robots_url = try URL.getRobotsUrl(self.allocator, req.url);
|
return self.processRequest(req);
|
||||||
errdefer self.allocator.free(robots_url);
|
|
||||||
|
|
||||||
// If we have this robots cached, we can take a fast path.
|
|
||||||
if (self.network.robot_store.get(robots_url)) |robot_entry| {
|
|
||||||
defer self.allocator.free(robots_url);
|
|
||||||
|
|
||||||
switch (robot_entry) {
|
|
||||||
// If we have a found robots entry, we check it.
|
|
||||||
.present => |robots| {
|
|
||||||
const path = URL.getPathname(req.url);
|
|
||||||
if (!robots.isAllowed(path)) {
|
|
||||||
req.error_callback(req.ctx, error.RobotsBlocked);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Otherwise, we assume we won't find it again.
|
|
||||||
.absent => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.processRequest(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.fetchRobotsThenProcessRequest(robots_url, req);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.processRequest(req);
|
const robots_url = try URL.getRobotsUrl(self.allocator, req.url);
|
||||||
|
errdefer self.allocator.free(robots_url);
|
||||||
|
|
||||||
|
// If we have this robots cached, we can take a fast path.
|
||||||
|
if (self.network.robot_store.get(robots_url)) |robot_entry| {
|
||||||
|
defer self.allocator.free(robots_url);
|
||||||
|
|
||||||
|
switch (robot_entry) {
|
||||||
|
// If we have a found robots entry, we check it.
|
||||||
|
.present => |robots| {
|
||||||
|
const path = URL.getPathname(req.url);
|
||||||
|
if (!robots.isAllowed(path)) {
|
||||||
|
req.error_callback(req.ctx, error.RobotsBlocked);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Otherwise, we assume we won't find it again.
|
||||||
|
.absent => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.processRequest(req);
|
||||||
|
}
|
||||||
|
return self.fetchRobotsThenProcessRequest(robots_url, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn processRequest(self: *Client, req: Request) !void {
|
fn processRequest(self: *Client, req: Request) !void {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ params: []const u8 = "",
|
|||||||
// We keep 41 for null-termination since HTML parser expects in this format.
|
// We keep 41 for null-termination since HTML parser expects in this format.
|
||||||
charset: [41]u8 = default_charset,
|
charset: [41]u8 = default_charset,
|
||||||
charset_len: usize = default_charset_len,
|
charset_len: usize = default_charset_len,
|
||||||
|
is_default_charset: bool = true,
|
||||||
|
|
||||||
/// String "UTF-8" continued by null characters.
|
/// String "UTF-8" continued by null characters.
|
||||||
const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
||||||
@@ -130,6 +131,7 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
|
|
||||||
var charset: [41]u8 = default_charset;
|
var charset: [41]u8 = default_charset;
|
||||||
var charset_len: usize = default_charset_len;
|
var charset_len: usize = default_charset_len;
|
||||||
|
var has_explicit_charset = false;
|
||||||
|
|
||||||
var it = std.mem.splitScalar(u8, params, ';');
|
var it = std.mem.splitScalar(u8, params, ';');
|
||||||
while (it.next()) |attr| {
|
while (it.next()) |attr| {
|
||||||
@@ -156,6 +158,7 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
// Null-terminate right after attribute value.
|
// Null-terminate right after attribute value.
|
||||||
charset[attribute_value.len] = 0;
|
charset[attribute_value.len] = 0;
|
||||||
charset_len = attribute_value.len;
|
charset_len = attribute_value.len;
|
||||||
|
has_explicit_charset = true;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,9 +168,137 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
.charset = charset,
|
.charset = charset,
|
||||||
.charset_len = charset_len,
|
.charset_len = charset_len,
|
||||||
.content_type = content_type,
|
.content_type = content_type,
|
||||||
|
.is_default_charset = !has_explicit_charset,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prescan the first 1024 bytes of an HTML document for a charset declaration.
|
||||||
|
/// Looks for `<meta charset="X">` and `<meta http-equiv="Content-Type" content="...;charset=X">`.
|
||||||
|
/// Returns the charset value or null if none found.
|
||||||
|
/// See: https://www.w3.org/International/questions/qa-html-encoding-declarations
|
||||||
|
pub fn prescanCharset(html: []const u8) ?[]const u8 {
|
||||||
|
const limit = @min(html.len, 1024);
|
||||||
|
const data = html[0..limit];
|
||||||
|
|
||||||
|
// Scan for <meta tags
|
||||||
|
var pos: usize = 0;
|
||||||
|
while (pos < data.len) {
|
||||||
|
// Find next '<'
|
||||||
|
pos = std.mem.indexOfScalarPos(u8, data, pos, '<') orelse return null;
|
||||||
|
pos += 1;
|
||||||
|
if (pos >= data.len) return null;
|
||||||
|
|
||||||
|
// Check for "meta" (case-insensitive)
|
||||||
|
if (pos + 4 >= data.len) return null;
|
||||||
|
var tag_buf: [4]u8 = undefined;
|
||||||
|
_ = std.ascii.lowerString(&tag_buf, data[pos..][0..4]);
|
||||||
|
if (!std.mem.eql(u8, &tag_buf, "meta")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pos += 4;
|
||||||
|
|
||||||
|
// Must be followed by whitespace or end of tag
|
||||||
|
if (pos >= data.len) return null;
|
||||||
|
if (data[pos] != ' ' and data[pos] != '\t' and data[pos] != '\n' and
|
||||||
|
data[pos] != '\r' and data[pos] != '/')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan attributes within this meta tag
|
||||||
|
const tag_end = std.mem.indexOfScalarPos(u8, data, pos, '>') orelse return null;
|
||||||
|
const attrs = data[pos..tag_end];
|
||||||
|
|
||||||
|
// Look for charset= attribute directly
|
||||||
|
if (findAttrValue(attrs, "charset")) |charset| {
|
||||||
|
if (charset.len > 0 and charset.len <= 40) return charset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for http-equiv="content-type" with content="...;charset=X"
|
||||||
|
if (findAttrValue(attrs, "http-equiv")) |he| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(he, "content-type")) {
|
||||||
|
if (findAttrValue(attrs, "content")) |content| {
|
||||||
|
if (extractCharsetFromContentType(content)) |charset| {
|
||||||
|
return charset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = tag_end + 1;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findAttrValue(attrs: []const u8, name: []const u8) ?[]const u8 {
|
||||||
|
var pos: usize = 0;
|
||||||
|
while (pos < attrs.len) {
|
||||||
|
// Skip whitespace
|
||||||
|
while (pos < attrs.len and (attrs[pos] == ' ' or attrs[pos] == '\t' or
|
||||||
|
attrs[pos] == '\n' or attrs[pos] == '\r'))
|
||||||
|
{
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if (pos >= attrs.len) return null;
|
||||||
|
|
||||||
|
// Read attribute name
|
||||||
|
const attr_start = pos;
|
||||||
|
while (pos < attrs.len and attrs[pos] != '=' and attrs[pos] != ' ' and
|
||||||
|
attrs[pos] != '\t' and attrs[pos] != '>' and attrs[pos] != '/')
|
||||||
|
{
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
const attr_name = attrs[attr_start..pos];
|
||||||
|
|
||||||
|
// Skip whitespace around =
|
||||||
|
while (pos < attrs.len and (attrs[pos] == ' ' or attrs[pos] == '\t')) pos += 1;
|
||||||
|
if (pos >= attrs.len or attrs[pos] != '=') {
|
||||||
|
// No '=' found - skip this token. Advance at least one byte to avoid infinite loop.
|
||||||
|
if (pos == attr_start) pos += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pos += 1; // skip '='
|
||||||
|
while (pos < attrs.len and (attrs[pos] == ' ' or attrs[pos] == '\t')) pos += 1;
|
||||||
|
if (pos >= attrs.len) return null;
|
||||||
|
|
||||||
|
// Read attribute value
|
||||||
|
const value = blk: {
|
||||||
|
if (attrs[pos] == '"' or attrs[pos] == '\'') {
|
||||||
|
const quote = attrs[pos];
|
||||||
|
pos += 1;
|
||||||
|
const val_start = pos;
|
||||||
|
while (pos < attrs.len and attrs[pos] != quote) pos += 1;
|
||||||
|
const val = attrs[val_start..pos];
|
||||||
|
if (pos < attrs.len) pos += 1; // skip closing quote
|
||||||
|
break :blk val;
|
||||||
|
} else {
|
||||||
|
const val_start = pos;
|
||||||
|
while (pos < attrs.len and attrs[pos] != ' ' and attrs[pos] != '\t' and
|
||||||
|
attrs[pos] != '>' and attrs[pos] != '/')
|
||||||
|
{
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
break :blk attrs[val_start..pos];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(attr_name, name)) return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extractCharsetFromContentType(content: []const u8) ?[]const u8 {
|
||||||
|
var it = std.mem.splitScalar(u8, content, ';');
|
||||||
|
while (it.next()) |part| {
|
||||||
|
const trimmed = std.mem.trimLeft(u8, part, &.{ ' ', '\t' });
|
||||||
|
if (trimmed.len > 8 and std.ascii.eqlIgnoreCase(trimmed[0..8], "charset=")) {
|
||||||
|
const val = std.mem.trim(u8, trimmed[8..], &.{ ' ', '\t', '"', '\'' });
|
||||||
|
if (val.len > 0 and val.len <= 40) return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sniff(body: []const u8) ?Mime {
|
pub fn sniff(body: []const u8) ?Mime {
|
||||||
// 0x0C is form feed
|
// 0x0C is form feed
|
||||||
const content = std.mem.trimLeft(u8, body, &.{ ' ', '\t', '\n', '\r', 0x0C });
|
const content = std.mem.trimLeft(u8, body, &.{ ' ', '\t', '\n', '\r', 0x0C });
|
||||||
@@ -178,15 +309,30 @@ pub fn sniff(body: []const u8) ?Mime {
|
|||||||
if (content[0] != '<') {
|
if (content[0] != '<') {
|
||||||
if (std.mem.startsWith(u8, content, &.{ 0xEF, 0xBB, 0xBF })) {
|
if (std.mem.startsWith(u8, content, &.{ 0xEF, 0xBB, 0xBF })) {
|
||||||
// UTF-8 BOM
|
// UTF-8 BOM
|
||||||
return .{ .content_type = .{ .text_plain = {} } };
|
return .{
|
||||||
|
.content_type = .{ .text_plain = {} },
|
||||||
|
.charset = default_charset,
|
||||||
|
.charset_len = default_charset_len,
|
||||||
|
.is_default_charset = false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (std.mem.startsWith(u8, content, &.{ 0xFE, 0xFF })) {
|
if (std.mem.startsWith(u8, content, &.{ 0xFE, 0xFF })) {
|
||||||
// UTF-16 big-endian BOM
|
// UTF-16 big-endian BOM
|
||||||
return .{ .content_type = .{ .text_plain = {} } };
|
return .{
|
||||||
|
.content_type = .{ .text_plain = {} },
|
||||||
|
.charset = .{ 'U', 'T', 'F', '-', '1', '6', 'B', 'E' } ++ .{0} ** 33,
|
||||||
|
.charset_len = 8,
|
||||||
|
.is_default_charset = false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (std.mem.startsWith(u8, content, &.{ 0xFF, 0xFE })) {
|
if (std.mem.startsWith(u8, content, &.{ 0xFF, 0xFE })) {
|
||||||
// UTF-16 little-endian BOM
|
// UTF-16 little-endian BOM
|
||||||
return .{ .content_type = .{ .text_plain = {} } };
|
return .{
|
||||||
|
.content_type = .{ .text_plain = {} },
|
||||||
|
.charset = .{ 'U', 'T', 'F', '-', '1', '6', 'L', 'E' } ++ .{0} ** 33,
|
||||||
|
.charset_len = 8,
|
||||||
|
.is_default_charset = false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -540,6 +686,24 @@ test "Mime: sniff" {
|
|||||||
|
|
||||||
try expectHTML("<!-->");
|
try expectHTML("<!-->");
|
||||||
try expectHTML(" \n\t <!-->");
|
try expectHTML(" \n\t <!-->");
|
||||||
|
|
||||||
|
{
|
||||||
|
const mime = Mime.sniff(&.{ 0xEF, 0xBB, 0xBF }).?;
|
||||||
|
try testing.expectEqual(.text_plain, std.meta.activeTag(mime.content_type));
|
||||||
|
try testing.expectEqual("UTF-8", mime.charsetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const mime = Mime.sniff(&.{ 0xFE, 0xFF }).?;
|
||||||
|
try testing.expectEqual(.text_plain, std.meta.activeTag(mime.content_type));
|
||||||
|
try testing.expectEqual("UTF-16BE", mime.charsetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const mime = Mime.sniff(&.{ 0xFF, 0xFE }).?;
|
||||||
|
try testing.expectEqual(.text_plain, std.meta.activeTag(mime.content_type));
|
||||||
|
try testing.expectEqual("UTF-16LE", mime.charsetString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Expectation = struct {
|
const Expectation = struct {
|
||||||
@@ -576,3 +740,35 @@ fn expect(expected: Expectation, input: []const u8) !void {
|
|||||||
try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ());
|
try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Mime: prescanCharset" {
|
||||||
|
// <meta charset="X">
|
||||||
|
try testing.expectEqual("utf-8", Mime.prescanCharset("<html><head><meta charset=\"utf-8\">").?);
|
||||||
|
try testing.expectEqual("iso-8859-1", Mime.prescanCharset("<html><head><meta charset=\"iso-8859-1\">").?);
|
||||||
|
try testing.expectEqual("shift_jis", Mime.prescanCharset("<meta charset='shift_jis'>").?);
|
||||||
|
|
||||||
|
// Case-insensitive tag matching
|
||||||
|
try testing.expectEqual("utf-8", Mime.prescanCharset("<META charset=\"utf-8\">").?);
|
||||||
|
try testing.expectEqual("utf-8", Mime.prescanCharset("<Meta charset=\"utf-8\">").?);
|
||||||
|
|
||||||
|
// <meta http-equiv="Content-Type" content="text/html; charset=X">
|
||||||
|
try testing.expectEqual(
|
||||||
|
"iso-8859-1",
|
||||||
|
Mime.prescanCharset("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\">").?,
|
||||||
|
);
|
||||||
|
|
||||||
|
// No charset found
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset("<html><head><title>Test</title>"));
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset(""));
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset("no html here"));
|
||||||
|
|
||||||
|
// Self-closing meta without charset must not loop forever
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset("<meta foo=\"bar\"/>"));
|
||||||
|
|
||||||
|
// Charset after 1024 bytes should not be found
|
||||||
|
var long_html: [1100]u8 = undefined;
|
||||||
|
@memset(&long_html, ' ');
|
||||||
|
const suffix = "<meta charset=\"windows-1252\">";
|
||||||
|
@memcpy(long_html[1050 .. 1050 + suffix.len], suffix);
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset(&long_html));
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const storage = @import("webapi/storage/storage.zig");
|
|||||||
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
|
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
|
||||||
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
|
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
|
||||||
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
|
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
|
||||||
|
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||||
|
|
||||||
const HttpClient = @import("HttpClient.zig");
|
const HttpClient = @import("HttpClient.zig");
|
||||||
const ArenaPool = App.ArenaPool;
|
const ArenaPool = App.ArenaPool;
|
||||||
@@ -313,14 +314,16 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
|
|||||||
document._page = self;
|
document._page = self;
|
||||||
|
|
||||||
if (comptime builtin.is_test == false) {
|
if (comptime builtin.is_test == false) {
|
||||||
// HTML test runner manually calls these as necessary
|
if (parent == null) {
|
||||||
try self.js.scheduler.add(session.browser, struct {
|
// HTML test runner manually calls these as necessary
|
||||||
fn runIdleTasks(ctx: *anyopaque) !?u32 {
|
try self.js.scheduler.add(session.browser, struct {
|
||||||
const b: *@import("Browser.zig") = @ptrCast(@alignCast(ctx));
|
fn runIdleTasks(ctx: *anyopaque) !?u32 {
|
||||||
b.runIdleTasks();
|
const b: *@import("Browser.zig") = @ptrCast(@alignCast(ctx));
|
||||||
return 200;
|
b.runIdleTasks();
|
||||||
}
|
return 200;
|
||||||
}.runIdleTasks, 200, .{ .name = "page.runIdleTasks", .low_priority = true });
|
}
|
||||||
|
}.runIdleTasks, 200, .{ .name = "page.runIdleTasks", .low_priority = true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,16 +417,9 @@ pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
|
|||||||
return std.mem.startsWith(u8, url, current_origin);
|
return std.mem.startsWith(u8, url, current_origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up a blob URL in this page's registry, walking up the parent chain.
|
/// Look up a blob URL in this page's registry.
|
||||||
pub fn lookupBlobUrl(self: *Page, url: []const u8) ?*Blob {
|
pub fn lookupBlobUrl(self: *Page, url: []const u8) ?*Blob {
|
||||||
var current: ?*Page = self;
|
return self._blob_urls.get(url);
|
||||||
while (current) |page| {
|
|
||||||
if (page._blob_urls.get(url)) |blob| {
|
|
||||||
return blob;
|
|
||||||
}
|
|
||||||
current = page.parent;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
|
pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
|
||||||
@@ -464,7 +460,14 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
|||||||
|
|
||||||
// Content injection
|
// Content injection
|
||||||
if (is_blob) {
|
if (is_blob) {
|
||||||
const blob = self.lookupBlobUrl(request_url) orelse {
|
// For navigation, walk up the parent chain to find blob URLs
|
||||||
|
// (e.g., parent creates blob URL and sets iframe.src to it)
|
||||||
|
const blob = blk: {
|
||||||
|
var current: ?*Page = self.parent;
|
||||||
|
while (current) |page| {
|
||||||
|
if (page._blob_urls.get(request_url)) |b| break :blk b;
|
||||||
|
current = page.parent;
|
||||||
|
}
|
||||||
log.warn(.js, "invalid blob", .{ .url = request_url });
|
log.warn(.js, "invalid blob", .{ .url = request_url });
|
||||||
return error.BlobNotFound;
|
return error.BlobNotFound;
|
||||||
};
|
};
|
||||||
@@ -716,11 +719,14 @@ pub fn scriptsCompletedLoading(self: *Page) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void {
|
pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void {
|
||||||
blk: {
|
var ls: JS.Local.Scope = undefined;
|
||||||
var ls: JS.Local.Scope = undefined;
|
self.js.localScope(&ls);
|
||||||
self.js.localScope(&ls);
|
defer ls.deinit();
|
||||||
defer ls.deinit();
|
|
||||||
|
|
||||||
|
const entered = self.js.enter(&ls.handle_scope);
|
||||||
|
defer entered.exit();
|
||||||
|
|
||||||
|
blk: {
|
||||||
const event = Event.initTrusted(comptime .wrap("load"), .{}, self) catch |err| {
|
const event = Event.initTrusted(comptime .wrap("load"), .{}, self) catch |err| {
|
||||||
log.err(.page, "iframe event init", .{ .err = err, .url = iframe._src });
|
log.err(.page, "iframe event init", .{ .err = err, .url = iframe._src });
|
||||||
break :blk;
|
break :blk;
|
||||||
@@ -729,6 +735,7 @@ pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void {
|
|||||||
log.warn(.js, "iframe onload", .{ .err = err, .url = iframe._src });
|
log.warn(.js, "iframe onload", .{ .err = err, .url = iframe._src });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pendingLoadCompleted();
|
self.pendingLoadCompleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,13 +862,25 @@ fn pageDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
|||||||
if (self._parse_state == .pre) {
|
if (self._parse_state == .pre) {
|
||||||
// we lazily do this, because we might need the first chunk of data
|
// we lazily do this, because we might need the first chunk of data
|
||||||
// to sniff the content type
|
// to sniff the content type
|
||||||
const mime: Mime = blk: {
|
var mime: Mime = blk: {
|
||||||
if (transfer.response_header.?.contentType()) |ct| {
|
if (transfer.response_header.?.contentType()) |ct| {
|
||||||
break :blk try Mime.parse(ct);
|
break :blk try Mime.parse(ct);
|
||||||
}
|
}
|
||||||
break :blk Mime.sniff(data);
|
break :blk Mime.sniff(data);
|
||||||
} orelse .unknown;
|
} orelse .unknown;
|
||||||
|
|
||||||
|
// If the HTTP Content-Type header didn't specify a charset and this is HTML,
|
||||||
|
// prescan the first 1024 bytes for a <meta charset> declaration.
|
||||||
|
if (mime.content_type == .text_html and mime.is_default_charset) {
|
||||||
|
if (Mime.prescanCharset(data)) |charset| {
|
||||||
|
if (charset.len <= 40) {
|
||||||
|
@memcpy(mime.charset[0..charset.len], charset);
|
||||||
|
mime.charset[charset.len] = 0;
|
||||||
|
mime.charset_len = charset.len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.page, "navigate first chunk", .{
|
log.debug(.page, "navigate first chunk", .{
|
||||||
.content_type = mime.content_type,
|
.content_type = mime.content_type,
|
||||||
@@ -3273,14 +3292,14 @@ pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
|
|||||||
.type = self._type,
|
.type = self._type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const event = (try @import("webapi/event/MouseEvent.zig").init("click", .{
|
const mouse_event: *MouseEvent = try .initTrusted(comptime .wrap("click"), .{
|
||||||
.bubbles = true,
|
.bubbles = true,
|
||||||
.cancelable = true,
|
.cancelable = true,
|
||||||
.composed = true,
|
.composed = true,
|
||||||
.clientX = x,
|
.clientX = x,
|
||||||
.clientY = y,
|
.clientY = y,
|
||||||
}, self)).asEvent();
|
}, self);
|
||||||
try self._event_manager.dispatch(target.asEventTarget(), event);
|
try self._event_manager.dispatch(target.asEventTarget(), mouse_event.asEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
// callback when the "click" event reaches the pages.
|
// callback when the "click" event reaches the pages.
|
||||||
@@ -3524,13 +3543,16 @@ fn asUint(comptime string: anytype) std.meta.Int(
|
|||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
test "WebApi: Page" {
|
test "WebApi: Page" {
|
||||||
const filter: testing.LogFilter = .init(.http);
|
const filter: testing.LogFilter = .init(&.{ .http, .js });
|
||||||
defer filter.deinit();
|
defer filter.deinit();
|
||||||
|
|
||||||
try testing.htmlRunner("page", .{});
|
try testing.htmlRunner("page", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
test "WebApi: Frames" {
|
test "WebApi: Frames" {
|
||||||
|
const filter: testing.LogFilter = .init(&.{.js});
|
||||||
|
defer filter.deinit();
|
||||||
|
|
||||||
try testing.htmlRunner("frames", .{});
|
try testing.htmlRunner("frames", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,9 +63,6 @@ shutdown: bool = false,
|
|||||||
|
|
||||||
client: *HttpClient,
|
client: *HttpClient,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
buffer_pool: BufferPool,
|
|
||||||
|
|
||||||
script_pool: std.heap.MemoryPool(Script),
|
|
||||||
|
|
||||||
// We can download multiple sync modules in parallel, but we want to process
|
// We can download multiple sync modules in parallel, but we want to process
|
||||||
// them in order. We can't use an std.DoublyLinkedList, like the other script types,
|
// them in order. We can't use an std.DoublyLinkedList, like the other script types,
|
||||||
@@ -101,18 +98,14 @@ pub fn init(allocator: Allocator, http_client: *HttpClient, page: *Page) ScriptM
|
|||||||
.imported_modules = .empty,
|
.imported_modules = .empty,
|
||||||
.client = http_client,
|
.client = http_client,
|
||||||
.static_scripts_done = false,
|
.static_scripts_done = false,
|
||||||
.buffer_pool = BufferPool.init(allocator, 5),
|
|
||||||
.page_notified_of_completion = false,
|
.page_notified_of_completion = false,
|
||||||
.script_pool = std.heap.MemoryPool(Script).init(allocator),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *ScriptManager) void {
|
pub fn deinit(self: *ScriptManager) void {
|
||||||
// necessary to free any buffers scripts may be referencing
|
// necessary to free any arenas scripts may be referencing
|
||||||
self.reset();
|
self.reset();
|
||||||
|
|
||||||
self.buffer_pool.deinit();
|
|
||||||
self.script_pool.deinit();
|
|
||||||
self.imported_modules.deinit(self.allocator);
|
self.imported_modules.deinit(self.allocator);
|
||||||
// we don't deinit self.importmap b/c we use the page's arena for its
|
// we don't deinit self.importmap b/c we use the page's arena for its
|
||||||
// allocations.
|
// allocations.
|
||||||
@@ -121,7 +114,10 @@ pub fn deinit(self: *ScriptManager) void {
|
|||||||
pub fn reset(self: *ScriptManager) void {
|
pub fn reset(self: *ScriptManager) void {
|
||||||
var it = self.imported_modules.valueIterator();
|
var it = self.imported_modules.valueIterator();
|
||||||
while (it.next()) |value_ptr| {
|
while (it.next()) |value_ptr| {
|
||||||
self.buffer_pool.release(value_ptr.buffer);
|
switch (value_ptr.state) {
|
||||||
|
.done => |script| script.deinit(),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.imported_modules.clearRetainingCapacity();
|
self.imported_modules.clearRetainingCapacity();
|
||||||
|
|
||||||
@@ -138,13 +134,13 @@ pub fn reset(self: *ScriptManager) void {
|
|||||||
fn clearList(list: *std.DoublyLinkedList) void {
|
fn clearList(list: *std.DoublyLinkedList) void {
|
||||||
while (list.popFirst()) |n| {
|
while (list.popFirst()) |n| {
|
||||||
const script: *Script = @fieldParentPtr("node", n);
|
const script: *Script = @fieldParentPtr("node", n);
|
||||||
script.deinit(true);
|
script.deinit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getHeaders(self: *ScriptManager, url: [:0]const u8) !net_http.Headers {
|
fn getHeaders(self: *ScriptManager, arena: Allocator, url: [:0]const u8) !net_http.Headers {
|
||||||
var headers = try self.client.newHeaders();
|
var headers = try self.client.newHeaders();
|
||||||
try self.page.headersForRequest(self.page.arena, url, &headers);
|
try self.page.headersForRequest(arena, url, &headers);
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,19 +187,26 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var handover = false;
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
|
|
||||||
|
const arena = try page.getArena(.{ .debug = "addFromElement" });
|
||||||
|
errdefer if (!handover) {
|
||||||
|
page.releaseArena(arena);
|
||||||
|
};
|
||||||
|
|
||||||
var source: Script.Source = undefined;
|
var source: Script.Source = undefined;
|
||||||
var remote_url: ?[:0]const u8 = null;
|
var remote_url: ?[:0]const u8 = null;
|
||||||
const base_url = page.base();
|
const base_url = page.base();
|
||||||
if (element.getAttributeSafe(comptime .wrap("src"))) |src| {
|
if (element.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||||
if (try parseDataURI(page.arena, src)) |data_uri| {
|
if (try parseDataURI(arena, src)) |data_uri| {
|
||||||
source = .{ .@"inline" = data_uri };
|
source = .{ .@"inline" = data_uri };
|
||||||
} else {
|
} else {
|
||||||
remote_url = try URL.resolve(page.arena, base_url, src, .{});
|
remote_url = try URL.resolve(arena, base_url, src, .{});
|
||||||
source = .{ .remote = .{} };
|
source = .{ .remote = .{} };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var buf = std.Io.Writer.Allocating.init(page.arena);
|
var buf = std.Io.Writer.Allocating.init(arena);
|
||||||
try element.asNode().getChildTextContent(&buf.writer);
|
try element.asNode().getChildTextContent(&buf.writer);
|
||||||
try buf.writer.writeByte(0);
|
try buf.writer.writeByte(0);
|
||||||
const data = buf.written();
|
const data = buf.written();
|
||||||
@@ -211,6 +214,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
if (inline_source.len == 0) {
|
if (inline_source.len == 0) {
|
||||||
// we haven't set script_element._executed = true yet, which is good.
|
// we haven't set script_element._executed = true yet, which is good.
|
||||||
// If content is appended to the script, we will execute it then.
|
// If content is appended to the script, we will execute it then.
|
||||||
|
page.releaseArena(arena);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
source = .{ .@"inline" = inline_source };
|
source = .{ .@"inline" = inline_source };
|
||||||
@@ -218,15 +222,13 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
|
|
||||||
// Only set _executed (already-started) when we actually have content to execute
|
// Only set _executed (already-started) when we actually have content to execute
|
||||||
script_element._executed = true;
|
script_element._executed = true;
|
||||||
|
|
||||||
const script = try self.script_pool.create();
|
|
||||||
errdefer self.script_pool.destroy(script);
|
|
||||||
|
|
||||||
const is_inline = source == .@"inline";
|
const is_inline = source == .@"inline";
|
||||||
|
|
||||||
|
const script = try arena.create(Script);
|
||||||
script.* = .{
|
script.* = .{
|
||||||
.kind = kind,
|
.kind = kind,
|
||||||
.node = .{},
|
.node = .{},
|
||||||
|
.arena = arena,
|
||||||
.manager = self,
|
.manager = self,
|
||||||
.source = source,
|
.source = source,
|
||||||
.script_element = script_element,
|
.script_element = script_element,
|
||||||
@@ -270,7 +272,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
if (is_blocking == false) {
|
if (is_blocking == false) {
|
||||||
self.scriptList(script).remove(&script.node);
|
self.scriptList(script).remove(&script.node);
|
||||||
}
|
}
|
||||||
script.deinit(true);
|
// Let the outer errdefer handle releasing the arena if client.request fails
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.client.request(.{
|
try self.client.request(.{
|
||||||
@@ -278,7 +280,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.frame_id = page._frame_id,
|
.frame_id = page._frame_id,
|
||||||
.headers = try self.getHeaders(url),
|
.headers = try self.getHeaders(arena, url),
|
||||||
.blocking = is_blocking,
|
.blocking = is_blocking,
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
@@ -289,6 +291,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
.done_callback = Script.doneCallback,
|
.done_callback = Script.doneCallback,
|
||||||
.error_callback = Script.errorCallback,
|
.error_callback = Script.errorCallback,
|
||||||
});
|
});
|
||||||
|
handover = true;
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
var ls: js.Local.Scope = undefined;
|
var ls: js.Local.Scope = undefined;
|
||||||
@@ -318,7 +321,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
}
|
}
|
||||||
if (script.status == 0) {
|
if (script.status == 0) {
|
||||||
// an error (that we already logged)
|
// an error (that we already logged)
|
||||||
script.deinit(true);
|
script.deinit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +330,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
self.is_evaluating = true;
|
self.is_evaluating = true;
|
||||||
defer {
|
defer {
|
||||||
self.is_evaluating = was_evaluating;
|
self.is_evaluating = was_evaluating;
|
||||||
script.deinit(true);
|
script.deinit();
|
||||||
}
|
}
|
||||||
return script.eval(page);
|
return script.eval(page);
|
||||||
}
|
}
|
||||||
@@ -359,11 +362,14 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
}
|
}
|
||||||
errdefer _ = self.imported_modules.remove(url);
|
errdefer _ = self.imported_modules.remove(url);
|
||||||
|
|
||||||
const script = try self.script_pool.create();
|
const page = self.page;
|
||||||
errdefer self.script_pool.destroy(script);
|
const arena = try page.getArena(.{ .debug = "preloadImport" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
|
||||||
|
const script = try arena.create(Script);
|
||||||
script.* = .{
|
script.* = .{
|
||||||
.kind = .module,
|
.kind = .module,
|
||||||
|
.arena = arena,
|
||||||
.url = url,
|
.url = url,
|
||||||
.node = .{},
|
.node = .{},
|
||||||
.manager = self,
|
.manager = self,
|
||||||
@@ -373,11 +379,7 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
.mode = .import,
|
.mode = .import,
|
||||||
};
|
};
|
||||||
|
|
||||||
gop.value_ptr.* = ImportedModule{
|
gop.value_ptr.* = ImportedModule{};
|
||||||
.manager = self,
|
|
||||||
};
|
|
||||||
|
|
||||||
const page = self.page;
|
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
var ls: js.Local.Scope = undefined;
|
var ls: js.Local.Scope = undefined;
|
||||||
@@ -392,12 +394,18 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.client.request(.{
|
// This seems wrong since we're not dealing with an async import (unlike
|
||||||
|
// getAsyncModule below), but all we're trying to do here is pre-load the
|
||||||
|
// script for execution at some point in the future (when waitForImport is
|
||||||
|
// called).
|
||||||
|
self.async_scripts.append(&script.node);
|
||||||
|
|
||||||
|
self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.frame_id = page._frame_id,
|
.frame_id = page._frame_id,
|
||||||
.headers = try self.getHeaders(url),
|
.headers = try self.getHeaders(arena, url),
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
.notification = page._session.notification,
|
.notification = page._session.notification,
|
||||||
@@ -406,13 +414,10 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
.data_callback = Script.dataCallback,
|
.data_callback = Script.dataCallback,
|
||||||
.done_callback = Script.doneCallback,
|
.done_callback = Script.doneCallback,
|
||||||
.error_callback = Script.errorCallback,
|
.error_callback = Script.errorCallback,
|
||||||
});
|
}) catch |err| {
|
||||||
|
self.async_scripts.remove(&script.node);
|
||||||
// This seems wrong since we're not dealing with an async import (unlike
|
return err;
|
||||||
// getAsyncModule below), but all we're trying to do here is pre-load the
|
};
|
||||||
// script for execution at some point in the future (when waitForImport is
|
|
||||||
// called).
|
|
||||||
self.async_scripts.append(&script.node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
||||||
@@ -433,12 +438,12 @@ pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
|||||||
_ = try client.tick(200);
|
_ = try client.tick(200);
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
.done => {
|
.done => |script| {
|
||||||
var shared = false;
|
var shared = false;
|
||||||
const buffer = entry.value_ptr.buffer;
|
const buffer = entry.value_ptr.buffer;
|
||||||
const waiters = entry.value_ptr.waiters;
|
const waiters = entry.value_ptr.waiters;
|
||||||
|
|
||||||
if (waiters == 0) {
|
if (waiters == 1) {
|
||||||
self.imported_modules.removeByPtr(entry.key_ptr);
|
self.imported_modules.removeByPtr(entry.key_ptr);
|
||||||
} else {
|
} else {
|
||||||
shared = true;
|
shared = true;
|
||||||
@@ -447,7 +452,7 @@ pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
|||||||
return .{
|
return .{
|
||||||
.buffer = buffer,
|
.buffer = buffer,
|
||||||
.shared = shared,
|
.shared = shared,
|
||||||
.buffer_pool = &self.buffer_pool,
|
.script = script,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
.err => return error.Failed,
|
.err => return error.Failed,
|
||||||
@@ -456,11 +461,14 @@ pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.Callback, cb_data: *anyopaque, referrer: []const u8) !void {
|
pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.Callback, cb_data: *anyopaque, referrer: []const u8) !void {
|
||||||
const script = try self.script_pool.create();
|
const page = self.page;
|
||||||
errdefer self.script_pool.destroy(script);
|
const arena = try page.getArena(.{ .debug = "getAsyncImport" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
|
||||||
|
const script = try arena.create(Script);
|
||||||
script.* = .{
|
script.* = .{
|
||||||
.kind = .module,
|
.kind = .module,
|
||||||
|
.arena = arena,
|
||||||
.url = url,
|
.url = url,
|
||||||
.node = .{},
|
.node = .{},
|
||||||
.manager = self,
|
.manager = self,
|
||||||
@@ -473,7 +481,6 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
} },
|
} },
|
||||||
};
|
};
|
||||||
|
|
||||||
const page = self.page;
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
var ls: js.Local.Scope = undefined;
|
var ls: js.Local.Scope = undefined;
|
||||||
page.js.localScope(&ls);
|
page.js.localScope(&ls);
|
||||||
@@ -496,11 +503,12 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
self.is_evaluating = true;
|
self.is_evaluating = true;
|
||||||
defer self.is_evaluating = was_evaluating;
|
defer self.is_evaluating = was_evaluating;
|
||||||
|
|
||||||
try self.client.request(.{
|
self.async_scripts.append(&script.node);
|
||||||
|
self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.frame_id = page._frame_id,
|
.frame_id = page._frame_id,
|
||||||
.headers = try self.getHeaders(url),
|
.headers = try self.getHeaders(arena, url),
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
@@ -510,9 +518,10 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
.data_callback = Script.dataCallback,
|
.data_callback = Script.dataCallback,
|
||||||
.done_callback = Script.doneCallback,
|
.done_callback = Script.doneCallback,
|
||||||
.error_callback = Script.errorCallback,
|
.error_callback = Script.errorCallback,
|
||||||
});
|
}) catch |err| {
|
||||||
|
self.async_scripts.remove(&script.node);
|
||||||
self.async_scripts.append(&script.node);
|
return err;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called from the Page to let us know it's done parsing the HTML. Necessary that
|
// Called from the Page to let us know it's done parsing the HTML. Necessary that
|
||||||
@@ -537,18 +546,18 @@ fn evaluate(self: *ScriptManager) void {
|
|||||||
var script: *Script = @fieldParentPtr("node", n);
|
var script: *Script = @fieldParentPtr("node", n);
|
||||||
switch (script.mode) {
|
switch (script.mode) {
|
||||||
.async => {
|
.async => {
|
||||||
defer script.deinit(true);
|
defer script.deinit();
|
||||||
script.eval(page);
|
script.eval(page);
|
||||||
},
|
},
|
||||||
.import_async => |ia| {
|
.import_async => |ia| {
|
||||||
defer script.deinit(false);
|
|
||||||
if (script.status < 200 or script.status > 299) {
|
if (script.status < 200 or script.status > 299) {
|
||||||
|
script.deinit();
|
||||||
ia.callback(ia.data, error.FailedToLoad);
|
ia.callback(ia.data, error.FailedToLoad);
|
||||||
} else {
|
} else {
|
||||||
ia.callback(ia.data, .{
|
ia.callback(ia.data, .{
|
||||||
.shared = false,
|
.shared = false,
|
||||||
|
.script = script,
|
||||||
.buffer = script.source.remote,
|
.buffer = script.source.remote,
|
||||||
.buffer_pool = &self.buffer_pool,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -574,7 +583,7 @@ fn evaluate(self: *ScriptManager) void {
|
|||||||
}
|
}
|
||||||
defer {
|
defer {
|
||||||
_ = self.defer_scripts.popFirst();
|
_ = self.defer_scripts.popFirst();
|
||||||
script.deinit(true);
|
script.deinit();
|
||||||
}
|
}
|
||||||
script.eval(page);
|
script.eval(page);
|
||||||
}
|
}
|
||||||
@@ -625,11 +634,12 @@ fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const Script = struct {
|
pub const Script = struct {
|
||||||
complete: bool,
|
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
|
complete: bool,
|
||||||
status: u16 = 0,
|
status: u16 = 0,
|
||||||
source: Source,
|
source: Source,
|
||||||
url: []const u8,
|
url: []const u8,
|
||||||
|
arena: Allocator,
|
||||||
mode: ExecutionMode,
|
mode: ExecutionMode,
|
||||||
node: std.DoublyLinkedList.Node,
|
node: std.DoublyLinkedList.Node,
|
||||||
script_element: ?*Element.Html.Script,
|
script_element: ?*Element.Html.Script,
|
||||||
@@ -680,11 +690,8 @@ pub const Script = struct {
|
|||||||
import_async: ImportAsync,
|
import_async: ImportAsync,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn deinit(self: *Script, comptime release_buffer: bool) void {
|
fn deinit(self: *Script) void {
|
||||||
if ((comptime release_buffer) and self.source == .remote) {
|
self.manager.page.releaseArena(self.arena);
|
||||||
self.manager.buffer_pool.release(self.source.remote);
|
|
||||||
}
|
|
||||||
self.manager.script_pool.destroy(self);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn startCallback(transfer: *HttpClient.Transfer) !void {
|
fn startCallback(transfer: *HttpClient.Transfer) !void {
|
||||||
@@ -750,9 +757,9 @@ pub const Script = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
||||||
var buffer = self.manager.buffer_pool.get();
|
var buffer: std.ArrayList(u8) = .empty;
|
||||||
if (transfer.getContentLength()) |cl| {
|
if (transfer.getContentLength()) |cl| {
|
||||||
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
|
try buffer.ensureTotalCapacity(self.arena, cl);
|
||||||
}
|
}
|
||||||
self.source = .{ .remote = buffer };
|
self.source = .{ .remote = buffer };
|
||||||
return true;
|
return true;
|
||||||
@@ -766,7 +773,7 @@ pub const Script = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
fn _dataCallback(self: *Script, _: *HttpClient.Transfer, data: []const u8) !void {
|
fn _dataCallback(self: *Script, _: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
try self.source.remote.appendSlice(self.manager.allocator, data);
|
try self.source.remote.appendSlice(self.arena, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn doneCallback(ctx: *anyopaque) !void {
|
fn doneCallback(ctx: *anyopaque) !void {
|
||||||
@@ -783,9 +790,8 @@ pub const Script = struct {
|
|||||||
} else if (self.mode == .import) {
|
} else if (self.mode == .import) {
|
||||||
manager.async_scripts.remove(&self.node);
|
manager.async_scripts.remove(&self.node);
|
||||||
const entry = manager.imported_modules.getPtr(self.url).?;
|
const entry = manager.imported_modules.getPtr(self.url).?;
|
||||||
entry.state = .done;
|
entry.state = .{ .done = self };
|
||||||
entry.buffer = self.source.remote;
|
entry.buffer = self.source.remote;
|
||||||
self.deinit(false);
|
|
||||||
}
|
}
|
||||||
manager.evaluate();
|
manager.evaluate();
|
||||||
}
|
}
|
||||||
@@ -811,7 +817,7 @@ pub const Script = struct {
|
|||||||
const manager = self.manager;
|
const manager = self.manager;
|
||||||
manager.scriptList(self).remove(&self.node);
|
manager.scriptList(self).remove(&self.node);
|
||||||
if (manager.shutdown) {
|
if (manager.shutdown) {
|
||||||
self.deinit(true);
|
self.deinit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,7 +829,7 @@ pub const Script = struct {
|
|||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
self.deinit(true);
|
self.deinit();
|
||||||
manager.evaluate();
|
manager.evaluate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -951,76 +957,6 @@ pub const Script = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const BufferPool = struct {
|
|
||||||
count: usize,
|
|
||||||
available: List = .{},
|
|
||||||
allocator: Allocator,
|
|
||||||
max_concurrent_transfers: u8,
|
|
||||||
mem_pool: std.heap.MemoryPool(Container),
|
|
||||||
|
|
||||||
const List = std.SinglyLinkedList;
|
|
||||||
|
|
||||||
const Container = struct {
|
|
||||||
node: List.Node,
|
|
||||||
buf: std.ArrayList(u8),
|
|
||||||
};
|
|
||||||
|
|
||||||
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
|
|
||||||
return .{
|
|
||||||
.available = .{},
|
|
||||||
.count = 0,
|
|
||||||
.allocator = allocator,
|
|
||||||
.max_concurrent_transfers = max_concurrent_transfers,
|
|
||||||
.mem_pool = std.heap.MemoryPool(Container).init(allocator),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(self: *BufferPool) void {
|
|
||||||
const allocator = self.allocator;
|
|
||||||
|
|
||||||
var node = self.available.first;
|
|
||||||
while (node) |n| {
|
|
||||||
const container: *Container = @fieldParentPtr("node", n);
|
|
||||||
container.buf.deinit(allocator);
|
|
||||||
node = n.next;
|
|
||||||
}
|
|
||||||
self.mem_pool.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(self: *BufferPool) std.ArrayList(u8) {
|
|
||||||
const node = self.available.popFirst() orelse {
|
|
||||||
// return a new buffer
|
|
||||||
return .{};
|
|
||||||
};
|
|
||||||
|
|
||||||
self.count -= 1;
|
|
||||||
const container: *Container = @fieldParentPtr("node", node);
|
|
||||||
defer self.mem_pool.destroy(container);
|
|
||||||
return container.buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn release(self: *BufferPool, buffer: ArrayList(u8)) void {
|
|
||||||
// create mutable copy
|
|
||||||
var b = buffer;
|
|
||||||
|
|
||||||
if (self.count == self.max_concurrent_transfers) {
|
|
||||||
b.deinit(self.allocator);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = self.mem_pool.create() catch |err| {
|
|
||||||
b.deinit(self.allocator);
|
|
||||||
log.err(.http, "SM BufferPool release", .{ .err = err });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
b.clearRetainingCapacity();
|
|
||||||
container.* = .{ .buf = b, .node = .{} };
|
|
||||||
self.count += 1;
|
|
||||||
self.available.prepend(&container.node);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImportAsync = struct {
|
const ImportAsync = struct {
|
||||||
data: *anyopaque,
|
data: *anyopaque,
|
||||||
callback: ImportAsync.Callback,
|
callback: ImportAsync.Callback,
|
||||||
@@ -1030,12 +966,12 @@ const ImportAsync = struct {
|
|||||||
|
|
||||||
pub const ModuleSource = struct {
|
pub const ModuleSource = struct {
|
||||||
shared: bool,
|
shared: bool,
|
||||||
buffer_pool: *BufferPool,
|
script: *Script,
|
||||||
buffer: std.ArrayList(u8),
|
buffer: std.ArrayList(u8),
|
||||||
|
|
||||||
pub fn deinit(self: *ModuleSource) void {
|
pub fn deinit(self: *ModuleSource) void {
|
||||||
if (self.shared == false) {
|
if (self.shared == false) {
|
||||||
self.buffer_pool.release(self.buffer);
|
self.script.deinit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,15 +981,14 @@ pub const ModuleSource = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ImportedModule = struct {
|
const ImportedModule = struct {
|
||||||
manager: *ScriptManager,
|
waiters: u16 = 1,
|
||||||
state: State = .loading,
|
state: State = .loading,
|
||||||
buffer: std.ArrayList(u8) = .{},
|
buffer: std.ArrayList(u8) = .{},
|
||||||
waiters: u16 = 1,
|
|
||||||
|
|
||||||
const State = enum {
|
const State = union(enum) {
|
||||||
err,
|
err,
|
||||||
done,
|
|
||||||
loading,
|
loading,
|
||||||
|
done: *Script,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
// scheduler.run could trigger new http transfers, so do not
|
// scheduler.run could trigger new http transfers, so do not
|
||||||
// store http_client.active BEFORE this call and then use
|
// store http_client.active BEFORE this call and then use
|
||||||
// it AFTER.
|
// it AFTER.
|
||||||
const ms_to_next_task = try browser.runMacrotasks();
|
try browser.runMacrotasks();
|
||||||
|
|
||||||
// Each call to this runs scheduled load events.
|
// Each call to this runs scheduled load events.
|
||||||
try page.dispatchLoad();
|
try page.dispatchLoad();
|
||||||
@@ -423,16 +423,16 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
std.debug.assert(http_client.intercepted == 0);
|
std.debug.assert(http_client.intercepted == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var ms: u64 = ms_to_next_task orelse blk: {
|
var ms = blk: {
|
||||||
if (wait_ms - ms_remaining < 100) {
|
// if (wait_ms - ms_remaining < 100) {
|
||||||
if (comptime builtin.is_test) {
|
// if (comptime builtin.is_test) {
|
||||||
return .done;
|
// return .done;
|
||||||
}
|
// }
|
||||||
// Look, we want to exit ASAP, but we don't want
|
// // Look, we want to exit ASAP, but we don't want
|
||||||
// to exit so fast that we've run none of the
|
// // to exit so fast that we've run none of the
|
||||||
// background jobs.
|
// // background jobs.
|
||||||
break :blk 50;
|
// break :blk 50;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (browser.hasBackgroundTasks()) {
|
if (browser.hasBackgroundTasks()) {
|
||||||
// _we_ have nothing to run, but v8 is working on
|
// _we_ have nothing to run, but v8 is working on
|
||||||
@@ -441,9 +441,7 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
break :blk 20;
|
break :blk 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No http transfers, no cdp extra socket, no
|
break :blk browser.msToNextMacrotask() orelse return .done;
|
||||||
// scheduled tasks, we're done.
|
|
||||||
return .done;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ms > ms_remaining) {
|
if (ms > ms_remaining) {
|
||||||
@@ -470,9 +468,9 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
// We're here because we either have active HTTP
|
// We're here because we either have active HTTP
|
||||||
// connections, or exit_when_done == false (aka, there's
|
// connections, or exit_when_done == false (aka, there's
|
||||||
// an cdp_socket registered with the http client).
|
// an cdp_socket registered with the http client).
|
||||||
// We should continue to run lowPriority tasks, so we
|
// We should continue to run tasks, so we minimize how long
|
||||||
// minimize how long we'll poll for network I/O.
|
// we'll poll for network I/O.
|
||||||
var ms_to_wait = @min(200, ms_to_next_task orelse 200);
|
var ms_to_wait = @min(200, browser.msToNextMacrotask() orelse 200);
|
||||||
if (ms_to_wait > 10 and browser.hasBackgroundTasks()) {
|
if (ms_to_wait > 10 and browser.hasBackgroundTasks()) {
|
||||||
// if we have background tasks, we don't want to wait too
|
// if we have background tasks, we don't want to wait too
|
||||||
// long for a message from the client. We want to go back
|
// long for a message from the client. We want to go back
|
||||||
|
|||||||
104
src/browser/actions.zig
Normal file
104
src/browser/actions.zig
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// 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 lp = @import("../lightpanda.zig");
|
||||||
|
const DOMNode = @import("webapi/Node.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Event = @import("webapi/Event.zig");
|
||||||
|
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
|
||||||
|
pub fn click(node: *DOMNode, page: *Page) !void {
|
||||||
|
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||||
|
|
||||||
|
const mouse_event: *MouseEvent = try .initTrusted(comptime .wrap("click"), .{
|
||||||
|
.bubbles = true,
|
||||||
|
.cancelable = true,
|
||||||
|
.composed = true,
|
||||||
|
.clientX = 0,
|
||||||
|
.clientY = 0,
|
||||||
|
}, page);
|
||||||
|
|
||||||
|
page._event_manager.dispatch(el.asEventTarget(), mouse_event.asEvent()) catch |err| {
|
||||||
|
lp.log.err(.app, "click failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill(node: *DOMNode, text: []const u8, page: *Page) !void {
|
||||||
|
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||||
|
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
input.setValue(text, page) catch |err| {
|
||||||
|
lp.log.err(.app, "fill input failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
} else if (el.is(Element.Html.TextArea)) |textarea| {
|
||||||
|
textarea.setValue(text, page) catch |err| {
|
||||||
|
lp.log.err(.app, "fill textarea failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
} else if (el.is(Element.Html.Select)) |select| {
|
||||||
|
select.setValue(text, page) catch |err| {
|
||||||
|
lp.log.err(.app, "fill select failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return error.InvalidNodeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, page);
|
||||||
|
page._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| {
|
||||||
|
lp.log.err(.app, "dispatch input event failed", .{ .err = err });
|
||||||
|
};
|
||||||
|
|
||||||
|
const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, page);
|
||||||
|
page._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| {
|
||||||
|
lp.log.err(.app, "dispatch change event failed", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, page: *Page) !void {
|
||||||
|
if (node) |n| {
|
||||||
|
const el = n.is(Element) orelse return error.InvalidNodeType;
|
||||||
|
|
||||||
|
if (x) |val| {
|
||||||
|
el.setScrollLeft(val, page) catch |err| {
|
||||||
|
lp.log.err(.app, "setScrollLeft failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (y) |val| {
|
||||||
|
el.setScrollTop(val, page) catch |err| {
|
||||||
|
lp.log.err(.app, "setScrollTop failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const scroll_evt: *Event = try .initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, page);
|
||||||
|
page._event_manager.dispatch(el.asEventTarget(), scroll_evt) catch |err| {
|
||||||
|
lp.log.err(.app, "dispatch scroll event failed", .{ .err = err });
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
page.window.scrollTo(.{ .x = x orelse 0 }, y, page) catch |err| {
|
||||||
|
lp.log.err(.app, "scroll failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -259,17 +259,52 @@ pub fn classifyInteractivity(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isInteractiveRole(role: []const u8) bool {
|
pub fn isInteractiveRole(role: []const u8) bool {
|
||||||
const interactive_roles = [_][]const u8{
|
const MAX_LEN = "menuitemcheckbox".len;
|
||||||
"button", "link", "tab", "menuitem",
|
if (role.len > MAX_LEN) return false;
|
||||||
"menuitemcheckbox", "menuitemradio", "switch", "checkbox",
|
var buf: [MAX_LEN]u8 = undefined;
|
||||||
"radio", "slider", "spinbutton", "searchbox",
|
const lowered = std.ascii.lowerString(&buf, role);
|
||||||
"combobox", "option", "treeitem",
|
const interactive_roles = std.StaticStringMap(void).initComptime(.{
|
||||||
};
|
.{ "button", {} },
|
||||||
for (interactive_roles) |r| {
|
.{ "checkbox", {} },
|
||||||
if (std.ascii.eqlIgnoreCase(role, r)) return true;
|
.{ "combobox", {} },
|
||||||
}
|
.{ "iframe", {} },
|
||||||
return false;
|
.{ "link", {} },
|
||||||
|
.{ "listbox", {} },
|
||||||
|
.{ "menuitem", {} },
|
||||||
|
.{ "menuitemcheckbox", {} },
|
||||||
|
.{ "menuitemradio", {} },
|
||||||
|
.{ "option", {} },
|
||||||
|
.{ "radio", {} },
|
||||||
|
.{ "searchbox", {} },
|
||||||
|
.{ "slider", {} },
|
||||||
|
.{ "spinbutton", {} },
|
||||||
|
.{ "switch", {} },
|
||||||
|
.{ "tab", {} },
|
||||||
|
.{ "textbox", {} },
|
||||||
|
.{ "treeitem", {} },
|
||||||
|
});
|
||||||
|
return interactive_roles.has(lowered);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isContentRole(role: []const u8) bool {
|
||||||
|
const MAX_LEN = "columnheader".len;
|
||||||
|
if (role.len > MAX_LEN) return false;
|
||||||
|
var buf: [MAX_LEN]u8 = undefined;
|
||||||
|
const lowered = std.ascii.lowerString(&buf, role);
|
||||||
|
const content_roles = std.StaticStringMap(void).initComptime(.{
|
||||||
|
.{ "article", {} },
|
||||||
|
.{ "cell", {} },
|
||||||
|
.{ "columnheader", {} },
|
||||||
|
.{ "gridcell", {} },
|
||||||
|
.{ "heading", {} },
|
||||||
|
.{ "listitem", {} },
|
||||||
|
.{ "main", {} },
|
||||||
|
.{ "navigation", {} },
|
||||||
|
.{ "region", {} },
|
||||||
|
.{ "rowheader", {} },
|
||||||
|
});
|
||||||
|
return content_roles.has(lowered);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getRole(el: *Element) ?[]const u8 {
|
fn getRole(el: *Element) ?[]const u8 {
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ prev_context: *Context,
|
|||||||
|
|
||||||
// Takes the raw v8 isolate and extracts the context from it.
|
// Takes the raw v8 isolate and extracts the context from it.
|
||||||
pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
|
pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
|
||||||
const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate).?;
|
const ctx, const v8_context = Context.fromIsolate(.{ .handle = v8_isolate });
|
||||||
initWithContext(self, Context.fromC(v8_context), v8_context);
|
initWithContext(self, ctx, v8_context);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) void {
|
fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) void {
|
||||||
@@ -537,9 +537,7 @@ pub const Function = struct {
|
|||||||
|
|
||||||
pub fn call(comptime T: type, info_handle: *const v8.FunctionCallbackInfo, func: anytype, comptime opts: Opts) void {
|
pub fn call(comptime T: type, info_handle: *const v8.FunctionCallbackInfo, func: anytype, comptime opts: Opts) void {
|
||||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(info_handle).?;
|
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(info_handle).?;
|
||||||
const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate).?;
|
const ctx, const v8_context = Context.fromIsolate(.{ .handle = v8_isolate });
|
||||||
|
|
||||||
const ctx = Context.fromC(v8_context);
|
|
||||||
const info = FunctionCallbackInfo{ .handle = info_handle };
|
const info = FunctionCallbackInfo{ .handle = info_handle };
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
|
|||||||
@@ -119,12 +119,22 @@ const ModuleEntry = struct {
|
|||||||
resolver_promise: ?js.Promise.Global = null,
|
resolver_promise: ?js.Promise.Global = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn fromC(c_context: *const v8.Context) *Context {
|
pub fn fromC(c_context: *const v8.Context) ?*Context {
|
||||||
return @ptrCast(@alignCast(v8.v8__Context__GetAlignedPointerFromEmbedderData(c_context, 1)));
|
return @ptrCast(@alignCast(v8.v8__Context__GetAlignedPointerFromEmbedderData(c_context, 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fromIsolate(isolate: js.Isolate) *Context {
|
/// Returns the Context and v8::Context for the given isolate.
|
||||||
return fromC(v8.v8__Isolate__GetCurrentContext(isolate.handle).?);
|
/// If the current context is from a destroyed Context (e.g., navigated-away iframe),
|
||||||
|
/// falls back to the incumbent context (the calling context).
|
||||||
|
pub fn fromIsolate(isolate: js.Isolate) struct { *Context, *const v8.Context } {
|
||||||
|
const v8_context = v8.v8__Isolate__GetCurrentContext(isolate.handle).?;
|
||||||
|
if (fromC(v8_context)) |ctx| {
|
||||||
|
return .{ ctx, v8_context };
|
||||||
|
}
|
||||||
|
// The current context's Context struct has been freed (e.g., iframe navigated away).
|
||||||
|
// Fall back to the incumbent context (the calling context).
|
||||||
|
const v8_incumbent = v8.v8__Isolate__GetIncumbentContext(isolate.handle).?;
|
||||||
|
return .{ fromC(v8_incumbent).?, v8_incumbent };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Context) void {
|
pub fn deinit(self: *Context) void {
|
||||||
@@ -155,6 +165,11 @@ pub fn deinit(self: *Context) void {
|
|||||||
|
|
||||||
self.session.releaseOrigin(self.origin);
|
self.session.releaseOrigin(self.origin);
|
||||||
|
|
||||||
|
// Clear the embedder data so that if V8 keeps this context alive
|
||||||
|
// (because objects created in it are still referenced), we don't
|
||||||
|
// have a dangling pointer to our freed Context struct.
|
||||||
|
v8.v8__Context__SetAlignedPointerInEmbedderData(entered.handle, 1, null);
|
||||||
|
|
||||||
v8.v8__Global__Reset(&self.handle);
|
v8.v8__Global__Reset(&self.handle);
|
||||||
env.isolate.notifyContextDisposed();
|
env.isolate.notifyContextDisposed();
|
||||||
// There can be other tasks associated with this context that we need to
|
// There can be other tasks associated with this context that we need to
|
||||||
@@ -167,12 +182,11 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
|
|||||||
const env = self.env;
|
const env = self.env;
|
||||||
const isolate = env.isolate;
|
const isolate = env.isolate;
|
||||||
|
|
||||||
|
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
|
||||||
|
|
||||||
const origin = try self.session.getOrCreateOrigin(key);
|
const origin = try self.session.getOrCreateOrigin(key);
|
||||||
errdefer self.session.releaseOrigin(origin);
|
errdefer self.session.releaseOrigin(origin);
|
||||||
|
try origin.takeover(self.origin);
|
||||||
try self.origin.transferTo(origin);
|
|
||||||
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
|
|
||||||
self.origin.deinit(env.app);
|
|
||||||
|
|
||||||
self.origin = origin;
|
self.origin = origin;
|
||||||
|
|
||||||
@@ -255,6 +269,10 @@ pub fn toLocal(self: *Context, global: anytype) js.Local.ToLocalReturnType(@Type
|
|||||||
return l.toLocal(global);
|
return l.toLocal(global);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getIncumbent(self: *Context) *Page {
|
||||||
|
return fromC(v8.v8__Isolate__GetIncumbentContext(self.env.isolate.handle).?).?.page;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn stringToPersistedFunction(
|
pub fn stringToPersistedFunction(
|
||||||
self: *Context,
|
self: *Context,
|
||||||
function_body: []const u8,
|
function_body: []const u8,
|
||||||
@@ -306,15 +324,15 @@ pub fn module(self: *Context, comptime want_result: bool, local: *const js.Local
|
|||||||
}
|
}
|
||||||
|
|
||||||
const owned_url = try arena.dupeZ(u8, url);
|
const owned_url = try arena.dupeZ(u8, url);
|
||||||
|
if (cacheable and !gop.found_existing) {
|
||||||
|
gop.key_ptr.* = owned_url;
|
||||||
|
}
|
||||||
const m = try compileModule(local, src, owned_url);
|
const m = try compileModule(local, src, owned_url);
|
||||||
|
|
||||||
if (cacheable) {
|
if (cacheable) {
|
||||||
// compileModule is synchronous - nothing can modify the cache during compilation
|
// compileModule is synchronous - nothing can modify the cache during compilation
|
||||||
lp.assert(gop.value_ptr.module == null, "Context.module has module", .{});
|
lp.assert(gop.value_ptr.module == null, "Context.module has module", .{});
|
||||||
gop.value_ptr.module = try m.persist();
|
gop.value_ptr.module = try m.persist();
|
||||||
if (!gop.found_existing) {
|
|
||||||
gop.key_ptr.* = owned_url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break :blk .{ m, owned_url };
|
break :blk .{ m, owned_url };
|
||||||
@@ -476,7 +494,7 @@ fn resolveModuleCallback(
|
|||||||
) callconv(.c) ?*const v8.Module {
|
) callconv(.c) ?*const v8.Module {
|
||||||
_ = import_attributes;
|
_ = import_attributes;
|
||||||
|
|
||||||
const self = fromC(c_context.?);
|
const self = fromC(c_context.?).?;
|
||||||
const local = js.Local{
|
const local = js.Local{
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
.handle = c_context.?,
|
.handle = c_context.?,
|
||||||
@@ -509,7 +527,7 @@ pub fn dynamicModuleCallback(
|
|||||||
_ = host_defined_options;
|
_ = host_defined_options;
|
||||||
_ = import_attrs;
|
_ = import_attrs;
|
||||||
|
|
||||||
const self = fromC(c_context.?);
|
const self = fromC(c_context.?).?;
|
||||||
const local = js.Local{
|
const local = js.Local{
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
.handle = c_context.?,
|
.handle = c_context.?,
|
||||||
@@ -556,7 +574,7 @@ pub fn dynamicModuleCallback(
|
|||||||
|
|
||||||
pub fn metaObjectCallback(c_context: ?*v8.Context, c_module: ?*v8.Module, c_meta: ?*v8.Value) callconv(.c) void {
|
pub fn metaObjectCallback(c_context: ?*v8.Context, c_module: ?*v8.Module, c_meta: ?*v8.Value) callconv(.c) void {
|
||||||
// @HandleScope implement this without a fat context/local..
|
// @HandleScope implement this without a fat context/local..
|
||||||
const self = fromC(c_context.?);
|
const self = fromC(c_context.?).?;
|
||||||
var local = js.Local{
|
var local = js.Local{
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
.handle = c_context.?,
|
.handle = c_context.?,
|
||||||
|
|||||||
@@ -382,8 +382,7 @@ pub fn runMicrotasks(self: *Env) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMacrotasks(self: *Env) !?u64 {
|
pub fn runMacrotasks(self: *Env) !void {
|
||||||
var ms_to_next_task: ?u64 = null;
|
|
||||||
for (self.contexts[0..self.context_count]) |ctx| {
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
if (comptime builtin.is_test == false) {
|
if (comptime builtin.is_test == false) {
|
||||||
// I hate this comptime check as much as you do. But we have tests
|
// I hate this comptime check as much as you do. But we have tests
|
||||||
@@ -398,13 +397,17 @@ pub fn runMacrotasks(self: *Env) !?u64 {
|
|||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
const entered = ctx.enter(&hs);
|
const entered = ctx.enter(&hs);
|
||||||
defer entered.exit();
|
defer entered.exit();
|
||||||
|
try ctx.scheduler.run();
|
||||||
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 msToNextMacrotask(self: *Env) ?u64 {
|
||||||
|
var next_task: u64 = std.math.maxInt(u64);
|
||||||
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
|
const candidate = ctx.scheduler.msToNextHigh() orelse continue;
|
||||||
|
next_task = @min(candidate, next_task);
|
||||||
|
}
|
||||||
|
return if (next_task == std.math.maxInt(u64)) null else next_task;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pumpMessageLoop(self: *const Env) void {
|
pub fn pumpMessageLoop(self: *const Env) void {
|
||||||
@@ -492,20 +495,25 @@ pub fn terminate(self: *const Env) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
||||||
|
const promise_event = v8.v8__PromiseRejectMessage__GetEvent(&message_handle);
|
||||||
|
if (promise_event != v8.kPromiseRejectWithNoHandler and promise_event != v8.kPromiseHandlerAddedAfterReject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
||||||
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
||||||
const js_isolate = js.Isolate{ .handle = v8_isolate };
|
const isolate = js.Isolate{ .handle = v8_isolate };
|
||||||
const ctx = Context.fromIsolate(js_isolate);
|
const ctx, const v8_context = Context.fromIsolate(isolate);
|
||||||
|
|
||||||
const local = js.Local{
|
const local = js.Local{
|
||||||
.ctx = ctx,
|
.ctx = ctx,
|
||||||
.isolate = js_isolate,
|
.isolate = isolate,
|
||||||
.handle = v8.v8__Isolate__GetCurrentContext(v8_isolate).?,
|
.handle = v8_context,
|
||||||
.call_arena = ctx.call_arena,
|
.call_arena = ctx.call_arena,
|
||||||
};
|
};
|
||||||
|
|
||||||
const page = ctx.page;
|
const page = ctx.page;
|
||||||
page.window.unhandledPromiseRejection(.{
|
page.window.unhandledPromiseRejection(promise_event == v8.kPromiseRejectWithNoHandler, .{
|
||||||
.local = &local,
|
.local = &local,
|
||||||
.handle = &message_handle,
|
.handle = &message_handle,
|
||||||
}, page) catch |err| {
|
}, page) catch |err| {
|
||||||
|
|||||||
@@ -1212,6 +1212,12 @@ pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise {
|
|||||||
return resolver.promise();
|
return resolver.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn rejectErrorPromise(self: *const Local, value: js.PromiseResolver.RejectError) !js.Promise {
|
||||||
|
var resolver = js.PromiseResolver.init(self);
|
||||||
|
resolver.rejectError("Local.rejectPromise", value);
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn resolvePromise(self: *const Local, value: anytype) !js.Promise {
|
pub fn resolvePromise(self: *const Local, value: anytype) !js.Promise {
|
||||||
var resolver = js.PromiseResolver.init(self);
|
var resolver = js.PromiseResolver.init(self);
|
||||||
resolver.resolve("Local.resolvePromise", value);
|
resolver.resolve("Local.resolvePromise", value);
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
|||||||
// if v8 hasn't called the finalizer directly itself.
|
// if v8 hasn't called the finalizer directly itself.
|
||||||
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
|
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
|
||||||
|
|
||||||
|
taken_over: std.ArrayList(*Origin),
|
||||||
|
|
||||||
pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
|
pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
|
||||||
const arena = try app.arena_pool.acquire();
|
const arena = try app.arena_pool.acquire();
|
||||||
errdefer app.arena_pool.release(arena);
|
errdefer app.arena_pool.release(arena);
|
||||||
@@ -86,14 +88,19 @@ pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
|
|||||||
.rc = 1,
|
.rc = 1,
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
.key = owned_key,
|
.key = owned_key,
|
||||||
.globals = .empty,
|
|
||||||
.temps = .empty,
|
.temps = .empty,
|
||||||
|
.globals = .empty,
|
||||||
|
.taken_over = .empty,
|
||||||
.security_token = token_global,
|
.security_token = token_global,
|
||||||
};
|
};
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Origin, app: *App) void {
|
pub fn deinit(self: *Origin, app: *App) void {
|
||||||
|
for (self.taken_over.items) |o| {
|
||||||
|
o.deinit(app);
|
||||||
|
}
|
||||||
|
|
||||||
// Call finalizers before releasing anything
|
// Call finalizers before releasing anything
|
||||||
{
|
{
|
||||||
var it = self.finalizer_callbacks.valueIterator();
|
var it = self.finalizer_callbacks.valueIterator();
|
||||||
@@ -196,42 +203,44 @@ pub fn createFinalizerCallback(
|
|||||||
return fc;
|
return fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn transferTo(self: *Origin, dest: *Origin) !void {
|
pub fn takeover(self: *Origin, original: *Origin) !void {
|
||||||
const arena = dest.arena;
|
const arena = self.arena;
|
||||||
|
|
||||||
try dest.globals.ensureUnusedCapacity(arena, self.globals.items.len);
|
try self.globals.ensureUnusedCapacity(arena, original.globals.items.len);
|
||||||
for (self.globals.items) |obj| {
|
for (original.globals.items) |obj| {
|
||||||
dest.globals.appendAssumeCapacity(obj);
|
self.globals.appendAssumeCapacity(obj);
|
||||||
}
|
}
|
||||||
self.globals.clearRetainingCapacity();
|
original.globals.clearRetainingCapacity();
|
||||||
|
|
||||||
{
|
{
|
||||||
try dest.temps.ensureUnusedCapacity(arena, self.temps.count());
|
try self.temps.ensureUnusedCapacity(arena, original.temps.count());
|
||||||
var it = self.temps.iterator();
|
var it = original.temps.iterator();
|
||||||
while (it.next()) |kv| {
|
while (it.next()) |kv| {
|
||||||
try dest.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
try self.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
||||||
}
|
}
|
||||||
self.temps.clearRetainingCapacity();
|
original.temps.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
try dest.finalizer_callbacks.ensureUnusedCapacity(arena, self.finalizer_callbacks.count());
|
try self.finalizer_callbacks.ensureUnusedCapacity(arena, original.finalizer_callbacks.count());
|
||||||
var it = self.finalizer_callbacks.iterator();
|
var it = original.finalizer_callbacks.iterator();
|
||||||
while (it.next()) |kv| {
|
while (it.next()) |kv| {
|
||||||
kv.value_ptr.*.origin = dest;
|
kv.value_ptr.*.origin = self;
|
||||||
try dest.finalizer_callbacks.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
try self.finalizer_callbacks.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
||||||
}
|
}
|
||||||
self.finalizer_callbacks.clearRetainingCapacity();
|
original.finalizer_callbacks.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
try dest.identity_map.ensureUnusedCapacity(arena, self.identity_map.count());
|
try self.identity_map.ensureUnusedCapacity(arena, original.identity_map.count());
|
||||||
var it = self.identity_map.iterator();
|
var it = original.identity_map.iterator();
|
||||||
while (it.next()) |kv| {
|
while (it.next()) |kv| {
|
||||||
try dest.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
try self.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
||||||
}
|
}
|
||||||
self.identity_map.clearRetainingCapacity();
|
original.identity_map.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try self.taken_over.append(self.arena, original);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A type that has a finalizer can have its finalizer called one of two ways.
|
// A type that has a finalizer can have its finalizer called one of two ways.
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
|
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
const DOMException = @import("../webapi/DOMException.zig");
|
||||||
|
|
||||||
const PromiseResolver = @This();
|
const PromiseResolver = @This();
|
||||||
|
|
||||||
@@ -63,14 +65,19 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const RejectError = union(enum) {
|
pub const RejectError = union(enum) {
|
||||||
generic: []const u8,
|
generic: []const u8,
|
||||||
type_error: []const u8,
|
type_error: []const u8,
|
||||||
|
dom_exception: anyerror,
|
||||||
};
|
};
|
||||||
pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void {
|
pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void {
|
||||||
const handle = switch (err) {
|
const handle = switch (err) {
|
||||||
.type_error => |str| self.local.isolate.createTypeError(str),
|
.type_error => |str| self.local.isolate.createTypeError(str),
|
||||||
.generic => |str| self.local.isolate.createError(str),
|
.generic => |str| self.local.isolate.createError(str),
|
||||||
|
.dom_exception => |exception| {
|
||||||
|
self.reject(source, DOMException.fromError(exception));
|
||||||
|
return;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
|
self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
|
||||||
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });
|
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(self: *Scheduler) !?u64 {
|
pub fn run(self: *Scheduler) !void {
|
||||||
_ = try self.runQueue(&self.low_priority);
|
const now = milliTimestamp(.monotonic);
|
||||||
return self.runQueue(&self.high_priority);
|
try self.runQueue(&self.low_priority, now);
|
||||||
|
try self.runQueue(&self.high_priority, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hasReadyTasks(self: *Scheduler) bool {
|
pub fn hasReadyTasks(self: *Scheduler) bool {
|
||||||
@@ -84,16 +85,23 @@ pub fn hasReadyTasks(self: *Scheduler) bool {
|
|||||||
return queueuHasReadyTask(&self.low_priority, now) or queueuHasReadyTask(&self.high_priority, now);
|
return queueuHasReadyTask(&self.low_priority, now) or queueuHasReadyTask(&self.high_priority, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
pub fn msToNextHigh(self: *Scheduler) ?u64 {
|
||||||
if (queue.count() == 0) {
|
const task = self.high_priority.peek() orelse return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = milliTimestamp(.monotonic);
|
const now = milliTimestamp(.monotonic);
|
||||||
|
if (task.run_at <= now) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return @intCast(task.run_at - now);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runQueue(self: *Scheduler, queue: *Queue, now: u64) !void {
|
||||||
|
if (queue.count() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
while (queue.peek()) |*task_| {
|
while (queue.peek()) |*task_| {
|
||||||
if (task_.run_at > now) {
|
if (task_.run_at > now) {
|
||||||
return @intCast(task_.run_at - now);
|
return;
|
||||||
}
|
}
|
||||||
var task = queue.remove();
|
var task = queue.remove();
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -114,7 +122,7 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
|||||||
try self.low_priority.add(task);
|
try self.low_priority.add(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn queueuHasReadyTask(queue: *Queue, now: u64) bool {
|
fn queueuHasReadyTask(queue: *Queue, now: u64) bool {
|
||||||
|
|||||||
@@ -725,6 +725,8 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/collections.zig"),
|
@import("../webapi/collections.zig"),
|
||||||
@import("../webapi/Console.zig"),
|
@import("../webapi/Console.zig"),
|
||||||
@import("../webapi/Crypto.zig"),
|
@import("../webapi/Crypto.zig"),
|
||||||
|
@import("../webapi/Permissions.zig"),
|
||||||
|
@import("../webapi/StorageManager.zig"),
|
||||||
@import("../webapi/CSS.zig"),
|
@import("../webapi/CSS.zig"),
|
||||||
@import("../webapi/css/CSSRule.zig"),
|
@import("../webapi/css/CSSRule.zig"),
|
||||||
@import("../webapi/css/CSSRuleList.zig"),
|
@import("../webapi/css/CSSRuleList.zig"),
|
||||||
@@ -848,6 +850,7 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/event/FocusEvent.zig"),
|
@import("../webapi/event/FocusEvent.zig"),
|
||||||
@import("../webapi/event/WheelEvent.zig"),
|
@import("../webapi/event/WheelEvent.zig"),
|
||||||
@import("../webapi/event/TextEvent.zig"),
|
@import("../webapi/event/TextEvent.zig"),
|
||||||
|
@import("../webapi/event/InputEvent.zig"),
|
||||||
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
||||||
@import("../webapi/MessageChannel.zig"),
|
@import("../webapi/MessageChannel.zig"),
|
||||||
@import("../webapi/MessagePort.zig"),
|
@import("../webapi/MessagePort.zig"),
|
||||||
|
|||||||
@@ -124,352 +124,362 @@ fn hasVisibleContent(root: *Node) bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensureNewline(state: *State, writer: *std.Io.Writer) !void {
|
const Context = struct {
|
||||||
if (!state.last_char_was_newline) {
|
state: State,
|
||||||
try writer.writeByte('\n');
|
writer: *std.Io.Writer,
|
||||||
state.last_char_was_newline = true;
|
page: *Page,
|
||||||
|
|
||||||
|
fn ensureNewline(self: *Context) !void {
|
||||||
|
if (!self.state.last_char_was_newline) {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
fn render(self: *Context, node: *Node) error{WriteFailed}!void {
|
||||||
|
switch (node._type) {
|
||||||
|
.document, .document_fragment => {
|
||||||
|
try self.renderChildren(node);
|
||||||
|
},
|
||||||
|
.element => |el| {
|
||||||
|
try self.renderElement(el);
|
||||||
|
},
|
||||||
|
.cdata => |cd| {
|
||||||
|
if (node.is(Node.CData.Text)) |_| {
|
||||||
|
var text = cd.getData().str();
|
||||||
|
if (self.state.pre_node) |pre| {
|
||||||
|
if (node.parentNode() == pre and node.nextSibling() == null) {
|
||||||
|
text = std.mem.trimRight(u8, text, " \t\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try self.renderText(text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderChildren(self: *Context, parent: *Node) !void {
|
||||||
|
var it = parent.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
try self.render(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderElement(self: *Context, el: *Element) !void {
|
||||||
|
const tag = el.getTag();
|
||||||
|
|
||||||
|
if (!isVisibleElement(el)) return;
|
||||||
|
|
||||||
|
// --- Opening Tag Logic ---
|
||||||
|
|
||||||
|
// Ensure block elements start on a new line (double newline for paragraphs etc)
|
||||||
|
if (tag.isBlock() and !self.state.in_table) {
|
||||||
|
try self.ensureNewline();
|
||||||
|
if (shouldAddSpacing(tag)) {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
} else if (tag == .li or tag == .tr) {
|
||||||
|
try self.ensureNewline();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefixes
|
||||||
|
switch (tag) {
|
||||||
|
.h1 => try self.writer.writeAll("# "),
|
||||||
|
.h2 => try self.writer.writeAll("## "),
|
||||||
|
.h3 => try self.writer.writeAll("### "),
|
||||||
|
.h4 => try self.writer.writeAll("#### "),
|
||||||
|
.h5 => try self.writer.writeAll("##### "),
|
||||||
|
.h6 => try self.writer.writeAll("###### "),
|
||||||
|
.ul => {
|
||||||
|
if (self.state.list_depth < self.state.list_stack.len) {
|
||||||
|
self.state.list_stack[self.state.list_depth] = .{ .type = .unordered, .index = 0 };
|
||||||
|
self.state.list_depth += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.ol => {
|
||||||
|
if (self.state.list_depth < self.state.list_stack.len) {
|
||||||
|
self.state.list_stack[self.state.list_depth] = .{ .type = .ordered, .index = 1 };
|
||||||
|
self.state.list_depth += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.li => {
|
||||||
|
const indent = if (self.state.list_depth > 0) self.state.list_depth - 1 else 0;
|
||||||
|
for (0..indent) |_| try self.writer.writeAll(" ");
|
||||||
|
|
||||||
|
if (self.state.list_depth > 0 and self.state.list_stack[self.state.list_depth - 1].type == .ordered) {
|
||||||
|
const current_list = &self.state.list_stack[self.state.list_depth - 1];
|
||||||
|
try self.writer.print("{d}. ", .{current_list.index});
|
||||||
|
current_list.index += 1;
|
||||||
|
} else {
|
||||||
|
try self.writer.writeAll("- ");
|
||||||
|
}
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.table => {
|
||||||
|
self.state.in_table = true;
|
||||||
|
self.state.table_row_index = 0;
|
||||||
|
self.state.table_col_count = 0;
|
||||||
|
},
|
||||||
|
.tr => {
|
||||||
|
self.state.table_col_count = 0;
|
||||||
|
try self.writer.writeByte('|');
|
||||||
|
},
|
||||||
|
.td, .th => {
|
||||||
|
// Note: leading pipe handled by previous cell closing or tr opening
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
},
|
||||||
|
.blockquote => {
|
||||||
|
try self.writer.writeAll("> ");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.pre => {
|
||||||
|
try self.writer.writeAll("```\n");
|
||||||
|
self.state.pre_node = el.asNode();
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
},
|
||||||
|
.code => {
|
||||||
|
if (self.state.pre_node == null) {
|
||||||
|
try self.writer.writeByte('`');
|
||||||
|
self.state.in_code = true;
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.b, .strong => {
|
||||||
|
try self.writer.writeAll("**");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.i, .em => {
|
||||||
|
try self.writer.writeAll("*");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.s, .del => {
|
||||||
|
try self.writer.writeAll("~~");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.hr => {
|
||||||
|
try self.writer.writeAll("---\n");
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.br => {
|
||||||
|
if (self.state.in_table) {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
} else {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.img => {
|
||||||
|
try self.writer.writeAll(";
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||||
|
const absolute_src = URL.resolve(self.page.call_arena, self.page.base(), src, .{ .encode = true }) catch src;
|
||||||
|
try self.writer.writeAll(absolute_src);
|
||||||
|
}
|
||||||
|
try self.writer.writeAll(")");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.anchor => {
|
||||||
|
const has_content = hasVisibleContent(el.asNode());
|
||||||
|
const label = getAnchorLabel(el);
|
||||||
|
const href_raw = el.getAttributeSafe(comptime .wrap("href"));
|
||||||
|
|
||||||
|
if (!has_content and label == null and href_raw == null) return;
|
||||||
|
|
||||||
|
const has_block = hasBlockDescendant(el.asNode());
|
||||||
|
const href = if (href_raw) |h| URL.resolve(self.page.call_arena, self.page.base(), h, .{ .encode = true }) catch h else null;
|
||||||
|
|
||||||
|
if (has_block) {
|
||||||
|
try self.renderChildren(el.asNode());
|
||||||
|
if (href) |h| {
|
||||||
|
if (!self.state.last_char_was_newline) try self.writer.writeByte('\n');
|
||||||
|
try self.writer.writeAll("([](");
|
||||||
|
try self.writer.writeAll(h);
|
||||||
|
try self.writer.writeAll("))\n");
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStandaloneAnchor(el)) {
|
||||||
|
if (!self.state.last_char_was_newline) try self.writer.writeByte('\n');
|
||||||
|
try self.writer.writeByte('[');
|
||||||
|
if (has_content) {
|
||||||
|
try self.renderChildren(el.asNode());
|
||||||
|
} else {
|
||||||
|
try self.writer.writeAll(label orelse "");
|
||||||
|
}
|
||||||
|
try self.writer.writeAll("](");
|
||||||
|
if (href) |h| {
|
||||||
|
try self.writer.writeAll(h);
|
||||||
|
}
|
||||||
|
try self.writer.writeAll(")\n");
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.writer.writeByte('[');
|
||||||
|
if (has_content) {
|
||||||
|
try self.renderChildren(el.asNode());
|
||||||
|
} else {
|
||||||
|
try self.writer.writeAll(label orelse "");
|
||||||
|
}
|
||||||
|
try self.writer.writeAll("](");
|
||||||
|
if (href) |h| {
|
||||||
|
try self.writer.writeAll(h);
|
||||||
|
}
|
||||||
|
try self.writer.writeByte(')');
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.input => {
|
||||||
|
const type_attr = el.getAttributeSafe(comptime .wrap("type")) orelse return;
|
||||||
|
if (std.ascii.eqlIgnoreCase(type_attr, "checkbox")) {
|
||||||
|
const checked = el.getAttributeSafe(comptime .wrap("checked")) != null;
|
||||||
|
try self.writer.writeAll(if (checked) "[x] " else "[ ] ");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Render Children ---
|
||||||
|
try self.renderChildren(el.asNode());
|
||||||
|
|
||||||
|
// --- Closing Tag Logic ---
|
||||||
|
|
||||||
|
// Suffixes
|
||||||
|
switch (tag) {
|
||||||
|
.pre => {
|
||||||
|
if (!self.state.last_char_was_newline) {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
try self.writer.writeAll("```\n");
|
||||||
|
self.state.pre_node = null;
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
},
|
||||||
|
.code => {
|
||||||
|
if (self.state.pre_node == null) {
|
||||||
|
try self.writer.writeByte('`');
|
||||||
|
self.state.in_code = false;
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.b, .strong => {
|
||||||
|
try self.writer.writeAll("**");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.i, .em => {
|
||||||
|
try self.writer.writeAll("*");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.s, .del => {
|
||||||
|
try self.writer.writeAll("~~");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.blockquote => {},
|
||||||
|
.ul, .ol => {
|
||||||
|
if (self.state.list_depth > 0) self.state.list_depth -= 1;
|
||||||
|
},
|
||||||
|
.table => {
|
||||||
|
self.state.in_table = false;
|
||||||
|
},
|
||||||
|
.tr => {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
if (self.state.table_row_index == 0) {
|
||||||
|
try self.writer.writeByte('|');
|
||||||
|
for (0..self.state.table_col_count) |_| {
|
||||||
|
try self.writer.writeAll("---|");
|
||||||
|
}
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
self.state.table_row_index += 1;
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
},
|
||||||
|
.td, .th => {
|
||||||
|
try self.writer.writeAll(" |");
|
||||||
|
self.state.table_col_count += 1;
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-block newlines
|
||||||
|
if (tag.isBlock() and !self.state.in_table) {
|
||||||
|
try self.ensureNewline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderText(self: *Context, text: []const u8) !void {
|
||||||
|
if (text.len == 0) return;
|
||||||
|
|
||||||
|
if (self.state.pre_node) |_| {
|
||||||
|
try self.writer.writeAll(text);
|
||||||
|
self.state.last_char_was_newline = text[text.len - 1] == '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for pure whitespace
|
||||||
|
if (isAllWhitespace(text)) {
|
||||||
|
if (!self.state.last_char_was_newline) {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse whitespace
|
||||||
|
var it = std.mem.tokenizeAny(u8, text, " \t\n\r");
|
||||||
|
var first = true;
|
||||||
|
while (it.next()) |word| {
|
||||||
|
if (!first or (!self.state.last_char_was_newline and std.ascii.isWhitespace(text[0]))) {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.escape(word);
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle trailing whitespace from the original text
|
||||||
|
if (!first and !self.state.last_char_was_newline and std.ascii.isWhitespace(text[text.len - 1])) {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape(self: *Context, text: []const u8) !void {
|
||||||
|
for (text) |c| {
|
||||||
|
switch (c) {
|
||||||
|
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '!', '|' => {
|
||||||
|
try self.writer.writeByte('\\');
|
||||||
|
try self.writer.writeByte(c);
|
||||||
|
},
|
||||||
|
else => try self.writer.writeByte(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
_ = opts;
|
_ = opts;
|
||||||
var state = State{};
|
var ctx: Context = .{
|
||||||
try render(node, &state, writer, page);
|
.state = .{},
|
||||||
if (!state.last_char_was_newline) {
|
.writer = writer,
|
||||||
|
.page = page,
|
||||||
|
};
|
||||||
|
try ctx.render(node);
|
||||||
|
if (!ctx.state.last_char_was_newline) {
|
||||||
try writer.writeByte('\n');
|
try writer.writeByte('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(node: *Node, state: *State, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
|
||||||
switch (node._type) {
|
|
||||||
.document, .document_fragment => {
|
|
||||||
try renderChildren(node, state, writer, page);
|
|
||||||
},
|
|
||||||
.element => |el| {
|
|
||||||
try renderElement(el, state, writer, page);
|
|
||||||
},
|
|
||||||
.cdata => |cd| {
|
|
||||||
if (node.is(Node.CData.Text)) |_| {
|
|
||||||
var text = cd.getData().str();
|
|
||||||
if (state.pre_node) |pre| {
|
|
||||||
if (node.parentNode() == pre and node.nextSibling() == null) {
|
|
||||||
text = std.mem.trimRight(u8, text, " \t\r\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try renderText(text, state, writer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn renderChildren(parent: *Node, state: *State, writer: *std.Io.Writer, page: *Page) !void {
|
|
||||||
var it = parent.childrenIterator();
|
|
||||||
while (it.next()) |child| {
|
|
||||||
try render(child, state, writer, page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Page) !void {
|
|
||||||
const tag = el.getTag();
|
|
||||||
|
|
||||||
if (!isVisibleElement(el)) return;
|
|
||||||
|
|
||||||
// --- Opening Tag Logic ---
|
|
||||||
|
|
||||||
// Ensure block elements start on a new line (double newline for paragraphs etc)
|
|
||||||
if (tag.isBlock() and !state.in_table) {
|
|
||||||
try ensureNewline(state, writer);
|
|
||||||
if (shouldAddSpacing(tag)) {
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
}
|
|
||||||
} else if (tag == .li or tag == .tr) {
|
|
||||||
try ensureNewline(state, writer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefixes
|
|
||||||
switch (tag) {
|
|
||||||
.h1 => try writer.writeAll("# "),
|
|
||||||
.h2 => try writer.writeAll("## "),
|
|
||||||
.h3 => try writer.writeAll("### "),
|
|
||||||
.h4 => try writer.writeAll("#### "),
|
|
||||||
.h5 => try writer.writeAll("##### "),
|
|
||||||
.h6 => try writer.writeAll("###### "),
|
|
||||||
.ul => {
|
|
||||||
if (state.list_depth < state.list_stack.len) {
|
|
||||||
state.list_stack[state.list_depth] = .{ .type = .unordered, .index = 0 };
|
|
||||||
state.list_depth += 1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.ol => {
|
|
||||||
if (state.list_depth < state.list_stack.len) {
|
|
||||||
state.list_stack[state.list_depth] = .{ .type = .ordered, .index = 1 };
|
|
||||||
state.list_depth += 1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.li => {
|
|
||||||
const indent = if (state.list_depth > 0) state.list_depth - 1 else 0;
|
|
||||||
for (0..indent) |_| try writer.writeAll(" ");
|
|
||||||
|
|
||||||
if (state.list_depth > 0 and state.list_stack[state.list_depth - 1].type == .ordered) {
|
|
||||||
const current_list = &state.list_stack[state.list_depth - 1];
|
|
||||||
try writer.print("{d}. ", .{current_list.index});
|
|
||||||
current_list.index += 1;
|
|
||||||
} else {
|
|
||||||
try writer.writeAll("- ");
|
|
||||||
}
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.table => {
|
|
||||||
state.in_table = true;
|
|
||||||
state.table_row_index = 0;
|
|
||||||
state.table_col_count = 0;
|
|
||||||
},
|
|
||||||
.tr => {
|
|
||||||
state.table_col_count = 0;
|
|
||||||
try writer.writeByte('|');
|
|
||||||
},
|
|
||||||
.td, .th => {
|
|
||||||
// Note: leading pipe handled by previous cell closing or tr opening
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
try writer.writeByte(' ');
|
|
||||||
},
|
|
||||||
.blockquote => {
|
|
||||||
try writer.writeAll("> ");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.pre => {
|
|
||||||
try writer.writeAll("```\n");
|
|
||||||
state.pre_node = el.asNode();
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
},
|
|
||||||
.code => {
|
|
||||||
if (state.pre_node == null) {
|
|
||||||
try writer.writeByte('`');
|
|
||||||
state.in_code = true;
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.b, .strong => {
|
|
||||||
try writer.writeAll("**");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.i, .em => {
|
|
||||||
try writer.writeAll("*");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.s, .del => {
|
|
||||||
try writer.writeAll("~~");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.hr => {
|
|
||||||
try writer.writeAll("---\n");
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
.br => {
|
|
||||||
if (state.in_table) {
|
|
||||||
try writer.writeByte(' ');
|
|
||||||
} else {
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
.img => {
|
|
||||||
try writer.writeAll(";
|
|
||||||
if (el.getAttributeSafe(comptime .wrap("src"))) |src| {
|
|
||||||
const absolute_src = URL.resolve(page.call_arena, page.base(), src, .{ .encode = true }) catch src;
|
|
||||||
try writer.writeAll(absolute_src);
|
|
||||||
}
|
|
||||||
try writer.writeAll(")");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
.anchor => {
|
|
||||||
const has_content = hasVisibleContent(el.asNode());
|
|
||||||
const label = getAnchorLabel(el);
|
|
||||||
const href_raw = el.getAttributeSafe(comptime .wrap("href"));
|
|
||||||
|
|
||||||
if (!has_content and label == null and href_raw == null) return;
|
|
||||||
|
|
||||||
const has_block = hasBlockDescendant(el.asNode());
|
|
||||||
const href = if (href_raw) |h| URL.resolve(page.call_arena, page.base(), h, .{ .encode = true }) catch h else null;
|
|
||||||
|
|
||||||
if (has_block) {
|
|
||||||
try renderChildren(el.asNode(), state, writer, page);
|
|
||||||
if (href) |h| {
|
|
||||||
if (!state.last_char_was_newline) try writer.writeByte('\n');
|
|
||||||
try writer.writeAll("([](");
|
|
||||||
try writer.writeAll(h);
|
|
||||||
try writer.writeAll("))\n");
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStandaloneAnchor(el)) {
|
|
||||||
if (!state.last_char_was_newline) try writer.writeByte('\n');
|
|
||||||
try writer.writeByte('[');
|
|
||||||
if (has_content) {
|
|
||||||
try renderChildren(el.asNode(), state, writer, page);
|
|
||||||
} else {
|
|
||||||
try writer.writeAll(label orelse "");
|
|
||||||
}
|
|
||||||
try writer.writeAll("](");
|
|
||||||
if (href) |h| {
|
|
||||||
try writer.writeAll(h);
|
|
||||||
}
|
|
||||||
try writer.writeAll(")\n");
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try writer.writeByte('[');
|
|
||||||
if (has_content) {
|
|
||||||
try renderChildren(el.asNode(), state, writer, page);
|
|
||||||
} else {
|
|
||||||
try writer.writeAll(label orelse "");
|
|
||||||
}
|
|
||||||
try writer.writeAll("](");
|
|
||||||
if (href) |h| {
|
|
||||||
try writer.writeAll(h);
|
|
||||||
}
|
|
||||||
try writer.writeByte(')');
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
.input => {
|
|
||||||
const type_attr = el.getAttributeSafe(comptime .wrap("type")) orelse return;
|
|
||||||
if (std.ascii.eqlIgnoreCase(type_attr, "checkbox")) {
|
|
||||||
const checked = el.getAttributeSafe(comptime .wrap("checked")) != null;
|
|
||||||
try writer.writeAll(if (checked) "[x] " else "[ ] ");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Render Children ---
|
|
||||||
try renderChildren(el.asNode(), state, writer, page);
|
|
||||||
|
|
||||||
// --- Closing Tag Logic ---
|
|
||||||
|
|
||||||
// Suffixes
|
|
||||||
switch (tag) {
|
|
||||||
.pre => {
|
|
||||||
if (!state.last_char_was_newline) {
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
}
|
|
||||||
try writer.writeAll("```\n");
|
|
||||||
state.pre_node = null;
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
},
|
|
||||||
.code => {
|
|
||||||
if (state.pre_node == null) {
|
|
||||||
try writer.writeByte('`');
|
|
||||||
state.in_code = false;
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.b, .strong => {
|
|
||||||
try writer.writeAll("**");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.i, .em => {
|
|
||||||
try writer.writeAll("*");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.s, .del => {
|
|
||||||
try writer.writeAll("~~");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.blockquote => {},
|
|
||||||
.ul, .ol => {
|
|
||||||
if (state.list_depth > 0) state.list_depth -= 1;
|
|
||||||
},
|
|
||||||
.table => {
|
|
||||||
state.in_table = false;
|
|
||||||
},
|
|
||||||
.tr => {
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
if (state.table_row_index == 0) {
|
|
||||||
try writer.writeByte('|');
|
|
||||||
for (0..state.table_col_count) |_| {
|
|
||||||
try writer.writeAll("---|");
|
|
||||||
}
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
}
|
|
||||||
state.table_row_index += 1;
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
},
|
|
||||||
.td, .th => {
|
|
||||||
try writer.writeAll(" |");
|
|
||||||
state.table_col_count += 1;
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post-block newlines
|
|
||||||
if (tag.isBlock() and !state.in_table) {
|
|
||||||
try ensureNewline(state, writer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn renderText(text: []const u8, state: *State, writer: *std.Io.Writer) !void {
|
|
||||||
if (text.len == 0) return;
|
|
||||||
|
|
||||||
if (state.pre_node) |_| {
|
|
||||||
try writer.writeAll(text);
|
|
||||||
state.last_char_was_newline = text[text.len - 1] == '\n';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for pure whitespace
|
|
||||||
if (isAllWhitespace(text)) {
|
|
||||||
if (!state.last_char_was_newline) {
|
|
||||||
try writer.writeByte(' ');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collapse whitespace
|
|
||||||
var it = std.mem.tokenizeAny(u8, text, " \t\n\r");
|
|
||||||
var first = true;
|
|
||||||
while (it.next()) |word| {
|
|
||||||
if (!first or (!state.last_char_was_newline and std.ascii.isWhitespace(text[0]))) {
|
|
||||||
try writer.writeByte(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
try escapeMarkdown(writer, word);
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle trailing whitespace from the original text
|
|
||||||
if (!first and !state.last_char_was_newline and std.ascii.isWhitespace(text[text.len - 1])) {
|
|
||||||
try writer.writeByte(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn escapeMarkdown(writer: *std.Io.Writer, text: []const u8) !void {
|
|
||||||
for (text) |c| {
|
|
||||||
switch (c) {
|
|
||||||
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '!', '|' => {
|
|
||||||
try writer.writeByte('\\');
|
|
||||||
try writer.writeByte(c);
|
|
||||||
},
|
|
||||||
else => try writer.writeByte(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn testMarkdownHTML(html: []const u8, expected: []const u8) !void {
|
fn testMarkdownHTML(html: []const u8, expected: []const u8) !void {
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
const page = try testing.test_session.createPage();
|
const page = try testing.test_session.createPage();
|
||||||
|
|||||||
@@ -24,11 +24,10 @@
|
|||||||
|
|
||||||
<script id=byId name="test1">
|
<script id=byId name="test1">
|
||||||
testing.expectEqual(1, document.querySelector.length);
|
testing.expectEqual(1, document.querySelector.length);
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => document.querySelector(''));
|
testing.expectError("SyntaxError", () => document.querySelector(''));
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(12, err.code);
|
testing.expectEqual(12, err.code);
|
||||||
testing.expectEqual("SyntaxError", err.name);
|
testing.expectEqual("SyntaxError", err.name);
|
||||||
testing.expectEqual("Syntax Error", err.message);
|
|
||||||
}, () => document.querySelector(''));
|
}, () => document.querySelector(''));
|
||||||
|
|
||||||
testing.expectEqual('test1', document.querySelector('#byId').getAttribute('name'));
|
testing.expectEqual('test1', document.querySelector('#byId').getAttribute('name'));
|
||||||
|
|||||||
@@ -34,11 +34,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=script1 name="test1">
|
<script id=script1 name="test1">
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => document.querySelectorAll(''));
|
testing.expectError("SyntaxError", () => document.querySelectorAll(''));
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(12, err.code);
|
testing.expectEqual(12, err.code);
|
||||||
testing.expectEqual("SyntaxError", err.name);
|
testing.expectEqual("SyntaxError", err.name);
|
||||||
testing.expectEqual("Syntax Error", err.message);
|
|
||||||
}, () => document.querySelectorAll(''));
|
}, () => document.querySelectorAll(''));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
|
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(3, err.code);
|
testing.expectEqual(3, err.code);
|
||||||
testing.expectEqual('Hierarchy Error', err.message);
|
testing.expectEqual('HierarchyRequestError', err.name);
|
||||||
testing.expectEqual(true, err instanceof DOMException);
|
testing.expectEqual(true, err instanceof DOMException);
|
||||||
testing.expectEqual(true, err instanceof Error);
|
testing.expectEqual(true, err instanceof Error);
|
||||||
}, () => link.appendChild(content));
|
}, () => link.appendChild(content));
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(8, err.code);
|
testing.expectEqual(8, err.code);
|
||||||
testing.expectEqual("NotFoundError", err.name);
|
testing.expectEqual("NotFoundError", err.name);
|
||||||
testing.expectEqual("Not Found", err.message);
|
|
||||||
}, () => el1.removeAttributeNode(script_id_node));
|
}, () => el1.removeAttributeNode(script_id_node));
|
||||||
|
|
||||||
testing.expectEqual(an1, el1.removeAttributeNode(an1));
|
testing.expectEqual(an1, el1.removeAttributeNode(an1));
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
testing.expectEqual('', $('#a0').href);
|
testing.expectEqual('', $('#a0').href);
|
||||||
|
|
||||||
testing.expectEqual(testing.BASE_URL + 'element/anchor1.html', $('#a1').href);
|
testing.expectEqual(testing.BASE_URL + 'element/anchor1.html', $('#a1').href);
|
||||||
testing.expectEqual(testing.ORIGIN + 'hello/world/anchor2.html', $('#a2').href);
|
testing.expectEqual(testing.ORIGIN + '/hello/world/anchor2.html', $('#a2').href);
|
||||||
testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href);
|
testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href);
|
||||||
|
|
||||||
testing.expectEqual(testing.BASE_URL + 'element/html/foo', $('#link').href);
|
testing.expectEqual(testing.BASE_URL + 'element/html/foo', $('#link').href);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
testing.expectEqual(testing.BASE_URL + 'element/html/hello', form.action)
|
testing.expectEqual(testing.BASE_URL + 'element/html/hello', form.action)
|
||||||
|
|
||||||
form.action = '/hello';
|
form.action = '/hello';
|
||||||
testing.expectEqual(testing.ORIGIN + 'hello', form.action)
|
testing.expectEqual(testing.ORIGIN + '/hello', form.action)
|
||||||
|
|
||||||
form.action = 'https://lightpanda.io/hello';
|
form.action = 'https://lightpanda.io/hello';
|
||||||
testing.expectEqual('https://lightpanda.io/hello', form.action)
|
testing.expectEqual('https://lightpanda.io/hello', form.action)
|
||||||
@@ -343,3 +343,123 @@
|
|||||||
testing.expectEqual('', form.elements['choice'].value)
|
testing.expectEqual('', form.elements['choice'].value)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Test: requestSubmit() fires the submit event (unlike submit()) -->
|
||||||
|
<form id="test_form2" action="/should-not-navigate2" method="get">
|
||||||
|
<input name="q" value="test2">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script id="requestSubmit_fires_submit_event">
|
||||||
|
{
|
||||||
|
const form = $('#test_form2');
|
||||||
|
let submitFired = false;
|
||||||
|
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
submitFired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
form.requestSubmit();
|
||||||
|
|
||||||
|
testing.expectEqual(true, submitFired);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Test: requestSubmit() with preventDefault stops navigation -->
|
||||||
|
<form id="test_form3" action="/should-not-navigate3" method="get">
|
||||||
|
<input name="q" value="test3">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script id="requestSubmit_respects_preventDefault">
|
||||||
|
{
|
||||||
|
const form = $('#test_form3');
|
||||||
|
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
form.requestSubmit();
|
||||||
|
|
||||||
|
// Form submission was prevented, so no navigation should be scheduled
|
||||||
|
testing.expectEqual(true, true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Test: requestSubmit() with non-submit-button submitter throws TypeError -->
|
||||||
|
<form id="test_form_rs1" action="/should-not-navigate4" method="get">
|
||||||
|
<input id="rs1_text" type="text" name="q" value="test">
|
||||||
|
<input id="rs1_submit" type="submit" value="Go">
|
||||||
|
<input id="rs1_image" type="image" src="x.png">
|
||||||
|
<button id="rs1_btn_submit" type="submit">Submit</button>
|
||||||
|
<button id="rs1_btn_reset" type="reset">Reset</button>
|
||||||
|
<button id="rs1_btn_button" type="button">Button</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script id="requestSubmit_rejects_non_submit_button">
|
||||||
|
{
|
||||||
|
const form = $('#test_form_rs1');
|
||||||
|
form.addEventListener('submit', (e) => e.preventDefault());
|
||||||
|
|
||||||
|
// A text input is not a submit button — should throw TypeError
|
||||||
|
testing.expectError('TypeError', () => {
|
||||||
|
form.requestSubmit($('#rs1_text'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// A reset button is not a submit button — should throw TypeError
|
||||||
|
testing.expectError('TypeError', () => {
|
||||||
|
form.requestSubmit($('#rs1_btn_reset'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// A <button type="button"> is not a submit button — should throw TypeError
|
||||||
|
testing.expectError('TypeError', () => {
|
||||||
|
form.requestSubmit($('#rs1_btn_button'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// A <div> is not a submit button — should throw TypeError
|
||||||
|
const div = document.createElement('div');
|
||||||
|
form.appendChild(div);
|
||||||
|
testing.expectError('TypeError', () => {
|
||||||
|
form.requestSubmit(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Test: requestSubmit() accepts valid submit buttons -->
|
||||||
|
<script id="requestSubmit_accepts_submit_buttons">
|
||||||
|
{
|
||||||
|
const form = $('#test_form_rs1');
|
||||||
|
let submitCount = 0;
|
||||||
|
form.addEventListener('submit', (e) => { e.preventDefault(); submitCount++; });
|
||||||
|
|
||||||
|
// <input type="submit"> is a valid submitter
|
||||||
|
form.requestSubmit($('#rs1_submit'));
|
||||||
|
testing.expectEqual(1, submitCount);
|
||||||
|
|
||||||
|
// <input type="image"> is a valid submitter
|
||||||
|
form.requestSubmit($('#rs1_image'));
|
||||||
|
testing.expectEqual(2, submitCount);
|
||||||
|
|
||||||
|
// <button type="submit"> is a valid submitter
|
||||||
|
form.requestSubmit($('#rs1_btn_submit'));
|
||||||
|
testing.expectEqual(3, submitCount);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Test: requestSubmit() with submitter not owned by form throws NotFoundError -->
|
||||||
|
<form id="test_form_rs2" action="/should-not-navigate5" method="get">
|
||||||
|
<input type="text" name="q" value="test">
|
||||||
|
</form>
|
||||||
|
<form id="test_form_rs3">
|
||||||
|
<input id="rs3_submit" type="submit" value="Other Submit">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script id="requestSubmit_rejects_wrong_form_submitter">
|
||||||
|
{
|
||||||
|
const form = $('#test_form_rs2');
|
||||||
|
|
||||||
|
// Submit button belongs to a different form — should throw NotFoundError
|
||||||
|
testing.expectError('NotFoundError', () => {
|
||||||
|
form.requestSubmit($('#rs3_submit'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
testing.expectEqual('test.png', img.getAttribute('src'));
|
testing.expectEqual('test.png', img.getAttribute('src'));
|
||||||
|
|
||||||
img.src = '/absolute/path.png';
|
img.src = '/absolute/path.png';
|
||||||
testing.expectEqual(testing.ORIGIN + 'absolute/path.png', img.src);
|
testing.expectEqual(testing.ORIGIN + '/absolute/path.png', img.src);
|
||||||
testing.expectEqual('/absolute/path.png', img.getAttribute('src'));
|
testing.expectEqual('/absolute/path.png', img.getAttribute('src'));
|
||||||
|
|
||||||
img.src = 'https://example.com/image.png';
|
img.src = 'https://example.com/image.png';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);
|
testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);
|
||||||
|
|
||||||
l2.href = '/over/9000';
|
l2.href = '/over/9000';
|
||||||
testing.expectEqual(testing.ORIGIN + 'over/9000', l2.href);
|
testing.expectEqual(testing.ORIGIN + '/over/9000', l2.href);
|
||||||
|
|
||||||
l2.crossOrigin = 'nope';
|
l2.crossOrigin = 'nope';
|
||||||
testing.expectEqual('anonymous', l2.crossOrigin);
|
testing.expectEqual('anonymous', l2.crossOrigin);
|
||||||
@@ -84,3 +84,24 @@
|
|||||||
testing.eventually(() => testing.expectEqual(true, result));
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="refs">
|
||||||
|
{
|
||||||
|
const rels = ['stylesheet', 'preload', 'modulepreload'];
|
||||||
|
const results = rels.map(() => false);
|
||||||
|
rels.forEach((rel, i) => {
|
||||||
|
let link = document.createElement('link')
|
||||||
|
link.rel = rel;
|
||||||
|
link.href = '/nope';
|
||||||
|
link.onload = () => results[i] = true;
|
||||||
|
document.documentElement.appendChild(link);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
results.forEach((r) => {
|
||||||
|
testing.expectEqual(true, r);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -66,11 +66,10 @@
|
|||||||
{
|
{
|
||||||
const container = $('#test-container');
|
const container = $('#test-container');
|
||||||
|
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => container.matches(''));
|
testing.expectError("SyntaxError", () => container.matches(''));
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(12, err.code);
|
testing.expectEqual(12, err.code);
|
||||||
testing.expectEqual("SyntaxError", err.name);
|
testing.expectEqual("SyntaxError", err.name);
|
||||||
testing.expectEqual("Syntax Error", err.message);
|
|
||||||
}, () => container.matches(''));
|
}, () => container.matches(''));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,11 +12,10 @@
|
|||||||
const p1 = $('#p1');
|
const p1 = $('#p1');
|
||||||
testing.expectEqual(null, p1.querySelector('#p1'));
|
testing.expectEqual(null, p1.querySelector('#p1'));
|
||||||
|
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => p1.querySelector(''));
|
testing.expectError("SyntaxError", () => p1.querySelector(''));
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(12, err.code);
|
testing.expectEqual(12, err.code);
|
||||||
testing.expectEqual("SyntaxError", err.name);
|
testing.expectEqual("SyntaxError", err.name);
|
||||||
testing.expectEqual("Syntax Error", err.message);
|
|
||||||
}, () => p1.querySelector(''));
|
}, () => p1.querySelector(''));
|
||||||
|
|
||||||
testing.expectEqual($('#c2'), p1.querySelector('#c2'));
|
testing.expectEqual($('#c2'), p1.querySelector('#c2'));
|
||||||
|
|||||||
@@ -24,11 +24,10 @@
|
|||||||
<script id=errors>
|
<script id=errors>
|
||||||
{
|
{
|
||||||
const root = $('#root');
|
const root = $('#root');
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => root.querySelectorAll(''));
|
testing.expectError("SyntaxError", () => root.querySelectorAll(''));
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(12, err.code);
|
testing.expectEqual(12, err.code);
|
||||||
testing.expectEqual("SyntaxError", err.name);
|
testing.expectEqual("SyntaxError", err.name);
|
||||||
testing.expectEqual("Syntax Error", err.message);
|
|
||||||
}, () => root.querySelectorAll(''));
|
}, () => root.querySelectorAll(''));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -43,8 +43,8 @@
|
|||||||
const container = $('#container');
|
const container = $('#container');
|
||||||
|
|
||||||
// Empty selectors
|
// Empty selectors
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => container.querySelector(''));
|
testing.expectError("SyntaxError", () => container.querySelector(''));
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => document.querySelectorAll(''));
|
testing.expectError("SyntaxError", () => document.querySelectorAll(''));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
38
src/browser/tests/event/report_error.html
Normal file
38
src/browser/tests/event/report_error.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=onerrorFiveArguments>
|
||||||
|
let called = false;
|
||||||
|
let argCount = 0;
|
||||||
|
window.onerror = function() {
|
||||||
|
called = true;
|
||||||
|
argCount = arguments.length;
|
||||||
|
return true; // suppress default
|
||||||
|
};
|
||||||
|
try { undefinedVariable; } catch(e) { window.reportError(e); }
|
||||||
|
testing.expectEqual(true, called);
|
||||||
|
testing.expectEqual(5, argCount);
|
||||||
|
window.onerror = null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorCalledBeforeEventListener>
|
||||||
|
let callOrder = [];
|
||||||
|
window.onerror = function() { callOrder.push('onerror'); return true; };
|
||||||
|
window.addEventListener('error', function() { callOrder.push('listener'); });
|
||||||
|
try { undefinedVariable; } catch(e) { window.reportError(e); }
|
||||||
|
testing.expectEqual('onerror', callOrder[0]);
|
||||||
|
testing.expectEqual('listener', callOrder[1]);
|
||||||
|
window.onerror = null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorReturnTrueSuppresses>
|
||||||
|
let listenerCalled = false;
|
||||||
|
window.onerror = function() { return true; };
|
||||||
|
window.addEventListener('error', function(e) {
|
||||||
|
// listener still fires even when onerror returns true
|
||||||
|
listenerCalled = true;
|
||||||
|
});
|
||||||
|
try { undefinedVariable; } catch(e) { window.reportError(e); }
|
||||||
|
testing.expectEqual(true, listenerCalled);
|
||||||
|
window.onerror = null;
|
||||||
|
</script>
|
||||||
25
src/browser/tests/frames/post_message.html
Normal file
25
src/browser/tests/frames/post_message.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<iframe id="receiver"></iframe>
|
||||||
|
|
||||||
|
<script id="messages">
|
||||||
|
{
|
||||||
|
let reply = null;
|
||||||
|
window.addEventListener('message', (e) => {
|
||||||
|
console.warn('reply')
|
||||||
|
reply = e.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
const iframe = $('#receiver');
|
||||||
|
iframe.src = 'support/message_receiver.html';
|
||||||
|
iframe.addEventListener('load', () => {
|
||||||
|
iframe.contentWindow.postMessage('ping', '*');
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual('pong', reply.data);
|
||||||
|
testing.expectEqual(testing.ORIGIN, reply.origin);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
9
src/browser/tests/frames/support/message_receiver.html
Normal file
9
src/browser/tests/frames/support/message_receiver.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script>
|
||||||
|
window.addEventListener('message', (e) => {
|
||||||
|
console.warn('Frame Message', e.data);
|
||||||
|
if (e.data === 'ping') {
|
||||||
|
window.top.postMessage({data: 'pong', origin: e.origin}, '*');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -2,37 +2,17 @@
|
|||||||
<script src="testing.js"></script>
|
<script src="testing.js"></script>
|
||||||
|
|
||||||
<script id=history>
|
<script id=history>
|
||||||
testing.expectEqual('auto', history.scrollRestoration);
|
// This test is a bit wonky. But it's trying to test navigation, which is
|
||||||
|
// something we can't do in the main page (we can't navigate away from this
|
||||||
history.scrollRestoration = 'manual';
|
// page and still assertOk in the test runner).
|
||||||
testing.expectEqual('manual', history.scrollRestoration);
|
// If support/history.html has a failed assertion, it'll log the error and
|
||||||
|
// stop the script. If it succeeds, it'll set support_history_completed
|
||||||
history.scrollRestoration = 'auto';
|
// which we can use here to assume everything passed.
|
||||||
testing.expectEqual('auto', history.scrollRestoration);
|
|
||||||
testing.expectEqual(null, history.state)
|
|
||||||
|
|
||||||
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/src/browser/tests/history_after_nav.skip.html');
|
|
||||||
testing.expectEqual({ testInProgress: true }, history.state);
|
|
||||||
|
|
||||||
history.pushState({ testInProgress: false }, null, 'http://127.0.0.1:9582/xhr/json');
|
|
||||||
history.replaceState({ "new": "field", testComplete: true }, null);
|
|
||||||
|
|
||||||
let state = { "new": "field", testComplete: true };
|
|
||||||
testing.expectEqual(state, history.state);
|
|
||||||
|
|
||||||
let popstateEventFired = false;
|
|
||||||
let popstateEventState = null;
|
|
||||||
|
|
||||||
window.addEventListener('popstate', (event) => {
|
|
||||||
popstateEventFired = true;
|
|
||||||
popstateEventState = event.state;
|
|
||||||
});
|
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.eventually(() => {
|
||||||
testing.expectEqual(true, popstateEventFired);
|
testing.expectEqual(true, window.support_history_completed);
|
||||||
testing.expectEqual({testInProgress: true }, popstateEventState);
|
testing.expectEqual(true, window.support_history_popstateEventFired);
|
||||||
})
|
testing.expectEqual({testInProgress: true }, window.support_history_popstateEventState);
|
||||||
|
});
|
||||||
history.back();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<iframe id=frame src="support/history.html"></iframe>
|
||||||
|
|||||||
14
src/browser/tests/mcp_actions.html
Normal file
14
src/browser/tests/mcp_actions.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<button id="btn" onclick="window.clicked = true;">Click Me</button>
|
||||||
|
<input id="inp" oninput="window.inputVal = this.value" onchange="window.changed = true;">
|
||||||
|
<select id="sel" onchange="window.selChanged = this.value">
|
||||||
|
<option value="opt1">Option 1</option>
|
||||||
|
<option value="opt2">Option 2</option>
|
||||||
|
</select>
|
||||||
|
<div id="scrollbox" style="width: 100px; height: 100px; overflow: scroll;" onscroll="window.scrolled = true;">
|
||||||
|
<div style="height: 500px;">Long content</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -27,3 +27,44 @@
|
|||||||
testing.expectEqual(false, navigator.javaEnabled());
|
testing.expectEqual(false, navigator.javaEnabled());
|
||||||
testing.expectEqual(false, navigator.webdriver);
|
testing.expectEqual(false, navigator.webdriver);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=permission_query>
|
||||||
|
testing.async(async (restore) => {
|
||||||
|
const p = navigator.permissions.query({ name: 'notifications' });
|
||||||
|
testing.expectTrue(p instanceof Promise);
|
||||||
|
const status = await p;
|
||||||
|
restore();
|
||||||
|
testing.expectEqual('prompt', status.state);
|
||||||
|
testing.expectEqual('notifications', status.name);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=storage_estimate>
|
||||||
|
testing.async(async (restore) => {
|
||||||
|
const p = navigator.storage.estimate();
|
||||||
|
testing.expectTrue(p instanceof Promise);
|
||||||
|
|
||||||
|
const estimate = await p;
|
||||||
|
restore();
|
||||||
|
testing.expectEqual(0, estimate.usage);
|
||||||
|
testing.expectEqual(1024 * 1024 * 1024, estimate.quota);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=deviceMemory>
|
||||||
|
testing.expectEqual(8, navigator.deviceMemory);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=getBattery>
|
||||||
|
testing.async(async (restore) => {
|
||||||
|
const p = navigator.getBattery();
|
||||||
|
try {
|
||||||
|
await p;
|
||||||
|
testing.fail('getBattery should reject');
|
||||||
|
} catch (err) {
|
||||||
|
restore();
|
||||||
|
testing.expectEqual('NotSupportedError', err.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -203,3 +203,39 @@
|
|||||||
testing.expectEqual(true, response.body !== null);
|
testing.expectEqual(true, response.body !== null);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=fetch_blob_url>
|
||||||
|
testing.async(async (restore) => {
|
||||||
|
// Create a blob and get its URL
|
||||||
|
const blob = new Blob(['Hello from blob!'], { type: 'text/plain' });
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const response = await fetch(blobUrl);
|
||||||
|
restore();
|
||||||
|
|
||||||
|
testing.expectEqual(200, response.status);
|
||||||
|
testing.expectEqual(true, response.ok);
|
||||||
|
testing.expectEqual(blobUrl, response.url);
|
||||||
|
testing.expectEqual('text/plain', response.headers.get('Content-Type'));
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
testing.expectEqual('Hello from blob!', text);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=abort>
|
||||||
|
testing.async(async (restore) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
controller.abort();
|
||||||
|
try {
|
||||||
|
await fetch('http://127.0.0.1:9582/xhr', { signal: controller.signal });
|
||||||
|
testain.fail('fetch should have been aborted');
|
||||||
|
} catch (e) {
|
||||||
|
restore();
|
||||||
|
testing.expectEqual("AbortError", e.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -283,3 +283,26 @@
|
|||||||
testing.expectEqual(XMLHttpRequest.UNSENT, req.readyState);
|
testing.expectEqual(XMLHttpRequest.UNSENT, req.readyState);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=xhr_blob_url>
|
||||||
|
testing.async(async (restore) => {
|
||||||
|
// Create a blob and get its URL
|
||||||
|
const blob = new Blob(['Hello from blob!'], { type: 'text/plain' });
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const req = new XMLHttpRequest();
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
req.onload = resolve;
|
||||||
|
req.open('GET', blobUrl);
|
||||||
|
req.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
restore();
|
||||||
|
testing.expectEqual(200, req.status);
|
||||||
|
testing.expectEqual('Hello from blob!', req.responseText);
|
||||||
|
testing.expectEqual(blobUrl, req.responseURL);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(8, err.code);
|
testing.expectEqual(8, err.code);
|
||||||
testing.expectEqual("NotFoundError", err.name);
|
testing.expectEqual("NotFoundError", err.name);
|
||||||
testing.expectEqual("Not Found", err.message);
|
|
||||||
}, () => d1.insertBefore(document.createElement('div'), d2));
|
}, () => d1.insertBefore(document.createElement('div'), d2));
|
||||||
|
|
||||||
let c1 = document.createElement('div');
|
let c1 = document.createElement('div');
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(8, err.code);
|
testing.expectEqual(8, err.code);
|
||||||
testing.expectEqual("NotFoundError", err.name);
|
testing.expectEqual("NotFoundError", err.name);
|
||||||
testing.expectEqual("Not Found", err.message);
|
|
||||||
}, () => $('#d1').removeChild($('#p1')));
|
}, () => $('#d1').removeChild($('#p1')));
|
||||||
|
|
||||||
const p1 = $('#p1');
|
const p1 = $('#p1');
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(3, err.code);
|
testing.expectEqual(3, err.code);
|
||||||
testing.expectEqual("HierarchyRequestError", err.name);
|
testing.expectEqual("HierarchyRequestError", err.name);
|
||||||
testing.expectEqual("Hierarchy Error", err.message);
|
|
||||||
}, () => d1.replaceChild(c4, c3));
|
}, () => d1.replaceChild(c4, c3));
|
||||||
|
|
||||||
testing.expectEqual(c2, d1.replaceChild(c4, c2));
|
testing.expectEqual(c2, d1.replaceChild(c4, c2));
|
||||||
|
|||||||
@@ -451,12 +451,12 @@
|
|||||||
const p1 = $('#p1');
|
const p1 = $('#p1');
|
||||||
|
|
||||||
// Test setStart with offset beyond node length
|
// Test setStart with offset beyond node length
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.setStart(p1, 999);
|
range.setStart(p1, 999);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test with negative offset (wraps to large u32)
|
// Test with negative offset (wraps to large u32)
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.setStart(p1.firstChild, -1);
|
range.setStart(p1.firstChild, -1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -468,12 +468,12 @@
|
|||||||
const p1 = $('#p1');
|
const p1 = $('#p1');
|
||||||
|
|
||||||
// Test setEnd with offset beyond node length
|
// Test setEnd with offset beyond node length
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.setEnd(p1, 999);
|
range.setEnd(p1, 999);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test with text node
|
// Test with text node
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.setEnd(p1.firstChild, 9999);
|
range.setEnd(p1.firstChild, 9999);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -525,11 +525,11 @@
|
|||||||
range.setEnd(p1, 1);
|
range.setEnd(p1, 1);
|
||||||
|
|
||||||
// Test comparePoint with invalid offset
|
// Test comparePoint with invalid offset
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.comparePoint(p1, 20);
|
range.comparePoint(p1, 20);
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.comparePoint(p1.firstChild, -1);
|
range.comparePoint(p1.firstChild, -1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -650,11 +650,11 @@
|
|||||||
range.setEnd(p1, 1);
|
range.setEnd(p1, 1);
|
||||||
|
|
||||||
// Invalid offset should throw IndexSizeError
|
// Invalid offset should throw IndexSizeError
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.isPointInRange(p1, 999);
|
range.isPointInRange(p1, 999);
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.isPointInRange(p1.firstChild, 9999);
|
range.isPointInRange(p1.firstChild, 9999);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -854,11 +854,11 @@
|
|||||||
range2.setStart(p, 0);
|
range2.setStart(p, 0);
|
||||||
|
|
||||||
// Invalid how parameter should throw NotSupportedError
|
// Invalid how parameter should throw NotSupportedError
|
||||||
testing.expectError('NotSupportedError: Not Supported', () => {
|
testing.expectError('NotSupportedError:', () => {
|
||||||
range1.compareBoundaryPoints(4, range2);
|
range1.compareBoundaryPoints(4, range2);
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.expectError('NotSupportedError: Not Supported', () => {
|
testing.expectError('NotSupportedError:', () => {
|
||||||
range1.compareBoundaryPoints(99, range2);
|
range1.compareBoundaryPoints(99, range2);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -883,7 +883,7 @@
|
|||||||
range2.setEnd(foreignP, 1);
|
range2.setEnd(foreignP, 1);
|
||||||
|
|
||||||
// Comparing ranges in different documents should throw WrongDocumentError
|
// Comparing ranges in different documents should throw WrongDocumentError
|
||||||
testing.expectError('WrongDocumentError: wrong_document_error', () => {
|
testing.expectError('WrongDocumentError:', () => {
|
||||||
range1.compareBoundaryPoints(Range.START_TO_START, range2);
|
range1.compareBoundaryPoints(Range.START_TO_START, range2);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div id="host2"></div>
|
<div id="host2"></div>
|
||||||
<div id="host3"></div>
|
<div id="host3"></div>
|
||||||
|
|
||||||
<!-- <script id="attachShadow_open">
|
<script id="attachShadow_open">
|
||||||
{
|
{
|
||||||
const host = $('#host1');
|
const host = $('#host1');
|
||||||
const shadow = host.attachShadow({ mode: 'open' });
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
shadow.replaceChildren('New content');
|
shadow.replaceChildren('New content');
|
||||||
testing.expectEqual('New content', shadow.innerHTML);
|
testing.expectEqual('New content', shadow.innerHTML);
|
||||||
}
|
}
|
||||||
</script> -->
|
</script>
|
||||||
|
|
||||||
<script id="getElementById">
|
<script id="getElementById">
|
||||||
{
|
{
|
||||||
@@ -154,3 +154,16 @@
|
|||||||
testing.expectEqual(null, shadow.getElementById('nonexistent'));
|
testing.expectEqual(null, shadow.getElementById('nonexistent'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script id=adoptedStyleSheets>
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
const acss = shadow.adoptedStyleSheets;
|
||||||
|
testing.expectEqual(0, acss.length);
|
||||||
|
acss.push(new CSSStyleSheet());
|
||||||
|
testing.expectEqual(1, acss.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
33
src/browser/tests/support/history.html
Normal file
33
src/browser/tests/support/history.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id=history>
|
||||||
|
testing.expectEqual('auto', history.scrollRestoration);
|
||||||
|
|
||||||
|
history.scrollRestoration = 'manual';
|
||||||
|
testing.expectEqual('manual', history.scrollRestoration);
|
||||||
|
|
||||||
|
history.scrollRestoration = 'auto';
|
||||||
|
testing.expectEqual('auto', history.scrollRestoration);
|
||||||
|
testing.expectEqual(null, history.state)
|
||||||
|
|
||||||
|
history.pushState({ testInProgress: true }, null, testing.BASE_URL + 'history_after_nav.skip.html');
|
||||||
|
testing.expectEqual({ testInProgress: true }, history.state);
|
||||||
|
|
||||||
|
history.pushState({ testInProgress: false }, null, testing.ORIGIN + '/xhr/json');
|
||||||
|
history.replaceState({ "new": "field", testComplete: true }, null);
|
||||||
|
|
||||||
|
let state = { "new": "field", testComplete: true };
|
||||||
|
testing.expectEqual(state, history.state);
|
||||||
|
|
||||||
|
let popstateEventFired = false;
|
||||||
|
let popstateEventState = null;
|
||||||
|
|
||||||
|
window.top.support_history_completed = true;
|
||||||
|
window.addEventListener('popstate', (event) => {
|
||||||
|
window.top.window.support_history_popstateEventFired = true;
|
||||||
|
window.top.window.support_history_popstateEventState = event.state;
|
||||||
|
});
|
||||||
|
|
||||||
|
history.back();
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -99,8 +99,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// our test runner sets this to true
|
const IS_TEST_RUNNER = window.navigator.userAgent.startsWith("Lightpanda/");
|
||||||
const IS_TEST_RUNNER = window._lightpanda_skip_auto_assert === true;
|
|
||||||
|
|
||||||
window.testing = {
|
window.testing = {
|
||||||
fail: fail,
|
fail: fail,
|
||||||
@@ -114,17 +113,17 @@
|
|||||||
eventually: eventually,
|
eventually: eventually,
|
||||||
IS_TEST_RUNNER: IS_TEST_RUNNER,
|
IS_TEST_RUNNER: IS_TEST_RUNNER,
|
||||||
HOST: '127.0.0.1',
|
HOST: '127.0.0.1',
|
||||||
ORIGIN: 'http://127.0.0.1:9582/',
|
ORIGIN: 'http://127.0.0.1:9582',
|
||||||
BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/',
|
BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window.navigator.userAgent.startsWith("Lightpanda/") == false) {
|
if (IS_TEST_RUNNER === false) {
|
||||||
// The page is running in a different browser. Probably a developer making sure
|
// The page is running in a different browser. Probably a developer making sure
|
||||||
// a test is correct. There are a few tweaks we need to do to make this a
|
// a test is correct. There are a few tweaks we need to do to make this a
|
||||||
// seemless, namely around adapting paths/urls.
|
// seemless, namely around adapting paths/urls.
|
||||||
console.warn(`The page is not being executed in the test runner, certain behavior has been adjusted`);
|
console.warn(`The page is not being executed in the test runner, certain behavior has been adjusted`);
|
||||||
window.testing.HOST = location.hostname;
|
window.testing.HOST = location.hostname;
|
||||||
window.testing.ORIGIN = location.origin + '/';
|
window.testing.ORIGIN = location.origin;
|
||||||
window.testing.BASE_URL = location.origin + '/src/browser/tests/';
|
window.testing.BASE_URL = location.origin + '/src/browser/tests/';
|
||||||
window.addEventListener('load', testing.assertOk);
|
window.addEventListener('load', testing.assertOk);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
testing.expectEqual('ceil', atob('Y2VpbA')); // 6 chars, len%4==2, needs '=='
|
testing.expectEqual('ceil', atob('Y2VpbA')); // 6 chars, len%4==2, needs '=='
|
||||||
|
|
||||||
// length % 4 == 1 must still throw
|
// length % 4 == 1 must still throw
|
||||||
testing.expectError('InvalidCharacterError: Invalid Character', () => {
|
testing.expectError('InvalidCharacterError', () => {
|
||||||
atob('Y');
|
atob('Y');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
34
src/browser/tests/window/window_event.html
Normal file
34
src/browser/tests/window/window_event.html
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=windowEventUndefinedOutsideHandler>
|
||||||
|
testing.expectEqual(undefined, window.event);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=windowEventSetDuringWindowHandler>
|
||||||
|
var capturedEvent = null;
|
||||||
|
|
||||||
|
window.addEventListener('test-event', function(e) {
|
||||||
|
capturedEvent = window.event;
|
||||||
|
});
|
||||||
|
|
||||||
|
var ev = new Event('test-event');
|
||||||
|
window.dispatchEvent(ev);
|
||||||
|
|
||||||
|
testing.expectEqual(ev, capturedEvent);
|
||||||
|
testing.expectEqual(undefined, window.event);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=windowEventRestoredAfterHandler>
|
||||||
|
var captured2 = null;
|
||||||
|
|
||||||
|
window.addEventListener('test-event-2', function(e) {
|
||||||
|
captured2 = window.event;
|
||||||
|
});
|
||||||
|
|
||||||
|
var ev2 = new Event('test-event-2');
|
||||||
|
window.dispatchEvent(ev2);
|
||||||
|
|
||||||
|
testing.expectEqual(ev2, captured2);
|
||||||
|
testing.expectEqual(undefined, window.event);
|
||||||
|
</script>
|
||||||
@@ -104,13 +104,27 @@ pub fn getMessage(self: *const DOMException) []const u8 {
|
|||||||
}
|
}
|
||||||
return switch (self._code) {
|
return switch (self._code) {
|
||||||
.none => "",
|
.none => "",
|
||||||
.invalid_character_error => "Invalid Character",
|
|
||||||
.index_size_error => "Index or size is negative or greater than the allowed amount",
|
.index_size_error => "Index or size is negative or greater than the allowed amount",
|
||||||
.syntax_error => "Syntax Error",
|
.hierarchy_error => "The operation would yield an incorrect node tree",
|
||||||
.not_supported => "Not Supported",
|
.wrong_document_error => "The object is in the wrong document",
|
||||||
.not_found => "Not Found",
|
.invalid_character_error => "The string contains invalid characters",
|
||||||
.hierarchy_error => "Hierarchy Error",
|
.no_modification_allowed_error => "The object can not be modified",
|
||||||
else => @tagName(self._code),
|
.not_found => "The object can not be found here",
|
||||||
|
.not_supported => "The operation is not supported",
|
||||||
|
.inuse_attribute_error => "The attribute already in use",
|
||||||
|
.invalid_state_error => "The object is in an invalid state",
|
||||||
|
.syntax_error => "The string did not match the expected pattern",
|
||||||
|
.invalid_modification_error => "The object can not be modified in this way",
|
||||||
|
.namespace_error => "The operation is not allowed by Namespaces in XML",
|
||||||
|
.invalid_access_error => "The object does not support the operation or argument",
|
||||||
|
.security_error => "The operation is insecure",
|
||||||
|
.network_error => "A network error occurred",
|
||||||
|
.abort_error => "The operation was aborted",
|
||||||
|
.url_mismatch_error => "The given URL does not match another URL",
|
||||||
|
.quota_exceeded_error => "The quota has been exceeded",
|
||||||
|
.timeout_error => "The operation timed out",
|
||||||
|
.invalid_node_type_error => "The supplied node is incorrect or has an incorrect ancestor for this operation",
|
||||||
|
.data_clone_error => "The object can not be cloned",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -365,6 +365,11 @@ pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@i
|
|||||||
return (try KeyboardEvent.init("", null, page)).asEvent();
|
return (try KeyboardEvent.init("", null, page)).asEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, normalized, "inputevent")) {
|
||||||
|
const InputEvent = @import("event/InputEvent.zig");
|
||||||
|
return (try InputEvent.init("", null, page)).asEvent();
|
||||||
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, normalized, "mouseevent") or std.mem.eql(u8, normalized, "mouseevents")) {
|
if (std.mem.eql(u8, normalized, "mouseevent") or std.mem.eql(u8, normalized, "mouseevents")) {
|
||||||
const MouseEvent = @import("event/MouseEvent.zig");
|
const MouseEvent = @import("event/MouseEvent.zig");
|
||||||
return (try MouseEvent.init("", null, page)).asEvent();
|
return (try MouseEvent.init("", null, page)).asEvent();
|
||||||
|
|||||||
@@ -93,12 +93,12 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void {
|
pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void {
|
||||||
self._callback.release();
|
if (shutdown) {
|
||||||
if ((comptime IS_DEBUG) and !shutdown) {
|
self._callback.release();
|
||||||
std.debug.assert(self._observing.items.len == 0);
|
session.releaseArena(self._arena);
|
||||||
|
} else if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.releaseArena(self._arena);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
|
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
|
||||||
@@ -111,7 +111,6 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
|
|||||||
|
|
||||||
// Register with page if this is our first observation
|
// Register with page if this is our first observation
|
||||||
if (self._observing.items.len == 0) {
|
if (self._observing.items.len == 0) {
|
||||||
page.js.strongRef(self);
|
|
||||||
try page.registerIntersectionObserver(self);
|
try page.registerIntersectionObserver(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,22 +145,18 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self._observing.items.len == 0) {
|
|
||||||
page.js.safeWeakRef(self);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
|
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
|
||||||
page.unregisterIntersectionObserver(self);
|
|
||||||
self._observing.clearRetainingCapacity();
|
|
||||||
self._previous_states.clearRetainingCapacity();
|
self._previous_states.clearRetainingCapacity();
|
||||||
|
|
||||||
for (self._pending_entries.items) |entry| {
|
for (self._pending_entries.items) |entry| {
|
||||||
entry.deinit(false, page._session);
|
entry.deinit(false, page._session);
|
||||||
}
|
}
|
||||||
self._pending_entries.clearRetainingCapacity();
|
self._pending_entries.clearRetainingCapacity();
|
||||||
page.js.safeWeakRef(self);
|
|
||||||
|
self._observing.clearRetainingCapacity();
|
||||||
|
page.unregisterIntersectionObserver(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
|
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
|
||||||
@@ -363,7 +358,6 @@ pub const JsApi = struct {
|
|||||||
pub const name = "IntersectionObserver";
|
pub const name = "IntersectionObserver";
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
pub const weak = true;
|
|
||||||
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
|
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -86,12 +86,12 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void {
|
pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void {
|
||||||
self._callback.release();
|
if (shutdown) {
|
||||||
if ((comptime IS_DEBUG) and !shutdown) {
|
self._callback.release();
|
||||||
std.debug.assert(self._observing.items.len == 0);
|
session.releaseArena(self._arena);
|
||||||
|
} else if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.releaseArena(self._arena);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
|
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
|
||||||
@@ -158,7 +158,6 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
|
|||||||
|
|
||||||
// Register with page if this is our first observation
|
// Register with page if this is our first observation
|
||||||
if (self._observing.items.len == 0) {
|
if (self._observing.items.len == 0) {
|
||||||
page.js.strongRef(self);
|
|
||||||
try page.registerMutationObserver(self);
|
try page.registerMutationObserver(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,13 +168,13 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnect(self: *MutationObserver, page: *Page) void {
|
pub fn disconnect(self: *MutationObserver, page: *Page) void {
|
||||||
page.unregisterMutationObserver(self);
|
|
||||||
self._observing.clearRetainingCapacity();
|
|
||||||
for (self._pending_records.items) |record| {
|
for (self._pending_records.items) |record| {
|
||||||
record.deinit(false, page._session);
|
record.deinit(false, page._session);
|
||||||
}
|
}
|
||||||
self._pending_records.clearRetainingCapacity();
|
self._pending_records.clearRetainingCapacity();
|
||||||
page.js.safeWeakRef(self);
|
|
||||||
|
self._observing.clearRetainingCapacity();
|
||||||
|
page.unregisterMutationObserver(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
|
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
|
||||||
@@ -441,7 +440,6 @@ pub const JsApi = struct {
|
|||||||
pub const name = "MutationObserver";
|
pub const name = "MutationObserver";
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
pub const weak = true;
|
|
||||||
pub const finalizer = bridge.finalizer(MutationObserver.deinit);
|
pub const finalizer = bridge.finalizer(MutationObserver.deinit);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,21 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
|
|
||||||
const PluginArray = @import("PluginArray.zig");
|
const PluginArray = @import("PluginArray.zig");
|
||||||
|
const Permissions = @import("Permissions.zig");
|
||||||
|
const StorageManager = @import("StorageManager.zig");
|
||||||
|
|
||||||
const Navigator = @This();
|
const Navigator = @This();
|
||||||
_pad: bool = false,
|
_pad: bool = false,
|
||||||
_plugins: PluginArray = .{},
|
_plugins: PluginArray = .{},
|
||||||
|
_permissions: Permissions = .{},
|
||||||
|
_storage: StorageManager = .{},
|
||||||
|
|
||||||
pub const init: Navigator = .{};
|
pub const init: Navigator = .{};
|
||||||
|
|
||||||
@@ -55,6 +63,19 @@ pub fn getPlugins(self: *Navigator) *PluginArray {
|
|||||||
return &self._plugins;
|
return &self._plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getPermissions(self: *Navigator) *Permissions {
|
||||||
|
return &self._permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getStorage(self: *Navigator) *StorageManager {
|
||||||
|
return &self._storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise {
|
||||||
|
log.info(.not_implemented, "navigator.getBattery", .{});
|
||||||
|
return page.js.local.?.rejectErrorPromise(.{ .dom_exception = error.NotSupported });
|
||||||
|
}
|
||||||
|
|
||||||
pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
|
pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
|
||||||
try validateProtocolHandlerScheme(scheme);
|
try validateProtocolHandlerScheme(scheme);
|
||||||
try validateProtocolHandlerURL(url, page);
|
try validateProtocolHandlerURL(url, page);
|
||||||
@@ -144,6 +165,7 @@ pub const JsApi = struct {
|
|||||||
pub const onLine = bridge.property(true, .{ .template = false });
|
pub const onLine = bridge.property(true, .{ .template = false });
|
||||||
pub const cookieEnabled = bridge.property(true, .{ .template = false });
|
pub const cookieEnabled = bridge.property(true, .{ .template = false });
|
||||||
pub const hardwareConcurrency = bridge.property(4, .{ .template = false });
|
pub const hardwareConcurrency = bridge.property(4, .{ .template = false });
|
||||||
|
pub const deviceMemory = bridge.property(@as(f64, 8.0), .{ .template = false });
|
||||||
pub const maxTouchPoints = bridge.property(0, .{ .template = false });
|
pub const maxTouchPoints = bridge.property(0, .{ .template = false });
|
||||||
pub const vendor = bridge.property("", .{ .template = false });
|
pub const vendor = bridge.property("", .{ .template = false });
|
||||||
pub const product = bridge.property("Gecko", .{ .template = false });
|
pub const product = bridge.property("Gecko", .{ .template = false });
|
||||||
@@ -156,4 +178,12 @@ pub const JsApi = struct {
|
|||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{});
|
pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{});
|
||||||
|
pub const getBattery = bridge.function(Navigator.getBattery, .{});
|
||||||
|
pub const permissions = bridge.accessor(Navigator.getPermissions, null, .{});
|
||||||
|
pub const storage = bridge.accessor(Navigator.getStorage, null, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "WebApi: Navigator" {
|
||||||
|
try testing.htmlRunner("navigator", .{});
|
||||||
|
}
|
||||||
|
|||||||
94
src/browser/webapi/Permissions.zig
Normal file
94
src/browser/webapi/Permissions.zig
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
const Page = @import("../Page.zig");
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub fn registerTypes() []const type {
|
||||||
|
return &.{ Permissions, PermissionStatus };
|
||||||
|
}
|
||||||
|
|
||||||
|
const Permissions = @This();
|
||||||
|
|
||||||
|
// Padding to avoid zero-size struct pointer collisions
|
||||||
|
_pad: bool = false,
|
||||||
|
|
||||||
|
const QueryDescriptor = struct {
|
||||||
|
name: []const u8,
|
||||||
|
};
|
||||||
|
// We always report 'prompt' (the default safe value — neither granted nor denied).
|
||||||
|
pub fn query(_: *const Permissions, qd: QueryDescriptor, page: *Page) !js.Promise {
|
||||||
|
const arena = try page.getArena(.{ .debug = "PermissionStatus" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
|
||||||
|
const status = try arena.create(PermissionStatus);
|
||||||
|
status.* = .{
|
||||||
|
._arena = arena,
|
||||||
|
._state = "prompt",
|
||||||
|
._name = try arena.dupe(u8, qd.name),
|
||||||
|
};
|
||||||
|
return page.js.local.?.resolvePromise(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PermissionStatus = struct {
|
||||||
|
_arena: Allocator,
|
||||||
|
_name: []const u8,
|
||||||
|
_state: []const u8,
|
||||||
|
|
||||||
|
pub fn deinit(self: *PermissionStatus, _: bool, session: *Session) void {
|
||||||
|
session.releaseArena(self._arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getName(self: *const PermissionStatus) []const u8 {
|
||||||
|
return self._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getState(self: *const PermissionStatus) []const u8 {
|
||||||
|
return self._state;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(PermissionStatus);
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "PermissionStatus";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const weak = true;
|
||||||
|
pub const finalizer = bridge.finalizer(PermissionStatus.deinit);
|
||||||
|
};
|
||||||
|
pub const name = bridge.accessor(getName, null, .{});
|
||||||
|
pub const state = bridge.accessor(getState, null, .{});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(Permissions);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "Permissions";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const empty_with_no_proto = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const query = bridge.function(Permissions.query, .{ .dom_exception = true });
|
||||||
|
};
|
||||||
@@ -40,6 +40,7 @@ _mode: Mode,
|
|||||||
_host: *Element,
|
_host: *Element,
|
||||||
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},
|
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},
|
||||||
_removed_ids: std.StringHashMapUnmanaged(void) = .{},
|
_removed_ids: std.StringHashMapUnmanaged(void) = .{},
|
||||||
|
_adopted_style_sheets: ?js.Object.Global = null,
|
||||||
|
|
||||||
pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot {
|
pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot {
|
||||||
return page._factory.documentFragment(ShadowRoot{
|
return page._factory.documentFragment(ShadowRoot{
|
||||||
@@ -99,6 +100,20 @@ pub fn getElementById(self: *ShadowRoot, id: []const u8, page: *Page) ?*Element
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getAdoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object.Global {
|
||||||
|
if (self._adopted_style_sheets) |ass| {
|
||||||
|
return ass;
|
||||||
|
}
|
||||||
|
const js_arr = page.js.local.?.newArray(0);
|
||||||
|
const js_obj = js_arr.toObject();
|
||||||
|
self._adopted_style_sheets = try js_obj.persist();
|
||||||
|
return self._adopted_style_sheets.?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setAdoptedStyleSheets(self: *ShadowRoot, sheets: js.Object) !void {
|
||||||
|
self._adopted_style_sheets = try sheets.persist();
|
||||||
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(ShadowRoot);
|
pub const bridge = js.Bridge(ShadowRoot);
|
||||||
|
|
||||||
@@ -121,6 +136,7 @@ pub const JsApi = struct {
|
|||||||
}
|
}
|
||||||
return self.getElementById(try value.toZig([]const u8), page);
|
return self.getElementById(try value.toZig([]const u8), page);
|
||||||
}
|
}
|
||||||
|
pub const adoptedStyleSheets = bridge.accessor(ShadowRoot.getAdoptedStyleSheets, ShadowRoot.setAdoptedStyleSheets, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
|
|||||||
71
src/browser/webapi/StorageManager.zig
Normal file
71
src/browser/webapi/StorageManager.zig
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// 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/js.zig");
|
||||||
|
const Page = @import("../Page.zig");
|
||||||
|
|
||||||
|
pub fn registerTypes() []const type {
|
||||||
|
return &.{ StorageManager, StorageEstimate };
|
||||||
|
}
|
||||||
|
|
||||||
|
const StorageManager = @This();
|
||||||
|
|
||||||
|
_pad: bool = false,
|
||||||
|
|
||||||
|
pub fn estimate(_: *const StorageManager, page: *Page) !js.Promise {
|
||||||
|
const est = try page._factory.create(StorageEstimate{
|
||||||
|
._usage = 0,
|
||||||
|
._quota = 1024 * 1024 * 1024, // 1 GiB
|
||||||
|
});
|
||||||
|
return page.js.local.?.resolvePromise(est);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StorageEstimate = struct {
|
||||||
|
_quota: u64,
|
||||||
|
_usage: u64,
|
||||||
|
|
||||||
|
fn getUsage(self: *const StorageEstimate) u64 {
|
||||||
|
return self._usage;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getQuota(self: *const StorageEstimate) u64 {
|
||||||
|
return self._quota;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(StorageEstimate);
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "StorageEstimate";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
pub const quota = bridge.accessor(getQuota, null, .{});
|
||||||
|
pub const usage = bridge.accessor(getUsage, null, .{});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(StorageManager);
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "StorageManager";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const empty_with_no_proto = true;
|
||||||
|
};
|
||||||
|
pub const estimate = bridge.function(StorageManager.estimate, .{});
|
||||||
|
};
|
||||||
@@ -66,7 +66,10 @@ _on_load: ?js.Function.Global = null,
|
|||||||
_on_pageshow: ?js.Function.Global = null,
|
_on_pageshow: ?js.Function.Global = null,
|
||||||
_on_popstate: ?js.Function.Global = null,
|
_on_popstate: ?js.Function.Global = null,
|
||||||
_on_error: ?js.Function.Global = null,
|
_on_error: ?js.Function.Global = null,
|
||||||
_on_unhandled_rejection: ?js.Function.Global = null, // TODO: invoke on error
|
_on_message: ?js.Function.Global = null,
|
||||||
|
_on_rejection_handled: ?js.Function.Global = null,
|
||||||
|
_on_unhandled_rejection: ?js.Function.Global = null,
|
||||||
|
_current_event: ?*Event = null,
|
||||||
_location: *Location,
|
_location: *Location,
|
||||||
_timer_id: u30 = 0,
|
_timer_id: u30 = 0,
|
||||||
_timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},
|
_timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},
|
||||||
@@ -89,6 +92,10 @@ pub fn asEventTarget(self: *Window) *EventTarget {
|
|||||||
return self._proto;
|
return self._proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getEvent(self: *const Window) ?*Event {
|
||||||
|
return self._current_event;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getSelf(self: *Window) *Window {
|
pub fn getSelf(self: *Window) *Window {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -208,6 +215,22 @@ pub fn setOnError(self: *Window, setter: ?FunctionSetter) void {
|
|||||||
self._on_error = getFunctionFromSetter(setter);
|
self._on_error = getFunctionFromSetter(setter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getOnMessage(self: *const Window) ?js.Function.Global {
|
||||||
|
return self._on_message;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnMessage(self: *Window, setter: ?FunctionSetter) void {
|
||||||
|
self._on_message = getFunctionFromSetter(setter);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnRejectionHandled(self: *const Window) ?js.Function.Global {
|
||||||
|
return self._on_rejection_handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnRejectionHandled(self: *Window, setter: ?FunctionSetter) void {
|
||||||
|
self._on_rejection_handled = getFunctionFromSetter(setter);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getOnUnhandledRejection(self: *const Window) ?js.Function.Global {
|
pub fn getOnUnhandledRejection(self: *const Window) ?js.Function.Global {
|
||||||
return self._on_unhandled_rejection;
|
return self._on_unhandled_rejection;
|
||||||
}
|
}
|
||||||
@@ -334,7 +357,11 @@ pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
|
|||||||
|
|
||||||
const event = error_event.asEvent();
|
const event = error_event.asEvent();
|
||||||
event._prevent_default = prevent_default;
|
event._prevent_default = prevent_default;
|
||||||
try page._event_manager.dispatch(self.asEventTarget(), event);
|
// Pass null as handler: onerror was already called above with 5 args.
|
||||||
|
// We still dispatch so that addEventListener('error', ...) listeners fire.
|
||||||
|
try page._event_manager.dispatchDirect(self.asEventTarget(), event, null, .{
|
||||||
|
.context = "window.reportError",
|
||||||
|
});
|
||||||
|
|
||||||
if (comptime builtin.is_test == false) {
|
if (comptime builtin.is_test == false) {
|
||||||
if (!event._prevent_default) {
|
if (!event._prevent_default) {
|
||||||
@@ -369,19 +396,26 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons
|
|||||||
// In a full implementation, we would validate the origin
|
// In a full implementation, we would validate the origin
|
||||||
_ = target_origin;
|
_ = target_origin;
|
||||||
|
|
||||||
// postMessage queues a task (not a microtask), so use the scheduler
|
// self = the window that will get the message
|
||||||
const arena = try page.getArena(.{ .debug = "Window.schedule" });
|
// page = the context calling postMessage
|
||||||
errdefer page.releaseArena(arena);
|
const target_page = self._page;
|
||||||
|
const source_window = target_page.js.getIncumbent().window;
|
||||||
|
|
||||||
const origin = try self._location.getOrigin(page);
|
const arena = try target_page.getArena(.{ .debug = "Window.postMessage" });
|
||||||
|
errdefer target_page.releaseArena(arena);
|
||||||
|
|
||||||
|
// Origin should be the source window's origin (where the message came from)
|
||||||
|
const origin = try source_window._location.getOrigin(page);
|
||||||
const callback = try arena.create(PostMessageCallback);
|
const callback = try arena.create(PostMessageCallback);
|
||||||
callback.* = .{
|
callback.* = .{
|
||||||
.page = page,
|
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
.message = message,
|
.message = message,
|
||||||
|
.page = target_page,
|
||||||
|
.source = source_window,
|
||||||
.origin = try arena.dupe(u8, origin),
|
.origin = try arena.dupe(u8, origin),
|
||||||
};
|
};
|
||||||
try page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
|
||||||
|
try target_page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
||||||
.name = "postMessage",
|
.name = "postMessage",
|
||||||
.low_priority = false,
|
.low_priority = false,
|
||||||
.finalizer = PostMessageCallback.cancelled,
|
.finalizer = PostMessageCallback.cancelled,
|
||||||
@@ -547,7 +581,7 @@ pub fn scrollBy(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
|
|||||||
return self.scrollTo(.{ .x = absx }, absy, page);
|
return self.scrollTo(.{ .x = absx }, absy, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection, page: *Page) !void {
|
pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.PromiseRejection, page: *Page) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.js, "unhandled rejection", .{
|
log.debug(.js, "unhandled rejection", .{
|
||||||
.value = rejection.reason(),
|
.value = rejection.reason(),
|
||||||
@@ -555,13 +589,20 @@ pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const event_name, const attribute_callback = blk: {
|
||||||
|
if (no_handler) {
|
||||||
|
break :blk .{ "unhandledrejection", self._on_unhandled_rejection };
|
||||||
|
}
|
||||||
|
break :blk .{ "rejectionhandled", self._on_rejection_handled };
|
||||||
|
};
|
||||||
|
|
||||||
const target = self.asEventTarget();
|
const target = self.asEventTarget();
|
||||||
if (page._event_manager.hasDirectListeners(target, "unhandledrejection", self._on_unhandled_rejection)) {
|
if (page._event_manager.hasDirectListeners(target, event_name, attribute_callback)) {
|
||||||
const event = (try @import("event/PromiseRejectionEvent.zig").init("unhandledrejection", .{
|
const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{
|
||||||
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
||||||
.promise = try rejection.promise().temp(),
|
.promise = try rejection.promise().temp(),
|
||||||
}, page)).asEvent();
|
}, page)).asEvent();
|
||||||
try page._event_manager.dispatchDirect(target, event, self._on_unhandled_rejection, .{ .context = "window.unhandledrejection" });
|
try page._event_manager.dispatchDirect(target, event, attribute_callback, .{ .context = "window.unhandledrejection" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,6 +743,7 @@ const ScheduleCallback = struct {
|
|||||||
|
|
||||||
const PostMessageCallback = struct {
|
const PostMessageCallback = struct {
|
||||||
page: *Page,
|
page: *Page,
|
||||||
|
source: *Window,
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
origin: []const u8,
|
origin: []const u8,
|
||||||
message: js.Value.Temp,
|
message: js.Value.Temp,
|
||||||
@@ -712,7 +754,7 @@ const PostMessageCallback = struct {
|
|||||||
|
|
||||||
fn cancelled(ctx: *anyopaque) void {
|
fn cancelled(ctx: *anyopaque) void {
|
||||||
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
|
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
|
||||||
self.page.releaseArena(self.arena);
|
self.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(ctx: *anyopaque) !?u32 {
|
fn run(ctx: *anyopaque) !?u32 {
|
||||||
@@ -722,14 +764,17 @@ const PostMessageCallback = struct {
|
|||||||
const page = self.page;
|
const page = self.page;
|
||||||
const window = page.window;
|
const window = page.window;
|
||||||
|
|
||||||
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
|
const event_target = window.asEventTarget();
|
||||||
.data = self.message,
|
if (page._event_manager.hasDirectListeners(event_target, "message", window._on_message)) {
|
||||||
.origin = self.origin,
|
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
|
||||||
.source = window,
|
.data = self.message,
|
||||||
.bubbles = false,
|
.origin = self.origin,
|
||||||
.cancelable = false,
|
.source = self.source,
|
||||||
}, page)).asEvent();
|
.bubbles = false,
|
||||||
try page._event_manager.dispatch(window.asEventTarget(), event);
|
.cancelable = false,
|
||||||
|
}, page)).asEvent();
|
||||||
|
try page._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" });
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -783,7 +828,10 @@ pub const JsApi = struct {
|
|||||||
pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{});
|
pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{});
|
||||||
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
|
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
|
||||||
pub const onerror = bridge.accessor(Window.getOnError, Window.setOnError, .{});
|
pub const onerror = bridge.accessor(Window.getOnError, Window.setOnError, .{});
|
||||||
|
pub const onmessage = bridge.accessor(Window.getOnMessage, Window.setOnMessage, .{});
|
||||||
|
pub const onrejectionhandled = bridge.accessor(Window.getOnRejectionHandled, Window.setOnRejectionHandled, .{});
|
||||||
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
|
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
|
||||||
|
pub const event = bridge.accessor(Window.getEvent, null, .{ .null_as_undefined = true });
|
||||||
pub const fetch = bridge.function(Window.fetch, .{});
|
pub const fetch = bridge.function(Window.fetch, .{});
|
||||||
pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});
|
pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});
|
||||||
pub const setTimeout = bridge.function(Window.setTimeout, .{});
|
pub const setTimeout = bridge.function(Window.setTimeout, .{});
|
||||||
@@ -853,3 +901,7 @@ test "WebApi: Window" {
|
|||||||
test "WebApi: Window scroll" {
|
test "WebApi: Window scroll" {
|
||||||
try testing.htmlRunner("window_scroll.html", .{});
|
try testing.htmlRunner("window_scroll.html", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "WebApi: Window.onerror" {
|
||||||
|
try testing.htmlRunner("event/report_error.html", .{});
|
||||||
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ pub const JsApi = struct {
|
|||||||
|
|
||||||
const testing = @import("../../../testing.zig");
|
const testing = @import("../../../testing.zig");
|
||||||
test "WebApi: CSSStyleSheet" {
|
test "WebApi: CSSStyleSheet" {
|
||||||
const filter: testing.LogFilter = .init(.js);
|
const filter: testing.LogFilter = .init(&.{.js});
|
||||||
defer filter.deinit();
|
defer filter.deinit();
|
||||||
try testing.htmlRunner("css/stylesheet.html", .{});
|
try testing.htmlRunner("css/stylesheet.html", .{});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,47 @@ pub fn submit(self: *Form, page: *Page) !void {
|
|||||||
return page.submitForm(null, self, .{ .fire_event = false });
|
return page.submitForm(null, self, .{ .fire_event = false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// https://html.spec.whatwg.org/multipage/forms.html#dom-form-requestsubmit
|
||||||
|
/// Like submit(), but fires the submit event and validates the form.
|
||||||
|
pub fn requestSubmit(self: *Form, submitter: ?*Element, page: *Page) !void {
|
||||||
|
const submitter_element = if (submitter) |s| blk: {
|
||||||
|
// The submitter must be a submit button.
|
||||||
|
if (!isSubmitButton(s)) return error.TypeError;
|
||||||
|
|
||||||
|
// The submitter's form owner must be this form element.
|
||||||
|
const submitter_form = getFormOwner(s, page);
|
||||||
|
if (submitter_form == null or submitter_form.? != self) return error.NotFound;
|
||||||
|
|
||||||
|
break :blk s;
|
||||||
|
} else self.asElement();
|
||||||
|
|
||||||
|
return page.submitForm(submitter_element, self, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the element is a submit button per the HTML spec:
|
||||||
|
/// - <input type="submit"> or <input type="image">
|
||||||
|
/// - <button type="submit"> (including default, since button's default type is "submit")
|
||||||
|
fn isSubmitButton(element: *Element) bool {
|
||||||
|
if (element.is(Input)) |input| {
|
||||||
|
return input._input_type == .submit or input._input_type == .image;
|
||||||
|
}
|
||||||
|
if (element.is(Button)) |button| {
|
||||||
|
return std.mem.eql(u8, button.getType(), "submit");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the form owner of a submittable element (Input or Button).
|
||||||
|
fn getFormOwner(element: *Element, page: *Page) ?*Form {
|
||||||
|
if (element.is(Input)) |input| {
|
||||||
|
return input.getForm(page);
|
||||||
|
}
|
||||||
|
if (element.is(Button)) |button| {
|
||||||
|
return button.getForm(page);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(Form);
|
pub const bridge = js.Bridge(Form);
|
||||||
pub const Meta = struct {
|
pub const Meta = struct {
|
||||||
@@ -132,6 +173,7 @@ pub const JsApi = struct {
|
|||||||
pub const elements = bridge.accessor(Form.getElements, null, .{});
|
pub const elements = bridge.accessor(Form.getElements, null, .{});
|
||||||
pub const length = bridge.accessor(Form.getLength, null, .{});
|
pub const length = bridge.accessor(Form.getLength, null, .{});
|
||||||
pub const submit = bridge.function(Form.submit, .{});
|
pub const submit = bridge.function(Form.submit, .{});
|
||||||
|
pub const requestSubmit = bridge.function(Form.requestSubmit, .{ .dom_exception = true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../../../testing.zig");
|
const testing = @import("../../../../testing.zig");
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const HtmlElement = @import("../Html.zig");
|
|||||||
const Form = @import("Form.zig");
|
const Form = @import("Form.zig");
|
||||||
const Selection = @import("../../Selection.zig");
|
const Selection = @import("../../Selection.zig");
|
||||||
const Event = @import("../../Event.zig");
|
const Event = @import("../../Event.zig");
|
||||||
|
const InputEvent = @import("../../event/InputEvent.zig");
|
||||||
|
|
||||||
const Input = @This();
|
const Input = @This();
|
||||||
|
|
||||||
@@ -103,6 +104,11 @@ fn dispatchSelectionChangeEvent(self: *Input, page: *Page) !void {
|
|||||||
try page._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
try page._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dispatchInputEvent(self: *Input, data: ?[]const u8, input_type: []const u8, page: *Page) !void {
|
||||||
|
const event = try InputEvent.initTrusted(comptime .wrap("input"), .{ .data = data, .inputType = input_type }, page);
|
||||||
|
try page._event_manager.dispatch(self.asElement().asEventTarget(), event.asEvent());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn asElement(self: *Input) *Element {
|
pub fn asElement(self: *Input) *Element {
|
||||||
return self._proto._proto;
|
return self._proto._proto;
|
||||||
}
|
}
|
||||||
@@ -425,6 +431,7 @@ pub fn innerInsert(self: *Input, str: []const u8, page: *Page) !void {
|
|||||||
try self.setValue(new_value, page);
|
try self.setValue(new_value, page);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
try self.dispatchInputEvent(str, "insertText", page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getSelectionDirection(self: *const Input) []const u8 {
|
pub fn getSelectionDirection(self: *const Input) []const u8 {
|
||||||
|
|||||||
@@ -93,13 +93,21 @@ pub fn linkAddedCallback(self: *Link, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const element = self.asElement();
|
const element = self.asElement();
|
||||||
// Exit if rel not set.
|
|
||||||
const rel = element.getAttributeSafe(comptime .wrap("rel")) orelse return;
|
const rel = element.getAttributeSafe(comptime .wrap("rel")) orelse return;
|
||||||
// Exit if rel is not stylesheet.
|
const loadable_rels = std.StaticStringMap(void).initComptime(.{
|
||||||
if (!std.mem.eql(u8, rel, "stylesheet")) return;
|
.{ "stylesheet", {} },
|
||||||
// Exit if href not set.
|
.{ "preload", {} },
|
||||||
|
.{ "modulepreload", {} },
|
||||||
|
});
|
||||||
|
if (loadable_rels.has(rel) == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const href = element.getAttributeSafe(comptime .wrap("href")) orelse return;
|
const href = element.getAttributeSafe(comptime .wrap("href")) orelse return;
|
||||||
if (href.len == 0) return;
|
if (href.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try page._to_load.append(page.arena, self._proto);
|
try page._to_load.append(page.arena, self._proto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const HtmlElement = @import("../Html.zig");
|
|||||||
const Form = @import("Form.zig");
|
const Form = @import("Form.zig");
|
||||||
const Selection = @import("../../Selection.zig");
|
const Selection = @import("../../Selection.zig");
|
||||||
const Event = @import("../../Event.zig");
|
const Event = @import("../../Event.zig");
|
||||||
|
const InputEvent = @import("../../event/InputEvent.zig");
|
||||||
|
|
||||||
const TextArea = @This();
|
const TextArea = @This();
|
||||||
|
|
||||||
@@ -55,6 +56,11 @@ fn dispatchSelectionChangeEvent(self: *TextArea, page: *Page) !void {
|
|||||||
try page._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
try page._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dispatchInputEvent(self: *TextArea, data: ?[]const u8, input_type: []const u8, page: *Page) !void {
|
||||||
|
const event = try InputEvent.initTrusted(comptime .wrap("input"), .{ .data = data, .inputType = input_type }, page);
|
||||||
|
try page._event_manager.dispatch(self.asElement().asEventTarget(), event.asEvent());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn asElement(self: *TextArea) *Element {
|
pub fn asElement(self: *TextArea) *Element {
|
||||||
return self._proto._proto;
|
return self._proto._proto;
|
||||||
}
|
}
|
||||||
@@ -189,6 +195,7 @@ pub fn innerInsert(self: *TextArea, str: []const u8, page: *Page) !void {
|
|||||||
try self.setValue(new_value, page);
|
try self.setValue(new_value, page);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
try self.dispatchInputEvent(str, "insertText", page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getSelectionDirection(self: *const TextArea) []const u8 {
|
pub fn getSelectionDirection(self: *const TextArea) []const u8 {
|
||||||
|
|||||||
121
src/browser/webapi/event/InputEvent.zig
Normal file
121
src/browser/webapi/event/InputEvent.zig
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// 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 String = @import("../../../string.zig").String;
|
||||||
|
const Page = @import("../../Page.zig");
|
||||||
|
const Session = @import("../../Session.zig");
|
||||||
|
const js = @import("../../js/js.zig");
|
||||||
|
|
||||||
|
const Event = @import("../Event.zig");
|
||||||
|
const UIEvent = @import("UIEvent.zig");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const InputEvent = @This();
|
||||||
|
|
||||||
|
_proto: *UIEvent,
|
||||||
|
_data: ?[]const u8,
|
||||||
|
// TODO: add dataTransfer
|
||||||
|
_input_type: []const u8,
|
||||||
|
_is_composing: bool,
|
||||||
|
|
||||||
|
pub const InputEventOptions = struct {
|
||||||
|
data: ?[]const u8 = null,
|
||||||
|
inputType: ?[]const u8 = null,
|
||||||
|
isComposing: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Options = Event.inheritOptions(
|
||||||
|
InputEvent,
|
||||||
|
InputEventOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*InputEvent {
|
||||||
|
const arena = try page.getArena(.{ .debug = "InputEvent.trusted" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
return initWithTrusted(arena, typ, _opts, true, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*InputEvent {
|
||||||
|
const arena = try page.getArena(.{ .debug = "InputEvent" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
const type_string = try String.init(arena, typ, .{});
|
||||||
|
return initWithTrusted(arena, type_string, _opts, false, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*InputEvent {
|
||||||
|
const opts = _opts orelse Options{};
|
||||||
|
|
||||||
|
const event = try page._factory.uiEvent(
|
||||||
|
arena,
|
||||||
|
typ,
|
||||||
|
InputEvent{
|
||||||
|
._proto = undefined,
|
||||||
|
._data = if (opts.data) |d| try arena.dupe(u8, d) else null,
|
||||||
|
._input_type = if (opts.inputType) |it| try arena.dupe(u8, it) else "",
|
||||||
|
._is_composing = opts.isComposing,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Event.populatePrototypes(event, opts, trusted);
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event
|
||||||
|
const rootevt = event._proto._proto;
|
||||||
|
rootevt._bubbles = true;
|
||||||
|
rootevt._cancelable = false;
|
||||||
|
rootevt._composed = true;
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *InputEvent, shutdown: bool, session: *Session) void {
|
||||||
|
self._proto.deinit(shutdown, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asEvent(self: *InputEvent) *Event {
|
||||||
|
return self._proto.asEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getData(self: *const InputEvent) ?[]const u8 {
|
||||||
|
return self._data;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getInputType(self: *const InputEvent) []const u8 {
|
||||||
|
return self._input_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getIsComposing(self: *const InputEvent) bool {
|
||||||
|
return self._is_composing;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(InputEvent);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "InputEvent";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const weak = true;
|
||||||
|
pub const finalizer = bridge.finalizer(InputEvent.deinit);
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const constructor = bridge.constructor(InputEvent.init, .{});
|
||||||
|
pub const data = bridge.accessor(InputEvent.getData, null, .{});
|
||||||
|
pub const inputType = bridge.accessor(InputEvent.getInputType, null, .{});
|
||||||
|
pub const isComposing = bridge.accessor(InputEvent.getIsComposing, null, .{});
|
||||||
|
};
|
||||||
@@ -219,6 +219,13 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
|
|||||||
);
|
);
|
||||||
|
|
||||||
Event.populatePrototypes(event, opts, trusted);
|
Event.populatePrototypes(event, opts, trusted);
|
||||||
|
|
||||||
|
// https://w3c.github.io/uievents/#event-type-keyup
|
||||||
|
const rootevt = event._proto._proto;
|
||||||
|
rootevt._bubbles = true;
|
||||||
|
rootevt._cancelable = true;
|
||||||
|
rootevt._composed = true;
|
||||||
|
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const EventTarget = @import("../EventTarget.zig");
|
|||||||
const UIEvent = @import("UIEvent.zig");
|
const UIEvent = @import("UIEvent.zig");
|
||||||
const PointerEvent = @import("PointerEvent.zig");
|
const PointerEvent = @import("PointerEvent.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const MouseEvent = @This();
|
const MouseEvent = @This();
|
||||||
|
|
||||||
pub const MouseButton = enum(u8) {
|
pub const MouseButton = enum(u8) {
|
||||||
@@ -83,12 +85,21 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent {
|
|||||||
const arena = try page.getArena(.{ .debug = "MouseEvent" });
|
const arena = try page.getArena(.{ .debug = "MouseEvent" });
|
||||||
errdefer page.releaseArena(arena);
|
errdefer page.releaseArena(arena);
|
||||||
const type_string = try String.init(arena, typ, .{});
|
const type_string = try String.init(arena, typ, .{});
|
||||||
|
return initWithTrusted(arena, type_string, _opts, false, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*MouseEvent {
|
||||||
|
const arena = try page.getArena(.{ .debug = "MouseEvent.trusted" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
return initWithTrusted(arena, typ, _opts, true, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*MouseEvent {
|
||||||
const opts = _opts orelse Options{};
|
const opts = _opts orelse Options{};
|
||||||
|
|
||||||
const event = try page._factory.uiEvent(
|
const event = try page._factory.uiEvent(
|
||||||
arena,
|
arena,
|
||||||
type_string,
|
typ,
|
||||||
MouseEvent{
|
MouseEvent{
|
||||||
._type = .generic,
|
._type = .generic,
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
@@ -106,7 +117,7 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Event.populatePrototypes(event, opts, false);
|
Event.populatePrototypes(event, opts, trusted);
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub const Type = union(enum) {
|
|||||||
keyboard_event: *@import("KeyboardEvent.zig"),
|
keyboard_event: *@import("KeyboardEvent.zig"),
|
||||||
focus_event: *@import("FocusEvent.zig"),
|
focus_event: *@import("FocusEvent.zig"),
|
||||||
text_event: *@import("TextEvent.zig"),
|
text_event: *@import("TextEvent.zig"),
|
||||||
|
input_event: *@import("InputEvent.zig"),
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const UIEventOptions = struct {
|
pub const UIEventOptions = struct {
|
||||||
@@ -88,6 +89,7 @@ pub fn is(self: *UIEvent, comptime T: type) ?*T {
|
|||||||
.keyboard_event => |e| return if (T == @import("KeyboardEvent.zig")) e else null,
|
.keyboard_event => |e| return if (T == @import("KeyboardEvent.zig")) e else null,
|
||||||
.focus_event => |e| return if (T == @import("FocusEvent.zig")) e else null,
|
.focus_event => |e| return if (T == @import("FocusEvent.zig")) e else null,
|
||||||
.text_event => |e| return if (T == @import("TextEvent.zig")) e else null,
|
.text_event => |e| return if (T == @import("TextEvent.zig")) e else null,
|
||||||
|
.input_event => |e| return if (T == @import("InputEvent.zig")) e else null,
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,11 @@ const js = @import("../../js/js.zig");
|
|||||||
const Page = @import("../../Page.zig");
|
const Page = @import("../../Page.zig");
|
||||||
const URL = @import("../../URL.zig");
|
const URL = @import("../../URL.zig");
|
||||||
|
|
||||||
|
const Blob = @import("../Blob.zig");
|
||||||
const Request = @import("Request.zig");
|
const Request = @import("Request.zig");
|
||||||
const Response = @import("Response.zig");
|
const Response = @import("Response.zig");
|
||||||
|
const AbortSignal = @import("../AbortSignal.zig");
|
||||||
|
const DOMException = @import("../DOMException.zig");
|
||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
@@ -38,17 +41,29 @@ _buf: std.ArrayList(u8),
|
|||||||
_response: *Response,
|
_response: *Response,
|
||||||
_resolver: js.PromiseResolver.Global,
|
_resolver: js.PromiseResolver.Global,
|
||||||
_owns_response: bool,
|
_owns_response: bool,
|
||||||
|
_signal: ?*AbortSignal,
|
||||||
|
|
||||||
pub const Input = Request.Input;
|
pub const Input = Request.Input;
|
||||||
pub const InitOpts = Request.InitOpts;
|
pub const InitOpts = Request.InitOpts;
|
||||||
|
|
||||||
pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
|
pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
|
||||||
const request = try Request.init(input, options, page);
|
const request = try Request.init(input, options, page);
|
||||||
|
const resolver = page.js.local.?.createPromiseResolver();
|
||||||
|
|
||||||
|
if (request._signal) |signal| {
|
||||||
|
if (signal._aborted) {
|
||||||
|
resolver.reject("fetch aborted", DOMException.init("The operation was aborted.", "AbortError"));
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, request._url, "blob:")) {
|
||||||
|
return handleBlobUrl(request._url, resolver, page);
|
||||||
|
}
|
||||||
|
|
||||||
const response = try Response.init(null, .{ .status = 0 }, page);
|
const response = try Response.init(null, .{ .status = 0 }, page);
|
||||||
errdefer response.deinit(true, page._session);
|
errdefer response.deinit(true, page._session);
|
||||||
|
|
||||||
const resolver = page.js.local.?.createPromiseResolver();
|
|
||||||
|
|
||||||
const fetch = try response._arena.create(Fetch);
|
const fetch = try response._arena.create(Fetch);
|
||||||
fetch.* = .{
|
fetch.* = .{
|
||||||
._page = page,
|
._page = page,
|
||||||
@@ -57,6 +72,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
|
|||||||
._resolver = try resolver.persist(),
|
._resolver = try resolver.persist(),
|
||||||
._response = response,
|
._response = response,
|
||||||
._owns_response = true,
|
._owns_response = true,
|
||||||
|
._signal = request._signal,
|
||||||
};
|
};
|
||||||
|
|
||||||
const http_client = page._session.browser.http_client;
|
const http_client = page._session.browser.http_client;
|
||||||
@@ -90,6 +106,26 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
|
|||||||
return resolver.promise();
|
return resolver.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handleBlobUrl(url: []const u8, resolver: js.PromiseResolver, page: *Page) !js.Promise {
|
||||||
|
const blob: *Blob = page.lookupBlobUrl(url) orelse {
|
||||||
|
resolver.rejectError("fetch blob error", .{ .type_error = "BlobNotFound" });
|
||||||
|
return resolver.promise();
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = try Response.init(null, .{ .status = 200 }, page);
|
||||||
|
response._body = try response._arena.dupe(u8, blob._slice);
|
||||||
|
response._url = try response._arena.dupeZ(u8, url);
|
||||||
|
response._type = .basic;
|
||||||
|
|
||||||
|
if (blob._mime.len > 0) {
|
||||||
|
try response._headers.append("Content-Type", blob._mime, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const js_val = try page.js.local.?.zigValueToJs(response, .{});
|
||||||
|
resolver.resolve("fetch blob done", js_val);
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
|
fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
|
||||||
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
|
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -101,6 +137,12 @@ fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
|
|||||||
fn httpHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
|
fn httpHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
|
||||||
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
|
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
|
||||||
|
if (self._signal) |signal| {
|
||||||
|
if (signal._aborted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const arena = self._response._arena;
|
const arena = self._response._arena;
|
||||||
if (transfer.getContentLength()) |cl| {
|
if (transfer.getContentLength()) |cl| {
|
||||||
try self._buf.ensureTotalCapacity(arena, cl);
|
try self._buf.ensureTotalCapacity(arena, cl);
|
||||||
@@ -150,6 +192,14 @@ fn httpHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
|
|||||||
|
|
||||||
fn httpDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
fn httpDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
|
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
|
||||||
|
// Check if aborted
|
||||||
|
if (self._signal) |signal| {
|
||||||
|
if (signal._aborted) {
|
||||||
|
return error.Abort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try self._buf.appendSlice(self._response._arena, data);
|
try self._buf.appendSlice(self._response._arena, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const URL = @import("../URL.zig");
|
|||||||
const Page = @import("../../Page.zig");
|
const Page = @import("../../Page.zig");
|
||||||
const Headers = @import("Headers.zig");
|
const Headers = @import("Headers.zig");
|
||||||
const Blob = @import("../Blob.zig");
|
const Blob = @import("../Blob.zig");
|
||||||
|
const AbortSignal = @import("../AbortSignal.zig");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const Request = @This();
|
const Request = @This();
|
||||||
@@ -36,6 +37,7 @@ _body: ?[]const u8,
|
|||||||
_arena: Allocator,
|
_arena: Allocator,
|
||||||
_cache: Cache,
|
_cache: Cache,
|
||||||
_credentials: Credentials,
|
_credentials: Credentials,
|
||||||
|
_signal: ?*AbortSignal,
|
||||||
|
|
||||||
pub const Input = union(enum) {
|
pub const Input = union(enum) {
|
||||||
request: *Request,
|
request: *Request,
|
||||||
@@ -48,6 +50,7 @@ pub const InitOpts = struct {
|
|||||||
body: ?[]const u8 = null,
|
body: ?[]const u8 = null,
|
||||||
cache: Cache = .default,
|
cache: Cache = .default,
|
||||||
credentials: Credentials = .@"same-origin",
|
credentials: Credentials = .@"same-origin",
|
||||||
|
signal: ?*AbortSignal = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Credentials = enum {
|
const Credentials = enum {
|
||||||
@@ -97,6 +100,13 @@ pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request {
|
|||||||
.request => |r| r._body,
|
.request => |r| r._body,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const signal = if (opts.signal) |s|
|
||||||
|
s
|
||||||
|
else switch (input) {
|
||||||
|
.url => null,
|
||||||
|
.request => |r| r._signal,
|
||||||
|
};
|
||||||
|
|
||||||
return page._factory.create(Request{
|
return page._factory.create(Request{
|
||||||
._url = url,
|
._url = url,
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
@@ -105,6 +115,7 @@ pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request {
|
|||||||
._cache = opts.cache,
|
._cache = opts.cache,
|
||||||
._credentials = opts.credentials,
|
._credentials = opts.credentials,
|
||||||
._body = body,
|
._body = body,
|
||||||
|
._signal = signal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +155,10 @@ pub fn getCredentials(self: *const Request) []const u8 {
|
|||||||
return @tagName(self._credentials);
|
return @tagName(self._credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getSignal(self: *const Request) ?*AbortSignal {
|
||||||
|
return self._signal;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getHeaders(self: *Request, page: *Page) !*Headers {
|
pub fn getHeaders(self: *Request, page: *Page) !*Headers {
|
||||||
if (self._headers) |headers| {
|
if (self._headers) |headers| {
|
||||||
return headers;
|
return headers;
|
||||||
@@ -200,6 +215,7 @@ pub fn clone(self: *const Request, page: *Page) !*Request {
|
|||||||
._cache = self._cache,
|
._cache = self._cache,
|
||||||
._credentials = self._credentials,
|
._credentials = self._credentials,
|
||||||
._body = self._body,
|
._body = self._body,
|
||||||
|
._signal = self._signal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +234,7 @@ pub const JsApi = struct {
|
|||||||
pub const headers = bridge.accessor(Request.getHeaders, null, .{});
|
pub const headers = bridge.accessor(Request.getHeaders, null, .{});
|
||||||
pub const cache = bridge.accessor(Request.getCache, null, .{});
|
pub const cache = bridge.accessor(Request.getCache, null, .{});
|
||||||
pub const credentials = bridge.accessor(Request.getCredentials, null, .{});
|
pub const credentials = bridge.accessor(Request.getCredentials, null, .{});
|
||||||
|
pub const signal = bridge.accessor(Request.getSignal, null, .{});
|
||||||
pub const blob = bridge.function(Request.blob, .{});
|
pub const blob = bridge.function(Request.blob, .{});
|
||||||
pub const text = bridge.function(Request.text, .{});
|
pub const text = bridge.function(Request.text, .{});
|
||||||
pub const json = bridge.function(Request.json, .{});
|
pub const json = bridge.function(Request.json, .{});
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const Page = @import("../../Page.zig");
|
|||||||
const Session = @import("../../Session.zig");
|
const Session = @import("../../Session.zig");
|
||||||
|
|
||||||
const Node = @import("../Node.zig");
|
const Node = @import("../Node.zig");
|
||||||
|
const Blob = @import("../Blob.zig");
|
||||||
const Event = @import("../Event.zig");
|
const Event = @import("../Event.zig");
|
||||||
const Headers = @import("Headers.zig");
|
const Headers = @import("Headers.zig");
|
||||||
const EventTarget = @import("../EventTarget.zig");
|
const EventTarget = @import("../EventTarget.zig");
|
||||||
@@ -211,6 +212,11 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const page = self._page;
|
const page = self._page;
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, self._url, "blob:")) {
|
||||||
|
return self.handleBlobUrl(page);
|
||||||
|
}
|
||||||
|
|
||||||
const http_client = page._session.browser.http_client;
|
const http_client = page._session.browser.http_client;
|
||||||
var headers = try http_client.newHeaders();
|
var headers = try http_client.newHeaders();
|
||||||
|
|
||||||
@@ -242,6 +248,39 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
|
|||||||
|
|
||||||
page.js.strongRef(self);
|
page.js.strongRef(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handleBlobUrl(self: *XMLHttpRequest, page: *Page) !void {
|
||||||
|
const blob = page.lookupBlobUrl(self._url) orelse {
|
||||||
|
self.handleError(error.BlobNotFound);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self._response_status = 200;
|
||||||
|
self._response_url = self._url;
|
||||||
|
|
||||||
|
try self._response_data.appendSlice(self._arena, blob._slice);
|
||||||
|
self._response_len = blob._slice.len;
|
||||||
|
|
||||||
|
try self.stateChanged(.headers_received, page);
|
||||||
|
try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, page);
|
||||||
|
try self.stateChanged(.loading, page);
|
||||||
|
try self._proto.dispatch(.progress, .{
|
||||||
|
.total = self._response_len orelse 0,
|
||||||
|
.loaded = self._response_data.items.len,
|
||||||
|
}, page);
|
||||||
|
try self.stateChanged(.done, page);
|
||||||
|
|
||||||
|
const loaded = self._response_data.items.len;
|
||||||
|
try self._proto.dispatch(.load, .{
|
||||||
|
.total = loaded,
|
||||||
|
.loaded = loaded,
|
||||||
|
}, page);
|
||||||
|
try self._proto.dispatch(.load_end, .{
|
||||||
|
.total = loaded,
|
||||||
|
.loaded = loaded,
|
||||||
|
}, page);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getReadyState(self: *const XMLHttpRequest) u32 {
|
pub fn getReadyState(self: *const XMLHttpRequest) u32 {
|
||||||
return @intFromEnum(self._ready_state);
|
return @intFromEnum(self._ready_state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
pub fn processMessage(cmd: anytype) !void {
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
@@ -24,6 +25,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
setFocusEmulationEnabled,
|
setFocusEmulationEnabled,
|
||||||
setDeviceMetricsOverride,
|
setDeviceMetricsOverride,
|
||||||
setTouchEmulationEnabled,
|
setTouchEmulationEnabled,
|
||||||
|
setUserAgentOverride,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -31,6 +33,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
.setFocusEmulationEnabled => return setFocusEmulationEnabled(cmd),
|
.setFocusEmulationEnabled => return setFocusEmulationEnabled(cmd),
|
||||||
.setDeviceMetricsOverride => return setDeviceMetricsOverride(cmd),
|
.setDeviceMetricsOverride => return setDeviceMetricsOverride(cmd),
|
||||||
.setTouchEmulationEnabled => return setTouchEmulationEnabled(cmd),
|
.setTouchEmulationEnabled => return setTouchEmulationEnabled(cmd),
|
||||||
|
.setUserAgentOverride => return setUserAgentOverride(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,3 +67,8 @@ fn setDeviceMetricsOverride(cmd: anytype) !void {
|
|||||||
fn setTouchEmulationEnabled(cmd: anytype) !void {
|
fn setTouchEmulationEnabled(cmd: anytype) !void {
|
||||||
return cmd.sendResult(null, .{});
|
return cmd.sendResult(null, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setUserAgentOverride(cmd: anytype) !void {
|
||||||
|
log.info(.app, "setUserAgentOverride ignored", .{});
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ const std = @import("std");
|
|||||||
pub fn processMessage(cmd: anytype) !void {
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
enable,
|
enable,
|
||||||
|
disable,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
.enable => return cmd.sendResult(null, .{}),
|
.enable => return cmd.sendResult(null, .{}),
|
||||||
|
.disable => return cmd.sendResult(null, .{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
getSemanticTree,
|
getSemanticTree,
|
||||||
getInteractiveElements,
|
getInteractiveElements,
|
||||||
getStructuredData,
|
getStructuredData,
|
||||||
|
clickNode,
|
||||||
|
fillNode,
|
||||||
|
scrollNode,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -39,6 +42,9 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
.getSemanticTree => return getSemanticTree(cmd),
|
.getSemanticTree => return getSemanticTree(cmd),
|
||||||
.getInteractiveElements => return getInteractiveElements(cmd),
|
.getInteractiveElements => return getInteractiveElements(cmd),
|
||||||
.getStructuredData => return getStructuredData(cmd),
|
.getStructuredData => return getStructuredData(cmd),
|
||||||
|
.clickNode => return clickNode(cmd),
|
||||||
|
.fillNode => return fillNode(cmd),
|
||||||
|
.scrollNode => return scrollNode(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,24 +52,32 @@ fn getSemanticTree(cmd: anytype) !void {
|
|||||||
const Params = struct {
|
const Params = struct {
|
||||||
format: ?enum { text } = null,
|
format: ?enum { text } = null,
|
||||||
prune: ?bool = null,
|
prune: ?bool = null,
|
||||||
|
interactiveOnly: ?bool = null,
|
||||||
|
backendNodeId: ?Node.Id = null,
|
||||||
|
maxDepth: ?u32 = null,
|
||||||
};
|
};
|
||||||
const params = (try cmd.params(Params)) orelse Params{};
|
const params = (try cmd.params(Params)) orelse Params{};
|
||||||
|
|
||||||
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
const dom_node = page.document.asNode();
|
|
||||||
|
const dom_node = if (params.backendNodeId) |nodeId|
|
||||||
|
(bc.node_registry.lookup_by_id.get(nodeId) orelse return error.InvalidNodeId).dom
|
||||||
|
else
|
||||||
|
page.document.asNode();
|
||||||
|
|
||||||
var st = SemanticTree{
|
var st = SemanticTree{
|
||||||
.dom_node = dom_node,
|
.dom_node = dom_node,
|
||||||
.registry = &bc.node_registry,
|
.registry = &bc.node_registry,
|
||||||
.page = page,
|
.page = page,
|
||||||
.arena = cmd.arena,
|
.arena = cmd.arena,
|
||||||
.prune = params.prune orelse false,
|
.prune = params.prune orelse true,
|
||||||
|
.interactive_only = params.interactiveOnly orelse false,
|
||||||
|
.max_depth = params.maxDepth orelse std.math.maxInt(u32) - 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (params.format) |format| {
|
if (params.format) |format| {
|
||||||
if (format == .text) {
|
if (format == .text) {
|
||||||
st.prune = params.prune orelse true;
|
|
||||||
var aw: std.Io.Writer.Allocating = .init(cmd.arena);
|
var aw: std.Io.Writer.Allocating = .init(cmd.arena);
|
||||||
defer aw.deinit();
|
defer aw.deinit();
|
||||||
try st.textStringify(&aw.writer);
|
try st.textStringify(&aw.writer);
|
||||||
@@ -146,6 +160,76 @@ fn getStructuredData(cmd: anytype) !void {
|
|||||||
}, .{});
|
}, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clickNode(cmd: anytype) !void {
|
||||||
|
const Params = struct {
|
||||||
|
nodeId: ?Node.Id = null,
|
||||||
|
backendNodeId: ?Node.Id = null,
|
||||||
|
};
|
||||||
|
const params = (try cmd.params(Params)) orelse return error.InvalidParam;
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||||
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
|
|
||||||
|
const node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;
|
||||||
|
const node = bc.node_registry.lookup_by_id.get(node_id) orelse return error.InvalidNodeId;
|
||||||
|
|
||||||
|
lp.actions.click(node.dom, page) catch |err| {
|
||||||
|
if (err == error.InvalidNodeType) return error.InvalidParam;
|
||||||
|
return error.InternalError;
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmd.sendResult(.{}, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fillNode(cmd: anytype) !void {
|
||||||
|
const Params = struct {
|
||||||
|
nodeId: ?Node.Id = null,
|
||||||
|
backendNodeId: ?Node.Id = null,
|
||||||
|
text: []const u8,
|
||||||
|
};
|
||||||
|
const params = (try cmd.params(Params)) orelse return error.InvalidParam;
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||||
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
|
|
||||||
|
const node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;
|
||||||
|
const node = bc.node_registry.lookup_by_id.get(node_id) orelse return error.InvalidNodeId;
|
||||||
|
|
||||||
|
lp.actions.fill(node.dom, params.text, page) catch |err| {
|
||||||
|
if (err == error.InvalidNodeType) return error.InvalidParam;
|
||||||
|
return error.InternalError;
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmd.sendResult(.{}, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scrollNode(cmd: anytype) !void {
|
||||||
|
const Params = struct {
|
||||||
|
nodeId: ?Node.Id = null,
|
||||||
|
backendNodeId: ?Node.Id = null,
|
||||||
|
x: ?i32 = null,
|
||||||
|
y: ?i32 = null,
|
||||||
|
};
|
||||||
|
const params = (try cmd.params(Params)) orelse return error.InvalidParam;
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||||
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
|
|
||||||
|
const maybe_node_id = params.nodeId orelse params.backendNodeId;
|
||||||
|
|
||||||
|
var target_node: ?*DOMNode = null;
|
||||||
|
if (maybe_node_id) |node_id| {
|
||||||
|
const node = bc.node_registry.lookup_by_id.get(node_id) orelse return error.InvalidNodeId;
|
||||||
|
target_node = node.dom;
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.actions.scroll(target_node, params.x, params.y, page) catch |err| {
|
||||||
|
if (err == error.InvalidNodeType) return error.InvalidParam;
|
||||||
|
return error.InternalError;
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmd.sendResult(.{}, .{});
|
||||||
|
}
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
test "cdp.lp: getMarkdown" {
|
test "cdp.lp: getMarkdown" {
|
||||||
var ctx = testing.context();
|
var ctx = testing.context();
|
||||||
@@ -195,3 +279,63 @@ test "cdp.lp: getStructuredData" {
|
|||||||
const result = ctx.client.?.sent.items[0].object.get("result").?.object;
|
const result = ctx.client.?.sent.items[0].object.get("result").?.object;
|
||||||
try testing.expect(result.get("structuredData") != null);
|
try testing.expect(result.get("structuredData") != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "cdp.lp: action tools" {
|
||||||
|
var ctx = testing.context();
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
const bc = try ctx.loadBrowserContext(.{});
|
||||||
|
const page = try bc.session.createPage();
|
||||||
|
const url = "http://localhost:9582/src/browser/tests/mcp_actions.html";
|
||||||
|
try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
|
||||||
|
_ = bc.session.wait(5000);
|
||||||
|
|
||||||
|
// Test Click
|
||||||
|
const btn = page.document.getElementById("btn", page).?.asNode();
|
||||||
|
const btn_id = (try bc.node_registry.register(btn)).id;
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 1,
|
||||||
|
.method = "LP.clickNode",
|
||||||
|
.params = .{ .backendNodeId = btn_id },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test Fill Input
|
||||||
|
const inp = page.document.getElementById("inp", page).?.asNode();
|
||||||
|
const inp_id = (try bc.node_registry.register(inp)).id;
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 2,
|
||||||
|
.method = "LP.fillNode",
|
||||||
|
.params = .{ .backendNodeId = inp_id, .text = "hello" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test Fill Select
|
||||||
|
const sel = page.document.getElementById("sel", page).?.asNode();
|
||||||
|
const sel_id = (try bc.node_registry.register(sel)).id;
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 3,
|
||||||
|
.method = "LP.fillNode",
|
||||||
|
.params = .{ .backendNodeId = sel_id, .text = "opt2" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test Scroll
|
||||||
|
const scrollbox = page.document.getElementById("scrollbox", page).?.asNode();
|
||||||
|
const scrollbox_id = (try bc.node_registry.register(scrollbox)).id;
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 4,
|
||||||
|
.method = "LP.scrollNode",
|
||||||
|
.params = .{ .backendNodeId = scrollbox_id, .y = 50 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Evaluate assertions
|
||||||
|
var ls: lp.js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
var try_catch: lp.js.TryCatch = undefined;
|
||||||
|
try_catch.init(&ls.local);
|
||||||
|
defer try_catch.deinit();
|
||||||
|
|
||||||
|
const result = try ls.local.compileAndRun("window.clicked === true && window.inputVal === 'hello' && window.changed === true && window.selChanged === 'opt2' && window.scrolled === true", null);
|
||||||
|
|
||||||
|
try testing.expect(result.isTrue());
|
||||||
|
}
|
||||||
|
|||||||
@@ -663,7 +663,7 @@ test "cdp.page: getFrameTree" {
|
|||||||
|
|
||||||
test "cdp.page: captureScreenshot" {
|
test "cdp.page: captureScreenshot" {
|
||||||
const LogFilter = @import("../../testing.zig").LogFilter;
|
const LogFilter = @import("../../testing.zig").LogFilter;
|
||||||
const filter: LogFilter = .init(.not_implemented);
|
const filter: LogFilter = .init(&.{.not_implemented});
|
||||||
defer filter.deinit();
|
defer filter.deinit();
|
||||||
|
|
||||||
var ctx = testing.context();
|
var ctx = testing.context();
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ const std = @import("std");
|
|||||||
pub fn processMessage(cmd: anytype) !void {
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
enable,
|
enable,
|
||||||
|
disable,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
.enable => return cmd.sendResult(null, .{}),
|
.enable => return cmd.sendResult(null, .{}),
|
||||||
|
.disable => return cmd.sendResult(null, .{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ const std = @import("std");
|
|||||||
pub fn processMessage(cmd: anytype) !void {
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
enable,
|
enable,
|
||||||
|
disable,
|
||||||
setIgnoreCertificateErrors,
|
setIgnoreCertificateErrors,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
.enable => return cmd.sendResult(null, .{}),
|
.enable => return cmd.sendResult(null, .{}),
|
||||||
|
.disable => return cmd.sendResult(null, .{}),
|
||||||
.setIgnoreCertificateErrors => return setIgnoreCertificateErrors(cmd),
|
.setIgnoreCertificateErrors => return setIgnoreCertificateErrors(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ pub const markdown = @import("browser/markdown.zig");
|
|||||||
pub const SemanticTree = @import("SemanticTree.zig");
|
pub const SemanticTree = @import("SemanticTree.zig");
|
||||||
pub const CDPNode = @import("cdp/Node.zig");
|
pub const CDPNode = @import("cdp/Node.zig");
|
||||||
pub const interactive = @import("browser/interactive.zig");
|
pub const interactive = @import("browser/interactive.zig");
|
||||||
|
pub const actions = @import("browser/actions.zig");
|
||||||
pub const structured_data = @import("browser/structured_data.zig");
|
pub const structured_data = @import("browser/structured_data.zig");
|
||||||
pub const mcp = @import("mcp.zig");
|
pub const mcp = @import("mcp.zig");
|
||||||
pub const build_config = @import("build_config");
|
pub const build_config = @import("build_config");
|
||||||
|
|||||||
63
src/main.zig
63
src/main.zig
@@ -59,7 +59,11 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
|||||||
return std.process.cleanExit();
|
return std.process.cleanExit();
|
||||||
},
|
},
|
||||||
.version => {
|
.version => {
|
||||||
std.debug.print("{s}\n", .{lp.build_config.git_commit});
|
if (lp.build_config.git_version) |version| {
|
||||||
|
std.debug.print("{s} ({s})\n", .{ version, lp.build_config.git_commit });
|
||||||
|
} else {
|
||||||
|
std.debug.print("{s}\n", .{lp.build_config.git_commit});
|
||||||
|
}
|
||||||
return std.process.cleanExit();
|
return std.process.cleanExit();
|
||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
@@ -75,18 +79,21 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
|||||||
log.opts.filter_scopes = lfs;
|
log.opts.filter_scopes = lfs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// must be installed before any other threads
|
||||||
|
const sighandler = try main_arena.create(SigHandler);
|
||||||
|
sighandler.* = .{ .arena = main_arena };
|
||||||
|
try sighandler.install();
|
||||||
|
|
||||||
// _app is global to handle graceful shutdown.
|
// _app is global to handle graceful shutdown.
|
||||||
var app = try App.init(allocator, &args);
|
var app = try App.init(allocator, &args);
|
||||||
|
|
||||||
defer app.deinit();
|
defer app.deinit();
|
||||||
|
|
||||||
|
try sighandler.on(lp.Network.stop, .{&app.network});
|
||||||
|
|
||||||
app.telemetry.record(.{ .run = {} });
|
app.telemetry.record(.{ .run = {} });
|
||||||
|
|
||||||
switch (args.mode) {
|
switch (args.mode) {
|
||||||
.serve => |opts| {
|
.serve => |opts| {
|
||||||
const sighandler = try main_arena.create(SigHandler);
|
|
||||||
sighandler.* = .{ .arena = main_arena };
|
|
||||||
try sighandler.install();
|
|
||||||
|
|
||||||
log.debug(.app, "startup", .{ .mode = "serve", .snapshot = app.snapshot.fromEmbedded() });
|
log.debug(.app, "startup", .{ .mode = "serve", .snapshot = app.snapshot.fromEmbedded() });
|
||||||
const address = std.net.Address.parseIp(opts.host, opts.port) catch |err| {
|
const address = std.net.Address.parseIp(opts.host, opts.port) catch |err| {
|
||||||
log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port });
|
log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port });
|
||||||
@@ -94,12 +101,22 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var server = lp.Server.init(app, address) catch |err| {
|
var server = lp.Server.init(app, address) catch |err| {
|
||||||
log.fatal(.app, "server run error", .{ .err = err });
|
if (err == error.AddressInUse) {
|
||||||
|
log.fatal(.app, "address already in use", .{
|
||||||
|
.host = opts.host,
|
||||||
|
.port = opts.port,
|
||||||
|
.hint = "Another process is already listening on this address. " ++
|
||||||
|
"Stop the other process or use --port to choose a different port.",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.fatal(.app, "server run error", .{ .err = err });
|
||||||
|
}
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
defer server.deinit();
|
defer server.deinit();
|
||||||
|
|
||||||
try sighandler.on(lp.Network.stop, .{&app.network});
|
try sighandler.on(lp.Server.shutdown, .{server});
|
||||||
|
|
||||||
app.network.run();
|
app.network.run();
|
||||||
},
|
},
|
||||||
.fetch => |opts| {
|
.fetch => |opts| {
|
||||||
@@ -122,10 +139,10 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
|||||||
fetch_opts.writer = &writer.interface;
|
fetch_opts.writer = &writer.interface;
|
||||||
}
|
}
|
||||||
|
|
||||||
lp.fetch(app, url, fetch_opts) catch |err| {
|
var worker_thread = try std.Thread.spawn(.{}, fetchThread, .{ app, url, fetch_opts });
|
||||||
log.fatal(.app, "fetch error", .{ .err = err, .url = url });
|
defer worker_thread.join();
|
||||||
return err;
|
|
||||||
};
|
app.network.run();
|
||||||
},
|
},
|
||||||
.mcp => {
|
.mcp => {
|
||||||
log.info(.mcp, "starting server", .{});
|
log.info(.mcp, "starting server", .{});
|
||||||
@@ -137,11 +154,27 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
|||||||
var mcp_server: *lp.mcp.Server = try .init(allocator, app, &stdout.interface);
|
var mcp_server: *lp.mcp.Server = try .init(allocator, app, &stdout.interface);
|
||||||
defer mcp_server.deinit();
|
defer mcp_server.deinit();
|
||||||
|
|
||||||
var stdin_buf: [64 * 1024]u8 = undefined;
|
var worker_thread = try std.Thread.spawn(.{}, mcpThread, .{ mcp_server, app });
|
||||||
var stdin = std.fs.File.stdin().reader(&stdin_buf);
|
defer worker_thread.join();
|
||||||
|
|
||||||
try lp.mcp.router.processRequests(mcp_server, &stdin.interface);
|
app.network.run();
|
||||||
},
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fetchThread(app: *App, url: [:0]const u8, fetch_opts: lp.FetchOpts) void {
|
||||||
|
defer app.network.stop();
|
||||||
|
lp.fetch(app, url, fetch_opts) catch |err| {
|
||||||
|
log.fatal(.app, "fetch error", .{ .err = err, .url = url });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mcpThread(mcp_server: *lp.mcp.Server, app: *App) void {
|
||||||
|
defer app.network.stop();
|
||||||
|
var stdin_buf: [64 * 1024]u8 = undefined;
|
||||||
|
var stdin = std.fs.File.stdin().reader(&stdin_buf);
|
||||||
|
lp.mcp.router.processRequests(mcp_server, &stdin.interface) catch |err| {
|
||||||
|
log.fatal(.mcp, "mcp error", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ test "MCP.router - handleMessage - synchronous unit tests" {
|
|||||||
|
|
||||||
// 5. Parse error
|
// 5. Parse error
|
||||||
{
|
{
|
||||||
const filter: testing.LogFilter = .init(.mcp);
|
const filter: testing.LogFilter = .init(&.{.mcp});
|
||||||
defer filter.deinit();
|
defer filter.deinit();
|
||||||
|
|
||||||
try handleMessage(server, aa, "invalid json");
|
try handleMessage(server, aa, "invalid json");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const log = lp.log;
|
|||||||
const js = lp.js;
|
const js = lp.js;
|
||||||
|
|
||||||
const Element = @import("../browser/webapi/Element.zig");
|
const Element = @import("../browser/webapi/Element.zig");
|
||||||
|
const DOMNode = @import("../browser/webapi/Node.zig");
|
||||||
const Selector = @import("../browser/webapi/selector/Selector.zig");
|
const Selector = @import("../browser/webapi/selector/Selector.zig");
|
||||||
const protocol = @import("protocol.zig");
|
const protocol = @import("protocol.zig");
|
||||||
const Server = @import("Server.zig");
|
const Server = @import("Server.zig");
|
||||||
@@ -69,7 +70,9 @@ pub const tool_list = [_]protocol.Tool{
|
|||||||
\\{
|
\\{
|
||||||
\\ "type": "object",
|
\\ "type": "object",
|
||||||
\\ "properties": {
|
\\ "properties": {
|
||||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before fetching the semantic tree." }
|
\\ "url": { "type": "string", "description": "Optional URL to navigate to before fetching the semantic tree." },
|
||||||
|
\\ "backendNodeId": { "type": "integer", "description": "Optional backend node ID to get the tree for a specific element instead of the document root." },
|
||||||
|
\\ "maxDepth": { "type": "integer", "description": "Optional maximum depth of the tree to return. Useful for exploring high-level structure first." }
|
||||||
\\ }
|
\\ }
|
||||||
\\}
|
\\}
|
||||||
),
|
),
|
||||||
@@ -98,6 +101,47 @@ pub const tool_list = [_]protocol.Tool{
|
|||||||
\\}
|
\\}
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
.{
|
||||||
|
.name = "click",
|
||||||
|
.description = "Click on an interactive element.",
|
||||||
|
.inputSchema = protocol.minify(
|
||||||
|
\\{
|
||||||
|
\\ "type": "object",
|
||||||
|
\\ "properties": {
|
||||||
|
\\ "backendNodeId": { "type": "integer", "description": "The backend node ID of the element to click." }
|
||||||
|
\\ },
|
||||||
|
\\ "required": ["backendNodeId"]
|
||||||
|
\\}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.name = "fill",
|
||||||
|
.description = "Fill text into an input element.",
|
||||||
|
.inputSchema = protocol.minify(
|
||||||
|
\\{
|
||||||
|
\\ "type": "object",
|
||||||
|
\\ "properties": {
|
||||||
|
\\ "backendNodeId": { "type": "integer", "description": "The backend node ID of the input element to fill." },
|
||||||
|
\\ "text": { "type": "string", "description": "The text to fill into the input element." }
|
||||||
|
\\ },
|
||||||
|
\\ "required": ["backendNodeId", "text"]
|
||||||
|
\\}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.name = "scroll",
|
||||||
|
.description = "Scroll the page or a specific element.",
|
||||||
|
.inputSchema = protocol.minify(
|
||||||
|
\\{
|
||||||
|
\\ "type": "object",
|
||||||
|
\\ "properties": {
|
||||||
|
\\ "backendNodeId": { "type": "integer", "description": "Optional: The backend node ID of the element to scroll. If omitted, scrolls the window." },
|
||||||
|
\\ "x": { "type": "integer", "description": "Optional: The horizontal scroll offset." },
|
||||||
|
\\ "y": { "type": "integer", "description": "Optional: The vertical scroll offset." }
|
||||||
|
\\ }
|
||||||
|
\\}
|
||||||
|
),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
||||||
@@ -119,6 +163,8 @@ const ToolStreamingText = struct {
|
|||||||
action: enum { markdown, links, semantic_tree },
|
action: enum { markdown, links, semantic_tree },
|
||||||
registry: ?*CDPNode.Registry = null,
|
registry: ?*CDPNode.Registry = null,
|
||||||
arena: ?std.mem.Allocator = null,
|
arena: ?std.mem.Allocator = null,
|
||||||
|
backendNodeId: ?u32 = null,
|
||||||
|
maxDepth: ?u32 = null,
|
||||||
|
|
||||||
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void {
|
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void {
|
||||||
try jw.beginWriteRaw();
|
try jw.beginWriteRaw();
|
||||||
@@ -154,12 +200,24 @@ const ToolStreamingText = struct {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
.semantic_tree => {
|
.semantic_tree => {
|
||||||
|
var root_node = self.page.document.asNode();
|
||||||
|
if (self.backendNodeId) |node_id| {
|
||||||
|
if (self.registry) |registry| {
|
||||||
|
if (registry.lookup_by_id.get(node_id)) |n| {
|
||||||
|
root_node = n.dom;
|
||||||
|
} else {
|
||||||
|
log.warn(.mcp, "semantic_tree id missing", .{ .id = node_id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const st = lp.SemanticTree{
|
const st = lp.SemanticTree{
|
||||||
.dom_node = self.page.document.asNode(),
|
.dom_node = root_node,
|
||||||
.registry = self.registry.?,
|
.registry = self.registry.?,
|
||||||
.page = self.page,
|
.page = self.page,
|
||||||
.arena = self.arena.?,
|
.arena = self.arena.?,
|
||||||
.prune = true,
|
.prune = true,
|
||||||
|
.max_depth = self.maxDepth orelse std.math.maxInt(u32) - 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
st.textStringify(w) catch |err| {
|
st.textStringify(w) catch |err| {
|
||||||
@@ -182,6 +240,9 @@ const ToolAction = enum {
|
|||||||
structuredData,
|
structuredData,
|
||||||
evaluate,
|
evaluate,
|
||||||
semantic_tree,
|
semantic_tree,
|
||||||
|
click,
|
||||||
|
fill,
|
||||||
|
scroll,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tool_map = std.StaticStringMap(ToolAction).initComptime(.{
|
const tool_map = std.StaticStringMap(ToolAction).initComptime(.{
|
||||||
@@ -193,6 +254,9 @@ const tool_map = std.StaticStringMap(ToolAction).initComptime(.{
|
|||||||
.{ "structuredData", .structuredData },
|
.{ "structuredData", .structuredData },
|
||||||
.{ "evaluate", .evaluate },
|
.{ "evaluate", .evaluate },
|
||||||
.{ "semantic_tree", .semantic_tree },
|
.{ "semantic_tree", .semantic_tree },
|
||||||
|
.{ "click", .click },
|
||||||
|
.{ "fill", .fill },
|
||||||
|
.{ "scroll", .scroll },
|
||||||
});
|
});
|
||||||
|
|
||||||
pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
||||||
@@ -221,6 +285,9 @@ pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Reque
|
|||||||
.structuredData => try handleStructuredData(server, arena, req.id.?, call_params.arguments),
|
.structuredData => try handleStructuredData(server, arena, req.id.?, call_params.arguments),
|
||||||
.evaluate => try handleEvaluate(server, arena, req.id.?, call_params.arguments),
|
.evaluate => try handleEvaluate(server, arena, req.id.?, call_params.arguments),
|
||||||
.semantic_tree => try handleSemanticTree(server, arena, req.id.?, call_params.arguments),
|
.semantic_tree => try handleSemanticTree(server, arena, req.id.?, call_params.arguments),
|
||||||
|
.click => try handleClick(server, arena, req.id.?, call_params.arguments),
|
||||||
|
.fill => try handleFill(server, arena, req.id.?, call_params.arguments),
|
||||||
|
.scroll => try handleScroll(server, arena, req.id.?, call_params.arguments),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,9 +344,13 @@ fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, ar
|
|||||||
fn handleSemanticTree(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
fn handleSemanticTree(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
||||||
const TreeParams = struct {
|
const TreeParams = struct {
|
||||||
url: ?[:0]const u8 = null,
|
url: ?[:0]const u8 = null,
|
||||||
|
backendNodeId: ?u32 = null,
|
||||||
|
maxDepth: ?u32 = null,
|
||||||
};
|
};
|
||||||
|
var tree_args: TreeParams = .{};
|
||||||
if (arguments) |args_raw| {
|
if (arguments) |args_raw| {
|
||||||
if (std.json.parseFromValueLeaky(TreeParams, arena, args_raw, .{ .ignore_unknown_fields = true })) |args| {
|
if (std.json.parseFromValueLeaky(TreeParams, arena, args_raw, .{ .ignore_unknown_fields = true })) |args| {
|
||||||
|
tree_args = args;
|
||||||
if (args.url) |u| {
|
if (args.url) |u| {
|
||||||
try performGoto(server, u, id);
|
try performGoto(server, u, id);
|
||||||
}
|
}
|
||||||
@@ -290,7 +361,7 @@ fn handleSemanticTree(server: *Server, arena: std.mem.Allocator, id: std.json.Va
|
|||||||
};
|
};
|
||||||
|
|
||||||
const content = [_]protocol.TextContent(ToolStreamingText){.{
|
const content = [_]protocol.TextContent(ToolStreamingText){.{
|
||||||
.text = .{ .page = page, .action = .semantic_tree, .registry = &server.node_registry, .arena = arena },
|
.text = .{ .page = page, .action = .semantic_tree, .registry = &server.node_registry, .arena = arena, .backendNodeId = tree_args.backendNodeId, .maxDepth = tree_args.maxDepth },
|
||||||
}};
|
}};
|
||||||
try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content });
|
try server.sendResult(id, protocol.CallToolResult(ToolStreamingText){ .content = &content });
|
||||||
}
|
}
|
||||||
@@ -380,6 +451,87 @@ fn handleEvaluate(server: *Server, arena: std.mem.Allocator, id: std.json.Value,
|
|||||||
try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handleClick(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
||||||
|
const ClickParams = struct {
|
||||||
|
backendNodeId: CDPNode.Id,
|
||||||
|
};
|
||||||
|
const args = try parseArguments(ClickParams, arena, arguments, server, id, "click");
|
||||||
|
|
||||||
|
const page = server.session.currentPage() orelse {
|
||||||
|
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||||
|
};
|
||||||
|
|
||||||
|
const node = server.node_registry.lookup_by_id.get(args.backendNodeId) orelse {
|
||||||
|
return server.sendError(id, .InvalidParams, "Node not found");
|
||||||
|
};
|
||||||
|
|
||||||
|
lp.actions.click(node.dom, page) catch |err| {
|
||||||
|
if (err == error.InvalidNodeType) {
|
||||||
|
return server.sendError(id, .InvalidParams, "Node is not an HTML element");
|
||||||
|
}
|
||||||
|
return server.sendError(id, .InternalError, "Failed to click element");
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = [_]protocol.TextContent([]const u8){.{ .text = "Clicked successfully." }};
|
||||||
|
try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleFill(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
||||||
|
const FillParams = struct {
|
||||||
|
backendNodeId: CDPNode.Id,
|
||||||
|
text: []const u8,
|
||||||
|
};
|
||||||
|
const args = try parseArguments(FillParams, arena, arguments, server, id, "fill");
|
||||||
|
|
||||||
|
const page = server.session.currentPage() orelse {
|
||||||
|
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||||
|
};
|
||||||
|
|
||||||
|
const node = server.node_registry.lookup_by_id.get(args.backendNodeId) orelse {
|
||||||
|
return server.sendError(id, .InvalidParams, "Node not found");
|
||||||
|
};
|
||||||
|
|
||||||
|
lp.actions.fill(node.dom, args.text, page) catch |err| {
|
||||||
|
if (err == error.InvalidNodeType) {
|
||||||
|
return server.sendError(id, .InvalidParams, "Node is not an input, textarea or select");
|
||||||
|
}
|
||||||
|
return server.sendError(id, .InternalError, "Failed to fill element");
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = [_]protocol.TextContent([]const u8){.{ .text = "Filled successfully." }};
|
||||||
|
try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleScroll(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
||||||
|
const ScrollParams = struct {
|
||||||
|
backendNodeId: ?CDPNode.Id = null,
|
||||||
|
x: ?i32 = null,
|
||||||
|
y: ?i32 = null,
|
||||||
|
};
|
||||||
|
const args = try parseArguments(ScrollParams, arena, arguments, server, id, "scroll");
|
||||||
|
|
||||||
|
const page = server.session.currentPage() orelse {
|
||||||
|
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||||
|
};
|
||||||
|
|
||||||
|
var target_node: ?*DOMNode = null;
|
||||||
|
if (args.backendNodeId) |node_id| {
|
||||||
|
const node = server.node_registry.lookup_by_id.get(node_id) orelse {
|
||||||
|
return server.sendError(id, .InvalidParams, "Node not found");
|
||||||
|
};
|
||||||
|
target_node = node.dom;
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.actions.scroll(target_node, args.x, args.y, page) catch |err| {
|
||||||
|
if (err == error.InvalidNodeType) {
|
||||||
|
return server.sendError(id, .InvalidParams, "Node is not an element");
|
||||||
|
}
|
||||||
|
return server.sendError(id, .InternalError, "Failed to scroll");
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = [_]protocol.TextContent([]const u8){.{ .text = "Scrolled successfully." }};
|
||||||
|
try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||||
|
}
|
||||||
fn parseArguments(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value, server: *Server, id: std.json.Value, tool_name: []const u8) !T {
|
fn parseArguments(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value, server: *Server, id: std.json.Value, tool_name: []const u8) !T {
|
||||||
if (arguments == null) {
|
if (arguments == null) {
|
||||||
try server.sendError(id, .InvalidParams, "Missing arguments");
|
try server.sendError(id, .InvalidParams, "Missing arguments");
|
||||||
@@ -455,3 +607,66 @@ test "MCP - evaluate error reporting" {
|
|||||||
\\}
|
\\}
|
||||||
, out_alloc.writer.buffered());
|
, out_alloc.writer.buffered());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "MCP - Actions: click, fill, scroll" {
|
||||||
|
defer testing.reset();
|
||||||
|
const allocator = testing.allocator;
|
||||||
|
const app = testing.test_app;
|
||||||
|
|
||||||
|
var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator);
|
||||||
|
defer out_alloc.deinit();
|
||||||
|
|
||||||
|
var server = try Server.init(allocator, app, &out_alloc.writer);
|
||||||
|
defer server.deinit();
|
||||||
|
|
||||||
|
const aa = testing.arena_allocator;
|
||||||
|
const page = try server.session.createPage();
|
||||||
|
const url = "http://localhost:9582/src/browser/tests/mcp_actions.html";
|
||||||
|
try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
|
||||||
|
_ = server.session.wait(5000);
|
||||||
|
|
||||||
|
// Test Click
|
||||||
|
const btn = page.document.getElementById("btn", page).?.asNode();
|
||||||
|
const btn_id = (try server.node_registry.register(btn)).id;
|
||||||
|
var btn_id_buf: [12]u8 = undefined;
|
||||||
|
const btn_id_str = std.fmt.bufPrint(&btn_id_buf, "{d}", .{btn_id}) catch unreachable;
|
||||||
|
const click_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"click\",\"arguments\":{\"backendNodeId\":", btn_id_str, "}}}" });
|
||||||
|
try router.handleMessage(server, aa, click_msg);
|
||||||
|
|
||||||
|
// Test Fill Input
|
||||||
|
const inp = page.document.getElementById("inp", page).?.asNode();
|
||||||
|
const inp_id = (try server.node_registry.register(inp)).id;
|
||||||
|
var inp_id_buf: [12]u8 = undefined;
|
||||||
|
const inp_id_str = std.fmt.bufPrint(&inp_id_buf, "{d}", .{inp_id}) catch unreachable;
|
||||||
|
const fill_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"fill\",\"arguments\":{\"backendNodeId\":", inp_id_str, ",\"text\":\"hello\"}}}" });
|
||||||
|
try router.handleMessage(server, aa, fill_msg);
|
||||||
|
|
||||||
|
// Test Fill Select
|
||||||
|
const sel = page.document.getElementById("sel", page).?.asNode();
|
||||||
|
const sel_id = (try server.node_registry.register(sel)).id;
|
||||||
|
var sel_id_buf: [12]u8 = undefined;
|
||||||
|
const sel_id_str = std.fmt.bufPrint(&sel_id_buf, "{d}", .{sel_id}) catch unreachable;
|
||||||
|
const fill_sel_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"fill\",\"arguments\":{\"backendNodeId\":", sel_id_str, ",\"text\":\"opt2\"}}}" });
|
||||||
|
try router.handleMessage(server, aa, fill_sel_msg);
|
||||||
|
|
||||||
|
// Test Scroll
|
||||||
|
const scrollbox = page.document.getElementById("scrollbox", page).?.asNode();
|
||||||
|
const scrollbox_id = (try server.node_registry.register(scrollbox)).id;
|
||||||
|
var scroll_id_buf: [12]u8 = undefined;
|
||||||
|
const scroll_id_str = std.fmt.bufPrint(&scroll_id_buf, "{d}", .{scrollbox_id}) catch unreachable;
|
||||||
|
const scroll_msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":{\"name\":\"scroll\",\"arguments\":{\"backendNodeId\":", scroll_id_str, ",\"y\":50}}}" });
|
||||||
|
try router.handleMessage(server, aa, scroll_msg);
|
||||||
|
|
||||||
|
// Evaluate assertions
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
var try_catch: js.TryCatch = undefined;
|
||||||
|
try_catch.init(&ls.local);
|
||||||
|
defer try_catch.deinit();
|
||||||
|
|
||||||
|
const result = try ls.local.compileAndRun("window.clicked === true && window.inputVal === 'hello' && window.changed === true && window.selChanged === 'opt2' && window.scrolled === true", null);
|
||||||
|
|
||||||
|
try testing.expect(result.isTrue());
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ const Listener = struct {
|
|||||||
onAccept: *const fn (ctx: *anyopaque, socket: posix.socket_t) void,
|
onAccept: *const fn (ctx: *anyopaque, socket: posix.socket_t) void,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Number of fixed pollfds entries (wakeup pipe + listener).
|
||||||
|
const PSEUDO_POLLFDS = 2;
|
||||||
|
|
||||||
|
const MAX_TICK_CALLBACKS = 16;
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
|
||||||
config: *const Config,
|
config: *const Config,
|
||||||
@@ -57,6 +62,22 @@ wakeup_pipe: [2]posix.fd_t = .{ -1, -1 },
|
|||||||
|
|
||||||
shutdown: std.atomic.Value(bool) = .init(false),
|
shutdown: std.atomic.Value(bool) = .init(false),
|
||||||
|
|
||||||
|
// Multi is a heavy structure that can consume up to 2MB of RAM.
|
||||||
|
// Currently, Runtime is used sparingly, and we only create it on demand.
|
||||||
|
// When Runtime becomes truly shared, it should become a regular field.
|
||||||
|
multi: ?*libcurl.CurlM = null,
|
||||||
|
submission_mutex: std.Thread.Mutex = .{},
|
||||||
|
submission_queue: std.DoublyLinkedList = .{},
|
||||||
|
|
||||||
|
callbacks: [MAX_TICK_CALLBACKS]TickCallback = undefined,
|
||||||
|
callbacks_len: usize = 0,
|
||||||
|
callbacks_mutex: std.Thread.Mutex = .{},
|
||||||
|
|
||||||
|
const TickCallback = struct {
|
||||||
|
ctx: *anyopaque,
|
||||||
|
fun: *const fn (*anyopaque) void,
|
||||||
|
};
|
||||||
|
|
||||||
const ZigToCurlAllocator = struct {
|
const ZigToCurlAllocator = struct {
|
||||||
// C11 requires malloc to return memory aligned to max_align_t (16 bytes on x86_64).
|
// C11 requires malloc to return memory aligned to max_align_t (16 bytes on x86_64).
|
||||||
// We match this guarantee since libcurl expects malloc-compatible alignment.
|
// We match this guarantee since libcurl expects malloc-compatible alignment.
|
||||||
@@ -185,8 +206,8 @@ pub fn init(allocator: Allocator, config: *const Config) !Runtime {
|
|||||||
|
|
||||||
const pipe = try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true });
|
const pipe = try posix.pipe2(.{ .NONBLOCK = true, .CLOEXEC = true });
|
||||||
|
|
||||||
// 0 is wakeup, 1 is listener
|
// 0 is wakeup, 1 is listener, rest for curl fds
|
||||||
const pollfds = try allocator.alloc(posix.pollfd, 2);
|
const pollfds = try allocator.alloc(posix.pollfd, PSEUDO_POLLFDS + config.httpMaxConcurrent());
|
||||||
errdefer allocator.free(pollfds);
|
errdefer allocator.free(pollfds);
|
||||||
|
|
||||||
@memset(pollfds, .{ .fd = -1, .events = 0, .revents = 0 });
|
@memset(pollfds, .{ .fd = -1, .events = 0, .revents = 0 });
|
||||||
@@ -216,16 +237,23 @@ pub fn init(allocator: Allocator, config: *const Config) !Runtime {
|
|||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.config = config,
|
.config = config,
|
||||||
.ca_blob = ca_blob,
|
.ca_blob = ca_blob,
|
||||||
.robot_store = RobotStore.init(allocator),
|
|
||||||
.connections = connections,
|
|
||||||
.available = available,
|
|
||||||
.web_bot_auth = web_bot_auth,
|
|
||||||
.pollfds = pollfds,
|
.pollfds = pollfds,
|
||||||
.wakeup_pipe = pipe,
|
.wakeup_pipe = pipe,
|
||||||
|
|
||||||
|
.available = available,
|
||||||
|
.connections = connections,
|
||||||
|
|
||||||
|
.robot_store = RobotStore.init(allocator),
|
||||||
|
.web_bot_auth = web_bot_auth,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Runtime) void {
|
pub fn deinit(self: *Runtime) void {
|
||||||
|
if (self.multi) |multi| {
|
||||||
|
libcurl.curl_multi_cleanup(multi) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
for (&self.wakeup_pipe) |*fd| {
|
for (&self.wakeup_pipe) |*fd| {
|
||||||
if (fd.* >= 0) {
|
if (fd.* >= 0) {
|
||||||
posix.close(fd.*);
|
posix.close(fd.*);
|
||||||
@@ -285,42 +313,105 @@ pub fn bind(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(self: *Runtime) void {
|
pub fn onTick(self: *Runtime, ctx: *anyopaque, callback: *const fn (*anyopaque) void) void {
|
||||||
while (!self.shutdown.load(.acquire)) {
|
self.callbacks_mutex.lock();
|
||||||
const listener = self.listener orelse return;
|
defer self.callbacks_mutex.unlock();
|
||||||
|
|
||||||
_ = posix.poll(self.pollfds, -1) catch |err| {
|
lp.assert(self.callbacks_len < MAX_TICK_CALLBACKS, "too many ticks", .{});
|
||||||
|
|
||||||
|
self.callbacks[self.callbacks_len] = .{
|
||||||
|
.ctx = ctx,
|
||||||
|
.fun = callback,
|
||||||
|
};
|
||||||
|
self.callbacks_len += 1;
|
||||||
|
|
||||||
|
self.wakeupPoll();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fireTicks(self: *Runtime) void {
|
||||||
|
self.callbacks_mutex.lock();
|
||||||
|
defer self.callbacks_mutex.unlock();
|
||||||
|
|
||||||
|
for (self.callbacks[0..self.callbacks_len]) |*callback| {
|
||||||
|
callback.fun(callback.ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(self: *Runtime) void {
|
||||||
|
var drain_buf: [64]u8 = undefined;
|
||||||
|
var running_handles: c_int = 0;
|
||||||
|
|
||||||
|
const poll_fd = &self.pollfds[0];
|
||||||
|
const listen_fd = &self.pollfds[1];
|
||||||
|
|
||||||
|
// Please note that receiving a shutdown command does not terminate all connections.
|
||||||
|
// When gracefully shutting down a server, we at least want to send the remaining
|
||||||
|
// telemetry, but we stop accepting new connections. It is the responsibility
|
||||||
|
// of external code to terminate its requests upon shutdown.
|
||||||
|
while (true) {
|
||||||
|
self.drainQueue();
|
||||||
|
|
||||||
|
if (self.multi) |multi| {
|
||||||
|
// Kickstart newly added handles (DNS/connect) so that
|
||||||
|
// curl registers its sockets before we poll.
|
||||||
|
libcurl.curl_multi_perform(multi, &running_handles) catch |err| {
|
||||||
|
lp.log.err(.app, "curl perform", .{ .err = err });
|
||||||
|
};
|
||||||
|
|
||||||
|
self.preparePollFds(multi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// for ontick to work, you need to wake up periodically
|
||||||
|
const timeout = blk: {
|
||||||
|
const min_timeout = 250; // 250ms
|
||||||
|
if (self.multi == null) {
|
||||||
|
break :blk min_timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
const curl_timeout = self.getCurlTimeout();
|
||||||
|
if (curl_timeout == 0) {
|
||||||
|
break :blk min_timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk @min(min_timeout, curl_timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = posix.poll(self.pollfds, timeout) catch |err| {
|
||||||
lp.log.err(.app, "poll", .{ .err = err });
|
lp.log.err(.app, "poll", .{ .err = err });
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// check wakeup socket
|
// check wakeup pipe
|
||||||
if (self.pollfds[0].revents != 0) {
|
if (poll_fd.revents != 0) {
|
||||||
self.pollfds[0].revents = 0;
|
poll_fd.revents = 0;
|
||||||
|
while (true)
|
||||||
// If we were woken up, perhaps everything was cancelled and the iteration can be completed.
|
_ = posix.read(self.wakeup_pipe[0], &drain_buf) catch break;
|
||||||
if (self.shutdown.load(.acquire)) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check new connections;
|
// accept new connections
|
||||||
if (self.pollfds[1].revents == 0) continue;
|
if (listen_fd.revents != 0) {
|
||||||
self.pollfds[1].revents = 0;
|
listen_fd.revents = 0;
|
||||||
|
self.acceptConnections();
|
||||||
|
}
|
||||||
|
|
||||||
const socket = posix.accept(listener.socket, null, null, posix.SOCK.NONBLOCK) catch |err| {
|
if (self.multi) |multi| {
|
||||||
switch (err) {
|
// Drive transfers and process completions.
|
||||||
error.SocketNotListening, error.ConnectionAborted => {
|
libcurl.curl_multi_perform(multi, &running_handles) catch |err| {
|
||||||
self.pollfds[1] = .{ .fd = -1, .events = 0, .revents = 0 };
|
lp.log.err(.app, "curl perform", .{ .err = err });
|
||||||
self.listener = null;
|
};
|
||||||
},
|
self.processCompletions(multi);
|
||||||
error.WouldBlock => {},
|
}
|
||||||
else => {
|
|
||||||
lp.log.err(.app, "accept", .{ .err = err });
|
|
||||||
},
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
listener.onAccept(listener.ctx, socket);
|
self.fireTicks();
|
||||||
|
|
||||||
|
if (self.shutdown.load(.acquire) and running_handles == 0) {
|
||||||
|
// Check if fireTicks submitted new requests (e.g. telemetry flush).
|
||||||
|
// If so, continue the loop to drain and send them before exiting.
|
||||||
|
self.submission_mutex.lock();
|
||||||
|
const has_pending = self.submission_queue.first != null;
|
||||||
|
self.submission_mutex.unlock();
|
||||||
|
if (!has_pending) break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.listener) |listener| {
|
if (self.listener) |listener| {
|
||||||
@@ -337,9 +428,132 @@ pub fn run(self: *Runtime) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn submitRequest(self: *Runtime, conn: *net_http.Connection) void {
|
||||||
|
self.submission_mutex.lock();
|
||||||
|
self.submission_queue.append(&conn.node);
|
||||||
|
self.submission_mutex.unlock();
|
||||||
|
self.wakeupPoll();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wakeupPoll(self: *Runtime) void {
|
||||||
|
_ = posix.write(self.wakeup_pipe[1], &.{1}) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drainQueue(self: *Runtime) void {
|
||||||
|
self.submission_mutex.lock();
|
||||||
|
defer self.submission_mutex.unlock();
|
||||||
|
|
||||||
|
if (self.submission_queue.first == null) return;
|
||||||
|
|
||||||
|
const multi = self.multi orelse blk: {
|
||||||
|
const m = libcurl.curl_multi_init() orelse {
|
||||||
|
lp.assert(false, "curl multi init failed", .{});
|
||||||
|
unreachable;
|
||||||
|
};
|
||||||
|
self.multi = m;
|
||||||
|
break :blk m;
|
||||||
|
};
|
||||||
|
|
||||||
|
while (self.submission_queue.popFirst()) |node| {
|
||||||
|
const conn: *net_http.Connection = @fieldParentPtr("node", node);
|
||||||
|
conn.setPrivate(conn) catch |err| {
|
||||||
|
lp.log.err(.app, "curl set private", .{ .err = err });
|
||||||
|
self.releaseConnection(conn);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
libcurl.curl_multi_add_handle(multi, conn.easy) catch |err| {
|
||||||
|
lp.log.err(.app, "curl multi add", .{ .err = err });
|
||||||
|
self.releaseConnection(conn);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn stop(self: *Runtime) void {
|
pub fn stop(self: *Runtime) void {
|
||||||
self.shutdown.store(true, .release);
|
self.shutdown.store(true, .release);
|
||||||
_ = posix.write(self.wakeup_pipe[1], &.{1}) catch {};
|
self.wakeupPoll();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn acceptConnections(self: *Runtime) void {
|
||||||
|
if (self.shutdown.load(.acquire)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const listener = self.listener orelse return;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const socket = posix.accept(listener.socket, null, null, posix.SOCK.NONBLOCK) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.WouldBlock => break,
|
||||||
|
error.SocketNotListening => {
|
||||||
|
self.pollfds[1] = .{ .fd = -1, .events = 0, .revents = 0 };
|
||||||
|
self.listener = null;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
error.ConnectionAborted => {
|
||||||
|
lp.log.warn(.app, "accept connection aborted", .{});
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
lp.log.err(.app, "accept error", .{ .err = err });
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
listener.onAccept(listener.ctx, socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preparePollFds(self: *Runtime, multi: *libcurl.CurlM) void {
|
||||||
|
const curl_fds = self.pollfds[PSEUDO_POLLFDS..];
|
||||||
|
@memset(curl_fds, .{ .fd = -1, .events = 0, .revents = 0 });
|
||||||
|
|
||||||
|
var fd_count: c_uint = 0;
|
||||||
|
const wait_fds: []libcurl.CurlWaitFd = @ptrCast(curl_fds);
|
||||||
|
libcurl.curl_multi_waitfds(multi, wait_fds, &fd_count) catch |err| {
|
||||||
|
lp.log.err(.app, "curl waitfds", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getCurlTimeout(self: *Runtime) i32 {
|
||||||
|
const multi = self.multi orelse return -1;
|
||||||
|
var timeout_ms: c_long = -1;
|
||||||
|
libcurl.curl_multi_timeout(multi, &timeout_ms) catch return -1;
|
||||||
|
return @intCast(@min(timeout_ms, std.math.maxInt(i32)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processCompletions(self: *Runtime, multi: *libcurl.CurlM) void {
|
||||||
|
var msgs_in_queue: c_int = 0;
|
||||||
|
while (libcurl.curl_multi_info_read(multi, &msgs_in_queue)) |msg| {
|
||||||
|
switch (msg.data) {
|
||||||
|
.done => |maybe_err| {
|
||||||
|
if (maybe_err) |err| {
|
||||||
|
lp.log.warn(.app, "curl transfer error", .{ .err = err });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
const easy: *libcurl.Curl = msg.easy_handle;
|
||||||
|
var ptr: *anyopaque = undefined;
|
||||||
|
libcurl.curl_easy_getinfo(easy, .private, &ptr) catch
|
||||||
|
lp.assert(false, "curl getinfo private", .{});
|
||||||
|
const conn: *net_http.Connection = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
|
libcurl.curl_multi_remove_handle(multi, easy) catch {};
|
||||||
|
self.releaseConnection(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
if (@sizeOf(posix.pollfd) != @sizeOf(libcurl.CurlWaitFd)) {
|
||||||
|
@compileError("pollfd and CurlWaitFd size mismatch");
|
||||||
|
}
|
||||||
|
if (@offsetOf(posix.pollfd, "fd") != @offsetOf(libcurl.CurlWaitFd, "fd") or
|
||||||
|
@offsetOf(posix.pollfd, "events") != @offsetOf(libcurl.CurlWaitFd, "events") or
|
||||||
|
@offsetOf(posix.pollfd, "revents") != @offsetOf(libcurl.CurlWaitFd, "revents"))
|
||||||
|
{
|
||||||
|
@compileError("pollfd and CurlWaitFd layout mismatch");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getConnection(self: *Runtime) ?*net_http.Connection {
|
pub fn getConnection(self: *Runtime) ?*net_http.Connection {
|
||||||
|
|||||||
@@ -386,11 +386,18 @@ pub const Connection = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(self: *const Connection) !void {
|
pub fn reset(self: *const Connection) !void {
|
||||||
|
try libcurl.curl_easy_setopt(self.easy, .proxy, null);
|
||||||
|
try libcurl.curl_easy_setopt(self.easy, .http_header, null);
|
||||||
|
|
||||||
try libcurl.curl_easy_setopt(self.easy, .header_data, null);
|
try libcurl.curl_easy_setopt(self.easy, .header_data, null);
|
||||||
try libcurl.curl_easy_setopt(self.easy, .header_function, null);
|
try libcurl.curl_easy_setopt(self.easy, .header_function, null);
|
||||||
|
|
||||||
try libcurl.curl_easy_setopt(self.easy, .write_data, null);
|
try libcurl.curl_easy_setopt(self.easy, .write_data, null);
|
||||||
try libcurl.curl_easy_setopt(self.easy, .write_function, null);
|
try libcurl.curl_easy_setopt(self.easy, .write_function, discardBody);
|
||||||
try libcurl.curl_easy_setopt(self.easy, .proxy, null);
|
}
|
||||||
|
|
||||||
|
fn discardBody(_: [*]const u8, count: usize, len: usize, _: ?*anyopaque) usize {
|
||||||
|
return count * len;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setProxy(self: *const Connection, proxy: ?[:0]const u8) !void {
|
pub fn setProxy(self: *const Connection, proxy: ?[:0]const u8) !void {
|
||||||
|
|||||||
@@ -308,6 +308,7 @@ pub fn Reader(comptime EXPECT_MASK: bool) type {
|
|||||||
pub const WsConnection = struct {
|
pub const WsConnection = struct {
|
||||||
// CLOSE, 2 length, code
|
// CLOSE, 2 length, code
|
||||||
const CLOSE_NORMAL = [_]u8{ 136, 2, 3, 232 }; // code: 1000
|
const CLOSE_NORMAL = [_]u8{ 136, 2, 3, 232 }; // code: 1000
|
||||||
|
const CLOSE_GOING_AWAY = [_]u8{ 136, 2, 3, 233 }; // code: 1001
|
||||||
const CLOSE_TOO_BIG = [_]u8{ 136, 2, 3, 241 }; // 1009
|
const CLOSE_TOO_BIG = [_]u8{ 136, 2, 3, 241 }; // 1009
|
||||||
const CLOSE_PROTOCOL_ERROR = [_]u8{ 136, 2, 3, 234 }; //code: 1002
|
const CLOSE_PROTOCOL_ERROR = [_]u8{ 136, 2, 3, 234 }; //code: 1002
|
||||||
// "private-use" close codes must be from 4000-49999
|
// "private-use" close codes must be from 4000-49999
|
||||||
@@ -583,6 +584,10 @@ pub const WsConnection = struct {
|
|||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sendClose(self: *WsConnection) void {
|
||||||
|
self.send(&CLOSE_GOING_AWAY) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn shutdown(self: *WsConnection) void {
|
pub fn shutdown(self: *WsConnection) void {
|
||||||
posix.shutdown(self.socket, .recv) catch {};
|
posix.shutdown(self.socket, .recv) catch {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -629,10 +629,13 @@ pub fn curl_easy_setopt(easy: *Curl, comptime option: CurlOption, value: anytype
|
|||||||
.write_function => blk: {
|
.write_function => blk: {
|
||||||
const cb: c.curl_write_callback = switch (@typeInfo(@TypeOf(value))) {
|
const cb: c.curl_write_callback = switch (@typeInfo(@TypeOf(value))) {
|
||||||
.null => null,
|
.null => null,
|
||||||
.@"fn" => struct {
|
.@"fn" => |info| struct {
|
||||||
fn cb(buffer: [*c]u8, count: usize, len: usize, user: ?*anyopaque) callconv(.c) usize {
|
fn cb(buffer: [*c]u8, count: usize, len: usize, user: ?*anyopaque) callconv(.c) usize {
|
||||||
const u = user orelse unreachable;
|
const user_arg = if (@typeInfo(info.params[3].type.?) == .optional)
|
||||||
return value(@ptrCast(buffer), count, len, u);
|
user
|
||||||
|
else
|
||||||
|
user orelse unreachable;
|
||||||
|
return value(@ptrCast(buffer), count, len, user_arg);
|
||||||
}
|
}
|
||||||
}.cb,
|
}.cb,
|
||||||
else => @compileError("expected Zig function or null for " ++ @tagName(option) ++ ", got " ++ @typeName(@TypeOf(value))),
|
else => @compileError("expected Zig function or null for " ++ @tagName(option) ++ ", got " ++ @typeName(@TypeOf(value))),
|
||||||
@@ -753,6 +756,15 @@ pub fn curl_multi_poll(
|
|||||||
try errorMCheck(c.curl_multi_poll(multi, raw_fds, @intCast(extra_fds.len), timeout_ms, numfds));
|
try errorMCheck(c.curl_multi_poll(multi, raw_fds, @intCast(extra_fds.len), timeout_ms, numfds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn curl_multi_waitfds(multi: *CurlM, ufds: []CurlWaitFd, fd_count: *c_uint) ErrorMulti!void {
|
||||||
|
const raw_fds: [*c]c.curl_waitfd = if (ufds.len == 0) null else @ptrCast(ufds.ptr);
|
||||||
|
try errorMCheck(c.curl_multi_waitfds(multi, raw_fds, @intCast(ufds.len), fd_count));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn curl_multi_timeout(multi: *CurlM, timeout_ms: *c_long) ErrorMulti!void {
|
||||||
|
try errorMCheck(c.curl_multi_timeout(multi, timeout_ms));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn curl_multi_info_read(multi: *CurlM, msgs_in_queue: *c_int) ?CurlMsg {
|
pub fn curl_multi_info_read(multi: *CurlM, msgs_in_queue: *c_int) ?CurlMsg {
|
||||||
const ptr = c.curl_multi_info_read(multi, msgs_in_queue);
|
const ptr = c.curl_multi_info_read(multi, msgs_in_queue);
|
||||||
if (ptr == null) return null;
|
if (ptr == null) return null;
|
||||||
|
|||||||
@@ -2,140 +2,132 @@ const std = @import("std");
|
|||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const build_config = @import("build_config");
|
const build_config = @import("build_config");
|
||||||
|
|
||||||
const Thread = std.Thread;
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
const App = @import("../App.zig");
|
const App = @import("../App.zig");
|
||||||
const Config = @import("../Config.zig");
|
const Config = @import("../Config.zig");
|
||||||
const telemetry = @import("telemetry.zig");
|
const telemetry = @import("telemetry.zig");
|
||||||
|
const Runtime = @import("../network/Runtime.zig");
|
||||||
const Connection = @import("../network/http.zig").Connection;
|
const Connection = @import("../network/http.zig").Connection;
|
||||||
|
|
||||||
const URL = "https://telemetry.lightpanda.io";
|
const URL = "https://telemetry.lightpanda.io";
|
||||||
const MAX_BATCH_SIZE = 20;
|
const BUFFER_SIZE = 1024;
|
||||||
|
const MAX_BODY_SIZE = 500 * 1024; // 500KB server limit
|
||||||
|
|
||||||
pub const LightPanda = struct {
|
const LightPanda = @This();
|
||||||
running: bool,
|
|
||||||
thread: ?std.Thread,
|
|
||||||
allocator: Allocator,
|
|
||||||
mutex: std.Thread.Mutex,
|
|
||||||
cond: Thread.Condition,
|
|
||||||
connection: Connection,
|
|
||||||
config: *const Config,
|
|
||||||
pending: std.DoublyLinkedList,
|
|
||||||
mem_pool: std.heap.MemoryPool(LightPandaEvent),
|
|
||||||
|
|
||||||
pub fn init(app: *App) !LightPanda {
|
allocator: Allocator,
|
||||||
const connection = try app.network.newConnection();
|
runtime: *Runtime,
|
||||||
errdefer connection.deinit();
|
writer: std.Io.Writer.Allocating,
|
||||||
|
|
||||||
try connection.setURL(URL);
|
/// Protects concurrent producers in send().
|
||||||
try connection.setMethod(.POST);
|
mutex: std.Thread.Mutex = .{},
|
||||||
|
|
||||||
const allocator = app.allocator;
|
iid: ?[36]u8 = null,
|
||||||
return .{
|
run_mode: Config.RunMode = .serve,
|
||||||
.cond = .{},
|
|
||||||
.mutex = .{},
|
head: std.atomic.Value(usize) = .init(0),
|
||||||
.pending = .{},
|
tail: std.atomic.Value(usize) = .init(0),
|
||||||
.thread = null,
|
dropped: std.atomic.Value(usize) = .init(0),
|
||||||
.running = true,
|
buffer: [BUFFER_SIZE]telemetry.Event = undefined,
|
||||||
.allocator = allocator,
|
|
||||||
.connection = connection,
|
pub fn init(self: *LightPanda, app: *App, iid: ?[36]u8, run_mode: Config.RunMode) !void {
|
||||||
.config = app.config,
|
self.* = .{
|
||||||
.mem_pool = std.heap.MemoryPool(LightPandaEvent).init(allocator),
|
.iid = iid,
|
||||||
};
|
.run_mode = run_mode,
|
||||||
|
.allocator = app.allocator,
|
||||||
|
.runtime = &app.network,
|
||||||
|
.writer = std.Io.Writer.Allocating.init(app.allocator),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.runtime.onTick(@ptrCast(self), flushCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *LightPanda) void {
|
||||||
|
self.writer.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(self: *LightPanda, raw_event: telemetry.Event) !void {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
const t = self.tail.load(.monotonic);
|
||||||
|
const h = self.head.load(.acquire);
|
||||||
|
if (t - h >= BUFFER_SIZE) {
|
||||||
|
_ = self.dropped.fetchAdd(1, .monotonic);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *LightPanda) void {
|
self.buffer[t % BUFFER_SIZE] = raw_event;
|
||||||
if (self.thread) |*thread| {
|
self.tail.store(t + 1, .release);
|
||||||
self.mutex.lock();
|
}
|
||||||
self.running = false;
|
|
||||||
self.mutex.unlock();
|
fn flushCallback(ctx: *anyopaque) void {
|
||||||
self.cond.signal();
|
const self: *LightPanda = @ptrCast(@alignCast(ctx));
|
||||||
thread.join();
|
self.postEvent() catch |err| {
|
||||||
}
|
log.warn(.telemetry, "flush error", .{ .err = err });
|
||||||
self.mem_pool.deinit();
|
};
|
||||||
self.connection.deinit();
|
}
|
||||||
|
|
||||||
|
fn postEvent(self: *LightPanda) !void {
|
||||||
|
const conn = self.runtime.getConnection() orelse {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
errdefer self.runtime.releaseConnection(conn);
|
||||||
|
|
||||||
|
const h = self.head.load(.monotonic);
|
||||||
|
const t = self.tail.load(.acquire);
|
||||||
|
const dropped = self.dropped.swap(0, .monotonic);
|
||||||
|
|
||||||
|
if (h == t and dropped == 0) {
|
||||||
|
self.runtime.releaseConnection(conn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
errdefer _ = self.dropped.fetchAdd(dropped, .monotonic);
|
||||||
|
|
||||||
|
self.writer.clearRetainingCapacity();
|
||||||
|
|
||||||
|
if (dropped > 0) {
|
||||||
|
_ = try self.writeEvent(.{ .buffer_overflow = .{ .dropped = dropped } });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(self: *LightPanda, iid: ?[]const u8, run_mode: Config.RunMode, raw_event: telemetry.Event) !void {
|
var sent: usize = 0;
|
||||||
const event = try self.mem_pool.create();
|
for (h..t) |i| {
|
||||||
event.* = .{
|
const fit = try self.writeEvent(self.buffer[i % BUFFER_SIZE]);
|
||||||
.iid = iid,
|
if (!fit) break;
|
||||||
.mode = run_mode,
|
|
||||||
.event = raw_event,
|
|
||||||
.node = .{},
|
|
||||||
};
|
|
||||||
|
|
||||||
self.mutex.lock();
|
sent += 1;
|
||||||
defer self.mutex.unlock();
|
|
||||||
if (self.thread == null) {
|
|
||||||
self.thread = try std.Thread.spawn(.{}, run, .{self});
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pending.append(&event.node);
|
|
||||||
self.cond.signal();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(self: *LightPanda) void {
|
try conn.setURL(URL);
|
||||||
var aw = std.Io.Writer.Allocating.init(self.allocator);
|
try conn.setMethod(.POST);
|
||||||
defer aw.deinit();
|
try conn.setBody(self.writer.written());
|
||||||
|
|
||||||
var batch: [MAX_BATCH_SIZE]*LightPandaEvent = undefined;
|
self.head.store(h + sent, .release);
|
||||||
self.mutex.lock();
|
self.runtime.submitRequest(conn);
|
||||||
while (true) {
|
}
|
||||||
while (self.pending.first != null) {
|
|
||||||
const b = self.collectBatch(&batch);
|
fn writeEvent(self: *LightPanda, event: telemetry.Event) !bool {
|
||||||
self.mutex.unlock();
|
const iid: ?[]const u8 = if (self.iid) |*id| id else null;
|
||||||
self.postEvent(b, &aw) catch |err| {
|
const wrapped = LightPandaEvent{ .iid = iid, .mode = self.run_mode, .event = event };
|
||||||
log.warn(.telemetry, "post error", .{ .err = err });
|
|
||||||
};
|
const checkpoint = self.writer.written().len;
|
||||||
self.mutex.lock();
|
|
||||||
}
|
try std.json.Stringify.value(&wrapped, .{ .emit_null_optional_fields = false }, &self.writer.writer);
|
||||||
if (self.running == false) {
|
try self.writer.writer.writeByte('\n');
|
||||||
return;
|
|
||||||
}
|
if (self.writer.written().len > MAX_BODY_SIZE) {
|
||||||
self.cond.wait(&self.mutex);
|
self.writer.shrinkRetainingCapacity(checkpoint);
|
||||||
}
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
fn postEvent(self: *LightPanda, events: []*LightPandaEvent, aw: *std.Io.Writer.Allocating) !void {
|
}
|
||||||
defer for (events) |e| {
|
|
||||||
self.mem_pool.destroy(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
defer aw.clearRetainingCapacity();
|
|
||||||
for (events) |event| {
|
|
||||||
try std.json.Stringify.value(event, .{ .emit_null_optional_fields = false }, &aw.writer);
|
|
||||||
try aw.writer.writeByte('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.connection.setBody(aw.written());
|
|
||||||
const status = try self.connection.request(&self.config.http_headers);
|
|
||||||
|
|
||||||
if (status != 200) {
|
|
||||||
log.warn(.telemetry, "server error", .{ .status = status });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collectBatch(self: *LightPanda, into: []*LightPandaEvent) []*LightPandaEvent {
|
|
||||||
var i: usize = 0;
|
|
||||||
while (self.pending.popFirst()) |node| {
|
|
||||||
into[i] = @fieldParentPtr("node", node);
|
|
||||||
i += 1;
|
|
||||||
if (i == MAX_BATCH_SIZE) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return into[0..i];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const LightPandaEvent = struct {
|
const LightPandaEvent = struct {
|
||||||
iid: ?[]const u8,
|
iid: ?[]const u8,
|
||||||
mode: Config.RunMode,
|
mode: Config.RunMode,
|
||||||
event: telemetry.Event,
|
event: telemetry.Event,
|
||||||
node: std.DoublyLinkedList.Node,
|
|
||||||
|
|
||||||
pub fn jsonStringify(self: *const LightPandaEvent, writer: anytype) !void {
|
pub fn jsonStringify(self: *const LightPandaEvent, writer: anytype) !void {
|
||||||
try writer.beginObject();
|
try writer.beginObject();
|
||||||
@@ -153,7 +145,7 @@ const LightPandaEvent = struct {
|
|||||||
try writer.write(builtin.cpu.arch);
|
try writer.write(builtin.cpu.arch);
|
||||||
|
|
||||||
try writer.objectField("version");
|
try writer.objectField("version");
|
||||||
try writer.write(build_config.git_commit);
|
try writer.write(build_config.git_version orelse build_config.git_commit);
|
||||||
|
|
||||||
try writer.objectField("event");
|
try writer.objectField("event");
|
||||||
try writer.write(@tagName(std.meta.activeTag(self.event)));
|
try writer.write(@tagName(std.meta.activeTag(self.event)));
|
||||||
|
|||||||
@@ -11,26 +11,21 @@ const uuidv4 = @import("../id.zig").uuidv4;
|
|||||||
const IID_FILE = "iid";
|
const IID_FILE = "iid";
|
||||||
|
|
||||||
pub fn isDisabled() bool {
|
pub fn isDisabled() bool {
|
||||||
|
if (builtin.mode == .Debug or builtin.is_test) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return std.process.hasEnvVarConstant("LIGHTPANDA_DISABLE_TELEMETRY");
|
return std.process.hasEnvVarConstant("LIGHTPANDA_DISABLE_TELEMETRY");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Telemetry = TelemetryT(blk: {
|
pub const Telemetry = TelemetryT(@import("lightpanda.zig"));
|
||||||
if (builtin.mode == .Debug or builtin.is_test) break :blk NoopProvider;
|
|
||||||
break :blk @import("lightpanda.zig").LightPanda;
|
|
||||||
});
|
|
||||||
|
|
||||||
fn TelemetryT(comptime P: type) type {
|
fn TelemetryT(comptime P: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
// an "install" id that we [try to] persist and re-use between runs
|
provider: *P,
|
||||||
// null on IO error
|
|
||||||
iid: ?[36]u8,
|
|
||||||
|
|
||||||
provider: P,
|
|
||||||
|
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
|
|
||||||
run_mode: Config.RunMode,
|
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
pub fn init(app: *App, run_mode: Config.RunMode) !Self {
|
pub fn init(app: *App, run_mode: Config.RunMode) !Self {
|
||||||
@@ -39,27 +34,29 @@ fn TelemetryT(comptime P: type) type {
|
|||||||
log.info(.telemetry, "telemetry status", .{ .disabled = disabled });
|
log.info(.telemetry, "telemetry status", .{ .disabled = disabled });
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = try P.init(app);
|
const iid: ?[36]u8 = if (disabled) null else getOrCreateId(app.app_dir_path);
|
||||||
errdefer provider.deinit();
|
|
||||||
|
const provider = try app.allocator.create(P);
|
||||||
|
errdefer app.allocator.destroy(provider);
|
||||||
|
|
||||||
|
try P.init(provider, app, iid, run_mode);
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.disabled = disabled,
|
.disabled = disabled,
|
||||||
.run_mode = run_mode,
|
|
||||||
.provider = provider,
|
.provider = provider,
|
||||||
.iid = if (disabled) null else getOrCreateId(app.app_dir_path),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self, allocator: Allocator) void {
|
||||||
self.provider.deinit();
|
self.provider.deinit();
|
||||||
|
allocator.destroy(self.provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record(self: *Self, event: Event) void {
|
pub fn record(self: *Self, event: Event) void {
|
||||||
if (self.disabled) {
|
if (self.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const iid: ?[]const u8 = if (self.iid) |*iid| iid else null;
|
self.provider.send(event) catch |err| {
|
||||||
self.provider.send(iid, self.run_mode, event) catch |err| {
|
|
||||||
log.warn(.telemetry, "record error", .{ .err = err, .type = @tagName(std.meta.activeTag(event)) });
|
log.warn(.telemetry, "record error", .{ .err = err, .type = @tagName(std.meta.activeTag(event)) });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -105,6 +102,7 @@ fn getOrCreateId(app_dir_path_: ?[]const u8) ?[36]u8 {
|
|||||||
pub const Event = union(enum) {
|
pub const Event = union(enum) {
|
||||||
run: void,
|
run: void,
|
||||||
navigate: Navigate,
|
navigate: Navigate,
|
||||||
|
buffer_overflow: BufferOverflow,
|
||||||
flag: []const u8, // used for testing
|
flag: []const u8, // used for testing
|
||||||
|
|
||||||
const Navigate = struct {
|
const Navigate = struct {
|
||||||
@@ -112,36 +110,35 @@ pub const Event = union(enum) {
|
|||||||
proxy: bool,
|
proxy: bool,
|
||||||
driver: []const u8 = "cdp",
|
driver: []const u8 = "cdp",
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const NoopProvider = struct {
|
const BufferOverflow = struct {
|
||||||
fn init(_: *App) !NoopProvider {
|
dropped: usize,
|
||||||
return .{};
|
};
|
||||||
}
|
|
||||||
fn deinit(_: NoopProvider) void {}
|
|
||||||
pub fn send(_: NoopProvider, _: ?[]const u8, _: Config.RunMode, _: Event) !void {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern fn setenv(name: [*:0]u8, value: [*:0]u8, override: c_int) c_int;
|
extern fn setenv(name: [*:0]u8, value: [*:0]u8, override: c_int) c_int;
|
||||||
extern fn unsetenv(name: [*:0]u8) c_int;
|
extern fn unsetenv(name: [*:0]u8) c_int;
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
test "telemetry: disabled by environment" {
|
test "telemetry: always disabled in debug builds" {
|
||||||
|
// Must be disabled regardless of environment variable.
|
||||||
|
_ = unsetenv(@constCast("LIGHTPANDA_DISABLE_TELEMETRY"));
|
||||||
|
try testing.expectEqual(true, isDisabled());
|
||||||
|
|
||||||
_ = setenv(@constCast("LIGHTPANDA_DISABLE_TELEMETRY"), @constCast(""), 0);
|
_ = setenv(@constCast("LIGHTPANDA_DISABLE_TELEMETRY"), @constCast(""), 0);
|
||||||
defer _ = unsetenv(@constCast("LIGHTPANDA_DISABLE_TELEMETRY"));
|
defer _ = unsetenv(@constCast("LIGHTPANDA_DISABLE_TELEMETRY"));
|
||||||
|
try testing.expectEqual(true, isDisabled());
|
||||||
|
|
||||||
const FailingProvider = struct {
|
const FailingProvider = struct {
|
||||||
fn init(_: *App) !@This() {
|
fn init(_: *@This(), _: *App, _: ?[36]u8, _: Config.RunMode) !void {}
|
||||||
return .{};
|
fn deinit(_: *@This()) void {}
|
||||||
}
|
pub fn send(_: *@This(), _: Event) !void {
|
||||||
fn deinit(_: @This()) void {}
|
|
||||||
pub fn send(_: @This(), _: ?[]const u8, _: Config.RunMode, _: Event) !void {
|
|
||||||
unreachable;
|
unreachable;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var telemetry = try TelemetryT(FailingProvider).init(undefined, .serve);
|
var telemetry = try TelemetryT(FailingProvider).init(testing.test_app, .serve);
|
||||||
defer telemetry.deinit();
|
defer telemetry.deinit(testing.test_app.allocator);
|
||||||
telemetry.record(.{ .run = {} });
|
telemetry.record(.{ .run = {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +162,9 @@ test "telemetry: getOrCreateId" {
|
|||||||
|
|
||||||
test "telemetry: sends event to provider" {
|
test "telemetry: sends event to provider" {
|
||||||
var telemetry = try TelemetryT(MockProvider).init(testing.test_app, .serve);
|
var telemetry = try TelemetryT(MockProvider).init(testing.test_app, .serve);
|
||||||
defer telemetry.deinit();
|
defer telemetry.deinit(testing.test_app.allocator);
|
||||||
const mock = &telemetry.provider;
|
telemetry.disabled = false;
|
||||||
|
const mock = telemetry.provider;
|
||||||
|
|
||||||
telemetry.record(.{ .flag = "1" });
|
telemetry.record(.{ .flag = "1" });
|
||||||
telemetry.record(.{ .flag = "2" });
|
telemetry.record(.{ .flag = "2" });
|
||||||
@@ -179,15 +177,11 @@ test "telemetry: sends event to provider" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MockProvider = struct {
|
const MockProvider = struct {
|
||||||
iid: ?[]const u8,
|
|
||||||
run_mode: ?Config.RunMode,
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
events: std.ArrayList(Event),
|
events: std.ArrayList(Event),
|
||||||
|
|
||||||
fn init(app: *App) !@This() {
|
fn init(self: *MockProvider, app: *App, _: ?[36]u8, _: Config.RunMode) !void {
|
||||||
return .{
|
self.* = .{
|
||||||
.iid = null,
|
|
||||||
.run_mode = null,
|
|
||||||
.events = .{},
|
.events = .{},
|
||||||
.allocator = app.allocator,
|
.allocator = app.allocator,
|
||||||
};
|
};
|
||||||
@@ -195,15 +189,7 @@ const MockProvider = struct {
|
|||||||
fn deinit(self: *MockProvider) void {
|
fn deinit(self: *MockProvider) void {
|
||||||
self.events.deinit(self.allocator);
|
self.events.deinit(self.allocator);
|
||||||
}
|
}
|
||||||
pub fn send(self: *MockProvider, iid: ?[]const u8, run_mode: Config.RunMode, events: Event) !void {
|
pub fn send(self: *MockProvider, event: Event) !void {
|
||||||
if (self.iid == null) {
|
try self.events.append(self.allocator, event);
|
||||||
try testing.expectEqual(null, self.run_mode);
|
|
||||||
self.iid = iid.?;
|
|
||||||
self.run_mode = run_mode;
|
|
||||||
} else {
|
|
||||||
try testing.expectEqual(self.iid.?, iid.?);
|
|
||||||
try testing.expectEqual(self.run_mode.?, run_mode);
|
|
||||||
}
|
|
||||||
try self.events.append(self.allocator, events);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -616,12 +616,12 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
|
|||||||
pub const LogFilter = struct {
|
pub const LogFilter = struct {
|
||||||
old_filter: []const log.Scope,
|
old_filter: []const log.Scope,
|
||||||
|
|
||||||
/// Sets the log filter to only include the specified scope.
|
/// Sets the log filter to suppress the specified scope(s).
|
||||||
/// Returns a LogFilter that should be deinitialized to restore previous filters.
|
/// Returns a LogFilter that should be deinitialized to restore previous filters.
|
||||||
pub fn init(comptime scope: log.Scope) LogFilter {
|
pub fn init(comptime scopes: []const log.Scope) LogFilter {
|
||||||
|
comptime std.debug.assert(@TypeOf(scopes) == []const log.Scope);
|
||||||
const old_filter = log.opts.filter_scopes;
|
const old_filter = log.opts.filter_scopes;
|
||||||
const new_filter = comptime &[_]log.Scope{scope};
|
log.opts.filter_scopes = scopes;
|
||||||
log.opts.filter_scopes = new_filter;
|
|
||||||
return .{ .old_filter = old_filter };
|
return .{ .old_filter = old_filter };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user