mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-29 16:10:04 +00:00
Compare commits
129 Commits
navigate-e
...
v0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d0126d953 | ||
|
|
b79193f621 | ||
|
|
1d0f38b29f | ||
|
|
8fcf12f74c | ||
|
|
c26938c333 | ||
|
|
c9394fbc43 | ||
|
|
a6cc21b449 | ||
|
|
e072ff3c4a | ||
|
|
5e4e4dcbc6 | ||
|
|
beef458c3c | ||
|
|
1dcccef080 | ||
|
|
66342b35db | ||
|
|
0efab26c7b | ||
|
|
85bf8669dd | ||
|
|
a69efb9d3f | ||
|
|
e97c9959fa | ||
|
|
68e9d3b9ea | ||
|
|
0c1c26462c | ||
|
|
ce85fa53b0 | ||
|
|
d8bbaff506 | ||
|
|
447ef83e0a | ||
|
|
6d4966e83d | ||
|
|
42440f1503 | ||
|
|
26827efe34 | ||
|
|
e2682ab9fe | ||
|
|
34518dfa98 | ||
|
|
9579f727b3 | ||
|
|
7c976209cc | ||
|
|
e76b9936ea | ||
|
|
b0daf2f96e | ||
|
|
d2e7c41d67 | ||
|
|
2a0c8f01b9 | ||
|
|
83378a68c8 | ||
|
|
5382e59d71 | ||
|
|
bb7da6aafb | ||
|
|
f7fd68ca3d | ||
|
|
1ab6659c04 | ||
|
|
4893a79d37 | ||
|
|
00d6195590 | ||
|
|
100b2a6a95 | ||
|
|
b317bf7854 | ||
|
|
dea6156a2b | ||
|
|
d8d07fb095 | ||
|
|
a8437afadd | ||
|
|
1fd61ce6a4 | ||
|
|
ea757407f5 | ||
|
|
00e18e24b9 | ||
|
|
1927a16089 | ||
|
|
35da652a5d | ||
|
|
ed3a562d84 | ||
|
|
fd5fbe3ea1 | ||
|
|
641c6c3f42 | ||
|
|
cdd7399016 | ||
|
|
74eee75e47 | ||
|
|
2e45d547c2 | ||
|
|
28e1d6e8c8 | ||
|
|
8837193643 | ||
|
|
c5ab10cf43 | ||
|
|
90f6495e93 | ||
|
|
4cbd1da749 | ||
|
|
9477a8be42 | ||
|
|
b0f0df5632 | ||
|
|
d2c90486da | ||
|
|
3d7801df05 | ||
|
|
c962858f61 | ||
|
|
b0d9ebaf3a | ||
|
|
9881a4d288 | ||
|
|
96e80cc2fc | ||
|
|
7887ca6a45 | ||
|
|
633aee9439 | ||
|
|
27a85c1241 | ||
|
|
2e4996d6c9 | ||
|
|
3f8ad1ae35 | ||
|
|
5c71e0f93b | ||
|
|
a124f5caa9 | ||
|
|
96a53c4e97 | ||
|
|
927cbe7b11 | ||
|
|
b365ffcc8d | ||
|
|
9d6bc5b615 | ||
|
|
2b2882c76d | ||
|
|
f058cf0697 | ||
|
|
346ae14bcd | ||
|
|
c30de2bb32 | ||
|
|
5e43f76a0a | ||
|
|
2b4409248e | ||
|
|
21464dfa55 | ||
|
|
cf7bddd887 | ||
|
|
455fe5d2ba | ||
|
|
b764a7a0dc | ||
|
|
b776cf1647 | ||
|
|
4c37a8e766 | ||
|
|
707db8173f | ||
|
|
1412c5821c | ||
|
|
4f236d0b30 | ||
|
|
b18ec4dee3 | ||
|
|
0e3f8c9e42 | ||
|
|
c4bf37fb5b | ||
|
|
4fc09eccdf | ||
|
|
67f979be77 | ||
|
|
f475f3440e | ||
|
|
56e30a9c97 | ||
|
|
d3522e0e36 | ||
|
|
5417a8d9b0 | ||
|
|
d15a384f9a | ||
|
|
f419f05a5e | ||
|
|
c2827a0f16 | ||
|
|
263dab0bdf | ||
|
|
3c98e4f71e | ||
|
|
73574dce52 | ||
|
|
c459325a5f | ||
|
|
37ac465695 | ||
|
|
a8298a0fda | ||
|
|
7404b20228 | ||
|
|
b782cc6389 | ||
|
|
4538464df4 | ||
|
|
9081a813e7 | ||
|
|
0dfd5ce940 | ||
|
|
2bbbb4662e | ||
|
|
a651c0a2d1 | ||
|
|
5174212183 | ||
|
|
d48a6619a3 | ||
|
|
dd079f0c0e | ||
|
|
d193ab6dc0 | ||
|
|
4872aabc87 | ||
|
|
c4380b91f4 | ||
|
|
3f2f56d603 | ||
|
|
0a705b15ce | ||
|
|
4cf61d101c | ||
|
|
d0d2850458 |
16
.github/actions/install/action.yml
vendored
16
.github/actions/install/action.yml
vendored
@@ -2,10 +2,6 @@ name: "Browsercore install"
|
|||||||
description: "Install deps for the project browsercore"
|
description: "Install deps for the project browsercore"
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
zig:
|
|
||||||
description: 'Zig version to install'
|
|
||||||
required: false
|
|
||||||
default: '0.15.2'
|
|
||||||
arch:
|
arch:
|
||||||
description: 'CPU arch used to select the v8 lib'
|
description: 'CPU arch used to select the v8 lib'
|
||||||
required: false
|
required: false
|
||||||
@@ -17,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.1.33'
|
default: 'v0.1.35'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
@@ -38,9 +34,8 @@ runs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
|
sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
|
||||||
|
|
||||||
|
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
||||||
- uses: mlugg/setup-zig@v2
|
- uses: mlugg/setup-zig@v2
|
||||||
with:
|
|
||||||
version: ${{ inputs.zig }}
|
|
||||||
|
|
||||||
- name: Cache v8
|
- name: Cache v8
|
||||||
id: cache-v8
|
id: cache-v8
|
||||||
@@ -61,11 +56,8 @@ runs:
|
|||||||
- name: install v8
|
- name: install v8
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p v8/out/${{ inputs.os }}/debug/obj/zig/
|
mkdir -p v8
|
||||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/debug/obj/zig/libc_v8.a
|
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8.a
|
||||||
|
|
||||||
mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/
|
|
||||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a
|
|
||||||
|
|
||||||
- name: Cache libiconv
|
- name: Cache libiconv
|
||||||
id: cache-libiconv
|
id: cache-libiconv
|
||||||
|
|||||||
30
.github/workflows/build.yml
vendored
30
.github/workflows/build.yml
vendored
@@ -5,8 +5,12 @@ 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' }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "2 2 * * *"
|
- cron: "2 2 * * *"
|
||||||
|
|
||||||
@@ -26,10 +30,9 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
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
|
submodules: recursive
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
@@ -38,7 +41,7 @@ jobs:
|
|||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: Rename binary
|
- 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 }}
|
||||||
@@ -53,7 +56,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: nightly
|
tag: ${{ env.RELEASE }}
|
||||||
|
|
||||||
build-linux-aarch64:
|
build-linux-aarch64:
|
||||||
env:
|
env:
|
||||||
@@ -76,7 +79,7 @@ jobs:
|
|||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: Rename binary
|
- 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 }}
|
||||||
@@ -91,7 +94,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: nightly
|
tag: ${{ env.RELEASE }}
|
||||||
|
|
||||||
build-macos-aarch64:
|
build-macos-aarch64:
|
||||||
env:
|
env:
|
||||||
@@ -116,7 +119,7 @@ jobs:
|
|||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: Rename binary
|
- 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 }}
|
||||||
@@ -131,19 +134,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: nightly
|
tag: ${{ env.RELEASE }}
|
||||||
|
|
||||||
build-macos-x86_64:
|
build-macos-x86_64:
|
||||||
env:
|
env:
|
||||||
ARCH: x86_64
|
ARCH: x86_64
|
||||||
OS: macos
|
OS: macos
|
||||||
|
|
||||||
# macos-13 runs on x86 CPU. see
|
runs-on: macos-14-large
|
||||||
# https://github.com/actions/runner-images?tab=readme-ov-file
|
|
||||||
# If we want to build for macos-14 or superior, we need to switch to
|
|
||||||
# macos-14-large.
|
|
||||||
# No need for now, but maybe we will need it in the short term.
|
|
||||||
runs-on: macos-13
|
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -159,7 +157,7 @@ jobs:
|
|||||||
arch: ${{env.ARCH}}
|
arch: ${{env.ARCH}}
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: Rename binary
|
- 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 }}
|
||||||
@@ -174,4 +172,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
tag: nightly
|
tag: ${{ env.RELEASE }}
|
||||||
|
|||||||
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'
|
path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'
|
||||||
# branch should not be protected
|
# branch should not be protected
|
||||||
branch: 'main'
|
branch: 'main'
|
||||||
allowlist: krichprollsch,francisbouvier,katie-lpd,sjorsdonkers,bornlex
|
allowlist: krichprollsch,francisbouvier,katie-lpd
|
||||||
|
|
||||||
remote-organization-name: lightpanda-io
|
remote-organization-name: lightpanda-io
|
||||||
remote-repository-name: cla
|
remote-repository-name: cla
|
||||||
|
|||||||
68
.github/workflows/e2e-integration-test.yml
vendored
Normal file
68
.github/workflows/e2e-integration-test.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
name: e2e-integration-test
|
||||||
|
|
||||||
|
env:
|
||||||
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "4 4 * * *"
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
zig-build-release:
|
||||||
|
name: zig build release
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
# Don't run the CI with draft PR.
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
|
- 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 }})
|
||||||
|
|
||||||
|
- name: upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
path: |
|
||||||
|
zig-out/bin/lightpanda
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
demo-scripts:
|
||||||
|
name: demo-integration-scripts
|
||||||
|
needs: zig-build-release
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: 'lightpanda-io/demo'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- run: npm install
|
||||||
|
|
||||||
|
- name: download artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
- name: run end to end integration tests
|
||||||
|
run: |
|
||||||
|
./lightpanda serve & echo $! > LPD.pid
|
||||||
|
go run integration/main.go
|
||||||
|
kill `cat LPD.pid`
|
||||||
7
.github/workflows/e2e-test.yml
vendored
7
.github/workflows/e2e-test.yml
vendored
@@ -49,16 +49,15 @@ 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
|
submodules: recursive
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: zig build release
|
- name: zig build release
|
||||||
run: zig build -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@v4
|
||||||
@@ -122,7 +121,7 @@ jobs:
|
|||||||
needs: zig-build-release
|
needs: zig-build-release
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MAX_MEMORY: 27000
|
MAX_MEMORY: 28000
|
||||||
MAX_AVG_DURATION: 23
|
MAX_AVG_DURATION: 23
|
||||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
|
|||||||
3
.github/workflows/wpt.yml
vendored
3
.github/workflows/wpt.yml
vendored
@@ -22,10 +22,9 @@ jobs:
|
|||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
|
|
||||||
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
|
submodules: recursive
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|||||||
13
.github/workflows/zig-fmt.yml
vendored
13
.github/workflows/zig-fmt.yml
vendored
@@ -1,8 +1,5 @@
|
|||||||
name: zig-fmt
|
name: zig-fmt
|
||||||
|
|
||||||
env:
|
|
||||||
ZIG_VERSION: 0.15.2
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
@@ -32,14 +29,13 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: mlugg/setup-zig@v2
|
- uses: actions/checkout@v6
|
||||||
with:
|
|
||||||
version: ${{ env.ZIG_VERSION }}
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
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
|
- name: Run zig fmt
|
||||||
id: fmt
|
id: fmt
|
||||||
run: |
|
run: |
|
||||||
@@ -58,6 +54,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "${delimiter}" >> "${GITHUB_OUTPUT}"
|
echo "${delimiter}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Fail the job
|
- name: Fail the job
|
||||||
if: steps.fmt.outputs.zig_fmt_errs != ''
|
if: steps.fmt.outputs.zig_fmt_errs != ''
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|||||||
7
.github/workflows/zig-test.yml
vendored
7
.github/workflows/zig-test.yml
vendored
@@ -47,16 +47,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
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
|
submodules: recursive
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: zig build debug
|
- name: zig build debug
|
||||||
run: zig build
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -104,7 +103,7 @@ jobs:
|
|||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
- name: zig build test
|
- name: zig build test
|
||||||
run: zig build test -- --json > bench.json
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a test -- --json > bench.json
|
||||||
|
|
||||||
- name: write commit
|
- name: write commit
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
zig-cache
|
zig-cache
|
||||||
/.zig-cache/
|
/.zig-cache/
|
||||||
|
/.lp-cache/
|
||||||
zig-out
|
zig-out
|
||||||
/vendor/netsurf/out
|
/vendor/netsurf/out
|
||||||
/vendor/libiconv/
|
/vendor/libiconv/
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -22,9 +22,6 @@
|
|||||||
[submodule "vendor/nghttp2"]
|
[submodule "vendor/nghttp2"]
|
||||||
path = vendor/nghttp2
|
path = vendor/nghttp2
|
||||||
url = https://github.com/nghttp2/nghttp2.git
|
url = https://github.com/nghttp2/nghttp2.git
|
||||||
[submodule "vendor/mbedtls"]
|
|
||||||
path = vendor/mbedtls
|
|
||||||
url = https://github.com/Mbed-TLS/mbedtls.git
|
|
||||||
[submodule "vendor/zlib"]
|
[submodule "vendor/zlib"]
|
||||||
path = vendor/zlib
|
path = vendor/zlib
|
||||||
url = https://github.com/madler/zlib.git
|
url = https://github.com/madler/zlib.git
|
||||||
|
|||||||
51
Dockerfile
51
Dockerfile
@@ -1,10 +1,9 @@
|
|||||||
FROM debian:stable
|
FROM debian:stable-slim
|
||||||
|
|
||||||
ARG MINISIG=0.12
|
ARG MINISIG=0.12
|
||||||
ARG ZIG=0.15.2
|
|
||||||
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.1.33
|
ARG ZIG_V8=v0.1.34
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update -yq && \
|
RUN apt-get update -yq && \
|
||||||
@@ -17,25 +16,25 @@ RUN apt-get update -yq && \
|
|||||||
|
|
||||||
# install minisig
|
# install minisig
|
||||||
RUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${MINISIG}/minisign-${MINISIG}-linux.tar.gz && \
|
RUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${MINISIG}/minisign-${MINISIG}-linux.tar.gz && \
|
||||||
tar xvzf minisign-${MINISIG}-linux.tar.gz
|
tar xvzf minisign-${MINISIG}-linux.tar.gz -C /
|
||||||
|
|
||||||
# install zig
|
|
||||||
RUN case $TARGETPLATFORM in \
|
|
||||||
"linux/arm64") ARCH="aarch64" ;; \
|
|
||||||
*) ARCH="x86_64" ;; \
|
|
||||||
esac && \
|
|
||||||
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
|
||||||
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig && \
|
|
||||||
minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG} && \
|
|
||||||
tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
|
||||||
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
|
|
||||||
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
|
|
||||||
|
|
||||||
# clone lightpanda
|
# clone lightpanda
|
||||||
RUN git clone https://github.com/lightpanda-io/browser.git
|
RUN git clone https://github.com/lightpanda-io/browser.git
|
||||||
|
|
||||||
WORKDIR /browser
|
WORKDIR /browser
|
||||||
|
|
||||||
|
# install zig
|
||||||
|
RUN ZIG=$(grep '\.minimum_zig_version = "' "build.zig.zon" | cut -d'"' -f2) && \
|
||||||
|
case $TARGETPLATFORM in \
|
||||||
|
"linux/arm64") ARCH="aarch64" ;; \
|
||||||
|
*) ARCH="x86_64" ;; \
|
||||||
|
esac && \
|
||||||
|
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
||||||
|
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig && \
|
||||||
|
/minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG} && \
|
||||||
|
tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
||||||
|
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
|
||||||
|
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
|
||||||
|
|
||||||
# install deps
|
# install deps
|
||||||
RUN git submodule init && \
|
RUN git submodule init && \
|
||||||
git submodule update --recursive
|
git submodule update --recursive
|
||||||
@@ -50,11 +49,16 @@ RUN case $TARGETPLATFORM in \
|
|||||||
*) ARCH="x86_64" ;; \
|
*) ARCH="x86_64" ;; \
|
||||||
esac && \
|
esac && \
|
||||||
curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
|
curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
|
||||||
mkdir -p v8/out/linux/release/obj/zig/ && \
|
mkdir -p v8/ && \
|
||||||
mv libc_v8.a v8/out/linux/release/obj/zig/libc_v8.a
|
mv libc_v8.a v8/libc_v8.a
|
||||||
|
|
||||||
# build release
|
# build release
|
||||||
RUN make build
|
RUN zig build -Doptimize=ReleaseSafe -Dprebuilt_v8_path=v8/libc_v8.a -Dgit_commit=$$(git rev-parse --short HEAD)
|
||||||
|
|
||||||
|
FROM debian:stable-slim
|
||||||
|
|
||||||
|
RUN apt-get update -yq && \
|
||||||
|
apt-get install -yq tini
|
||||||
|
|
||||||
FROM debian:stable-slim
|
FROM debian:stable-slim
|
||||||
|
|
||||||
@@ -62,7 +66,12 @@ FROM debian:stable-slim
|
|||||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
|
|
||||||
COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
|
COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
|
||||||
|
COPY --from=1 /usr/bin/tini /usr/bin/tini
|
||||||
|
|
||||||
EXPOSE 9222/tcp
|
EXPOSE 9222/tcp
|
||||||
|
|
||||||
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222"]
|
# Lightpanda install only some signal handlers, and PID 1 doesn't have a default SIGTERM signal handler.
|
||||||
|
# Using "tini" as PID1 ensures that signals work as expected, so e.g. "docker stop" will not hang.
|
||||||
|
# (See https://github.com/krallin/tini#why-tini).
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
|
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222", "--log_level", "info"]
|
||||||
|
|||||||
80
Makefile
80
Makefile
@@ -34,7 +34,7 @@ endif
|
|||||||
|
|
||||||
## Display this help screen
|
## Display this help screen
|
||||||
help:
|
help:
|
||||||
@printf "\e[36m%-35s %s\e[0m\n" "Command" "Usage"
|
@printf "\033[36m%-35s %s\033[0m\n" "Command" "Usage"
|
||||||
@sed -n -e '/^## /{'\
|
@sed -n -e '/^## /{'\
|
||||||
-e 's/## //g;'\
|
-e 's/## //g;'\
|
||||||
-e 'h;'\
|
-e 'h;'\
|
||||||
@@ -47,54 +47,43 @@ help:
|
|||||||
|
|
||||||
# $(ZIG) commands
|
# $(ZIG) commands
|
||||||
# ------------
|
# ------------
|
||||||
.PHONY: build build-dev run run-release shell test bench download-zig wpt data get-v8 build-v8 build-v8-dev
|
.PHONY: build build-dev run run-release shell test bench wpt data end2end
|
||||||
.PHONY: end2end
|
|
||||||
|
|
||||||
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
|
|
||||||
|
|
||||||
## Download the zig recommended version
|
|
||||||
download-zig:
|
|
||||||
$(eval url = "https://ziglang.org/download/$(zig_version)/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
|
|
||||||
$(eval dest = "/tmp/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
|
|
||||||
@printf "\e[36mDownload zig version $(zig_version)...\e[0m\n"
|
|
||||||
@curl -o "$(dest)" -L "$(url)" || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
|
||||||
@printf "\e[33mDownloaded $(dest)\e[0m\n"
|
|
||||||
|
|
||||||
## Build in release-safe mode
|
## Build in release-safe mode
|
||||||
build:
|
build:
|
||||||
@printf "\e[36mBuilding (release safe)...\e[0m\n"
|
@printf "\033[36mBuilding (release safe)...\033[0m\n"
|
||||||
$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
@printf "\e[33mBuild OK\e[0m\n"
|
@printf "\033[33mBuild OK\033[0m\n"
|
||||||
|
|
||||||
## Build in debug mode
|
## Build in debug mode
|
||||||
build-dev:
|
build-dev:
|
||||||
@printf "\e[36mBuilding (debug)...\e[0m\n"
|
@printf "\033[36mBuilding (debug)...\033[0m\n"
|
||||||
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
@printf "\e[33mBuild OK\e[0m\n"
|
@printf "\033[33mBuild OK\033[0m\n"
|
||||||
|
|
||||||
## Run the server in release mode
|
## Run the server in release mode
|
||||||
run: build
|
run: build
|
||||||
@printf "\e[36mRunning...\e[0m\n"
|
@printf "\033[36mRunning...\033[0m\n"
|
||||||
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
|
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
|
||||||
|
|
||||||
## Run the server in debug mode
|
## Run the server in debug mode
|
||||||
run-debug: build-dev
|
run-debug: build-dev
|
||||||
@printf "\e[36mRunning...\e[0m\n"
|
@printf "\033[36mRunning...\033[0m\n"
|
||||||
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
|
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
|
||||||
|
|
||||||
## Run a JS shell in debug mode
|
## Run a JS shell in debug mode
|
||||||
shell:
|
shell:
|
||||||
@printf "\e[36mBuilding shell...\e[0m\n"
|
@printf "\033[36mBuilding shell...\033[0m\n"
|
||||||
@$(ZIG) build shell || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
|
|
||||||
## Run WPT tests
|
## Run WPT tests
|
||||||
wpt:
|
wpt:
|
||||||
@printf "\e[36mBuilding wpt...\e[0m\n"
|
@printf "\033[36mBuilding wpt...\033[0m\n"
|
||||||
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||||
|
|
||||||
wpt-summary:
|
wpt-summary:
|
||||||
@printf "\e[36mBuilding wpt...\e[0m\n"
|
@printf "\033[36mBuilding wpt...\033[0m\n"
|
||||||
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\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)
|
||||||
@@ -112,19 +101,6 @@ end2end:
|
|||||||
@test -d ../demo
|
@test -d ../demo
|
||||||
cd ../demo && go run runner/main.go
|
cd ../demo && go run runner/main.go
|
||||||
|
|
||||||
## v8
|
|
||||||
get-v8:
|
|
||||||
@printf "\e[36mGetting v8 source...\e[0m\n"
|
|
||||||
@$(ZIG) build get-v8
|
|
||||||
|
|
||||||
build-v8-dev:
|
|
||||||
@printf "\e[36mBuilding v8 (dev)...\e[0m\n"
|
|
||||||
@$(ZIG) build build-v8
|
|
||||||
|
|
||||||
build-v8:
|
|
||||||
@printf "\e[36mBuilding v8...\e[0m\n"
|
|
||||||
@$(ZIG) build -Doptimize=ReleaseSafe build-v8
|
|
||||||
|
|
||||||
# Install and build required dependencies commands
|
# Install and build required dependencies commands
|
||||||
# ------------
|
# ------------
|
||||||
.PHONY: install-submodule
|
.PHONY: install-submodule
|
||||||
@@ -151,27 +127,27 @@ ICONV := $(BC)vendor/libiconv/out/$(OS)-$(ARCH)
|
|||||||
# TODO: this way of linking libiconv is not ideal. We should have a more generic way
|
# TODO: this way of linking libiconv is not ideal. We should have a more generic way
|
||||||
# and stick to a specif version. Maybe build from source. Anyway not now.
|
# and stick to a specif version. Maybe build from source. Anyway not now.
|
||||||
_install-netsurf: clean-netsurf
|
_install-netsurf: clean-netsurf
|
||||||
@printf "\e[36mInstalling NetSurf...\e[0m\n" && \
|
@printf "\033[36mInstalling NetSurf...\033[0m\n" && \
|
||||||
ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\e[33mERROR: you need to execute 'make install-libiconv'\e[0m\n"; exit 1;) && \
|
ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\033[33mERROR: you need to execute 'make install-libiconv'\033[0m\n"; exit 1;) && \
|
||||||
mkdir -p $(BC_NS) && \
|
mkdir -p $(BC_NS) && \
|
||||||
cp -R vendor/netsurf/share $(BC_NS) && \
|
cp -R vendor/netsurf/share $(BC_NS) && \
|
||||||
export PREFIX=$(BC_NS) && \
|
export PREFIX=$(BC_NS) && \
|
||||||
export OPTLDFLAGS="-L$(ICONV)/lib" && \
|
export OPTLDFLAGS="-L$(ICONV)/lib" && \
|
||||||
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
|
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
|
||||||
printf "\e[33mInstalling libwapcaplet...\e[0m\n" && \
|
printf "\033[33mInstalling libwapcaplet...\033[0m\n" && \
|
||||||
cd vendor/netsurf/libwapcaplet && \
|
cd vendor/netsurf/libwapcaplet && \
|
||||||
BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \
|
BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \
|
||||||
cd ../libparserutils && \
|
cd ../libparserutils && \
|
||||||
printf "\e[33mInstalling libparserutils...\e[0m\n" && \
|
printf "\033[33mInstalling libparserutils...\033[0m\n" && \
|
||||||
BUILDDIR=$(BC_NS)/build/libparserutils make install && \
|
BUILDDIR=$(BC_NS)/build/libparserutils make install && \
|
||||||
cd ../libhubbub && \
|
cd ../libhubbub && \
|
||||||
printf "\e[33mInstalling libhubbub...\e[0m\n" && \
|
printf "\033[33mInstalling libhubbub...\033[0m\n" && \
|
||||||
BUILDDIR=$(BC_NS)/build/libhubbub make install && \
|
BUILDDIR=$(BC_NS)/build/libhubbub make install && \
|
||||||
rm src/treebuilder/autogenerated-element-type.c && \
|
rm src/treebuilder/autogenerated-element-type.c && \
|
||||||
cd ../libdom && \
|
cd ../libdom && \
|
||||||
printf "\e[33mInstalling libdom...\e[0m\n" && \
|
printf "\033[33mInstalling libdom...\033[0m\n" && \
|
||||||
BUILDDIR=$(BC_NS)/build/libdom make install && \
|
BUILDDIR=$(BC_NS)/build/libdom make install && \
|
||||||
printf "\e[33mRunning libdom example...\e[0m\n" && \
|
printf "\033[33mRunning libdom example...\033[0m\n" && \
|
||||||
cd examples && \
|
cd examples && \
|
||||||
$(ZIG) cc \
|
$(ZIG) cc \
|
||||||
-I$(ICONV)/include \
|
-I$(ICONV)/include \
|
||||||
@@ -188,14 +164,14 @@ _install-netsurf: clean-netsurf
|
|||||||
$(ICONV)/lib/libiconv.a && \
|
$(ICONV)/lib/libiconv.a && \
|
||||||
./a.out > /dev/null && \
|
./a.out > /dev/null && \
|
||||||
rm a.out && \
|
rm a.out && \
|
||||||
printf "\e[36mDone NetSurf $(OS)\e[0m\n"
|
printf "\033[36mDone NetSurf $(OS)\033[0m\n"
|
||||||
|
|
||||||
clean-netsurf:
|
clean-netsurf:
|
||||||
@printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
|
@printf "\033[36mCleaning NetSurf build...\033[0m\n" && \
|
||||||
rm -Rf $(BC_NS)
|
rm -Rf $(BC_NS)
|
||||||
|
|
||||||
test-netsurf:
|
test-netsurf:
|
||||||
@printf "\e[36mTesting NetSurf...\e[0m\n" && \
|
@printf "\033[36mTesting NetSurf...\033[0m\n" && \
|
||||||
export PREFIX=$(BC_NS) && \
|
export PREFIX=$(BC_NS) && \
|
||||||
export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \
|
export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \
|
||||||
export CFLAGS="-I$(ICONV)/include -I$(BC_NS)/include" && \
|
export CFLAGS="-I$(ICONV)/include -I$(BC_NS)/include" && \
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -158,8 +158,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
|
||||||
@@ -168,12 +166,13 @@ 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),
|
[zig-v8-fork](https://github.com/lightpanda-io/zig-v8-fork/),
|
||||||
[Libcurl](https://curl.se/libcurl/),
|
[Libcurl](https://curl.se/libcurl/),
|
||||||
|
[Brotli](https://github.com/google/brotli),
|
||||||
[Netsurf libs](https://www.netsurf-browser.org/) and
|
[Netsurf libs](https://www.netsurf-browser.org/) and
|
||||||
[Mimalloc](https://microsoft.github.io/mimalloc).
|
[Mimalloc](https://microsoft.github.io/mimalloc).
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
@@ -246,22 +245,6 @@ Note: when Mimalloc is built in dev mode, you can dump memory stats with the
|
|||||||
env var `MIMALLOC_SHOW_STATS=1`. See
|
env var `MIMALLOC_SHOW_STATS=1`. See
|
||||||
[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html).
|
[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html).
|
||||||
|
|
||||||
**v8**
|
|
||||||
|
|
||||||
First, get the tools necessary for building V8, as well as the V8 source code:
|
|
||||||
|
|
||||||
```
|
|
||||||
make get-v8
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, build v8. This build task is very long and cpu consuming, as you will build v8 from sources.
|
|
||||||
|
|
||||||
```
|
|
||||||
make build-v8
|
|
||||||
```
|
|
||||||
|
|
||||||
For dev env, use `make build-v8-dev`.
|
|
||||||
|
|
||||||
## Test
|
## Test
|
||||||
|
|
||||||
### Unit Tests
|
### Unit Tests
|
||||||
|
|||||||
140
build.zig
140
build.zig
@@ -21,36 +21,21 @@ const builtin = @import("builtin");
|
|||||||
|
|
||||||
const Build = std.Build;
|
const Build = std.Build;
|
||||||
|
|
||||||
/// Do not rename this constant. It is scanned by some scripts to determine
|
|
||||||
/// which zig version to install.
|
|
||||||
const recommended_zig_version = "0.15.2";
|
|
||||||
|
|
||||||
pub fn build(b: *Build) !void {
|
pub fn build(b: *Build) !void {
|
||||||
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
|
||||||
.eq => {},
|
|
||||||
.lt => {
|
|
||||||
@compileError("The minimum version of Zig required to compile is '" ++ recommended_zig_version ++ "', found '" ++ builtin.zig_version_string ++ "'.");
|
|
||||||
},
|
|
||||||
.gt => {
|
|
||||||
std.debug.print(
|
|
||||||
"WARNING: Recommended Zig version '{s}', but found '{s}', build may fail...\n\n",
|
|
||||||
.{ recommended_zig_version, builtin.zig_version_string },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var opts = b.addOptions();
|
|
||||||
opts.addOption(
|
|
||||||
[]const u8,
|
|
||||||
"git_commit",
|
|
||||||
b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
|
|
||||||
);
|
|
||||||
|
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
// We're still using llvm because the new x86 backend seems to crash
|
const manifest = Manifest.init(b);
|
||||||
// with v8. This can be reproduced in zig-v8-fork.
|
|
||||||
|
const git_commit = b.option([]const u8, "git_commit", "Current git commit");
|
||||||
|
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
|
||||||
|
|
||||||
|
var opts = b.addOptions();
|
||||||
|
opts.addOption([]const u8, "version", manifest.version);
|
||||||
|
opts.addOption([]const u8, "git_commit", git_commit orelse "dev");
|
||||||
|
|
||||||
|
// We're still using llvm because the new x86 backend seems to crash with v8.
|
||||||
|
// This can be reproduced in zig-v8-fork.
|
||||||
|
|
||||||
const lightpanda_module = b.addModule("lightpanda", .{
|
const lightpanda_module = b.addModule("lightpanda", .{
|
||||||
.root_source_file = b.path("src/main.zig"),
|
.root_source_file = b.path("src/main.zig"),
|
||||||
@@ -59,7 +44,7 @@ pub fn build(b: *Build) !void {
|
|||||||
.link_libc = true,
|
.link_libc = true,
|
||||||
.link_libcpp = true,
|
.link_libcpp = true,
|
||||||
});
|
});
|
||||||
try addDependencies(b, lightpanda_module, opts);
|
try addDependencies(b, lightpanda_module, opts, prebuilt_v8_path);
|
||||||
|
|
||||||
{
|
{
|
||||||
// browser
|
// browser
|
||||||
@@ -113,7 +98,7 @@ pub fn build(b: *Build) !void {
|
|||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
try addDependencies(b, wpt_module, opts);
|
try addDependencies(b, wpt_module, opts, prebuilt_v8_path);
|
||||||
|
|
||||||
// compile and install
|
// compile and install
|
||||||
const wpt = b.addExecutable(.{
|
const wpt = b.addExecutable(.{
|
||||||
@@ -131,27 +116,9 @@ pub fn build(b: *Build) !void {
|
|||||||
const wpt_step = b.step("wpt", "WPT tests");
|
const wpt_step = b.step("wpt", "WPT tests");
|
||||||
wpt_step.dependOn(&wpt_cmd.step);
|
wpt_step.dependOn(&wpt_cmd.step);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
// get v8
|
|
||||||
// -------
|
|
||||||
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
|
|
||||||
const get_v8 = b.addRunArtifact(v8.artifact("get-v8"));
|
|
||||||
const get_step = b.step("get-v8", "Get v8");
|
|
||||||
get_step.dependOn(&get_v8.step);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// build v8
|
|
||||||
// -------
|
|
||||||
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
|
|
||||||
const build_v8 = b.addRunArtifact(v8.artifact("build-v8"));
|
|
||||||
const build_step = b.step("build-v8", "Build v8");
|
|
||||||
build_step.dependOn(&build_v8.step);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !void {
|
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, prebuilt_v8_path: ?[]const u8) !void {
|
||||||
try moduleNetSurf(b, mod);
|
try moduleNetSurf(b, mod);
|
||||||
mod.addImport("build_config", opts.createModule());
|
mod.addImport("build_config", opts.createModule());
|
||||||
|
|
||||||
@@ -159,6 +126,8 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
|
|||||||
const dep_opts = .{
|
const dep_opts = .{
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = mod.optimize.?,
|
.optimize = mod.optimize.?,
|
||||||
|
.prebuilt_v8_path = prebuilt_v8_path,
|
||||||
|
.cache_root = b.pathFromRoot(".lp-cache"),
|
||||||
};
|
};
|
||||||
|
|
||||||
mod.addIncludePath(b.path("vendor/lightpanda"));
|
mod.addIncludePath(b.path("vendor/lightpanda"));
|
||||||
@@ -171,36 +140,6 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
|
|||||||
const v8_mod = b.dependency("v8", dep_opts).module("v8");
|
const v8_mod = b.dependency("v8", dep_opts).module("v8");
|
||||||
v8_mod.addOptions("default_exports", v8_opts);
|
v8_mod.addOptions("default_exports", v8_opts);
|
||||||
mod.addImport("v8", v8_mod);
|
mod.addImport("v8", v8_mod);
|
||||||
|
|
||||||
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
|
|
||||||
const os = switch (target.result.os.tag) {
|
|
||||||
.linux => "linux",
|
|
||||||
.macos => "macos",
|
|
||||||
else => return error.UnsupportedPlatform,
|
|
||||||
};
|
|
||||||
var lib_path = try std.fmt.allocPrint(
|
|
||||||
mod.owner.allocator,
|
|
||||||
"v8/out/{s}/{s}/obj/zig/libc_v8.a",
|
|
||||||
.{ os, release_dir },
|
|
||||||
);
|
|
||||||
std.fs.cwd().access(lib_path, .{}) catch {
|
|
||||||
// legacy path
|
|
||||||
lib_path = try std.fmt.allocPrint(
|
|
||||||
mod.owner.allocator,
|
|
||||||
"v8/out/{s}/obj/zig/libc_v8.a",
|
|
||||||
.{release_dir},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
mod.addObjectFile(mod.owner.path(lib_path));
|
|
||||||
|
|
||||||
switch (target.result.os.tag) {
|
|
||||||
.macos => {
|
|
||||||
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
|
|
||||||
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
|
|
||||||
mod.linkFramework("CoreFoundation", .{});
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -374,14 +313,27 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
|
|||||||
mod.addCMacro("STDC_HEADERS", "1");
|
mod.addCMacro("STDC_HEADERS", "1");
|
||||||
mod.addCMacro("TIME_WITH_SYS_TIME", "1");
|
mod.addCMacro("TIME_WITH_SYS_TIME", "1");
|
||||||
mod.addCMacro("USE_NGHTTP2", "1");
|
mod.addCMacro("USE_NGHTTP2", "1");
|
||||||
mod.addCMacro("USE_MBEDTLS", "1");
|
mod.addCMacro("USE_OPENSSL", "1");
|
||||||
|
mod.addCMacro("OPENSSL_IS_BORINGSSL", "1");
|
||||||
mod.addCMacro("USE_THREADS_POSIX", "1");
|
mod.addCMacro("USE_THREADS_POSIX", "1");
|
||||||
mod.addCMacro("USE_UNIX_SOCKETS", "1");
|
mod.addCMacro("USE_UNIX_SOCKETS", "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
try buildZlib(b, mod);
|
try buildZlib(b, mod);
|
||||||
try buildBrotli(b, mod);
|
try buildBrotli(b, mod);
|
||||||
try buildMbedtls(b, mod);
|
const boringssl_dep = b.dependency("boringssl-zig", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = mod.optimize.?,
|
||||||
|
.force_pic = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ssl = boringssl_dep.artifact("ssl");
|
||||||
|
ssl.bundle_ubsan_rt = false;
|
||||||
|
const crypto = boringssl_dep.artifact("crypto");
|
||||||
|
crypto.bundle_ubsan_rt = false;
|
||||||
|
|
||||||
|
mod.linkLibrary(ssl);
|
||||||
|
mod.linkLibrary(crypto);
|
||||||
try buildNghttp2(b, mod);
|
try buildNghttp2(b, mod);
|
||||||
try buildCurl(b, mod);
|
try buildCurl(b, mod);
|
||||||
try buildAda(b, mod);
|
try buildAda(b, mod);
|
||||||
@@ -842,8 +794,9 @@ fn buildCurl(b: *Build, m: *Build.Module) !void {
|
|||||||
root ++ "lib/vauth/spnego_sspi.c",
|
root ++ "lib/vauth/spnego_sspi.c",
|
||||||
root ++ "lib/vauth/vauth.c",
|
root ++ "lib/vauth/vauth.c",
|
||||||
root ++ "lib/vtls/cipher_suite.c",
|
root ++ "lib/vtls/cipher_suite.c",
|
||||||
root ++ "lib/vtls/mbedtls.c",
|
root ++ "lib/vtls/openssl.c",
|
||||||
root ++ "lib/vtls/mbedtls_threadlock.c",
|
root ++ "lib/vtls/hostcheck.c",
|
||||||
|
root ++ "lib/vtls/keylog.c",
|
||||||
root ++ "lib/vtls/vtls.c",
|
root ++ "lib/vtls/vtls.c",
|
||||||
root ++ "lib/vtls/vtls_scache.c",
|
root ++ "lib/vtls/vtls_scache.c",
|
||||||
root ++ "lib/vtls/x509asn1.c",
|
root ++ "lib/vtls/x509asn1.c",
|
||||||
@@ -881,3 +834,28 @@ pub fn buildAda(b: *Build, m: *Build.Module) !void {
|
|||||||
// Expose ada module to main module.
|
// Expose ada module to main module.
|
||||||
m.addImport("ada", ada_mod);
|
m.addImport("ada", ada_mod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Manifest = struct {
|
||||||
|
version: []const u8,
|
||||||
|
minimum_zig_version: []const u8,
|
||||||
|
|
||||||
|
fn init(b: *std.Build) Manifest {
|
||||||
|
const input = @embedFile("build.zig.zon");
|
||||||
|
|
||||||
|
var diagnostics: std.zon.parse.Diagnostics = .{};
|
||||||
|
defer diagnostics.deinit(b.allocator);
|
||||||
|
|
||||||
|
return std.zon.parse.fromSlice(Manifest, b.allocator, input, &diagnostics, .{
|
||||||
|
.free_on_error = true,
|
||||||
|
.ignore_unknown_fields = true,
|
||||||
|
}) catch |err| {
|
||||||
|
switch (err) {
|
||||||
|
error.OutOfMemory => @panic("OOM"),
|
||||||
|
error.ParseZon => {
|
||||||
|
std.debug.print("Parse diagnostics:\n{f}\n", .{diagnostics});
|
||||||
|
std.process.exit(1);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
.{
|
.{
|
||||||
.name = .browser,
|
.name = .browser,
|
||||||
.paths = .{""},
|
|
||||||
.version = "0.0.0",
|
.version = "0.0.0",
|
||||||
.fingerprint = 0xda130f3af836cea0,
|
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
|
||||||
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/84cdca7cd9065f67c7933388f2091810fc485bc6.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/e047d2a4d5af5783763f0f6a652fab8982a08603.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH67vcAwCuN2gBsAO8TBzEw523KMroIKGrdZwc-Q-y",
|
.hash = "v8-0.0.0-xddH65gMBACRBQMM7EwmVgfi94FJyyX-0jpe5KhXYhfv",
|
||||||
},
|
},
|
||||||
//.v8 = .{ .path = "../zig-v8-fork" }
|
// .v8 = .{ .path = "../zig-v8-fork" },
|
||||||
.@"ada-singleheader" = .{
|
.@"ada-singleheader" = .{
|
||||||
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
|
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
|
||||||
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
|
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
|
||||||
},
|
},
|
||||||
|
.@"boringssl-zig" = .{
|
||||||
|
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
|
||||||
|
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
.paths = .{""},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ fn getContentType(file_path: []const u8) []const u8 {
|
|||||||
return "application/json";
|
return "application/json";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (std.mem.endsWith(u8, file_path, ".mjs")) {
|
||||||
|
// mjs are ECMAScript modules
|
||||||
|
return "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
if (std.mem.endsWith(u8, file_path, ".html")) {
|
if (std.mem.endsWith(u8, file_path, ".html")) {
|
||||||
return "text/html";
|
return "text/html";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub const App = struct {
|
|||||||
telemetry: Telemetry,
|
telemetry: Telemetry,
|
||||||
app_dir_path: ?[]const u8,
|
app_dir_path: ?[]const u8,
|
||||||
notification: *Notification,
|
notification: *Notification,
|
||||||
|
shutdown: bool = false,
|
||||||
|
|
||||||
pub const RunMode = enum {
|
pub const RunMode = enum {
|
||||||
help,
|
help,
|
||||||
@@ -82,9 +83,14 @@ pub const App = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *App) void {
|
pub fn deinit(self: *App) void {
|
||||||
|
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const allocator = self.allocator;
|
const allocator = self.allocator;
|
||||||
if (self.app_dir_path) |app_dir_path| {
|
if (self.app_dir_path) |app_dir_path| {
|
||||||
allocator.free(app_dir_path);
|
allocator.free(app_dir_path);
|
||||||
|
self.app_dir_path = null;
|
||||||
}
|
}
|
||||||
self.telemetry.deinit();
|
self.telemetry.deinit();
|
||||||
self.notification.deinit();
|
self.notification.deinit();
|
||||||
|
|||||||
@@ -392,6 +392,15 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
.stack = self.page.js.stackTrace() catch "???",
|
.stack = self.page.js.stackTrace() catch "???",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// It's possible, but unlikely, for client.request to immediately finish
|
||||||
|
// a request, thus calling our callback. We generally don't want a call
|
||||||
|
// from v8 (which is why we're here), to result in a new script evaluation.
|
||||||
|
// So we block even the slightest change that `client.request` immediately
|
||||||
|
// executes a callback.
|
||||||
|
const was_evaluating = self.is_evaluating;
|
||||||
|
self.is_evaluating = true;
|
||||||
|
defer self.is_evaluating = was_evaluating;
|
||||||
|
|
||||||
try self.client.request(.{
|
try self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
|
|||||||
58
src/browser/canvas/CanvasRenderingContext2D.zig
Normal file
58
src/browser/canvas/CanvasRenderingContext2D.zig
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// 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 color = @import("../cssom/color.zig");
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
/// This class doesn't implement a `constructor`.
|
||||||
|
/// It can be obtained with a call to `HTMLCanvasElement#getContext`.
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
|
||||||
|
const CanvasRenderingContext2D = @This();
|
||||||
|
/// Fill color.
|
||||||
|
/// TODO: Add support for `CanvasGradient` and `CanvasPattern`.
|
||||||
|
fill_style: color.RGBA = color.RGBA.Named.black,
|
||||||
|
|
||||||
|
pub fn _fillRect(
|
||||||
|
self: *const CanvasRenderingContext2D,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
width: f64,
|
||||||
|
height: f64,
|
||||||
|
) void {
|
||||||
|
_ = self;
|
||||||
|
_ = x;
|
||||||
|
_ = y;
|
||||||
|
_ = width;
|
||||||
|
_ = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_fillStyle(self: *const CanvasRenderingContext2D, page: *Page) ![]const u8 {
|
||||||
|
var w = std.Io.Writer.Allocating.init(page.call_arena);
|
||||||
|
try self.fill_style.format(&w.writer);
|
||||||
|
return w.written();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_fillStyle(
|
||||||
|
self: *CanvasRenderingContext2D,
|
||||||
|
value: []const u8,
|
||||||
|
) !void {
|
||||||
|
// Prefer the same fill_style if fails.
|
||||||
|
self.fill_style = color.RGBA.parse(value) catch self.fill_style;
|
||||||
|
}
|
||||||
145
src/browser/canvas/WebGLRenderingContext.zig
Normal file
145
src/browser/canvas/WebGLRenderingContext.zig
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// 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 WebGLRenderingContext = @This();
|
||||||
|
_: u8 = 0,
|
||||||
|
|
||||||
|
/// On Chrome and Safari, a call to `getSupportedExtensions` returns total of 39.
|
||||||
|
/// The reference for it lists lesser number of extensions:
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Using_Extensions#extension_list
|
||||||
|
pub const Extension = union(enum) {
|
||||||
|
ANGLE_instanced_arrays: void,
|
||||||
|
EXT_blend_minmax: void,
|
||||||
|
EXT_clip_control: void,
|
||||||
|
EXT_color_buffer_half_float: void,
|
||||||
|
EXT_depth_clamp: void,
|
||||||
|
EXT_disjoint_timer_query: void,
|
||||||
|
EXT_float_blend: void,
|
||||||
|
EXT_frag_depth: void,
|
||||||
|
EXT_polygon_offset_clamp: void,
|
||||||
|
EXT_shader_texture_lod: void,
|
||||||
|
EXT_texture_compression_bptc: void,
|
||||||
|
EXT_texture_compression_rgtc: void,
|
||||||
|
EXT_texture_filter_anisotropic: void,
|
||||||
|
EXT_texture_mirror_clamp_to_edge: void,
|
||||||
|
EXT_sRGB: void,
|
||||||
|
KHR_parallel_shader_compile: void,
|
||||||
|
OES_element_index_uint: void,
|
||||||
|
OES_fbo_render_mipmap: void,
|
||||||
|
OES_standard_derivatives: void,
|
||||||
|
OES_texture_float: void,
|
||||||
|
OES_texture_float_linear: void,
|
||||||
|
OES_texture_half_float: void,
|
||||||
|
OES_texture_half_float_linear: void,
|
||||||
|
OES_vertex_array_object: void,
|
||||||
|
WEBGL_blend_func_extended: void,
|
||||||
|
WEBGL_color_buffer_float: void,
|
||||||
|
WEBGL_compressed_texture_astc: void,
|
||||||
|
WEBGL_compressed_texture_etc: void,
|
||||||
|
WEBGL_compressed_texture_etc1: void,
|
||||||
|
WEBGL_compressed_texture_pvrtc: void,
|
||||||
|
WEBGL_compressed_texture_s3tc: void,
|
||||||
|
WEBGL_compressed_texture_s3tc_srgb: void,
|
||||||
|
WEBGL_debug_renderer_info: Type.WEBGL_debug_renderer_info,
|
||||||
|
WEBGL_debug_shaders: void,
|
||||||
|
WEBGL_depth_texture: void,
|
||||||
|
WEBGL_draw_buffers: void,
|
||||||
|
WEBGL_lose_context: Type.WEBGL_lose_context,
|
||||||
|
WEBGL_multi_draw: void,
|
||||||
|
WEBGL_polygon_mode: void,
|
||||||
|
|
||||||
|
/// Reified enum type from the fields of this union.
|
||||||
|
const Kind = blk: {
|
||||||
|
const info = @typeInfo(Extension).@"union";
|
||||||
|
const fields = info.fields;
|
||||||
|
var items: [fields.len]std.builtin.Type.EnumField = undefined;
|
||||||
|
for (fields, 0..) |field, i| {
|
||||||
|
items[i] = .{ .name = field.name, .value = i };
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk @Type(.{
|
||||||
|
.@"enum" = .{
|
||||||
|
.tag_type = std.math.IntFittingRange(0, if (fields.len == 0) 0 else fields.len - 1),
|
||||||
|
.fields = &items,
|
||||||
|
.decls = &.{},
|
||||||
|
.is_exhaustive = true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Returns the `Extension.Kind` by its name.
|
||||||
|
fn find(name: []const u8) ?Kind {
|
||||||
|
// Just to make you really sad, this function has to be case-insensitive.
|
||||||
|
// So here we copy what's being done in `std.meta.stringToEnum` but replace
|
||||||
|
// the comparison function.
|
||||||
|
const kvs = comptime build_kvs: {
|
||||||
|
const T = Extension.Kind;
|
||||||
|
const EnumKV = struct { []const u8, T };
|
||||||
|
var kvs_array: [@typeInfo(T).@"enum".fields.len]EnumKV = undefined;
|
||||||
|
for (@typeInfo(T).@"enum".fields, 0..) |enumField, i| {
|
||||||
|
kvs_array[i] = .{ enumField.name, @field(T, enumField.name) };
|
||||||
|
}
|
||||||
|
break :build_kvs kvs_array[0..];
|
||||||
|
};
|
||||||
|
const Map = std.StaticStringMapWithEql(Extension.Kind, std.static_string_map.eqlAsciiIgnoreCase);
|
||||||
|
const map = Map.initComptime(kvs);
|
||||||
|
return map.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension types.
|
||||||
|
pub const Type = struct {
|
||||||
|
pub const WEBGL_debug_renderer_info = struct {
|
||||||
|
pub const UNMASKED_VENDOR_WEBGL: u64 = 0x9245;
|
||||||
|
pub const UNMASKED_RENDERER_WEBGL: u64 = 0x9246;
|
||||||
|
|
||||||
|
pub fn get_UNMASKED_VENDOR_WEBGL() u64 {
|
||||||
|
return UNMASKED_VENDOR_WEBGL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_UNMASKED_RENDERER_WEBGL() u64 {
|
||||||
|
return UNMASKED_RENDERER_WEBGL;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WEBGL_lose_context = struct {
|
||||||
|
_: u8 = 0,
|
||||||
|
pub fn _loseContext(_: *const WEBGL_lose_context) void {}
|
||||||
|
pub fn _restoreContext(_: *const WEBGL_lose_context) void {}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Enables a WebGL extension.
|
||||||
|
pub fn _getExtension(self: *const WebGLRenderingContext, name: []const u8) ?Extension {
|
||||||
|
_ = self;
|
||||||
|
|
||||||
|
const tag = Extension.find(name) orelse return null;
|
||||||
|
|
||||||
|
return switch (tag) {
|
||||||
|
.WEBGL_debug_renderer_info => @unionInit(Extension, "WEBGL_debug_renderer_info", .{}),
|
||||||
|
.WEBGL_lose_context => @unionInit(Extension, "WEBGL_lose_context", .{}),
|
||||||
|
inline else => |comptime_enum| @unionInit(Extension, @tagName(comptime_enum), {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a list of all the supported WebGL extensions.
|
||||||
|
pub fn _getSupportedExtensions(_: *const WebGLRenderingContext) []const []const u8 {
|
||||||
|
return std.meta.fieldNames(Extension.Kind);
|
||||||
|
}
|
||||||
13
src/browser/canvas/root.zig
Normal file
13
src/browser/canvas/root.zig
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//! Canvas API.
|
||||||
|
//! https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
|
||||||
|
|
||||||
|
const CanvasRenderingContext2D = @import("CanvasRenderingContext2D.zig");
|
||||||
|
const WebGLRenderingContext = @import("WebGLRenderingContext.zig");
|
||||||
|
const Extension = WebGLRenderingContext.Extension;
|
||||||
|
|
||||||
|
pub const Interfaces = .{
|
||||||
|
CanvasRenderingContext2D,
|
||||||
|
WebGLRenderingContext,
|
||||||
|
Extension.Type.WEBGL_debug_renderer_info,
|
||||||
|
Extension.Type.WEBGL_lose_context,
|
||||||
|
};
|
||||||
@@ -190,7 +190,7 @@ fn isNumericWithUnit(value: []const u8) bool {
|
|||||||
return CSSKeywords.isValidUnit(unit);
|
return CSSKeywords.isValidUnit(unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isHexColor(value: []const u8) bool {
|
pub fn isHexColor(value: []const u8) bool {
|
||||||
if (value.len == 0) {
|
if (value.len == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -199,7 +199,7 @@ fn isHexColor(value: []const u8) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hex_part = value[1..];
|
const hex_part = value[1..];
|
||||||
if (hex_part.len != 3 and hex_part.len != 6 and hex_part.len != 8) {
|
if (hex_part.len != 3 and hex_part.len != 4 and hex_part.len != 6 and hex_part.len != 8) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,6 +551,7 @@ test "Browser: CSS.StyleDeclaration: isNumericWithUnit - edge cases and invalid
|
|||||||
|
|
||||||
test "Browser: CSS.StyleDeclaration: isHexColor - valid hex colors" {
|
test "Browser: CSS.StyleDeclaration: isHexColor - valid hex colors" {
|
||||||
try testing.expect(isHexColor("#000"));
|
try testing.expect(isHexColor("#000"));
|
||||||
|
try testing.expect(isHexColor("#0000"));
|
||||||
try testing.expect(isHexColor("#fff"));
|
try testing.expect(isHexColor("#fff"));
|
||||||
try testing.expect(isHexColor("#123456"));
|
try testing.expect(isHexColor("#123456"));
|
||||||
try testing.expect(isHexColor("#abcdef"));
|
try testing.expect(isHexColor("#abcdef"));
|
||||||
@@ -563,7 +564,6 @@ test "Browser: CSS.StyleDeclaration: isHexColor - invalid hex colors" {
|
|||||||
try testing.expect(!isHexColor("#"));
|
try testing.expect(!isHexColor("#"));
|
||||||
try testing.expect(!isHexColor("000"));
|
try testing.expect(!isHexColor("000"));
|
||||||
try testing.expect(!isHexColor("#00"));
|
try testing.expect(!isHexColor("#00"));
|
||||||
try testing.expect(!isHexColor("#0000"));
|
|
||||||
try testing.expect(!isHexColor("#00000"));
|
try testing.expect(!isHexColor("#00000"));
|
||||||
try testing.expect(!isHexColor("#0000000"));
|
try testing.expect(!isHexColor("#0000000"));
|
||||||
try testing.expect(!isHexColor("#000000000"));
|
try testing.expect(!isHexColor("#000000000"));
|
||||||
|
|||||||
283
src/browser/cssom/color.zig
Normal file
283
src/browser/cssom/color.zig
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
|
||||||
|
const CSSParser = @import("CSSParser.zig");
|
||||||
|
const isHexColor = @import("CSSStyleDeclaration.zig").isHexColor;
|
||||||
|
|
||||||
|
pub const RGBA = packed struct(u32) {
|
||||||
|
r: u8,
|
||||||
|
g: u8,
|
||||||
|
b: u8,
|
||||||
|
/// Opaque by default.
|
||||||
|
a: u8 = std.math.maxInt(u8),
|
||||||
|
|
||||||
|
pub const Named = struct {
|
||||||
|
// Basic colors (CSS Level 1)
|
||||||
|
pub const black: RGBA = .init(0, 0, 0, 1);
|
||||||
|
pub const silver: RGBA = .init(192, 192, 192, 1);
|
||||||
|
pub const gray: RGBA = .init(128, 128, 128, 1);
|
||||||
|
pub const white: RGBA = .init(255, 255, 255, 1);
|
||||||
|
pub const maroon: RGBA = .init(128, 0, 0, 1);
|
||||||
|
pub const red: RGBA = .init(255, 0, 0, 1);
|
||||||
|
pub const purple: RGBA = .init(128, 0, 128, 1);
|
||||||
|
pub const fuchsia: RGBA = .init(255, 0, 255, 1);
|
||||||
|
pub const green: RGBA = .init(0, 128, 0, 1);
|
||||||
|
pub const lime: RGBA = .init(0, 255, 0, 1);
|
||||||
|
pub const olive: RGBA = .init(128, 128, 0, 1);
|
||||||
|
pub const yellow: RGBA = .init(255, 255, 0, 1);
|
||||||
|
pub const navy: RGBA = .init(0, 0, 128, 1);
|
||||||
|
pub const blue: RGBA = .init(0, 0, 255, 1);
|
||||||
|
pub const teal: RGBA = .init(0, 128, 128, 1);
|
||||||
|
pub const aqua: RGBA = .init(0, 255, 255, 1);
|
||||||
|
|
||||||
|
// Extended colors (CSS Level 2+)
|
||||||
|
pub const aliceblue: RGBA = .init(240, 248, 255, 1);
|
||||||
|
pub const antiquewhite: RGBA = .init(250, 235, 215, 1);
|
||||||
|
pub const aquamarine: RGBA = .init(127, 255, 212, 1);
|
||||||
|
pub const azure: RGBA = .init(240, 255, 255, 1);
|
||||||
|
pub const beige: RGBA = .init(245, 245, 220, 1);
|
||||||
|
pub const bisque: RGBA = .init(255, 228, 196, 1);
|
||||||
|
pub const blanchedalmond: RGBA = .init(255, 235, 205, 1);
|
||||||
|
pub const blueviolet: RGBA = .init(138, 43, 226, 1);
|
||||||
|
pub const brown: RGBA = .init(165, 42, 42, 1);
|
||||||
|
pub const burlywood: RGBA = .init(222, 184, 135, 1);
|
||||||
|
pub const cadetblue: RGBA = .init(95, 158, 160, 1);
|
||||||
|
pub const chartreuse: RGBA = .init(127, 255, 0, 1);
|
||||||
|
pub const chocolate: RGBA = .init(210, 105, 30, 1);
|
||||||
|
pub const coral: RGBA = .init(255, 127, 80, 1);
|
||||||
|
pub const cornflowerblue: RGBA = .init(100, 149, 237, 1);
|
||||||
|
pub const cornsilk: RGBA = .init(255, 248, 220, 1);
|
||||||
|
pub const crimson: RGBA = .init(220, 20, 60, 1);
|
||||||
|
pub const cyan: RGBA = .init(0, 255, 255, 1); // Synonym of aqua
|
||||||
|
pub const darkblue: RGBA = .init(0, 0, 139, 1);
|
||||||
|
pub const darkcyan: RGBA = .init(0, 139, 139, 1);
|
||||||
|
pub const darkgoldenrod: RGBA = .init(184, 134, 11, 1);
|
||||||
|
pub const darkgray: RGBA = .init(169, 169, 169, 1);
|
||||||
|
pub const darkgreen: RGBA = .init(0, 100, 0, 1);
|
||||||
|
pub const darkgrey: RGBA = .init(169, 169, 169, 1); // Synonym of darkgray
|
||||||
|
pub const darkkhaki: RGBA = .init(189, 183, 107, 1);
|
||||||
|
pub const darkmagenta: RGBA = .init(139, 0, 139, 1);
|
||||||
|
pub const darkolivegreen: RGBA = .init(85, 107, 47, 1);
|
||||||
|
pub const darkorange: RGBA = .init(255, 140, 0, 1);
|
||||||
|
pub const darkorchid: RGBA = .init(153, 50, 204, 1);
|
||||||
|
pub const darkred: RGBA = .init(139, 0, 0, 1);
|
||||||
|
pub const darksalmon: RGBA = .init(233, 150, 122, 1);
|
||||||
|
pub const darkseagreen: RGBA = .init(143, 188, 143, 1);
|
||||||
|
pub const darkslateblue: RGBA = .init(72, 61, 139, 1);
|
||||||
|
pub const darkslategray: RGBA = .init(47, 79, 79, 1);
|
||||||
|
pub const darkslategrey: RGBA = .init(47, 79, 79, 1); // Synonym of darkslategray
|
||||||
|
pub const darkturquoise: RGBA = .init(0, 206, 209, 1);
|
||||||
|
pub const darkviolet: RGBA = .init(148, 0, 211, 1);
|
||||||
|
pub const deeppink: RGBA = .init(255, 20, 147, 1);
|
||||||
|
pub const deepskyblue: RGBA = .init(0, 191, 255, 1);
|
||||||
|
pub const dimgray: RGBA = .init(105, 105, 105, 1);
|
||||||
|
pub const dimgrey: RGBA = .init(105, 105, 105, 1); // Synonym of dimgray
|
||||||
|
pub const dodgerblue: RGBA = .init(30, 144, 255, 1);
|
||||||
|
pub const firebrick: RGBA = .init(178, 34, 34, 1);
|
||||||
|
pub const floralwhite: RGBA = .init(255, 250, 240, 1);
|
||||||
|
pub const forestgreen: RGBA = .init(34, 139, 34, 1);
|
||||||
|
pub const gainsboro: RGBA = .init(220, 220, 220, 1);
|
||||||
|
pub const ghostwhite: RGBA = .init(248, 248, 255, 1);
|
||||||
|
pub const gold: RGBA = .init(255, 215, 0, 1);
|
||||||
|
pub const goldenrod: RGBA = .init(218, 165, 32, 1);
|
||||||
|
pub const greenyellow: RGBA = .init(173, 255, 47, 1);
|
||||||
|
pub const grey: RGBA = .init(128, 128, 128, 1); // Synonym of gray
|
||||||
|
pub const honeydew: RGBA = .init(240, 255, 240, 1);
|
||||||
|
pub const hotpink: RGBA = .init(255, 105, 180, 1);
|
||||||
|
pub const indianred: RGBA = .init(205, 92, 92, 1);
|
||||||
|
pub const indigo: RGBA = .init(75, 0, 130, 1);
|
||||||
|
pub const ivory: RGBA = .init(255, 255, 240, 1);
|
||||||
|
pub const khaki: RGBA = .init(240, 230, 140, 1);
|
||||||
|
pub const lavender: RGBA = .init(230, 230, 250, 1);
|
||||||
|
pub const lavenderblush: RGBA = .init(255, 240, 245, 1);
|
||||||
|
pub const lawngreen: RGBA = .init(124, 252, 0, 1);
|
||||||
|
pub const lemonchiffon: RGBA = .init(255, 250, 205, 1);
|
||||||
|
pub const lightblue: RGBA = .init(173, 216, 230, 1);
|
||||||
|
pub const lightcoral: RGBA = .init(240, 128, 128, 1);
|
||||||
|
pub const lightcyan: RGBA = .init(224, 255, 255, 1);
|
||||||
|
pub const lightgoldenrodyellow: RGBA = .init(250, 250, 210, 1);
|
||||||
|
pub const lightgray: RGBA = .init(211, 211, 211, 1);
|
||||||
|
pub const lightgreen: RGBA = .init(144, 238, 144, 1);
|
||||||
|
pub const lightgrey: RGBA = .init(211, 211, 211, 1); // Synonym of lightgray
|
||||||
|
pub const lightpink: RGBA = .init(255, 182, 193, 1);
|
||||||
|
pub const lightsalmon: RGBA = .init(255, 160, 122, 1);
|
||||||
|
pub const lightseagreen: RGBA = .init(32, 178, 170, 1);
|
||||||
|
pub const lightskyblue: RGBA = .init(135, 206, 250, 1);
|
||||||
|
pub const lightslategray: RGBA = .init(119, 136, 153, 1);
|
||||||
|
pub const lightslategrey: RGBA = .init(119, 136, 153, 1); // Synonym of lightslategray
|
||||||
|
pub const lightsteelblue: RGBA = .init(176, 196, 222, 1);
|
||||||
|
pub const lightyellow: RGBA = .init(255, 255, 224, 1);
|
||||||
|
pub const limegreen: RGBA = .init(50, 205, 50, 1);
|
||||||
|
pub const linen: RGBA = .init(250, 240, 230, 1);
|
||||||
|
pub const magenta: RGBA = .init(255, 0, 255, 1); // Synonym of fuchsia
|
||||||
|
pub const mediumaquamarine: RGBA = .init(102, 205, 170, 1);
|
||||||
|
pub const mediumblue: RGBA = .init(0, 0, 205, 1);
|
||||||
|
pub const mediumorchid: RGBA = .init(186, 85, 211, 1);
|
||||||
|
pub const mediumpurple: RGBA = .init(147, 112, 219, 1);
|
||||||
|
pub const mediumseagreen: RGBA = .init(60, 179, 113, 1);
|
||||||
|
pub const mediumslateblue: RGBA = .init(123, 104, 238, 1);
|
||||||
|
pub const mediumspringgreen: RGBA = .init(0, 250, 154, 1);
|
||||||
|
pub const mediumturquoise: RGBA = .init(72, 209, 204, 1);
|
||||||
|
pub const mediumvioletred: RGBA = .init(199, 21, 133, 1);
|
||||||
|
pub const midnightblue: RGBA = .init(25, 25, 112, 1);
|
||||||
|
pub const mintcream: RGBA = .init(245, 255, 250, 1);
|
||||||
|
pub const mistyrose: RGBA = .init(255, 228, 225, 1);
|
||||||
|
pub const moccasin: RGBA = .init(255, 228, 181, 1);
|
||||||
|
pub const navajowhite: RGBA = .init(255, 222, 173, 1);
|
||||||
|
pub const oldlace: RGBA = .init(253, 245, 230, 1);
|
||||||
|
pub const olivedrab: RGBA = .init(107, 142, 35, 1);
|
||||||
|
pub const orange: RGBA = .init(255, 165, 0, 1);
|
||||||
|
pub const orangered: RGBA = .init(255, 69, 0, 1);
|
||||||
|
pub const orchid: RGBA = .init(218, 112, 214, 1);
|
||||||
|
pub const palegoldenrod: RGBA = .init(238, 232, 170, 1);
|
||||||
|
pub const palegreen: RGBA = .init(152, 251, 152, 1);
|
||||||
|
pub const paleturquoise: RGBA = .init(175, 238, 238, 1);
|
||||||
|
pub const palevioletred: RGBA = .init(219, 112, 147, 1);
|
||||||
|
pub const papayawhip: RGBA = .init(255, 239, 213, 1);
|
||||||
|
pub const peachpuff: RGBA = .init(255, 218, 185, 1);
|
||||||
|
pub const peru: RGBA = .init(205, 133, 63, 1);
|
||||||
|
pub const pink: RGBA = .init(255, 192, 203, 1);
|
||||||
|
pub const plum: RGBA = .init(221, 160, 221, 1);
|
||||||
|
pub const powderblue: RGBA = .init(176, 224, 230, 1);
|
||||||
|
pub const rebeccapurple: RGBA = .init(102, 51, 153, 1);
|
||||||
|
pub const rosybrown: RGBA = .init(188, 143, 143, 1);
|
||||||
|
pub const royalblue: RGBA = .init(65, 105, 225, 1);
|
||||||
|
pub const saddlebrown: RGBA = .init(139, 69, 19, 1);
|
||||||
|
pub const salmon: RGBA = .init(250, 128, 114, 1);
|
||||||
|
pub const sandybrown: RGBA = .init(244, 164, 96, 1);
|
||||||
|
pub const seagreen: RGBA = .init(46, 139, 87, 1);
|
||||||
|
pub const seashell: RGBA = .init(255, 245, 238, 1);
|
||||||
|
pub const sienna: RGBA = .init(160, 82, 45, 1);
|
||||||
|
pub const skyblue: RGBA = .init(135, 206, 235, 1);
|
||||||
|
pub const slateblue: RGBA = .init(106, 90, 205, 1);
|
||||||
|
pub const slategray: RGBA = .init(112, 128, 144, 1);
|
||||||
|
pub const slategrey: RGBA = .init(112, 128, 144, 1); // Synonym of slategray
|
||||||
|
pub const snow: RGBA = .init(255, 250, 250, 1);
|
||||||
|
pub const springgreen: RGBA = .init(0, 255, 127, 1);
|
||||||
|
pub const steelblue: RGBA = .init(70, 130, 180, 1);
|
||||||
|
pub const tan: RGBA = .init(210, 180, 140, 1);
|
||||||
|
pub const thistle: RGBA = .init(216, 191, 216, 1);
|
||||||
|
pub const tomato: RGBA = .init(255, 99, 71, 1);
|
||||||
|
pub const transparent: RGBA = .init(0, 0, 0, 0);
|
||||||
|
pub const turquoise: RGBA = .init(64, 224, 208, 1);
|
||||||
|
pub const violet: RGBA = .init(238, 130, 238, 1);
|
||||||
|
pub const wheat: RGBA = .init(245, 222, 179, 1);
|
||||||
|
pub const whitesmoke: RGBA = .init(245, 245, 245, 1);
|
||||||
|
pub const yellowgreen: RGBA = .init(154, 205, 50, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(r: u8, g: u8, b: u8, a: f32) RGBA {
|
||||||
|
const clamped = std.math.clamp(a, 0, 1);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = @intFromFloat(clamped * 255) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds a color by its name.
|
||||||
|
pub fn find(name: []const u8) ?RGBA {
|
||||||
|
const match = std.meta.stringToEnum(std.meta.DeclEnum(Named), name) orelse return null;
|
||||||
|
|
||||||
|
return switch (match) {
|
||||||
|
inline else => |comptime_enum| @field(Named, @tagName(comptime_enum)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the given color.
|
||||||
|
/// Currently we only parse hex colors and named colors; other variants
|
||||||
|
/// require CSS evaluation.
|
||||||
|
pub fn parse(input: []const u8) !RGBA {
|
||||||
|
if (!isHexColor(input)) {
|
||||||
|
// Try named colors.
|
||||||
|
return find(input) orelse return error.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slice = input[1..];
|
||||||
|
switch (slice.len) {
|
||||||
|
// This means the digit for a color is repeated.
|
||||||
|
// Given HEX is #f0c, its interpreted the same as #FF00CC.
|
||||||
|
3 => {
|
||||||
|
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
|
||||||
|
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
|
||||||
|
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = 255 };
|
||||||
|
},
|
||||||
|
4 => {
|
||||||
|
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
|
||||||
|
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
|
||||||
|
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
|
||||||
|
const a = try std.fmt.parseInt(u8, &.{ slice[3], slice[3] }, 16);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = a };
|
||||||
|
},
|
||||||
|
// Regular HEX format.
|
||||||
|
6 => {
|
||||||
|
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
|
||||||
|
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
|
||||||
|
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = 255 };
|
||||||
|
},
|
||||||
|
8 => {
|
||||||
|
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
|
||||||
|
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
|
||||||
|
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
|
||||||
|
const a = try std.fmt.parseInt(u8, slice[6..8], 16);
|
||||||
|
return .{ .r = r, .g = g, .b = b, .a = a };
|
||||||
|
},
|
||||||
|
else => return error.Invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// By default, browsers prefer lowercase formatting.
|
||||||
|
const format_upper = false;
|
||||||
|
|
||||||
|
/// Formats the `Color` according to web expectations.
|
||||||
|
/// If color is opaque, HEX is preferred; RGBA otherwise.
|
||||||
|
pub fn format(self: *const RGBA, writer: *Io.Writer) Io.Writer.Error!void {
|
||||||
|
if (self.isOpaque()) {
|
||||||
|
// Convert RGB to HEX.
|
||||||
|
// https://gristle.tripod.com/hexconv.html
|
||||||
|
// Hexadecimal characters up to 15.
|
||||||
|
const char: []const u8 = "0123456789" ++ if (format_upper) "ABCDEF" else "abcdef";
|
||||||
|
// This variant always prefers 6 digit format, +1 is for hash char.
|
||||||
|
const buffer = [7]u8{
|
||||||
|
'#',
|
||||||
|
char[self.r >> 4],
|
||||||
|
char[self.r & 15],
|
||||||
|
char[self.g >> 4],
|
||||||
|
char[self.g & 15],
|
||||||
|
char[self.b >> 4],
|
||||||
|
char[self.b & 15],
|
||||||
|
};
|
||||||
|
|
||||||
|
return writer.writeAll(&buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer RGBA format for everything else.
|
||||||
|
return writer.print("rgba({d}, {d}, {d}, {d:.2})", .{ self.r, self.g, self.b, self.normalizedAlpha() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if `Color` is opaque.
|
||||||
|
pub inline fn isOpaque(self: *const RGBA) bool {
|
||||||
|
return self.a == std.math.maxInt(u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the normalized alpha value.
|
||||||
|
pub inline fn normalizedAlpha(self: *const RGBA) f32 {
|
||||||
|
return @as(f32, @floatFromInt(self.a)) / 255;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -319,9 +319,67 @@ pub const Document = struct {
|
|||||||
log.debug(.web_api, "not implemented", .{ .feature = "Document hasFocus" });
|
log.debug(.web_api, "not implemented", .{ .feature = "Document hasFocus" });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn _open(_: *parser.Document, page: *Page) !*parser.DocumentHTML {
|
||||||
|
if (page.open) {
|
||||||
|
return page.window.document;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This implementation is invalid.
|
||||||
|
// According to MDN, we should cleanup registered listeners.
|
||||||
|
// So we sould cleanup previous DOM memory.
|
||||||
|
// But this implementation is more simple for now.
|
||||||
|
const html_doc = try parser.documentHTMLParseFromStr("");
|
||||||
|
try page.setDocument(html_doc);
|
||||||
|
page.open = true;
|
||||||
|
|
||||||
|
return page.window.document;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _close(_: *parser.Document, page: *Page) !void {
|
||||||
|
page.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _write(self: *parser.Document, str: []const u8, page: *Page) !void {
|
||||||
|
_ = try _open(self, page);
|
||||||
|
|
||||||
|
const document = parser.documentHTMLToDocument(page.window.document);
|
||||||
|
const fragment = try parser.documentParseFragmentFromStr(document, str);
|
||||||
|
const fragment_node = parser.documentFragmentToNode(fragment);
|
||||||
|
|
||||||
|
const fragment_html = parser.nodeFirstChild(fragment_node) orelse return;
|
||||||
|
const fragment_head = parser.nodeFirstChild(fragment_html) orelse return;
|
||||||
|
const fragment_body = parser.nodeNextSibling(fragment_head) orelse return;
|
||||||
|
|
||||||
|
const document_node = parser.documentToNode(document);
|
||||||
|
const document_html = parser.nodeFirstChild(document_node) orelse return;
|
||||||
|
const document_head = parser.nodeFirstChild(document_html) orelse return;
|
||||||
|
const document_body = parser.nodeNextSibling(document_head) orelse return;
|
||||||
|
|
||||||
|
{
|
||||||
|
const children = try parser.nodeGetChildNodes(fragment_head);
|
||||||
|
// always index 0, because nodeAppendChild moves the node out of
|
||||||
|
// the nodeList and into the new tree
|
||||||
|
while (parser.nodeListItem(children, 0)) |child| {
|
||||||
|
_ = try parser.nodeAppendChild(document_head, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const children = try parser.nodeGetChildNodes(fragment_body);
|
||||||
|
// always index 0, because nodeAppendChild moves the node out of
|
||||||
|
// the nodeList and into the new tree
|
||||||
|
while (parser.nodeListItem(children, 0)) |child| {
|
||||||
|
_ = try parser.nodeAppendChild(document_body, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser: DOM.Document" {
|
test "Browser: DOM.Document" {
|
||||||
try testing.htmlRunner("dom/document.html");
|
try testing.htmlRunner("dom/document.html");
|
||||||
}
|
}
|
||||||
|
test "Browser: DOM.Document.write" {
|
||||||
|
try testing.htmlRunner("dom/document_write.html");
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ pub const EventTarget = struct {
|
|||||||
// --------
|
// --------
|
||||||
pub fn constructor(page: *Page) !*parser.EventTarget {
|
pub fn constructor(page: *Page) !*parser.EventTarget {
|
||||||
const et = try page.arena.create(EventTarget);
|
const et = try page.arena.create(EventTarget);
|
||||||
|
et.* = .{};
|
||||||
return @ptrCast(&et.base);
|
return @ptrCast(&et.base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -390,7 +390,23 @@ pub const Node = struct {
|
|||||||
return parser.nodeHasChildNodes(self);
|
return parser.nodeHasChildNodes(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_template(self: *parser.Node) !bool {
|
||||||
|
if (parser.nodeType(self) != .element) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const e = parser.nodeToElement(self);
|
||||||
|
return try parser.elementTag(e) == .template;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
|
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
|
||||||
|
// special case for template:
|
||||||
|
// > The Node.childNodes property of the <template> element is always empty
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template#usage_notes
|
||||||
|
if (try is_template(self)) {
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
|
||||||
const allocator = page.arena;
|
const allocator = page.arena;
|
||||||
var list: NodeList = .{};
|
var list: NodeList = .{};
|
||||||
|
|
||||||
|
|||||||
84
src/browser/events/PageTransitionEvent.zig
Normal file
84
src/browser/events/PageTransitionEvent.zig
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
const Window = @import("../html/window.zig").Window;
|
||||||
|
|
||||||
|
const parser = @import("../netsurf.zig");
|
||||||
|
const Event = @import("../events/event.zig").Event;
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent
|
||||||
|
const PageTransitionEvent = @This();
|
||||||
|
|
||||||
|
pub const prototype = *Event;
|
||||||
|
pub const union_make_copy = true;
|
||||||
|
|
||||||
|
pub const EventInit = struct {
|
||||||
|
persisted: ?bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
proto: parser.Event,
|
||||||
|
persisted: bool,
|
||||||
|
|
||||||
|
pub fn constructor(event_type: []const u8, opts: EventInit) !PageTransitionEvent {
|
||||||
|
const event = try parser.eventCreate();
|
||||||
|
defer parser.eventDestroy(event);
|
||||||
|
|
||||||
|
try parser.eventInit(event, event_type, .{});
|
||||||
|
parser.eventSetInternalType(event, .page_transition_event);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.proto = event.*,
|
||||||
|
.persisted = opts.persisted orelse false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageTransitionKind = enum { show, hide };
|
||||||
|
|
||||||
|
pub fn dispatch(window: *Window, kind: PageTransitionKind, persisted: bool) void {
|
||||||
|
const evt_type = switch (kind) {
|
||||||
|
.show => "pageshow",
|
||||||
|
.hide => "pagehide",
|
||||||
|
};
|
||||||
|
|
||||||
|
log.debug(.script_event, "dispatch event", .{
|
||||||
|
.type = evt_type,
|
||||||
|
.source = "navigation",
|
||||||
|
});
|
||||||
|
|
||||||
|
var evt = PageTransitionEvent.constructor(evt_type, .{ .persisted = persisted }) catch |err| {
|
||||||
|
log.err(.app, "event constructor error", .{
|
||||||
|
.err = err,
|
||||||
|
.type = evt_type,
|
||||||
|
.source = "navigation",
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = parser.eventTargetDispatchEvent(
|
||||||
|
@as(*parser.EventTarget, @ptrCast(window)),
|
||||||
|
&evt.proto,
|
||||||
|
) catch |err| {
|
||||||
|
log.err(.app, "dispatch event error", .{
|
||||||
|
.err = err,
|
||||||
|
.type = evt_type,
|
||||||
|
.source = "navigation",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
|||||||
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
|
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
|
||||||
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
|
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
|
||||||
const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent;
|
const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent;
|
||||||
const NavigateEvent = @import("../navigation/root.zig").NavigateEvent;
|
const PageTransitionEvent = @import("../events/PageTransitionEvent.zig");
|
||||||
|
|
||||||
// Event interfaces
|
// Event interfaces
|
||||||
pub const Interfaces = .{
|
pub const Interfaces = .{
|
||||||
@@ -54,7 +54,7 @@ pub const Interfaces = .{
|
|||||||
PopStateEvent,
|
PopStateEvent,
|
||||||
CompositionEvent,
|
CompositionEvent,
|
||||||
NavigationCurrentEntryChangeEvent,
|
NavigationCurrentEntryChangeEvent,
|
||||||
NavigateEvent,
|
PageTransitionEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Union = generate.Union(Interfaces);
|
pub const Union = generate.Union(Interfaces);
|
||||||
@@ -87,7 +87,7 @@ pub const Event = struct {
|
|||||||
.navigation_current_entry_change_event => .{
|
.navigation_current_entry_change_event => .{
|
||||||
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
|
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
|
||||||
},
|
},
|
||||||
.navigate_event => .{ .NavigateEvent = @as(*NavigateEvent, @ptrCast(evt)).* },
|
.page_transition_event => .{ .PageTransitionEvent = @as(*PageTransitionEvent, @ptrCast(evt)).* },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,17 +254,13 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
|
|||||||
self.body_used = true;
|
self.body_used = true;
|
||||||
|
|
||||||
if (self.body) |body| {
|
if (self.body) |body| {
|
||||||
const p = std.json.parseFromSliceLeaky(
|
const value = js.Value.fromJson(page.js, body) catch |e| {
|
||||||
std.json.Value,
|
|
||||||
page.call_arena,
|
|
||||||
body,
|
|
||||||
.{},
|
|
||||||
) catch |e| {
|
|
||||||
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
|
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
|
||||||
return error.SyntaxError;
|
return error.SyntaxError;
|
||||||
};
|
};
|
||||||
|
const pvalue = try value.persist(page.js);
|
||||||
|
|
||||||
return page.js.resolvePromise(p);
|
return page.js.resolvePromise(pvalue);
|
||||||
}
|
}
|
||||||
return page.js.resolvePromise(null);
|
return page.js.resolvePromise(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,17 +179,13 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
|
|||||||
|
|
||||||
if (self.body) |body| {
|
if (self.body) |body| {
|
||||||
self.body_used = true;
|
self.body_used = true;
|
||||||
const p = std.json.parseFromSliceLeaky(
|
const value = js.Value.fromJson(page.js, body) catch |e| {
|
||||||
std.json.Value,
|
|
||||||
page.call_arena,
|
|
||||||
body,
|
|
||||||
.{},
|
|
||||||
) catch |e| {
|
|
||||||
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
|
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
|
||||||
return error.SyntaxError;
|
return error.SyntaxError;
|
||||||
};
|
};
|
||||||
|
const pvalue = try value.persist(page.js);
|
||||||
|
|
||||||
return page.js.resolvePromise(p);
|
return page.js.resolvePromise(pvalue);
|
||||||
}
|
}
|
||||||
return page.js.resolvePromise(null);
|
return page.js.resolvePromise(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ pub fn set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_state(_: *History, page: *Page) !?js.Value {
|
pub fn get_state(_: *History, page: *Page) !?js.Value {
|
||||||
if (page.session.navigation.currentEntry().state) |state| {
|
if (page.session.navigation.currentEntry().state.value) |state| {
|
||||||
const value = try js.Value.fromJson(page.js, state);
|
const value = try js.Value.fromJson(page.js, state);
|
||||||
return value;
|
return value;
|
||||||
} else {
|
} else {
|
||||||
@@ -61,18 +61,15 @@ pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]
|
|||||||
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
||||||
|
|
||||||
const json = state.toJson(arena) catch return error.DataClone;
|
const json = state.toJson(arena) catch return error.DataClone;
|
||||||
_ = try page.session.navigation.pushEntry(url, json, page, true);
|
_ = try page.session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
||||||
const arena = page.session.arena;
|
const arena = page.session.arena;
|
||||||
|
|
||||||
const entry = page.session.navigation.currentEntry();
|
|
||||||
const json = try state.toJson(arena);
|
|
||||||
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
||||||
|
|
||||||
entry.state = json;
|
const json = try state.toJson(arena);
|
||||||
entry.url = url;
|
_ = try page.session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn go(_: *const History, delta: i32, page: *Page) !void {
|
pub fn go(_: *const History, delta: i32, page: *Page) !void {
|
||||||
@@ -89,7 +86,7 @@ pub fn go(_: *const History, delta: i32, page: *Page) !void {
|
|||||||
|
|
||||||
if (entry.url) |url| {
|
if (entry.url) |url| {
|
||||||
if (try page.isSameOrigin(url)) {
|
if (try page.isSameOrigin(url)) {
|
||||||
PopStateEvent.dispatch(entry.state, page);
|
PopStateEvent.dispatch(entry.state.value, page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ const DataSet = @import("DataSet.zig");
|
|||||||
|
|
||||||
const StyleSheet = @import("../cssom/StyleSheet.zig");
|
const StyleSheet = @import("../cssom/StyleSheet.zig");
|
||||||
const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
|
const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
|
||||||
|
const CanvasRenderingContext2D = @import("../canvas/CanvasRenderingContext2D.zig");
|
||||||
|
const WebGLRenderingContext = @import("../canvas/WebGLRenderingContext.zig");
|
||||||
|
|
||||||
|
const WalkerChildren = @import("../dom/walker.zig").WalkerChildren;
|
||||||
|
|
||||||
// HTMLElement interfaces
|
// HTMLElement interfaces
|
||||||
pub const Interfaces = .{
|
pub const Interfaces = .{
|
||||||
@@ -487,6 +491,29 @@ pub const HTMLCanvasElement = struct {
|
|||||||
pub const Self = parser.Canvas;
|
pub const Self = parser.Canvas;
|
||||||
pub const prototype = *HTMLElement;
|
pub const prototype = *HTMLElement;
|
||||||
pub const subtype = .node;
|
pub const subtype = .node;
|
||||||
|
|
||||||
|
/// This should be a union once we support other context types.
|
||||||
|
const ContextAttributes = struct {
|
||||||
|
alpha: bool,
|
||||||
|
color_space: []const u8 = "srgb",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Returns a drawing context on the canvas, or null if the context identifier
|
||||||
|
/// is not supported, or the canvas has already been set to a different context mode.
|
||||||
|
pub fn _getContext(
|
||||||
|
ctx_type: []const u8,
|
||||||
|
_: ?ContextAttributes,
|
||||||
|
) ?union(enum) { @"2d": CanvasRenderingContext2D, webgl: WebGLRenderingContext } {
|
||||||
|
if (std.mem.eql(u8, ctx_type, "2d")) {
|
||||||
|
return .{ .@"2d" = .{} };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, ctx_type, "webgl") or std.mem.eql(u8, ctx_type, "experimental-webgl")) {
|
||||||
|
return .{ .webgl = .{} };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const HTMLDListElement = struct {
|
pub const HTMLDListElement = struct {
|
||||||
@@ -1200,11 +1227,22 @@ pub const HTMLTemplateElement = struct {
|
|||||||
pub const subtype = .node;
|
pub const subtype = .node;
|
||||||
|
|
||||||
pub fn get_content(self: *parser.Template, page: *Page) !*parser.DocumentFragment {
|
pub fn get_content(self: *parser.Template, page: *Page) !*parser.DocumentFragment {
|
||||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
const n: *parser.Node = @ptrCast(@alignCast(self));
|
||||||
|
const state = try page.getOrCreateNodeState(n);
|
||||||
if (state.template_content) |tc| {
|
if (state.template_content) |tc| {
|
||||||
return tc;
|
return tc;
|
||||||
}
|
}
|
||||||
const tc = try parser.documentCreateDocumentFragment(@ptrCast(page.window.document));
|
const tc = try parser.documentCreateDocumentFragment(@ptrCast(page.window.document));
|
||||||
|
const ntc: *parser.Node = @ptrCast(@alignCast(tc));
|
||||||
|
|
||||||
|
// move existing template's childnodes to the fragment.
|
||||||
|
const walker = WalkerChildren{};
|
||||||
|
var next: ?*parser.Node = null;
|
||||||
|
while (true) {
|
||||||
|
next = try walker.get_next(n, next) orelse break;
|
||||||
|
_ = try parser.nodeAppendChild(ntc, next.?);
|
||||||
|
}
|
||||||
|
|
||||||
state.template_content = tc;
|
state.template_content = tc;
|
||||||
return tc;
|
return tc;
|
||||||
}
|
}
|
||||||
@@ -1356,3 +1394,7 @@ test "Browser: HTML.HtmlScriptElement" {
|
|||||||
test "Browser: HTML.HtmlSlotElement" {
|
test "Browser: HTML.HtmlSlotElement" {
|
||||||
try testing.htmlRunner("html/slot.html");
|
try testing.htmlRunner("html/slot.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Browser: HTML.HTMLCanvasElement" {
|
||||||
|
try testing.htmlRunner("html/canvas.html");
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ pub const Location = struct {
|
|||||||
break :blk try std.fmt.allocPrint(page.arena, "#{s}", .{hash});
|
break :blk try std.fmt.allocPrint(page.arena, "#{s}", .{hash});
|
||||||
};
|
};
|
||||||
|
|
||||||
return page.navigateFromWebAPI(normalized_hash, .{ .reason = .script }, .replace);
|
return page.navigateFromWebAPI(normalized_hash, .{ .reason = .script }, .{ .replace = null });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_protocol(self: *Location) []const u8 {
|
pub fn get_protocol(self: *Location) []const u8 {
|
||||||
@@ -96,7 +96,7 @@ pub const Location = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
|
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
|
||||||
return page.navigateFromWebAPI(url, .{ .reason = .script }, .replace);
|
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .replace = null });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _reload(_: *const Location, page: *Page) !void {
|
pub fn _reload(_: *const Location, page: *Page) !void {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const types = @import("types.zig");
|
|||||||
const Caller = @import("Caller.zig");
|
const Caller = @import("Caller.zig");
|
||||||
const NamedFunction = Caller.NamedFunction;
|
const NamedFunction = Caller.NamedFunction;
|
||||||
const PersistentObject = v8.Persistent(v8.Object);
|
const PersistentObject = v8.Persistent(v8.Object);
|
||||||
|
const PersistentValue = v8.Persistent(v8.Value);
|
||||||
const PersistentModule = v8.Persistent(v8.Module);
|
const PersistentModule = v8.Persistent(v8.Module);
|
||||||
const PersistentPromise = v8.Persistent(v8.Promise);
|
const PersistentPromise = v8.Persistent(v8.Promise);
|
||||||
const PersistentFunction = v8.Persistent(v8.Function);
|
const PersistentFunction = v8.Persistent(v8.Function);
|
||||||
@@ -70,6 +71,9 @@ identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty,
|
|||||||
// we now simply persist every time persist() is called.
|
// we now simply persist every time persist() is called.
|
||||||
js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty,
|
js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty,
|
||||||
|
|
||||||
|
// js_value_list tracks persisted js values.
|
||||||
|
js_value_list: std.ArrayListUnmanaged(PersistentValue) = .empty,
|
||||||
|
|
||||||
// Various web APIs depend on having a persistent promise resolver. They
|
// Various web APIs depend on having a persistent promise resolver. They
|
||||||
// require for this PromiseResolver to be valid for a lifetime longer than
|
// require for this PromiseResolver to be valid for a lifetime longer than
|
||||||
// the function that resolves/rejects them.
|
// the function that resolves/rejects them.
|
||||||
@@ -149,6 +153,10 @@ pub fn deinit(self: *Context) void {
|
|||||||
p.deinit();
|
p.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (self.js_value_list.items) |*p| {
|
||||||
|
p.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
for (self.persisted_promise_resolvers.items) |*p| {
|
for (self.persisted_promise_resolvers.items) |*p| {
|
||||||
p.deinit();
|
p.deinit();
|
||||||
}
|
}
|
||||||
@@ -222,63 +230,54 @@ pub fn exec(self: *Context, src: []const u8, name: ?[]const u8) !js.Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) {
|
pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) {
|
||||||
if (cacheable) {
|
const mod, const owned_url = blk: {
|
||||||
if (self.module_cache.get(url)) |entry| {
|
const arena = self.arena;
|
||||||
// The dynamic import will create an entry without the
|
|
||||||
// module to prevent multiple calls from asynchronously
|
// gop will _always_ initiated if cacheable == true
|
||||||
// loading the same module. If we're here, without the
|
var gop: std.StringHashMapUnmanaged(ModuleEntry).GetOrPutResult = undefined;
|
||||||
// module, then it's time to load it.
|
if (cacheable) {
|
||||||
if (entry.module != null) {
|
gop = try self.module_cache.getOrPut(arena, url);
|
||||||
return if (comptime want_result) entry else {};
|
if (gop.found_existing) {
|
||||||
|
if (gop.value_ptr.module != null) {
|
||||||
|
return if (comptime want_result) gop.value_ptr.* else {};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// first time seing this
|
||||||
|
gop.value_ptr.* = .{};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const m = try compileModule(self.isolate, src, url);
|
const m = try compileModule(self.isolate, src, url);
|
||||||
|
const owned_url = try arena.dupeZ(u8, url);
|
||||||
|
|
||||||
const arena = self.arena;
|
if (cacheable) {
|
||||||
const owned_url = try arena.dupe(u8, url);
|
// compileModule is synchronous - nothing can modify the cache during compilation
|
||||||
|
std.debug.assert(gop.value_ptr.module == null);
|
||||||
|
|
||||||
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
|
gop.value_ptr.module = PersistentModule.init(self.isolate, m);
|
||||||
errdefer _ = self.module_identifier.remove(m.getIdentityHash());
|
if (!gop.found_existing) {
|
||||||
|
gop.key_ptr.* = owned_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk .{ m, owned_url };
|
||||||
|
};
|
||||||
|
|
||||||
|
try self.postCompileModule(mod, owned_url);
|
||||||
|
|
||||||
const v8_context = self.v8_context;
|
const v8_context = self.v8_context;
|
||||||
{
|
if (try mod.instantiate(v8_context, resolveModuleCallback) == false) {
|
||||||
// Non-async modules are blocking. We can download them in
|
|
||||||
// parallel, but they need to be processed serially. So we
|
|
||||||
// want to get the list of dependent modules this module has
|
|
||||||
// and start downloading them asap.
|
|
||||||
const requests = m.getModuleRequests();
|
|
||||||
for (0..requests.length()) |i| {
|
|
||||||
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
|
|
||||||
const specifier = try self.jsStringToZig(req.getSpecifier(), .{});
|
|
||||||
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
|
|
||||||
self.call_arena,
|
|
||||||
specifier,
|
|
||||||
owned_url,
|
|
||||||
);
|
|
||||||
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
|
|
||||||
if (!gop.found_existing) {
|
|
||||||
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
|
|
||||||
gop.key_ptr.* = owned_specifier;
|
|
||||||
gop.value_ptr.* = .{};
|
|
||||||
try self.script_manager.?.preloadImport(owned_specifier, url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
|
|
||||||
return error.ModuleInstantiationError;
|
return error.ModuleInstantiationError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const evaluated = m.evaluate(v8_context) catch {
|
const evaluated = mod.evaluate(v8_context) catch {
|
||||||
std.debug.assert(m.getStatus() == .kErrored);
|
std.debug.assert(mod.getStatus() == .kErrored);
|
||||||
|
|
||||||
// Some module-loading errors aren't handled by TryCatch. We need to
|
// Some module-loading errors aren't handled by TryCatch. We need to
|
||||||
// get the error from the module itself.
|
// get the error from the module itself.
|
||||||
log.warn(.js, "evaluate module", .{
|
log.warn(.js, "evaluate module", .{
|
||||||
.specifier = owned_url,
|
.specifier = owned_url,
|
||||||
.message = self.valueToString(m.getException(), .{}) catch "???",
|
.message = self.valueToString(mod.getException(), .{}) catch "???",
|
||||||
});
|
});
|
||||||
return error.EvaluationError;
|
return error.EvaluationError;
|
||||||
};
|
};
|
||||||
@@ -301,28 +300,46 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
|
|||||||
// be cached
|
// be cached
|
||||||
std.debug.assert(cacheable);
|
std.debug.assert(cacheable);
|
||||||
|
|
||||||
const persisted_module = PersistentModule.init(self.isolate, m);
|
// entry has to have been created atop this function
|
||||||
const persisted_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
|
const entry = self.module_cache.getPtr(owned_url).?;
|
||||||
|
|
||||||
var gop = try self.module_cache.getOrPut(arena, owned_url);
|
// and the module must have been set after we compiled it
|
||||||
if (gop.found_existing) {
|
std.debug.assert(entry.module != null);
|
||||||
// If we're here, it's because we had a cache entry, but no
|
std.debug.assert(entry.module_promise == null);
|
||||||
// module. This happens because both our synch and async
|
|
||||||
// module loaders create the entry to prevent concurrent
|
|
||||||
// loads of the same resource (like Go's Singleflight).
|
|
||||||
std.debug.assert(gop.value_ptr.module == null);
|
|
||||||
std.debug.assert(gop.value_ptr.module_promise == null);
|
|
||||||
|
|
||||||
gop.value_ptr.module = persisted_module;
|
entry.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
|
||||||
gop.value_ptr.module_promise = persisted_promise;
|
return if (comptime want_result) entry.* else {};
|
||||||
} else {
|
}
|
||||||
gop.value_ptr.* = ModuleEntry{
|
|
||||||
.module = persisted_module,
|
// After we compile a module, whether it's a top-level one, or a nested one,
|
||||||
.module_promise = persisted_promise,
|
// we always want to track its identity (so that, if this module imports other
|
||||||
.resolver_promise = null,
|
// modules, we can resolve the full URL), and preload any dependent modules.
|
||||||
};
|
fn postCompileModule(self: *Context, mod: v8.Module, url: [:0]const u8) !void {
|
||||||
|
try self.module_identifier.putNoClobber(self.arena, mod.getIdentityHash(), url);
|
||||||
|
|
||||||
|
const v8_context = self.v8_context;
|
||||||
|
|
||||||
|
// Non-async modules are blocking. We can download them in parallel, but
|
||||||
|
// they need to be processed serially. So we want to get the list of
|
||||||
|
// dependent modules this module has and start downloading them asap.
|
||||||
|
const requests = mod.getModuleRequests();
|
||||||
|
const script_manager = self.script_manager.?;
|
||||||
|
for (0..requests.length()) |i| {
|
||||||
|
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
|
||||||
|
const specifier = try self.jsStringToZig(req.getSpecifier(), .{});
|
||||||
|
const normalized_specifier = try script_manager.resolveSpecifier(
|
||||||
|
self.call_arena,
|
||||||
|
specifier,
|
||||||
|
url,
|
||||||
|
);
|
||||||
|
const nested_gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
|
||||||
|
if (!nested_gop.found_existing) {
|
||||||
|
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
|
||||||
|
nested_gop.key_ptr.* = owned_specifier;
|
||||||
|
nested_gop.value_ptr.* = .{};
|
||||||
|
try script_manager.preloadImport(owned_specifier, url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return if (comptime want_result) gop.value_ptr.* else {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// == Creators ==
|
// == Creators ==
|
||||||
@@ -400,9 +417,8 @@ pub fn zigValueToJs(self: *Context, value: anytype) !v8.Value {
|
|||||||
},
|
},
|
||||||
.pointer => |ptr| switch (ptr.size) {
|
.pointer => |ptr| switch (ptr.size) {
|
||||||
.one => {
|
.one => {
|
||||||
const type_name = @typeName(ptr.child);
|
if (types.has(ptr.child)) {
|
||||||
if (@hasField(types.Lookup, type_name)) {
|
const template = self.templates[types.getId(ptr.child)];
|
||||||
const template = self.templates[@field(types.LOOKUP, type_name)];
|
|
||||||
const js_obj = try self.mapZigInstanceToJs(template, value);
|
const js_obj = try self.mapZigInstanceToJs(template, value);
|
||||||
return js_obj.toValue();
|
return js_obj.toValue();
|
||||||
}
|
}
|
||||||
@@ -436,9 +452,8 @@ pub fn zigValueToJs(self: *Context, value: anytype) !v8.Value {
|
|||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
.@"struct" => |s| {
|
.@"struct" => |s| {
|
||||||
const type_name = @typeName(T);
|
if (types.has(T)) {
|
||||||
if (@hasField(types.Lookup, type_name)) {
|
const template = self.templates[types.getId(T)];
|
||||||
const template = self.templates[@field(types.LOOKUP, type_name)];
|
|
||||||
const js_obj = try self.mapZigInstanceToJs(template, value);
|
const js_obj = try self.mapZigInstanceToJs(template, value);
|
||||||
return js_obj.toValue();
|
return js_obj.toValue();
|
||||||
}
|
}
|
||||||
@@ -574,8 +589,7 @@ pub fn mapZigInstanceToJs(self: *Context, js_obj_or_template: anytype, value: an
|
|||||||
// well as any meta data we'll need to use it later.
|
// well as any meta data we'll need to use it later.
|
||||||
// See the TaggedAnyOpaque struct for more details.
|
// See the TaggedAnyOpaque struct for more details.
|
||||||
const tao = try arena.create(TaggedAnyOpaque);
|
const tao = try arena.create(TaggedAnyOpaque);
|
||||||
const meta_index = @field(types.LOOKUP, @typeName(ptr.child));
|
const meta = self.meta_lookup[types.getId(ptr.child)];
|
||||||
const meta = self.meta_lookup[meta_index];
|
|
||||||
|
|
||||||
tao.* = .{
|
tao.* = .{
|
||||||
.ptr = value,
|
.ptr = value,
|
||||||
@@ -655,7 +669,7 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
|
|||||||
if (!js_value.isObject()) {
|
if (!js_value.isObject()) {
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
}
|
}
|
||||||
if (@hasField(types.Lookup, @typeName(ptr.child))) {
|
if (types.has(ptr.child)) {
|
||||||
const js_obj = js_value.castTo(v8.Object);
|
const js_obj = js_value.castTo(v8.Object);
|
||||||
return self.typeTaggedAnyOpaque(named_function, *types.Receiver(ptr.child), js_obj);
|
return self.typeTaggedAnyOpaque(named_function, *types.Receiver(ptr.child), js_obj);
|
||||||
}
|
}
|
||||||
@@ -771,55 +785,55 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
|
|||||||
// Extracted so that it can be used in both jsValueToZig and in
|
// Extracted so that it can be used in both jsValueToZig and in
|
||||||
// probeJsValueToZig. Avoids having to duplicate this logic when probing.
|
// probeJsValueToZig. Avoids having to duplicate this logic when probing.
|
||||||
fn jsValueToStruct(self: *Context, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !?T {
|
fn jsValueToStruct(self: *Context, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !?T {
|
||||||
if (T == js.Function) {
|
return switch (T) {
|
||||||
if (!js_value.isFunction()) {
|
js.Function => {
|
||||||
return null;
|
if (!js_value.isFunction()) {
|
||||||
}
|
return null;
|
||||||
return try self.createFunction(js_value);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
|
return try self.createFunction(js_value);
|
||||||
const VT = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child;
|
},
|
||||||
const arr = (try self.jsValueToTypedArray(VT, js_value)) orelse return null;
|
// zig fmt: off
|
||||||
return .{ .values = arr };
|
js.TypedArray(u8), js.TypedArray(u16), js.TypedArray(u32), js.TypedArray(u64),
|
||||||
}
|
js.TypedArray(i8), js.TypedArray(i16), js.TypedArray(i32), js.TypedArray(i64),
|
||||||
|
js.TypedArray(f32), js.TypedArray(f64),
|
||||||
if (T == js.String) {
|
// zig fmt: on
|
||||||
return .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) };
|
=> {
|
||||||
}
|
const ValueType = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child;
|
||||||
|
const slice = (try self.jsValueToTypedArray(ValueType, js_value)) orelse return null;
|
||||||
const js_obj = js_value.castTo(v8.Object);
|
return .{ .values = slice };
|
||||||
|
},
|
||||||
if (comptime T == js.Object) {
|
js.String => .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) },
|
||||||
// Caller wants an opaque js.Object. Probably a parameter
|
// Caller wants an opaque js.Object. Probably a parameter
|
||||||
// that it needs to pass back into a callback
|
// that it needs to pass back into a callback.
|
||||||
return js.Object{
|
js.Object => js.Object{
|
||||||
.js_obj = js_obj,
|
.js_obj = js_value.castTo(v8.Object),
|
||||||
.context = self,
|
.context = self,
|
||||||
};
|
},
|
||||||
}
|
else => {
|
||||||
|
const js_obj = js_value.castTo(v8.Object);
|
||||||
|
if (!js_value.isObject()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!js_value.isObject()) {
|
const v8_context = self.v8_context;
|
||||||
return null;
|
const isolate = self.isolate;
|
||||||
}
|
var value: T = undefined;
|
||||||
|
inline for (@typeInfo(T).@"struct".fields) |field| {
|
||||||
const v8_context = self.v8_context;
|
const name = field.name;
|
||||||
const isolate = self.isolate;
|
const key = v8.String.initUtf8(isolate, name);
|
||||||
|
if (js_obj.has(v8_context, key.toValue())) {
|
||||||
var value: T = undefined;
|
@field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(v8_context, key));
|
||||||
inline for (@typeInfo(T).@"struct".fields) |field| {
|
} else if (@typeInfo(field.type) == .optional) {
|
||||||
const name = field.name;
|
@field(value, name) = null;
|
||||||
const key = v8.String.initUtf8(isolate, name);
|
} else {
|
||||||
if (js_obj.has(v8_context, key.toValue())) {
|
const dflt = field.defaultValue() orelse return null;
|
||||||
@field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(v8_context, key));
|
@field(value, name) = dflt;
|
||||||
} else if (@typeInfo(field.type) == .optional) {
|
}
|
||||||
@field(value, name) = null;
|
}
|
||||||
} else {
|
return value;
|
||||||
const dflt = field.defaultValue() orelse return null;
|
},
|
||||||
@field(value, name) = dflt;
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn jsValueToTypedArray(_: *Context, comptime T: type, js_value: v8.Value) !?[]T {
|
fn jsValueToTypedArray(_: *Context, comptime T: type, js_value: v8.Value) !?[]T {
|
||||||
@@ -1189,31 +1203,14 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
|
|||||||
};
|
};
|
||||||
|
|
||||||
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
|
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
|
||||||
self.arena, // might need to survive until the module is loaded
|
self.arena,
|
||||||
specifier,
|
specifier,
|
||||||
referrer_path,
|
referrer_path,
|
||||||
);
|
);
|
||||||
|
|
||||||
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
|
const entry = self.module_cache.getPtr(normalized_specifier).?;
|
||||||
if (gop.found_existing) {
|
if (entry.module) |m| {
|
||||||
if (gop.value_ptr.module) |m| {
|
return m.castToModule().handle;
|
||||||
return m.handle;
|
|
||||||
}
|
|
||||||
// We don't have a module, but we do have a cache entry for it
|
|
||||||
// That means we're already trying to load it. We just have
|
|
||||||
// to wait for it to be done.
|
|
||||||
} else {
|
|
||||||
// I don't think it's possible for us to be here. This is
|
|
||||||
// only ever called by v8 when we evaluate a module. But
|
|
||||||
// before evaluating, we should have already started
|
|
||||||
// downloading all of the module's nested modules. So it
|
|
||||||
// should be impossible that this is the first time we've
|
|
||||||
// heard about this module.
|
|
||||||
// But, I'm not confident enough in that, and ther's little
|
|
||||||
// harm in handling this case.
|
|
||||||
@branchHint(.unlikely);
|
|
||||||
gop.value_ptr.* = .{};
|
|
||||||
try self.script_manager.?.preloadImport(normalized_specifier, referrer_path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var source = try self.script_manager.?.waitForImport(normalized_specifier);
|
var source = try self.script_manager.?.waitForImport(normalized_specifier);
|
||||||
@@ -1223,26 +1220,10 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
|
|||||||
try_catch.init(self);
|
try_catch.init(self);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
const entry = self.module(true, source.src(), normalized_specifier, true) catch |err| {
|
const mod = try compileModule(self.isolate, source.src(), normalized_specifier);
|
||||||
switch (err) {
|
try self.postCompileModule(mod, normalized_specifier);
|
||||||
error.EvaluationError => {
|
entry.module = PersistentModule.init(self.isolate, mod);
|
||||||
// This is a sentinel value telling us that the error was already
|
return entry.module.?.castToModule().handle;
|
||||||
// logged. Some module-loading errors aren't captured by Try/Catch.
|
|
||||||
// We need to handle those errors differently, where the module
|
|
||||||
// exists.
|
|
||||||
},
|
|
||||||
else => log.warn(.js, "compile resolved module", .{
|
|
||||||
.specifier = normalized_specifier,
|
|
||||||
.stack = try_catch.stack(self.call_arena) catch null,
|
|
||||||
.src = try_catch.sourceLine(self.call_arena) catch "err",
|
|
||||||
.line = try_catch.sourceLineNumber() orelse 0,
|
|
||||||
.exception = (try_catch.exception(self.call_arena) catch @errorName(err)) orelse @errorName(err),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
// entry.module is always set when returning from self.module()
|
|
||||||
return entry.module.?.handle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Will get passed to ScriptManager and then passed back to us when
|
// Will get passed to ScriptManager and then passed back to us when
|
||||||
@@ -1317,7 +1298,32 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
|
|||||||
// `dynamicModuleSourceCallback`, but we can skip some steps
|
// `dynamicModuleSourceCallback`, but we can skip some steps
|
||||||
// since the module is alrady loaded,
|
// since the module is alrady loaded,
|
||||||
std.debug.assert(gop.value_ptr.module != null);
|
std.debug.assert(gop.value_ptr.module != null);
|
||||||
std.debug.assert(gop.value_ptr.module_promise != null);
|
|
||||||
|
// If the module hasn't been evaluated yet (it was only instantiated
|
||||||
|
// as a static import dependency), we need to evaluate it now.
|
||||||
|
if (gop.value_ptr.module_promise == null) {
|
||||||
|
const mod = gop.value_ptr.module.?.castToModule();
|
||||||
|
const status = mod.getStatus();
|
||||||
|
if (status == .kEvaluated or status == .kEvaluating) {
|
||||||
|
// Module was already evaluated (shouldn't normally happen, but handle it).
|
||||||
|
// Create a pre-resolved promise with the module namespace.
|
||||||
|
const persisted_module_resolver = v8.Persistent(v8.PromiseResolver).init(isolate, v8.PromiseResolver.init(self.v8_context));
|
||||||
|
try self.persisted_promise_resolvers.append(self.arena, persisted_module_resolver);
|
||||||
|
var module_resolver = persisted_module_resolver.castToPromiseResolver();
|
||||||
|
_ = module_resolver.resolve(self.v8_context, mod.getModuleNamespace());
|
||||||
|
gop.value_ptr.module_promise = PersistentPromise.init(self.isolate, module_resolver.getPromise());
|
||||||
|
} else {
|
||||||
|
// the module was loaded, but not evaluated, we _have_ to evaluate it now
|
||||||
|
const evaluated = mod.evaluate(self.v8_context) catch {
|
||||||
|
std.debug.assert(status == .kErrored);
|
||||||
|
const error_msg = v8.String.initUtf8(isolate, "Module evaluation failed");
|
||||||
|
_ = resolver.reject(self.v8_context, error_msg.toValue());
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
std.debug.assert(evaluated.isPromise());
|
||||||
|
gop.value_ptr.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// like before, we want to set this up so that if anything else
|
// like before, we want to set this up so that if anything else
|
||||||
// tries to load this module, it can just return our promise
|
// tries to load this module, it can just return our promise
|
||||||
@@ -1454,14 +1460,13 @@ pub fn typeTaggedAnyOpaque(self: *const Context, comptime named_function: NamedF
|
|||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
}
|
}
|
||||||
|
|
||||||
const type_name = @typeName(T);
|
if (!types.has(T)) {
|
||||||
if (@hasField(types.Lookup, type_name) == false) {
|
|
||||||
@compileError(named_function.full_name ++ "has an unknown Zig type: " ++ @typeName(R));
|
@compileError(named_function.full_name ++ "has an unknown Zig type: " ++ @typeName(R));
|
||||||
}
|
}
|
||||||
|
|
||||||
const op = js_obj.getInternalField(0).castTo(v8.External).get();
|
const op = js_obj.getInternalField(0).castTo(v8.External).get();
|
||||||
const tao: *TaggedAnyOpaque = @ptrCast(@alignCast(op));
|
const tao: *TaggedAnyOpaque = @ptrCast(@alignCast(op));
|
||||||
const expected_type_index = @field(types.LOOKUP, type_name);
|
const expected_type_index = types.getId(T);
|
||||||
|
|
||||||
var type_index = tao.index;
|
var type_index = tao.index;
|
||||||
if (type_index == expected_type_index) {
|
if (type_index == expected_type_index) {
|
||||||
@@ -1489,7 +1494,7 @@ pub fn typeTaggedAnyOpaque(self: *const Context, comptime named_function: NamedF
|
|||||||
total_offset += @intCast(proto_offset);
|
total_offset += @intCast(proto_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prototype_index = types.PROTOTYPE_TABLE[type_index];
|
const prototype_index = types.PrototypeTable[type_index];
|
||||||
if (prototype_index == expected_type_index) {
|
if (prototype_index == expected_type_index) {
|
||||||
return @ptrFromInt(base_ptr + total_offset);
|
return @ptrFromInt(base_ptr + total_offset);
|
||||||
}
|
}
|
||||||
@@ -1582,7 +1587,7 @@ fn probeJsValueToZig(self: *Context, comptime named_function: NamedFunction, com
|
|||||||
if (!js_value.isObject()) {
|
if (!js_value.isObject()) {
|
||||||
return .{ .invalid = {} };
|
return .{ .invalid = {} };
|
||||||
}
|
}
|
||||||
if (@hasField(types.Lookup, @typeName(ptr.child))) {
|
if (types.has(ptr.child)) {
|
||||||
const js_obj = js_value.castTo(v8.Object);
|
const js_obj = js_value.castTo(v8.Object);
|
||||||
// There's a bit of overhead in doing this, so instead
|
// There's a bit of overhead in doing this, so instead
|
||||||
// of having a version of typeTaggedAnyOpaque which
|
// of having a version of typeTaggedAnyOpaque which
|
||||||
|
|||||||
@@ -111,16 +111,14 @@ pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
|
|||||||
const Struct = s.defaultValue().?;
|
const Struct = s.defaultValue().?;
|
||||||
if (@hasDecl(Struct, "prototype")) {
|
if (@hasDecl(Struct, "prototype")) {
|
||||||
const TI = @typeInfo(Struct.prototype);
|
const TI = @typeInfo(Struct.prototype);
|
||||||
const proto_name = @typeName(types.Receiver(TI.pointer.child));
|
const ProtoType = types.Receiver(TI.pointer.child);
|
||||||
if (@hasField(types.Lookup, proto_name) == false) {
|
if (!types.has(ProtoType)) {
|
||||||
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ proto_name, @typeName(Struct) }));
|
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ @typeName(ProtoType), @typeName(Struct) }));
|
||||||
}
|
}
|
||||||
// Hey, look! This is our first real usage of the types.LOOKUP.
|
// Hey, look! This is our first real usage of the `types.Index`.
|
||||||
// Just like we said above, given a type, we can get its
|
// Just like we said above, given a type, we can get its
|
||||||
// template index.
|
// template index.
|
||||||
|
templates[i].inherit(templates[types.getId(ProtoType)]);
|
||||||
const proto_index = @field(types.LOOKUP, proto_name);
|
|
||||||
templates[i].inherit(templates[proto_index]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// while we're here, let's populate our meta lookup
|
// while we're here, let's populate our meta lookup
|
||||||
|
|||||||
@@ -104,10 +104,8 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_cal
|
|||||||
// though it's also a Window, we need to set the prototype for this
|
// though it's also a Window, we need to set the prototype for this
|
||||||
// specific instance of the the Window.
|
// specific instance of the the Window.
|
||||||
if (@hasDecl(Global, "prototype")) {
|
if (@hasDecl(Global, "prototype")) {
|
||||||
const proto_type = types.Receiver(@typeInfo(Global.prototype).pointer.child);
|
const ProtoType = types.Receiver(@typeInfo(Global.prototype).pointer.child);
|
||||||
const proto_name = @typeName(proto_type);
|
js_global.inherit(templates[types.getId(ProtoType)]);
|
||||||
const proto_index = @field(types.LOOKUP, proto_name);
|
|
||||||
js_global.inherit(templates[proto_index]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const context_local = v8.Context.init(isolate, global_template, null);
|
const context_local = v8.Context.init(isolate, global_template, null);
|
||||||
@@ -123,14 +121,12 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_cal
|
|||||||
const Struct = s.defaultValue().?;
|
const Struct = s.defaultValue().?;
|
||||||
|
|
||||||
if (@hasDecl(Struct, "prototype")) {
|
if (@hasDecl(Struct, "prototype")) {
|
||||||
const proto_type = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
|
const ProtoType = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
|
||||||
const proto_name = @typeName(proto_type);
|
if (!types.has(ProtoType)) {
|
||||||
if (@hasField(types.Lookup, proto_name) == false) {
|
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ @typeName(ProtoType));
|
||||||
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const proto_index = @field(types.LOOKUP, proto_name);
|
const proto_obj = templates[types.getId(ProtoType)].getFunction(v8_context).toObject();
|
||||||
const proto_obj = templates[proto_index].getFunction(v8_context).toObject();
|
|
||||||
|
|
||||||
const self_obj = templates[i].getFunction(v8_context).toObject();
|
const self_obj = templates[i].getFunction(v8_context).toObject();
|
||||||
_ = self_obj.setPrototype(v8_context, proto_obj);
|
_ = self_obj.setPrototype(v8_context, proto_obj);
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ const NamedFunction = Context.NamedFunction;
|
|||||||
// Env.JsObject. Want a TypedArray? Env.TypedArray.
|
// Env.JsObject. Want a TypedArray? Env.TypedArray.
|
||||||
pub fn TypedArray(comptime T: type) type {
|
pub fn TypedArray(comptime T: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
pub const _TYPED_ARRAY_ID_KLUDGE = true;
|
|
||||||
|
|
||||||
values: []const T,
|
values: []const T,
|
||||||
|
|
||||||
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
|
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
|
||||||
@@ -150,6 +148,8 @@ pub const Exception = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Value = struct {
|
pub const Value = struct {
|
||||||
|
const PersistentValue = v8.Persistent(v8.Value);
|
||||||
|
|
||||||
value: v8.Value,
|
value: v8.Value,
|
||||||
context: *const Context,
|
context: *const Context,
|
||||||
|
|
||||||
@@ -163,6 +163,15 @@ pub const Value = struct {
|
|||||||
const value = try v8.Json.parse(ctx.v8_context, json_string);
|
const value = try v8.Json.parse(ctx.v8_context, json_string);
|
||||||
return Value{ .context = ctx, .value = value };
|
return Value{ .context = ctx, .value = value };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn persist(self: Value, context: *Context) !Value {
|
||||||
|
const js_value = self.value;
|
||||||
|
|
||||||
|
const persisted = PersistentValue.init(context.isolate, js_value);
|
||||||
|
try context.js_value_list.append(context.arena, persisted);
|
||||||
|
|
||||||
|
return Value{ .context = context, .value = persisted.toValue() };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ValueIterator = struct {
|
pub const ValueIterator = struct {
|
||||||
@@ -327,68 +336,73 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
|
|||||||
return v8.initNull(isolate).toValue();
|
return v8.initNull(isolate).toValue();
|
||||||
},
|
},
|
||||||
.@"struct" => {
|
.@"struct" => {
|
||||||
const T = @TypeOf(value);
|
switch (@TypeOf(value)) {
|
||||||
|
ArrayBuffer => {
|
||||||
if (T == ArrayBuffer) {
|
const values = value.values;
|
||||||
const values = value.values;
|
const len = values.len;
|
||||||
const len = values.len;
|
var array_buffer: v8.ArrayBuffer = undefined;
|
||||||
var array_buffer: v8.ArrayBuffer = undefined;
|
const backing_store = v8.BackingStore.init(isolate, len);
|
||||||
const backing_store = v8.BackingStore.init(isolate, len);
|
|
||||||
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
|
|
||||||
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
|
|
||||||
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
|
||||||
|
|
||||||
return .{ .handle = array_buffer.handle };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
|
|
||||||
const values = value.values;
|
|
||||||
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
|
|
||||||
const len = values.len;
|
|
||||||
const bits = switch (@typeInfo(value_type)) {
|
|
||||||
.int => |n| n.bits,
|
|
||||||
.float => |f| f.bits,
|
|
||||||
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
|
|
||||||
};
|
|
||||||
|
|
||||||
var array_buffer: v8.ArrayBuffer = undefined;
|
|
||||||
if (len == 0) {
|
|
||||||
array_buffer = v8.ArrayBuffer.init(isolate, 0);
|
|
||||||
} else {
|
|
||||||
const buffer_len = len * bits / 8;
|
|
||||||
const backing_store = v8.BackingStore.init(isolate, buffer_len);
|
|
||||||
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
|
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
|
||||||
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
|
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
|
||||||
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
||||||
}
|
|
||||||
|
|
||||||
switch (@typeInfo(value_type)) {
|
return .{ .handle = array_buffer.handle };
|
||||||
.int => |n| switch (n.signedness) {
|
},
|
||||||
.unsigned => switch (n.bits) {
|
// zig fmt: off
|
||||||
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
|
TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64),
|
||||||
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
|
TypedArray(i8), TypedArray(i16), TypedArray(i32), TypedArray(i64),
|
||||||
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
|
TypedArray(f32), TypedArray(f64),
|
||||||
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
|
// zig fmt: on
|
||||||
|
=> {
|
||||||
|
const values = value.values;
|
||||||
|
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
|
||||||
|
const len = values.len;
|
||||||
|
const bits = switch (@typeInfo(value_type)) {
|
||||||
|
.int => |n| n.bits,
|
||||||
|
.float => |f| f.bits,
|
||||||
|
else => @compileError("Invalid TypedArray type: " ++ @typeName(value_type)),
|
||||||
|
};
|
||||||
|
|
||||||
|
var array_buffer: v8.ArrayBuffer = undefined;
|
||||||
|
if (len == 0) {
|
||||||
|
array_buffer = v8.ArrayBuffer.init(isolate, 0);
|
||||||
|
} else {
|
||||||
|
const buffer_len = len * bits / 8;
|
||||||
|
const backing_store = v8.BackingStore.init(isolate, buffer_len);
|
||||||
|
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
|
||||||
|
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
|
||||||
|
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (@typeInfo(value_type)) {
|
||||||
|
.int => |n| switch (n.signedness) {
|
||||||
|
.unsigned => switch (n.bits) {
|
||||||
|
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
.signed => switch (n.bits) {
|
||||||
|
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.float => |f| switch (f.bits) {
|
||||||
|
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
|
||||||
|
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
|
||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
.signed => switch (n.bits) {
|
|
||||||
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
|
|
||||||
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
|
|
||||||
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
|
|
||||||
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
|
|
||||||
else => {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
.float => |f| switch (f.bits) {
|
|
||||||
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
|
|
||||||
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
|
|
||||||
else => {},
|
else => {},
|
||||||
},
|
}
|
||||||
else => {},
|
// We normally don't fail in this function unless fail == true
|
||||||
}
|
// but this can never be valid.
|
||||||
// We normally don't fail in this function unless fail == true
|
@compileError("Invalid TypedArray type: " ++ @typeName(value_type));
|
||||||
// but this can never be valid.
|
},
|
||||||
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
|
else => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail),
|
.@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const Interfaces = generate.Tuple(.{
|
|||||||
@import("../xhr/xhr.zig").Interfaces,
|
@import("../xhr/xhr.zig").Interfaces,
|
||||||
@import("../navigation/root.zig").Interfaces,
|
@import("../navigation/root.zig").Interfaces,
|
||||||
@import("../file/root.zig").Interfaces,
|
@import("../file/root.zig").Interfaces,
|
||||||
|
@import("../canvas/root.zig").Interfaces,
|
||||||
@import("../xhr/form_data.zig").Interfaces,
|
@import("../xhr/form_data.zig").Interfaces,
|
||||||
@import("../xmlserializer/xmlserializer.zig").Interfaces,
|
@import("../xmlserializer/xmlserializer.zig").Interfaces,
|
||||||
@import("../fetch/fetch.zig").Interfaces,
|
@import("../fetch/fetch.zig").Interfaces,
|
||||||
@@ -26,104 +27,127 @@ const Interfaces = generate.Tuple(.{
|
|||||||
|
|
||||||
pub const Types = @typeInfo(Interfaces).@"struct".fields;
|
pub const Types = @typeInfo(Interfaces).@"struct".fields;
|
||||||
|
|
||||||
// Imagine we have a type Cat which has a getter:
|
/// Integer type we use for `Index` enum. Can be u8 at min.
|
||||||
//
|
pub const BackingInt = std.math.IntFittingRange(0, @max(std.math.maxInt(u8), Types.len));
|
||||||
// fn get_owner(self: *Cat) *Owner {
|
|
||||||
// return self.owner;
|
/// Imagine we have a type `Cat` which has a getter:
|
||||||
// }
|
///
|
||||||
//
|
/// fn get_owner(self: *Cat) *Owner {
|
||||||
// When we execute caller.getter, we'll end up doing something like:
|
/// return self.owner;
|
||||||
// const res = @call(.auto, Cat.get_owner, .{cat_instance});
|
/// }
|
||||||
//
|
///
|
||||||
// How do we turn `res`, which is an *Owner, into something we can return
|
/// When we execute `caller.getter`, we'll end up doing something like:
|
||||||
// to v8? We need the ObjectTemplate associated with Owner. How do we
|
///
|
||||||
// get that? Well, we store all the ObjectTemplates in an array that's
|
/// const res = @call(.auto, Cat.get_owner, .{cat_instance});
|
||||||
// tied to env. So we do something like:
|
///
|
||||||
//
|
/// How do we turn `res`, which is an *Owner, into something we can return
|
||||||
// env.templates[index_of_owner].initInstance(...);
|
/// to v8? We need the ObjectTemplate associated with Owner. How do we
|
||||||
//
|
/// get that? Well, we store all the ObjectTemplates in an array that's
|
||||||
// But how do we get that `index_of_owner`? `Lookup` is a struct
|
/// tied to env. So we do something like:
|
||||||
// that looks like:
|
///
|
||||||
//
|
/// env.templates[index_of_owner].initInstance(...);
|
||||||
// const Lookup = struct {
|
///
|
||||||
// comptime cat: usize = 0,
|
/// But how do we get that `index_of_owner`? `Index` is an enum
|
||||||
// comptime owner: usize = 1,
|
/// that looks like:
|
||||||
// ...
|
///
|
||||||
// }
|
/// pub const Index = enum(BackingInt) {
|
||||||
//
|
/// cat = 0,
|
||||||
// So to get the template index of `owner`, we can do:
|
/// owner = 1,
|
||||||
//
|
/// ...
|
||||||
// const index_id = @field(type_lookup, @typeName(@TypeOf(res));
|
/// }
|
||||||
//
|
///
|
||||||
pub const Lookup = blk: {
|
/// (`BackingInt` is calculated at comptime regarding to interfaces we have)
|
||||||
var fields: [Types.len]std.builtin.Type.StructField = undefined;
|
/// So to get the template index of `owner`, simply do:
|
||||||
|
///
|
||||||
|
/// const index_id = types.getId(@TypeOf(res));
|
||||||
|
pub const Index = blk: {
|
||||||
|
var fields: [Types.len]std.builtin.Type.EnumField = undefined;
|
||||||
for (Types, 0..) |s, i| {
|
for (Types, 0..) |s, i| {
|
||||||
|
|
||||||
// This prototype type check has nothing to do with building our
|
|
||||||
// Lookup. But we put it here, early, so that the rest of the
|
|
||||||
// code doesn't have to worry about checking if Struct.prototype is
|
|
||||||
// a pointer.
|
|
||||||
const Struct = s.defaultValue().?;
|
const Struct = s.defaultValue().?;
|
||||||
if (@hasDecl(Struct, "prototype") and @typeInfo(Struct.prototype) != .pointer) {
|
fields[i] = .{ .name = @typeName(Receiver(Struct)), .value = i };
|
||||||
@compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s} must be a pointer", .{ @typeName(Struct.prototype), @typeName(Struct) }));
|
|
||||||
}
|
|
||||||
|
|
||||||
fields[i] = .{
|
|
||||||
.name = @typeName(Receiver(Struct)),
|
|
||||||
.type = usize,
|
|
||||||
.is_comptime = true,
|
|
||||||
.alignment = @alignOf(usize),
|
|
||||||
.default_value_ptr = &i,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
break :blk @Type(.{ .@"struct" = .{
|
|
||||||
.layout = .auto,
|
break :blk @Type(.{
|
||||||
.decls = &.{},
|
.@"enum" = .{
|
||||||
.is_tuple = false,
|
.fields = &fields,
|
||||||
.fields = &fields,
|
.tag_type = BackingInt,
|
||||||
} });
|
.is_exhaustive = true,
|
||||||
|
.decls = &.{},
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const LOOKUP = Lookup{};
|
/// Returns a boolean indicating if a type exist in the `Index`.
|
||||||
|
pub inline fn has(t: type) bool {
|
||||||
|
return @hasField(Index, @typeName(t));
|
||||||
|
}
|
||||||
|
|
||||||
// Creates a list where the index of a type contains its prototype index
|
/// Returns the `Index` for the given type.
|
||||||
// const Animal = struct{};
|
pub inline fn getIndex(t: type) Index {
|
||||||
// const Cat = struct{
|
return @field(Index, @typeName(t));
|
||||||
// pub const prototype = *Animal;
|
}
|
||||||
// };
|
|
||||||
//
|
/// Returns the ID for the given type.
|
||||||
// Would create an array: [0, 0]
|
pub inline fn getId(t: type) BackingInt {
|
||||||
// Animal, at index, 0, has no prototype, so we set it to itself
|
return @intFromEnum(getIndex(t));
|
||||||
// Cat, at index 1, has an Animal prototype, so we set it to 0.
|
}
|
||||||
//
|
|
||||||
// When we're trying to pass an argument to a Zig function, we'll know the
|
/// Creates a list where the index of a type contains its prototype index.
|
||||||
// target type (the function parameter type), and we'll have a
|
/// const Animal = struct{};
|
||||||
// TaggedAnyOpaque which will have the index of the type of that parameter.
|
/// const Cat = struct{
|
||||||
// We'll use the PROTOTYPE_TABLE to see if the TaggedAnyType should be
|
/// pub const prototype = *Animal;
|
||||||
// cast to a prototype.
|
/// };
|
||||||
pub const PROTOTYPE_TABLE = blk: {
|
///
|
||||||
var table: [Types.len]u16 = undefined;
|
/// Would create an array of indexes:
|
||||||
|
/// [Index.Animal, Index.Animal]
|
||||||
|
///
|
||||||
|
/// `Animal`, at index, 0, has no prototype, so we set it to itself.
|
||||||
|
/// `Cat`, at index 1, has an `Animal` prototype, so we set it to `Animal`.
|
||||||
|
///
|
||||||
|
/// When we're trying to pass an argument to a Zig function, we'll know the
|
||||||
|
/// target type (the function parameter type), and we'll have a
|
||||||
|
/// TaggedAnyOpaque which will have the index of the type of that parameter.
|
||||||
|
/// We'll use the `PrototypeTable` to see if the TaggedAnyType should be
|
||||||
|
/// cast to a prototype.
|
||||||
|
pub const PrototypeTable = blk: {
|
||||||
|
var table: [Types.len]BackingInt = undefined;
|
||||||
for (Types, 0..) |s, i| {
|
for (Types, 0..) |s, i| {
|
||||||
var prototype_index = i;
|
|
||||||
const Struct = s.defaultValue().?;
|
const Struct = s.defaultValue().?;
|
||||||
if (@hasDecl(Struct, "prototype")) {
|
table[i] = proto_index: {
|
||||||
const TI = @typeInfo(Struct.prototype);
|
if (@hasDecl(Struct, "prototype")) {
|
||||||
const proto_name = @typeName(Receiver(TI.pointer.child));
|
const prototype_field = @field(Struct, "prototype");
|
||||||
prototype_index = @field(LOOKUP, proto_name);
|
// This prototype type check has nothing to do with building our
|
||||||
}
|
// Lookup. But we put it here, early, so that the rest of the
|
||||||
table[i] = prototype_index;
|
// code doesn't have to worry about checking if Struct.prototype is
|
||||||
|
// a pointer.
|
||||||
|
break :proto_index switch (@typeInfo(prototype_field)) {
|
||||||
|
.pointer => |pointer| getId(Receiver(pointer.child)),
|
||||||
|
inline else => @compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s}' must be a pointer", .{
|
||||||
|
prototype_field,
|
||||||
|
@typeName(Struct),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
break :proto_index i;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
break :blk table;
|
break :blk table;
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is essentially meta data for each type. Each is stored in env.meta_lookup
|
/// This is essentially meta data for each type. Each is stored in `env.meta_lookup`.
|
||||||
// The index for a type can be retrieved via:
|
/// The index for a type can be retrieved via:
|
||||||
// const index = @field(TYPE_LOOKUP, @typeName(Receiver(Struct)));
|
/// const index = types.getIndex(Receiver(Struct));
|
||||||
// const meta = env.meta_lookup[index];
|
/// const meta = env.meta_lookup[@intFromEnum(index)];
|
||||||
|
///
|
||||||
|
/// Or:
|
||||||
|
/// const id = types.getId(Receiver(Struct));
|
||||||
|
/// const meta = env.meta_lookup[id];
|
||||||
pub const Meta = struct {
|
pub const Meta = struct {
|
||||||
// Every type is given a unique index. That index is used to lookup various
|
// Every type is given a unique index. That index is used to lookup various
|
||||||
// things, i.e. the prototype chain.
|
// things, i.e. the prototype chain.
|
||||||
index: u16,
|
index: BackingInt,
|
||||||
|
|
||||||
// We store the type's subtype here, so that when we create an instance of
|
// We store the type's subtype here, so that when we create an instance of
|
||||||
// the type, and bind it to JavaScript, we can store the subtype along with
|
// the type, and bind it to JavaScript, we can store the subtype along with
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub const Mime = struct {
|
|||||||
// IANA defines max. charset value length as 40.
|
// IANA defines max. charset value length as 40.
|
||||||
// We keep 41 for null-termination since HTML parser expects in this format.
|
// We keep 41 for null-termination since HTML parser expects in this format.
|
||||||
charset: [41]u8 = default_charset,
|
charset: [41]u8 = default_charset,
|
||||||
|
charset_len: usize = 5,
|
||||||
|
|
||||||
/// String "UTF-8" continued by null characters.
|
/// String "UTF-8" continued by null characters.
|
||||||
pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
||||||
@@ -53,9 +54,25 @@ pub const Mime = struct {
|
|||||||
other: struct { type: []const u8, sub_type: []const u8 },
|
other: struct { type: []const u8, sub_type: []const u8 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn contentTypeString(mime: *const Mime) []const u8 {
|
||||||
|
return switch (mime.content_type) {
|
||||||
|
.text_xml => "text/xml",
|
||||||
|
.text_html => "text/html",
|
||||||
|
.text_javascript => "application/javascript",
|
||||||
|
.text_plain => "text/plain",
|
||||||
|
.text_css => "text/css",
|
||||||
|
.application_json => "application/json",
|
||||||
|
else => "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the null-terminated charset value.
|
/// Returns the null-terminated charset value.
|
||||||
pub fn charsetString(mime: *const Mime) [:0]const u8 {
|
pub fn charsetStringZ(mime: *const Mime) [:0]const u8 {
|
||||||
return @ptrCast(&mime.charset);
|
return mime.charset[0..mime.charset_len :0];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn charsetString(mime: *const Mime) []const u8 {
|
||||||
|
return mime.charset[0..mime.charset_len];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes quotes of value if quotes are given.
|
/// Removes quotes of value if quotes are given.
|
||||||
@@ -99,6 +116,7 @@ pub const Mime = struct {
|
|||||||
const params = trimLeft(normalized[type_len..]);
|
const params = trimLeft(normalized[type_len..]);
|
||||||
|
|
||||||
var charset: [41]u8 = undefined;
|
var charset: [41]u8 = undefined;
|
||||||
|
var charset_len: usize = undefined;
|
||||||
|
|
||||||
var it = std.mem.splitScalar(u8, params, ';');
|
var it = std.mem.splitScalar(u8, params, ';');
|
||||||
while (it.next()) |attr| {
|
while (it.next()) |attr| {
|
||||||
@@ -124,6 +142,7 @@ pub const Mime = struct {
|
|||||||
@memcpy(charset[0..attribute_value.len], attribute_value);
|
@memcpy(charset[0..attribute_value.len], attribute_value);
|
||||||
// Null-terminate right after attribute value.
|
// Null-terminate right after attribute value.
|
||||||
charset[attribute_value.len] = 0;
|
charset[attribute_value.len] = 0;
|
||||||
|
charset_len = attribute_value.len;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,6 +150,7 @@ pub const Mime = struct {
|
|||||||
return .{
|
return .{
|
||||||
.params = params,
|
.params = params,
|
||||||
.charset = charset,
|
.charset = charset,
|
||||||
|
.charset_len = charset_len,
|
||||||
.content_type = content_type,
|
.content_type = content_type,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -511,9 +531,9 @@ fn expect(expected: Expectation, input: []const u8) !void {
|
|||||||
|
|
||||||
if (expected.charset) |ec| {
|
if (expected.charset) |ec| {
|
||||||
// We remove the null characters for testing purposes here.
|
// We remove the null characters for testing purposes here.
|
||||||
try testing.expectEqual(ec, actual.charsetString()[0..ec.len]);
|
try testing.expectEqual(ec, actual.charsetString());
|
||||||
} else {
|
} else {
|
||||||
const m: Mime = .unknown;
|
const m: Mime = .unknown;
|
||||||
try testing.expectEqual(m.charsetString(), actual.charsetString());
|
try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const Navigation = @This();
|
|||||||
const NavigationKind = @import("root.zig").NavigationKind;
|
const NavigationKind = @import("root.zig").NavigationKind;
|
||||||
const NavigationHistoryEntry = @import("root.zig").NavigationHistoryEntry;
|
const NavigationHistoryEntry = @import("root.zig").NavigationHistoryEntry;
|
||||||
const NavigationTransition = @import("root.zig").NavigationTransition;
|
const NavigationTransition = @import("root.zig").NavigationTransition;
|
||||||
|
const NavigationState = @import("root.zig").NavigationState;
|
||||||
const NavigationCurrentEntryChangeEvent = @import("root.zig").NavigationCurrentEntryChangeEvent;
|
const NavigationCurrentEntryChangeEvent = @import("root.zig").NavigationCurrentEntryChangeEvent;
|
||||||
|
|
||||||
const NavigationEventTarget = @import("NavigationEventTarget.zig");
|
const NavigationEventTarget = @import("NavigationEventTarget.zig");
|
||||||
@@ -110,10 +111,10 @@ pub fn _forward(self: *Navigation, page: *Page) !NavigationReturn {
|
|||||||
pub fn updateEntries(self: *Navigation, url: []const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void {
|
pub fn updateEntries(self: *Navigation, url: []const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
.replace => {
|
.replace => {
|
||||||
_ = try self.replaceEntry(url, null, page, dispatch);
|
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = null }, page, dispatch);
|
||||||
},
|
},
|
||||||
.push => |state| {
|
.push => |state| {
|
||||||
_ = try self.pushEntry(url, state, page, dispatch);
|
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, dispatch);
|
||||||
},
|
},
|
||||||
.traverse => |index| {
|
.traverse => |index| {
|
||||||
self.index = index;
|
self.index = index;
|
||||||
@@ -132,7 +133,13 @@ pub fn processNavigation(self: *Navigation, page: *Page) !void {
|
|||||||
|
|
||||||
/// Pushes an entry into the Navigation stack WITHOUT actually navigating to it.
|
/// Pushes an entry into the Navigation stack WITHOUT actually navigating to it.
|
||||||
/// For that, use `navigate`.
|
/// For that, use `navigate`.
|
||||||
pub fn pushEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry {
|
pub fn pushEntry(
|
||||||
|
self: *Navigation,
|
||||||
|
_url: []const u8,
|
||||||
|
state: NavigationState,
|
||||||
|
page: *Page,
|
||||||
|
dispatch: bool,
|
||||||
|
) !*NavigationHistoryEntry {
|
||||||
const arena = page.session.arena;
|
const arena = page.session.arena;
|
||||||
|
|
||||||
const url = try arena.dupe(u8, _url);
|
const url = try arena.dupe(u8, _url);
|
||||||
@@ -160,18 +167,24 @@ pub fn pushEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page:
|
|||||||
// we don't always have a current entry...
|
// we don't always have a current entry...
|
||||||
const previous = if (self.entries.items.len > 0) self.currentEntry() else null;
|
const previous = if (self.entries.items.len > 0) self.currentEntry() else null;
|
||||||
try self.entries.append(arena, entry);
|
try self.entries.append(arena, entry);
|
||||||
|
self.index = index;
|
||||||
|
|
||||||
if (previous) |prev| {
|
if (previous) |prev| {
|
||||||
if (dispatch) {
|
if (dispatch) {
|
||||||
NavigationCurrentEntryChangeEvent.dispatch(self, prev, .push);
|
NavigationCurrentEntryChangeEvent.dispatch(self, prev, .push);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.index = index;
|
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replaceEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry {
|
pub fn replaceEntry(
|
||||||
|
self: *Navigation,
|
||||||
|
_url: []const u8,
|
||||||
|
state: NavigationState,
|
||||||
|
page: *Page,
|
||||||
|
dispatch: bool,
|
||||||
|
) !*NavigationHistoryEntry {
|
||||||
const arena = page.session.arena;
|
const arena = page.session.arena;
|
||||||
const url = try arena.dupe(u8, _url);
|
const url = try arena.dupe(u8, _url);
|
||||||
|
|
||||||
@@ -184,7 +197,7 @@ pub fn replaceEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, pag
|
|||||||
const entry = try arena.create(NavigationHistoryEntry);
|
const entry = try arena.create(NavigationHistoryEntry);
|
||||||
entry.* = NavigationHistoryEntry{
|
entry.* = NavigationHistoryEntry{
|
||||||
.id = id_str,
|
.id = id_str,
|
||||||
.key = id_str,
|
.key = previous.key,
|
||||||
.url = url,
|
.url = url,
|
||||||
.state = state,
|
.state = state,
|
||||||
};
|
};
|
||||||
@@ -242,7 +255,20 @@ pub fn navigate(
|
|||||||
// todo: Fire navigate event
|
// todo: Fire navigate event
|
||||||
try finished.resolve({});
|
try finished.resolve({});
|
||||||
|
|
||||||
_ = try self.pushEntry(url, state, page, true);
|
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true);
|
||||||
|
} else {
|
||||||
|
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.replace => |state| {
|
||||||
|
if (is_same_document) {
|
||||||
|
page.url = new_url;
|
||||||
|
|
||||||
|
try committed.resolve({});
|
||||||
|
// todo: Fire navigate event
|
||||||
|
try finished.resolve({});
|
||||||
|
|
||||||
|
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true);
|
||||||
} else {
|
} else {
|
||||||
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
|
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
|
||||||
}
|
}
|
||||||
@@ -263,7 +289,6 @@ pub fn navigate(
|
|||||||
.reload => {
|
.reload => {
|
||||||
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
|
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
|
||||||
},
|
},
|
||||||
else => unreachable,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
@@ -275,7 +300,13 @@ pub fn navigate(
|
|||||||
pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn {
|
pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn {
|
||||||
const opts = _opts orelse NavigateOptions{};
|
const opts = _opts orelse NavigateOptions{};
|
||||||
const json = if (opts.state) |state| state.toJson(page.session.arena) catch return error.DataClone else null;
|
const json = if (opts.state) |state| state.toJson(page.session.arena) catch return error.DataClone else null;
|
||||||
return try self.navigate(_url, .{ .push = json }, page);
|
|
||||||
|
const kind: NavigationKind = switch (opts.history) {
|
||||||
|
.replace => .{ .replace = json },
|
||||||
|
.push, .auto => .{ .push = json },
|
||||||
|
};
|
||||||
|
|
||||||
|
return try self.navigate(_url, kind, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ReloadOptions = struct {
|
pub const ReloadOptions = struct {
|
||||||
@@ -290,7 +321,7 @@ pub fn _reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !Navigatio
|
|||||||
const entry = self.currentEntry();
|
const entry = self.currentEntry();
|
||||||
if (opts.state) |state| {
|
if (opts.state) |state| {
|
||||||
const previous = entry;
|
const previous = entry;
|
||||||
entry.state = state.toJson(arena) catch return error.DataClone;
|
entry.state = .{ .source = .navigation, .value = state.toJson(arena) catch return error.DataClone };
|
||||||
NavigationCurrentEntryChangeEvent.dispatch(self, previous, .reload);
|
NavigationCurrentEntryChangeEvent.dispatch(self, previous, .reload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +354,6 @@ pub fn _updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions
|
|||||||
const arena = page.session.arena;
|
const arena = page.session.arena;
|
||||||
|
|
||||||
const previous = self.currentEntry();
|
const previous = self.currentEntry();
|
||||||
self.currentEntry().state = options.state.toJson(arena) catch return error.DataClone;
|
self.currentEntry().state = .{ .source = .navigation, .value = options.state.toJson(arena) catch return error.DataClone };
|
||||||
NavigationCurrentEntryChangeEvent.dispatch(self, previous, null);
|
NavigationCurrentEntryChangeEvent.dispatch(self, previous, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,11 +51,16 @@ pub const NavigationType = enum {
|
|||||||
|
|
||||||
pub const NavigationKind = union(NavigationType) {
|
pub const NavigationKind = union(NavigationType) {
|
||||||
push: ?[]const u8,
|
push: ?[]const u8,
|
||||||
replace,
|
replace: ?[]const u8,
|
||||||
traverse: usize,
|
traverse: usize,
|
||||||
reload,
|
reload,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const NavigationState = struct {
|
||||||
|
source: enum { history, navigation },
|
||||||
|
value: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry
|
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry
|
||||||
pub const NavigationHistoryEntry = struct {
|
pub const NavigationHistoryEntry = struct {
|
||||||
pub const prototype = *EventTarget;
|
pub const prototype = *EventTarget;
|
||||||
@@ -64,7 +69,7 @@ pub const NavigationHistoryEntry = struct {
|
|||||||
id: []const u8,
|
id: []const u8,
|
||||||
key: []const u8,
|
key: []const u8,
|
||||||
url: ?[]const u8,
|
url: ?[]const u8,
|
||||||
state: ?[]const u8,
|
state: NavigationState,
|
||||||
|
|
||||||
pub fn get_id(self: *const NavigationHistoryEntry) []const u8 {
|
pub fn get_id(self: *const NavigationHistoryEntry) []const u8 {
|
||||||
return self.id;
|
return self.id;
|
||||||
@@ -95,12 +100,16 @@ pub const NavigationHistoryEntry = struct {
|
|||||||
return self.url;
|
return self.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !?js.Value {
|
pub const StateReturn = union(enum) { value: ?js.Value, undefined: void };
|
||||||
if (self.state) |state| {
|
|
||||||
return try js.Value.fromJson(page.js, state);
|
pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !StateReturn {
|
||||||
} else {
|
if (self.state.source == .navigation) {
|
||||||
return null;
|
if (self.state.value) |value| {
|
||||||
|
return .{ .value = try js.Value.fromJson(page.js, value) };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return .undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -208,138 +217,6 @@ pub const NavigationCurrentEntryChangeEvent = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const NavigateEvent = struct {
|
|
||||||
pub const prototype = *Event;
|
|
||||||
pub const union_make_copy = true;
|
|
||||||
|
|
||||||
pub const EventInit = struct {
|
|
||||||
canIntercept: ?bool = false,
|
|
||||||
// todo: destination
|
|
||||||
downloadRequest: ?[]const u8 = null,
|
|
||||||
// todo: formData
|
|
||||||
hashChange: ?bool = false,
|
|
||||||
hasUAVisualTransition: ?bool = false,
|
|
||||||
// info: ?js.Value,
|
|
||||||
navigationType: ?NavigationType = .push,
|
|
||||||
// todo: signal
|
|
||||||
// todo: sourceElement
|
|
||||||
userInitiated: ?bool = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
proto: parser.Event,
|
|
||||||
can_intercept: bool,
|
|
||||||
download_request: ?[]const u8,
|
|
||||||
// todo: desintation
|
|
||||||
hash_change: bool,
|
|
||||||
has_ua_visual_transition: bool,
|
|
||||||
info: []const u8,
|
|
||||||
navigation_type: NavigationType,
|
|
||||||
// todo: signal
|
|
||||||
// todo: sourceElement
|
|
||||||
user_initiated: bool,
|
|
||||||
|
|
||||||
pub fn constructor(event_type: []const u8, opts: EventInit) !NavigateEvent {
|
|
||||||
const event = try parser.eventCreate();
|
|
||||||
defer parser.eventDestroy(event);
|
|
||||||
|
|
||||||
try parser.eventInit(event, event_type, .{});
|
|
||||||
parser.eventSetInternalType(event, .navigate_event);
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.proto = event.*,
|
|
||||||
.can_intercept = opts.canIntercept orelse false,
|
|
||||||
.download_request = opts.downloadRequest orelse null,
|
|
||||||
.hash_change = opts.hashChange orelse false,
|
|
||||||
.has_ua_visual_transition = opts.hasUAVisualTransition orelse false,
|
|
||||||
.info = undefined,
|
|
||||||
.navigation_type = opts.navigationType orelse .push,
|
|
||||||
// .info = if (opts.info) |info| try info.toString(page.arena) else null,
|
|
||||||
.user_initiated = opts.userInitiated orelse false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const InterceptOptions = struct {
|
|
||||||
// runs after currentEntry is updated
|
|
||||||
handler: ?js.Function,
|
|
||||||
// runs before currentEntry is updated
|
|
||||||
precommitHandler: ?js.Function,
|
|
||||||
focusReset: ?enum { @"after-transition", manual },
|
|
||||||
scroll: ?enum { @"after-transition", manual },
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/NavigateEvent/intercept
|
|
||||||
pub fn intercept(
|
|
||||||
self: *NavigateEvent,
|
|
||||||
opts: ?InterceptOptions,
|
|
||||||
_: *Page,
|
|
||||||
) !void {
|
|
||||||
if (!self.can_intercept) {
|
|
||||||
return error.Security;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.proto.in_dispatch) {
|
|
||||||
return error.InvalidState;
|
|
||||||
}
|
|
||||||
|
|
||||||
// try parser.eventPreventDefault(&self.proto);
|
|
||||||
|
|
||||||
if (opts) |options| {
|
|
||||||
if (options.precommitHandler) |handler| {
|
|
||||||
_ = try handler.call(void, .{});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update current entry here.
|
|
||||||
|
|
||||||
if (opts) |options| {
|
|
||||||
if (options.handler) |handler| {
|
|
||||||
const result = try handler.call(js.Value, .{});
|
|
||||||
|
|
||||||
if (result.value.isPromise()) {
|
|
||||||
// must be stored and resolved based on nav outcome.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: handle focusReset and scroll
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scroll(self: *NavigateEvent) !void {
|
|
||||||
_ = self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dispatch(navigation: *Navigation, from: *NavigationHistoryEntry, typ: ?NavigationType) void {
|
|
||||||
log.debug(.script_event, "dispatch event", .{
|
|
||||||
.type = "navigate",
|
|
||||||
.source = "navigation",
|
|
||||||
});
|
|
||||||
|
|
||||||
var evt = NavigateEvent.constructor(
|
|
||||||
"navigate",
|
|
||||||
.{ .from = from, .navigationType = typ },
|
|
||||||
) catch |err| {
|
|
||||||
log.err(.app, "event constructor error", .{
|
|
||||||
.err = err,
|
|
||||||
.type = "navigate",
|
|
||||||
.source = "navigation",
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
_ = parser.eventTargetDispatchEvent(
|
|
||||||
@as(*parser.EventTarget, @ptrCast(navigation)),
|
|
||||||
&evt.proto,
|
|
||||||
) catch |err| {
|
|
||||||
log.err(.app, "dispatch event error", .{
|
|
||||||
.err = err,
|
|
||||||
.type = "navigate",
|
|
||||||
.source = "navigation",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser: Navigation" {
|
test "Browser: Navigation" {
|
||||||
try testing.htmlRunner("html/navigation/navigation.html");
|
try testing.htmlRunner("html/navigation/navigation.html");
|
||||||
|
|||||||
@@ -561,7 +561,7 @@ pub const EventType = enum(u8) {
|
|||||||
pop_state = 9,
|
pop_state = 9,
|
||||||
composition_event = 10,
|
composition_event = 10,
|
||||||
navigation_current_entry_change_event = 11,
|
navigation_current_entry_change_event = 11,
|
||||||
navigate_event = 12,
|
page_transition_event = 12,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const MutationEvent = c.dom_mutation_event;
|
pub const MutationEvent = c.dom_mutation_event;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
|||||||
|
|
||||||
const NavigationKind = @import("navigation/root.zig").NavigationKind;
|
const NavigationKind = @import("navigation/root.zig").NavigationKind;
|
||||||
const NavigationCurrentEntryChangeEvent = @import("navigation/root.zig").NavigationCurrentEntryChangeEvent;
|
const NavigationCurrentEntryChangeEvent = @import("navigation/root.zig").NavigationCurrentEntryChangeEvent;
|
||||||
|
const PageTransitionEvent = @import("events/PageTransitionEvent.zig");
|
||||||
|
|
||||||
const js = @import("js/js.zig");
|
const js = @import("js/js.zig");
|
||||||
const URL = @import("../url.zig").URL;
|
const URL = @import("../url.zig").URL;
|
||||||
@@ -82,6 +83,8 @@ pub const Page = struct {
|
|||||||
|
|
||||||
// indicates intention to navigate to another page on the next loop execution.
|
// indicates intention to navigate to another page on the next loop execution.
|
||||||
delayed_navigation: bool = false,
|
delayed_navigation: bool = false,
|
||||||
|
req_id: ?usize = null,
|
||||||
|
navigated_options: ?NavigatedOpts = null,
|
||||||
|
|
||||||
state_pool: *std.heap.MemoryPool(State),
|
state_pool: *std.heap.MemoryPool(State),
|
||||||
|
|
||||||
@@ -102,6 +105,10 @@ pub const Page = struct {
|
|||||||
notified_network_idle: IdleNotification = .init,
|
notified_network_idle: IdleNotification = .init,
|
||||||
notified_network_almost_idle: IdleNotification = .init,
|
notified_network_almost_idle: IdleNotification = .init,
|
||||||
|
|
||||||
|
// Indicates if the page's document is open or close.
|
||||||
|
// Relates with https://developer.mozilla.org/en-US/docs/Web/API/Document/open
|
||||||
|
open: bool = false,
|
||||||
|
|
||||||
const Mode = union(enum) {
|
const Mode = union(enum) {
|
||||||
pre: void,
|
pre: void,
|
||||||
err: anyerror,
|
err: anyerror,
|
||||||
@@ -168,6 +175,9 @@ pub const Page = struct {
|
|||||||
self.http_client.abort();
|
self.http_client.abort();
|
||||||
self.script_manager.reset();
|
self.script_manager.reset();
|
||||||
|
|
||||||
|
parser.deinit();
|
||||||
|
parser.init();
|
||||||
|
|
||||||
self.load_state = .parsing;
|
self.load_state = .parsing;
|
||||||
self.mode = .{ .pre = {} };
|
self.mode = .{ .pre = {} };
|
||||||
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||||
@@ -545,11 +555,14 @@ pub const Page = struct {
|
|||||||
try self.reset();
|
try self.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const req_id = self.http_client.nextReqId();
|
||||||
|
|
||||||
log.info(.http, "navigate", .{
|
log.info(.http, "navigate", .{
|
||||||
.url = request_url,
|
.url = request_url,
|
||||||
.method = opts.method,
|
.method = opts.method,
|
||||||
.reason = opts.reason,
|
.reason = opts.reason,
|
||||||
.body = opts.body != null,
|
.body = opts.body != null,
|
||||||
|
.req_id = req_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// if the url is about:blank, we load an empty HTML document in the
|
// if the url is about:blank, we load an empty HTML document in the
|
||||||
@@ -567,22 +580,39 @@ pub const Page = struct {
|
|||||||
self.documentIsComplete();
|
self.documentIsComplete();
|
||||||
|
|
||||||
self.session.browser.notification.dispatch(.page_navigate, &.{
|
self.session.browser.notification.dispatch(.page_navigate, &.{
|
||||||
|
.req_id = req_id,
|
||||||
.opts = opts,
|
.opts = opts,
|
||||||
.url = request_url,
|
.url = request_url,
|
||||||
.timestamp = timestamp(),
|
.timestamp = timestamp(),
|
||||||
});
|
});
|
||||||
|
|
||||||
self.session.browser.notification.dispatch(.page_navigated, &.{
|
self.session.browser.notification.dispatch(.page_navigated, &.{
|
||||||
|
.req_id = req_id,
|
||||||
|
.opts = .{
|
||||||
|
.cdp_id = opts.cdp_id,
|
||||||
|
.reason = opts.reason,
|
||||||
|
.method = opts.method,
|
||||||
|
},
|
||||||
.url = request_url,
|
.url = request_url,
|
||||||
.timestamp = timestamp(),
|
.timestamp = timestamp(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// force next request id manually b/c we won't create a real req.
|
||||||
|
_ = self.http_client.incrReqId();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const owned_url = try self.arena.dupeZ(u8, request_url);
|
const owned_url = try self.arena.dupeZ(u8, request_url);
|
||||||
self.url = try URL.parse(owned_url, null);
|
self.url = try URL.parse(owned_url, null);
|
||||||
|
|
||||||
|
self.req_id = req_id;
|
||||||
|
self.navigated_options = .{
|
||||||
|
.cdp_id = opts.cdp_id,
|
||||||
|
.reason = opts.reason,
|
||||||
|
.method = opts.method,
|
||||||
|
};
|
||||||
|
|
||||||
var headers = try self.http_client.newHeaders();
|
var headers = try self.http_client.newHeaders();
|
||||||
if (opts.header) |hdr| try headers.add(hdr);
|
if (opts.header) |hdr| try headers.add(hdr);
|
||||||
try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, owned_url, &headers);
|
try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, owned_url, &headers);
|
||||||
@@ -590,6 +620,7 @@ pub const Page = struct {
|
|||||||
// We dispatch page_navigate event before sending the request.
|
// We dispatch page_navigate event before sending the request.
|
||||||
// It ensures the event page_navigated is not dispatched before this one.
|
// It ensures the event page_navigated is not dispatched before this one.
|
||||||
self.session.browser.notification.dispatch(.page_navigate, &.{
|
self.session.browser.notification.dispatch(.page_navigate, &.{
|
||||||
|
.req_id = req_id,
|
||||||
.opts = opts,
|
.opts = opts,
|
||||||
.url = owned_url,
|
.url = owned_url,
|
||||||
.timestamp = timestamp(),
|
.timestamp = timestamp(),
|
||||||
@@ -655,13 +686,20 @@ pub const Page = struct {
|
|||||||
log.err(.browser, "document is complete", .{ .err = err });
|
log.err(.browser, "document is complete", .{ .err = err });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
std.debug.assert(self.req_id != null);
|
||||||
|
std.debug.assert(self.navigated_options != null);
|
||||||
self.session.browser.notification.dispatch(.page_navigated, &.{
|
self.session.browser.notification.dispatch(.page_navigated, &.{
|
||||||
|
.req_id = self.req_id.?,
|
||||||
|
.opts = self.navigated_options.?,
|
||||||
.url = self.url.raw,
|
.url = self.url.raw,
|
||||||
.timestamp = timestamp(),
|
.timestamp = timestamp(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _documentIsComplete(self: *Page) !void {
|
fn _documentIsComplete(self: *Page) !void {
|
||||||
|
self.session.browser.runMicrotasks();
|
||||||
|
self.session.browser.runMessageLoop();
|
||||||
|
|
||||||
try HTMLDocument.documentIsComplete(self.window.document, self);
|
try HTMLDocument.documentIsComplete(self.window.document, self);
|
||||||
|
|
||||||
// dispatch window.load event
|
// dispatch window.load event
|
||||||
@@ -674,6 +712,8 @@ pub const Page = struct {
|
|||||||
parser.toEventTarget(Window, &self.window),
|
parser.toEventTarget(Window, &self.window),
|
||||||
loadevt,
|
loadevt,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
PageTransitionEvent.dispatch(&self.window, .show, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void {
|
fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void {
|
||||||
@@ -707,14 +747,14 @@ pub const Page = struct {
|
|||||||
log.debug(.http, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len });
|
log.debug(.http, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len });
|
||||||
|
|
||||||
self.mode = switch (mime.content_type) {
|
self.mode = switch (mime.content_type) {
|
||||||
.text_html => .{ .html = try parser.Parser.init(mime.charsetString()) },
|
.text_html => .{ .html = try parser.Parser.init(mime.charsetStringZ()) },
|
||||||
|
|
||||||
.application_json,
|
.application_json,
|
||||||
.text_javascript,
|
.text_javascript,
|
||||||
.text_css,
|
.text_css,
|
||||||
.text_plain,
|
.text_plain,
|
||||||
=> blk: {
|
=> blk: {
|
||||||
var p = try parser.Parser.init(mime.charsetString());
|
var p = try parser.Parser.init(mime.charsetStringZ());
|
||||||
try p.process("<html><head><meta charset=\"utf-8\"></head><body><pre>");
|
try p.process("<html><head><meta charset=\"utf-8\"></head><body><pre>");
|
||||||
break :blk .{ .text = p };
|
break :blk .{ .text = p };
|
||||||
},
|
},
|
||||||
@@ -756,6 +796,9 @@ pub const Page = struct {
|
|||||||
var self: *Page = @ptrCast(@alignCast(ctx));
|
var self: *Page = @ptrCast(@alignCast(ctx));
|
||||||
self.clearTransferArena();
|
self.clearTransferArena();
|
||||||
|
|
||||||
|
// We need to handle different navigation types differently.
|
||||||
|
try self.session.navigation.processNavigation(self);
|
||||||
|
|
||||||
switch (self.mode) {
|
switch (self.mode) {
|
||||||
.pre => {
|
.pre => {
|
||||||
// Received a response without a body like: https://httpbin.io/status/200
|
// Received a response without a body like: https://httpbin.io/status/200
|
||||||
@@ -834,9 +877,6 @@ pub const Page = struct {
|
|||||||
unreachable;
|
unreachable;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to handle different navigation types differently.
|
|
||||||
try self.session.navigation.processNavigation(self);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||||
@@ -914,7 +954,7 @@ pub const Page = struct {
|
|||||||
fn windowClicked(node: *parser.EventNode, event: *parser.Event) void {
|
fn windowClicked(node: *parser.EventNode, event: *parser.Event) void {
|
||||||
const self: *Page = @fieldParentPtr("window_clicked_event_node", node);
|
const self: *Page = @fieldParentPtr("window_clicked_event_node", node);
|
||||||
self._windowClicked(event) catch |err| {
|
self._windowClicked(event) catch |err| {
|
||||||
log.err(.browser, "click handler error", .{ .err = err });
|
log.err(.input, "click handler error", .{ .err = err });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,18 +966,22 @@ pub const Page = struct {
|
|||||||
.a => {
|
.a => {
|
||||||
const element: *parser.Element = @ptrCast(node);
|
const element: *parser.Element = @ptrCast(node);
|
||||||
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
|
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
|
||||||
|
log.debug(.input, "window click on link", .{ .tag = tag, .href = href });
|
||||||
try self.navigateFromWebAPI(href, .{}, .{ .push = null });
|
try self.navigateFromWebAPI(href, .{}, .{ .push = null });
|
||||||
|
return;
|
||||||
},
|
},
|
||||||
.input => {
|
.input => {
|
||||||
const element: *parser.Element = @ptrCast(node);
|
const element: *parser.Element = @ptrCast(node);
|
||||||
const input_type = try parser.inputGetType(@ptrCast(element));
|
const input_type = try parser.inputGetType(@ptrCast(element));
|
||||||
if (std.ascii.eqlIgnoreCase(input_type, "submit")) {
|
if (std.ascii.eqlIgnoreCase(input_type, "submit")) {
|
||||||
|
log.debug(.input, "window click on submit input", .{ .tag = tag });
|
||||||
return self.elementSubmitForm(element);
|
return self.elementSubmitForm(element);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.button => {
|
.button => {
|
||||||
const element: *parser.Element = @ptrCast(node);
|
const element: *parser.Element = @ptrCast(node);
|
||||||
const button_type = try parser.buttonGetType(@ptrCast(element));
|
const button_type = try parser.buttonGetType(@ptrCast(element));
|
||||||
|
log.debug(.input, "window click on button", .{ .tag = tag, .button_type = button_type });
|
||||||
if (std.ascii.eqlIgnoreCase(button_type, "submit")) {
|
if (std.ascii.eqlIgnoreCase(button_type, "submit")) {
|
||||||
return self.elementSubmitForm(element);
|
return self.elementSubmitForm(element);
|
||||||
}
|
}
|
||||||
@@ -949,6 +993,12 @@ pub const Page = struct {
|
|||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
log.debug(.input, "window click on element", .{ .tag = tag });
|
||||||
|
// Set the focus on the clicked element.
|
||||||
|
// Thanks to parser.nodeHTMLGetTagType, we know nod is an element.
|
||||||
|
// We assume we have a ElementHTML.
|
||||||
|
const Document = @import("dom/document.zig").Document;
|
||||||
|
try Document.setFocus(@ptrCast(self.window.document), @as(*parser.ElementHTML, @ptrCast(node)), self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const KeyboardEvent = struct {
|
pub const KeyboardEvent = struct {
|
||||||
@@ -991,7 +1041,7 @@ pub const Page = struct {
|
|||||||
fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void {
|
fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void {
|
||||||
const self: *Page = @fieldParentPtr("keydown_event_node", node);
|
const self: *Page = @fieldParentPtr("keydown_event_node", node);
|
||||||
self._keydownCallback(event) catch |err| {
|
self._keydownCallback(event) catch |err| {
|
||||||
log.err(.browser, "keydown handler error", .{ .err = err });
|
log.err(.input, "keydown handler error", .{ .err = err });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1005,23 +1055,29 @@ pub const Page = struct {
|
|||||||
if (std.mem.eql(u8, new_key, "Dead")) {
|
if (std.mem.eql(u8, new_key, "Dead")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
.input => {
|
.input => {
|
||||||
const element: *parser.Element = @ptrCast(node);
|
const element: *parser.Element = @ptrCast(node);
|
||||||
const input_type = try parser.inputGetType(@ptrCast(element));
|
const input_type = try parser.inputGetType(@ptrCast(element));
|
||||||
if (std.mem.eql(u8, input_type, "text")) {
|
log.debug(.input, "key down on input", .{ .tag = tag, .key = new_key, .input_type = input_type });
|
||||||
if (std.mem.eql(u8, new_key, "Enter")) {
|
if (std.mem.eql(u8, new_key, "Enter")) {
|
||||||
const form = (try self.formForElement(element)) orelse return;
|
const form = (try self.formForElement(element)) orelse return;
|
||||||
return self.submitForm(@ptrCast(form), null);
|
return self.submitForm(@ptrCast(form), null);
|
||||||
}
|
|
||||||
|
|
||||||
const value = try parser.inputGetValue(@ptrCast(element));
|
|
||||||
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
|
|
||||||
try parser.inputSetValue(@ptrCast(element), new_value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, input_type, "radio")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (std.mem.eql(u8, input_type, "checkbox")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = try parser.inputGetValue(@ptrCast(element));
|
||||||
|
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
|
||||||
|
try parser.inputSetValue(@ptrCast(element), new_value);
|
||||||
},
|
},
|
||||||
.textarea => {
|
.textarea => {
|
||||||
|
log.debug(.input, "key down on textarea", .{ .tag = tag, .key = new_key });
|
||||||
const value = try parser.textareaGetValue(@ptrCast(node));
|
const value = try parser.textareaGetValue(@ptrCast(node));
|
||||||
if (std.mem.eql(u8, new_key, "Enter")) {
|
if (std.mem.eql(u8, new_key, "Enter")) {
|
||||||
new_key = "\n";
|
new_key = "\n";
|
||||||
@@ -1029,7 +1085,9 @@ pub const Page = struct {
|
|||||||
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
|
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
|
||||||
try parser.textareaSetValue(@ptrCast(node), new_value);
|
try parser.textareaSetValue(@ptrCast(node), new_value);
|
||||||
},
|
},
|
||||||
else => {},
|
else => {
|
||||||
|
log.debug(.input, "key down event", .{ .tag = tag, .key = new_key });
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1216,6 +1274,10 @@ pub const Page = struct {
|
|||||||
const current_origin = try self.origin(self.call_arena);
|
const current_origin = try self.origin(self.call_arena);
|
||||||
return std.mem.startsWith(u8, url, current_origin);
|
return std.mem.startsWith(u8, url, current_origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getTitle(self: *const Page) ![]const u8 {
|
||||||
|
return try parser.documentHTMLGetTitle(self.window.document);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const NavigateReason = enum {
|
pub const NavigateReason = enum {
|
||||||
@@ -1236,6 +1298,12 @@ pub const NavigateOpts = struct {
|
|||||||
force: bool = false,
|
force: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const NavigatedOpts = struct {
|
||||||
|
cdp_id: ?i64 = null,
|
||||||
|
reason: NavigateReason = .address_bar,
|
||||||
|
method: Http.Method = .GET,
|
||||||
|
};
|
||||||
|
|
||||||
const IdleNotification = union(enum) {
|
const IdleNotification = union(enum) {
|
||||||
// hasn't started yet.
|
// hasn't started yet.
|
||||||
init,
|
init,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const Mime = @import("../mime.zig").Mime;
|
|||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const Http = @import("../../http/Http.zig");
|
const Http = @import("../../http/Http.zig");
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
|
||||||
// XHR interfaces
|
// XHR interfaces
|
||||||
// https://xhr.spec.whatwg.org/#interface-xmlhttprequest
|
// https://xhr.spec.whatwg.org/#interface-xmlhttprequest
|
||||||
@@ -128,21 +129,19 @@ pub const XMLHttpRequest = struct {
|
|||||||
JSON,
|
JSON,
|
||||||
};
|
};
|
||||||
|
|
||||||
const JSONValue = std.json.Value;
|
|
||||||
|
|
||||||
const Response = union(ResponseType) {
|
const Response = union(ResponseType) {
|
||||||
Empty: void,
|
Empty: void,
|
||||||
Text: []const u8,
|
Text: []const u8,
|
||||||
ArrayBuffer: void,
|
ArrayBuffer: void,
|
||||||
Blob: void,
|
Blob: void,
|
||||||
Document: *parser.Document,
|
Document: *parser.Document,
|
||||||
JSON: JSONValue,
|
JSON: js.Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ResponseObj = union(enum) {
|
const ResponseObj = union(enum) {
|
||||||
Document: *parser.Document,
|
Document: *parser.Document,
|
||||||
Failure: void,
|
Failure: void,
|
||||||
JSON: JSONValue,
|
JSON: js.Value,
|
||||||
|
|
||||||
fn deinit(self: ResponseObj) void {
|
fn deinit(self: ResponseObj) void {
|
||||||
switch (self) {
|
switch (self) {
|
||||||
@@ -605,7 +604,7 @@ pub const XMLHttpRequest = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://xhr.spec.whatwg.org/#the-response-attribute
|
// https://xhr.spec.whatwg.org/#the-response-attribute
|
||||||
pub fn get_response(self: *XMLHttpRequest) !?Response {
|
pub fn get_response(self: *XMLHttpRequest, page: *Page) !?Response {
|
||||||
if (self.response_type == .Empty or self.response_type == .Text) {
|
if (self.response_type == .Empty or self.response_type == .Text) {
|
||||||
if (self.state == .loading or self.state == .done) {
|
if (self.state == .loading or self.state == .done) {
|
||||||
return .{ .Text = try self.get_responseText() };
|
return .{ .Text = try self.get_responseText() };
|
||||||
@@ -652,7 +651,7 @@ pub const XMLHttpRequest = struct {
|
|||||||
// TODO Let jsonObject be the result of running parse JSON from bytes
|
// TODO Let jsonObject be the result of running parse JSON from bytes
|
||||||
// on this’s received bytes. If that threw an exception, then return
|
// on this’s received bytes. If that threw an exception, then return
|
||||||
// null.
|
// null.
|
||||||
self.setResponseObjJSON();
|
self.setResponseObjJSON(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.response_obj) |obj| {
|
if (self.response_obj) |obj| {
|
||||||
@@ -678,7 +677,7 @@ pub const XMLHttpRequest = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fbs = std.io.fixedBufferStream(self.response_bytes.items);
|
var fbs = std.io.fixedBufferStream(self.response_bytes.items);
|
||||||
const doc = parser.documentHTMLParse(fbs.reader(), mime.charsetString()) catch {
|
const doc = parser.documentHTMLParse(fbs.reader(), mime.charsetStringZ()) catch {
|
||||||
self.response_obj = .{ .Failure = {} };
|
self.response_obj = .{ .Failure = {} };
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -691,22 +690,24 @@ pub const XMLHttpRequest = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// setResponseObjJSON parses the received bytes as a std.json.Value.
|
// setResponseObjJSON parses the received bytes as a js.Value.
|
||||||
fn setResponseObjJSON(self: *XMLHttpRequest) void {
|
fn setResponseObjJSON(self: *XMLHttpRequest, page: *Page) void {
|
||||||
// TODO should we use parseFromSliceLeaky if we expect the allocator is
|
const value = js.Value.fromJson(
|
||||||
// already an arena?
|
page.js,
|
||||||
const p = std.json.parseFromSliceLeaky(
|
|
||||||
JSONValue,
|
|
||||||
self.arena,
|
|
||||||
self.response_bytes.items,
|
self.response_bytes.items,
|
||||||
.{},
|
|
||||||
) catch |e| {
|
) catch |e| {
|
||||||
log.warn(.http, "invalid json", .{ .err = e, .url = self.url, .source = "xhr" });
|
log.warn(.http, "invalid json", .{ .err = e, .url = self.url, .source = "xhr" });
|
||||||
self.response_obj = .{ .Failure = {} };
|
self.response_obj = .{ .Failure = {} };
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.response_obj = .{ .JSON = p };
|
const pvalue = value.persist(page.js) catch |e| {
|
||||||
|
log.warn(.http, "persist v8 json value", .{ .err = e, .url = self.url, .source = "xhr" });
|
||||||
|
self.response_obj = .{ .Failure = {} };
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.response_obj = .{ .JSON = pvalue };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 {
|
pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 {
|
||||||
|
|||||||
@@ -230,6 +230,11 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
|||||||
asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command),
|
asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command),
|
||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
|
13 => switch (@as(u104, @bitCast(domain[0..13].*))) {
|
||||||
|
asUint(u104, "Accessibility") => return @import("domains/accessibility.zig").processMessage(command),
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,6 +473,14 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
return if (raw_url.len == 0) null else raw_url;
|
return if (raw_url.len == 0) null else raw_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getTitle(self: *const Self) ?[]const u8 {
|
||||||
|
const page = self.session.currentPage() orelse return null;
|
||||||
|
return page.getTitle() catch |err| {
|
||||||
|
log.err(.cdp, "page title", .{ .err = err });
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn networkEnable(self: *Self) !void {
|
pub fn networkEnable(self: *Self) !void {
|
||||||
try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail);
|
try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail);
|
||||||
try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart);
|
try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart);
|
||||||
@@ -538,7 +551,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
|
|
||||||
pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void {
|
pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void {
|
||||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||||
return @import("domains/page.zig").pageNavigated(self, msg);
|
defer self.resetNotificationArena();
|
||||||
|
return @import("domains/page.zig").pageNavigated(self.notification_arena, self, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void {
|
pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void {
|
||||||
|
|||||||
38
src/cdp/domains/accessibility.zig
Normal file
38
src/cdp/domains/accessibility.zig
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
|
const action = std.meta.stringToEnum(enum {
|
||||||
|
enable,
|
||||||
|
disable,
|
||||||
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
.enable => return enable(cmd),
|
||||||
|
.disable => return disable(cmd),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn enable(cmd: anytype) !void {
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disable(cmd: anytype) !void {
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
grantPermissions,
|
grantPermissions,
|
||||||
getWindowForTarget,
|
getWindowForTarget,
|
||||||
setDownloadBehavior,
|
setDownloadBehavior,
|
||||||
|
close,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -54,6 +55,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
.grantPermissions => return grantPermissions(cmd),
|
.grantPermissions => return grantPermissions(cmd),
|
||||||
.getWindowForTarget => return getWindowForTarget(cmd),
|
.getWindowForTarget => return getWindowForTarget(cmd),
|
||||||
.setDownloadBehavior => return setDownloadBehavior(cmd),
|
.setDownloadBehavior => return setDownloadBehavior(cmd),
|
||||||
|
.close => return cmd.sendResult(null, .{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const css = @import("../../browser/dom/css.zig");
|
|||||||
const parser = @import("../../browser/netsurf.zig");
|
const parser = @import("../../browser/netsurf.zig");
|
||||||
const dom_node = @import("../../browser/dom/node.zig");
|
const dom_node = @import("../../browser/dom/node.zig");
|
||||||
const Element = @import("../../browser/dom/element.zig").Element;
|
const Element = @import("../../browser/dom/element.zig").Element;
|
||||||
|
const dump = @import("../../browser/dump.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 {
|
||||||
@@ -41,6 +42,8 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
getBoxModel,
|
getBoxModel,
|
||||||
requestChildNodes,
|
requestChildNodes,
|
||||||
getFrameOwner,
|
getFrameOwner,
|
||||||
|
getOuterHTML,
|
||||||
|
requestNode,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -58,6 +61,8 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
.getBoxModel => return getBoxModel(cmd),
|
.getBoxModel => return getBoxModel(cmd),
|
||||||
.requestChildNodes => return requestChildNodes(cmd),
|
.requestChildNodes => return requestChildNodes(cmd),
|
||||||
.getFrameOwner => return getFrameOwner(cmd),
|
.getFrameOwner => return getFrameOwner(cmd),
|
||||||
|
.getOuterHTML => return getOuterHTML(cmd),
|
||||||
|
.requestNode => return requestNode(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,6 +499,38 @@ fn getFrameOwner(cmd: anytype) !void {
|
|||||||
return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{});
|
return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn getOuterHTML(cmd: anytype) !void {
|
||||||
|
const params = (try cmd.params(struct {
|
||||||
|
nodeId: ?Node.Id = null,
|
||||||
|
backendNodeId: ?Node.Id = null,
|
||||||
|
objectId: ?[]const u8 = null,
|
||||||
|
includeShadowDOM: bool = false,
|
||||||
|
})) orelse return error.InvalidParams;
|
||||||
|
|
||||||
|
if (params.includeShadowDOM) {
|
||||||
|
log.warn(.cdp, "not implemented", .{ .feature = "DOM.getOuterHTML: Not implemented includeShadowDOM parameter" });
|
||||||
|
}
|
||||||
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
|
||||||
|
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
|
||||||
|
|
||||||
|
var aw = std.Io.Writer.Allocating.init(cmd.arena);
|
||||||
|
try dump.writeNode(node._node, .{}, &aw.writer);
|
||||||
|
|
||||||
|
return cmd.sendResult(.{ .outerHTML = aw.written() }, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requestNode(cmd: anytype) !void {
|
||||||
|
const params = (try cmd.params(struct {
|
||||||
|
objectId: []const u8,
|
||||||
|
})) orelse return error.InvalidParams;
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
const node = try getNode(cmd.arena, bc, null, null, params.objectId);
|
||||||
|
|
||||||
|
return cmd.sendResult(.{ .nodeId = node.id }, .{});
|
||||||
|
}
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
|
|
||||||
test "cdp.dom: getSearchResults unknown search id" {
|
test "cdp.dom: getSearchResults unknown search id" {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const CdpStorage = @import("storage.zig");
|
const CdpStorage = @import("storage.zig");
|
||||||
const Transfer = @import("../../http/Client.zig").Transfer;
|
const Transfer = @import("../../http/Client.zig").Transfer;
|
||||||
const Notification = @import("../../notification.zig").Notification;
|
const Notification = @import("../../notification.zig").Notification;
|
||||||
|
const Mime = @import("../../browser/mime.zig").Mime;
|
||||||
|
|
||||||
pub fn processMessage(cmd: anytype) !void {
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
@@ -242,14 +243,18 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification.
|
|||||||
}
|
}
|
||||||
|
|
||||||
const transfer = msg.transfer;
|
const transfer = msg.transfer;
|
||||||
|
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id});
|
||||||
// We're missing a bunch of fields, but, for now, this seems like enough
|
// We're missing a bunch of fields, but, for now, this seems like enough
|
||||||
try bc.cdp.sendEvent("Network.requestWillBeSent", .{
|
try bc.cdp.sendEvent("Network.requestWillBeSent", .{
|
||||||
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
|
.requestId = loader_id,
|
||||||
.frameId = target_id,
|
.frameId = target_id,
|
||||||
.loaderId = bc.loader_id,
|
.loaderId = loader_id,
|
||||||
.documentUrl = DocumentUrlWriter.init(&page.url.uri),
|
.type = msg.transfer.req.resource_type.string(),
|
||||||
|
.documentURL = DocumentUrlWriter.init(&page.url.uri),
|
||||||
.request = TransferAsRequestWriter.init(transfer),
|
.request = TransferAsRequestWriter.init(transfer),
|
||||||
.initiator = .{ .type = "other" },
|
.initiator = .{ .type = "other" },
|
||||||
|
.redirectHasExtraInfo = false, // TODO change after adding Network.requestWillBeSentExtraInfo
|
||||||
|
.hasUserGesture = false,
|
||||||
}, .{ .session_id = session_id });
|
}, .{ .session_id = session_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,12 +264,16 @@ pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notific
|
|||||||
const session_id = bc.session_id orelse return;
|
const session_id = bc.session_id orelse return;
|
||||||
const target_id = bc.target_id orelse unreachable;
|
const target_id = bc.target_id orelse unreachable;
|
||||||
|
|
||||||
|
const transfer = msg.transfer;
|
||||||
|
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id});
|
||||||
|
|
||||||
// We're missing a bunch of fields, but, for now, this seems like enough
|
// We're missing a bunch of fields, but, for now, this seems like enough
|
||||||
try bc.cdp.sendEvent("Network.responseReceived", .{
|
try bc.cdp.sendEvent("Network.responseReceived", .{
|
||||||
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}),
|
.requestId = loader_id,
|
||||||
.loaderId = bc.loader_id,
|
|
||||||
.frameId = target_id,
|
.frameId = target_id,
|
||||||
|
.loaderId = loader_id,
|
||||||
.response = TransferAsResponseWriter.init(arena, msg.transfer),
|
.response = TransferAsResponseWriter.init(arena, msg.transfer),
|
||||||
|
.hasExtraInfo = false, // TODO change after adding Network.responseReceivedExtraInfo
|
||||||
}, .{ .session_id = session_id });
|
}, .{ .session_id = session_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,6 +401,20 @@ const TransferAsResponseWriter = struct {
|
|||||||
try jws.write(@as(std.http.Status, @enumFromInt(status)).phrase() orelse "Unknown");
|
try jws.write(@as(std.http.Status, @enumFromInt(status)).phrase() orelse "Unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const mime: Mime = blk: {
|
||||||
|
if (transfer.response_header.?.contentType()) |ct| {
|
||||||
|
break :blk try Mime.parse(ct);
|
||||||
|
}
|
||||||
|
break :blk .unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
try jws.objectField("mimeType");
|
||||||
|
try jws.write(mime.contentTypeString());
|
||||||
|
try jws.objectField("charset");
|
||||||
|
try jws.write(mime.charsetString());
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// chromedp doesn't like having duplicate header names. It's pretty
|
// chromedp doesn't like having duplicate header names. It's pretty
|
||||||
// common to get these from a server (e.g. for Cache-Control), but
|
// common to get these from a server (e.g. for Cache-Control), but
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const std = @import("std");
|
|||||||
const Page = @import("../../browser/page.zig").Page;
|
const Page = @import("../../browser/page.zig").Page;
|
||||||
const timestampF = @import("../../datetime.zig").timestamp;
|
const timestampF = @import("../../datetime.zig").timestamp;
|
||||||
const Notification = @import("../../notification.zig").Notification;
|
const Notification = @import("../../notification.zig").Notification;
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
createIsolatedWorld,
|
createIsolatedWorld,
|
||||||
navigate,
|
navigate,
|
||||||
stopLoading,
|
stopLoading,
|
||||||
|
close,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -42,6 +44,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
.createIsolatedWorld => return createIsolatedWorld(cmd),
|
.createIsolatedWorld => return createIsolatedWorld(cmd),
|
||||||
.navigate => return navigate(cmd),
|
.navigate => return navigate(cmd),
|
||||||
.stopLoading => return cmd.sendResult(null, .{}),
|
.stopLoading => return cmd.sendResult(null, .{}),
|
||||||
|
.close => return close(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,14 +132,51 @@ fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
|
|||||||
}, .{});
|
}, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn close(cmd: anytype) !void {
|
||||||
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
|
||||||
|
const target_id = bc.target_id orelse return error.TargetNotLoaded;
|
||||||
|
|
||||||
|
// can't be null if we have a target_id
|
||||||
|
std.debug.assert(bc.session.page != null);
|
||||||
|
|
||||||
|
try cmd.sendResult(.{}, .{});
|
||||||
|
|
||||||
|
// Following code is similar to target.closeTarget
|
||||||
|
//
|
||||||
|
// could be null, created but never attached
|
||||||
|
if (bc.session_id) |session_id| {
|
||||||
|
// Inspector.detached event
|
||||||
|
try cmd.sendEvent("Inspector.detached", .{
|
||||||
|
.reason = "Render process gone.",
|
||||||
|
}, .{ .session_id = session_id });
|
||||||
|
|
||||||
|
// detachedFromTarget event
|
||||||
|
try cmd.sendEvent("Target.detachedFromTarget", .{
|
||||||
|
.targetId = target_id,
|
||||||
|
.sessionId = session_id,
|
||||||
|
.reason = "Render process gone.",
|
||||||
|
}, .{});
|
||||||
|
|
||||||
|
bc.session_id = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bc.session.removePage();
|
||||||
|
for (bc.isolated_worlds.items) |*world| {
|
||||||
|
world.deinit();
|
||||||
|
}
|
||||||
|
bc.isolated_worlds.clearRetainingCapacity();
|
||||||
|
bc.target_id = null;
|
||||||
|
}
|
||||||
|
|
||||||
fn createIsolatedWorld(cmd: anytype) !void {
|
fn createIsolatedWorld(cmd: anytype) !void {
|
||||||
const params = (try cmd.params(struct {
|
const params = (try cmd.params(struct {
|
||||||
frameId: []const u8,
|
frameId: []const u8,
|
||||||
worldName: []const u8,
|
worldName: []const u8,
|
||||||
grantUniveralAccess: bool,
|
grantUniveralAccess: bool = false,
|
||||||
})) orelse return error.InvalidParams;
|
})) orelse return error.InvalidParams;
|
||||||
if (!params.grantUniveralAccess) {
|
if (!params.grantUniveralAccess) {
|
||||||
std.debug.print("grantUniveralAccess == false is not yet implemented", .{});
|
log.warn(.cdp, "not implemented", .{ .feature = "grantUniveralAccess == false is not yet implemented" });
|
||||||
// When grantUniveralAccess == false and the client attempts to resolve
|
// When grantUniveralAccess == false and the client attempts to resolve
|
||||||
// or otherwise access a DOM or other JS Object from another context that should fail.
|
// or otherwise access a DOM or other JS Object from another context that should fail.
|
||||||
}
|
}
|
||||||
@@ -175,7 +215,6 @@ fn navigate(cmd: anytype) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
var page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
bc.loader_id = bc.cdp.loader_id_gen.next();
|
|
||||||
|
|
||||||
try page.navigate(params.url, .{
|
try page.navigate(params.url, .{
|
||||||
.reason = .address_bar,
|
.reason = .address_bar,
|
||||||
@@ -188,8 +227,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
|||||||
// things, but no session.
|
// things, but no session.
|
||||||
const session_id = bc.session_id orelse return;
|
const session_id = bc.session_id orelse return;
|
||||||
|
|
||||||
bc.loader_id = bc.cdp.loader_id_gen.next();
|
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{event.req_id});
|
||||||
const loader_id = bc.loader_id;
|
|
||||||
const target_id = bc.target_id orelse unreachable;
|
const target_id = bc.target_id orelse unreachable;
|
||||||
|
|
||||||
bc.reset();
|
bc.reset();
|
||||||
@@ -233,6 +271,30 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
|||||||
try cdp.sendEvent("Page.frameStartedLoading", .{
|
try cdp.sendEvent("Page.frameStartedLoading", .{
|
||||||
.frameId = target_id,
|
.frameId = target_id,
|
||||||
}, .{ .session_id = session_id });
|
}, .{ .session_id = session_id });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pageRemove(bc: anytype) !void {
|
||||||
|
// The main page is going to be removed, we need to remove contexts from other worlds first.
|
||||||
|
for (bc.isolated_worlds.items) |*isolated_world| {
|
||||||
|
try isolated_world.removeContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pageCreated(bc: anytype, page: *Page) !void {
|
||||||
|
for (bc.isolated_worlds.items) |*isolated_world| {
|
||||||
|
try isolated_world.createContextAndLoadPolyfills(bc.arena, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void {
|
||||||
|
// detachTarget could be called, in which case, we still have a page doing
|
||||||
|
// things, but no session.
|
||||||
|
const session_id = bc.session_id orelse return;
|
||||||
|
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{event.req_id});
|
||||||
|
const target_id = bc.target_id orelse unreachable;
|
||||||
|
const timestamp = event.timestamp;
|
||||||
|
|
||||||
|
var cdp = bc.cdp;
|
||||||
|
|
||||||
// Drivers are sensitive to the order of events. Some more than others.
|
// Drivers are sensitive to the order of events. Some more than others.
|
||||||
// The result for the Page.navigate seems like it _must_ come after
|
// The result for the Page.navigate seems like it _must_ come after
|
||||||
@@ -259,6 +321,17 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
|||||||
}, .{ .session_id = session_id });
|
}, .{ .session_id = session_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reason_: ?[]const u8 = switch (event.opts.reason) {
|
||||||
|
.anchor => "anchorClick",
|
||||||
|
.script, .history, .navigation => "scriptInitiated",
|
||||||
|
.form => switch (event.opts.method) {
|
||||||
|
.GET => "formSubmissionGet",
|
||||||
|
.POST => "formSubmissionPost",
|
||||||
|
else => unreachable,
|
||||||
|
},
|
||||||
|
.address_bar => null,
|
||||||
|
};
|
||||||
|
|
||||||
if (reason_ != null) {
|
if (reason_ != null) {
|
||||||
try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{
|
try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{
|
||||||
.frameId = target_id,
|
.frameId = target_id,
|
||||||
@@ -292,37 +365,14 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pageRemove(bc: anytype) !void {
|
|
||||||
// The main page is going to be removed, we need to remove contexts from other worlds first.
|
|
||||||
for (bc.isolated_worlds.items) |*isolated_world| {
|
|
||||||
try isolated_world.removeContext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pageCreated(bc: anytype, page: *Page) !void {
|
|
||||||
for (bc.isolated_worlds.items) |*isolated_world| {
|
|
||||||
try isolated_world.createContextAndLoadPolyfills(bc.arena, page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !void {
|
|
||||||
// detachTarget could be called, in which case, we still have a page doing
|
|
||||||
// things, but no session.
|
|
||||||
const session_id = bc.session_id orelse return;
|
|
||||||
const loader_id = bc.loader_id;
|
|
||||||
const target_id = bc.target_id orelse unreachable;
|
|
||||||
const timestamp = event.timestamp;
|
|
||||||
|
|
||||||
var cdp = bc.cdp;
|
|
||||||
// frameNavigated event
|
// frameNavigated event
|
||||||
try cdp.sendEvent("Page.frameNavigated", .{
|
try cdp.sendEvent("Page.frameNavigated", .{
|
||||||
.type = "Navigation",
|
.type = "Navigation",
|
||||||
.frame = Frame{
|
.frame = Frame{
|
||||||
.id = target_id,
|
.id = target_id,
|
||||||
.url = event.url,
|
.url = event.url,
|
||||||
.loaderId = bc.loader_id,
|
.loaderId = loader_id,
|
||||||
.securityOrigin = bc.security_origin,
|
.securityOrigin = bc.security_origin,
|
||||||
.secureContextType = bc.secure_context_type,
|
.secureContextType = bc.secure_context_type,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const LOADER_ID = "LOADERID42AA389647D702B4D805F49A";
|
|||||||
|
|
||||||
pub fn processMessage(cmd: anytype) !void {
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
|
getTargets,
|
||||||
attachToTarget,
|
attachToTarget,
|
||||||
closeTarget,
|
closeTarget,
|
||||||
createBrowserContext,
|
createBrowserContext,
|
||||||
@@ -38,6 +39,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
.getTargets => return getTargets(cmd),
|
||||||
.attachToTarget => return attachToTarget(cmd),
|
.attachToTarget => return attachToTarget(cmd),
|
||||||
.closeTarget => return closeTarget(cmd),
|
.closeTarget => return closeTarget(cmd),
|
||||||
.createBrowserContext => return createBrowserContext(cmd),
|
.createBrowserContext => return createBrowserContext(cmd),
|
||||||
@@ -52,6 +54,31 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn getTargets(cmd: anytype) !void {
|
||||||
|
// Some clients like Stagehand expects to have an existing context.
|
||||||
|
const bc = cmd.browser_context orelse cmd.createBrowserContext() catch |err| switch (err) {
|
||||||
|
error.AlreadyExists => unreachable,
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
|
|
||||||
|
const target_id = bc.target_id orelse {
|
||||||
|
return cmd.sendResult(.{
|
||||||
|
.targetInfos = [_]TargetInfo{},
|
||||||
|
}, .{ .include_session_id = false });
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmd.sendResult(.{
|
||||||
|
.targetInfos = [_]TargetInfo{.{
|
||||||
|
.targetId = target_id,
|
||||||
|
.type = "page",
|
||||||
|
.title = bc.getTitle() orelse "about:blank",
|
||||||
|
.url = bc.getURL() orelse "about:blank",
|
||||||
|
.attached = true,
|
||||||
|
.canAccessOpener = false,
|
||||||
|
}},
|
||||||
|
}, .{ .include_session_id = false });
|
||||||
|
}
|
||||||
|
|
||||||
fn getBrowserContexts(cmd: anytype) !void {
|
fn getBrowserContexts(cmd: anytype) !void {
|
||||||
var browser_context_ids: []const []const u8 = undefined;
|
var browser_context_ids: []const []const u8 = undefined;
|
||||||
if (cmd.browser_context) |bc| {
|
if (cmd.browser_context) |bc| {
|
||||||
@@ -167,7 +194,7 @@ fn createTarget(cmd: anytype) !void {
|
|||||||
.targetInfo = TargetInfo{
|
.targetInfo = TargetInfo{
|
||||||
.attached = false,
|
.attached = false,
|
||||||
.targetId = target_id,
|
.targetId = target_id,
|
||||||
.title = params.url,
|
.title = "about:blank",
|
||||||
.browserContextId = bc.id,
|
.browserContextId = bc.id,
|
||||||
.url = "about:blank",
|
.url = "about:blank",
|
||||||
},
|
},
|
||||||
@@ -178,9 +205,11 @@ fn createTarget(cmd: anytype) !void {
|
|||||||
try doAttachtoTarget(cmd, target_id);
|
try doAttachtoTarget(cmd, target_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
try page.navigate(params.url, .{
|
if (!std.mem.eql(u8, "about:blank", params.url)) {
|
||||||
.reason = .address_bar,
|
try page.navigate(params.url, .{
|
||||||
});
|
.reason = .address_bar,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try cmd.sendResult(.{
|
try cmd.sendResult(.{
|
||||||
.targetId = target_id,
|
.targetId = target_id,
|
||||||
@@ -199,12 +228,10 @@ fn attachToTarget(cmd: anytype) !void {
|
|||||||
return error.UnknownTargetId;
|
return error.UnknownTargetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bc.session_id != null) {
|
if (bc.session_id == null) {
|
||||||
return error.SessionAlreadyLoaded;
|
try doAttachtoTarget(cmd, target_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
try doAttachtoTarget(cmd, target_id);
|
|
||||||
|
|
||||||
return cmd.sendResult(
|
return cmd.sendResult(
|
||||||
.{ .sessionId = bc.session_id },
|
.{ .sessionId = bc.session_id },
|
||||||
.{ .include_session_id = false },
|
.{ .include_session_id = false },
|
||||||
@@ -269,8 +296,8 @@ fn getTargetInfo(cmd: anytype) !void {
|
|||||||
.targetInfo = TargetInfo{
|
.targetInfo = TargetInfo{
|
||||||
.targetId = target_id,
|
.targetId = target_id,
|
||||||
.type = "page",
|
.type = "page",
|
||||||
.title = "",
|
.title = bc.getTitle() orelse "about:blank",
|
||||||
.url = "",
|
.url = bc.getURL() orelse "about:blank",
|
||||||
.attached = true,
|
.attached = true,
|
||||||
.canAccessOpener = false,
|
.canAccessOpener = false,
|
||||||
},
|
},
|
||||||
@@ -281,8 +308,8 @@ fn getTargetInfo(cmd: anytype) !void {
|
|||||||
.targetInfo = TargetInfo{
|
.targetInfo = TargetInfo{
|
||||||
.targetId = "TID-STARTUP-B",
|
.targetId = "TID-STARTUP-B",
|
||||||
.type = "browser",
|
.type = "browser",
|
||||||
.title = "",
|
.title = "about:blank",
|
||||||
.url = "",
|
.url = "about:blank",
|
||||||
.attached = true,
|
.attached = true,
|
||||||
.canAccessOpener = false,
|
.canAccessOpener = false,
|
||||||
},
|
},
|
||||||
@@ -628,8 +655,8 @@ test "cdp.target: getTargetInfo" {
|
|||||||
try ctx.expectSentResult(.{
|
try ctx.expectSentResult(.{
|
||||||
.targetInfo = .{
|
.targetInfo = .{
|
||||||
.type = "browser",
|
.type = "browser",
|
||||||
.title = "",
|
.title = "about:blank",
|
||||||
.url = "",
|
.url = "about:blank",
|
||||||
.attached = true,
|
.attached = true,
|
||||||
.canAccessOpener = false,
|
.canAccessOpener = false,
|
||||||
},
|
},
|
||||||
@@ -662,7 +689,7 @@ test "cdp.target: getTargetInfo" {
|
|||||||
.targetId = "TID-A",
|
.targetId = "TID-A",
|
||||||
.type = "page",
|
.type = "page",
|
||||||
.title = "",
|
.title = "",
|
||||||
.url = "",
|
.url = "about:blank",
|
||||||
.attached = true,
|
.attached = true,
|
||||||
.canAccessOpener = false,
|
.canAccessOpener = false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -261,6 +261,16 @@ pub fn fulfillTransfer(self: *Client, transfer: *Transfer, status: u16, headers:
|
|||||||
return transfer.fulfill(status, headers, body);
|
return transfer.fulfill(status, headers, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn nextReqId(self: *Client) usize {
|
||||||
|
return self.next_request_id + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn incrReqId(self: *Client) usize {
|
||||||
|
const id = self.next_request_id + 1;
|
||||||
|
self.next_request_id = id;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
fn makeTransfer(self: *Client, req: Request) !*Transfer {
|
fn makeTransfer(self: *Client, req: Request) !*Transfer {
|
||||||
errdefer req.headers.deinit();
|
errdefer req.headers.deinit();
|
||||||
|
|
||||||
@@ -273,8 +283,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
|
|||||||
const transfer = try self.transfer_pool.create();
|
const transfer = try self.transfer_pool.create();
|
||||||
errdefer self.transfer_pool.destroy(transfer);
|
errdefer self.transfer_pool.destroy(transfer);
|
||||||
|
|
||||||
const id = self.next_request_id + 1;
|
const id = self.incrReqId();
|
||||||
self.next_request_id = id;
|
|
||||||
transfer.* = .{
|
transfer.* = .{
|
||||||
.arena = ArenaAllocator.init(self.allocator),
|
.arena = ArenaAllocator.init(self.allocator),
|
||||||
.id = id,
|
.id = id,
|
||||||
@@ -679,6 +688,19 @@ pub const Request = struct {
|
|||||||
xhr,
|
xhr,
|
||||||
script,
|
script,
|
||||||
fetch,
|
fetch,
|
||||||
|
|
||||||
|
// Allowed Values: Document, Stylesheet, Image, Media, Font, Script,
|
||||||
|
// TextTrack, XHR, Fetch, Prefetch, EventSource, WebSocket, Manifest,
|
||||||
|
// SignedExchange, Ping, CSPViolationReport, Preflight, FedCM, Other
|
||||||
|
// https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ResourceType
|
||||||
|
pub fn string(self: ResourceType) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.document => "Document",
|
||||||
|
.xhr => "XHR",
|
||||||
|
.script => "Script",
|
||||||
|
.fetch => "Fetch",
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub const Scope = enum {
|
|||||||
fetch,
|
fetch,
|
||||||
polyfill,
|
polyfill,
|
||||||
interceptor,
|
interceptor,
|
||||||
|
input,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
|
|||||||
72
src/main.zig
72
src/main.zig
@@ -23,67 +23,42 @@ const Allocator = std.mem.Allocator;
|
|||||||
const log = @import("log.zig");
|
const log = @import("log.zig");
|
||||||
const App = @import("app.zig").App;
|
const App = @import("app.zig").App;
|
||||||
const Server = @import("server.zig").Server;
|
const Server = @import("server.zig").Server;
|
||||||
|
const SigHandler = @import("sighandler.zig").SigHandler;
|
||||||
const Browser = @import("browser/browser.zig").Browser;
|
const Browser = @import("browser/browser.zig").Browser;
|
||||||
const DumpStripMode = @import("browser/dump.zig").Opts.StripMode;
|
const DumpStripMode = @import("browser/dump.zig").Opts.StripMode;
|
||||||
|
|
||||||
const build_config = @import("build_config");
|
const build_config = @import("build_config");
|
||||||
|
|
||||||
var _app: ?*App = null;
|
|
||||||
var _server: ?Server = null;
|
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
// allocator
|
// allocator
|
||||||
// - in Debug mode we use the General Purpose Allocator to detect memory leaks
|
// - in Debug mode we use the General Purpose Allocator to detect memory leaks
|
||||||
// - in Release mode we use the c allocator
|
// - in Release mode we use the c allocator
|
||||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
var gpa_instance: std.heap.DebugAllocator(.{}) = .init;
|
||||||
const alloc = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator;
|
const gpa = if (builtin.mode == .Debug) gpa_instance.allocator() else std.heap.c_allocator;
|
||||||
|
|
||||||
defer if (builtin.mode == .Debug) {
|
defer if (builtin.mode == .Debug) {
|
||||||
if (gpa.detectLeaks()) std.posix.exit(1);
|
if (gpa_instance.detectLeaks()) std.posix.exit(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
run(alloc) catch |err| {
|
var arena_instance = std.heap.ArenaAllocator.init(gpa);
|
||||||
|
const arena = arena_instance.allocator();
|
||||||
|
defer arena_instance.deinit();
|
||||||
|
|
||||||
|
var sighandler = SigHandler{ .arena = arena };
|
||||||
|
try sighandler.install();
|
||||||
|
|
||||||
|
run(gpa, arena, &sighandler) catch |err| {
|
||||||
// If explicit filters were set, they won't be valid anymore because
|
// If explicit filters were set, they won't be valid anymore because
|
||||||
// the args_arena is gone. We need to set it to something that's not
|
// the arena is gone. We need to set it to something that's not
|
||||||
// invalid. (We should just move the args_arena up to main)
|
// invalid. (We should just move the arena up to main)
|
||||||
log.opts.filter_scopes = &.{};
|
log.opts.filter_scopes = &.{};
|
||||||
log.fatal(.app, "exit", .{ .err = err });
|
log.fatal(.app, "exit", .{ .err = err });
|
||||||
std.posix.exit(1);
|
std.posix.exit(1);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle app shutdown gracefuly on signals.
|
fn run(gpa: Allocator, arena: Allocator, sighandler: *SigHandler) !void {
|
||||||
fn shutdown() void {
|
const args = try parseArgs(arena);
|
||||||
const sigaction: std.posix.Sigaction = .{
|
|
||||||
.handler = .{
|
|
||||||
.handler = struct {
|
|
||||||
pub fn handler(_: c_int) callconv(.c) void {
|
|
||||||
// Shutdown service gracefuly.
|
|
||||||
if (_server) |server| {
|
|
||||||
server.deinit();
|
|
||||||
}
|
|
||||||
if (_app) |app| {
|
|
||||||
app.deinit();
|
|
||||||
}
|
|
||||||
std.posix.exit(0);
|
|
||||||
}
|
|
||||||
}.handler,
|
|
||||||
},
|
|
||||||
.mask = std.posix.empty_sigset,
|
|
||||||
.flags = 0,
|
|
||||||
};
|
|
||||||
// Exit the program on SIGINT signal. When running the browser in a Docker
|
|
||||||
// container, sending a CTRL-C (SIGINT) signal is catched but doesn't exit
|
|
||||||
// the program. Here we force exiting on SIGINT.
|
|
||||||
std.posix.sigaction(std.posix.SIG.INT, &sigaction, null);
|
|
||||||
std.posix.sigaction(std.posix.SIG.TERM, &sigaction, null);
|
|
||||||
std.posix.sigaction(std.posix.SIG.QUIT, &sigaction, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(alloc: Allocator) !void {
|
|
||||||
var args_arena = std.heap.ArenaAllocator.init(alloc);
|
|
||||||
defer args_arena.deinit();
|
|
||||||
const args = try parseArgs(args_arena.allocator());
|
|
||||||
|
|
||||||
switch (args.mode) {
|
switch (args.mode) {
|
||||||
.help => {
|
.help => {
|
||||||
@@ -110,13 +85,13 @@ fn run(alloc: Allocator) !void {
|
|||||||
const user_agent = blk: {
|
const user_agent = blk: {
|
||||||
const USER_AGENT = "User-Agent: Lightpanda/1.0";
|
const USER_AGENT = "User-Agent: Lightpanda/1.0";
|
||||||
if (args.userAgentSuffix()) |suffix| {
|
if (args.userAgentSuffix()) |suffix| {
|
||||||
break :blk try std.fmt.allocPrintSentinel(args_arena.allocator(), "{s} {s}", .{ USER_AGENT, suffix }, 0);
|
break :blk try std.fmt.allocPrintSentinel(arena, "{s} {s}", .{ USER_AGENT, suffix }, 0);
|
||||||
}
|
}
|
||||||
break :blk USER_AGENT;
|
break :blk USER_AGENT;
|
||||||
};
|
};
|
||||||
|
|
||||||
// _app is global to handle graceful shutdown.
|
// _app is global to handle graceful shutdown.
|
||||||
_app = try App.init(alloc, .{
|
var app = try App.init(gpa, .{
|
||||||
.run_mode = args.mode,
|
.run_mode = args.mode,
|
||||||
.http_proxy = args.httpProxy(),
|
.http_proxy = args.httpProxy(),
|
||||||
.proxy_bearer_token = args.proxyBearerToken(),
|
.proxy_bearer_token = args.proxyBearerToken(),
|
||||||
@@ -127,24 +102,23 @@ fn run(alloc: Allocator) !void {
|
|||||||
.http_max_concurrent = args.httpMaxConcurrent(),
|
.http_max_concurrent = args.httpMaxConcurrent(),
|
||||||
.user_agent = user_agent,
|
.user_agent = user_agent,
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = _app.?;
|
|
||||||
defer app.deinit();
|
defer app.deinit();
|
||||||
app.telemetry.record(.{ .run = {} });
|
app.telemetry.record(.{ .run = {} });
|
||||||
|
|
||||||
switch (args.mode) {
|
switch (args.mode) {
|
||||||
.serve => |opts| {
|
.serve => |opts| {
|
||||||
log.debug(.app, "startup", .{ .mode = "serve" });
|
log.debug(.app, "startup", .{ .mode = "serve" });
|
||||||
const address = std.net.Address.parseIp4(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 });
|
||||||
return args.printUsageAndExit(false);
|
return args.printUsageAndExit(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// _server is global to handle graceful shutdown.
|
// _server is global to handle graceful shutdown.
|
||||||
_server = try Server.init(app, address);
|
var server = try Server.init(app, address);
|
||||||
const server = &_server.?;
|
|
||||||
defer server.deinit();
|
defer server.deinit();
|
||||||
|
|
||||||
|
try sighandler.on(Server.stop, .{&server});
|
||||||
|
|
||||||
// max timeout of 1 week.
|
// max timeout of 1 week.
|
||||||
const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(i32, opts.timeout) * 1000;
|
const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(i32, opts.timeout) * 1000;
|
||||||
server.run(address, timeout) catch |err| {
|
server.run(address, timeout) catch |err| {
|
||||||
@@ -888,7 +862,7 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, path, "/xhr/json")) {
|
if (std.mem.eql(u8, path, "/xhr/json")) {
|
||||||
return req.respond("{\"over\":\"9000!!!\"}", .{
|
return req.respond("{\"over\":\"9000!!!\",\"updated_at\":1765867200000}", .{
|
||||||
.extra_headers = &.{
|
.extra_headers = &.{
|
||||||
.{ .name = "Content-Type", .value = "application/json" },
|
.{ .name = "Content-Type", .value = "application/json" },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -90,14 +90,17 @@ pub const Notification = struct {
|
|||||||
pub const PageRemove = struct {};
|
pub const PageRemove = struct {};
|
||||||
|
|
||||||
pub const PageNavigate = struct {
|
pub const PageNavigate = struct {
|
||||||
|
req_id: usize,
|
||||||
timestamp: u32,
|
timestamp: u32,
|
||||||
url: []const u8,
|
url: []const u8,
|
||||||
opts: page.NavigateOpts,
|
opts: page.NavigateOpts,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PageNavigated = struct {
|
pub const PageNavigated = struct {
|
||||||
|
req_id: usize,
|
||||||
timestamp: u32,
|
timestamp: u32,
|
||||||
url: []const u8,
|
url: []const u8,
|
||||||
|
opts: page.NavigatedOpts,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const PageNetworkIdle = struct {
|
pub const PageNetworkIdle = struct {
|
||||||
@@ -296,6 +299,7 @@ test "Notification" {
|
|||||||
|
|
||||||
// noop
|
// noop
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.req_id = 1,
|
||||||
.timestamp = 4,
|
.timestamp = 4,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
.opts = .{},
|
.opts = .{},
|
||||||
@@ -305,6 +309,7 @@ test "Notification" {
|
|||||||
|
|
||||||
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.req_id = 1,
|
||||||
.timestamp = 4,
|
.timestamp = 4,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
.opts = .{},
|
.opts = .{},
|
||||||
@@ -313,6 +318,7 @@ test "Notification" {
|
|||||||
|
|
||||||
notifier.unregisterAll(&tc);
|
notifier.unregisterAll(&tc);
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.req_id = 1,
|
||||||
.timestamp = 10,
|
.timestamp = 10,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
.opts = .{},
|
.opts = .{},
|
||||||
@@ -322,21 +328,23 @@ test "Notification" {
|
|||||||
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.req_id = 1,
|
||||||
.timestamp = 10,
|
.timestamp = 10,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
.opts = .{},
|
.opts = .{},
|
||||||
});
|
});
|
||||||
notifier.dispatch(.page_navigated, &.{ .timestamp = 6, .url = undefined });
|
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(14, tc.page_navigate);
|
try testing.expectEqual(14, tc.page_navigate);
|
||||||
try testing.expectEqual(6, tc.page_navigated);
|
try testing.expectEqual(6, tc.page_navigated);
|
||||||
|
|
||||||
notifier.unregisterAll(&tc);
|
notifier.unregisterAll(&tc);
|
||||||
notifier.dispatch(.page_navigate, &.{
|
notifier.dispatch(.page_navigate, &.{
|
||||||
|
.req_id = 1,
|
||||||
.timestamp = 100,
|
.timestamp = 100,
|
||||||
.url = undefined,
|
.url = undefined,
|
||||||
.opts = .{},
|
.opts = .{},
|
||||||
});
|
});
|
||||||
notifier.dispatch(.page_navigated, &.{ .timestamp = 100, .url = undefined });
|
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(14, tc.page_navigate);
|
try testing.expectEqual(14, tc.page_navigate);
|
||||||
try testing.expectEqual(6, tc.page_navigated);
|
try testing.expectEqual(6, tc.page_navigated);
|
||||||
|
|
||||||
@@ -344,27 +352,27 @@ test "Notification" {
|
|||||||
// unregister
|
// unregister
|
||||||
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
|
||||||
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
|
||||||
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
|
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(114, tc.page_navigate);
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
try testing.expectEqual(1006, tc.page_navigated);
|
try testing.expectEqual(1006, tc.page_navigated);
|
||||||
|
|
||||||
notifier.unregister(.page_navigate, &tc);
|
notifier.unregister(.page_navigate, &tc);
|
||||||
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
|
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(114, tc.page_navigate);
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
try testing.expectEqual(2006, tc.page_navigated);
|
try testing.expectEqual(2006, tc.page_navigated);
|
||||||
|
|
||||||
notifier.unregister(.page_navigated, &tc);
|
notifier.unregister(.page_navigated, &tc);
|
||||||
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
|
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(114, tc.page_navigate);
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
try testing.expectEqual(2006, tc.page_navigated);
|
try testing.expectEqual(2006, tc.page_navigated);
|
||||||
|
|
||||||
// already unregistered, try anyways
|
// already unregistered, try anyways
|
||||||
notifier.unregister(.page_navigated, &tc);
|
notifier.unregister(.page_navigated, &tc);
|
||||||
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
|
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
|
||||||
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
|
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
|
||||||
try testing.expectEqual(114, tc.page_navigate);
|
try testing.expectEqual(114, tc.page_navigate);
|
||||||
try testing.expectEqual(2006, tc.page_navigated);
|
try testing.expectEqual(2006, tc.page_navigated);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const MAX_MESSAGE_SIZE = 512 * 1024 + 14 + 140;
|
|||||||
|
|
||||||
pub const Server = struct {
|
pub const Server = struct {
|
||||||
app: *App,
|
app: *App,
|
||||||
shutdown: bool,
|
shutdown: bool = false,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
client: ?posix.socket_t,
|
client: ?posix.socket_t,
|
||||||
listener: ?posix.socket_t,
|
listener: ?posix.socket_t,
|
||||||
@@ -53,16 +53,36 @@ pub const Server = struct {
|
|||||||
.app = app,
|
.app = app,
|
||||||
.client = null,
|
.client = null,
|
||||||
.listener = null,
|
.listener = null,
|
||||||
.shutdown = false,
|
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.json_version_response = json_version_response,
|
.json_version_response = json_version_response,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Interrupts the server so that main can complete normally and call all defer handlers.
|
||||||
|
pub fn stop(self: *Server) void {
|
||||||
|
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux and BSD/macOS handle canceling a socket blocked on accept differently.
|
||||||
|
// For Linux, we use std.shutdown, which will cause accept to return error.SocketNotListening (EINVAL).
|
||||||
|
// For BSD, shutdown will return an error. Instead we call posix.close, which will result with error.ConnectionAborted (BADF).
|
||||||
|
if (self.listener) |listener| switch (builtin.target.os.tag) {
|
||||||
|
.linux => posix.shutdown(listener, .recv) catch |err| {
|
||||||
|
log.warn(.app, "listener shutdown", .{ .err = err });
|
||||||
|
},
|
||||||
|
.macos, .freebsd, .netbsd, .openbsd => {
|
||||||
|
self.listener = null;
|
||||||
|
posix.close(listener);
|
||||||
|
},
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Server) void {
|
pub fn deinit(self: *Server) void {
|
||||||
self.shutdown = true;
|
|
||||||
if (self.listener) |listener| {
|
if (self.listener) |listener| {
|
||||||
posix.close(listener);
|
posix.close(listener);
|
||||||
|
self.listener = null;
|
||||||
}
|
}
|
||||||
// *if* server.run is running, we should really wait for it to return
|
// *if* server.run is running, we should really wait for it to return
|
||||||
// before existing from here.
|
// before existing from here.
|
||||||
@@ -83,14 +103,19 @@ pub const Server = struct {
|
|||||||
try posix.listen(listener, 1);
|
try posix.listen(listener, 1);
|
||||||
|
|
||||||
log.info(.app, "server running", .{ .address = address });
|
log.info(.app, "server running", .{ .address = address });
|
||||||
while (true) {
|
while (!@atomicLoad(bool, &self.shutdown, .monotonic)) {
|
||||||
const socket = posix.accept(listener, null, null, posix.SOCK.NONBLOCK) catch |err| {
|
const socket = posix.accept(listener, null, null, posix.SOCK.NONBLOCK) catch |err| {
|
||||||
if (self.shutdown) {
|
switch (err) {
|
||||||
return;
|
error.SocketNotListening, error.ConnectionAborted => {
|
||||||
|
log.info(.app, "server stopped", .{});
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
log.err(.app, "CDP accept", .{ .err = err });
|
||||||
|
std.Thread.sleep(std.time.ns_per_s);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
log.err(.app, "CDP accept", .{ .err = err });
|
|
||||||
std.Thread.sleep(std.time.ns_per_s);
|
|
||||||
continue;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.client = socket;
|
self.client = socket;
|
||||||
|
|||||||
88
src/sighandler.zig
Normal file
88
src/sighandler.zig
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
//! This structure processes operating system signals (SIGINT, SIGTERM)
|
||||||
|
//! and runs callbacks to clean up the system gracefully.
|
||||||
|
//!
|
||||||
|
//! The structure does not clear the memory allocated in the arena,
|
||||||
|
//! clear the entire arena when exiting the program.
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const log = @import("log.zig");
|
||||||
|
|
||||||
|
pub const SigHandler = struct {
|
||||||
|
arena: Allocator,
|
||||||
|
|
||||||
|
sigset: std.posix.sigset_t = undefined,
|
||||||
|
handle_thread: ?std.Thread = null,
|
||||||
|
|
||||||
|
attempt: u32 = 0,
|
||||||
|
listeners: std.ArrayList(Listener) = .empty,
|
||||||
|
|
||||||
|
pub const Listener = struct {
|
||||||
|
args: []const u8,
|
||||||
|
start: *const fn (context: *const anyopaque) void,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn install(self: *SigHandler) !void {
|
||||||
|
// Block SIGINT and SIGTERM for the current thread and all created from it
|
||||||
|
self.sigset = std.posix.sigemptyset();
|
||||||
|
std.posix.sigaddset(&self.sigset, std.posix.SIG.INT);
|
||||||
|
std.posix.sigaddset(&self.sigset, std.posix.SIG.TERM);
|
||||||
|
std.posix.sigaddset(&self.sigset, std.posix.SIG.QUIT);
|
||||||
|
std.posix.sigprocmask(std.posix.SIG.BLOCK, &self.sigset, null);
|
||||||
|
|
||||||
|
self.handle_thread = try std.Thread.spawn(.{ .allocator = self.arena }, SigHandler.sighandle, .{self});
|
||||||
|
self.handle_thread.?.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on(self: *SigHandler, func: anytype, args: std.meta.ArgsTuple(@TypeOf(func))) !void {
|
||||||
|
assert(@typeInfo(@TypeOf(func)).@"fn".return_type.? == void);
|
||||||
|
|
||||||
|
const Args = @TypeOf(args);
|
||||||
|
const TypeErased = struct {
|
||||||
|
fn start(context: *const anyopaque) void {
|
||||||
|
const args_casted: *const Args = @ptrCast(@alignCast(context));
|
||||||
|
@call(.auto, func, args_casted.*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buffer = try self.arena.alignedAlloc(u8, .of(Args), @sizeOf(Args));
|
||||||
|
errdefer self.arena.free(buffer);
|
||||||
|
|
||||||
|
const bytes: []const u8 = @ptrCast((&args)[0..1]);
|
||||||
|
@memcpy(buffer, bytes);
|
||||||
|
|
||||||
|
try self.listeners.append(self.arena, .{
|
||||||
|
.args = buffer,
|
||||||
|
.start = TypeErased.start,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sighandle(self: *SigHandler) noreturn {
|
||||||
|
while (true) {
|
||||||
|
var sig: c_int = 0;
|
||||||
|
|
||||||
|
const rc = std.c.sigwait(&self.sigset, &sig);
|
||||||
|
if (rc != 0) {
|
||||||
|
log.err(.app, "Unable to process signal {}", .{rc});
|
||||||
|
std.process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sig) {
|
||||||
|
std.posix.SIG.INT, std.posix.SIG.TERM => {
|
||||||
|
if (self.attempt > 1) {
|
||||||
|
std.process.exit(1);
|
||||||
|
}
|
||||||
|
self.attempt += 1;
|
||||||
|
|
||||||
|
log.info(.app, "Received termination signal...", .{});
|
||||||
|
for (self.listeners.items) |*item| {
|
||||||
|
item.start(item.args.ptr);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
else => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
25
src/tests/dom/document_write.html
Normal file
25
src/tests/dom/document_write.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<a id="a1" href="foo" class="ok">OK</a>
|
||||||
|
<p id="p1" class="ok empty">
|
||||||
|
<span id="s1"></span>
|
||||||
|
</p>
|
||||||
|
<p id="p2"> And</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script id=document_write>
|
||||||
|
document.open();
|
||||||
|
document.write("<p id=ok>Hello world!</p>");
|
||||||
|
document.write("<p>I am a fish</p>");
|
||||||
|
document.write("<p>The number is 42</p>");
|
||||||
|
document.close();
|
||||||
|
|
||||||
|
const ok = document.getElementById("ok");
|
||||||
|
testing.expectEqual('Hello world!', ok.innerText);
|
||||||
|
|
||||||
|
const content = document.firstElementChild.innerHTML;
|
||||||
|
testing.expectEqual('<head></head><body><p id="ok">Hello world!</p><p>I am a fish</p><p>The number is 42</p></body>', content);
|
||||||
|
</script>
|
||||||
@@ -113,4 +113,13 @@
|
|||||||
// doesn't crash on null receiver
|
// doesn't crash on null receiver
|
||||||
content.addEventListener('he2', null);
|
content.addEventListener('he2', null);
|
||||||
content.dispatchEvent(new Event('he2'));
|
content.dispatchEvent(new Event('he2'));
|
||||||
|
|
||||||
|
// Test that EventTarget constructor properly initializes vtable
|
||||||
|
const et = new EventTarget();
|
||||||
|
testing.expectEqual('[object EventTarget]', et.toString());
|
||||||
|
|
||||||
|
let constructorTestCalled = false;
|
||||||
|
et.addEventListener('test', () => { constructorTestCalled = true; });
|
||||||
|
et.dispatchEvent(new Event('test'));
|
||||||
|
testing.expectEqual(true, constructorTestCalled);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
testing.async(promise1, (json) => {
|
testing.async(promise1, (json) => {
|
||||||
testing.expectEqual({over: '9000!!!'}, json);
|
testing.expectEqual("number", typeof json.updated_at);
|
||||||
|
testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
testing.async(promise1, (json) => {
|
testing.async(promise1, (json) => {
|
||||||
testing.expectEqual({over: '9000!!!'}, json);
|
testing.expectEqual("number", typeof json.updated_at);
|
||||||
|
testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
116
src/tests/html/canvas.html
Normal file
116
src/tests/html/canvas.html
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=CanvasRenderingContext2D>
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
testing.expectEqual(true, ctx instanceof CanvasRenderingContext2D);
|
||||||
|
// We can't really test this but let's try to call it at least.
|
||||||
|
ctx.fillRect(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=CanvasRenderingContext2D#fillStyle>
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("2d");
|
||||||
|
|
||||||
|
// Black by default.
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#000000");
|
||||||
|
ctx.fillStyle = "red";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#ff0000");
|
||||||
|
ctx.fillStyle = "rebeccapurple";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#663399");
|
||||||
|
// No changes made if color is invalid.
|
||||||
|
ctx.fillStyle = "invalid-color";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#663399");
|
||||||
|
ctx.fillStyle = "#fc0";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#ffcc00");
|
||||||
|
ctx.fillStyle = "#ff0000";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "#ff0000");
|
||||||
|
ctx.fillStyle = "#fF00000F";
|
||||||
|
testing.expectEqual(ctx.fillStyle, "rgba(255, 0, 0, 0.06)");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=WebGLRenderingContext#getSupportedExtensions>
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("webgl");
|
||||||
|
testing.expectEqual(true, ctx instanceof WebGLRenderingContext);
|
||||||
|
|
||||||
|
const supportedExtensions = ctx.getSupportedExtensions();
|
||||||
|
// The order Chrome prefer.
|
||||||
|
const expectedExtensions = [
|
||||||
|
"ANGLE_instanced_arrays",
|
||||||
|
"EXT_blend_minmax",
|
||||||
|
"EXT_clip_control",
|
||||||
|
"EXT_color_buffer_half_float",
|
||||||
|
"EXT_depth_clamp",
|
||||||
|
"EXT_disjoint_timer_query",
|
||||||
|
"EXT_float_blend",
|
||||||
|
"EXT_frag_depth",
|
||||||
|
"EXT_polygon_offset_clamp",
|
||||||
|
"EXT_shader_texture_lod",
|
||||||
|
"EXT_texture_compression_bptc",
|
||||||
|
"EXT_texture_compression_rgtc",
|
||||||
|
"EXT_texture_filter_anisotropic",
|
||||||
|
"EXT_texture_mirror_clamp_to_edge",
|
||||||
|
"EXT_sRGB",
|
||||||
|
"KHR_parallel_shader_compile",
|
||||||
|
"OES_element_index_uint",
|
||||||
|
"OES_fbo_render_mipmap",
|
||||||
|
"OES_standard_derivatives",
|
||||||
|
"OES_texture_float",
|
||||||
|
"OES_texture_float_linear",
|
||||||
|
"OES_texture_half_float",
|
||||||
|
"OES_texture_half_float_linear",
|
||||||
|
"OES_vertex_array_object",
|
||||||
|
"WEBGL_blend_func_extended",
|
||||||
|
"WEBGL_color_buffer_float",
|
||||||
|
"WEBGL_compressed_texture_astc",
|
||||||
|
"WEBGL_compressed_texture_etc",
|
||||||
|
"WEBGL_compressed_texture_etc1",
|
||||||
|
"WEBGL_compressed_texture_pvrtc",
|
||||||
|
"WEBGL_compressed_texture_s3tc",
|
||||||
|
"WEBGL_compressed_texture_s3tc_srgb",
|
||||||
|
"WEBGL_debug_renderer_info",
|
||||||
|
"WEBGL_debug_shaders",
|
||||||
|
"WEBGL_depth_texture",
|
||||||
|
"WEBGL_draw_buffers",
|
||||||
|
"WEBGL_lose_context",
|
||||||
|
"WEBGL_multi_draw",
|
||||||
|
"WEBGL_polygon_mode"
|
||||||
|
];
|
||||||
|
|
||||||
|
testing.expectEqual(expectedExtensions.length, supportedExtensions.length);
|
||||||
|
for (let i = 0; i < expectedExtensions.length; i++) {
|
||||||
|
testing.expectEqual(expectedExtensions[i], supportedExtensions[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=WebGLRenderingCanvas#getExtension>
|
||||||
|
// WEBGL_debug_renderer_info
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("webgl");
|
||||||
|
const rendererInfo = ctx.getExtension("WEBGL_debug_renderer_info");
|
||||||
|
testing.expectEqual(true, rendererInfo instanceof WEBGL_debug_renderer_info);
|
||||||
|
|
||||||
|
testing.expectEqual(rendererInfo.UNMASKED_VENDOR_WEBGL, 0x9245);
|
||||||
|
testing.expectEqual(rendererInfo.UNMASKED_RENDERER_WEBGL, 0x9246);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WEBGL_lose_context
|
||||||
|
{
|
||||||
|
const element = document.createElement("canvas");
|
||||||
|
const ctx = element.getContext("webgl");
|
||||||
|
const loseContext = ctx.getExtension("WEBGL_lose_context");
|
||||||
|
testing.expectEqual(true, loseContext instanceof WEBGL_lose_context);
|
||||||
|
|
||||||
|
loseContext.loseContext();
|
||||||
|
loseContext.restoreContext();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -20,3 +20,19 @@
|
|||||||
testing.expectEqual('P', t.content.childNodes[1].tagName);
|
testing.expectEqual('P', t.content.childNodes[1].tagName);
|
||||||
testing.expectEqual('9000!', t.content.childNodes[1].innerHTML);
|
testing.expectEqual('9000!', t.content.childNodes[1].innerHTML);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template id="hello"><p>hello, world</p></template>
|
||||||
|
|
||||||
|
<script id=template_parsing>
|
||||||
|
const tt = document.getElementById('hello');
|
||||||
|
testing.expectEqual('<p>hello, world</p>', tt.innerHTML);
|
||||||
|
|
||||||
|
// > The Node.childNodes property of the <template> element is always empty
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template#usage_notes
|
||||||
|
testing.expectEqual(0, tt.childNodes.length);
|
||||||
|
|
||||||
|
let out = document.createElement('div');
|
||||||
|
out.appendChild(tt.content.cloneNode(true));
|
||||||
|
|
||||||
|
testing.expectEqual('<p>hello, world</p>', out.innerHTML);
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -65,6 +65,8 @@
|
|||||||
testing.expectEqual(200, req3.status);
|
testing.expectEqual(200, req3.status);
|
||||||
testing.expectEqual('OK', req3.statusText);
|
testing.expectEqual('OK', req3.statusText);
|
||||||
testing.expectEqual('9000!!!', req3.response.over);
|
testing.expectEqual('9000!!!', req3.response.over);
|
||||||
|
testing.expectEqual("number", typeof req3.response.updated_at);
|
||||||
|
testing.expectEqual(1765867200000, req3.response.updated_at);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
1
vendor/mbedtls
vendored
1
vendor/mbedtls
vendored
Submodule vendor/mbedtls deleted from c765c831e5
Reference in New Issue
Block a user