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