mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 15:40:04 +00:00
Compare commits
264 Commits
| 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 | ||
|
|
43b210dcf5 | ||
|
|
16e7c0841d | ||
|
|
0a705b15ce | ||
|
|
2f2870c066 | ||
|
|
9c277ae26e | ||
|
|
19b9ba8601 | ||
|
|
92ddb5640d | ||
|
|
38c6a9bd9d | ||
|
|
3cc53b579b | ||
|
|
c009669ec8 | ||
|
|
0e3f18367a | ||
|
|
4cf61d101c | ||
|
|
47ceabc43f | ||
|
|
dc4927d49e | ||
|
|
bc29fce41a | ||
|
|
97c92d7873 | ||
|
|
68fbe742eb | ||
|
|
5b08188b93 | ||
|
|
aa884803e3 | ||
|
|
d0d2850458 | ||
|
|
f9087d3840 | ||
|
|
0fab9be5c2 | ||
|
|
53c73c5851 | ||
|
|
996837ab0c | ||
|
|
74a5438587 | ||
|
|
1fd28cef40 | ||
|
|
7c825cbe82 | ||
|
|
40522d8720 | ||
|
|
2446580db9 | ||
|
|
70e02dcfc7 | ||
|
|
235337d1c9 | ||
|
|
8a867bc9c2 | ||
|
|
7aafab9c0a | ||
|
|
10c2d7dc87 | ||
|
|
9b990da7fa | ||
|
|
93542c9756 | ||
|
|
4be7fa178c | ||
|
|
5785c147da | ||
|
|
b68675bb94 | ||
|
|
3307a664c4 | ||
|
|
dd43be4818 | ||
|
|
c491648941 | ||
|
|
1085950b88 | ||
|
|
1d91d24b12 | ||
|
|
cc83d85542 | ||
|
|
706a87a458 | ||
|
|
3ec15ad1f7 | ||
|
|
07e603ecda | ||
|
|
52fc2c365f | ||
|
|
8f3620adf0 | ||
|
|
f7abf0956f | ||
|
|
73217f7832 | ||
|
|
52fb2010fc | ||
|
|
03ffcdb604 | ||
|
|
20314fccec | ||
|
|
018e95bea7 | ||
|
|
c9dc4ef57a | ||
|
|
6c9d013e20 | ||
|
|
d2d10d5db4 | ||
|
|
37a8a24528 | ||
|
|
d0b83c674c | ||
|
|
b58ff2c869 | ||
|
|
b2e41837d9 | ||
|
|
2e6ec1e23b | ||
|
|
7808d12de2 | ||
|
|
1015fc09ee | ||
|
|
1c37b1c70e | ||
|
|
28ec8d4b94 | ||
|
|
6e42df2e71 | ||
|
|
6b924e8a4c | ||
|
|
80ae3c9fc6 | ||
|
|
2422c8718c | ||
|
|
b5ef8418a6 | ||
|
|
8d4cf400ce | ||
|
|
c6a0368c61 | ||
|
|
033eb82ae5 | ||
|
|
2d14452dda | ||
|
|
a69164b482 | ||
|
|
d4d35670a0 | ||
|
|
b40e7ece91 | ||
|
|
9c4367b26e | ||
|
|
0eb639ac76 | ||
|
|
9778eed1ed | ||
|
|
8b4ffeb911 | ||
|
|
b55b9bba0a | ||
|
|
82a45253de | ||
|
|
4c957041e2 | ||
|
|
b8f9598de3 | ||
|
|
907bd33d87 | ||
|
|
e9b08f19cf | ||
|
|
f97697535f | ||
|
|
e80c8d5bff | ||
|
|
70a009a52b | ||
|
|
8ab9364f19 | ||
|
|
186655e614 | ||
|
|
43958b81f8 | ||
|
|
2d8a95946a | ||
|
|
a7c3bad9ad | ||
|
|
7d39bc979f | ||
|
|
d60d3ebaac | ||
|
|
ba66b7c5db | ||
|
|
8342f0c394 | ||
|
|
69884b9d8d | ||
|
|
c568a75599 | ||
|
|
9deb5249a9 | ||
|
|
fb6fbffe3f | ||
|
|
510c61cc20 | ||
|
|
6915738e02 | ||
|
|
4f62cc833b | ||
|
|
46ffb801db | ||
|
|
d2065f713f | ||
|
|
6f8c3abb55 | ||
|
|
163a0e8b70 | ||
|
|
ca3efb3ad9 | ||
|
|
4468932346 | ||
|
|
9a03ba61c5 | ||
|
|
fe3777041d | ||
|
|
1c579a98b4 | ||
|
|
3e10cf0a64 | ||
|
|
ef9784a7d4 | ||
|
|
6f1c3c8fd2 | ||
|
|
e12c650ea5 | ||
|
|
9373cbb440 | ||
|
|
fd6d038956 | ||
|
|
9845392b71 | ||
|
|
0795b7a583 | ||
|
|
29f0e71f10 | ||
|
|
1a47f7b5a8 | ||
|
|
6a30ab7a57 | ||
|
|
758f7deb93 | ||
|
|
9f4e3bf792 | ||
|
|
a5dfe8ab28 | ||
|
|
288379aa7d | ||
|
|
a9739bf361 | ||
|
|
c69adcb163 | ||
|
|
14a23123c0 | ||
|
|
09be5e23f1 | ||
|
|
0aaed08c1e |
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.1'
|
|
||||||
arch:
|
arch:
|
||||||
description: 'CPU arch used to select the v8 lib'
|
description: 'CPU arch used to select the v8 lib'
|
||||||
required: false
|
required: false
|
||||||
@@ -17,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.1
|
|
||||||
|
|
||||||
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.1
|
|
||||||
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"]
|
||||||
|
|||||||
91
Makefile
91
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,77 +47,60 @@ 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
|
## Test - `grep` is used to filter out the huge compile command on build
|
||||||
|
ifeq ($(OS), macos)
|
||||||
test:
|
test:
|
||||||
@TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
|
@script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \
|
||||||
|
| grep --line-buffered -v "^/.*zig test -freference-trace"
|
||||||
|
else
|
||||||
|
test:
|
||||||
|
@script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' /dev/null 2>&1 \
|
||||||
|
| grep --line-buffered -v "^/.*zig test -freference-trace"
|
||||||
|
endif
|
||||||
|
|
||||||
## Run demo/runner end to end tests
|
## Run demo/runner end to end tests
|
||||||
end2end:
|
end2end:
|
||||||
@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
|
||||||
@@ -144,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 \
|
||||||
@@ -181,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" && \
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -18,7 +18,7 @@ Lightpanda is the open-source browser made for headless usage:
|
|||||||
|
|
||||||
- Javascript execution
|
- Javascript execution
|
||||||
- Support of Web APIs (partial, WIP)
|
- Support of Web APIs (partial, WIP)
|
||||||
- Compatible with Playwright[^1], Puppeteer, chromedp through CDP
|
- Compatible with Playwright[^1], Puppeteer, chromedp through [CDP](https://chromedevtools.github.io/devtools-protocol/)
|
||||||
|
|
||||||
Fast web automation for AI agents, LLM training, scraping and testing:
|
Fast web automation for AI agents, LLM training, scraping and testing:
|
||||||
|
|
||||||
@@ -158,22 +158,21 @@ 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
|
||||||
|
|
||||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to
|
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:
|
||||||
|
|
||||||
@@ -190,10 +189,10 @@ For systems with [Nix](https://nixos.org/download/), you can use the devShell:
|
|||||||
nix develop
|
nix develop
|
||||||
```
|
```
|
||||||
|
|
||||||
For MacOS, you only need cmake:
|
For MacOS, you need [Xcode](https://developer.apple.com/xcode/) and the following pacakges from homebrew:
|
||||||
|
|
||||||
```
|
```
|
||||||
brew install cmake
|
brew install cmake pkgconf
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install and build dependencies
|
### Install and build dependencies
|
||||||
@@ -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
|
||||||
|
|||||||
172
build.zig
172
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.1";
|
|
||||||
|
|
||||||
pub fn build(b: *Build) !void {
|
pub fn build(b: *Build) !void {
|
||||||
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
|
||||||
.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,16 +313,30 @@ 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);
|
||||||
|
|
||||||
switch (target.result.os.tag) {
|
switch (target.result.os.tag) {
|
||||||
.macos => {
|
.macos => {
|
||||||
@@ -841,11 +794,68 @@ 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",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn buildAda(b: *Build, m: *Build.Module) !void {
|
||||||
|
const ada_dep = b.dependency("ada-singleheader", .{});
|
||||||
|
|
||||||
|
const ada_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("vendor/ada/root.zig"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ada_lib = b.addLibrary(.{
|
||||||
|
.name = "ada",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.link_libcpp = true,
|
||||||
|
.target = m.resolved_target,
|
||||||
|
.optimize = m.optimize,
|
||||||
|
}),
|
||||||
|
.linkage = .static,
|
||||||
|
});
|
||||||
|
|
||||||
|
ada_lib.addCSourceFile(.{
|
||||||
|
.file = ada_dep.path("ada.cpp"),
|
||||||
|
.flags = &.{ "-std=c++20", "-O3" },
|
||||||
|
.language = .cpp,
|
||||||
|
});
|
||||||
|
|
||||||
|
ada_lib.installHeader(ada_dep.path("ada_c.h"), "ada_c.h");
|
||||||
|
|
||||||
|
// Link the library to ada module.
|
||||||
|
ada_mod.linkLibrary(ada_lib);
|
||||||
|
// Expose ada module to main module.
|
||||||
|
m.addImport("ada", ada_mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,13 +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/305bb3706716d32d59b2ffa674731556caa1002b.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/e047d2a4d5af5783763f0f6a652fab8982a08603.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
|
.hash = "v8-0.0.0-xddH65gMBACRBQMM7EwmVgfi94FJyyX-0jpe5KhXYhfv",
|
||||||
|
},
|
||||||
|
// .v8 = .{ .path = "../zig-v8-fork" },
|
||||||
|
.@"ada-singleheader" = .{
|
||||||
|
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
|
||||||
|
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
|
||||||
|
},
|
||||||
|
.@"boringssl-zig" = .{
|
||||||
|
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
|
||||||
|
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
|
||||||
},
|
},
|
||||||
//.v8 = .{ .path = "../zig-v8-fork" }
|
|
||||||
},
|
},
|
||||||
|
.paths = .{""},
|
||||||
}
|
}
|
||||||
|
|||||||
12
flake.lock
generated
12
flake.lock
generated
@@ -75,11 +75,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1756822655,
|
"lastModified": 1760968520,
|
||||||
"narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
|
"narHash": "sha256-EjGslHDzCBKOVr+dnDB1CAD7wiQSHfUt3suOpFj9O1Q=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
|
"rev": "e755547441a0413942a37692f7bf7fc6315bb7f6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -136,11 +136,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1756555914,
|
"lastModified": 1760747435,
|
||||||
"narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=",
|
"narHash": "sha256-wNB/W3x+or4mdNxFPNOH5/WFckNpKgFRZk7OnOsLtm0=",
|
||||||
"owner": "mitchellh",
|
"owner": "mitchellh",
|
||||||
"repo": "zig-overlay",
|
"repo": "zig-overlay",
|
||||||
"rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6",
|
"rev": "d0f239b887b1ac736c0f3dde91bf5bf2ecf3a420",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
targetPkgs =
|
targetPkgs =
|
||||||
pkgs: with pkgs; [
|
pkgs: with pkgs; [
|
||||||
# Build Tools
|
# Build Tools
|
||||||
zigpkgs."0.15.1"
|
zigpkgs."0.15.2"
|
||||||
zls
|
zls
|
||||||
python3
|
python3
|
||||||
pkg-config
|
pkg-config
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
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,
|
||||||
|
};
|
||||||
@@ -562,7 +562,7 @@ pub const Selector = union(enum) {
|
|||||||
|
|
||||||
const ntag = try n.tag();
|
const ntag = try n.tag();
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase("intput", ntag)) {
|
if (std.ascii.eqlIgnoreCase("input", ntag)) {
|
||||||
const ntype = try n.attr("type");
|
const ntype = try n.attr("type");
|
||||||
if (ntype == null) return false;
|
if (ntype == null) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
@@ -313,9 +314,72 @@ pub const Document = struct {
|
|||||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||||
state.adopted_style_sheets = try sheets.persist();
|
state.adopted_style_sheets = try sheets.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn _hasFocus(_: *parser.Document) bool {
|
||||||
|
log.debug(.web_api, "not implemented", .{ .feature = "Document hasFocus" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ pub const Union = union(enum) {
|
|||||||
screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
|
screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
|
||||||
performance: *@import("performance.zig").Performance,
|
performance: *@import("performance.zig").Performance,
|
||||||
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
|
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
|
||||||
|
navigation: *@import("../navigation/Navigation.zig"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// EventTarget implementation
|
// EventTarget implementation
|
||||||
@@ -82,6 +83,11 @@ pub const EventTarget = struct {
|
|||||||
.media_query_list => {
|
.media_query_list => {
|
||||||
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||||
},
|
},
|
||||||
|
.navigation => {
|
||||||
|
const NavigationEventTarget = @import("../navigation/NavigationEventTarget.zig");
|
||||||
|
const base: *NavigationEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
|
||||||
|
return .{ .navigation = @fieldParentPtr("proto", base) };
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ const Opts = struct {
|
|||||||
|
|
||||||
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection
|
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection
|
||||||
// HTMLCollection is re implemented in zig here because libdom
|
// HTMLCollection is re implemented in zig here because libdom
|
||||||
// dom_html_collection expects a comparison function callback as arguement.
|
// dom_html_collection expects a comparison function callback as argument.
|
||||||
// But we wanted a dynamically comparison here, according to the match tagname.
|
// But we wanted a dynamically comparison here, according to the match tagname.
|
||||||
pub const HTMLCollection = struct {
|
pub const HTMLCollection = struct {
|
||||||
matcher: Matcher,
|
matcher: Matcher,
|
||||||
|
|||||||
@@ -360,25 +360,53 @@ pub const Node = struct {
|
|||||||
node: Union,
|
node: Union,
|
||||||
};
|
};
|
||||||
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
|
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
|
||||||
if (options) |options_| if (options_.composed) {
|
const composed = if (options) |opts| opts.composed else false;
|
||||||
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const root = parser.nodeGetRootNode(self);
|
var current_root = parser.nodeGetRootNode(self);
|
||||||
if (page.getNodeState(root)) |state| {
|
|
||||||
if (state.shadow_root) |sr| {
|
while (true) {
|
||||||
return .{ .shadow_root = sr };
|
const node_type = parser.nodeType(current_root);
|
||||||
|
|
||||||
|
if (node_type == .document_fragment) {
|
||||||
|
if (parser.documentFragmentGetHost(@ptrCast(current_root))) |host| {
|
||||||
|
if (page.getNodeState(host)) |state| {
|
||||||
|
if (state.shadow_root) |sr| {
|
||||||
|
if (!composed) {
|
||||||
|
return .{ .shadow_root = sr };
|
||||||
|
}
|
||||||
|
current_root = parser.nodeGetRootNode(@ptrCast(sr.host));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return .{ .node = try Node.toInterface(root) };
|
return .{ .node = try Node.toInterface(current_root) };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _hasChildNodes(self: *parser.Node) bool {
|
pub fn _hasChildNodes(self: *parser.Node) bool {
|
||||||
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 = .{};
|
||||||
|
|
||||||
@@ -461,7 +489,7 @@ pub const Node = struct {
|
|||||||
|
|
||||||
// Check if the hierarchy node tree constraints are respected.
|
// Check if the hierarchy node tree constraints are respected.
|
||||||
// For now, it checks only if new nodes are not self.
|
// For now, it checks only if new nodes are not self.
|
||||||
// TODO implements the others contraints.
|
// TODO implements the others constraints.
|
||||||
// see https://dom.spec.whatwg.org/#concept-node-tree
|
// see https://dom.spec.whatwg.org/#concept-node-tree
|
||||||
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
|
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
|
||||||
for (nodes) |n| {
|
for (nodes) |n| {
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ pub const NodeIterator = struct {
|
|||||||
defer self.callbackEnd();
|
defer self.callbackEnd();
|
||||||
|
|
||||||
if (self.pointer_before_current) {
|
if (self.pointer_before_current) {
|
||||||
self.pointer_before_current = false;
|
|
||||||
// Unlike TreeWalker, NodeIterator starts at the first node
|
// Unlike TreeWalker, NodeIterator starts at the first node
|
||||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
||||||
self.pointer_before_current = false;
|
self.pointer_before_current = false;
|
||||||
@@ -116,6 +115,7 @@ pub const NodeIterator = struct {
|
|||||||
|
|
||||||
if (try self.firstChild(self.reference_node)) |child| {
|
if (try self.firstChild(self.reference_node)) |child| {
|
||||||
self.reference_node = child;
|
self.reference_node = child;
|
||||||
|
self.pointer_before_current = false;
|
||||||
return try Node.toInterface(child);
|
return try Node.toInterface(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ test "Performance: now" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var after = perf._now();
|
var after = perf._now();
|
||||||
while (after <= now) { // Loop untill after > now
|
while (after <= now) { // Loop until after > now
|
||||||
try testing.expectEqual(after, now);
|
try testing.expectEqual(after, now);
|
||||||
after = perf._now();
|
after = perf._now();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ pub const Range = struct {
|
|||||||
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
|
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
|
||||||
try ensureValidOffset(node, offset_);
|
try ensureValidOffset(node, offset_);
|
||||||
const offset: u32 = @intCast(offset_);
|
const offset: u32 = @intCast(offset_);
|
||||||
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
|
const position = compare(node, offset, self.proto.end_node, self.proto.end_offset) catch |err| switch (err) {
|
||||||
error.WrongDocument => blk: {
|
error.WrongDocument => blk: {
|
||||||
// allow a node with a different root than the current, or
|
// allow a node with a different root than the current, or
|
||||||
// a disconnected one. Treat it as if it's "after", so that
|
// a disconnected one. Treat it as if it's "after", so that
|
||||||
@@ -103,7 +103,7 @@ pub const Range = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (position == 1) {
|
if (position == 1) {
|
||||||
// if we're setting the node after the current start, the end must
|
// if we're setting the node after the current end, the end must
|
||||||
// be set too.
|
// be set too.
|
||||||
self.proto.end_offset = offset;
|
self.proto.end_offset = offset;
|
||||||
self.proto.end_node = node;
|
self.proto.end_node = node;
|
||||||
@@ -378,7 +378,7 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
|
|||||||
|
|
||||||
const child_parent, const child_index = try getParentAndIndex(child);
|
const child_parent, const child_index = try getParentAndIndex(child);
|
||||||
std.debug.assert(node_a == child_parent);
|
std.debug.assert(node_a == child_parent);
|
||||||
return if (child_index < offset_a) -1 else 1;
|
return if (offset_a <= child_index) -1 else 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -227,7 +227,6 @@ pub const TreeWalker = struct {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (!result.should_descend) {
|
if (!result.should_descend) {
|
||||||
// This is an .accept node - return it
|
// This is an .accept node - return it
|
||||||
self.current_node = result.node;
|
self.current_node = result.node;
|
||||||
|
|||||||
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",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
57
src/browser/events/composition_event.zig
Normal file
57
src/browser/events/composition_event.zig
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const parser = @import("../netsurf.zig");
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
|
||||||
|
pub const CompositionEvent = struct {
|
||||||
|
data: []const u8,
|
||||||
|
proto: parser.Event,
|
||||||
|
|
||||||
|
pub const union_make_copy = true;
|
||||||
|
pub const prototype = *parser.Event;
|
||||||
|
|
||||||
|
pub const ConstructorOptions = struct {
|
||||||
|
data: []const u8 = "",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn constructor(event_type: []const u8, options_: ?ConstructorOptions) !CompositionEvent {
|
||||||
|
const options: ConstructorOptions = options_ orelse .{};
|
||||||
|
|
||||||
|
const event = try parser.eventCreate();
|
||||||
|
defer parser.eventDestroy(event);
|
||||||
|
try parser.eventInit(event, event_type, .{});
|
||||||
|
parser.eventSetInternalType(event, .composition_event);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.proto = event.*,
|
||||||
|
.data = options.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_data(self: *const CompositionEvent) []const u8 {
|
||||||
|
return self.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "Browser: Events.Composition" {
|
||||||
|
try testing.htmlRunner("events/composition.html");
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
@@ -37,6 +38,9 @@ const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
|
|||||||
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
||||||
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
||||||
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
|
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
|
||||||
|
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
|
||||||
|
const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent;
|
||||||
|
const PageTransitionEvent = @import("../events/PageTransitionEvent.zig");
|
||||||
|
|
||||||
// Event interfaces
|
// Event interfaces
|
||||||
pub const Interfaces = .{
|
pub const Interfaces = .{
|
||||||
@@ -48,6 +52,9 @@ pub const Interfaces = .{
|
|||||||
ErrorEvent,
|
ErrorEvent,
|
||||||
MessageEvent,
|
MessageEvent,
|
||||||
PopStateEvent,
|
PopStateEvent,
|
||||||
|
CompositionEvent,
|
||||||
|
NavigationCurrentEntryChangeEvent,
|
||||||
|
PageTransitionEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Union = generate.Union(Interfaces);
|
pub const Union = generate.Union(Interfaces);
|
||||||
@@ -72,10 +79,15 @@ pub const Event = struct {
|
|||||||
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
|
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
|
||||||
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
|
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
|
||||||
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
|
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
|
||||||
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
|
.error_event => .{ .ErrorEvent = (@as(*ErrorEvent, @fieldParentPtr("proto", evt))).* },
|
||||||
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
|
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
|
||||||
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
|
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
|
||||||
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
|
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
|
||||||
|
.composition_event => .{ .CompositionEvent = (@as(*CompositionEvent, @fieldParentPtr("proto", evt))).* },
|
||||||
|
.navigation_current_entry_change_event => .{
|
||||||
|
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
|
||||||
|
},
|
||||||
|
.page_transition_event => .{ .PageTransitionEvent = @as(*PageTransitionEvent, @ptrCast(evt)).* },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,8 +235,6 @@ pub const EventHandler = struct {
|
|||||||
node: parser.EventNode,
|
node: parser.EventNode,
|
||||||
listener: *parser.EventListener,
|
listener: *parser.EventListener,
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
|
||||||
|
|
||||||
pub const Listener = union(enum) {
|
pub const Listener = union(enum) {
|
||||||
function: js.Function,
|
function: js.Function,
|
||||||
object: js.Object,
|
object: js.Object,
|
||||||
@@ -396,6 +406,40 @@ const SignalCallback = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn DirectEventHandler(
|
||||||
|
comptime TargetT: type,
|
||||||
|
target: *TargetT,
|
||||||
|
event_type: []const u8,
|
||||||
|
maybe_listener: ?EventHandler.Listener,
|
||||||
|
cb: *?js.Function,
|
||||||
|
page_arena: std.mem.Allocator,
|
||||||
|
) !void {
|
||||||
|
const event_target = parser.toEventTarget(TargetT, target);
|
||||||
|
|
||||||
|
// Check if we have a listener set.
|
||||||
|
if (cb.*) |callback| {
|
||||||
|
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
|
||||||
|
std.debug.assert(listener != null);
|
||||||
|
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maybe_listener) |listener| {
|
||||||
|
switch (listener) {
|
||||||
|
// If an object is given as listener, do nothing.
|
||||||
|
.object => {},
|
||||||
|
.function => |callback| {
|
||||||
|
_ = try EventHandler.register(page_arena, event_target, event_type, listener, null) orelse unreachable;
|
||||||
|
cb.* = callback;
|
||||||
|
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just unset the listener.
|
||||||
|
cb.* = null;
|
||||||
|
}
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser: Event" {
|
test "Browser: Event" {
|
||||||
try testing.htmlRunner("events/event.html");
|
try testing.htmlRunner("events/event.html");
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
284
src/browser/file/Blob.zig
Normal file
284
src/browser/file/Blob.zig
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
// 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 Writer = std.Io.Writer;
|
||||||
|
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
|
||||||
|
const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||||
|
|
||||||
|
/// https://w3c.github.io/FileAPI/#blob-section
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Web/API/Blob
|
||||||
|
const Blob = @This();
|
||||||
|
|
||||||
|
/// Immutable slice of blob.
|
||||||
|
/// Note that another blob may hold a pointer/slice to this,
|
||||||
|
/// so its better to leave the deallocation of it to arena allocator.
|
||||||
|
slice: []const u8,
|
||||||
|
/// MIME attached to blob. Can be an empty string.
|
||||||
|
mime: []const u8,
|
||||||
|
|
||||||
|
const ConstructorOptions = struct {
|
||||||
|
/// MIME type.
|
||||||
|
type: []const u8 = "",
|
||||||
|
/// How to handle line endings (CR and LF).
|
||||||
|
/// `transparent` means do nothing, `native` expects CRLF (\r\n) on Windows.
|
||||||
|
endings: []const u8 = "transparent",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Creates a new Blob.
|
||||||
|
pub fn constructor(
|
||||||
|
maybe_blob_parts: ?[]const []const u8,
|
||||||
|
maybe_options: ?ConstructorOptions,
|
||||||
|
page: *Page,
|
||||||
|
) !Blob {
|
||||||
|
const options: ConstructorOptions = maybe_options orelse .{};
|
||||||
|
// Setup MIME; This can be any string according to my observations.
|
||||||
|
const mime: []const u8 = blk: {
|
||||||
|
const t = options.type;
|
||||||
|
if (t.len == 0) {
|
||||||
|
break :blk "";
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk try page.arena.dupe(u8, t);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (maybe_blob_parts) |blob_parts| {
|
||||||
|
var w: Writer.Allocating = .init(page.arena);
|
||||||
|
const use_native_endings = std.mem.eql(u8, options.endings, "native");
|
||||||
|
try writeBlobParts(&w.writer, blob_parts, use_native_endings);
|
||||||
|
|
||||||
|
return .{ .slice = w.written(), .mime = mime };
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have `blob_parts`, why would you want a Blob anyway then?
|
||||||
|
return .{ .slice = "", .mime = mime };
|
||||||
|
}
|
||||||
|
|
||||||
|
const largest_vector = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
|
||||||
|
/// Array of possible vector sizes for the current arch in decrementing order.
|
||||||
|
/// We may move this to some file for SIMD helpers in the future.
|
||||||
|
const vector_sizes = blk: {
|
||||||
|
// Required for length calculation.
|
||||||
|
var n: usize = largest_vector;
|
||||||
|
var total: usize = 0;
|
||||||
|
while (n != 2) : (n /= 2) total += 1;
|
||||||
|
// Populate an array with vector sizes.
|
||||||
|
n = largest_vector;
|
||||||
|
var i: usize = 0;
|
||||||
|
var items: [total]usize = undefined;
|
||||||
|
while (n != 2) : (n /= 2) {
|
||||||
|
defer i += 1;
|
||||||
|
items[i] = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk items;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Writes blob parts to given `Writer` with desired endings.
|
||||||
|
fn writeBlobParts(
|
||||||
|
writer: *Writer,
|
||||||
|
blob_parts: []const []const u8,
|
||||||
|
use_native_endings: bool,
|
||||||
|
) !void {
|
||||||
|
// Transparent.
|
||||||
|
if (!use_native_endings) {
|
||||||
|
for (blob_parts) |part| {
|
||||||
|
try writer.writeAll(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Windows support.
|
||||||
|
|
||||||
|
// Linux & Unix.
|
||||||
|
// Both Firefox and Chrome implement it as such:
|
||||||
|
// CRLF => LF
|
||||||
|
// CR => LF
|
||||||
|
// So even though CR is not followed by LF, it gets replaced.
|
||||||
|
//
|
||||||
|
// I believe this is because such scenario is possible:
|
||||||
|
// ```
|
||||||
|
// let parts = [ "the quick\r", "\nbrown fox" ];
|
||||||
|
// ```
|
||||||
|
// In the example, one should have to check the part before in order to
|
||||||
|
// understand that CRLF is being presented in the final buffer.
|
||||||
|
// So they took a simpler approach, here's what given blob parts produce:
|
||||||
|
// ```
|
||||||
|
// "the quick\n\nbrown fox"
|
||||||
|
// ```
|
||||||
|
scan_parts: for (blob_parts) |part| {
|
||||||
|
var end: usize = 0;
|
||||||
|
|
||||||
|
inline for (vector_sizes) |vector_len| {
|
||||||
|
const Vec = @Vector(vector_len, u8);
|
||||||
|
|
||||||
|
while (end + vector_len <= part.len) : (end += vector_len) {
|
||||||
|
const cr: Vec = @splat('\r');
|
||||||
|
// Load chunk as vectors.
|
||||||
|
const slice = part[end..][0..vector_len];
|
||||||
|
const chunk: Vec = slice.*;
|
||||||
|
// Look for CR.
|
||||||
|
const match = chunk == cr;
|
||||||
|
|
||||||
|
// Create a bitset out of match vector.
|
||||||
|
const bitset = std.bit_set.IntegerBitSet(vector_len){
|
||||||
|
.mask = @bitCast(@intFromBool(match)),
|
||||||
|
};
|
||||||
|
|
||||||
|
var iter = bitset.iterator(.{});
|
||||||
|
var relative_start: usize = 0;
|
||||||
|
while (iter.next()) |index| {
|
||||||
|
_ = try writer.writeVec(&.{ slice[relative_start..index], "\n" });
|
||||||
|
|
||||||
|
if (index + 1 != slice.len and slice[index + 1] == '\n') {
|
||||||
|
relative_start = index + 2;
|
||||||
|
} else {
|
||||||
|
relative_start = index + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = try writer.writeVec(&.{slice[relative_start..]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar scan fallback.
|
||||||
|
var relative_start: usize = end;
|
||||||
|
while (end < part.len) {
|
||||||
|
if (part[end] == '\r') {
|
||||||
|
_ = try writer.writeVec(&.{ part[relative_start..end], "\n" });
|
||||||
|
|
||||||
|
// Part ends with CR. We can continue to next part.
|
||||||
|
if (end + 1 == part.len) {
|
||||||
|
continue :scan_parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If next char is LF, skip it too.
|
||||||
|
if (part[end + 1] == '\n') {
|
||||||
|
relative_start = end + 2;
|
||||||
|
} else {
|
||||||
|
relative_start = end + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
end += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the remaining. We get this in such situations:
|
||||||
|
// `the quick brown\rfox`
|
||||||
|
// `the quick brown\r\nfox`
|
||||||
|
try writer.writeAll(part[relative_start..end]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a Promise that resolves with the contents of the blob
|
||||||
|
/// as binary data contained in an ArrayBuffer.
|
||||||
|
pub fn _arrayBuffer(self: *const Blob, page: *Page) !js.Promise {
|
||||||
|
return page.js.resolvePromise(js.ArrayBuffer{ .values = self.slice });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a ReadableStream which upon reading returns the data
|
||||||
|
/// contained within the Blob.
|
||||||
|
pub fn _stream(self: *const Blob, page: *Page) !*ReadableStream {
|
||||||
|
const stream = try ReadableStream.constructor(null, null, page);
|
||||||
|
try stream.queue.append(page.arena, .{
|
||||||
|
.uint8array = .{ .values = self.slice },
|
||||||
|
});
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a Promise that resolves with a string containing
|
||||||
|
/// the contents of the blob, interpreted as UTF-8.
|
||||||
|
pub fn _text(self: *const Blob, page: *Page) !js.Promise {
|
||||||
|
return page.js.resolvePromise(self.slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension to Blob; works on Firefox and Safari.
|
||||||
|
/// https://developer.mozilla.org/en-US/docs/Web/API/Blob/bytes
|
||||||
|
/// Returns a Promise that resolves with a Uint8Array containing
|
||||||
|
/// the contents of the blob as an array of bytes.
|
||||||
|
pub fn _bytes(self: *const Blob, page: *Page) !js.Promise {
|
||||||
|
return page.js.resolvePromise(js.TypedArray(u8){ .values = self.slice });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new Blob object which contains data
|
||||||
|
/// from a subset of the blob on which it's called.
|
||||||
|
pub fn _slice(
|
||||||
|
self: *const Blob,
|
||||||
|
maybe_start: ?i32,
|
||||||
|
maybe_end: ?i32,
|
||||||
|
maybe_content_type: ?[]const u8,
|
||||||
|
page: *Page,
|
||||||
|
) !Blob {
|
||||||
|
const mime: []const u8 = blk: {
|
||||||
|
if (maybe_content_type) |content_type| {
|
||||||
|
if (content_type.len == 0) {
|
||||||
|
break :blk "";
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk try page.arena.dupe(u8, content_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const slice = self.slice;
|
||||||
|
if (maybe_start) |_start| {
|
||||||
|
const start = blk: {
|
||||||
|
if (_start < 0) {
|
||||||
|
break :blk slice.len -| @abs(_start);
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk @min(slice.len, @as(u31, @intCast(_start)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const end: usize = blk: {
|
||||||
|
if (maybe_end) |_end| {
|
||||||
|
if (_end < 0) {
|
||||||
|
break :blk @max(start, slice.len -| @abs(_end));
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk @min(slice.len, @max(start, @as(u31, @intCast(_end))));
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk slice.len;
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{ .slice = slice[start..end], .mime = mime };
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ .slice = slice, .mime = mime };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the size of the Blob in bytes.
|
||||||
|
pub fn get_size(self: *const Blob) usize {
|
||||||
|
return self.slice.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the type of Blob; likely a MIME type, yet anything can be given.
|
||||||
|
pub fn get_type(self: *const Blob) []const u8 {
|
||||||
|
return self.mime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "Browser: File.Blob" {
|
||||||
|
try testing.htmlRunner("file/blob.html");
|
||||||
|
}
|
||||||
@@ -21,14 +21,12 @@ const std = @import("std");
|
|||||||
// https://w3c.github.io/FileAPI/#file-section
|
// https://w3c.github.io/FileAPI/#file-section
|
||||||
const File = @This();
|
const File = @This();
|
||||||
|
|
||||||
// Very incomplete. The prototype for this is Blob, which we don't have.
|
/// TODO: Implement File API.
|
||||||
// This minimum "implementation" is added because some JavaScript code just
|
|
||||||
// checks: if (x instanceof File) throw Error(...)
|
|
||||||
pub fn constructor() File {
|
pub fn constructor() File {
|
||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser: File" {
|
test "Browser: File.File" {
|
||||||
try testing.htmlRunner("xhr/file.html");
|
try testing.htmlRunner("file/file.html");
|
||||||
}
|
}
|
||||||
7
src/browser/file/root.zig
Normal file
7
src/browser/file/root.zig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//! File API.
|
||||||
|
//! https://developer.mozilla.org/en-US/docs/Web/API/File_API
|
||||||
|
|
||||||
|
pub const Interfaces = .{
|
||||||
|
@import("./Blob.zig"),
|
||||||
|
@import("./File.zig"),
|
||||||
|
};
|
||||||
@@ -21,140 +21,76 @@ const log = @import("../../log.zig");
|
|||||||
|
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
|
const Window = @import("window.zig").Window;
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
|
||||||
const History = @This();
|
const History = @This();
|
||||||
|
|
||||||
const HistoryEntry = struct {
|
|
||||||
url: []const u8,
|
|
||||||
// This is serialized as JSON because
|
|
||||||
// History must survive a JsContext.
|
|
||||||
state: ?[]u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ScrollRestorationMode = enum {
|
const ScrollRestorationMode = enum {
|
||||||
|
pub const ENUM_JS_USE_TAG = true;
|
||||||
|
|
||||||
auto,
|
auto,
|
||||||
manual,
|
manual,
|
||||||
|
|
||||||
pub fn fromString(str: []const u8) ?ScrollRestorationMode {
|
|
||||||
for (std.enums.values(ScrollRestorationMode)) |mode| {
|
|
||||||
if (std.ascii.eqlIgnoreCase(str, @tagName(mode))) {
|
|
||||||
return mode;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toString(self: ScrollRestorationMode) []const u8 {
|
|
||||||
return @tagName(self);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
scroll_restoration: ScrollRestorationMode = .auto,
|
scroll_restoration: ScrollRestorationMode = .auto,
|
||||||
stack: std.ArrayListUnmanaged(HistoryEntry) = .empty,
|
|
||||||
current: ?usize = null,
|
|
||||||
|
|
||||||
pub fn get_length(self: *History) u32 {
|
pub fn get_length(_: *History, page: *Page) u32 {
|
||||||
return @intCast(self.stack.items.len);
|
return @intCast(page.session.navigation.entries.items.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
|
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
|
||||||
return self.scroll_restoration;
|
return self.scroll_restoration;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
|
pub fn set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
|
||||||
self.scroll_restoration = ScrollRestorationMode.fromString(mode) orelse self.scroll_restoration;
|
self.scroll_restoration = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_state(self: *History, page: *Page) !?js.Value {
|
pub fn get_state(_: *History, page: *Page) !?js.Value {
|
||||||
if (self.current) |curr| {
|
if (page.session.navigation.currentEntry().state.value) |state| {
|
||||||
const entry = self.stack.items[curr];
|
const value = try js.Value.fromJson(page.js, state);
|
||||||
if (entry.state) |state| {
|
return value;
|
||||||
const value = try js.Value.fromJson(page.js, state);
|
|
||||||
return value;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pushNavigation(self: *History, _url: []const u8, page: *Page) !void {
|
pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
||||||
const arena = page.session.arena;
|
const arena = page.session.arena;
|
||||||
const url = try arena.dupe(u8, _url);
|
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
||||||
|
|
||||||
const entry = HistoryEntry{ .state = null, .url = url };
|
const json = state.toJson(arena) catch return error.DataClone;
|
||||||
try self.stack.append(arena, entry);
|
_ = try page.session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);
|
||||||
self.current = self.stack.items.len - 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatchPopStateEvent(state: ?[]const u8, page: *Page) void {
|
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
||||||
log.debug(.script_event, "dispatch popstate event", .{
|
|
||||||
.type = "popstate",
|
|
||||||
.source = "history",
|
|
||||||
});
|
|
||||||
History._dispatchPopStateEvent(state, page) catch |err| {
|
|
||||||
log.err(.app, "dispatch popstate event error", .{
|
|
||||||
.err = err,
|
|
||||||
.type = "popstate",
|
|
||||||
.source = "history",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _dispatchPopStateEvent(state: ?[]const u8, page: *Page) !void {
|
|
||||||
var evt = try PopStateEvent.constructor("popstate", .{ .state = state });
|
|
||||||
|
|
||||||
_ = try parser.eventTargetDispatchEvent(
|
|
||||||
@as(*parser.EventTarget, @ptrCast(&page.window)),
|
|
||||||
&evt.proto,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _pushState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
|
||||||
const arena = page.session.arena;
|
const arena = page.session.arena;
|
||||||
|
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
||||||
|
|
||||||
const json = try state.toJson(arena);
|
const json = try state.toJson(arena);
|
||||||
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
_ = try page.session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true);
|
||||||
const entry = HistoryEntry{ .state = json, .url = url };
|
|
||||||
try self.stack.append(arena, entry);
|
|
||||||
self.current = self.stack.items.len - 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _replaceState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
|
pub fn go(_: *const History, delta: i32, page: *Page) !void {
|
||||||
const arena = page.session.arena;
|
|
||||||
|
|
||||||
if (self.current) |curr| {
|
|
||||||
const entry = &self.stack.items[curr];
|
|
||||||
const json = try state.toJson(arena);
|
|
||||||
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
|
|
||||||
entry.* = HistoryEntry{ .state = json, .url = url };
|
|
||||||
} else {
|
|
||||||
try self._pushState(state, "", _url, page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn go(self: *History, delta: i32, page: *Page) !void {
|
|
||||||
// 0 behaves the same as no argument, both reloading the page.
|
// 0 behaves the same as no argument, both reloading the page.
|
||||||
// If this is getting called, there SHOULD be an entry, atleast from pushNavigation.
|
|
||||||
const current = self.current.?;
|
|
||||||
|
|
||||||
|
const current = page.session.navigation.index;
|
||||||
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
|
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
|
||||||
if (index_s < 0 or index_s > self.stack.items.len - 1) {
|
if (index_s < 0 or index_s > page.session.navigation.entries.items.len - 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = @as(usize, @intCast(index_s));
|
const index = @as(usize, @intCast(index_s));
|
||||||
const entry = self.stack.items[index];
|
const entry = page.session.navigation.entries.items[index];
|
||||||
self.current = index;
|
|
||||||
|
|
||||||
if (try page.isSameOrigin(entry.url)) {
|
if (entry.url) |url| {
|
||||||
History.dispatchPopStateEvent(entry.state, page);
|
if (try page.isSameOrigin(url)) {
|
||||||
|
PopStateEvent.dispatch(entry.state.value, page);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try page.navigateFromWebAPI(entry.url, .{ .reason = .history });
|
_ = try page.session.navigation.navigate(entry.url, .{ .traverse = index }, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
|
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
|
||||||
@@ -207,9 +143,38 @@ pub const PopStateEvent = struct {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dispatch(state: ?[]const u8, page: *Page) void {
|
||||||
|
log.debug(.script_event, "dispatch popstate event", .{
|
||||||
|
.type = "popstate",
|
||||||
|
.source = "history",
|
||||||
|
});
|
||||||
|
|
||||||
|
var evt = PopStateEvent.constructor("popstate", .{ .state = state }) catch |err| {
|
||||||
|
log.err(.app, "event constructor error", .{
|
||||||
|
.err = err,
|
||||||
|
.type = "popstate",
|
||||||
|
.source = "history",
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = parser.eventTargetDispatchEvent(
|
||||||
|
parser.toEventTarget(Window, &page.window),
|
||||||
|
&evt.proto,
|
||||||
|
) catch |err| {
|
||||||
|
log.err(.app, "dispatch popstate event error", .{
|
||||||
|
.err = err,
|
||||||
|
.type = "popstate",
|
||||||
|
.source = "history",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser: HTML.History" {
|
test "Browser: HTML.History" {
|
||||||
try testing.htmlRunner("html/history.html");
|
try testing.htmlRunner("html/history/history.html");
|
||||||
|
try testing.htmlRunner("html/history/history2.html");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,12 +42,12 @@ pub const HTMLDocument = struct {
|
|||||||
// JS funcs
|
// JS funcs
|
||||||
// --------
|
// --------
|
||||||
|
|
||||||
pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 {
|
||||||
// libdom's document_html get_domain always returns null, this is
|
// libdom's document_html get_domain always returns null, this is
|
||||||
// the way MDN recommends getting the domain anyways, since document.domain
|
// the way MDN recommends getting the domain anyways, since document.domain
|
||||||
// is deprecated.
|
// is deprecated.
|
||||||
const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
|
const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
|
||||||
return location.get_host(page);
|
return location.get_host();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
|
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
|
||||||
@@ -195,7 +195,7 @@ pub const HTMLDocument = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
|
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
|
||||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
|
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
|
||||||
|
|||||||
@@ -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 = .{
|
||||||
@@ -218,36 +222,36 @@ pub const HTMLAnchorElement = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_href(self: *parser.Anchor) ![]const u8 {
|
pub fn get_href(self: *parser.Anchor) ![]const u8 {
|
||||||
return try parser.anchorGetHref(self);
|
return parser.anchorGetHref(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void {
|
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void {
|
||||||
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
|
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
|
||||||
return try parser.anchorSetHref(self, full);
|
return parser.anchorSetHref(self, full);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
|
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
|
||||||
return try parser.anchorGetHrefLang(self);
|
return parser.anchorGetHrefLang(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
|
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
|
||||||
return try parser.anchorSetHrefLang(self, href);
|
return parser.anchorSetHrefLang(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_type(self: *parser.Anchor) ![]const u8 {
|
pub fn get_type(self: *parser.Anchor) ![]const u8 {
|
||||||
return try parser.anchorGetType(self);
|
return parser.anchorGetType(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
|
pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
|
||||||
return try parser.anchorSetType(self, t);
|
return parser.anchorSetType(self, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rel(self: *parser.Anchor) ![]const u8 {
|
pub fn get_rel(self: *parser.Anchor) ![]const u8 {
|
||||||
return try parser.anchorGetRel(self);
|
return parser.anchorGetRel(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
|
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
|
||||||
return try parser.anchorSetRel(self, t);
|
return parser.anchorSetRel(self, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_text(self: *parser.Anchor) !?[]const u8 {
|
pub fn get_text(self: *parser.Anchor) !?[]const u8 {
|
||||||
@@ -269,182 +273,175 @@ pub const HTMLAnchorElement = struct {
|
|||||||
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
|
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
|
||||||
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
|
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
|
||||||
}
|
}
|
||||||
return .empty;
|
return error.NotProvided;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return try u.get_origin(page);
|
defer u.destructor();
|
||||||
|
return u.get_origin(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return u.get_protocol();
|
defer u.destructor();
|
||||||
|
|
||||||
|
return page.call_arena.dupe(u8, u.get_protocol());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
pub fn set_protocol(self: *parser.Anchor, protocol: []const u8, page: *Page) !void {
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
|
try u.set_protocol(protocol);
|
||||||
|
|
||||||
u.uri.scheme = v;
|
const href = try u._toString(page);
|
||||||
const href = try u.toString(arena);
|
return parser.anchorSetHref(self, href);
|
||||||
try parser.anchorSetHref(self, href);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return try u.get_host(page);
|
defer u.destructor();
|
||||||
|
|
||||||
|
return page.call_arena.dupe(u8, u.get_host());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
pub fn set_host(self: *parser.Anchor, host: []const u8, page: *Page) !void {
|
||||||
// search : separator
|
|
||||||
var p: ?u16 = null;
|
|
||||||
var h: []const u8 = undefined;
|
|
||||||
for (v, 0..) |c, i| {
|
|
||||||
if (c == ':') {
|
|
||||||
h = v[0..i];
|
|
||||||
p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
|
try u.set_host(host);
|
||||||
|
|
||||||
if (p) |pp| {
|
const href = try u._toString(page);
|
||||||
u.uri.host = .{ .raw = h };
|
return parser.anchorSetHref(self, href);
|
||||||
u.uri.port = pp;
|
|
||||||
} else {
|
|
||||||
u.uri.host = .{ .raw = v };
|
|
||||||
u.uri.port = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const href = try u.toString(arena);
|
|
||||||
try parser.anchorSetHref(self, href);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return u.get_hostname();
|
defer u.destructor();
|
||||||
|
return page.call_arena.dupe(u8, u.get_hostname());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
u.uri.host = .{ .raw = v };
|
defer u.destructor();
|
||||||
const href = try u.toString(arena);
|
try u.set_hostname(hostname);
|
||||||
try parser.anchorSetHref(self, href);
|
|
||||||
|
const href = try u._toString(page);
|
||||||
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return try u.get_port(page);
|
defer u.destructor();
|
||||||
|
return page.call_arena.dupe(u8, u.get_port());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void {
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
|
|
||||||
if (v != null and v.?.len > 0) {
|
if (maybe_port) |port| {
|
||||||
u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
|
try u.set_port(port);
|
||||||
} else {
|
} else {
|
||||||
u.uri.port = null;
|
u.clearPort();
|
||||||
}
|
}
|
||||||
|
|
||||||
const href = try u.toString(arena);
|
const href = try u._toString(page);
|
||||||
try parser.anchorSetHref(self, href);
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return u.get_username();
|
defer u.destructor();
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
const username = u.get_username();
|
||||||
const arena = page.arena;
|
if (username.len == 0) {
|
||||||
var u = try url(self, page);
|
return "";
|
||||||
|
|
||||||
if (v) |vv| {
|
|
||||||
u.uri.user = .{ .raw = vv };
|
|
||||||
} else {
|
|
||||||
u.uri.user = null;
|
|
||||||
}
|
}
|
||||||
const href = try u.toString(arena);
|
|
||||||
|
|
||||||
try parser.anchorSetHref(self, href);
|
return page.call_arena.dupe(u8, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
|
||||||
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
|
|
||||||
|
const username = if (maybe_username) |username| username else "";
|
||||||
|
try u.set_username(username);
|
||||||
|
|
||||||
|
const href = try u._toString(page);
|
||||||
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return try page.arena.dupe(u8, u.get_password());
|
defer u.destructor();
|
||||||
|
|
||||||
|
return page.call_arena.dupe(u8, u.get_password());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
|
|
||||||
if (v) |vv| {
|
const password = if (maybe_password) |password| password else "";
|
||||||
u.uri.password = .{ .raw = vv };
|
try u.set_password(password);
|
||||||
} else {
|
|
||||||
u.uri.password = null;
|
|
||||||
}
|
|
||||||
const href = try u.toString(arena);
|
|
||||||
|
|
||||||
try parser.anchorSetHref(self, href);
|
const href = try u._toString(page);
|
||||||
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return u.get_pathname();
|
defer u.destructor();
|
||||||
|
|
||||||
|
return page.call_arena.dupe(u8, u.get_pathname());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
|
pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void {
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
u.uri.path = .{ .raw = v };
|
defer u.destructor();
|
||||||
const href = try u.toString(arena);
|
|
||||||
|
|
||||||
try parser.anchorSetHref(self, href);
|
try u.set_pathname(pathname);
|
||||||
|
|
||||||
|
const href = try u._toString(page);
|
||||||
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return try u.get_search(page);
|
defer u.destructor();
|
||||||
|
// This allocates in page arena so no need to dupe.
|
||||||
|
return u.get_search(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
try u.set_search(v, page);
|
try u.set_search(v, page);
|
||||||
|
|
||||||
const href = try u.toString(page.call_arena);
|
const href = try u._toString(page);
|
||||||
try parser.anchorSetHref(self, href);
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO return a disposable string
|
|
||||||
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
|
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
|
||||||
var u = try url(self, page);
|
var u = url(self, page) catch return "";
|
||||||
return try u.get_hash(page);
|
defer u.destructor();
|
||||||
|
|
||||||
|
return page.call_arena.dupe(u8, u.get_hash());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
|
pub fn set_hash(self: *parser.Anchor, maybe_hash: ?[]const u8, page: *Page) !void {
|
||||||
const arena = page.arena;
|
|
||||||
var u = try url(self, page);
|
var u = try url(self, page);
|
||||||
|
defer u.destructor();
|
||||||
|
|
||||||
if (v) |vv| {
|
if (maybe_hash) |hash| {
|
||||||
u.uri.fragment = .{ .raw = vv };
|
try u.set_hash(hash);
|
||||||
} else {
|
} else {
|
||||||
u.uri.fragment = null;
|
u.clearHash();
|
||||||
}
|
}
|
||||||
const href = try u.toString(arena);
|
|
||||||
|
|
||||||
try parser.anchorSetHref(self, href);
|
const href = try u._toString(page);
|
||||||
|
return parser.anchorSetHref(self, href);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -494,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 {
|
||||||
@@ -732,6 +752,9 @@ pub const HTMLInputElement = struct {
|
|||||||
pub fn set_value(self: *parser.Input, value: []const u8) !void {
|
pub fn set_value(self: *parser.Input, value: []const u8) !void {
|
||||||
try parser.inputSetValue(self, value);
|
try parser.inputSetValue(self, value);
|
||||||
}
|
}
|
||||||
|
pub fn _select(_: *parser.Input) void {
|
||||||
|
log.debug(.web_api, "not implemented", .{ .feature = "HTMLInputElement select" });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const HTMLLIElement = struct {
|
pub const HTMLLIElement = struct {
|
||||||
@@ -1204,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;
|
||||||
}
|
}
|
||||||
@@ -1354,8 +1388,13 @@ test "Browser: HTML.HtmlScriptElement" {
|
|||||||
try testing.htmlRunner("html/script/import.html");
|
try testing.htmlRunner("html/script/import.html");
|
||||||
try testing.htmlRunner("html/script/dynamic_import.html");
|
try testing.htmlRunner("html/script/dynamic_import.html");
|
||||||
try testing.htmlRunner("html/script/importmap.html");
|
try testing.htmlRunner("html/script/importmap.html");
|
||||||
|
try testing.htmlRunner("html/script/order.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Browser: HTML.HtmlSlotElement" {
|
test "Browser: HTML.HtmlSlotElement" {
|
||||||
try testing.htmlRunner("html/slot.html");
|
try testing.htmlRunner("html/slot.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Browser: HTML.HTMLCanvasElement" {
|
||||||
|
try testing.htmlRunner("html/canvas.html");
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ pub const ErrorEvent = struct {
|
|||||||
const event = try parser.eventCreate();
|
const event = try parser.eventCreate();
|
||||||
defer parser.eventDestroy(event);
|
defer parser.eventDestroy(event);
|
||||||
try parser.eventInit(event, event_type, .{});
|
try parser.eventInit(event, event_type, .{});
|
||||||
parser.eventSetInternalType(event, .event);
|
parser.eventSetInternalType(event, .error_event);
|
||||||
|
|
||||||
const o = opts orelse ErrorEventInit{};
|
const o = opts orelse ErrorEventInit{};
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,7 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const Uri = @import("std").Uri;
|
const std = @import("std");
|
||||||
|
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const URL = @import("../url/url.zig").URL;
|
const URL = @import("../url/url.zig").URL;
|
||||||
|
|
||||||
@@ -25,36 +24,55 @@ const URL = @import("../url/url.zig").URL;
|
|||||||
pub const Location = struct {
|
pub const Location = struct {
|
||||||
url: URL,
|
url: URL,
|
||||||
|
|
||||||
|
/// Initializes the `Location` to be used in `Window`.
|
||||||
/// Browsers give such initial values when user not navigated yet:
|
/// Browsers give such initial values when user not navigated yet:
|
||||||
/// Chrome -> chrome://new-tab-page/
|
/// Chrome -> chrome://new-tab-page/
|
||||||
/// Firefox -> about:newtab
|
/// Firefox -> about:newtab
|
||||||
/// Safari -> favorites://
|
/// Safari -> favorites://
|
||||||
pub const default = Location{
|
pub fn init(url: []const u8) !Location {
|
||||||
.url = .initWithoutSearchParams(Uri.parse("about:blank") catch unreachable),
|
return .{ .url = try .initForLocation(url) };
|
||||||
};
|
}
|
||||||
|
|
||||||
pub fn get_href(self: *Location, page: *Page) ![]const u8 {
|
pub fn get_href(self: *Location, page: *Page) ![]const u8 {
|
||||||
return self.url.get_href(page);
|
return self.url.get_href(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_href(_: *const Location, href: []const u8, page: *Page) !void {
|
pub fn set_href(_: *const Location, href: []const u8, page: *Page) !void {
|
||||||
return page.navigateFromWebAPI(href, .{ .reason = .script });
|
return page.navigateFromWebAPI(href, .{ .reason = .script }, .{ .push = null });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_hash(_: *const Location, hash: []const u8, page: *Page) !void {
|
||||||
|
const normalized_hash = blk: {
|
||||||
|
if (hash.len == 0) {
|
||||||
|
const old_url = page.url.raw;
|
||||||
|
|
||||||
|
break :blk if (std.mem.indexOfScalar(u8, old_url, '#')) |index|
|
||||||
|
old_url[0..index]
|
||||||
|
else
|
||||||
|
old_url;
|
||||||
|
} else if (hash[0] == '#')
|
||||||
|
break :blk hash
|
||||||
|
else
|
||||||
|
break :blk try std.fmt.allocPrint(page.arena, "#{s}", .{hash});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
return self.url.get_protocol();
|
return self.url.get_protocol();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_host(self: *Location, page: *Page) ![]const u8 {
|
pub fn get_host(self: *Location) []const u8 {
|
||||||
return self.url.get_host(page);
|
return self.url.get_host();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hostname(self: *Location) []const u8 {
|
pub fn get_hostname(self: *Location) []const u8 {
|
||||||
return self.url.get_hostname();
|
return self.url.get_hostname();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_port(self: *Location, page: *Page) ![]const u8 {
|
pub fn get_port(self: *Location) []const u8 {
|
||||||
return self.url.get_port(page);
|
return self.url.get_port();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_pathname(self: *Location) []const u8 {
|
pub fn get_pathname(self: *Location) []const u8 {
|
||||||
@@ -65,8 +83,8 @@ pub const Location = struct {
|
|||||||
return self.url.get_search(page);
|
return self.url.get_search(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
|
pub fn get_hash(self: *Location) []const u8 {
|
||||||
return self.url.get_hash(page);
|
return self.url.get_hash();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
|
pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
|
||||||
@@ -74,19 +92,19 @@ pub const Location = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
|
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
|
||||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
|
||||||
}
|
}
|
||||||
|
|
||||||
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 });
|
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .replace = null });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _reload(_: *const Location, page: *Page) !void {
|
pub fn _reload(_: *const Location, page: *Page) !void {
|
||||||
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script });
|
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script }, .reload);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
|
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
|
||||||
return try self.get_href(page);
|
return self.get_href(page);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub const SVGElement = struct {
|
|||||||
// Currently the prototype chain is not implemented (will not be returned by toInterface())
|
// Currently the prototype chain is not implemented (will not be returned by toInterface())
|
||||||
// For that we need parser.SvgElement and the derived types with tags in the v-table.
|
// For that we need parser.SvgElement and the derived types with tags in the v-table.
|
||||||
pub const prototype = *Element;
|
pub const prototype = *Element;
|
||||||
// While this is a Node, could consider not exposing the subtype untill we have
|
// While this is a Node, could consider not exposing the subtype until we have
|
||||||
// a Self type to cast to.
|
// a Self type to cast to.
|
||||||
pub const subtype = .node;
|
pub const subtype = .node;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const Page = @import("../page.zig").Page;
|
|||||||
|
|
||||||
const Navigator = @import("navigator.zig").Navigator;
|
const Navigator = @import("navigator.zig").Navigator;
|
||||||
const History = @import("History.zig");
|
const History = @import("History.zig");
|
||||||
|
const Navigation = @import("../navigation/Navigation.zig");
|
||||||
const Location = @import("location.zig").Location;
|
const Location = @import("location.zig").Location;
|
||||||
const Crypto = @import("../crypto/crypto.zig").Crypto;
|
const Crypto = @import("../crypto/crypto.zig").Crypto;
|
||||||
const Console = @import("../console/console.zig").Console;
|
const Console = @import("../console/console.zig").Console;
|
||||||
@@ -41,6 +42,9 @@ const Request = @import("../fetch/Request.zig");
|
|||||||
const fetchFn = @import("../fetch/fetch.zig").fetch;
|
const fetchFn = @import("../fetch/fetch.zig").fetch;
|
||||||
|
|
||||||
const storage = @import("../storage/storage.zig");
|
const storage = @import("../storage/storage.zig");
|
||||||
|
const ErrorEvent = @import("error_event.zig").ErrorEvent;
|
||||||
|
|
||||||
|
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#interface-window-extensions
|
// https://dom.spec.whatwg.org/#interface-window-extensions
|
||||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
|
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
|
||||||
@@ -52,7 +56,7 @@ pub const Window = struct {
|
|||||||
|
|
||||||
document: *parser.DocumentHTML,
|
document: *parser.DocumentHTML,
|
||||||
target: []const u8 = "",
|
target: []const u8 = "",
|
||||||
location: Location = .default,
|
location: Location,
|
||||||
storage_shelf: ?*storage.Shelf = null,
|
storage_shelf: ?*storage.Shelf = null,
|
||||||
|
|
||||||
// counter for having unique timer ids
|
// counter for having unique timer ids
|
||||||
@@ -68,6 +72,7 @@ pub const Window = struct {
|
|||||||
scroll_x: u32 = 0,
|
scroll_x: u32 = 0,
|
||||||
scroll_y: u32 = 0,
|
scroll_y: u32 = 0,
|
||||||
onload_callback: ?js.Function = null,
|
onload_callback: ?js.Function = null,
|
||||||
|
onpopstate_callback: ?js.Function = null,
|
||||||
|
|
||||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
|
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
|
||||||
var fbs = std.io.fixedBufferStream("");
|
var fbs = std.io.fixedBufferStream("");
|
||||||
@@ -78,6 +83,7 @@ pub const Window = struct {
|
|||||||
return .{
|
return .{
|
||||||
.document = html_doc,
|
.document = html_doc,
|
||||||
.target = target orelse "",
|
.target = target orelse "",
|
||||||
|
.location = try .init("about:blank"),
|
||||||
.navigator = navigator orelse .{},
|
.navigator = navigator orelse .{},
|
||||||
.performance = Performance.init(),
|
.performance = Performance.init(),
|
||||||
};
|
};
|
||||||
@@ -88,6 +94,10 @@ pub const Window = struct {
|
|||||||
try parser.documentHTMLSetLocation(Location, self.document, &self.location);
|
try parser.documentHTMLSetLocation(Location, self.document, &self.location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn changeLocation(self: *Window, new_url: []const u8, page: *Page) !void {
|
||||||
|
return self.location.url.reinit(new_url, page);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
|
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
|
||||||
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
|
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
|
||||||
self.document = doc;
|
self.document = doc;
|
||||||
@@ -109,31 +119,17 @@ pub const Window = struct {
|
|||||||
|
|
||||||
/// Sets `onload_callback`.
|
/// Sets `onload_callback`.
|
||||||
pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
|
pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
|
||||||
const event_target = parser.toEventTarget(Window, self);
|
try DirectEventHandler(Window, self, "load", maybe_listener, &self.onload_callback, page.arena);
|
||||||
const event_type = "load";
|
}
|
||||||
|
|
||||||
// Check if we have a listener set.
|
/// Returns `onpopstate_callback`.
|
||||||
if (self.onload_callback) |callback| {
|
pub fn get_onpopstate(self: *const Window) ?js.Function {
|
||||||
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
|
return self.onpopstate_callback;
|
||||||
std.debug.assert(listener != null);
|
}
|
||||||
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maybe_listener) |listener| {
|
/// Sets `onpopstate_callback`.
|
||||||
switch (listener) {
|
pub fn set_onpopstate(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
|
||||||
// If an object is given as listener, do nothing.
|
try DirectEventHandler(Window, self, "popstate", maybe_listener, &self.onpopstate_callback, page.arena);
|
||||||
.object => {},
|
|
||||||
.function => |callback| {
|
|
||||||
_ = try EventHandler.register(page.arena, event_target, event_type, listener, null) orelse unreachable;
|
|
||||||
self.onload_callback = callback;
|
|
||||||
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Just unset the listener.
|
|
||||||
self.onload_callback = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_location(self: *Window) *Location {
|
pub fn get_location(self: *Window) *Location {
|
||||||
@@ -141,7 +137,7 @@ pub const Window = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
|
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
|
||||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
|
||||||
}
|
}
|
||||||
|
|
||||||
// frames return the window itself, but accessing it via a pseudo
|
// frames return the window itself, but accessing it via a pseudo
|
||||||
@@ -189,6 +185,10 @@ pub const Window = struct {
|
|||||||
return &page.session.history;
|
return &page.session.history;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_navigation(_: *Window, page: *Page) *Navigation {
|
||||||
|
return &page.session.navigation;
|
||||||
|
}
|
||||||
|
|
||||||
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
|
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
|
||||||
pub fn get_innerHeight(_: *Window, page: *Page) u32 {
|
pub fn get_innerHeight(_: *Window, page: *Page) u32 {
|
||||||
// We do not have scrollbars or padding so this is the same as Element.clientHeight
|
// We do not have scrollbars or padding so this is the same as Element.clientHeight
|
||||||
@@ -247,8 +247,8 @@ pub const Window = struct {
|
|||||||
_ = self.timers.remove(id);
|
_ = self.timers.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !u32 {
|
pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !void {
|
||||||
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
|
_ = try self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _setImmediate(self: *Window, cbk: js.Function, page: *Page) !u32 {
|
pub fn _setImmediate(self: *Window, cbk: js.Function, page: *Page) !u32 {
|
||||||
@@ -281,6 +281,25 @@ pub const Window = struct {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn _reportError(self: *Window, err: js.Object, page: *Page) !void {
|
||||||
|
var error_event = try ErrorEvent.constructor("error", .{
|
||||||
|
.@"error" = err,
|
||||||
|
});
|
||||||
|
_ = try parser.eventTargetDispatchEvent(
|
||||||
|
parser.toEventTarget(Window, self),
|
||||||
|
@as(*parser.Event, &error_event.proto),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (parser.eventDefaultPrevented(&error_event.proto) == false) {
|
||||||
|
const err_string = err.toString() catch "Unknown error";
|
||||||
|
log.info(.user_script, "error", .{
|
||||||
|
.err = err_string,
|
||||||
|
.stack = page.stackTrace() catch "???",
|
||||||
|
.source = "window.reportError",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const CreateTimeoutOpts = struct {
|
const CreateTimeoutOpts = struct {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
args: []js.Object = &.{},
|
args: []js.Object = &.{},
|
||||||
|
|||||||
@@ -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.?.getModule(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);
|
||||||
}
|
}
|
||||||
@@ -750,9 +764,16 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
|
|||||||
unreachable;
|
unreachable;
|
||||||
},
|
},
|
||||||
.@"enum" => |e| {
|
.@"enum" => |e| {
|
||||||
switch (@typeInfo(e.tag_type)) {
|
if (@hasDecl(T, "ENUM_JS_USE_TAG")) {
|
||||||
.int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_value, self.v8_context)),
|
const str = try self.jsValueToZig(named_function, []const u8, js_value);
|
||||||
else => @compileError(named_function.full_name ++ " has an unsupported enum parameter type: " ++ @typeName(T)),
|
return std.meta.stringToEnum(T, str) orelse return error.InvalidEnumValue;
|
||||||
|
} else {
|
||||||
|
switch (@typeInfo(e.tag_type)) {
|
||||||
|
.int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_value, self.v8_context)),
|
||||||
|
else => {
|
||||||
|
@compileError(named_function.full_name ++ " has an unsupported enum parameter type: " ++ @typeName(T));
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
@@ -764,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 {
|
||||||
@@ -1182,60 +1203,27 @@ 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.?.getModule(normalized_specifier, referrer_path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var fetch_result = try self.script_manager.?.waitForModule(normalized_specifier);
|
var source = try self.script_manager.?.waitForImport(normalized_specifier);
|
||||||
defer fetch_result.deinit();
|
defer source.deinit();
|
||||||
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(self);
|
try_catch.init(self);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
const entry = self.module(true, fetch_result.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
|
||||||
@@ -1290,7 +1278,7 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Next, we need to actually load it.
|
// Next, we need to actually load it.
|
||||||
self.script_manager.?.getAsyncModule(specifier, dynamicModuleSourceCallback, state, referrer) catch |err| {
|
self.script_manager.?.getAsyncImport(specifier, dynamicModuleSourceCallback, state, referrer) catch |err| {
|
||||||
const error_msg = v8.String.initUtf8(isolate, @errorName(err));
|
const error_msg = v8.String.initUtf8(isolate, @errorName(err));
|
||||||
_ = resolver.reject(self.v8_context, error_msg.toValue());
|
_ = resolver.reject(self.v8_context, error_msg.toValue());
|
||||||
};
|
};
|
||||||
@@ -1310,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
|
||||||
@@ -1323,24 +1336,24 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
|
|||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dynamicModuleSourceCallback(ctx: *anyopaque, fetch_result_: anyerror!ScriptManager.GetResult) void {
|
fn dynamicModuleSourceCallback(ctx: *anyopaque, module_source_: anyerror!ScriptManager.ModuleSource) void {
|
||||||
const state: *DynamicModuleResolveState = @ptrCast(@alignCast(ctx));
|
const state: *DynamicModuleResolveState = @ptrCast(@alignCast(ctx));
|
||||||
var self = state.context;
|
var self = state.context;
|
||||||
|
|
||||||
var fetch_result = fetch_result_ catch |err| {
|
var ms = module_source_ catch |err| {
|
||||||
const error_msg = v8.String.initUtf8(self.isolate, @errorName(err));
|
const error_msg = v8.String.initUtf8(self.isolate, @errorName(err));
|
||||||
_ = state.resolver.castToPromiseResolver().reject(self.v8_context, error_msg.toValue());
|
_ = state.resolver.castToPromiseResolver().reject(self.v8_context, error_msg.toValue());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const module_entry = blk: {
|
const module_entry = blk: {
|
||||||
defer fetch_result.deinit();
|
defer ms.deinit();
|
||||||
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(self);
|
try_catch.init(self);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
break :blk self.module(true, fetch_result.src(), state.specifier, true) catch {
|
break :blk self.module(true, ms.src(), state.specifier, true) catch {
|
||||||
const ex = try_catch.exception(self.call_arena) catch |err| @errorName(err) orelse "unknown error";
|
const ex = try_catch.exception(self.call_arena) catch |err| @errorName(err) orelse "unknown error";
|
||||||
log.err(.js, "module compilation failed", .{
|
log.err(.js, "module compilation failed", .{
|
||||||
.specifier = state.specifier,
|
.specifier = state.specifier,
|
||||||
@@ -1447,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) {
|
||||||
@@ -1482,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);
|
||||||
}
|
}
|
||||||
@@ -1575,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);
|
||||||
|
|||||||
@@ -28,7 +28,14 @@ pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector
|
|||||||
// If necessary, turn a void context into something we can safely ptrCast
|
// If necessary, turn a void context into something we can safely ptrCast
|
||||||
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
|
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
|
||||||
|
|
||||||
const channel = v8.InspectorChannel.init(safe_context, InspectorContainer.onInspectorResponse, InspectorContainer.onInspectorEvent, isolate);
|
const channel = v8.InspectorChannel.init(
|
||||||
|
safe_context,
|
||||||
|
InspectorContainer.onInspectorResponse,
|
||||||
|
InspectorContainer.onInspectorEvent,
|
||||||
|
InspectorContainer.onRunMessageLoopOnPause,
|
||||||
|
InspectorContainer.onQuitMessageLoopOnPause,
|
||||||
|
isolate,
|
||||||
|
);
|
||||||
|
|
||||||
const client = v8.InspectorClient.init();
|
const client = v8.InspectorClient.init();
|
||||||
|
|
||||||
@@ -109,6 +116,8 @@ pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []con
|
|||||||
const NoopInspector = struct {
|
const NoopInspector = struct {
|
||||||
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
|
||||||
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
|
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
|
||||||
|
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {}
|
||||||
|
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
|
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -58,6 +56,10 @@ pub fn TypedArray(comptime T: type) type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const ArrayBuffer = struct {
|
||||||
|
values: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
pub const PromiseResolver = struct {
|
pub const PromiseResolver = struct {
|
||||||
context: *Context,
|
context: *Context,
|
||||||
resolver: v8.PromiseResolver,
|
resolver: v8.PromiseResolver,
|
||||||
@@ -146,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,
|
||||||
|
|
||||||
@@ -159,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 {
|
||||||
@@ -323,63 +336,86 @@ 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)) {
|
||||||
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
|
ArrayBuffer => {
|
||||||
const values = value.values;
|
const values = value.values;
|
||||||
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
|
const len = values.len;
|
||||||
const len = values.len;
|
var array_buffer: v8.ArrayBuffer = undefined;
|
||||||
const bits = switch (@typeInfo(value_type)) {
|
const backing_store = v8.BackingStore.init(isolate, len);
|
||||||
.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),
|
||||||
.@"enum" => {
|
.@"enum" => {
|
||||||
const T = @TypeOf(value);
|
const T = @TypeOf(value);
|
||||||
if (@hasDecl(T, "toString")) {
|
if (@hasDecl(T, "toString")) {
|
||||||
|
// This should be deprecated in favor of the ENUM_JS_USE_TAG.
|
||||||
return simpleZigValueToJs(isolate, value.toString(), fail);
|
return simpleZigValueToJs(isolate, value.toString(), fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (@hasDecl(T, "ENUM_JS_USE_TAG")) {
|
||||||
|
return simpleZigValueToJs(isolate, @tagName(value), fail);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ const Interfaces = generate.Tuple(.{
|
|||||||
@import("../storage/storage.zig").Interfaces,
|
@import("../storage/storage.zig").Interfaces,
|
||||||
@import("../url/url.zig").Interfaces,
|
@import("../url/url.zig").Interfaces,
|
||||||
@import("../xhr/xhr.zig").Interfaces,
|
@import("../xhr/xhr.zig").Interfaces,
|
||||||
|
@import("../navigation/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("../xhr/File.zig"),
|
|
||||||
@import("../xmlserializer/xmlserializer.zig").Interfaces,
|
@import("../xmlserializer/xmlserializer.zig").Interfaces,
|
||||||
@import("../fetch/fetch.zig").Interfaces,
|
@import("../fetch/fetch.zig").Interfaces,
|
||||||
@import("../streams/streams.zig").Interfaces,
|
@import("../streams/streams.zig").Interfaces,
|
||||||
@@ -25,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
359
src/browser/navigation/Navigation.zig
Normal file
359
src/browser/navigation/Navigation.zig
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
const URL = @import("../../url.zig").URL;
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
|
||||||
|
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||||
|
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||||
|
|
||||||
|
const parser = @import("../netsurf.zig");
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Navigation
|
||||||
|
const Navigation = @This();
|
||||||
|
|
||||||
|
const NavigationKind = @import("root.zig").NavigationKind;
|
||||||
|
const NavigationHistoryEntry = @import("root.zig").NavigationHistoryEntry;
|
||||||
|
const NavigationTransition = @import("root.zig").NavigationTransition;
|
||||||
|
const NavigationState = @import("root.zig").NavigationState;
|
||||||
|
const NavigationCurrentEntryChangeEvent = @import("root.zig").NavigationCurrentEntryChangeEvent;
|
||||||
|
|
||||||
|
const NavigationEventTarget = @import("NavigationEventTarget.zig");
|
||||||
|
|
||||||
|
pub const prototype = *NavigationEventTarget;
|
||||||
|
proto: NavigationEventTarget = NavigationEventTarget{},
|
||||||
|
|
||||||
|
index: usize = 0,
|
||||||
|
// Need to be stable pointers, because Events can reference entries.
|
||||||
|
entries: std.ArrayListUnmanaged(*NavigationHistoryEntry) = .empty,
|
||||||
|
next_entry_id: usize = 0,
|
||||||
|
|
||||||
|
pub fn resetForNewPage(self: *Navigation) void {
|
||||||
|
// libdom will automatically clean this up when a new page is made.
|
||||||
|
// We must create a new target whenever we create a new page.
|
||||||
|
self.proto = NavigationEventTarget{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_canGoBack(self: *const Navigation) bool {
|
||||||
|
return self.index > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_canGoForward(self: *const Navigation) bool {
|
||||||
|
return self.entries.items.len > self.index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn currentEntry(self: *Navigation) *NavigationHistoryEntry {
|
||||||
|
return self.entries.items[self.index];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_currentEntry(self: *Navigation) *NavigationHistoryEntry {
|
||||||
|
return self.currentEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_transition(_: *const Navigation) ?NavigationTransition {
|
||||||
|
// For now, all transitions are just considered complete.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavigationReturn = struct {
|
||||||
|
committed: js.Promise,
|
||||||
|
finished: js.Promise,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn _back(self: *Navigation, page: *Page) !NavigationReturn {
|
||||||
|
if (!self.get_canGoBack()) {
|
||||||
|
return error.InvalidStateError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const new_index = self.index - 1;
|
||||||
|
const next_entry = self.entries.items[new_index];
|
||||||
|
self.index = new_index;
|
||||||
|
|
||||||
|
return self.navigate(next_entry.url, .{ .traverse = new_index }, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _entries(self: *const Navigation) []*NavigationHistoryEntry {
|
||||||
|
return self.entries.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _forward(self: *Navigation, page: *Page) !NavigationReturn {
|
||||||
|
if (!self.get_canGoForward()) {
|
||||||
|
return error.InvalidStateError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const new_index = self.index + 1;
|
||||||
|
const next_entry = self.entries.items[new_index];
|
||||||
|
self.index = new_index;
|
||||||
|
|
||||||
|
return self.navigate(next_entry.url, .{ .traverse = new_index }, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn updateEntries(self: *Navigation, url: []const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void {
|
||||||
|
switch (kind) {
|
||||||
|
.replace => {
|
||||||
|
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = null }, page, dispatch);
|
||||||
|
},
|
||||||
|
.push => |state| {
|
||||||
|
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, dispatch);
|
||||||
|
},
|
||||||
|
.traverse => |index| {
|
||||||
|
self.index = index;
|
||||||
|
},
|
||||||
|
.reload => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is for after true navigation processing, where we need to ensure that our entries are up to date.
|
||||||
|
// This is only really safe to run in the `pageDoneCallback` where we can guarantee that the URL and NavigationKind are correct.
|
||||||
|
pub fn processNavigation(self: *Navigation, page: *Page) !void {
|
||||||
|
const url = page.url.raw;
|
||||||
|
const kind: NavigationKind = page.session.navigation_kind orelse .{ .push = null };
|
||||||
|
try self.updateEntries(url, kind, page, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes an entry into the Navigation stack WITHOUT actually navigating to it.
|
||||||
|
/// For that, use `navigate`.
|
||||||
|
pub fn pushEntry(
|
||||||
|
self: *Navigation,
|
||||||
|
_url: []const u8,
|
||||||
|
state: NavigationState,
|
||||||
|
page: *Page,
|
||||||
|
dispatch: bool,
|
||||||
|
) !*NavigationHistoryEntry {
|
||||||
|
const arena = page.session.arena;
|
||||||
|
|
||||||
|
const url = try arena.dupe(u8, _url);
|
||||||
|
|
||||||
|
// truncates our history here.
|
||||||
|
if (self.entries.items.len > self.index + 1) {
|
||||||
|
self.entries.shrinkRetainingCapacity(self.index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = self.entries.items.len;
|
||||||
|
|
||||||
|
const id = self.next_entry_id;
|
||||||
|
self.next_entry_id += 1;
|
||||||
|
|
||||||
|
const id_str = try std.fmt.allocPrint(arena, "{d}", .{id});
|
||||||
|
|
||||||
|
const entry = try arena.create(NavigationHistoryEntry);
|
||||||
|
entry.* = NavigationHistoryEntry{
|
||||||
|
.id = id_str,
|
||||||
|
.key = id_str,
|
||||||
|
.url = url,
|
||||||
|
.state = state,
|
||||||
|
};
|
||||||
|
|
||||||
|
// we don't always have a current entry...
|
||||||
|
const previous = if (self.entries.items.len > 0) self.currentEntry() else null;
|
||||||
|
try self.entries.append(arena, entry);
|
||||||
|
self.index = index;
|
||||||
|
|
||||||
|
if (previous) |prev| {
|
||||||
|
if (dispatch) {
|
||||||
|
NavigationCurrentEntryChangeEvent.dispatch(self, prev, .push);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replaceEntry(
|
||||||
|
self: *Navigation,
|
||||||
|
_url: []const u8,
|
||||||
|
state: NavigationState,
|
||||||
|
page: *Page,
|
||||||
|
dispatch: bool,
|
||||||
|
) !*NavigationHistoryEntry {
|
||||||
|
const arena = page.session.arena;
|
||||||
|
const url = try arena.dupe(u8, _url);
|
||||||
|
|
||||||
|
const previous = self.currentEntry();
|
||||||
|
|
||||||
|
const id = self.next_entry_id;
|
||||||
|
self.next_entry_id += 1;
|
||||||
|
const id_str = try std.fmt.allocPrint(arena, "{d}", .{id});
|
||||||
|
|
||||||
|
const entry = try arena.create(NavigationHistoryEntry);
|
||||||
|
entry.* = NavigationHistoryEntry{
|
||||||
|
.id = id_str,
|
||||||
|
.key = previous.key,
|
||||||
|
.url = url,
|
||||||
|
.state = state,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.entries.items[self.index] = entry;
|
||||||
|
|
||||||
|
if (dispatch) {
|
||||||
|
NavigationCurrentEntryChangeEvent.dispatch(self, previous, .replace);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavigateOptions = struct {
|
||||||
|
const NavigateOptionsHistory = enum {
|
||||||
|
pub const ENUM_JS_USE_TAG = true;
|
||||||
|
|
||||||
|
auto,
|
||||||
|
push,
|
||||||
|
replace,
|
||||||
|
};
|
||||||
|
|
||||||
|
state: ?js.Object = null,
|
||||||
|
info: ?js.Object = null,
|
||||||
|
history: NavigateOptionsHistory = .auto,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn navigate(
|
||||||
|
self: *Navigation,
|
||||||
|
_url: ?[]const u8,
|
||||||
|
kind: NavigationKind,
|
||||||
|
page: *Page,
|
||||||
|
) !NavigationReturn {
|
||||||
|
const arena = page.session.arena;
|
||||||
|
const url = _url orelse return error.MissingURL;
|
||||||
|
|
||||||
|
// https://github.com/WICG/navigation-api/issues/95
|
||||||
|
//
|
||||||
|
// These will only settle on same-origin navigation (mostly intended for SPAs).
|
||||||
|
// It is fine (and expected) for these to not settle on cross-origin requests :)
|
||||||
|
const committed = try page.js.createPromiseResolver(.page);
|
||||||
|
const finished = try page.js.createPromiseResolver(.page);
|
||||||
|
|
||||||
|
const new_url_string = try URL.stitch(arena, url, page.url.raw, .{});
|
||||||
|
const new_url = try URL.parse(new_url_string, null);
|
||||||
|
|
||||||
|
const is_same_document = try page.url.eqlDocument(&new_url, arena);
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
.push => |state| {
|
||||||
|
if (is_same_document) {
|
||||||
|
page.url = new_url;
|
||||||
|
|
||||||
|
try committed.resolve({});
|
||||||
|
// todo: Fire navigate event
|
||||||
|
try finished.resolve({});
|
||||||
|
|
||||||
|
_ = 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 {
|
||||||
|
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.traverse => |index| {
|
||||||
|
self.index = index;
|
||||||
|
|
||||||
|
if (is_same_document) {
|
||||||
|
page.url = new_url;
|
||||||
|
|
||||||
|
try committed.resolve({});
|
||||||
|
// todo: Fire navigate event
|
||||||
|
try finished.resolve({});
|
||||||
|
} else {
|
||||||
|
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.reload => {
|
||||||
|
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.committed = committed.promise(),
|
||||||
|
.finished = finished.promise(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn {
|
||||||
|
const opts = _opts orelse NavigateOptions{};
|
||||||
|
const json = if (opts.state) |state| state.toJson(page.session.arena) catch return error.DataClone else null;
|
||||||
|
|
||||||
|
const kind: NavigationKind = switch (opts.history) {
|
||||||
|
.replace => .{ .replace = json },
|
||||||
|
.push, .auto => .{ .push = json },
|
||||||
|
};
|
||||||
|
|
||||||
|
return try self.navigate(_url, kind, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ReloadOptions = struct {
|
||||||
|
state: ?js.Object = null,
|
||||||
|
info: ?js.Object = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn _reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !NavigationReturn {
|
||||||
|
const arena = page.session.arena;
|
||||||
|
|
||||||
|
const opts = _opts orelse ReloadOptions{};
|
||||||
|
const entry = self.currentEntry();
|
||||||
|
if (opts.state) |state| {
|
||||||
|
const previous = entry;
|
||||||
|
entry.state = .{ .source = .navigation, .value = state.toJson(arena) catch return error.DataClone };
|
||||||
|
NavigationCurrentEntryChangeEvent.dispatch(self, previous, .reload);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.navigate(entry.url, .reload, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TraverseToOptions = struct {
|
||||||
|
info: ?js.Object = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn _traverseTo(self: *Navigation, key: []const u8, _opts: ?TraverseToOptions, page: *Page) !NavigationReturn {
|
||||||
|
if (_opts != null) {
|
||||||
|
log.debug(.browser, "not implemented", .{ .options = _opts });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self.entries.items, 0..) |entry, i| {
|
||||||
|
if (std.mem.eql(u8, key, entry.key)) {
|
||||||
|
return try self.navigate(entry.url, .{ .traverse = i }, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.InvalidStateError;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const UpdateCurrentEntryOptions = struct {
|
||||||
|
state: js.Object,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn _updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions, page: *Page) !void {
|
||||||
|
const arena = page.session.arena;
|
||||||
|
|
||||||
|
const previous = self.currentEntry();
|
||||||
|
self.currentEntry().state = .{ .source = .navigation, .value = options.state.toJson(arena) catch return error.DataClone };
|
||||||
|
NavigationCurrentEntryChangeEvent.dispatch(self, previous, null);
|
||||||
|
}
|
||||||
58
src/browser/navigation/NavigationEventTarget.zig
Normal file
58
src/browser/navigation/NavigationEventTarget.zig
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||||
|
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||||
|
|
||||||
|
const parser = @import("../netsurf.zig");
|
||||||
|
|
||||||
|
pub const NavigationEventTarget = @This();
|
||||||
|
|
||||||
|
pub const prototype = *EventTarget;
|
||||||
|
// Extend libdom event target for pure zig struct.
|
||||||
|
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .navigation },
|
||||||
|
|
||||||
|
oncurrententrychange_cbk: ?js.Function = null,
|
||||||
|
|
||||||
|
fn register(
|
||||||
|
self: *NavigationEventTarget,
|
||||||
|
alloc: std.mem.Allocator,
|
||||||
|
typ: []const u8,
|
||||||
|
listener: EventHandler.Listener,
|
||||||
|
) !?js.Function {
|
||||||
|
const target = parser.toEventTarget(NavigationEventTarget, self);
|
||||||
|
|
||||||
|
// The only time this can return null if the listener is already
|
||||||
|
// registered. But before calling `register`, all of our functions
|
||||||
|
// remove any existing listener, so it should be impossible to get null
|
||||||
|
// from this function call.
|
||||||
|
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
|
||||||
|
return eh.callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unregister(self: *NavigationEventTarget, typ: []const u8, cbk_id: usize) !void {
|
||||||
|
const et = parser.toEventTarget(NavigationEventTarget, self);
|
||||||
|
// check if event target has already this listener
|
||||||
|
const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id);
|
||||||
|
if (lst == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove listener
|
||||||
|
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_oncurrententrychange(self: *NavigationEventTarget) ?js.Function {
|
||||||
|
return self.oncurrententrychange_cbk;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_oncurrententrychange(self: *NavigationEventTarget, listener: ?EventHandler.Listener, page: *Page) !void {
|
||||||
|
if (self.oncurrententrychange_cbk) |cbk| try self.unregister("currententrychange", cbk.id);
|
||||||
|
if (listener) |listen| {
|
||||||
|
self.oncurrententrychange_cbk = try self.register(page.arena, "currententrychange", listen);
|
||||||
|
} else {
|
||||||
|
self.oncurrententrychange_cbk = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
224
src/browser/navigation/root.zig
Normal file
224
src/browser/navigation/root.zig
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
const URL = @import("../../url.zig").URL;
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
|
||||||
|
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||||
|
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||||
|
|
||||||
|
const parser = @import("../netsurf.zig");
|
||||||
|
|
||||||
|
const Navigation = @import("Navigation.zig");
|
||||||
|
const NavigationEventTarget = @import("NavigationEventTarget.zig");
|
||||||
|
|
||||||
|
pub const Interfaces = .{
|
||||||
|
Navigation,
|
||||||
|
NavigationEventTarget,
|
||||||
|
NavigationActivation,
|
||||||
|
NavigationTransition,
|
||||||
|
NavigationHistoryEntry,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const NavigationType = enum {
|
||||||
|
pub const ENUM_JS_USE_TAG = true;
|
||||||
|
|
||||||
|
push,
|
||||||
|
replace,
|
||||||
|
traverse,
|
||||||
|
reload,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const NavigationKind = union(NavigationType) {
|
||||||
|
push: ?[]const u8,
|
||||||
|
replace: ?[]const u8,
|
||||||
|
traverse: usize,
|
||||||
|
reload,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const NavigationState = struct {
|
||||||
|
source: enum { history, navigation },
|
||||||
|
value: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry
|
||||||
|
pub const NavigationHistoryEntry = struct {
|
||||||
|
pub const prototype = *EventTarget;
|
||||||
|
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain },
|
||||||
|
|
||||||
|
id: []const u8,
|
||||||
|
key: []const u8,
|
||||||
|
url: ?[]const u8,
|
||||||
|
state: NavigationState,
|
||||||
|
|
||||||
|
pub fn get_id(self: *const NavigationHistoryEntry) []const u8 {
|
||||||
|
return self.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_index(self: *const NavigationHistoryEntry, page: *Page) i32 {
|
||||||
|
const navigation = page.session.navigation;
|
||||||
|
for (navigation.entries.items, 0..) |entry, i| {
|
||||||
|
if (std.mem.eql(u8, entry.id, self.id)) {
|
||||||
|
return @intCast(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_key(self: *const NavigationHistoryEntry) []const u8 {
|
||||||
|
return self.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sameDocument(self: *const NavigationHistoryEntry, page: *Page) !bool {
|
||||||
|
const _url = self.url orelse return false;
|
||||||
|
const url = try URL.parse(_url, null);
|
||||||
|
return page.url.eqlDocument(&url, page.call_arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_url(self: *const NavigationHistoryEntry) ?[]const u8 {
|
||||||
|
return self.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const StateReturn = union(enum) { value: ?js.Value, undefined: void };
|
||||||
|
|
||||||
|
pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !StateReturn {
|
||||||
|
if (self.state.source == .navigation) {
|
||||||
|
if (self.state.value) |value| {
|
||||||
|
return .{ .value = try js.Value.fromJson(page.js, value) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation
|
||||||
|
pub const NavigationActivation = struct {
|
||||||
|
const NavigationActivationType = enum {
|
||||||
|
pub const ENUM_JS_USE_TAG = true;
|
||||||
|
|
||||||
|
push,
|
||||||
|
reload,
|
||||||
|
replace,
|
||||||
|
traverse,
|
||||||
|
};
|
||||||
|
|
||||||
|
entry: NavigationHistoryEntry,
|
||||||
|
from: ?NavigationHistoryEntry = null,
|
||||||
|
type: NavigationActivationType,
|
||||||
|
|
||||||
|
pub fn get_entry(self: *const NavigationActivation) NavigationHistoryEntry {
|
||||||
|
return self.entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_from(self: *const NavigationActivation) ?NavigationHistoryEntry {
|
||||||
|
return self.from;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_navigationType(self: *const NavigationActivation) NavigationActivationType {
|
||||||
|
return self.type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationTransition
|
||||||
|
pub const NavigationTransition = struct {
|
||||||
|
finished: js.Promise,
|
||||||
|
from: NavigationHistoryEntry,
|
||||||
|
navigation_type: NavigationActivation.NavigationActivationType,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Event = @import("../events/event.zig").Event;
|
||||||
|
|
||||||
|
pub const NavigationCurrentEntryChangeEvent = struct {
|
||||||
|
pub const prototype = *Event;
|
||||||
|
pub const union_make_copy = true;
|
||||||
|
|
||||||
|
pub const EventInit = struct {
|
||||||
|
from: *NavigationHistoryEntry,
|
||||||
|
navigationType: ?NavigationType = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
proto: parser.Event,
|
||||||
|
from: *NavigationHistoryEntry,
|
||||||
|
navigation_type: ?NavigationType,
|
||||||
|
|
||||||
|
pub fn constructor(event_type: []const u8, opts: EventInit) !NavigationCurrentEntryChangeEvent {
|
||||||
|
const event = try parser.eventCreate();
|
||||||
|
defer parser.eventDestroy(event);
|
||||||
|
|
||||||
|
try parser.eventInit(event, event_type, .{});
|
||||||
|
parser.eventSetInternalType(event, .navigation_current_entry_change_event);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.proto = event.*,
|
||||||
|
.from = opts.from,
|
||||||
|
.navigation_type = opts.navigationType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_from(self: *NavigationCurrentEntryChangeEvent) *NavigationHistoryEntry {
|
||||||
|
return self.from;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_navigationType(self: *const NavigationCurrentEntryChangeEvent) ?NavigationType {
|
||||||
|
return self.navigation_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch(navigation: *Navigation, from: *NavigationHistoryEntry, typ: ?NavigationType) void {
|
||||||
|
log.debug(.script_event, "dispatch event", .{
|
||||||
|
.type = "currententrychange",
|
||||||
|
.source = "navigation",
|
||||||
|
});
|
||||||
|
|
||||||
|
var evt = NavigationCurrentEntryChangeEvent.constructor(
|
||||||
|
"currententrychange",
|
||||||
|
.{ .from = from, .navigationType = typ },
|
||||||
|
) catch |err| {
|
||||||
|
log.err(.app, "event constructor error", .{
|
||||||
|
.err = err,
|
||||||
|
.type = "currententrychange",
|
||||||
|
.source = "navigation",
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = parser.eventTargetDispatchEvent(
|
||||||
|
@as(*parser.EventTarget, @ptrCast(navigation)),
|
||||||
|
&evt.proto,
|
||||||
|
) catch |err| {
|
||||||
|
log.err(.app, "dispatch event error", .{
|
||||||
|
.err = err,
|
||||||
|
.type = "currententrychange",
|
||||||
|
.source = "navigation",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "Browser: Navigation" {
|
||||||
|
try testing.htmlRunner("html/navigation/navigation.html");
|
||||||
|
try testing.htmlRunner("html/navigation/navigation_currententrychange.html");
|
||||||
|
}
|
||||||
@@ -559,6 +559,9 @@ pub const EventType = enum(u8) {
|
|||||||
message_event = 7,
|
message_event = 7,
|
||||||
keyboard_event = 8,
|
keyboard_event = 8,
|
||||||
pop_state = 9,
|
pop_state = 9,
|
||||||
|
composition_event = 10,
|
||||||
|
navigation_current_entry_change_event = 11,
|
||||||
|
page_transition_event = 12,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const MutationEvent = c.dom_mutation_event;
|
pub const MutationEvent = c.dom_mutation_event;
|
||||||
@@ -830,6 +833,7 @@ pub const EventTargetTBase = extern struct {
|
|||||||
message_port = 7,
|
message_port = 7,
|
||||||
screen = 8,
|
screen = 8,
|
||||||
screen_orientation = 9,
|
screen_orientation = 9,
|
||||||
|
navigation = 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
vtable: ?*const c.struct_dom_event_target_vtable = &c.struct_dom_event_target_vtable{
|
vtable: ?*const c.struct_dom_event_target_vtable = &c.struct_dom_event_target_vtable{
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ const ScriptManager = @import("ScriptManager.zig");
|
|||||||
const SlotChangeMonitor = @import("SlotChangeMonitor.zig");
|
const SlotChangeMonitor = @import("SlotChangeMonitor.zig");
|
||||||
const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
||||||
|
|
||||||
|
const NavigationKind = @import("navigation/root.zig").NavigationKind;
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -79,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),
|
||||||
|
|
||||||
@@ -99,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,
|
||||||
@@ -165,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 });
|
||||||
@@ -485,16 +498,16 @@ pub const Page = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
std.debug.print("\nprimary schedule: {d}\n", .{self.scheduler.primary.count()});
|
std.debug.print("\nhigh_priority schedule: {d}\n", .{self.scheduler.high_priority.count()});
|
||||||
var it = self.scheduler.primary.iterator();
|
var it = self.scheduler.high_priority.iterator();
|
||||||
while (it.next()) |task| {
|
while (it.next()) |task| {
|
||||||
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now });
|
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
std.debug.print("\nsecondary schedule: {d}\n", .{self.scheduler.secondary.count()});
|
std.debug.print("\nlow_priority schedule: {d}\n", .{self.scheduler.low_priority.count()});
|
||||||
var it = self.scheduler.secondary.iterator();
|
var it = self.scheduler.low_priority.iterator();
|
||||||
while (it.next()) |task| {
|
while (it.next()) |task| {
|
||||||
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now });
|
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now });
|
||||||
}
|
}
|
||||||
@@ -542,27 +555,64 @@ 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, nothing to do.
|
// if the url is about:blank, we load an empty HTML document in the
|
||||||
|
// page and dispatch the events.
|
||||||
if (std.mem.eql(u8, "about:blank", request_url)) {
|
if (std.mem.eql(u8, "about:blank", request_url)) {
|
||||||
const html_doc = try parser.documentHTMLParseFromStr("");
|
const html_doc = try parser.documentHTMLParseFromStr("");
|
||||||
try self.setDocument(html_doc);
|
try self.setDocument(html_doc);
|
||||||
|
|
||||||
|
// Assume we parsed the document.
|
||||||
|
// It's important to force a reset during the following navigation.
|
||||||
|
self.mode = .parsed;
|
||||||
|
|
||||||
// We do not processHTMLDoc here as we know we don't have any scripts
|
// We do not processHTMLDoc here as we know we don't have any scripts
|
||||||
// This assumption may be false when CDP Page.addScriptToEvaluateOnNewDocument is implemented
|
// This assumption may be false when CDP Page.addScriptToEvaluateOnNewDocument is implemented
|
||||||
try HTMLDocument.documentIsComplete(self.window.document, self);
|
self.documentIsComplete();
|
||||||
|
|
||||||
|
self.session.browser.notification.dispatch(.page_navigate, &.{
|
||||||
|
.req_id = req_id,
|
||||||
|
.opts = opts,
|
||||||
|
.url = request_url,
|
||||||
|
.timestamp = timestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
.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);
|
||||||
@@ -570,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(),
|
||||||
@@ -635,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
|
||||||
@@ -654,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 {
|
||||||
@@ -687,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 };
|
||||||
},
|
},
|
||||||
@@ -736,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
|
||||||
@@ -814,9 +877,6 @@ pub const Page = struct {
|
|||||||
unreachable;
|
unreachable;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push the navigation after a successful load.
|
|
||||||
try self.session.history.pushNavigation(self.url.raw, self);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||||
@@ -859,7 +919,7 @@ pub const Page = struct {
|
|||||||
self.window.setStorageShelf(
|
self.window.setStorageShelf(
|
||||||
try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
|
try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
|
||||||
);
|
);
|
||||||
try self.window.replaceLocation(.{ .url = try self.url.toWebApi(self.arena) });
|
try self.window.changeLocation(self.url.raw, self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const MouseEvent = struct {
|
pub const MouseEvent = struct {
|
||||||
@@ -894,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 });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,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;
|
||||||
try self.navigateFromWebAPI(href, .{});
|
log.debug(.input, "window click on link", .{ .tag = tag, .href = href });
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -929,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 {
|
||||||
@@ -971,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 });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,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";
|
||||||
@@ -1009,6 +1085,33 @@ 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 => {
|
||||||
|
log.debug(.input, "key down event", .{ .tag = tag, .key = new_key });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertText is a shortcut to insert text into the active element.
|
||||||
|
pub fn insertText(self: *Page, v: []const u8) !void {
|
||||||
|
const Document = @import("dom/document.zig").Document;
|
||||||
|
const element = (try Document.getActiveElement(@ptrCast(self.window.document), self)) orelse return;
|
||||||
|
const node = parser.elementToNode(element);
|
||||||
|
|
||||||
|
const tag = (try parser.nodeHTMLGetTagType(node)) orelse return;
|
||||||
|
switch (tag) {
|
||||||
|
.input => {
|
||||||
|
const input_type = try parser.inputGetType(@ptrCast(element));
|
||||||
|
if (std.mem.eql(u8, input_type, "text")) {
|
||||||
|
const value = try parser.inputGetValue(@ptrCast(element));
|
||||||
|
const new_value = try std.mem.concat(self.arena, u8, &.{ value, v });
|
||||||
|
try parser.inputSetValue(@ptrCast(element), new_value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.textarea => {
|
||||||
|
const value = try parser.textareaGetValue(@ptrCast(node));
|
||||||
|
const new_value = try std.mem.concat(self.arena, u8, &.{ value, v });
|
||||||
|
try parser.textareaSetValue(@ptrCast(node), new_value);
|
||||||
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1018,8 +1121,32 @@ pub const Page = struct {
|
|||||||
// As such we schedule the function to be called as soon as possible.
|
// As such we schedule the function to be called as soon as possible.
|
||||||
// The page.arena is safe to use here, but the transfer_arena exists
|
// The page.arena is safe to use here, but the transfer_arena exists
|
||||||
// specifically for this type of lifetime.
|
// specifically for this type of lifetime.
|
||||||
pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts) !void {
|
pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts, kind: NavigationKind) !void {
|
||||||
const session = self.session;
|
const session = self.session;
|
||||||
|
const stitched_url = try URL.stitch(
|
||||||
|
session.transfer_arena,
|
||||||
|
url,
|
||||||
|
self.url.raw,
|
||||||
|
.{
|
||||||
|
.alloc = .always,
|
||||||
|
.null_terminated = true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Force will force a page load.
|
||||||
|
// Otherwise, we need to check if this is a true navigation.
|
||||||
|
if (!opts.force) {
|
||||||
|
// If we are navigating within the same document, just change URL.
|
||||||
|
const new_url = try URL.parse(stitched_url, null);
|
||||||
|
|
||||||
|
if (try self.url.eqlDocument(&new_url, session.transfer_arena)) {
|
||||||
|
self.url = new_url;
|
||||||
|
try self.window.changeLocation(self.url.raw, self);
|
||||||
|
try session.navigation.updateEntries(stitched_url, kind, self, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (session.queued_navigation != null) {
|
if (session.queued_navigation != null) {
|
||||||
// It might seem like this should never happen. And it might not,
|
// It might seem like this should never happen. And it might not,
|
||||||
// BUT..consider the case where we have script like:
|
// BUT..consider the case where we have script like:
|
||||||
@@ -1042,9 +1169,11 @@ pub const Page = struct {
|
|||||||
|
|
||||||
session.queued_navigation = .{
|
session.queued_navigation = .{
|
||||||
.opts = opts,
|
.opts = opts,
|
||||||
.url = try URL.stitch(session.transfer_arena, url, self.url.raw, .{ .alloc = .always }),
|
.url = stitched_url,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
session.navigation_kind = kind;
|
||||||
|
|
||||||
self.http_client.abort();
|
self.http_client.abort();
|
||||||
|
|
||||||
// In v8, this throws an exception which JS code cannot catch.
|
// In v8, this throws an exception which JS code cannot catch.
|
||||||
@@ -1095,7 +1224,7 @@ pub const Page = struct {
|
|||||||
} else {
|
} else {
|
||||||
action = try URL.concatQueryString(transfer_arena, action, buf.items);
|
action = try URL.concatQueryString(transfer_arena, action, buf.items);
|
||||||
}
|
}
|
||||||
try self.navigateFromWebAPI(action, opts);
|
try self.navigateFromWebAPI(action, opts, .{ .push = null });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isNodeAttached(self: *const Page, node: *parser.Node) bool {
|
pub fn isNodeAttached(self: *const Page, node: *parser.Node) bool {
|
||||||
@@ -1145,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 {
|
||||||
@@ -1153,6 +1286,7 @@ pub const NavigateReason = enum {
|
|||||||
form,
|
form,
|
||||||
script,
|
script,
|
||||||
history,
|
history,
|
||||||
|
navigation,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const NavigateOpts = struct {
|
pub const NavigateOpts = struct {
|
||||||
@@ -1161,6 +1295,13 @@ pub const NavigateOpts = struct {
|
|||||||
method: Http.Method = .GET,
|
method: Http.Method = .GET,
|
||||||
body: ?[]const u8 = null,
|
body: ?[]const u8 = null,
|
||||||
header: ?[:0]const u8 = null,
|
header: ?[:0]const u8 = null,
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ const FlatRenderer = struct {
|
|||||||
|
|
||||||
const Element = @import("dom/element.zig").Element;
|
const Element = @import("dom/element.zig").Element;
|
||||||
|
|
||||||
|
// Define the size of each element in the grid.
|
||||||
|
const default_w = 5;
|
||||||
|
const default_h = 5;
|
||||||
|
|
||||||
// we expect allocator to be an arena
|
// we expect allocator to be an arena
|
||||||
pub fn init(allocator: Allocator) FlatRenderer {
|
pub fn init(allocator: Allocator) FlatRenderer {
|
||||||
return .{
|
return .{
|
||||||
@@ -62,10 +66,10 @@ const FlatRenderer = struct {
|
|||||||
gop.value_ptr.* = x;
|
gop.value_ptr.* = x;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _x: f64 = @floatFromInt(x);
|
const _x: f64 = @floatFromInt(x * default_w);
|
||||||
const y: f64 = 0.0;
|
const y: f64 = 0.0;
|
||||||
const w: f64 = 1.0;
|
const w: f64 = default_w;
|
||||||
const h: f64 = 1.0;
|
const h: f64 = default_h;
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.x = _x,
|
.x = _x,
|
||||||
@@ -98,18 +102,20 @@ const FlatRenderer = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn width(self: *const FlatRenderer) u32 {
|
pub fn width(self: *const FlatRenderer) u32 {
|
||||||
return @max(@as(u32, @intCast(self.elements.items.len)), 1); // At least 1 pixel even if empty
|
return @max(@as(u32, @intCast(self.elements.items.len * default_w)), default_w); // At least default width pixels even if empty
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn height(_: *const FlatRenderer) u32 {
|
pub fn height(_: *const FlatRenderer) u32 {
|
||||||
return 1;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getElementAtPosition(self: *const FlatRenderer, x: i32, y: i32) ?*parser.Element {
|
pub fn getElementAtPosition(self: *const FlatRenderer, _x: i32, y: i32) ?*parser.Element {
|
||||||
if (y != 0 or x < 0) {
|
if (y < 0 or y > default_h or _x < 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const x = @divFloor(_x, default_w);
|
||||||
|
|
||||||
const elements = self.elements.items;
|
const elements = self.elements.items;
|
||||||
return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null;
|
return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ const Allocator = std.mem.Allocator;
|
|||||||
|
|
||||||
const js = @import("js/js.zig");
|
const js = @import("js/js.zig");
|
||||||
const Page = @import("page.zig").Page;
|
const Page = @import("page.zig").Page;
|
||||||
|
const NavigationKind = @import("navigation/root.zig").NavigationKind;
|
||||||
const Browser = @import("browser.zig").Browser;
|
const Browser = @import("browser.zig").Browser;
|
||||||
const NavigateOpts = @import("page.zig").NavigateOpts;
|
const NavigateOpts = @import("page.zig").NavigateOpts;
|
||||||
const History = @import("html/History.zig");
|
const History = @import("html/History.zig");
|
||||||
|
const Navigation = @import("navigation/Navigation.zig");
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
const parser = @import("netsurf.zig");
|
const parser = @import("netsurf.zig");
|
||||||
@@ -57,6 +59,8 @@ pub const Session = struct {
|
|||||||
// History is persistent across the "tab".
|
// History is persistent across the "tab".
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/History
|
// https://developer.mozilla.org/en-US/docs/Web/API/History
|
||||||
history: History = .{},
|
history: History = .{},
|
||||||
|
navigation: Navigation = .{},
|
||||||
|
navigation_kind: ?NavigationKind = null,
|
||||||
|
|
||||||
page: ?Page = null,
|
page: ?Page = null,
|
||||||
|
|
||||||
@@ -100,6 +104,9 @@ pub const Session = struct {
|
|||||||
// We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded
|
// We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded
|
||||||
parser.init();
|
parser.init();
|
||||||
|
|
||||||
|
// creates a new event target for Navigation
|
||||||
|
self.navigation.resetForNewPage();
|
||||||
|
|
||||||
const page_arena = &self.browser.page_arena;
|
const page_arena = &self.browser.page_arena;
|
||||||
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||||
_ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 });
|
_ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 });
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const Writer = std.Io.Writer;
|
||||||
|
const ada = @import("ada");
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
@@ -35,182 +37,235 @@ pub const Interfaces = .{
|
|||||||
EntryIterable,
|
EntryIterable,
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://url.spec.whatwg.org/#url
|
/// https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
|
||||||
//
|
|
||||||
// TODO we could avoid many of these getter string allocatoration in two differents
|
|
||||||
// way:
|
|
||||||
//
|
|
||||||
// 1. We can eventually get the slice of scheme *with* the following char in
|
|
||||||
// the underlying string. But I don't know if it's possible and how to do that.
|
|
||||||
// I mean, if the rawuri contains `https://foo.bar`, uri.scheme is a slice
|
|
||||||
// containing only `https`. I want `https:` so, in theory, I don't need to
|
|
||||||
// allocatorate data, I should be able to retrieve the scheme + the following `:`
|
|
||||||
// from rawuri.
|
|
||||||
//
|
|
||||||
// 2. The other way would be to copy the `std.Uri` code to have a dedicated
|
|
||||||
// parser including the characters we want for the web API.
|
|
||||||
pub const URL = struct {
|
pub const URL = struct {
|
||||||
uri: std.Uri,
|
internal: ada.URL,
|
||||||
|
/// We prefer in-house search params solution here;
|
||||||
|
/// ada's search params impl use more memory.
|
||||||
|
/// It also offers it's own iterator implementation
|
||||||
|
/// where we'd like to use ours.
|
||||||
search_params: URLSearchParams,
|
search_params: URLSearchParams,
|
||||||
|
|
||||||
pub const empty = URL{
|
pub const empty = URL{
|
||||||
.uri = .{ .scheme = "" },
|
.internal = null,
|
||||||
.search_params = .{},
|
.search_params = .{},
|
||||||
};
|
};
|
||||||
|
|
||||||
const URLArg = union(enum) {
|
// You can use an existing URL object for either argument, and it will be
|
||||||
url: *URL,
|
// stringified from the object's href property.
|
||||||
element: *parser.ElementHTML,
|
const ConstructorArg = union(enum) {
|
||||||
string: []const u8,
|
string: []const u8,
|
||||||
|
url: *const URL,
|
||||||
|
element: *parser.Element,
|
||||||
|
|
||||||
fn toString(self: URLArg, arena: Allocator) !?[]const u8 {
|
fn toString(self: ConstructorArg, page: *Page) ![]const u8 {
|
||||||
switch (self) {
|
return switch (self) {
|
||||||
.string => |s| return s,
|
.string => |s| s,
|
||||||
.url => |url| return try url.toString(arena),
|
.url => |url| url._toString(page),
|
||||||
.element => |e| return try parser.elementGetAttribute(@ptrCast(e), "href"),
|
.element => |e| {
|
||||||
}
|
const attrib = try parser.elementGetAttribute(@ptrCast(e), "href") orelse {
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
|
||||||
|
return attrib;
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn constructor(url: URLArg, base: ?URLArg, page: *Page) !URL {
|
pub fn constructor(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !URL {
|
||||||
const arena = page.arena;
|
const url_str = try url.toString(page);
|
||||||
const url_str = try url.toString(arena) orelse return error.InvalidArgument;
|
|
||||||
|
|
||||||
var raw: ?[]const u8 = null;
|
const internal = try blk: {
|
||||||
if (base) |b| {
|
if (maybe_base) |base| {
|
||||||
if (try b.toString(arena)) |bb| {
|
break :blk ada.parseWithBase(url_str, try base.toString(page));
|
||||||
raw = try @import("../../url.zig").URL.stitch(arena, url_str, bb, .{});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (raw == null) {
|
break :blk ada.parse(url_str);
|
||||||
// if it was a URL, then it's already be owned by the arena
|
|
||||||
raw = if (url == .url) url_str else try arena.dupe(u8, url_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uri = std.Uri.parse(raw.?) catch blk: {
|
|
||||||
if (!std.mem.endsWith(u8, raw.?, "://")) {
|
|
||||||
return error.TypeError;
|
|
||||||
}
|
|
||||||
// schema only is valid!
|
|
||||||
break :blk std.Uri{
|
|
||||||
.scheme = raw.?[0 .. raw.?.len - 3],
|
|
||||||
.host = .{ .percent_encoded = "" },
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return init(arena, uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(arena: Allocator, uri: std.Uri) !URL {
|
|
||||||
return .{
|
return .{
|
||||||
.uri = uri,
|
.internal = internal,
|
||||||
.search_params = try URLSearchParams.init(
|
.search_params = try prepareSearchParams(page.arena, internal),
|
||||||
arena,
|
|
||||||
uriComponentNullStr(uri.query),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn initWithoutSearchParams(uri: std.Uri) URL {
|
pub fn destructor(self: *const URL) void {
|
||||||
return .{ .uri = uri, .search_params = .{} };
|
// Not tracked by arena.
|
||||||
|
return ada.free(self.internal);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_origin(self: *URL, page: *Page) ![]const u8 {
|
/// Only to be used by `Location` API. `url` MUST NOT provide search params.
|
||||||
var aw = std.Io.Writer.Allocating.init(page.arena);
|
pub fn initForLocation(url: []const u8) !URL {
|
||||||
try self.uri.writeToStream(&aw.writer, .{
|
return .{ .internal = try ada.parse(url), .search_params = .{} };
|
||||||
.scheme = true,
|
|
||||||
.authentication = false,
|
|
||||||
.authority = true,
|
|
||||||
.path = false,
|
|
||||||
.query = false,
|
|
||||||
.fragment = false,
|
|
||||||
});
|
|
||||||
return aw.written();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get_href returns the URL by writing all its components.
|
/// Reinitializes the URL by parsing given `url`. Search params can be provided.
|
||||||
pub fn get_href(self: *URL, page: *Page) ![]const u8 {
|
pub fn reinit(self: *URL, url: []const u8, page: *Page) !void {
|
||||||
return self.toString(page.arena);
|
_ = ada.setHref(self.internal, url);
|
||||||
|
if (!ada.isValid(self.internal)) return error.Internal;
|
||||||
|
|
||||||
|
self.search_params = try prepareSearchParams(page.arena, self.internal);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _toString(self: *URL, page: *Page) ![]const u8 {
|
/// Prepares a `URLSearchParams` from given `internal`.
|
||||||
return self.toString(page.arena);
|
/// Resets `search` of `internal`.
|
||||||
|
fn prepareSearchParams(arena: Allocator, internal: ada.URL) !URLSearchParams {
|
||||||
|
const maybe_search = ada.getSearchNullable(internal);
|
||||||
|
// Empty.
|
||||||
|
if (maybe_search.data == null) return .{};
|
||||||
|
|
||||||
|
const search = maybe_search.data[0..maybe_search.length];
|
||||||
|
const search_params = URLSearchParams.initFromString(arena, search);
|
||||||
|
// After a call to this function, search params are tracked by
|
||||||
|
// `search_params`. So we reset the internal's search.
|
||||||
|
ada.clearSearch(internal);
|
||||||
|
|
||||||
|
return search_params;
|
||||||
}
|
}
|
||||||
|
|
||||||
// format the url with all its components.
|
pub fn clearPort(self: *const URL) void {
|
||||||
pub fn toString(self: *const URL, arena: Allocator) ![]const u8 {
|
return ada.clearPort(self.internal);
|
||||||
var aw = std.Io.Writer.Allocating.init(arena);
|
}
|
||||||
try self.uri.writeToStream(&aw.writer, .{
|
|
||||||
.scheme = true,
|
|
||||||
.authentication = true,
|
|
||||||
.authority = true,
|
|
||||||
.path = uriComponentNullStr(self.uri.path).len > 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
pub fn clearHash(self: *const URL) void {
|
||||||
|
return ada.clearHash(self.internal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a boolean indicating whether or not an absolute URL,
|
||||||
|
/// or a relative URL combined with a base URL, are parsable and valid.
|
||||||
|
pub fn static_canParse(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !bool {
|
||||||
|
const url_str = try url.toString(page);
|
||||||
|
|
||||||
|
if (maybe_base) |base| {
|
||||||
|
return ada.canParseWithBase(url_str, try base.toString(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ada.canParse(url_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alias to get_href.
|
||||||
|
pub fn _toString(self: *const URL, page: *Page) ![]const u8 {
|
||||||
|
return self.get_href(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters.
|
||||||
|
|
||||||
|
pub fn get_searchParams(self: *URL) *URLSearchParams {
|
||||||
|
return &self.search_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_origin(self: *const URL, page: *Page) ![]const u8 {
|
||||||
|
// `ada.getOriginNullable` allocates memory in order to find the `origin`.
|
||||||
|
// We'd like to use our arena allocator for such case;
|
||||||
|
// so here we allocate the `origin` in page arena and free the original.
|
||||||
|
const maybe_origin = ada.getOriginNullable(self.internal);
|
||||||
|
if (maybe_origin.data == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
defer ada.freeOwnedString(maybe_origin);
|
||||||
|
|
||||||
|
const origin = maybe_origin.data[0..maybe_origin.length];
|
||||||
|
return page.call_arena.dupe(u8, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_href(self: *const URL, page: *Page) ![]const u8 {
|
||||||
|
var w: Writer.Allocating = .init(page.arena);
|
||||||
|
|
||||||
|
// If URL is not valid, return immediately.
|
||||||
|
if (!ada.isValid(self.internal)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the earlier check passed, this can't be null.
|
||||||
|
const str = ada.getHrefNullable(self.internal);
|
||||||
|
const href = str.data[0..str.length];
|
||||||
|
// This can't be null either.
|
||||||
|
const comps = ada.getComponents(self.internal);
|
||||||
|
// If hash provided, we write it after we fit-in the search params.
|
||||||
|
const has_hash = comps.hash_start != ada.URLOmitted;
|
||||||
|
const href_part = if (has_hash) href[0..comps.hash_start] else href;
|
||||||
|
try w.writer.writeAll(href_part);
|
||||||
|
|
||||||
|
// Write search params if provided.
|
||||||
if (self.search_params.get_size() > 0) {
|
if (self.search_params.get_size() > 0) {
|
||||||
try aw.writer.writeByte('?');
|
try w.writer.writeByte('?');
|
||||||
try self.search_params.write(&aw.writer);
|
try self.search_params.write(&w.writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
// Write hash if provided before.
|
||||||
const fragment = uriComponentNullStr(self.uri.fragment);
|
const hash = self.get_hash();
|
||||||
if (fragment.len > 0) {
|
try w.writer.writeAll(hash);
|
||||||
try aw.writer.writeByte('#');
|
|
||||||
try aw.writer.writeAll(fragment);
|
return w.written();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_username(self: *const URL) []const u8 {
|
||||||
|
const username = ada.getUsernameNullable(self.internal);
|
||||||
|
if (username.data == null) {
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return aw.written();
|
return username.data[0..username.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_protocol(self: *const URL) []const u8 {
|
pub fn get_password(self: *const URL) []const u8 {
|
||||||
// std.Uri keeps a pointer to "https", "http" (scheme part) so we know
|
const password = ada.getPasswordNullable(self.internal);
|
||||||
// its followed by ':'.
|
if (password.data == null) {
|
||||||
const scheme = self.uri.scheme;
|
return "";
|
||||||
return scheme.ptr[0 .. scheme.len + 1];
|
}
|
||||||
|
|
||||||
|
return password.data[0..password.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_username(self: *URL) []const u8 {
|
pub fn get_port(self: *const URL) []const u8 {
|
||||||
return uriComponentNullStr(self.uri.user);
|
const port = ada.getPortNullable(self.internal);
|
||||||
|
if (port.data == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return port.data[0..port.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_password(self: *URL) []const u8 {
|
pub fn get_hash(self: *const URL) []const u8 {
|
||||||
return uriComponentNullStr(self.uri.password);
|
const hash = ada.getHashNullable(self.internal);
|
||||||
|
if (hash.data == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash.data[0..hash.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_host(self: *URL, page: *Page) ![]const u8 {
|
pub fn get_host(self: *const URL) []const u8 {
|
||||||
var aw = std.Io.Writer.Allocating.init(page.arena);
|
const host = ada.getHostNullable(self.internal);
|
||||||
try self.uri.writeToStream(&aw.writer, .{
|
if (host.data == null) {
|
||||||
.scheme = false,
|
return "";
|
||||||
.authentication = false,
|
}
|
||||||
.authority = true,
|
|
||||||
.path = false,
|
return host.data[0..host.length];
|
||||||
.query = false,
|
|
||||||
.fragment = false,
|
|
||||||
});
|
|
||||||
return aw.written();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hostname(self: *URL) []const u8 {
|
pub fn get_hostname(self: *const URL) []const u8 {
|
||||||
return uriComponentNullStr(self.uri.host);
|
const hostname = ada.getHostnameNullable(self.internal);
|
||||||
|
if (hostname.data == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostname.data[0..hostname.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_port(self: *URL, page: *Page) ![]const u8 {
|
pub fn get_pathname(self: *const URL) []const u8 {
|
||||||
const arena = page.arena;
|
const path = ada.getPathnameNullable(self.internal);
|
||||||
if (self.uri.port == null) return try arena.dupe(u8, "");
|
// Return a slash if path is null.
|
||||||
|
if (path.data == null) {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
var aw = std.Io.Writer.Allocating.init(arena);
|
return path.data[0..path.length];
|
||||||
try aw.writer.printInt(self.uri.port.?, 10, .lower, .{});
|
|
||||||
return aw.written();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_pathname(self: *URL) []const u8 {
|
/// get_search depends on the current state of `search_params`.
|
||||||
if (uriComponentStr(self.uri.path).len == 0) return "/";
|
pub fn get_search(self: *const URL, page: *Page) ![]const u8 {
|
||||||
return uriComponentStr(self.uri.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_search(self: *URL, page: *Page) ![]const u8 {
|
|
||||||
const arena = page.arena;
|
const arena = page.arena;
|
||||||
|
|
||||||
if (self.search_params.get_size() == 0) {
|
if (self.search_params.get_size() == 0) {
|
||||||
@@ -223,72 +278,104 @@ pub const URL = struct {
|
|||||||
return buf.items;
|
return buf.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_search(self: *URL, qs_: ?[]const u8, page: *Page) !void {
|
pub fn get_protocol(self: *const URL) []const u8 {
|
||||||
|
const protocol = ada.getProtocolNullable(self.internal);
|
||||||
|
if (protocol.data == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return protocol.data[0..protocol.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setters.
|
||||||
|
|
||||||
|
/// Ada-url don't define any errors, so we just prefer one unified
|
||||||
|
/// `Internal` error for failing cases.
|
||||||
|
const SetterError = error{Internal};
|
||||||
|
|
||||||
|
pub fn set_href(self: *URL, input: []const u8, page: *Page) !void {
|
||||||
|
_ = ada.setHref(self.internal, input);
|
||||||
|
if (!ada.isValid(self.internal)) return error.Internal;
|
||||||
|
// Can't call `get_search` here since it uses `search_params`.
|
||||||
|
self.search_params = try prepareSearchParams(page.arena, self.internal);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_host(self: *const URL, input: []const u8) SetterError!void {
|
||||||
|
_ = ada.setHost(self.internal, input);
|
||||||
|
if (!ada.isValid(self.internal)) return error.Internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_hostname(self: *const URL, input: []const u8) SetterError!void {
|
||||||
|
_ = ada.setHostname(self.internal, input);
|
||||||
|
if (!ada.isValid(self.internal)) return error.Internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_protocol(self: *const URL, input: []const u8) SetterError!void {
|
||||||
|
_ = ada.setProtocol(self.internal, input);
|
||||||
|
if (!ada.isValid(self.internal)) return error.Internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_username(self: *const URL, input: []const u8) SetterError!void {
|
||||||
|
_ = ada.setUsername(self.internal, input);
|
||||||
|
if (!ada.isValid(self.internal)) return error.Internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_password(self: *const URL, input: []const u8) SetterError!void {
|
||||||
|
_ = ada.setPassword(self.internal, input);
|
||||||
|
if (!ada.isValid(self.internal)) return error.Internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_port(self: *const URL, input: []const u8) SetterError!void {
|
||||||
|
_ = ada.setPort(self.internal, input);
|
||||||
|
if (!ada.isValid(self.internal)) return error.Internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_pathname(self: *const URL, input: []const u8) SetterError!void {
|
||||||
|
_ = ada.setPathname(self.internal, input);
|
||||||
|
if (!ada.isValid(self.internal)) return error.Internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_search(self: *URL, maybe_input: ?[]const u8, page: *Page) !void {
|
||||||
self.search_params = .{};
|
self.search_params = .{};
|
||||||
if (qs_) |qs| {
|
if (maybe_input) |input| {
|
||||||
self.search_params = try URLSearchParams.init(page.arena, qs);
|
self.search_params = try .initFromString(page.arena, input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hash(self: *URL, page: *Page) ![]const u8 {
|
pub fn set_hash(self: *const URL, input: []const u8) !void {
|
||||||
const arena = page.arena;
|
ada.setHash(self.internal, input);
|
||||||
if (self.uri.fragment == null) return try arena.dupe(u8, "");
|
if (!ada.isValid(self.internal)) return error.Internal;
|
||||||
|
|
||||||
return try std.mem.concat(arena, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_searchParams(self: *URL) *URLSearchParams {
|
|
||||||
return &self.search_params;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _toJSON(self: *URL, page: *Page) ![]const u8 {
|
|
||||||
return self.get_href(page);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// uriComponentNullStr converts an optional std.Uri.Component to string value.
|
|
||||||
// The string value can be undecoded.
|
|
||||||
fn uriComponentNullStr(c: ?std.Uri.Component) []const u8 {
|
|
||||||
if (c == null) return "";
|
|
||||||
|
|
||||||
return uriComponentStr(c.?);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn uriComponentStr(c: std.Uri.Component) []const u8 {
|
|
||||||
return switch (c) {
|
|
||||||
.raw => |v| v,
|
|
||||||
.percent_encoded => |v| v,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://url.spec.whatwg.org/#interface-urlsearchparams
|
|
||||||
pub const URLSearchParams = struct {
|
pub const URLSearchParams = struct {
|
||||||
entries: kv.List = .{},
|
entries: kv.List = .{},
|
||||||
|
|
||||||
const URLSearchParamsOpts = union(enum) {
|
pub const ConstructorOptions = union(enum) {
|
||||||
qs: []const u8,
|
query_string: []const u8,
|
||||||
form_data: *const FormData,
|
form_data: *const FormData,
|
||||||
js_obj: js.Object,
|
object: js.Object,
|
||||||
};
|
};
|
||||||
pub fn constructor(opts_: ?URLSearchParamsOpts, page: *Page) !URLSearchParams {
|
|
||||||
const opts = opts_ orelse return .{ .entries = .{} };
|
|
||||||
return switch (opts) {
|
|
||||||
.qs => |qs| init(page.arena, qs),
|
|
||||||
.form_data => |fd| .{ .entries = try fd.entries.clone(page.arena) },
|
|
||||||
.js_obj => |js_obj| {
|
|
||||||
const arena = page.arena;
|
|
||||||
var it = js_obj.nameIterator();
|
|
||||||
|
|
||||||
var entries: kv.List = .{};
|
pub fn constructor(maybe_options: ?ConstructorOptions, page: *Page) !URLSearchParams {
|
||||||
|
const options = maybe_options orelse return .{};
|
||||||
|
|
||||||
|
const arena = page.arena;
|
||||||
|
return switch (options) {
|
||||||
|
.query_string => |string| .{ .entries = try parseQuery(arena, string) },
|
||||||
|
.form_data => |form_data| .{ .entries = try form_data.entries.clone(arena) },
|
||||||
|
.object => |object| {
|
||||||
|
var it = object.nameIterator();
|
||||||
|
|
||||||
|
var entries = kv.List{};
|
||||||
try entries.ensureTotalCapacity(arena, it.count);
|
try entries.ensureTotalCapacity(arena, it.count);
|
||||||
|
|
||||||
while (try it.next()) |js_name| {
|
while (try it.next()) |js_name| {
|
||||||
const name = try js_name.toString(arena);
|
const name = try js_name.toString(arena);
|
||||||
const js_val = try js_obj.get(name);
|
const js_value = try object.get(name);
|
||||||
entries.appendOwnedAssumeCapacity(
|
const value = try js_value.toString(arena);
|
||||||
name,
|
|
||||||
try js_val.toString(arena),
|
entries.appendOwnedAssumeCapacity(name, value);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return .{ .entries = entries };
|
return .{ .entries = entries };
|
||||||
@@ -296,10 +383,9 @@ pub const URLSearchParams = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(arena: Allocator, qs_: ?[]const u8) !URLSearchParams {
|
/// Initializes URLSearchParams from a query string.
|
||||||
return .{
|
pub fn initFromString(arena: Allocator, query_string: []const u8) !URLSearchParams {
|
||||||
.entries = if (qs_) |qs| try parseQuery(arena, qs) else .{},
|
return .{ .entries = try parseQuery(arena, query_string) };
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_size(self: *const URLSearchParams) u32 {
|
pub fn get_size(self: *const URLSearchParams) u32 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -632,6 +646,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// debugger events
|
||||||
|
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {
|
||||||
|
// onRunMessageLoopOnPause is called when a breakpoint is hit.
|
||||||
|
// Until quit pause, we must continue to run a nested message loop
|
||||||
|
// to interact with the the debugger ony (ie. Chrome DevTools).
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {
|
||||||
|
// Quit breakpoint pause.
|
||||||
|
}
|
||||||
|
|
||||||
// This is hacky x 2. First, we create the JSON payload by gluing our
|
// This is hacky x 2. First, we create the JSON payload by gluing our
|
||||||
// session_id onto it. Second, we're much more client/websocket aware than
|
// session_id onto it. Second, we're much more client/websocket aware than
|
||||||
// we should be.
|
// we should be.
|
||||||
@@ -702,7 +727,7 @@ const IsolatedWorld = struct {
|
|||||||
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
|
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
|
||||||
// (assuming grantUniveralAccess will be set to True!).
|
// (assuming grantUniveralAccess will be set to True!).
|
||||||
// We just created the world and the page. The page's state lives in the session, but is update on navigation.
|
// We just created the world and the page. The page's state lives in the session, but is update on navigation.
|
||||||
// This also means this pointer becomes invalid after removePage untill a new page is created.
|
// This also means this pointer becomes invalid after removePage until a new page is created.
|
||||||
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
|
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
|
||||||
pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
|
pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
|
||||||
// if (self.executor.context != null) return error.Only1IsolatedContextSupported;
|
// if (self.executor.context != null) return error.Only1IsolatedContextSupported;
|
||||||
|
|||||||
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, .{});
|
||||||
|
}
|
||||||
@@ -38,16 +38,24 @@ const DEV_TOOLS_WINDOW_ID = 1923710101;
|
|||||||
pub fn processMessage(cmd: anytype) !void {
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
getVersion,
|
getVersion,
|
||||||
setDownloadBehavior,
|
setPermission,
|
||||||
getWindowForTarget,
|
|
||||||
setWindowBounds,
|
setWindowBounds,
|
||||||
|
resetPermissions,
|
||||||
|
grantPermissions,
|
||||||
|
getWindowForTarget,
|
||||||
|
setDownloadBehavior,
|
||||||
|
close,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
.getVersion => return getVersion(cmd),
|
.getVersion => return getVersion(cmd),
|
||||||
.setDownloadBehavior => return setDownloadBehavior(cmd),
|
.setPermission => return setPermission(cmd),
|
||||||
.getWindowForTarget => return getWindowForTarget(cmd),
|
|
||||||
.setWindowBounds => return setWindowBounds(cmd),
|
.setWindowBounds => return setWindowBounds(cmd),
|
||||||
|
.resetPermissions => return resetPermissions(cmd),
|
||||||
|
.grantPermissions => return grantPermissions(cmd),
|
||||||
|
.getWindowForTarget => return getWindowForTarget(cmd),
|
||||||
|
.setDownloadBehavior => return setDownloadBehavior(cmd),
|
||||||
|
.close => return cmd.sendResult(null, .{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +97,21 @@ fn setWindowBounds(cmd: anytype) !void {
|
|||||||
return cmd.sendResult(null, .{});
|
return cmd.sendResult(null, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: noop method
|
||||||
|
fn grantPermissions(cmd: anytype) !void {
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: noop method
|
||||||
|
fn setPermission(cmd: anytype) !void {
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: noop method
|
||||||
|
fn resetPermissions(cmd: anytype) !void {
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
test "cdp.browser: getVersion" {
|
test "cdp.browser: getVersion" {
|
||||||
var ctx = testing.context();
|
var ctx = testing.context();
|
||||||
|
|||||||
@@ -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" {
|
||||||
@@ -663,11 +700,11 @@ test "cdp.dom: getBoxModel" {
|
|||||||
.params = .{ .nodeId = 6 },
|
.params = .{ .nodeId = 6 },
|
||||||
});
|
});
|
||||||
try ctx.expectSentResult(.{ .model = BoxModel{
|
try ctx.expectSentResult(.{ .model = BoxModel{
|
||||||
.content = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
|
.content = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
|
||||||
.padding = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
|
.padding = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
|
||||||
.border = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
|
.border = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
|
||||||
.margin = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 },
|
.margin = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
|
||||||
.width = 1,
|
.width = 5,
|
||||||
.height = 1,
|
.height = 5,
|
||||||
} }, .{ .id = 5 });
|
} }, .{ .id = 5 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
dispatchKeyEvent,
|
dispatchKeyEvent,
|
||||||
dispatchMouseEvent,
|
dispatchMouseEvent,
|
||||||
|
insertText,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
.dispatchKeyEvent => return dispatchKeyEvent(cmd),
|
.dispatchKeyEvent => return dispatchKeyEvent(cmd),
|
||||||
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
|
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
|
||||||
|
.insertText => return insertText(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +117,20 @@ fn dispatchMouseEvent(cmd: anytype) !void {
|
|||||||
// result already sent
|
// result already sent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText
|
||||||
|
fn insertText(cmd: anytype) !void {
|
||||||
|
const params = (try cmd.params(struct {
|
||||||
|
text: []const u8, // The text to insert
|
||||||
|
})) orelse return error.InvalidParams;
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return;
|
||||||
|
const page = bc.session.currentPage() orelse return;
|
||||||
|
|
||||||
|
try page.insertText(params.text);
|
||||||
|
|
||||||
|
try cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|
||||||
fn clickNavigate(cmd: anytype, uri: std.Uri) !void {
|
fn clickNavigate(cmd: anytype, uri: std.Uri) !void {
|
||||||
const bc = cmd.browser_context.?;
|
const bc = cmd.browser_context.?;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
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 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;
|
||||||
|
|
||||||
@@ -31,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) {
|
||||||
@@ -41,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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,11 +86,33 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void {
|
|||||||
})) orelse return error.InvalidParams;
|
})) orelse return error.InvalidParams;
|
||||||
|
|
||||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
if (params.enabled) {
|
|
||||||
try bc.lifecycleEventsEnable();
|
if (params.enabled == false) {
|
||||||
} else {
|
|
||||||
bc.lifecycleEventsDisable();
|
bc.lifecycleEventsDisable();
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable lifecycle events.
|
||||||
|
try bc.lifecycleEventsEnable();
|
||||||
|
|
||||||
|
// When we enable lifecycle events, we must dispatch events for all
|
||||||
|
// attached targets.
|
||||||
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
|
|
||||||
|
if (page.load_state == .complete) {
|
||||||
|
try sendPageLifecycle(bc, "DOMContentLoaded", timestampF());
|
||||||
|
try sendPageLifecycle(bc, "load", timestampF());
|
||||||
|
|
||||||
|
const http_active = page.http_client.active;
|
||||||
|
const total_network_activity = http_active + page.http_client.intercepted;
|
||||||
|
if (page.notified_network_almost_idle.check(total_network_activity <= 2)) {
|
||||||
|
try sendPageLifecycle(bc, "networkAlmostIdle", timestampF());
|
||||||
|
}
|
||||||
|
if (page.notified_network_idle.check(total_network_activity == 0)) {
|
||||||
|
try sendPageLifecycle(bc, "networkIdle", timestampF());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return cmd.sendResult(null, .{});
|
return cmd.sendResult(null, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,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.
|
||||||
}
|
}
|
||||||
@@ -152,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,
|
||||||
@@ -165,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();
|
||||||
@@ -174,7 +235,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
|||||||
var cdp = bc.cdp;
|
var cdp = bc.cdp;
|
||||||
const reason_: ?[]const u8 = switch (event.opts.reason) {
|
const reason_: ?[]const u8 = switch (event.opts.reason) {
|
||||||
.anchor => "anchorClick",
|
.anchor => "anchorClick",
|
||||||
.script, .history => "scriptInitiated",
|
.script, .history, .navigation => "scriptInitiated",
|
||||||
.form => switch (event.opts.method) {
|
.form => switch (event.opts.method) {
|
||||||
.GET => "formSubmissionGet",
|
.GET => "formSubmissionGet",
|
||||||
.POST => "formSubmissionPost",
|
.POST => "formSubmissionPost",
|
||||||
@@ -210,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
|
||||||
@@ -236,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,
|
||||||
@@ -269,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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,9 +21,48 @@ const std = @import("std");
|
|||||||
pub fn processMessage(cmd: anytype) !void {
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
enable,
|
enable,
|
||||||
|
setIgnoreCertificateErrors,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
.enable => return cmd.sendResult(null, .{}),
|
.enable => return cmd.sendResult(null, .{}),
|
||||||
|
.setIgnoreCertificateErrors => return setIgnoreCertificateErrors(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setIgnoreCertificateErrors(cmd: anytype) !void {
|
||||||
|
const params = (try cmd.params(struct {
|
||||||
|
ignore: bool,
|
||||||
|
})) orelse return error.InvalidParams;
|
||||||
|
|
||||||
|
if (params.ignore) {
|
||||||
|
try cmd.cdp.browser.http_client.disableTlsVerify();
|
||||||
|
} else {
|
||||||
|
try cmd.cdp.browser.http_client.enableTlsVerify();
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
|
||||||
|
test "cdp.Security: setIgnoreCertificateErrors" {
|
||||||
|
var ctx = testing.context();
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
_ = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||||
|
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 8,
|
||||||
|
.method = "Security.setIgnoreCertificateErrors",
|
||||||
|
.params = .{ .ignore = true },
|
||||||
|
});
|
||||||
|
try ctx.expectSentResult(null, .{ .id = 8 });
|
||||||
|
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 9,
|
||||||
|
.method = "Security.setIgnoreCertificateErrors",
|
||||||
|
.params = .{ .ignore = false },
|
||||||
|
});
|
||||||
|
try ctx.expectSentResult(null, .{ .id = 9 });
|
||||||
|
}
|
||||||
|
|||||||
@@ -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| {
|
||||||
@@ -109,7 +136,7 @@ fn disposeBrowserContext(cmd: anytype) !void {
|
|||||||
|
|
||||||
fn createTarget(cmd: anytype) !void {
|
fn createTarget(cmd: anytype) !void {
|
||||||
const params = (try cmd.params(struct {
|
const params = (try cmd.params(struct {
|
||||||
// url: []const u8,
|
url: []const u8 = "about:blank",
|
||||||
// width: ?u64 = null,
|
// width: ?u64 = null,
|
||||||
// height: ?u64 = null,
|
// height: ?u64 = null,
|
||||||
browserContextId: ?[]const u8 = null,
|
browserContextId: ?[]const u8 = null,
|
||||||
@@ -178,6 +205,12 @@ fn createTarget(cmd: anytype) !void {
|
|||||||
try doAttachtoTarget(cmd, target_id);
|
try doAttachtoTarget(cmd, target_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!std.mem.eql(u8, "about:blank", params.url)) {
|
||||||
|
try page.navigate(params.url, .{
|
||||||
|
.reason = .address_bar,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try cmd.sendResult(.{
|
try cmd.sendResult(.{
|
||||||
.targetId = target_id,
|
.targetId = target_id,
|
||||||
}, .{});
|
}, .{});
|
||||||
@@ -195,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 },
|
||||||
@@ -265,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,
|
||||||
},
|
},
|
||||||
@@ -277,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,
|
||||||
},
|
},
|
||||||
@@ -517,7 +548,7 @@ test "cdp.target: createTarget" {
|
|||||||
{
|
{
|
||||||
var ctx = testing.context();
|
var ctx = testing.context();
|
||||||
defer ctx.deinit();
|
defer ctx.deinit();
|
||||||
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about/blank" } });
|
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about:blank" } });
|
||||||
|
|
||||||
// should create a browser context
|
// should create a browser context
|
||||||
const bc = ctx.cdp().browser_context.?;
|
const bc = ctx.cdp().browser_context.?;
|
||||||
@@ -529,7 +560,7 @@ test "cdp.target: createTarget" {
|
|||||||
defer ctx.deinit();
|
defer ctx.deinit();
|
||||||
// active auto attach to get the Target.attachedToTarget event.
|
// active auto attach to get the Target.attachedToTarget event.
|
||||||
try ctx.processMessage(.{ .id = 9, .method = "Target.setAutoAttach", .params = .{ .autoAttach = true, .waitForDebuggerOnStart = false } });
|
try ctx.processMessage(.{ .id = 9, .method = "Target.setAutoAttach", .params = .{ .autoAttach = true, .waitForDebuggerOnStart = false } });
|
||||||
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about/blank" } });
|
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about:blank" } });
|
||||||
|
|
||||||
// should create a browser context
|
// should create a browser context
|
||||||
const bc = ctx.cdp().browser_context.?;
|
const bc = ctx.cdp().browser_context.?;
|
||||||
@@ -624,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,
|
||||||
},
|
},
|
||||||
@@ -658,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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ notification: ?*Notification = null,
|
|||||||
// restoring, this originally-configured value is what it goes to.
|
// restoring, this originally-configured value is what it goes to.
|
||||||
http_proxy: ?[:0]const u8 = null,
|
http_proxy: ?[:0]const u8 = null,
|
||||||
|
|
||||||
|
// track if the client use a proxy for connections.
|
||||||
|
// We can't use http_proxy because we want also to track proxy configured via
|
||||||
|
// CDP.
|
||||||
|
use_proxy: bool,
|
||||||
|
|
||||||
// The complete user-agent header line
|
// The complete user-agent header line
|
||||||
user_agent: [:0]const u8,
|
user_agent: [:0]const u8,
|
||||||
|
|
||||||
@@ -126,6 +131,7 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie
|
|||||||
.handles = handles,
|
.handles = handles,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.http_proxy = opts.http_proxy,
|
.http_proxy = opts.http_proxy,
|
||||||
|
.use_proxy = opts.http_proxy != null,
|
||||||
.user_agent = opts.user_agent,
|
.user_agent = opts.user_agent,
|
||||||
.transfer_pool = transfer_pool,
|
.transfer_pool = transfer_pool,
|
||||||
};
|
};
|
||||||
@@ -255,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();
|
||||||
|
|
||||||
@@ -267,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,
|
||||||
@@ -315,6 +330,7 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void {
|
|||||||
for (self.handles.handles) |*h| {
|
for (self.handles.handles) |*h| {
|
||||||
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
|
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
|
||||||
}
|
}
|
||||||
|
self.use_proxy = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same restriction as changeProxy. Should be ok since this is only called on
|
// Same restriction as changeProxy. Should be ok since this is only called on
|
||||||
@@ -326,6 +342,37 @@ pub fn restoreOriginalProxy(self: *Client) !void {
|
|||||||
for (self.handles.handles) |*h| {
|
for (self.handles.handles) |*h| {
|
||||||
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy));
|
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy));
|
||||||
}
|
}
|
||||||
|
self.use_proxy = proxy != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable TLS verification on all connections.
|
||||||
|
pub fn enableTlsVerify(self: *const Client) !void {
|
||||||
|
for (self.handles.handles) |*h| {
|
||||||
|
const easy = h.conn.easy;
|
||||||
|
|
||||||
|
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 2)));
|
||||||
|
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 1)));
|
||||||
|
|
||||||
|
if (self.use_proxy) {
|
||||||
|
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 2)));
|
||||||
|
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable TLS verification on all connections.
|
||||||
|
pub fn disableTlsVerify(self: *const Client) !void {
|
||||||
|
for (self.handles.handles) |*h| {
|
||||||
|
const easy = h.conn.easy;
|
||||||
|
|
||||||
|
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0)));
|
||||||
|
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0)));
|
||||||
|
|
||||||
|
if (self.use_proxy) {
|
||||||
|
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 0)));
|
||||||
|
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
|
fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
|
||||||
@@ -641,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",
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -808,7 +868,7 @@ pub const Transfer = struct {
|
|||||||
self.deinit();
|
self.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// abortAuthChallenge is called when an auth chanllenge interception is
|
// abortAuthChallenge is called when an auth challenge interception is
|
||||||
// abort. We don't call self.client.endTransfer here b/c it has been done
|
// abort. We don't call self.client.endTransfer here b/c it has been done
|
||||||
// before interception process.
|
// before interception process.
|
||||||
pub fn abortAuthChallenge(self: *Transfer) void {
|
pub fn abortAuthChallenge(self: *Transfer) void {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub const Scope = enum {
|
|||||||
fetch,
|
fetch,
|
||||||
polyfill,
|
polyfill,
|
||||||
interceptor,
|
interceptor,
|
||||||
|
input,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Opts = struct {
|
const Opts = struct {
|
||||||
|
|||||||
78
src/main.zig
78
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| {
|
||||||
@@ -373,7 +347,11 @@ const Command = struct {
|
|||||||
\\ Defaults to
|
\\ Defaults to
|
||||||
++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++
|
++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++
|
||||||
\\
|
\\
|
||||||
\\ --user_agent_suffix
|
\\--log_filter_scopes
|
||||||
|
\\ Filter out too verbose logs per scope:
|
||||||
|
\\ http, unknown_prop, script_event, ...
|
||||||
|
\\
|
||||||
|
\\--user_agent_suffix
|
||||||
\\ Suffix to append to the Lightpanda/X.Y User-Agent
|
\\ Suffix to append to the Lightpanda/X.Y User-Agent
|
||||||
\\
|
\\
|
||||||
;
|
;
|
||||||
@@ -884,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;
|
||||||
@@ -487,7 +512,7 @@ pub const Client = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// called by CDP
|
// called by CDP
|
||||||
// Websocket frames have a variable lenght header. For server-client,
|
// Websocket frames have a variable length header. For server-client,
|
||||||
// it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have
|
// it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have
|
||||||
// writev, so we need to get creative. We'll JSON serialize to a
|
// writev, so we need to get creative. We'll JSON serialize to a
|
||||||
// buffer, where the first 10 bytes are reserved. We can then backfill
|
// buffer, where the first 10 bytes are reserved. We can then backfill
|
||||||
|
|||||||
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -402,19 +402,13 @@ pub fn htmlRunner(file: []const u8) !void {
|
|||||||
|
|
||||||
const url = try std.fmt.allocPrint(arena_allocator, "http://localhost:9582/src/tests/{s}", .{file});
|
const url = try std.fmt.allocPrint(arena_allocator, "http://localhost:9582/src/tests/{s}", .{file});
|
||||||
try page.navigate(url, .{});
|
try page.navigate(url, .{});
|
||||||
_ = page.wait(2000);
|
test_session.fetchWait(2000);
|
||||||
|
|
||||||
// page exits more aggressively in tests. We want to make sure this is called
|
// page exits more aggressively in tests. We want to make sure this is called
|
||||||
// at lease once.
|
// at lease once.
|
||||||
page.session.browser.runMicrotasks();
|
page.session.browser.runMicrotasks();
|
||||||
page.session.browser.runMessageLoop();
|
page.session.browser.runMessageLoop();
|
||||||
|
|
||||||
const needs_second_wait = try js_context.exec("testing._onPageWait.length > 0", "check_onPageWait");
|
|
||||||
if (needs_second_wait.value.toBool(page.js.isolate)) {
|
|
||||||
// sets the isSecondWait flag in testing.
|
|
||||||
_ = js_context.exec("testing._isSecondWait = true", "set_second_wait_flag") catch {};
|
|
||||||
_ = page.wait(2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
@import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start);
|
@import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start);
|
||||||
|
|
||||||
const value = js_context.exec("testing.getStatus()", "testing.getStatus()") catch |err| {
|
const value = js_context.exec("testing.getStatus()", "testing.getStatus()") catch |err| {
|
||||||
|
|||||||
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>
|
||||||
@@ -168,35 +168,35 @@
|
|||||||
|
|
||||||
<script id=dimensions>
|
<script id=dimensions>
|
||||||
const para = document.getElementById('para');
|
const para = document.getElementById('para');
|
||||||
testing.expectEqual(1, para.clientWidth);
|
testing.expectEqual(5, para.clientWidth);
|
||||||
testing.expectEqual(1, para.clientHeight);
|
testing.expectEqual(5, para.clientHeight);
|
||||||
|
|
||||||
// let r1 = document.getElementById('para').getBoundingClientRect();
|
let r1 = document.getElementById('para').getBoundingClientRect();
|
||||||
// testing.expectEqual(0, r1.x);
|
testing.expectEqual(0, r1.x);
|
||||||
// testing.expectEqual(0, r1.y);
|
testing.expectEqual(0, r1.y);
|
||||||
// testing.expectEqual(1, r1.width);
|
testing.expectEqual(5, r1.width);
|
||||||
// testing.expectEqual(2, r1.height);
|
testing.expectEqual(5, r1.height);
|
||||||
|
|
||||||
// let r2 = document.getElementById('content').getBoundingClientRect();
|
let r2 = document.getElementById('content').getBoundingClientRect();
|
||||||
// testing.expectEqual(1, r2.x);
|
testing.expectEqual(5, r2.x);
|
||||||
// testing.expectEqual(0, r2.y);
|
testing.expectEqual(0, r2.y);
|
||||||
// testing.expectEqual(1, r2.width);
|
testing.expectEqual(5, r2.width);
|
||||||
// testing.expectEqual(1, r2.height);
|
testing.expectEqual(5, r2.height);
|
||||||
|
|
||||||
// let r3 = document.getElementById('para').getBoundingClientRect();
|
let r3 = document.getElementById('para').getBoundingClientRect();
|
||||||
// testing.expectEqual(0, r3.x);
|
testing.expectEqual(0, r3.x);
|
||||||
// testing.expectEqual(0, r3.y);
|
testing.expectEqual(0, r3.y);
|
||||||
// testing.expectEqual(1, r3.width);
|
testing.expectEqual(5, r3.width);
|
||||||
// testing.expectEqual(1, r3.height);
|
testing.expectEqual(5, r3.height);
|
||||||
|
|
||||||
// testing.expectEqual(1, para.clientWidth);
|
testing.expectEqual(10, para.clientWidth);
|
||||||
// testing.expectEqual(1, para.clientHeight);
|
testing.expectEqual(5, para.clientHeight);
|
||||||
|
|
||||||
// let r4 = document.createElement('div').getBoundingClientRect();
|
let r4 = document.createElement('div').getBoundingClientRect();
|
||||||
// testing.expectEqual(0, r4.x);
|
testing.expectEqual(0, r4.x);
|
||||||
// testing.expectEqual(0, r4.y);
|
testing.expectEqual(0, r4.y);
|
||||||
// testing.expectEqual(0, r4.width);
|
testing.expectEqual(0, r4.width);
|
||||||
// testing.expectEqual(0, r4.height);
|
testing.expectEqual(0, r4.height);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=matches>
|
<script id=matches>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -122,13 +122,13 @@
|
|||||||
testing.expectEqual(1, entry.intersectionRatio);
|
testing.expectEqual(1, entry.intersectionRatio);
|
||||||
testing.expectEqual(0, entry.intersectionRect.x);
|
testing.expectEqual(0, entry.intersectionRect.x);
|
||||||
testing.expectEqual(0, entry.intersectionRect.y);
|
testing.expectEqual(0, entry.intersectionRect.y);
|
||||||
testing.expectEqual(1, entry.intersectionRect.width);
|
testing.expectEqual(5, entry.intersectionRect.width);
|
||||||
testing.expectEqual(1, entry.intersectionRect.height);
|
testing.expectEqual(5, entry.intersectionRect.height);
|
||||||
testing.expectEqual(true, entry.isIntersecting);
|
testing.expectEqual(true, entry.isIntersecting);
|
||||||
testing.expectEqual(0, entry.rootBounds.x);
|
testing.expectEqual(0, entry.rootBounds.x);
|
||||||
testing.expectEqual(0, entry.rootBounds.y);
|
testing.expectEqual(0, entry.rootBounds.y);
|
||||||
testing.expectEqual(1, entry.rootBounds.width);
|
testing.expectEqual(5, entry.rootBounds.width);
|
||||||
testing.expectEqual(1, entry.rootBounds.height);
|
testing.expectEqual(5, entry.rootBounds.height);
|
||||||
testing.expectEqual('[object HTMLDivElement]', entry.target.toString());
|
testing.expectEqual('[object HTMLDivElement]', entry.target.toString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<p id="para"> And</p>
|
<p id="para"> And</p>
|
||||||
<!--comment-->
|
<!--comment-->
|
||||||
</div>
|
</div>
|
||||||
|
<div id="rootNodeComposed"></div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
@@ -36,6 +37,26 @@ let first_child = content.firstChild.nextSibling; // nextSibling because of line
|
|||||||
testing.expectEqual('HTMLDocument', content.getRootNode().__proto__.constructor.name);
|
testing.expectEqual('HTMLDocument', content.getRootNode().__proto__.constructor.name);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=getRootNodeComposed>
|
||||||
|
const testContainer = $('#rootNodeComposed');
|
||||||
|
const shadowHost = document.createElement('div');
|
||||||
|
testContainer.appendChild(shadowHost);
|
||||||
|
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
|
||||||
|
const shadowChild = document.createElement('span');
|
||||||
|
shadowRoot.appendChild(shadowChild);
|
||||||
|
|
||||||
|
testing.expectEqual('ShadowRoot', shadowChild.getRootNode().__proto__.constructor.name);
|
||||||
|
testing.expectEqual('ShadowRoot', shadowChild.getRootNode({ composed: false }).__proto__.constructor.name);
|
||||||
|
testing.expectEqual('HTMLDocument', shadowChild.getRootNode({ composed: true }).__proto__.constructor.name);
|
||||||
|
testing.expectEqual('HTMLDocument', shadowHost.getRootNode().__proto__.constructor.name);
|
||||||
|
|
||||||
|
const disconnected = document.createElement('div');
|
||||||
|
const disconnectedChild = document.createElement('span');
|
||||||
|
disconnected.appendChild(disconnectedChild);
|
||||||
|
testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode().__proto__.constructor.name);
|
||||||
|
testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode({ composed: true }).__proto__.constructor.name);
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id=firstChild>
|
<script id=firstChild>
|
||||||
let body_first_child = document.body.firstChild;
|
let body_first_child = document.body.firstChild;
|
||||||
testing.expectEqual('div', body_first_child.localName);
|
testing.expectEqual('div', body_first_child.localName);
|
||||||
|
|||||||
36
src/tests/events/composition.html
Normal file
36
src/tests/events/composition.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=noNata>
|
||||||
|
{
|
||||||
|
let event = new CompositionEvent("test", {});
|
||||||
|
testing.expectEqual(true, event instanceof CompositionEvent);
|
||||||
|
testing.expectEqual(true, event instanceof Event);
|
||||||
|
|
||||||
|
testing.expectEqual("test", event.type);
|
||||||
|
testing.expectEqual("", event.data);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=withData>
|
||||||
|
{
|
||||||
|
let event = new CompositionEvent("test2", {data: "over 9000!"});
|
||||||
|
testing.expectEqual("test2", event.type);
|
||||||
|
testing.expectEqual("over 9000!", event.data);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=dispatch>
|
||||||
|
{
|
||||||
|
let called = 0;
|
||||||
|
document.addEventListener('CE', (e) => {
|
||||||
|
testing.expectEqual('test-data', e.data);
|
||||||
|
testing.expectEqual(true, e instanceof CompositionEvent);
|
||||||
|
called += 1
|
||||||
|
});
|
||||||
|
|
||||||
|
document.dispatchEvent(new CompositionEvent('CE', {data: 'test-data'}));
|
||||||
|
testing.expectEqual(1, called);
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
|||||||
125
src/tests/file/blob.html
Normal file
125
src/tests/file/blob.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=Blob/Blob.text>
|
||||||
|
{
|
||||||
|
const parts = ["\r\nthe quick brown\rfo\rx\r", "\njumps over\r\nthe\nlazy\r", "\ndog"];
|
||||||
|
// "transparent" ending should not modify the final buffer.
|
||||||
|
const blob = new Blob(parts, { type: "text/html" });
|
||||||
|
|
||||||
|
const expected = parts.join("");
|
||||||
|
testing.expectEqual(expected.length, blob.size);
|
||||||
|
testing.expectEqual("text/html", blob.type);
|
||||||
|
testing.async(blob.text(), result => testing.expectEqual(expected, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const parts = ["\rhello\r", "\nwor\r\nld"];
|
||||||
|
// "native" ending should modify the final buffer.
|
||||||
|
const blob = new Blob(parts, { endings: "native" });
|
||||||
|
|
||||||
|
const expected = "\nhello\n\nwor\nld";
|
||||||
|
testing.expectEqual(expected.length, blob.size);
|
||||||
|
testing.async(blob.text(), result => testing.expectEqual(expected, result));
|
||||||
|
|
||||||
|
testing.async(blob.arrayBuffer(), result => testing.expectEqual(true, result instanceof ArrayBuffer));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=Blob.stream>
|
||||||
|
{
|
||||||
|
const parts = ["may", "thy", "knife", "chip", "and", "shatter"];
|
||||||
|
const blob = new Blob(parts);
|
||||||
|
const reader = blob.stream().getReader();
|
||||||
|
|
||||||
|
testing.async(reader.read(), ({ done, value }) => {
|
||||||
|
const expected = new Uint8Array([109, 97, 121, 116, 104, 121, 107, 110,
|
||||||
|
105, 102, 101, 99, 104, 105, 112, 97,
|
||||||
|
110, 100, 115, 104, 97, 116, 116, 101,
|
||||||
|
114]);
|
||||||
|
testing.expectEqual(false, done);
|
||||||
|
testing.expectEqual(true, value instanceof Uint8Array);
|
||||||
|
testing.expectEqual(expected, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=Blob.arrayBuffer/Blob.slice>
|
||||||
|
{
|
||||||
|
const parts = ["la", "symphonie", "des", "éclairs"];
|
||||||
|
const blob = new Blob(parts);
|
||||||
|
testing.async(blob.arrayBuffer(), result => testing.expectEqual(true, result instanceof ArrayBuffer));
|
||||||
|
|
||||||
|
let temp = blob.slice(0);
|
||||||
|
testing.expectEqual(blob.size, temp.size);
|
||||||
|
testing.async(temp.text(), result => {
|
||||||
|
testing.expectEqual("lasymphoniedeséclairs", result);
|
||||||
|
});
|
||||||
|
|
||||||
|
temp = blob.slice(-4, -2, "custom");
|
||||||
|
testing.expectEqual(2, temp.size);
|
||||||
|
testing.expectEqual("custom", temp.type);
|
||||||
|
testing.async(temp.text(), result => testing.expectEqual("ai", result));
|
||||||
|
|
||||||
|
temp = blob.slice(14);
|
||||||
|
testing.expectEqual(8, temp.size);
|
||||||
|
testing.async(temp.text(), result => testing.expectEqual("éclairs", result));
|
||||||
|
|
||||||
|
temp = blob.slice(6, -10, "text/eclair");
|
||||||
|
testing.expectEqual(6, temp.size);
|
||||||
|
testing.expectEqual("text/eclair", temp.type);
|
||||||
|
testing.async(temp.text(), result => testing.expectEqual("honied", result));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Firefox and Safari only -->
|
||||||
|
<script id=Blob.bytes>
|
||||||
|
{
|
||||||
|
const parts = ["light ", "panda ", "rocks ", "!"];
|
||||||
|
const blob = new Blob(parts);
|
||||||
|
|
||||||
|
testing.async(blob.bytes(), result => {
|
||||||
|
const expected = new Uint8Array([108, 105, 103, 104, 116, 32, 112, 97,
|
||||||
|
110, 100, 97, 32, 114, 111, 99, 107, 115,
|
||||||
|
32, 33]);
|
||||||
|
testing.expectEqual(true, result instanceof Uint8Array);
|
||||||
|
testing.expectEqual(expected, result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for SIMD.
|
||||||
|
{
|
||||||
|
const parts = [
|
||||||
|
"\rThe opened package\r\nof potato\nchi\rps",
|
||||||
|
"held the\r\nanswer to the\r mystery. Both det\rectives looke\r\rd\r",
|
||||||
|
"\rat it but failed to realize\nit was\r\nthe\rkey\r\n",
|
||||||
|
"\r\nto solve the \rcrime.\r"
|
||||||
|
];
|
||||||
|
|
||||||
|
const blob = new Blob(parts, { type: "text/html", endings: "native" });
|
||||||
|
testing.expectEqual(161, blob.size);
|
||||||
|
testing.expectEqual("text/html", blob.type);
|
||||||
|
testing.async(blob.bytes(), result => {
|
||||||
|
const expected = new Uint8Array([10, 84, 104, 101, 32, 111, 112, 101, 110,
|
||||||
|
101, 100, 32, 112, 97, 99, 107, 97, 103,
|
||||||
|
101, 10, 111, 102, 32, 112, 111, 116, 97,
|
||||||
|
116, 111, 10, 99, 104, 105, 10, 112, 115,
|
||||||
|
104, 101, 108, 100, 32, 116, 104, 101, 10,
|
||||||
|
97, 110, 115, 119, 101, 114, 32, 116, 111,
|
||||||
|
32, 116, 104, 101, 10, 32, 109, 121, 115,
|
||||||
|
116, 101, 114, 121, 46, 32, 66, 111, 116,
|
||||||
|
104, 32, 100, 101, 116, 10, 101, 99, 116,
|
||||||
|
105, 118, 101, 115, 32, 108, 111, 111, 107,
|
||||||
|
101, 10, 10, 100, 10, 10, 97, 116, 32, 105,
|
||||||
|
116, 32, 98, 117, 116, 32, 102, 97, 105, 108,
|
||||||
|
101, 100, 32, 116, 111, 32, 114, 101, 97,
|
||||||
|
108, 105, 122, 101, 10, 105, 116, 32, 119, 97,
|
||||||
|
115, 10, 116, 104, 101, 10, 107, 101, 121,
|
||||||
|
10, 10, 116, 111, 32, 115, 111, 108, 118, 101,
|
||||||
|
32, 116, 104, 101, 32, 10, 99, 114, 105, 109,
|
||||||
|
101, 46, 10]);
|
||||||
|
testing.expectEqual(true, result instanceof Uint8Array);
|
||||||
|
testing.expectEqual(expected, result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
<script id=file>
|
<script id=file>
|
||||||
let f = new File()
|
let f = new File();
|
||||||
testing.expectEqual(true, f instanceof File);
|
testing.expectEqual(true, f instanceof File);
|
||||||
</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>
|
||||||
@@ -46,15 +46,15 @@
|
|||||||
testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);
|
testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);
|
||||||
|
|
||||||
// Return null since we only return elements when they have previously been localized
|
// Return null since we only return elements when they have previously been localized
|
||||||
testing.expectEqual(null, document.elementFromPoint(0.5, 0.5));
|
testing.expectEqual(null, document.elementFromPoint(2.5, 2.5));
|
||||||
testing.expectEqual([], document.elementsFromPoint(0.5, 0.5));
|
testing.expectEqual([], document.elementsFromPoint(2.5, 2.5));
|
||||||
|
|
||||||
let div1 = document.createElement('div');
|
let div1 = document.createElement('div');
|
||||||
document.body.appendChild(div1);
|
document.body.appendChild(div1);
|
||||||
div1.getClientRects(); // clal this to position it
|
div1.getClientRects(); // clal this to position it
|
||||||
testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(0.5, 0.5).toString());
|
testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(2.5, 2.5).toString());
|
||||||
|
|
||||||
let elems = document.elementsFromPoint(0.5, 0.5);
|
let elems = document.elementsFromPoint(2.5, 2.5);
|
||||||
testing.expectEqual(3, elems.length);
|
testing.expectEqual(3, elems.length);
|
||||||
testing.expectEqual('[object HTMLDivElement]', elems[0].toString());
|
testing.expectEqual('[object HTMLDivElement]', elems[0].toString());
|
||||||
testing.expectEqual('[object HTMLBodyElement]', elems[1].toString());
|
testing.expectEqual('[object HTMLBodyElement]', elems[1].toString());
|
||||||
@@ -66,11 +66,11 @@
|
|||||||
// Note this will be placed after the div of previous test
|
// Note this will be placed after the div of previous test
|
||||||
a.getClientRects();
|
a.getClientRects();
|
||||||
|
|
||||||
let a_again = document.elementFromPoint(1.5, 0.5);
|
let a_again = document.elementFromPoint(7.5, 0.5);
|
||||||
testing.expectEqual('[object HTMLAnchorElement]', a_again.toString());
|
testing.expectEqual('[object HTMLAnchorElement]', a_again.toString());
|
||||||
testing.expectEqual('https://lightpanda.io', a_again.href);
|
testing.expectEqual('https://lightpanda.io', a_again.href);
|
||||||
|
|
||||||
let a_agains = document.elementsFromPoint(1.5, 0.5);
|
let a_agains = document.elementsFromPoint(7.5, 0.5);
|
||||||
testing.expectEqual('https://lightpanda.io', a_agains[0].href);
|
testing.expectEqual('https://lightpanda.io', a_agains[0].href);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<script src="../testing.js"></script>
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
<script id=history>
|
<script id=history>
|
||||||
testing.expectEqual('auto', history.scrollRestoration);
|
testing.expectEqual('auto', history.scrollRestoration);
|
||||||
|
|
||||||
history.scrollRestoration = 'manual';
|
history.scrollRestoration = 'manual';
|
||||||
history.scrollRestoration = 'foo';
|
|
||||||
testing.expectEqual('manual', history.scrollRestoration);
|
testing.expectEqual('manual', history.scrollRestoration);
|
||||||
|
|
||||||
history.scrollRestoration = 'auto';
|
history.scrollRestoration = 'auto';
|
||||||
testing.expectEqual('auto', history.scrollRestoration);
|
testing.expectEqual('auto', history.scrollRestoration);
|
||||||
testing.expectEqual(null, history.state)
|
testing.expectEqual(null, history.state)
|
||||||
|
|
||||||
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/xhr/json');
|
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/src/tests/html/history/history_after_nav.html');
|
||||||
testing.expectEqual({ testInProgress: true }, history.state);
|
testing.expectEqual({ testInProgress: true }, history.state);
|
||||||
|
|
||||||
|
history.pushState({ testInProgress: false }, null, 'http://127.0.0.1:9582/xhr/json');
|
||||||
history.replaceState({ "new": "field", testComplete: true }, null);
|
history.replaceState({ "new": "field", testComplete: true }, null);
|
||||||
|
|
||||||
let state = { "new": "field", testComplete: true };
|
let state = { "new": "field", testComplete: true };
|
||||||
testing.expectEqual(state, history.state);
|
testing.expectEqual(state, history.state);
|
||||||
|
|
||||||
@@ -32,10 +33,5 @@
|
|||||||
testing.expectEqual(state, popstateEventState);
|
testing.expectEqual(state, popstateEventState);
|
||||||
})
|
})
|
||||||
|
|
||||||
testing.onPageWait(() => {
|
history.back();
|
||||||
testing.expectEqual(true, history.state && history.state.testComplete);
|
|
||||||
testing.expectEqual(state, history.state);
|
|
||||||
});
|
|
||||||
|
|
||||||
testing.expectEqual(undefined, history.go());
|
|
||||||
</script>
|
</script>
|
||||||
26
src/tests/html/history/history2.html
Normal file
26
src/tests/html/history/history2.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=history2>
|
||||||
|
history.pushState(
|
||||||
|
{"new": "field", testComplete: true },
|
||||||
|
null,
|
||||||
|
'http://127.0.0.1:9582/src/tests/html/history/history_after_nav.html'
|
||||||
|
);
|
||||||
|
|
||||||
|
let popstateEventFired = false;
|
||||||
|
let popstateEventState = null;
|
||||||
|
|
||||||
|
// uses the window event listener.
|
||||||
|
window.onpopstate = (event) => {
|
||||||
|
popstateEventFired = true;
|
||||||
|
popstateEventState = event.state;
|
||||||
|
};
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(true, popstateEventFired);
|
||||||
|
testing.expectEqual(true, popstateEventState.testComplete);
|
||||||
|
})
|
||||||
|
|
||||||
|
history.back();
|
||||||
|
</script>
|
||||||
6
src/tests/html/history/history_after_nav.html
Normal file
6
src/tests/html/history/history_after_nav.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=history2>
|
||||||
|
testing.expectEqual(true, history.state && history.state.testInProgress);
|
||||||
|
</script>
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
testing.expectEqual('https://lightpanda.io', link.origin);
|
testing.expectEqual('https://lightpanda.io', link.origin);
|
||||||
|
|
||||||
link.host = 'lightpanda.io:443';
|
link.host = 'lightpanda.io:443';
|
||||||
testing.expectEqual('lightpanda.io:443', link.host);
|
testing.expectEqual('lightpanda.io', link.host);
|
||||||
testing.expectEqual('443', link.port);
|
testing.expectEqual('', link.port);
|
||||||
testing.expectEqual('lightpanda.io', link.hostname);
|
testing.expectEqual('lightpanda.io', link.hostname);
|
||||||
|
|
||||||
link.host = 'lightpanda.io';
|
link.host = 'lightpanda.io';
|
||||||
@@ -42,9 +42,9 @@
|
|||||||
|
|
||||||
testing.expectEqual('', link.port);
|
testing.expectEqual('', link.port);
|
||||||
link.port = '443';
|
link.port = '443';
|
||||||
testing.expectEqual('foo.bar:443', link.host);
|
testing.expectEqual('foo.bar', link.host);
|
||||||
testing.expectEqual('foo.bar', link.hostname);
|
testing.expectEqual('foo.bar', link.hostname);
|
||||||
testing.expectEqual('https://foo.bar:443/?q=bar#frag', link.href);
|
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
|
||||||
link.port = null;
|
link.port = null;
|
||||||
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
|
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,21 @@
|
|||||||
testing.expectEqual("9582", location.port);
|
testing.expectEqual("9582", location.port);
|
||||||
testing.expectEqual("", location.search);
|
testing.expectEqual("", location.search);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=location_hash>
|
||||||
|
location.hash = "";
|
||||||
|
testing.expectEqual("", location.hash);
|
||||||
|
testing.expectEqual('http://localhost:9582/src/tests/html/location.html', location.href);
|
||||||
|
|
||||||
|
location.hash = "#abcdef";
|
||||||
|
testing.expectEqual("#abcdef", location.hash);
|
||||||
|
testing.expectEqual('http://localhost:9582/src/tests/html/location.html#abcdef', location.href);
|
||||||
|
|
||||||
|
location.hash = "xyzxyz";
|
||||||
|
testing.expectEqual("#xyzxyz", location.hash);
|
||||||
|
testing.expectEqual('http://localhost:9582/src/tests/html/location.html#xyzxyz', location.href);
|
||||||
|
|
||||||
|
location.hash = "";
|
||||||
|
testing.expectEqual("", location.hash);
|
||||||
|
testing.expectEqual('http://localhost:9582/src/tests/html/location.html', location.href);
|
||||||
|
</script>
|
||||||
|
|||||||
18
src/tests/html/navigation/navigation.html
Normal file
18
src/tests/html/navigation/navigation.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=navigation>
|
||||||
|
testing.expectEqual('object', typeof navigation);
|
||||||
|
testing.expectEqual('object', typeof navigation.currentEntry);
|
||||||
|
|
||||||
|
testing.expectEqual('string', typeof navigation.currentEntry.id);
|
||||||
|
testing.expectEqual('string', typeof navigation.currentEntry.key);
|
||||||
|
testing.expectEqual('string', typeof navigation.currentEntry.url);
|
||||||
|
|
||||||
|
const currentIndex = navigation.currentEntry.index;
|
||||||
|
|
||||||
|
navigation.navigate(
|
||||||
|
'http://localhost:9582/src/tests/html/navigation/navigation2.html',
|
||||||
|
{ state: { currentIndex: currentIndex, navTestInProgress: true } }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
8
src/tests/html/navigation/navigation2.html
Normal file
8
src/tests/html/navigation/navigation2.html
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=navigation2>
|
||||||
|
const state = navigation.currentEntry.getState();
|
||||||
|
testing.expectEqual(true, state.navTestInProgress);
|
||||||
|
testing.expectEqual(state.currentIndex + 1, navigation.currentEntry.index);
|
||||||
|
</script>
|
||||||
15
src/tests/html/navigation/navigation_currententrychange.html
Normal file
15
src/tests/html/navigation/navigation_currententrychange.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=navigation_currententrychange>
|
||||||
|
let currentEntryChanged = false;
|
||||||
|
|
||||||
|
navigation.addEventListener("currententrychange", () => {
|
||||||
|
currentEntryChanged = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Doesn't fully navigate if same document.
|
||||||
|
location.href = location.href + "#1";
|
||||||
|
|
||||||
|
testing.expectEqual(true, currentEntryChanged);
|
||||||
|
</script>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user