2 Commits

Author SHA1 Message Date
Muki Kiboigo
cb141c35b7 create proper navigate event 2025-11-11 21:01:27 -08:00
Muki Kiboigo
01c2f2c6ea wip navigate event 2025-11-11 20:20:39 -08:00
64 changed files with 891 additions and 2113 deletions

View File

@@ -2,6 +2,10 @@ name: "Browsercore install"
description: "Install deps for the project browsercore" description: "Install deps for the project browsercore"
inputs: inputs:
zig:
description: 'Zig version to install'
required: false
default: '0.15.2'
arch: arch:
description: 'CPU arch used to select the v8 lib' description: 'CPU arch used to select the v8 lib'
required: false required: false
@@ -13,7 +17,7 @@ inputs:
zig-v8: zig-v8:
description: 'zig v8 version to install' description: 'zig v8 version to install'
required: false required: false
default: 'v0.1.35' default: 'v0.1.33'
v8: v8:
description: 'v8 version to install' description: 'v8 version to install'
required: false required: false
@@ -34,8 +38,9 @@ runs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
# Zig version used from the `minimum_zig_version` field in build.zig.zon
- uses: mlugg/setup-zig@v2 - uses: mlugg/setup-zig@v2
with:
version: ${{ inputs.zig }}
- name: Cache v8 - name: Cache v8
id: cache-v8 id: cache-v8
@@ -56,8 +61,11 @@ runs:
- name: install v8 - name: install v8
shell: bash shell: bash
run: | run: |
mkdir -p v8 mkdir -p v8/out/${{ inputs.os }}/debug/obj/zig/
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8.a 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
- name: Cache libiconv - name: Cache libiconv
id: cache-libiconv id: cache-libiconv

View File

@@ -5,12 +5,8 @@ env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }} AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }} AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
on: on:
push:
tags:
- '*'
schedule: schedule:
- cron: "2 2 * * *" - cron: "2 2 * * *"
@@ -30,9 +26,10 @@ jobs:
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive submodules: recursive
- uses: ./.github/actions/install - uses: ./.github/actions/install
@@ -41,7 +38,7 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
- name: zig build - name: zig build
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -56,7 +53,7 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }} tag: nightly
build-linux-aarch64: build-linux-aarch64:
env: env:
@@ -79,7 +76,7 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
- name: zig build - name: zig build
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -94,7 +91,7 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }} tag: nightly
build-macos-aarch64: build-macos-aarch64:
env: env:
@@ -119,7 +116,7 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
- name: zig build - name: zig build
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -134,14 +131,19 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }} tag: nightly
build-macos-x86_64: build-macos-x86_64:
env: env:
ARCH: x86_64 ARCH: x86_64
OS: macos OS: macos
runs-on: macos-14-large # 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
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
@@ -157,7 +159,7 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
- name: zig build - name: zig build
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -172,4 +174,4 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }} tag: nightly

View File

@@ -28,7 +28,7 @@ jobs:
path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md' path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'
# branch should not be protected # branch should not be protected
branch: 'main' branch: 'main'
allowlist: krichprollsch,francisbouvier,katie-lpd allowlist: krichprollsch,francisbouvier,katie-lpd,sjorsdonkers,bornlex
remote-organization-name: lightpanda-io remote-organization-name: lightpanda-io
remote-repository-name: cla remote-repository-name: cla

View File

@@ -1,68 +0,0 @@
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`

View File

@@ -49,15 +49,16 @@ jobs:
if: github.event.pull_request.draft == false if: github.event.pull_request.draft == false
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive submodules: recursive
- uses: ./.github/actions/install - uses: ./.github/actions/install
- name: zig build release - name: zig build release
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: upload artifact - name: upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -121,7 +122,7 @@ jobs:
needs: zig-build-release needs: zig-build-release
env: env:
MAX_MEMORY: 28000 MAX_MEMORY: 27000
MAX_AVG_DURATION: 23 MAX_AVG_DURATION: 23
LIGHTPANDA_DISABLE_TELEMETRY: true LIGHTPANDA_DISABLE_TELEMETRY: true

View File

@@ -22,9 +22,10 @@ jobs:
timeout-minutes: 90 timeout-minutes: 90
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive submodules: recursive
- uses: ./.github/actions/install - uses: ./.github/actions/install

View File

@@ -1,5 +1,8 @@
name: zig-fmt name: zig-fmt
env:
ZIG_VERSION: 0.15.2
on: on:
pull_request: pull_request:
@@ -29,13 +32,14 @@ jobs:
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- uses: actions/checkout@v6 - uses: mlugg/setup-zig@v2
with:
version: ${{ env.ZIG_VERSION }}
- uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
# Zig version used from the `minimum_zig_version` field in build.zig.zon
- uses: mlugg/setup-zig@v2
- name: Run zig fmt - name: Run zig fmt
id: fmt id: fmt
run: | run: |
@@ -54,7 +58,6 @@ jobs:
fi fi
echo "${delimiter}" >> "${GITHUB_OUTPUT}" echo "${delimiter}" >> "${GITHUB_OUTPUT}"
- name: Fail the job - name: Fail the job
if: steps.fmt.outputs.zig_fmt_errs != '' if: steps.fmt.outputs.zig_fmt_errs != ''
run: exit 1 run: exit 1

View File

@@ -47,15 +47,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive submodules: recursive
- uses: ./.github/actions/install - uses: ./.github/actions/install
- name: zig build debug - name: zig build debug
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a run: zig build
- name: upload artifact - name: upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -103,7 +104,7 @@ jobs:
- uses: ./.github/actions/install - uses: ./.github/actions/install
- name: zig build test - name: zig build test
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a test -- --json > bench.json run: zig build test -- --json > bench.json
- name: write commit - name: write commit
run: | run: |

1
.gitignore vendored
View File

@@ -1,6 +1,5 @@
zig-cache zig-cache
/.zig-cache/ /.zig-cache/
/.lp-cache/
zig-out zig-out
/vendor/netsurf/out /vendor/netsurf/out
/vendor/libiconv/ /vendor/libiconv/

3
.gitmodules vendored
View File

@@ -22,6 +22,9 @@
[submodule "vendor/nghttp2"] [submodule "vendor/nghttp2"]
path = vendor/nghttp2 path = vendor/nghttp2
url = https://github.com/nghttp2/nghttp2.git url = https://github.com/nghttp2/nghttp2.git
[submodule "vendor/mbedtls"]
path = vendor/mbedtls
url = https://github.com/Mbed-TLS/mbedtls.git
[submodule "vendor/zlib"] [submodule "vendor/zlib"]
path = vendor/zlib path = vendor/zlib
url = https://github.com/madler/zlib.git url = https://github.com/madler/zlib.git

View File

@@ -1,9 +1,10 @@
FROM debian:stable-slim FROM debian:stable
ARG MINISIG=0.12 ARG MINISIG=0.12
ARG ZIG=0.15.2
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.0.365.4 ARG V8=14.0.365.4
ARG ZIG_V8=v0.1.34 ARG ZIG_V8=v0.1.33
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN apt-get update -yq && \ RUN apt-get update -yq && \
@@ -16,25 +17,25 @@ RUN apt-get update -yq && \
# install minisig # install minisig
RUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${MINISIG}/minisign-${MINISIG}-linux.tar.gz && \ RUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${MINISIG}/minisign-${MINISIG}-linux.tar.gz && \
tar xvzf minisign-${MINISIG}-linux.tar.gz -C / tar xvzf minisign-${MINISIG}-linux.tar.gz
# clone lightpanda
RUN git clone https://github.com/lightpanda-io/browser.git
WORKDIR /browser
# install zig # install zig
RUN ZIG=$(grep '\.minimum_zig_version = "' "build.zig.zon" | cut -d'"' -f2) && \ RUN case $TARGETPLATFORM in \
case $TARGETPLATFORM in \ "linux/arm64") ARCH="aarch64" ;; \
"linux/arm64") ARCH="aarch64" ;; \ *) ARCH="x86_64" ;; \
*) ARCH="x86_64" ;; \
esac && \ 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 && \
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig && \ 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} && \ minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG} && \
tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \ tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \ mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
# clone lightpanda
RUN git clone https://github.com/lightpanda-io/browser.git
WORKDIR /browser
# install deps # install deps
RUN git submodule init && \ RUN git submodule init && \
git submodule update --recursive git submodule update --recursive
@@ -49,16 +50,11 @@ RUN case $TARGETPLATFORM in \
*) ARCH="x86_64" ;; \ *) ARCH="x86_64" ;; \
esac && \ esac && \
curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \ curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
mkdir -p v8/ && \ mkdir -p v8/out/linux/release/obj/zig/ && \
mv libc_v8.a v8/libc_v8.a mv libc_v8.a v8/out/linux/release/obj/zig/libc_v8.a
# build release # build release
RUN zig build -Doptimize=ReleaseSafe -Dprebuilt_v8_path=v8/libc_v8.a -Dgit_commit=$$(git rev-parse --short HEAD) RUN make build
FROM debian:stable-slim
RUN apt-get update -yq && \
apt-get install -yq tini
FROM debian:stable-slim FROM debian:stable-slim
@@ -66,12 +62,7 @@ FROM debian:stable-slim
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
COPY --from=1 /usr/bin/tini /usr/bin/tini
EXPOSE 9222/tcp EXPOSE 9222/tcp
# Lightpanda install only some signal handlers, and PID 1 doesn't have a default SIGTERM signal handler. CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222"]
# 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"]

View File

@@ -34,7 +34,7 @@ endif
## Display this help screen ## Display this help screen
help: help:
@printf "\033[36m%-35s %s\033[0m\n" "Command" "Usage" @printf "\e[36m%-35s %s\e[0m\n" "Command" "Usage"
@sed -n -e '/^## /{'\ @sed -n -e '/^## /{'\
-e 's/## //g;'\ -e 's/## //g;'\
-e 'h;'\ -e 'h;'\
@@ -47,43 +47,54 @@ help:
# $(ZIG) commands # $(ZIG) commands
# ------------ # ------------
.PHONY: build build-dev run run-release shell test bench wpt data end2end .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"
## Build in release-safe mode ## Build in release-safe mode
build: build:
@printf "\033[36mBuilding (release safe)...\033[0m\n" @printf "\e[36mBuilding (release safe)...\e[0m\n"
$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;) $(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\033[33mBuild OK\033[0m\n" @printf "\e[33mBuild OK\e[0m\n"
## Build in debug mode ## Build in debug mode
build-dev: build-dev:
@printf "\033[36mBuilding (debug)...\033[0m\n" @printf "\e[36mBuilding (debug)...\e[0m\n"
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;) @$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\033[33mBuild OK\033[0m\n" @printf "\e[33mBuild OK\e[0m\n"
## Run the server in release mode ## Run the server in release mode
run: build run: build
@printf "\033[36mRunning...\033[0m\n" @printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;) @./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
## Run the server in debug mode ## Run the server in debug mode
run-debug: build-dev run-debug: build-dev
@printf "\033[36mRunning...\033[0m\n" @printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;) @./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
## Run a JS shell in debug mode ## Run a JS shell in debug mode
shell: shell:
@printf "\033[36mBuilding shell...\033[0m\n" @printf "\e[36mBuilding shell...\e[0m\n"
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;) @$(ZIG) build shell || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
## Run WPT tests ## Run WPT tests
wpt: wpt:
@printf "\033[36mBuilding wpt...\033[0m\n" @printf "\e[36mBuilding wpt...\e[0m\n"
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;) @$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
wpt-summary: wpt-summary:
@printf "\033[36mBuilding wpt...\033[0m\n" @printf "\e[36mBuilding wpt...\e[0m\n"
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;) @$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
## Test - `grep` is used to filter out the huge compile command on build ## Test - `grep` is used to filter out the huge compile command on build
ifeq ($(OS), macos) ifeq ($(OS), macos)
@@ -101,6 +112,19 @@ end2end:
@test -d ../demo @test -d ../demo
cd ../demo && go run runner/main.go cd ../demo && go run runner/main.go
## v8
get-v8:
@printf "\e[36mGetting v8 source...\e[0m\n"
@$(ZIG) build get-v8
build-v8-dev:
@printf "\e[36mBuilding v8 (dev)...\e[0m\n"
@$(ZIG) build build-v8
build-v8:
@printf "\e[36mBuilding v8...\e[0m\n"
@$(ZIG) build -Doptimize=ReleaseSafe build-v8
# Install and build required dependencies commands # Install and build required dependencies commands
# ------------ # ------------
.PHONY: install-submodule .PHONY: install-submodule
@@ -127,27 +151,27 @@ ICONV := $(BC)vendor/libiconv/out/$(OS)-$(ARCH)
# TODO: this way of linking libiconv is not ideal. We should have a more generic way # TODO: this way of linking libiconv is not ideal. We should have a more generic way
# and stick to a specif version. Maybe build from source. Anyway not now. # and stick to a specif version. Maybe build from source. Anyway not now.
_install-netsurf: clean-netsurf _install-netsurf: clean-netsurf
@printf "\033[36mInstalling NetSurf...\033[0m\n" && \ @printf "\e[36mInstalling NetSurf...\e[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;) && \ ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\e[33mERROR: you need to execute 'make install-libiconv'\e[0m\n"; exit 1;) && \
mkdir -p $(BC_NS) && \ mkdir -p $(BC_NS) && \
cp -R vendor/netsurf/share $(BC_NS) && \ cp -R vendor/netsurf/share $(BC_NS) && \
export PREFIX=$(BC_NS) && \ export PREFIX=$(BC_NS) && \
export OPTLDFLAGS="-L$(ICONV)/lib" && \ export OPTLDFLAGS="-L$(ICONV)/lib" && \
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \ export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
printf "\033[33mInstalling libwapcaplet...\033[0m\n" && \ printf "\e[33mInstalling libwapcaplet...\e[0m\n" && \
cd vendor/netsurf/libwapcaplet && \ cd vendor/netsurf/libwapcaplet && \
BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \ BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \
cd ../libparserutils && \ cd ../libparserutils && \
printf "\033[33mInstalling libparserutils...\033[0m\n" && \ printf "\e[33mInstalling libparserutils...\e[0m\n" && \
BUILDDIR=$(BC_NS)/build/libparserutils make install && \ BUILDDIR=$(BC_NS)/build/libparserutils make install && \
cd ../libhubbub && \ cd ../libhubbub && \
printf "\033[33mInstalling libhubbub...\033[0m\n" && \ printf "\e[33mInstalling libhubbub...\e[0m\n" && \
BUILDDIR=$(BC_NS)/build/libhubbub make install && \ BUILDDIR=$(BC_NS)/build/libhubbub make install && \
rm src/treebuilder/autogenerated-element-type.c && \ rm src/treebuilder/autogenerated-element-type.c && \
cd ../libdom && \ cd ../libdom && \
printf "\033[33mInstalling libdom...\033[0m\n" && \ printf "\e[33mInstalling libdom...\e[0m\n" && \
BUILDDIR=$(BC_NS)/build/libdom make install && \ BUILDDIR=$(BC_NS)/build/libdom make install && \
printf "\033[33mRunning libdom example...\033[0m\n" && \ printf "\e[33mRunning libdom example...\e[0m\n" && \
cd examples && \ cd examples && \
$(ZIG) cc \ $(ZIG) cc \
-I$(ICONV)/include \ -I$(ICONV)/include \
@@ -164,14 +188,14 @@ _install-netsurf: clean-netsurf
$(ICONV)/lib/libiconv.a && \ $(ICONV)/lib/libiconv.a && \
./a.out > /dev/null && \ ./a.out > /dev/null && \
rm a.out && \ rm a.out && \
printf "\033[36mDone NetSurf $(OS)\033[0m\n" printf "\e[36mDone NetSurf $(OS)\e[0m\n"
clean-netsurf: clean-netsurf:
@printf "\033[36mCleaning NetSurf build...\033[0m\n" && \ @printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
rm -Rf $(BC_NS) rm -Rf $(BC_NS)
test-netsurf: test-netsurf:
@printf "\033[36mTesting NetSurf...\033[0m\n" && \ @printf "\e[36mTesting NetSurf...\e[0m\n" && \
export PREFIX=$(BC_NS) && \ export PREFIX=$(BC_NS) && \
export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \ export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \
export CFLAGS="-I$(ICONV)/include -I$(BC_NS)/include" && \ export CFLAGS="-I$(ICONV)/include -I$(BC_NS)/include" && \

View File

@@ -158,6 +158,8 @@ Here are the key features we have implemented:
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time. NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
## Build from sources ## Build from sources
### Prerequisites ### Prerequisites
@@ -166,13 +168,12 @@ Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to
install it with the right version in order to build the project. install it with the right version in order to build the project.
Lightpanda also depends on Lightpanda also depends on
[zig-v8-fork](https://github.com/lightpanda-io/zig-v8-fork/), [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
[Libcurl](https://curl.se/libcurl/), [Libcurl](https://curl.se/libcurl/),
[Brotli](https://github.com/google/brotli),
[Netsurf libs](https://www.netsurf-browser.org/) and [Netsurf libs](https://www.netsurf-browser.org/) and
[Mimalloc](https://microsoft.github.io/mimalloc). [Mimalloc](https://microsoft.github.io/mimalloc).
To be able to build the v8 engine, you have to install some libs: To be able to build the v8 engine for zig-js-runtime, you have to install some libs:
For Debian/Ubuntu based Linux: For Debian/Ubuntu based Linux:
@@ -245,6 +246,22 @@ Note: when Mimalloc is built in dev mode, you can dump memory stats with the
env var `MIMALLOC_SHOW_STATS=1`. See env var `MIMALLOC_SHOW_STATS=1`. See
[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html). [https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html).
**v8**
First, get the tools necessary for building V8, as well as the V8 source code:
```
make get-v8
```
Next, build v8. This build task is very long and cpu consuming, as you will build v8 from sources.
```
make build-v8
```
For dev env, use `make build-v8-dev`.
## Test ## Test
### Unit Tests ### Unit Tests

140
build.zig
View File

@@ -21,21 +21,36 @@ const builtin = @import("builtin");
const Build = std.Build; const Build = std.Build;
/// Do not rename this constant. It is scanned by some scripts to determine
/// which zig version to install.
const recommended_zig_version = "0.15.2";
pub fn build(b: *Build) !void { pub fn build(b: *Build) !void {
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
.eq => {},
.lt => {
@compileError("The minimum version of Zig required to compile is '" ++ recommended_zig_version ++ "', found '" ++ builtin.zig_version_string ++ "'.");
},
.gt => {
std.debug.print(
"WARNING: Recommended Zig version '{s}', but found '{s}', build may fail...\n\n",
.{ recommended_zig_version, builtin.zig_version_string },
);
},
}
var opts = b.addOptions();
opts.addOption(
[]const u8,
"git_commit",
b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
);
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const manifest = Manifest.init(b); // We're still using llvm because the new x86 backend seems to crash
// with v8. This can be reproduced in zig-v8-fork.
const git_commit = b.option([]const u8, "git_commit", "Current git commit");
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
var opts = b.addOptions();
opts.addOption([]const u8, "version", manifest.version);
opts.addOption([]const u8, "git_commit", git_commit orelse "dev");
// We're still using llvm because the new x86 backend seems to crash with v8.
// This can be reproduced in zig-v8-fork.
const lightpanda_module = b.addModule("lightpanda", .{ const lightpanda_module = b.addModule("lightpanda", .{
.root_source_file = b.path("src/main.zig"), .root_source_file = b.path("src/main.zig"),
@@ -44,7 +59,7 @@ pub fn build(b: *Build) !void {
.link_libc = true, .link_libc = true,
.link_libcpp = true, .link_libcpp = true,
}); });
try addDependencies(b, lightpanda_module, opts, prebuilt_v8_path); try addDependencies(b, lightpanda_module, opts);
{ {
// browser // browser
@@ -98,7 +113,7 @@ pub fn build(b: *Build) !void {
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
try addDependencies(b, wpt_module, opts, prebuilt_v8_path); try addDependencies(b, wpt_module, opts);
// compile and install // compile and install
const wpt = b.addExecutable(.{ const wpt = b.addExecutable(.{
@@ -116,9 +131,27 @@ pub fn build(b: *Build) !void {
const wpt_step = b.step("wpt", "WPT tests"); const wpt_step = b.step("wpt", "WPT tests");
wpt_step.dependOn(&wpt_cmd.step); wpt_step.dependOn(&wpt_cmd.step);
} }
{
// get v8
// -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
const get_v8 = b.addRunArtifact(v8.artifact("get-v8"));
const get_step = b.step("get-v8", "Get v8");
get_step.dependOn(&get_v8.step);
}
{
// build v8
// -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
const build_v8 = b.addRunArtifact(v8.artifact("build-v8"));
const build_step = b.step("build-v8", "Build v8");
build_step.dependOn(&build_v8.step);
}
} }
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, prebuilt_v8_path: ?[]const u8) !void { fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !void {
try moduleNetSurf(b, mod); try moduleNetSurf(b, mod);
mod.addImport("build_config", opts.createModule()); mod.addImport("build_config", opts.createModule());
@@ -126,8 +159,6 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, pre
const dep_opts = .{ const dep_opts = .{
.target = target, .target = target,
.optimize = mod.optimize.?, .optimize = mod.optimize.?,
.prebuilt_v8_path = prebuilt_v8_path,
.cache_root = b.pathFromRoot(".lp-cache"),
}; };
mod.addIncludePath(b.path("vendor/lightpanda")); mod.addIncludePath(b.path("vendor/lightpanda"));
@@ -140,6 +171,36 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, pre
const v8_mod = b.dependency("v8", dep_opts).module("v8"); const v8_mod = b.dependency("v8", dep_opts).module("v8");
v8_mod.addOptions("default_exports", v8_opts); v8_mod.addOptions("default_exports", v8_opts);
mod.addImport("v8", v8_mod); mod.addImport("v8", v8_mod);
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
const os = switch (target.result.os.tag) {
.linux => "linux",
.macos => "macos",
else => return error.UnsupportedPlatform,
};
var lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/out/{s}/{s}/obj/zig/libc_v8.a",
.{ os, release_dir },
);
std.fs.cwd().access(lib_path, .{}) catch {
// legacy path
lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/out/{s}/obj/zig/libc_v8.a",
.{release_dir},
);
};
mod.addObjectFile(mod.owner.path(lib_path));
switch (target.result.os.tag) {
.macos => {
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
mod.linkFramework("CoreFoundation", .{});
},
else => {},
}
} }
{ {
@@ -313,27 +374,14 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, pre
mod.addCMacro("STDC_HEADERS", "1"); mod.addCMacro("STDC_HEADERS", "1");
mod.addCMacro("TIME_WITH_SYS_TIME", "1"); mod.addCMacro("TIME_WITH_SYS_TIME", "1");
mod.addCMacro("USE_NGHTTP2", "1"); mod.addCMacro("USE_NGHTTP2", "1");
mod.addCMacro("USE_OPENSSL", "1"); mod.addCMacro("USE_MBEDTLS", "1");
mod.addCMacro("OPENSSL_IS_BORINGSSL", "1");
mod.addCMacro("USE_THREADS_POSIX", "1"); mod.addCMacro("USE_THREADS_POSIX", "1");
mod.addCMacro("USE_UNIX_SOCKETS", "1"); mod.addCMacro("USE_UNIX_SOCKETS", "1");
} }
try buildZlib(b, mod); try buildZlib(b, mod);
try buildBrotli(b, mod); try buildBrotli(b, mod);
const boringssl_dep = b.dependency("boringssl-zig", .{ try buildMbedtls(b, mod);
.target = target,
.optimize = mod.optimize.?,
.force_pic = true,
});
const ssl = boringssl_dep.artifact("ssl");
ssl.bundle_ubsan_rt = false;
const crypto = boringssl_dep.artifact("crypto");
crypto.bundle_ubsan_rt = false;
mod.linkLibrary(ssl);
mod.linkLibrary(crypto);
try buildNghttp2(b, mod); try buildNghttp2(b, mod);
try buildCurl(b, mod); try buildCurl(b, mod);
try buildAda(b, mod); try buildAda(b, mod);
@@ -794,9 +842,8 @@ fn buildCurl(b: *Build, m: *Build.Module) !void {
root ++ "lib/vauth/spnego_sspi.c", root ++ "lib/vauth/spnego_sspi.c",
root ++ "lib/vauth/vauth.c", root ++ "lib/vauth/vauth.c",
root ++ "lib/vtls/cipher_suite.c", root ++ "lib/vtls/cipher_suite.c",
root ++ "lib/vtls/openssl.c", root ++ "lib/vtls/mbedtls.c",
root ++ "lib/vtls/hostcheck.c", root ++ "lib/vtls/mbedtls_threadlock.c",
root ++ "lib/vtls/keylog.c",
root ++ "lib/vtls/vtls.c", root ++ "lib/vtls/vtls.c",
root ++ "lib/vtls/vtls_scache.c", root ++ "lib/vtls/vtls_scache.c",
root ++ "lib/vtls/x509asn1.c", root ++ "lib/vtls/x509asn1.c",
@@ -834,28 +881,3 @@ pub fn buildAda(b: *Build, m: *Build.Module) !void {
// Expose ada module to main module. // Expose ada module to main module.
m.addImport("ada", ada_mod); m.addImport("ada", ada_mod);
} }
const Manifest = struct {
version: []const u8,
minimum_zig_version: []const u8,
fn init(b: *std.Build) Manifest {
const input = @embedFile("build.zig.zon");
var diagnostics: std.zon.parse.Diagnostics = .{};
defer diagnostics.deinit(b.allocator);
return std.zon.parse.fromSlice(Manifest, b.allocator, input, &diagnostics, .{
.free_on_error = true,
.ignore_unknown_fields = true,
}) catch |err| {
switch (err) {
error.OutOfMemory => @panic("OOM"),
error.ParseZon => {
std.debug.print("Parse diagnostics:\n{f}\n", .{diagnostics});
std.process.exit(1);
},
}
};
}
};

View File

@@ -1,22 +1,17 @@
.{ .{
.name = .browser, .name = .browser,
.paths = .{""},
.version = "0.0.0", .version = "0.0.0",
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications. .fingerprint = 0xda130f3af836cea0,
.minimum_zig_version = "0.15.2",
.dependencies = .{ .dependencies = .{
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/e047d2a4d5af5783763f0f6a652fab8982a08603.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/84cdca7cd9065f67c7933388f2091810fc485bc6.tar.gz",
.hash = "v8-0.0.0-xddH65gMBACRBQMM7EwmVgfi94FJyyX-0jpe5KhXYhfv", .hash = "v8-0.0.0-xddH67vcAwCuN2gBsAO8TBzEw523KMroIKGrdZwc-Q-y",
}, },
// .v8 = .{ .path = "../zig-v8-fork" }, //.v8 = .{ .path = "../zig-v8-fork" }
.@"ada-singleheader" = .{ .@"ada-singleheader" = .{
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip", .url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s", .hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
}, },
.@"boringssl-zig" = .{
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
},
}, },
.paths = .{""},
} }

View File

@@ -100,11 +100,6 @@ fn getContentType(file_path: []const u8) []const u8 {
return "application/json"; return "application/json";
} }
if (std.mem.endsWith(u8, file_path, ".mjs")) {
// mjs are ECMAScript modules
return "application/json";
}
if (std.mem.endsWith(u8, file_path, ".html")) { if (std.mem.endsWith(u8, file_path, ".html")) {
return "text/html"; return "text/html";
} }

View File

@@ -19,7 +19,6 @@ pub const App = struct {
telemetry: Telemetry, telemetry: Telemetry,
app_dir_path: ?[]const u8, app_dir_path: ?[]const u8,
notification: *Notification, notification: *Notification,
shutdown: bool = false,
pub const RunMode = enum { pub const RunMode = enum {
help, help,
@@ -83,14 +82,9 @@ pub const App = struct {
} }
pub fn deinit(self: *App) void { pub fn deinit(self: *App) void {
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
return;
}
const allocator = self.allocator; const allocator = self.allocator;
if (self.app_dir_path) |app_dir_path| { if (self.app_dir_path) |app_dir_path| {
allocator.free(app_dir_path); allocator.free(app_dir_path);
self.app_dir_path = null;
} }
self.telemetry.deinit(); self.telemetry.deinit();
self.notification.deinit(); self.notification.deinit();

View File

@@ -392,15 +392,6 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
.stack = self.page.js.stackTrace() catch "???", .stack = self.page.js.stackTrace() catch "???",
}); });
// It's possible, but unlikely, for client.request to immediately finish
// a request, thus calling our callback. We generally don't want a call
// from v8 (which is why we're here), to result in a new script evaluation.
// So we block even the slightest change that `client.request` immediately
// executes a callback.
const was_evaluating = self.is_evaluating;
self.is_evaluating = true;
defer self.is_evaluating = was_evaluating;
try self.client.request(.{ try self.client.request(.{
.url = url, .url = url,
.method = .GET, .method = .GET,

View File

@@ -1,58 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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;
}

View File

@@ -1,145 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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);
}

View File

@@ -1,13 +0,0 @@
//! 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,
};

View File

@@ -190,7 +190,7 @@ fn isNumericWithUnit(value: []const u8) bool {
return CSSKeywords.isValidUnit(unit); return CSSKeywords.isValidUnit(unit);
} }
pub fn isHexColor(value: []const u8) bool { fn isHexColor(value: []const u8) bool {
if (value.len == 0) { if (value.len == 0) {
return false; return false;
} }
@@ -199,7 +199,7 @@ pub fn isHexColor(value: []const u8) bool {
} }
const hex_part = value[1..]; const hex_part = value[1..];
if (hex_part.len != 3 and hex_part.len != 4 and hex_part.len != 6 and hex_part.len != 8) { if (hex_part.len != 3 and hex_part.len != 6 and hex_part.len != 8) {
return false; return false;
} }
@@ -551,7 +551,6 @@ test "Browser: CSS.StyleDeclaration: isNumericWithUnit - edge cases and invalid
test "Browser: CSS.StyleDeclaration: isHexColor - valid hex colors" { test "Browser: CSS.StyleDeclaration: isHexColor - valid hex colors" {
try testing.expect(isHexColor("#000")); try testing.expect(isHexColor("#000"));
try testing.expect(isHexColor("#0000"));
try testing.expect(isHexColor("#fff")); try testing.expect(isHexColor("#fff"));
try testing.expect(isHexColor("#123456")); try testing.expect(isHexColor("#123456"));
try testing.expect(isHexColor("#abcdef")); try testing.expect(isHexColor("#abcdef"));
@@ -564,6 +563,7 @@ test "Browser: CSS.StyleDeclaration: isHexColor - invalid hex colors" {
try testing.expect(!isHexColor("#")); try testing.expect(!isHexColor("#"));
try testing.expect(!isHexColor("000")); try testing.expect(!isHexColor("000"));
try testing.expect(!isHexColor("#00")); try testing.expect(!isHexColor("#00"));
try testing.expect(!isHexColor("#0000"));
try testing.expect(!isHexColor("#00000")); try testing.expect(!isHexColor("#00000"));
try testing.expect(!isHexColor("#0000000")); try testing.expect(!isHexColor("#0000000"));
try testing.expect(!isHexColor("#000000000")); try testing.expect(!isHexColor("#000000000"));

View File

@@ -1,283 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Io = std.Io;
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;
}
};

View File

@@ -319,67 +319,9 @@ pub const Document = struct {
log.debug(.web_api, "not implemented", .{ .feature = "Document hasFocus" }); log.debug(.web_api, "not implemented", .{ .feature = "Document hasFocus" });
return true; return true;
} }
pub fn _open(_: *parser.Document, page: *Page) !*parser.DocumentHTML {
if (page.open) {
return page.window.document;
}
// This implementation is invalid.
// According to MDN, we should cleanup registered listeners.
// So we sould cleanup previous DOM memory.
// But this implementation is more simple for now.
const html_doc = try parser.documentHTMLParseFromStr("");
try page.setDocument(html_doc);
page.open = true;
return page.window.document;
}
pub fn _close(_: *parser.Document, page: *Page) !void {
page.open = false;
}
pub fn _write(self: *parser.Document, str: []const u8, page: *Page) !void {
_ = try _open(self, page);
const document = parser.documentHTMLToDocument(page.window.document);
const fragment = try parser.documentParseFragmentFromStr(document, str);
const fragment_node = parser.documentFragmentToNode(fragment);
const fragment_html = parser.nodeFirstChild(fragment_node) orelse return;
const fragment_head = parser.nodeFirstChild(fragment_html) orelse return;
const fragment_body = parser.nodeNextSibling(fragment_head) orelse return;
const document_node = parser.documentToNode(document);
const document_html = parser.nodeFirstChild(document_node) orelse return;
const document_head = parser.nodeFirstChild(document_html) orelse return;
const document_body = parser.nodeNextSibling(document_head) orelse return;
{
const children = try parser.nodeGetChildNodes(fragment_head);
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
while (parser.nodeListItem(children, 0)) |child| {
_ = try parser.nodeAppendChild(document_head, child);
}
}
{
const children = try parser.nodeGetChildNodes(fragment_body);
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
while (parser.nodeListItem(children, 0)) |child| {
_ = try parser.nodeAppendChild(document_body, child);
}
}
}
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: DOM.Document" { test "Browser: DOM.Document" {
try testing.htmlRunner("dom/document.html"); try testing.htmlRunner("dom/document.html");
} }
test "Browser: DOM.Document.write" {
try testing.htmlRunner("dom/document_write.html");
}

View File

@@ -95,7 +95,6 @@ pub const EventTarget = struct {
// -------- // --------
pub fn constructor(page: *Page) !*parser.EventTarget { pub fn constructor(page: *Page) !*parser.EventTarget {
const et = try page.arena.create(EventTarget); const et = try page.arena.create(EventTarget);
et.* = .{};
return @ptrCast(&et.base); return @ptrCast(&et.base);
} }

View File

@@ -390,23 +390,7 @@ pub const Node = struct {
return parser.nodeHasChildNodes(self); return parser.nodeHasChildNodes(self);
} }
fn is_template(self: *parser.Node) !bool {
if (parser.nodeType(self) != .element) {
return false;
}
const e = parser.nodeToElement(self);
return try parser.elementTag(e) == .template;
}
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList { pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
// special case for template:
// > The Node.childNodes property of the <template> element is always empty
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template#usage_notes
if (try is_template(self)) {
return .{};
}
const allocator = page.arena; const allocator = page.arena;
var list: NodeList = .{}; var list: NodeList = .{};

View File

@@ -1,84 +0,0 @@
// 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",
});
};
}

View File

@@ -40,7 +40,7 @@ const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
const PopStateEvent = @import("../html/History.zig").PopStateEvent; const PopStateEvent = @import("../html/History.zig").PopStateEvent;
const CompositionEvent = @import("composition_event.zig").CompositionEvent; const CompositionEvent = @import("composition_event.zig").CompositionEvent;
const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent; const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent;
const PageTransitionEvent = @import("../events/PageTransitionEvent.zig"); const NavigateEvent = @import("../navigation/root.zig").NavigateEvent;
// Event interfaces // Event interfaces
pub const Interfaces = .{ pub const Interfaces = .{
@@ -54,7 +54,7 @@ pub const Interfaces = .{
PopStateEvent, PopStateEvent,
CompositionEvent, CompositionEvent,
NavigationCurrentEntryChangeEvent, NavigationCurrentEntryChangeEvent,
PageTransitionEvent, NavigateEvent,
}; };
pub const Union = generate.Union(Interfaces); pub const Union = generate.Union(Interfaces);
@@ -87,7 +87,7 @@ pub const Event = struct {
.navigation_current_entry_change_event => .{ .navigation_current_entry_change_event => .{
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*, .NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
}, },
.page_transition_event => .{ .PageTransitionEvent = @as(*PageTransitionEvent, @ptrCast(evt)).* }, .navigate_event => .{ .NavigateEvent = @as(*NavigateEvent, @ptrCast(evt)).* },
}; };
} }

View File

@@ -254,13 +254,17 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
self.body_used = true; self.body_used = true;
if (self.body) |body| { if (self.body) |body| {
const value = js.Value.fromJson(page.js, body) catch |e| { const p = std.json.parseFromSliceLeaky(
std.json.Value,
page.call_arena,
body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" }); log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
return error.SyntaxError; return error.SyntaxError;
}; };
const pvalue = try value.persist(page.js);
return page.js.resolvePromise(pvalue); return page.js.resolvePromise(p);
} }
return page.js.resolvePromise(null); return page.js.resolvePromise(null);
} }

View File

@@ -179,13 +179,17 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
if (self.body) |body| { if (self.body) |body| {
self.body_used = true; self.body_used = true;
const value = js.Value.fromJson(page.js, body) catch |e| { const p = std.json.parseFromSliceLeaky(
std.json.Value,
page.call_arena,
body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" }); log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
return error.SyntaxError; return error.SyntaxError;
}; };
const pvalue = try value.persist(page.js);
return page.js.resolvePromise(pvalue); return page.js.resolvePromise(p);
} }
return page.js.resolvePromise(null); return page.js.resolvePromise(null);
} }

View File

@@ -48,7 +48,7 @@ pub fn set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
} }
pub fn get_state(_: *History, page: *Page) !?js.Value { pub fn get_state(_: *History, page: *Page) !?js.Value {
if (page.session.navigation.currentEntry().state.value) |state| { if (page.session.navigation.currentEntry().state) |state| {
const value = try js.Value.fromJson(page.js, state); const value = try js.Value.fromJson(page.js, state);
return value; return value;
} else { } else {
@@ -61,15 +61,18 @@ pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw); const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
const json = state.toJson(arena) catch return error.DataClone; const json = state.toJson(arena) catch return error.DataClone;
_ = try page.session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true); _ = try page.session.navigation.pushEntry(url, json, page, true);
} }
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void { pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena; const arena = page.session.arena;
const entry = page.session.navigation.currentEntry();
const json = try state.toJson(arena);
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw); const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
const json = try state.toJson(arena); entry.state = json;
_ = try page.session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true); entry.url = url;
} }
pub fn go(_: *const History, delta: i32, page: *Page) !void { pub fn go(_: *const History, delta: i32, page: *Page) !void {
@@ -86,7 +89,7 @@ pub fn go(_: *const History, delta: i32, page: *Page) !void {
if (entry.url) |url| { if (entry.url) |url| {
if (try page.isSameOrigin(url)) { if (try page.isSameOrigin(url)) {
PopStateEvent.dispatch(entry.state.value, page); PopStateEvent.dispatch(entry.state, page);
} }
} }

View File

@@ -32,10 +32,6 @@ const DataSet = @import("DataSet.zig");
const StyleSheet = @import("../cssom/StyleSheet.zig"); const StyleSheet = @import("../cssom/StyleSheet.zig");
const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig"); const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
const CanvasRenderingContext2D = @import("../canvas/CanvasRenderingContext2D.zig");
const WebGLRenderingContext = @import("../canvas/WebGLRenderingContext.zig");
const WalkerChildren = @import("../dom/walker.zig").WalkerChildren;
// HTMLElement interfaces // HTMLElement interfaces
pub const Interfaces = .{ pub const Interfaces = .{
@@ -491,29 +487,6 @@ pub const HTMLCanvasElement = struct {
pub const Self = parser.Canvas; pub const Self = parser.Canvas;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const subtype = .node; pub const subtype = .node;
/// This should be a union once we support other context types.
const ContextAttributes = struct {
alpha: bool,
color_space: []const u8 = "srgb",
};
/// Returns a drawing context on the canvas, or null if the context identifier
/// is not supported, or the canvas has already been set to a different context mode.
pub fn _getContext(
ctx_type: []const u8,
_: ?ContextAttributes,
) ?union(enum) { @"2d": CanvasRenderingContext2D, webgl: WebGLRenderingContext } {
if (std.mem.eql(u8, ctx_type, "2d")) {
return .{ .@"2d" = .{} };
}
if (std.mem.eql(u8, ctx_type, "webgl") or std.mem.eql(u8, ctx_type, "experimental-webgl")) {
return .{ .webgl = .{} };
}
return null;
}
}; };
pub const HTMLDListElement = struct { pub const HTMLDListElement = struct {
@@ -1227,22 +1200,11 @@ pub const HTMLTemplateElement = struct {
pub const subtype = .node; pub const subtype = .node;
pub fn get_content(self: *parser.Template, page: *Page) !*parser.DocumentFragment { pub fn get_content(self: *parser.Template, page: *Page) !*parser.DocumentFragment {
const n: *parser.Node = @ptrCast(@alignCast(self)); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
const state = try page.getOrCreateNodeState(n);
if (state.template_content) |tc| { if (state.template_content) |tc| {
return tc; return tc;
} }
const tc = try parser.documentCreateDocumentFragment(@ptrCast(page.window.document)); const tc = try parser.documentCreateDocumentFragment(@ptrCast(page.window.document));
const ntc: *parser.Node = @ptrCast(@alignCast(tc));
// move existing template's childnodes to the fragment.
const walker = WalkerChildren{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(n, next) orelse break;
_ = try parser.nodeAppendChild(ntc, next.?);
}
state.template_content = tc; state.template_content = tc;
return tc; return tc;
} }
@@ -1394,7 +1356,3 @@ test "Browser: HTML.HtmlScriptElement" {
test "Browser: HTML.HtmlSlotElement" { test "Browser: HTML.HtmlSlotElement" {
try testing.htmlRunner("html/slot.html"); try testing.htmlRunner("html/slot.html");
} }
test "Browser: HTML.HTMLCanvasElement" {
try testing.htmlRunner("html/canvas.html");
}

View File

@@ -56,7 +56,7 @@ pub const Location = struct {
break :blk try std.fmt.allocPrint(page.arena, "#{s}", .{hash}); break :blk try std.fmt.allocPrint(page.arena, "#{s}", .{hash});
}; };
return page.navigateFromWebAPI(normalized_hash, .{ .reason = .script }, .{ .replace = null }); return page.navigateFromWebAPI(normalized_hash, .{ .reason = .script }, .replace);
} }
pub fn get_protocol(self: *Location) []const u8 { pub fn get_protocol(self: *Location) []const u8 {
@@ -96,7 +96,7 @@ pub const Location = struct {
} }
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void { pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .replace = null }); return page.navigateFromWebAPI(url, .{ .reason = .script }, .replace);
} }
pub fn _reload(_: *const Location, page: *Page) !void { pub fn _reload(_: *const Location, page: *Page) !void {

View File

@@ -14,7 +14,6 @@ const types = @import("types.zig");
const Caller = @import("Caller.zig"); const Caller = @import("Caller.zig");
const NamedFunction = Caller.NamedFunction; const NamedFunction = Caller.NamedFunction;
const PersistentObject = v8.Persistent(v8.Object); const PersistentObject = v8.Persistent(v8.Object);
const PersistentValue = v8.Persistent(v8.Value);
const PersistentModule = v8.Persistent(v8.Module); const PersistentModule = v8.Persistent(v8.Module);
const PersistentPromise = v8.Persistent(v8.Promise); const PersistentPromise = v8.Persistent(v8.Promise);
const PersistentFunction = v8.Persistent(v8.Function); const PersistentFunction = v8.Persistent(v8.Function);
@@ -71,9 +70,6 @@ identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty,
// we now simply persist every time persist() is called. // we now simply persist every time persist() is called.
js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty, js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty,
// js_value_list tracks persisted js values.
js_value_list: std.ArrayListUnmanaged(PersistentValue) = .empty,
// Various web APIs depend on having a persistent promise resolver. They // Various web APIs depend on having a persistent promise resolver. They
// require for this PromiseResolver to be valid for a lifetime longer than // require for this PromiseResolver to be valid for a lifetime longer than
// the function that resolves/rejects them. // the function that resolves/rejects them.
@@ -153,10 +149,6 @@ pub fn deinit(self: *Context) void {
p.deinit(); p.deinit();
} }
for (self.js_value_list.items) |*p| {
p.deinit();
}
for (self.persisted_promise_resolvers.items) |*p| { for (self.persisted_promise_resolvers.items) |*p| {
p.deinit(); p.deinit();
} }
@@ -230,54 +222,63 @@ pub fn exec(self: *Context, src: []const u8, name: ?[]const u8) !js.Value {
} }
pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) { pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) {
const mod, const owned_url = blk: { if (cacheable) {
const arena = self.arena; if (self.module_cache.get(url)) |entry| {
// The dynamic import will create an entry without the
// gop will _always_ initiated if cacheable == true // module to prevent multiple calls from asynchronously
var gop: std.StringHashMapUnmanaged(ModuleEntry).GetOrPutResult = undefined; // loading the same module. If we're here, without the
if (cacheable) { // module, then it's time to load it.
gop = try self.module_cache.getOrPut(arena, url); if (entry.module != null) {
if (gop.found_existing) { return if (comptime want_result) entry else {};
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);
if (cacheable) { const arena = self.arena;
// compileModule is synchronous - nothing can modify the cache during compilation const owned_url = try arena.dupe(u8, url);
std.debug.assert(gop.value_ptr.module == null);
gop.value_ptr.module = PersistentModule.init(self.isolate, m); try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
if (!gop.found_existing) { errdefer _ = self.module_identifier.remove(m.getIdentityHash());
gop.key_ptr.* = owned_url;
}
}
break :blk .{ m, owned_url };
};
try self.postCompileModule(mod, owned_url);
const v8_context = self.v8_context; const v8_context = self.v8_context;
if (try mod.instantiate(v8_context, resolveModuleCallback) == false) { {
// Non-async modules are blocking. We can download them in
// parallel, but they need to be processed serially. So we
// want to get the list of dependent modules this module has
// and start downloading them asap.
const requests = m.getModuleRequests();
for (0..requests.length()) |i| {
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
const specifier = try self.jsStringToZig(req.getSpecifier(), .{});
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
self.call_arena,
specifier,
owned_url,
);
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
if (!gop.found_existing) {
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
gop.key_ptr.* = owned_specifier;
gop.value_ptr.* = .{};
try self.script_manager.?.preloadImport(owned_specifier, url);
}
}
}
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
return error.ModuleInstantiationError; return error.ModuleInstantiationError;
} }
const evaluated = mod.evaluate(v8_context) catch { const evaluated = m.evaluate(v8_context) catch {
std.debug.assert(mod.getStatus() == .kErrored); std.debug.assert(m.getStatus() == .kErrored);
// Some module-loading errors aren't handled by TryCatch. We need to // Some module-loading errors aren't handled by TryCatch. We need to
// get the error from the module itself. // get the error from the module itself.
log.warn(.js, "evaluate module", .{ log.warn(.js, "evaluate module", .{
.specifier = owned_url, .specifier = owned_url,
.message = self.valueToString(mod.getException(), .{}) catch "???", .message = self.valueToString(m.getException(), .{}) catch "???",
}); });
return error.EvaluationError; return error.EvaluationError;
}; };
@@ -300,46 +301,28 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
// be cached // be cached
std.debug.assert(cacheable); std.debug.assert(cacheable);
// entry has to have been created atop this function const persisted_module = PersistentModule.init(self.isolate, m);
const entry = self.module_cache.getPtr(owned_url).?; const persisted_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
// and the module must have been set after we compiled it var gop = try self.module_cache.getOrPut(arena, owned_url);
std.debug.assert(entry.module != null); if (gop.found_existing) {
std.debug.assert(entry.module_promise == null); // 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);
entry.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle }); gop.value_ptr.module = persisted_module;
return if (comptime want_result) entry.* else {}; gop.value_ptr.module_promise = persisted_promise;
} } else {
gop.value_ptr.* = ModuleEntry{
// After we compile a module, whether it's a top-level one, or a nested one, .module = persisted_module,
// we always want to track its identity (so that, if this module imports other .module_promise = persisted_promise,
// modules, we can resolve the full URL), and preload any dependent modules. .resolver_promise = null,
fn postCompileModule(self: *Context, mod: v8.Module, url: [:0]const u8) !void { };
try self.module_identifier.putNoClobber(self.arena, mod.getIdentityHash(), url);
const v8_context = self.v8_context;
// Non-async modules are blocking. We can download them in parallel, but
// they need to be processed serially. So we want to get the list of
// dependent modules this module has and start downloading them asap.
const requests = mod.getModuleRequests();
const script_manager = self.script_manager.?;
for (0..requests.length()) |i| {
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
const specifier = try self.jsStringToZig(req.getSpecifier(), .{});
const normalized_specifier = try script_manager.resolveSpecifier(
self.call_arena,
specifier,
url,
);
const nested_gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
if (!nested_gop.found_existing) {
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
nested_gop.key_ptr.* = owned_specifier;
nested_gop.value_ptr.* = .{};
try script_manager.preloadImport(owned_specifier, url);
}
} }
return if (comptime want_result) gop.value_ptr.* else {};
} }
// == Creators == // == Creators ==
@@ -417,8 +400,9 @@ pub fn zigValueToJs(self: *Context, value: anytype) !v8.Value {
}, },
.pointer => |ptr| switch (ptr.size) { .pointer => |ptr| switch (ptr.size) {
.one => { .one => {
if (types.has(ptr.child)) { const type_name = @typeName(ptr.child);
const template = self.templates[types.getId(ptr.child)]; if (@hasField(types.Lookup, type_name)) {
const template = self.templates[@field(types.LOOKUP, type_name)];
const js_obj = try self.mapZigInstanceToJs(template, value); const js_obj = try self.mapZigInstanceToJs(template, value);
return js_obj.toValue(); return js_obj.toValue();
} }
@@ -452,8 +436,9 @@ pub fn zigValueToJs(self: *Context, value: anytype) !v8.Value {
else => {}, else => {},
}, },
.@"struct" => |s| { .@"struct" => |s| {
if (types.has(T)) { const type_name = @typeName(T);
const template = self.templates[types.getId(T)]; if (@hasField(types.Lookup, type_name)) {
const template = self.templates[@field(types.LOOKUP, type_name)];
const js_obj = try self.mapZigInstanceToJs(template, value); const js_obj = try self.mapZigInstanceToJs(template, value);
return js_obj.toValue(); return js_obj.toValue();
} }
@@ -589,7 +574,8 @@ pub fn mapZigInstanceToJs(self: *Context, js_obj_or_template: anytype, value: an
// well as any meta data we'll need to use it later. // well as any meta data we'll need to use it later.
// See the TaggedAnyOpaque struct for more details. // See the TaggedAnyOpaque struct for more details.
const tao = try arena.create(TaggedAnyOpaque); const tao = try arena.create(TaggedAnyOpaque);
const meta = self.meta_lookup[types.getId(ptr.child)]; const meta_index = @field(types.LOOKUP, @typeName(ptr.child));
const meta = self.meta_lookup[meta_index];
tao.* = .{ tao.* = .{
.ptr = value, .ptr = value,
@@ -669,7 +655,7 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
if (!js_value.isObject()) { if (!js_value.isObject()) {
return error.InvalidArgument; return error.InvalidArgument;
} }
if (types.has(ptr.child)) { if (@hasField(types.Lookup, @typeName(ptr.child))) {
const js_obj = js_value.castTo(v8.Object); const js_obj = js_value.castTo(v8.Object);
return self.typeTaggedAnyOpaque(named_function, *types.Receiver(ptr.child), js_obj); return self.typeTaggedAnyOpaque(named_function, *types.Receiver(ptr.child), js_obj);
} }
@@ -785,55 +771,55 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
// Extracted so that it can be used in both jsValueToZig and in // Extracted so that it can be used in both jsValueToZig and in
// probeJsValueToZig. Avoids having to duplicate this logic when probing. // probeJsValueToZig. Avoids having to duplicate this logic when probing.
fn jsValueToStruct(self: *Context, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !?T { fn jsValueToStruct(self: *Context, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !?T {
return switch (T) { if (T == js.Function) {
js.Function => { if (!js_value.isFunction()) {
if (!js_value.isFunction()) { return null;
return null; }
} return try self.createFunction(js_value);
}
return try self.createFunction(js_value); if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
}, const VT = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child;
// zig fmt: off const arr = (try self.jsValueToTypedArray(VT, js_value)) orelse return null;
js.TypedArray(u8), js.TypedArray(u16), js.TypedArray(u32), js.TypedArray(u64), return .{ .values = arr };
js.TypedArray(i8), js.TypedArray(i16), js.TypedArray(i32), js.TypedArray(i64), }
js.TypedArray(f32), js.TypedArray(f64),
// zig fmt: on if (T == js.String) {
=> { return .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) };
const ValueType = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child; }
const slice = (try self.jsValueToTypedArray(ValueType, js_value)) orelse return null;
return .{ .values = slice }; const js_obj = js_value.castTo(v8.Object);
},
js.String => .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) }, if (comptime T == js.Object) {
// Caller wants an opaque js.Object. Probably a parameter // Caller wants an opaque js.Object. Probably a parameter
// that it needs to pass back into a callback. // that it needs to pass back into a callback
js.Object => js.Object{ return js.Object{
.js_obj = js_value.castTo(v8.Object), .js_obj = js_obj,
.context = self, .context = self,
}, };
else => { }
const js_obj = js_value.castTo(v8.Object);
if (!js_value.isObject()) {
return null;
}
const v8_context = self.v8_context; if (!js_value.isObject()) {
const isolate = self.isolate; return null;
var value: T = undefined; }
inline for (@typeInfo(T).@"struct".fields) |field| {
const name = field.name; const v8_context = self.v8_context;
const key = v8.String.initUtf8(isolate, name); const isolate = self.isolate;
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)); var value: T = undefined;
} else if (@typeInfo(field.type) == .optional) { inline for (@typeInfo(T).@"struct".fields) |field| {
@field(value, name) = null; const name = field.name;
} else { const key = v8.String.initUtf8(isolate, name);
const dflt = field.defaultValue() orelse return null; if (js_obj.has(v8_context, key.toValue())) {
@field(value, name) = dflt; @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;
return value; } 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 { fn jsValueToTypedArray(_: *Context, comptime T: type, js_value: v8.Value) !?[]T {
@@ -1203,14 +1189,31 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
}; };
const normalized_specifier = try self.script_manager.?.resolveSpecifier( const normalized_specifier = try self.script_manager.?.resolveSpecifier(
self.arena, self.arena, // might need to survive until the module is loaded
specifier, specifier,
referrer_path, referrer_path,
); );
const entry = self.module_cache.getPtr(normalized_specifier).?; const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
if (entry.module) |m| { if (gop.found_existing) {
return m.castToModule().handle; 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);
} }
var source = try self.script_manager.?.waitForImport(normalized_specifier); var source = try self.script_manager.?.waitForImport(normalized_specifier);
@@ -1220,10 +1223,26 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
try_catch.init(self); try_catch.init(self);
defer try_catch.deinit(); defer try_catch.deinit();
const mod = try compileModule(self.isolate, source.src(), normalized_specifier); const entry = self.module(true, source.src(), normalized_specifier, true) catch |err| {
try self.postCompileModule(mod, normalized_specifier); switch (err) {
entry.module = PersistentModule.init(self.isolate, mod); error.EvaluationError => {
return entry.module.?.castToModule().handle; // 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;
} }
// Will get passed to ScriptManager and then passed back to us when // Will get passed to ScriptManager and then passed back to us when
@@ -1298,32 +1317,7 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
// `dynamicModuleSourceCallback`, but we can skip some steps // `dynamicModuleSourceCallback`, but we can skip some steps
// since the module is alrady loaded, // since the module is alrady loaded,
std.debug.assert(gop.value_ptr.module != null); std.debug.assert(gop.value_ptr.module != null);
std.debug.assert(gop.value_ptr.module_promise != null);
// If the module hasn't been evaluated yet (it was only instantiated
// as a static import dependency), we need to evaluate it now.
if (gop.value_ptr.module_promise == null) {
const mod = gop.value_ptr.module.?.castToModule();
const status = mod.getStatus();
if (status == .kEvaluated or status == .kEvaluating) {
// Module was already evaluated (shouldn't normally happen, but handle it).
// Create a pre-resolved promise with the module namespace.
const persisted_module_resolver = v8.Persistent(v8.PromiseResolver).init(isolate, v8.PromiseResolver.init(self.v8_context));
try self.persisted_promise_resolvers.append(self.arena, persisted_module_resolver);
var module_resolver = persisted_module_resolver.castToPromiseResolver();
_ = module_resolver.resolve(self.v8_context, mod.getModuleNamespace());
gop.value_ptr.module_promise = PersistentPromise.init(self.isolate, module_resolver.getPromise());
} else {
// the module was loaded, but not evaluated, we _have_ to evaluate it now
const evaluated = mod.evaluate(self.v8_context) catch {
std.debug.assert(status == .kErrored);
const error_msg = v8.String.initUtf8(isolate, "Module evaluation failed");
_ = resolver.reject(self.v8_context, error_msg.toValue());
return promise;
};
std.debug.assert(evaluated.isPromise());
gop.value_ptr.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
}
}
// like before, we want to set this up so that if anything else // like before, we want to set this up so that if anything else
// tries to load this module, it can just return our promise // tries to load this module, it can just return our promise
@@ -1460,13 +1454,14 @@ pub fn typeTaggedAnyOpaque(self: *const Context, comptime named_function: NamedF
return error.InvalidArgument; return error.InvalidArgument;
} }
if (!types.has(T)) { const type_name = @typeName(T);
if (@hasField(types.Lookup, type_name) == false) {
@compileError(named_function.full_name ++ "has an unknown Zig type: " ++ @typeName(R)); @compileError(named_function.full_name ++ "has an unknown Zig type: " ++ @typeName(R));
} }
const op = js_obj.getInternalField(0).castTo(v8.External).get(); const op = js_obj.getInternalField(0).castTo(v8.External).get();
const tao: *TaggedAnyOpaque = @ptrCast(@alignCast(op)); const tao: *TaggedAnyOpaque = @ptrCast(@alignCast(op));
const expected_type_index = types.getId(T); const expected_type_index = @field(types.LOOKUP, type_name);
var type_index = tao.index; var type_index = tao.index;
if (type_index == expected_type_index) { if (type_index == expected_type_index) {
@@ -1494,7 +1489,7 @@ pub fn typeTaggedAnyOpaque(self: *const Context, comptime named_function: NamedF
total_offset += @intCast(proto_offset); total_offset += @intCast(proto_offset);
} }
const prototype_index = types.PrototypeTable[type_index]; const prototype_index = types.PROTOTYPE_TABLE[type_index];
if (prototype_index == expected_type_index) { if (prototype_index == expected_type_index) {
return @ptrFromInt(base_ptr + total_offset); return @ptrFromInt(base_ptr + total_offset);
} }
@@ -1587,7 +1582,7 @@ fn probeJsValueToZig(self: *Context, comptime named_function: NamedFunction, com
if (!js_value.isObject()) { if (!js_value.isObject()) {
return .{ .invalid = {} }; return .{ .invalid = {} };
} }
if (types.has(ptr.child)) { if (@hasField(types.Lookup, @typeName(ptr.child))) {
const js_obj = js_value.castTo(v8.Object); const js_obj = js_value.castTo(v8.Object);
// There's a bit of overhead in doing this, so instead // There's a bit of overhead in doing this, so instead
// of having a version of typeTaggedAnyOpaque which // of having a version of typeTaggedAnyOpaque which

View File

@@ -111,14 +111,16 @@ pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
const Struct = s.defaultValue().?; const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) { if (@hasDecl(Struct, "prototype")) {
const TI = @typeInfo(Struct.prototype); const TI = @typeInfo(Struct.prototype);
const ProtoType = types.Receiver(TI.pointer.child); const proto_name = @typeName(types.Receiver(TI.pointer.child));
if (!types.has(ProtoType)) { if (@hasField(types.Lookup, proto_name) == false) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ @typeName(ProtoType), @typeName(Struct) })); @compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ proto_name, @typeName(Struct) }));
} }
// Hey, look! This is our first real usage of the `types.Index`. // Hey, look! This is our first real usage of the types.LOOKUP.
// Just like we said above, given a type, we can get its // Just like we said above, given a type, we can get its
// template index. // template index.
templates[i].inherit(templates[types.getId(ProtoType)]);
const proto_index = @field(types.LOOKUP, proto_name);
templates[i].inherit(templates[proto_index]);
} }
// while we're here, let's populate our meta lookup // while we're here, let's populate our meta lookup

View File

@@ -104,8 +104,10 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_cal
// though it's also a Window, we need to set the prototype for this // though it's also a Window, we need to set the prototype for this
// specific instance of the the Window. // specific instance of the the Window.
if (@hasDecl(Global, "prototype")) { if (@hasDecl(Global, "prototype")) {
const ProtoType = types.Receiver(@typeInfo(Global.prototype).pointer.child); const proto_type = types.Receiver(@typeInfo(Global.prototype).pointer.child);
js_global.inherit(templates[types.getId(ProtoType)]); const proto_name = @typeName(proto_type);
const proto_index = @field(types.LOOKUP, proto_name);
js_global.inherit(templates[proto_index]);
} }
const context_local = v8.Context.init(isolate, global_template, null); const context_local = v8.Context.init(isolate, global_template, null);
@@ -121,12 +123,14 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_cal
const Struct = s.defaultValue().?; const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) { if (@hasDecl(Struct, "prototype")) {
const ProtoType = types.Receiver(@typeInfo(Struct.prototype).pointer.child); const proto_type = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
if (!types.has(ProtoType)) { const proto_name = @typeName(proto_type);
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ @typeName(ProtoType)); if (@hasField(types.Lookup, proto_name) == false) {
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name);
} }
const proto_obj = templates[types.getId(ProtoType)].getFunction(v8_context).toObject(); const proto_index = @field(types.LOOKUP, proto_name);
const proto_obj = templates[proto_index].getFunction(v8_context).toObject();
const self_obj = templates[i].getFunction(v8_context).toObject(); const self_obj = templates[i].getFunction(v8_context).toObject();
_ = self_obj.setPrototype(v8_context, proto_obj); _ = self_obj.setPrototype(v8_context, proto_obj);

View File

@@ -48,6 +48,8 @@ const NamedFunction = Context.NamedFunction;
// Env.JsObject. Want a TypedArray? Env.TypedArray. // Env.JsObject. Want a TypedArray? Env.TypedArray.
pub fn TypedArray(comptime T: type) type { pub fn TypedArray(comptime T: type) type {
return struct { return struct {
pub const _TYPED_ARRAY_ID_KLUDGE = true;
values: []const T, values: []const T,
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) { pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
@@ -148,8 +150,6 @@ pub const Exception = struct {
}; };
pub const Value = struct { pub const Value = struct {
const PersistentValue = v8.Persistent(v8.Value);
value: v8.Value, value: v8.Value,
context: *const Context, context: *const Context,
@@ -163,15 +163,6 @@ pub const Value = struct {
const value = try v8.Json.parse(ctx.v8_context, json_string); const value = try v8.Json.parse(ctx.v8_context, json_string);
return Value{ .context = ctx, .value = value }; return Value{ .context = ctx, .value = value };
} }
pub fn persist(self: Value, context: *Context) !Value {
const js_value = self.value;
const persisted = PersistentValue.init(context.isolate, js_value);
try context.js_value_list.append(context.arena, persisted);
return Value{ .context = context, .value = persisted.toValue() };
}
}; };
pub const ValueIterator = struct { pub const ValueIterator = struct {
@@ -336,73 +327,68 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
return v8.initNull(isolate).toValue(); return v8.initNull(isolate).toValue();
}, },
.@"struct" => { .@"struct" => {
switch (@TypeOf(value)) { const T = @TypeOf(value);
ArrayBuffer => {
const values = value.values; if (T == ArrayBuffer) {
const len = values.len; const values = value.values;
var array_buffer: v8.ArrayBuffer = undefined; const len = values.len;
const backing_store = v8.BackingStore.init(isolate, 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);
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData())); const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]); @memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr()); array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
}
return .{ .handle = array_buffer.handle }; switch (@typeInfo(value_type)) {
}, .int => |n| switch (n.signedness) {
// zig fmt: off .unsigned => switch (n.bits) {
TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64), 8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
TypedArray(i8), TypedArray(i16), TypedArray(i32), TypedArray(i64), 16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
TypedArray(f32), TypedArray(f64), 32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
// zig fmt: on 64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
=> {
const values = value.values;
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
const len = values.len;
const bits = switch (@typeInfo(value_type)) {
.int => |n| n.bits,
.float => |f| f.bits,
else => @compileError("Invalid TypedArray type: " ++ @typeName(value_type)),
};
var array_buffer: v8.ArrayBuffer = undefined;
if (len == 0) {
array_buffer = v8.ArrayBuffer.init(isolate, 0);
} else {
const buffer_len = len * bits / 8;
const backing_store = v8.BackingStore.init(isolate, buffer_len);
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
}
switch (@typeInfo(value_type)) {
.int => |n| switch (n.signedness) {
.unsigned => switch (n.bits) {
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
.signed => switch (n.bits) {
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
},
.float => |f| switch (f.bits) {
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
else => {}, else => {},
}, },
.signed => switch (n.bits) {
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
},
.float => |f| switch (f.bits) {
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
else => {}, else => {},
} },
// We normally don't fail in this function unless fail == true else => {},
// but this can never be valid. }
@compileError("Invalid TypedArray type: " ++ @typeName(value_type)); // We normally don't fail in this function unless fail == true
}, // but this can never be valid.
else => {}, @compileError("Invalid TypeArray type: " ++ @typeName(value_type));
} }
}, },
.@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail), .@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail),

View File

@@ -18,7 +18,6 @@ const Interfaces = generate.Tuple(.{
@import("../xhr/xhr.zig").Interfaces, @import("../xhr/xhr.zig").Interfaces,
@import("../navigation/root.zig").Interfaces, @import("../navigation/root.zig").Interfaces,
@import("../file/root.zig").Interfaces, @import("../file/root.zig").Interfaces,
@import("../canvas/root.zig").Interfaces,
@import("../xhr/form_data.zig").Interfaces, @import("../xhr/form_data.zig").Interfaces,
@import("../xmlserializer/xmlserializer.zig").Interfaces, @import("../xmlserializer/xmlserializer.zig").Interfaces,
@import("../fetch/fetch.zig").Interfaces, @import("../fetch/fetch.zig").Interfaces,
@@ -27,127 +26,104 @@ const Interfaces = generate.Tuple(.{
pub const Types = @typeInfo(Interfaces).@"struct".fields; pub const Types = @typeInfo(Interfaces).@"struct".fields;
/// Integer type we use for `Index` enum. Can be u8 at min. // Imagine we have a type Cat which has a getter:
pub const BackingInt = std.math.IntFittingRange(0, @max(std.math.maxInt(u8), Types.len)); //
// fn get_owner(self: *Cat) *Owner {
/// Imagine we have a type `Cat` which has a getter: // return self.owner;
/// // }
/// 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});
/// //
/// When we execute `caller.getter`, we'll end up doing something like: // 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
/// const res = @call(.auto, Cat.get_owner, .{cat_instance}); // get that? Well, we store all the ObjectTemplates in an array that's
/// // tied to env. So we do something like:
/// 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 // env.templates[index_of_owner].initInstance(...);
/// get that? Well, we store all the ObjectTemplates in an array that's //
/// tied to env. So we do something like: // But how do we get that `index_of_owner`? `Lookup` is a struct
/// // that looks like:
/// env.templates[index_of_owner].initInstance(...); //
/// // const Lookup = struct {
/// But how do we get that `index_of_owner`? `Index` is an enum // comptime cat: usize = 0,
/// that looks like: // comptime owner: usize = 1,
/// // ...
/// pub const Index = enum(BackingInt) { // }
/// cat = 0, //
/// owner = 1, // So to get the template index of `owner`, we can do:
/// ... //
/// } // const index_id = @field(type_lookup, @typeName(@TypeOf(res));
/// //
/// (`BackingInt` is calculated at comptime regarding to interfaces we have) pub const Lookup = blk: {
/// So to get the template index of `owner`, simply do: var fields: [Types.len]std.builtin.Type.StructField = undefined;
///
/// const index_id = types.getId(@TypeOf(res));
pub const Index = blk: {
var fields: [Types.len]std.builtin.Type.EnumField = undefined;
for (Types, 0..) |s, i| { for (Types, 0..) |s, i| {
// This prototype type check has nothing to do with building our
// Lookup. But we put it here, early, so that the rest of the
// code doesn't have to worry about checking if Struct.prototype is
// a pointer.
const Struct = s.defaultValue().?; const Struct = s.defaultValue().?;
fields[i] = .{ .name = @typeName(Receiver(Struct)), .value = i }; 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) }));
}
break :blk @Type(.{ fields[i] = .{
.@"enum" = .{ .name = @typeName(Receiver(Struct)),
.fields = &fields, .type = usize,
.tag_type = BackingInt, .is_comptime = true,
.is_exhaustive = true, .alignment = @alignOf(usize),
.decls = &.{}, .default_value_ptr = &i,
},
});
};
/// Returns a boolean indicating if a type exist in the `Index`.
pub inline fn has(t: type) bool {
return @hasField(Index, @typeName(t));
}
/// 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| {
const Struct = s.defaultValue().?;
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 @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.is_tuple = false,
.fields = &fields,
} });
};
pub const LOOKUP = Lookup{};
// 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;
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;
}
break :blk table; break :blk table;
}; };
/// This is essentially meta data for each type. Each is stored in `env.meta_lookup`. // This is essentially meta data for each type. Each is stored in env.meta_lookup
/// The index for a type can be retrieved via: // The index for a type can be retrieved via:
/// const index = types.getIndex(Receiver(Struct)); // const index = @field(TYPE_LOOKUP, @typeName(Receiver(Struct)));
/// const meta = env.meta_lookup[@intFromEnum(index)]; // const meta = env.meta_lookup[index];
///
/// Or:
/// const id = types.getId(Receiver(Struct));
/// const meta = env.meta_lookup[id];
pub const Meta = struct { pub const Meta = struct {
// Every type is given a unique index. That index is used to lookup various // Every type is given a unique index. That index is used to lookup various
// things, i.e. the prototype chain. // things, i.e. the prototype chain.
index: BackingInt, index: u16,
// We store the type's subtype here, so that when we create an instance of // We store the type's subtype here, so that when we create an instance of
// the type, and bind it to JavaScript, we can store the subtype along with // the type, and bind it to JavaScript, we can store the subtype along with

View File

@@ -24,7 +24,6 @@ pub const Mime = struct {
// IANA defines max. charset value length as 40. // IANA defines max. charset value length as 40.
// We keep 41 for null-termination since HTML parser expects in this format. // We keep 41 for null-termination since HTML parser expects in this format.
charset: [41]u8 = default_charset, charset: [41]u8 = default_charset,
charset_len: usize = 5,
/// String "UTF-8" continued by null characters. /// String "UTF-8" continued by null characters.
pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36; pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
@@ -54,25 +53,9 @@ pub const Mime = struct {
other: struct { type: []const u8, sub_type: []const u8 }, other: struct { type: []const u8, sub_type: []const u8 },
}; };
pub fn contentTypeString(mime: *const Mime) []const u8 {
return switch (mime.content_type) {
.text_xml => "text/xml",
.text_html => "text/html",
.text_javascript => "application/javascript",
.text_plain => "text/plain",
.text_css => "text/css",
.application_json => "application/json",
else => "",
};
}
/// Returns the null-terminated charset value. /// Returns the null-terminated charset value.
pub fn charsetStringZ(mime: *const Mime) [:0]const u8 { pub fn charsetString(mime: *const Mime) [:0]const u8 {
return mime.charset[0..mime.charset_len :0]; return @ptrCast(&mime.charset);
}
pub fn charsetString(mime: *const Mime) []const u8 {
return mime.charset[0..mime.charset_len];
} }
/// Removes quotes of value if quotes are given. /// Removes quotes of value if quotes are given.
@@ -116,7 +99,6 @@ pub const Mime = struct {
const params = trimLeft(normalized[type_len..]); const params = trimLeft(normalized[type_len..]);
var charset: [41]u8 = undefined; var charset: [41]u8 = undefined;
var charset_len: usize = undefined;
var it = std.mem.splitScalar(u8, params, ';'); var it = std.mem.splitScalar(u8, params, ';');
while (it.next()) |attr| { while (it.next()) |attr| {
@@ -142,7 +124,6 @@ pub const Mime = struct {
@memcpy(charset[0..attribute_value.len], attribute_value); @memcpy(charset[0..attribute_value.len], attribute_value);
// Null-terminate right after attribute value. // Null-terminate right after attribute value.
charset[attribute_value.len] = 0; charset[attribute_value.len] = 0;
charset_len = attribute_value.len;
}, },
} }
} }
@@ -150,7 +131,6 @@ pub const Mime = struct {
return .{ return .{
.params = params, .params = params,
.charset = charset, .charset = charset,
.charset_len = charset_len,
.content_type = content_type, .content_type = content_type,
}; };
} }
@@ -531,9 +511,9 @@ fn expect(expected: Expectation, input: []const u8) !void {
if (expected.charset) |ec| { if (expected.charset) |ec| {
// We remove the null characters for testing purposes here. // We remove the null characters for testing purposes here.
try testing.expectEqual(ec, actual.charsetString()); try testing.expectEqual(ec, actual.charsetString()[0..ec.len]);
} else { } else {
const m: Mime = .unknown; const m: Mime = .unknown;
try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ()); try testing.expectEqual(m.charsetString(), actual.charsetString());
} }
} }

View File

@@ -35,7 +35,6 @@ const Navigation = @This();
const NavigationKind = @import("root.zig").NavigationKind; const NavigationKind = @import("root.zig").NavigationKind;
const NavigationHistoryEntry = @import("root.zig").NavigationHistoryEntry; const NavigationHistoryEntry = @import("root.zig").NavigationHistoryEntry;
const NavigationTransition = @import("root.zig").NavigationTransition; const NavigationTransition = @import("root.zig").NavigationTransition;
const NavigationState = @import("root.zig").NavigationState;
const NavigationCurrentEntryChangeEvent = @import("root.zig").NavigationCurrentEntryChangeEvent; const NavigationCurrentEntryChangeEvent = @import("root.zig").NavigationCurrentEntryChangeEvent;
const NavigationEventTarget = @import("NavigationEventTarget.zig"); const NavigationEventTarget = @import("NavigationEventTarget.zig");
@@ -111,10 +110,10 @@ pub fn _forward(self: *Navigation, page: *Page) !NavigationReturn {
pub fn updateEntries(self: *Navigation, url: []const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void { pub fn updateEntries(self: *Navigation, url: []const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void {
switch (kind) { switch (kind) {
.replace => { .replace => {
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = null }, page, dispatch); _ = try self.replaceEntry(url, null, page, dispatch);
}, },
.push => |state| { .push => |state| {
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, dispatch); _ = try self.pushEntry(url, state, page, dispatch);
}, },
.traverse => |index| { .traverse => |index| {
self.index = index; self.index = index;
@@ -133,13 +132,7 @@ pub fn processNavigation(self: *Navigation, page: *Page) !void {
/// Pushes an entry into the Navigation stack WITHOUT actually navigating to it. /// Pushes an entry into the Navigation stack WITHOUT actually navigating to it.
/// For that, use `navigate`. /// For that, use `navigate`.
pub fn pushEntry( pub fn pushEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry {
self: *Navigation,
_url: []const u8,
state: NavigationState,
page: *Page,
dispatch: bool,
) !*NavigationHistoryEntry {
const arena = page.session.arena; const arena = page.session.arena;
const url = try arena.dupe(u8, _url); const url = try arena.dupe(u8, _url);
@@ -167,24 +160,18 @@ pub fn pushEntry(
// we don't always have a current entry... // we don't always have a current entry...
const previous = if (self.entries.items.len > 0) self.currentEntry() else null; const previous = if (self.entries.items.len > 0) self.currentEntry() else null;
try self.entries.append(arena, entry); try self.entries.append(arena, entry);
self.index = index;
if (previous) |prev| { if (previous) |prev| {
if (dispatch) { if (dispatch) {
NavigationCurrentEntryChangeEvent.dispatch(self, prev, .push); NavigationCurrentEntryChangeEvent.dispatch(self, prev, .push);
} }
} }
self.index = index;
return entry; return entry;
} }
pub fn replaceEntry( pub fn replaceEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry {
self: *Navigation,
_url: []const u8,
state: NavigationState,
page: *Page,
dispatch: bool,
) !*NavigationHistoryEntry {
const arena = page.session.arena; const arena = page.session.arena;
const url = try arena.dupe(u8, _url); const url = try arena.dupe(u8, _url);
@@ -197,7 +184,7 @@ pub fn replaceEntry(
const entry = try arena.create(NavigationHistoryEntry); const entry = try arena.create(NavigationHistoryEntry);
entry.* = NavigationHistoryEntry{ entry.* = NavigationHistoryEntry{
.id = id_str, .id = id_str,
.key = previous.key, .key = id_str,
.url = url, .url = url,
.state = state, .state = state,
}; };
@@ -255,20 +242,7 @@ pub fn navigate(
// todo: Fire navigate event // todo: Fire navigate event
try finished.resolve({}); try finished.resolve({});
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true); _ = try self.pushEntry(url, state, page, true);
} else {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
}
},
.replace => |state| {
if (is_same_document) {
page.url = new_url;
try committed.resolve({});
// todo: Fire navigate event
try finished.resolve({});
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true);
} else { } else {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind); try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
} }
@@ -289,6 +263,7 @@ pub fn navigate(
.reload => { .reload => {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind); try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
}, },
else => unreachable,
} }
return .{ return .{
@@ -300,13 +275,7 @@ pub fn navigate(
pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn { pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn {
const opts = _opts orelse NavigateOptions{}; const opts = _opts orelse NavigateOptions{};
const json = if (opts.state) |state| state.toJson(page.session.arena) catch return error.DataClone else null; const json = if (opts.state) |state| state.toJson(page.session.arena) catch return error.DataClone else null;
return try self.navigate(_url, .{ .push = json }, page);
const kind: NavigationKind = switch (opts.history) {
.replace => .{ .replace = json },
.push, .auto => .{ .push = json },
};
return try self.navigate(_url, kind, page);
} }
pub const ReloadOptions = struct { pub const ReloadOptions = struct {
@@ -321,7 +290,7 @@ pub fn _reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !Navigatio
const entry = self.currentEntry(); const entry = self.currentEntry();
if (opts.state) |state| { if (opts.state) |state| {
const previous = entry; const previous = entry;
entry.state = .{ .source = .navigation, .value = state.toJson(arena) catch return error.DataClone }; entry.state = state.toJson(arena) catch return error.DataClone;
NavigationCurrentEntryChangeEvent.dispatch(self, previous, .reload); NavigationCurrentEntryChangeEvent.dispatch(self, previous, .reload);
} }
@@ -354,6 +323,6 @@ pub fn _updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions
const arena = page.session.arena; const arena = page.session.arena;
const previous = self.currentEntry(); const previous = self.currentEntry();
self.currentEntry().state = .{ .source = .navigation, .value = options.state.toJson(arena) catch return error.DataClone }; self.currentEntry().state = options.state.toJson(arena) catch return error.DataClone;
NavigationCurrentEntryChangeEvent.dispatch(self, previous, null); NavigationCurrentEntryChangeEvent.dispatch(self, previous, null);
} }

View File

@@ -51,16 +51,11 @@ pub const NavigationType = enum {
pub const NavigationKind = union(NavigationType) { pub const NavigationKind = union(NavigationType) {
push: ?[]const u8, push: ?[]const u8,
replace: ?[]const u8, replace,
traverse: usize, traverse: usize,
reload, reload,
}; };
pub const NavigationState = struct {
source: enum { history, navigation },
value: ?[]const u8,
};
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry // https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry
pub const NavigationHistoryEntry = struct { pub const NavigationHistoryEntry = struct {
pub const prototype = *EventTarget; pub const prototype = *EventTarget;
@@ -69,7 +64,7 @@ pub const NavigationHistoryEntry = struct {
id: []const u8, id: []const u8,
key: []const u8, key: []const u8,
url: ?[]const u8, url: ?[]const u8,
state: NavigationState, state: ?[]const u8,
pub fn get_id(self: *const NavigationHistoryEntry) []const u8 { pub fn get_id(self: *const NavigationHistoryEntry) []const u8 {
return self.id; return self.id;
@@ -100,16 +95,12 @@ pub const NavigationHistoryEntry = struct {
return self.url; return self.url;
} }
pub const StateReturn = union(enum) { value: ?js.Value, undefined: void }; pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !?js.Value {
if (self.state) |state| {
pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !StateReturn { return try js.Value.fromJson(page.js, state);
if (self.state.source == .navigation) { } else {
if (self.state.value) |value| { return null;
return .{ .value = try js.Value.fromJson(page.js, value) };
}
} }
return .undefined;
} }
}; };
@@ -217,6 +208,138 @@ pub const NavigationCurrentEntryChangeEvent = struct {
} }
}; };
pub const NavigateEvent = struct {
pub const prototype = *Event;
pub const union_make_copy = true;
pub const EventInit = struct {
canIntercept: ?bool = false,
// todo: destination
downloadRequest: ?[]const u8 = null,
// todo: formData
hashChange: ?bool = false,
hasUAVisualTransition: ?bool = false,
// info: ?js.Value,
navigationType: ?NavigationType = .push,
// todo: signal
// todo: sourceElement
userInitiated: ?bool = false,
};
proto: parser.Event,
can_intercept: bool,
download_request: ?[]const u8,
// todo: desintation
hash_change: bool,
has_ua_visual_transition: bool,
info: []const u8,
navigation_type: NavigationType,
// todo: signal
// todo: sourceElement
user_initiated: bool,
pub fn constructor(event_type: []const u8, opts: EventInit) !NavigateEvent {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .navigate_event);
return .{
.proto = event.*,
.can_intercept = opts.canIntercept orelse false,
.download_request = opts.downloadRequest orelse null,
.hash_change = opts.hashChange orelse false,
.has_ua_visual_transition = opts.hasUAVisualTransition orelse false,
.info = undefined,
.navigation_type = opts.navigationType orelse .push,
// .info = if (opts.info) |info| try info.toString(page.arena) else null,
.user_initiated = opts.userInitiated orelse false,
};
}
pub const InterceptOptions = struct {
// runs after currentEntry is updated
handler: ?js.Function,
// runs before currentEntry is updated
precommitHandler: ?js.Function,
focusReset: ?enum { @"after-transition", manual },
scroll: ?enum { @"after-transition", manual },
};
// https://developer.mozilla.org/en-US/docs/Web/API/NavigateEvent/intercept
pub fn intercept(
self: *NavigateEvent,
opts: ?InterceptOptions,
_: *Page,
) !void {
if (!self.can_intercept) {
return error.Security;
}
if (self.proto.in_dispatch) {
return error.InvalidState;
}
// try parser.eventPreventDefault(&self.proto);
if (opts) |options| {
if (options.precommitHandler) |handler| {
_ = try handler.call(void, .{});
}
}
// update current entry here.
if (opts) |options| {
if (options.handler) |handler| {
const result = try handler.call(js.Value, .{});
if (result.value.isPromise()) {
// must be stored and resolved based on nav outcome.
}
}
// todo: handle focusReset and scroll
}
}
pub fn scroll(self: *NavigateEvent) !void {
_ = self;
}
pub fn dispatch(navigation: *Navigation, from: *NavigationHistoryEntry, typ: ?NavigationType) void {
log.debug(.script_event, "dispatch event", .{
.type = "navigate",
.source = "navigation",
});
var evt = NavigateEvent.constructor(
"navigate",
.{ .from = from, .navigationType = typ },
) catch |err| {
log.err(.app, "event constructor error", .{
.err = err,
.type = "navigate",
.source = "navigation",
});
return;
};
_ = parser.eventTargetDispatchEvent(
@as(*parser.EventTarget, @ptrCast(navigation)),
&evt.proto,
) catch |err| {
log.err(.app, "dispatch event error", .{
.err = err,
.type = "navigate",
.source = "navigation",
});
};
}
};
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: Navigation" { test "Browser: Navigation" {
try testing.htmlRunner("html/navigation/navigation.html"); try testing.htmlRunner("html/navigation/navigation.html");

View File

@@ -561,7 +561,7 @@ pub const EventType = enum(u8) {
pop_state = 9, pop_state = 9,
composition_event = 10, composition_event = 10,
navigation_current_entry_change_event = 11, navigation_current_entry_change_event = 11,
page_transition_event = 12, navigate_event = 12,
}; };
pub const MutationEvent = c.dom_mutation_event; pub const MutationEvent = c.dom_mutation_event;

View File

@@ -37,7 +37,6 @@ const HTMLDocument = @import("html/document.zig").HTMLDocument;
const NavigationKind = @import("navigation/root.zig").NavigationKind; const NavigationKind = @import("navigation/root.zig").NavigationKind;
const NavigationCurrentEntryChangeEvent = @import("navigation/root.zig").NavigationCurrentEntryChangeEvent; const NavigationCurrentEntryChangeEvent = @import("navigation/root.zig").NavigationCurrentEntryChangeEvent;
const PageTransitionEvent = @import("events/PageTransitionEvent.zig");
const js = @import("js/js.zig"); const js = @import("js/js.zig");
const URL = @import("../url.zig").URL; const URL = @import("../url.zig").URL;
@@ -83,8 +82,6 @@ pub const Page = struct {
// indicates intention to navigate to another page on the next loop execution. // indicates intention to navigate to another page on the next loop execution.
delayed_navigation: bool = false, delayed_navigation: bool = false,
req_id: ?usize = null,
navigated_options: ?NavigatedOpts = null,
state_pool: *std.heap.MemoryPool(State), state_pool: *std.heap.MemoryPool(State),
@@ -105,10 +102,6 @@ pub const Page = struct {
notified_network_idle: IdleNotification = .init, notified_network_idle: IdleNotification = .init,
notified_network_almost_idle: IdleNotification = .init, notified_network_almost_idle: IdleNotification = .init,
// Indicates if the page's document is open or close.
// Relates with https://developer.mozilla.org/en-US/docs/Web/API/Document/open
open: bool = false,
const Mode = union(enum) { const Mode = union(enum) {
pre: void, pre: void,
err: anyerror, err: anyerror,
@@ -175,9 +168,6 @@ pub const Page = struct {
self.http_client.abort(); self.http_client.abort();
self.script_manager.reset(); self.script_manager.reset();
parser.deinit();
parser.init();
self.load_state = .parsing; self.load_state = .parsing;
self.mode = .{ .pre = {} }; self.mode = .{ .pre = {} };
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); _ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
@@ -555,14 +545,11 @@ pub const Page = struct {
try self.reset(); try self.reset();
} }
const req_id = self.http_client.nextReqId();
log.info(.http, "navigate", .{ log.info(.http, "navigate", .{
.url = request_url, .url = request_url,
.method = opts.method, .method = opts.method,
.reason = opts.reason, .reason = opts.reason,
.body = opts.body != null, .body = opts.body != null,
.req_id = req_id,
}); });
// if the url is about:blank, we load an empty HTML document in the // if the url is about:blank, we load an empty HTML document in the
@@ -580,39 +567,22 @@ pub const Page = struct {
self.documentIsComplete(); self.documentIsComplete();
self.session.browser.notification.dispatch(.page_navigate, &.{ self.session.browser.notification.dispatch(.page_navigate, &.{
.req_id = req_id,
.opts = opts, .opts = opts,
.url = request_url, .url = request_url,
.timestamp = timestamp(), .timestamp = timestamp(),
}); });
self.session.browser.notification.dispatch(.page_navigated, &.{ self.session.browser.notification.dispatch(.page_navigated, &.{
.req_id = req_id,
.opts = .{
.cdp_id = opts.cdp_id,
.reason = opts.reason,
.method = opts.method,
},
.url = request_url, .url = request_url,
.timestamp = timestamp(), .timestamp = timestamp(),
}); });
// force next request id manually b/c we won't create a real req.
_ = self.http_client.incrReqId();
return; return;
} }
const owned_url = try self.arena.dupeZ(u8, request_url); const owned_url = try self.arena.dupeZ(u8, request_url);
self.url = try URL.parse(owned_url, null); self.url = try URL.parse(owned_url, null);
self.req_id = req_id;
self.navigated_options = .{
.cdp_id = opts.cdp_id,
.reason = opts.reason,
.method = opts.method,
};
var headers = try self.http_client.newHeaders(); var headers = try self.http_client.newHeaders();
if (opts.header) |hdr| try headers.add(hdr); if (opts.header) |hdr| try headers.add(hdr);
try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, owned_url, &headers); try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, owned_url, &headers);
@@ -620,7 +590,6 @@ pub const Page = struct {
// We dispatch page_navigate event before sending the request. // We dispatch page_navigate event before sending the request.
// It ensures the event page_navigated is not dispatched before this one. // It ensures the event page_navigated is not dispatched before this one.
self.session.browser.notification.dispatch(.page_navigate, &.{ self.session.browser.notification.dispatch(.page_navigate, &.{
.req_id = req_id,
.opts = opts, .opts = opts,
.url = owned_url, .url = owned_url,
.timestamp = timestamp(), .timestamp = timestamp(),
@@ -686,20 +655,13 @@ pub const Page = struct {
log.err(.browser, "document is complete", .{ .err = err }); log.err(.browser, "document is complete", .{ .err = err });
}; };
std.debug.assert(self.req_id != null);
std.debug.assert(self.navigated_options != null);
self.session.browser.notification.dispatch(.page_navigated, &.{ self.session.browser.notification.dispatch(.page_navigated, &.{
.req_id = self.req_id.?,
.opts = self.navigated_options.?,
.url = self.url.raw, .url = self.url.raw,
.timestamp = timestamp(), .timestamp = timestamp(),
}); });
} }
fn _documentIsComplete(self: *Page) !void { fn _documentIsComplete(self: *Page) !void {
self.session.browser.runMicrotasks();
self.session.browser.runMessageLoop();
try HTMLDocument.documentIsComplete(self.window.document, self); try HTMLDocument.documentIsComplete(self.window.document, self);
// dispatch window.load event // dispatch window.load event
@@ -712,8 +674,6 @@ pub const Page = struct {
parser.toEventTarget(Window, &self.window), parser.toEventTarget(Window, &self.window),
loadevt, loadevt,
); );
PageTransitionEvent.dispatch(&self.window, .show, false);
} }
fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void { fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void {
@@ -747,14 +707,14 @@ pub const Page = struct {
log.debug(.http, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len }); log.debug(.http, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len });
self.mode = switch (mime.content_type) { self.mode = switch (mime.content_type) {
.text_html => .{ .html = try parser.Parser.init(mime.charsetStringZ()) }, .text_html => .{ .html = try parser.Parser.init(mime.charsetString()) },
.application_json, .application_json,
.text_javascript, .text_javascript,
.text_css, .text_css,
.text_plain, .text_plain,
=> blk: { => blk: {
var p = try parser.Parser.init(mime.charsetStringZ()); var p = try parser.Parser.init(mime.charsetString());
try p.process("<html><head><meta charset=\"utf-8\"></head><body><pre>"); try p.process("<html><head><meta charset=\"utf-8\"></head><body><pre>");
break :blk .{ .text = p }; break :blk .{ .text = p };
}, },
@@ -796,9 +756,6 @@ pub const Page = struct {
var self: *Page = @ptrCast(@alignCast(ctx)); var self: *Page = @ptrCast(@alignCast(ctx));
self.clearTransferArena(); self.clearTransferArena();
// We need to handle different navigation types differently.
try self.session.navigation.processNavigation(self);
switch (self.mode) { switch (self.mode) {
.pre => { .pre => {
// Received a response without a body like: https://httpbin.io/status/200 // Received a response without a body like: https://httpbin.io/status/200
@@ -877,6 +834,9 @@ pub const Page = struct {
unreachable; unreachable;
}, },
} }
// We need to handle different navigation types differently.
try self.session.navigation.processNavigation(self);
} }
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void { fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
@@ -954,7 +914,7 @@ pub const Page = struct {
fn windowClicked(node: *parser.EventNode, event: *parser.Event) void { fn windowClicked(node: *parser.EventNode, event: *parser.Event) void {
const self: *Page = @fieldParentPtr("window_clicked_event_node", node); const self: *Page = @fieldParentPtr("window_clicked_event_node", node);
self._windowClicked(event) catch |err| { self._windowClicked(event) catch |err| {
log.err(.input, "click handler error", .{ .err = err }); log.err(.browser, "click handler error", .{ .err = err });
}; };
} }
@@ -966,22 +926,18 @@ pub const Page = struct {
.a => { .a => {
const element: *parser.Element = @ptrCast(node); const element: *parser.Element = @ptrCast(node);
const href = (try parser.elementGetAttribute(element, "href")) orelse return; const href = (try parser.elementGetAttribute(element, "href")) orelse return;
log.debug(.input, "window click on link", .{ .tag = tag, .href = href });
try self.navigateFromWebAPI(href, .{}, .{ .push = null }); try self.navigateFromWebAPI(href, .{}, .{ .push = null });
return;
}, },
.input => { .input => {
const element: *parser.Element = @ptrCast(node); const element: *parser.Element = @ptrCast(node);
const input_type = try parser.inputGetType(@ptrCast(element)); const input_type = try parser.inputGetType(@ptrCast(element));
if (std.ascii.eqlIgnoreCase(input_type, "submit")) { if (std.ascii.eqlIgnoreCase(input_type, "submit")) {
log.debug(.input, "window click on submit input", .{ .tag = tag });
return self.elementSubmitForm(element); return self.elementSubmitForm(element);
} }
}, },
.button => { .button => {
const element: *parser.Element = @ptrCast(node); const element: *parser.Element = @ptrCast(node);
const button_type = try parser.buttonGetType(@ptrCast(element)); const button_type = try parser.buttonGetType(@ptrCast(element));
log.debug(.input, "window click on button", .{ .tag = tag, .button_type = button_type });
if (std.ascii.eqlIgnoreCase(button_type, "submit")) { if (std.ascii.eqlIgnoreCase(button_type, "submit")) {
return self.elementSubmitForm(element); return self.elementSubmitForm(element);
} }
@@ -993,12 +949,6 @@ pub const Page = struct {
}, },
else => {}, else => {},
} }
log.debug(.input, "window click on element", .{ .tag = tag });
// Set the focus on the clicked element.
// Thanks to parser.nodeHTMLGetTagType, we know nod is an element.
// We assume we have a ElementHTML.
const Document = @import("dom/document.zig").Document;
try Document.setFocus(@ptrCast(self.window.document), @as(*parser.ElementHTML, @ptrCast(node)), self);
} }
pub const KeyboardEvent = struct { pub const KeyboardEvent = struct {
@@ -1041,7 +991,7 @@ pub const Page = struct {
fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void { fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void {
const self: *Page = @fieldParentPtr("keydown_event_node", node); const self: *Page = @fieldParentPtr("keydown_event_node", node);
self._keydownCallback(event) catch |err| { self._keydownCallback(event) catch |err| {
log.err(.input, "keydown handler error", .{ .err = err }); log.err(.browser, "keydown handler error", .{ .err = err });
}; };
} }
@@ -1055,29 +1005,23 @@ pub const Page = struct {
if (std.mem.eql(u8, new_key, "Dead")) { if (std.mem.eql(u8, new_key, "Dead")) {
return; return;
} }
switch (tag) { switch (tag) {
.input => { .input => {
const element: *parser.Element = @ptrCast(node); const element: *parser.Element = @ptrCast(node);
const input_type = try parser.inputGetType(@ptrCast(element)); const input_type = try parser.inputGetType(@ptrCast(element));
log.debug(.input, "key down on input", .{ .tag = tag, .key = new_key, .input_type = input_type }); if (std.mem.eql(u8, input_type, "text")) {
if (std.mem.eql(u8, new_key, "Enter")) { if (std.mem.eql(u8, new_key, "Enter")) {
const form = (try self.formForElement(element)) orelse return; const form = (try self.formForElement(element)) orelse return;
return self.submitForm(@ptrCast(form), null); return self.submitForm(@ptrCast(form), null);
} }
if (std.mem.eql(u8, input_type, "radio")) { const value = try parser.inputGetValue(@ptrCast(element));
return; const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
try parser.inputSetValue(@ptrCast(element), new_value);
} }
if (std.mem.eql(u8, input_type, "checkbox")) {
return;
}
const value = try parser.inputGetValue(@ptrCast(element));
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
try parser.inputSetValue(@ptrCast(element), new_value);
}, },
.textarea => { .textarea => {
log.debug(.input, "key down on textarea", .{ .tag = tag, .key = new_key });
const value = try parser.textareaGetValue(@ptrCast(node)); const value = try parser.textareaGetValue(@ptrCast(node));
if (std.mem.eql(u8, new_key, "Enter")) { if (std.mem.eql(u8, new_key, "Enter")) {
new_key = "\n"; new_key = "\n";
@@ -1085,9 +1029,7 @@ pub const Page = struct {
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key }); const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
try parser.textareaSetValue(@ptrCast(node), new_value); try parser.textareaSetValue(@ptrCast(node), new_value);
}, },
else => { else => {},
log.debug(.input, "key down event", .{ .tag = tag, .key = new_key });
},
} }
} }
@@ -1274,10 +1216,6 @@ pub const Page = struct {
const current_origin = try self.origin(self.call_arena); const current_origin = try self.origin(self.call_arena);
return std.mem.startsWith(u8, url, current_origin); return std.mem.startsWith(u8, url, current_origin);
} }
pub fn getTitle(self: *const Page) ![]const u8 {
return try parser.documentHTMLGetTitle(self.window.document);
}
}; };
pub const NavigateReason = enum { pub const NavigateReason = enum {
@@ -1298,12 +1236,6 @@ pub const NavigateOpts = struct {
force: bool = false, force: bool = false,
}; };
pub const NavigatedOpts = struct {
cdp_id: ?i64 = null,
reason: NavigateReason = .address_bar,
method: Http.Method = .GET,
};
const IdleNotification = union(enum) { const IdleNotification = union(enum) {
// hasn't started yet. // hasn't started yet.
init, init,

View File

@@ -31,7 +31,6 @@ const Mime = @import("../mime.zig").Mime;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Http = @import("../../http/Http.zig"); const Http = @import("../../http/Http.zig");
const js = @import("../js/js.zig");
// XHR interfaces // XHR interfaces
// https://xhr.spec.whatwg.org/#interface-xmlhttprequest // https://xhr.spec.whatwg.org/#interface-xmlhttprequest
@@ -129,19 +128,21 @@ pub const XMLHttpRequest = struct {
JSON, JSON,
}; };
const JSONValue = std.json.Value;
const Response = union(ResponseType) { const Response = union(ResponseType) {
Empty: void, Empty: void,
Text: []const u8, Text: []const u8,
ArrayBuffer: void, ArrayBuffer: void,
Blob: void, Blob: void,
Document: *parser.Document, Document: *parser.Document,
JSON: js.Value, JSON: JSONValue,
}; };
const ResponseObj = union(enum) { const ResponseObj = union(enum) {
Document: *parser.Document, Document: *parser.Document,
Failure: void, Failure: void,
JSON: js.Value, JSON: JSONValue,
fn deinit(self: ResponseObj) void { fn deinit(self: ResponseObj) void {
switch (self) { switch (self) {
@@ -604,7 +605,7 @@ pub const XMLHttpRequest = struct {
} }
// https://xhr.spec.whatwg.org/#the-response-attribute // https://xhr.spec.whatwg.org/#the-response-attribute
pub fn get_response(self: *XMLHttpRequest, page: *Page) !?Response { pub fn get_response(self: *XMLHttpRequest) !?Response {
if (self.response_type == .Empty or self.response_type == .Text) { if (self.response_type == .Empty or self.response_type == .Text) {
if (self.state == .loading or self.state == .done) { if (self.state == .loading or self.state == .done) {
return .{ .Text = try self.get_responseText() }; return .{ .Text = try self.get_responseText() };
@@ -651,7 +652,7 @@ pub const XMLHttpRequest = struct {
// TODO Let jsonObject be the result of running parse JSON from bytes // TODO Let jsonObject be the result of running parse JSON from bytes
// on thiss received bytes. If that threw an exception, then return // on thiss received bytes. If that threw an exception, then return
// null. // null.
self.setResponseObjJSON(page); self.setResponseObjJSON();
} }
if (self.response_obj) |obj| { if (self.response_obj) |obj| {
@@ -677,7 +678,7 @@ pub const XMLHttpRequest = struct {
} }
var fbs = std.io.fixedBufferStream(self.response_bytes.items); var fbs = std.io.fixedBufferStream(self.response_bytes.items);
const doc = parser.documentHTMLParse(fbs.reader(), mime.charsetStringZ()) catch { const doc = parser.documentHTMLParse(fbs.reader(), mime.charsetString()) catch {
self.response_obj = .{ .Failure = {} }; self.response_obj = .{ .Failure = {} };
return; return;
}; };
@@ -690,24 +691,22 @@ pub const XMLHttpRequest = struct {
}; };
} }
// setResponseObjJSON parses the received bytes as a js.Value. // setResponseObjJSON parses the received bytes as a std.json.Value.
fn setResponseObjJSON(self: *XMLHttpRequest, page: *Page) void { fn setResponseObjJSON(self: *XMLHttpRequest) void {
const value = js.Value.fromJson( // TODO should we use parseFromSliceLeaky if we expect the allocator is
page.js, // already an arena?
const p = std.json.parseFromSliceLeaky(
JSONValue,
self.arena,
self.response_bytes.items, self.response_bytes.items,
.{},
) catch |e| { ) catch |e| {
log.warn(.http, "invalid json", .{ .err = e, .url = self.url, .source = "xhr" }); log.warn(.http, "invalid json", .{ .err = e, .url = self.url, .source = "xhr" });
self.response_obj = .{ .Failure = {} }; self.response_obj = .{ .Failure = {} };
return; return;
}; };
const pvalue = value.persist(page.js) catch |e| { self.response_obj = .{ .JSON = p };
log.warn(.http, "persist v8 json value", .{ .err = e, .url = self.url, .source = "xhr" });
self.response_obj = .{ .Failure = {} };
return;
};
self.response_obj = .{ .JSON = pvalue };
} }
pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 {

View File

@@ -230,11 +230,6 @@ pub fn CDPT(comptime TypeProvider: type) type {
asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command), asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command),
else => {}, else => {},
}, },
13 => switch (@as(u104, @bitCast(domain[0..13].*))) {
asUint(u104, "Accessibility") => return @import("domains/accessibility.zig").processMessage(command),
else => {},
},
else => {}, else => {},
} }
@@ -473,14 +468,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
return if (raw_url.len == 0) null else raw_url; return if (raw_url.len == 0) null else raw_url;
} }
pub fn getTitle(self: *const Self) ?[]const u8 {
const page = self.session.currentPage() orelse return null;
return page.getTitle() catch |err| {
log.err(.cdp, "page title", .{ .err = err });
return null;
};
}
pub fn networkEnable(self: *Self) !void { pub fn networkEnable(self: *Self) !void {
try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail); try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail);
try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart); try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart);
@@ -551,8 +538,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void { pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void {
const self: *Self = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
defer self.resetNotificationArena(); return @import("domains/page.zig").pageNavigated(self, msg);
return @import("domains/page.zig").pageNavigated(self.notification_arena, self, msg);
} }
pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void { pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void {

View File

@@ -1,38 +0,0 @@
// 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, .{});
}

View File

@@ -44,7 +44,6 @@ pub fn processMessage(cmd: anytype) !void {
grantPermissions, grantPermissions,
getWindowForTarget, getWindowForTarget,
setDownloadBehavior, setDownloadBehavior,
close,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
@@ -55,7 +54,6 @@ pub fn processMessage(cmd: anytype) !void {
.grantPermissions => return grantPermissions(cmd), .grantPermissions => return grantPermissions(cmd),
.getWindowForTarget => return getWindowForTarget(cmd), .getWindowForTarget => return getWindowForTarget(cmd),
.setDownloadBehavior => return setDownloadBehavior(cmd), .setDownloadBehavior => return setDownloadBehavior(cmd),
.close => return cmd.sendResult(null, .{}),
} }
} }

View File

@@ -24,7 +24,6 @@ const css = @import("../../browser/dom/css.zig");
const parser = @import("../../browser/netsurf.zig"); const parser = @import("../../browser/netsurf.zig");
const dom_node = @import("../../browser/dom/node.zig"); const dom_node = @import("../../browser/dom/node.zig");
const Element = @import("../../browser/dom/element.zig").Element; const Element = @import("../../browser/dom/element.zig").Element;
const dump = @import("../../browser/dump.zig");
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
@@ -42,8 +41,6 @@ pub fn processMessage(cmd: anytype) !void {
getBoxModel, getBoxModel,
requestChildNodes, requestChildNodes,
getFrameOwner, getFrameOwner,
getOuterHTML,
requestNode,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
@@ -61,8 +58,6 @@ pub fn processMessage(cmd: anytype) !void {
.getBoxModel => return getBoxModel(cmd), .getBoxModel => return getBoxModel(cmd),
.requestChildNodes => return requestChildNodes(cmd), .requestChildNodes => return requestChildNodes(cmd),
.getFrameOwner => return getFrameOwner(cmd), .getFrameOwner => return getFrameOwner(cmd),
.getOuterHTML => return getOuterHTML(cmd),
.requestNode => return requestNode(cmd),
} }
} }
@@ -499,38 +494,6 @@ fn getFrameOwner(cmd: anytype) !void {
return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{}); return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{});
} }
fn getOuterHTML(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
objectId: ?[]const u8 = null,
includeShadowDOM: bool = false,
})) orelse return error.InvalidParams;
if (params.includeShadowDOM) {
log.warn(.cdp, "not implemented", .{ .feature = "DOM.getOuterHTML: Not implemented includeShadowDOM parameter" });
}
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
var aw = std.Io.Writer.Allocating.init(cmd.arena);
try dump.writeNode(node._node, .{}, &aw.writer);
return cmd.sendResult(.{ .outerHTML = aw.written() }, .{});
}
fn requestNode(cmd: anytype) !void {
const params = (try cmd.params(struct {
objectId: []const u8,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const node = try getNode(cmd.arena, bc, null, null, params.objectId);
return cmd.sendResult(.{ .nodeId = node.id }, .{});
}
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");
test "cdp.dom: getSearchResults unknown search id" { test "cdp.dom: getSearchResults unknown search id" {

View File

@@ -22,7 +22,6 @@ const Allocator = std.mem.Allocator;
const CdpStorage = @import("storage.zig"); const CdpStorage = @import("storage.zig");
const Transfer = @import("../../http/Client.zig").Transfer; const Transfer = @import("../../http/Client.zig").Transfer;
const Notification = @import("../../notification.zig").Notification; const Notification = @import("../../notification.zig").Notification;
const Mime = @import("../../browser/mime.zig").Mime;
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
@@ -243,18 +242,14 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification.
} }
const transfer = msg.transfer; const transfer = msg.transfer;
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id});
// We're missing a bunch of fields, but, for now, this seems like enough // We're missing a bunch of fields, but, for now, this seems like enough
try bc.cdp.sendEvent("Network.requestWillBeSent", .{ try bc.cdp.sendEvent("Network.requestWillBeSent", .{
.requestId = loader_id, .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
.frameId = target_id, .frameId = target_id,
.loaderId = loader_id, .loaderId = bc.loader_id,
.type = msg.transfer.req.resource_type.string(), .documentUrl = DocumentUrlWriter.init(&page.url.uri),
.documentURL = DocumentUrlWriter.init(&page.url.uri),
.request = TransferAsRequestWriter.init(transfer), .request = TransferAsRequestWriter.init(transfer),
.initiator = .{ .type = "other" }, .initiator = .{ .type = "other" },
.redirectHasExtraInfo = false, // TODO change after adding Network.requestWillBeSentExtraInfo
.hasUserGesture = false,
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
@@ -264,16 +259,12 @@ pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notific
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;
const target_id = bc.target_id orelse unreachable; const target_id = bc.target_id orelse unreachable;
const transfer = msg.transfer;
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id});
// We're missing a bunch of fields, but, for now, this seems like enough // We're missing a bunch of fields, but, for now, this seems like enough
try bc.cdp.sendEvent("Network.responseReceived", .{ try bc.cdp.sendEvent("Network.responseReceived", .{
.requestId = loader_id, .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}),
.loaderId = bc.loader_id,
.frameId = target_id, .frameId = target_id,
.loaderId = loader_id,
.response = TransferAsResponseWriter.init(arena, msg.transfer), .response = TransferAsResponseWriter.init(arena, msg.transfer),
.hasExtraInfo = false, // TODO change after adding Network.responseReceivedExtraInfo
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
@@ -401,20 +392,6 @@ const TransferAsResponseWriter = struct {
try jws.write(@as(std.http.Status, @enumFromInt(status)).phrase() orelse "Unknown"); try jws.write(@as(std.http.Status, @enumFromInt(status)).phrase() orelse "Unknown");
} }
{
const mime: Mime = blk: {
if (transfer.response_header.?.contentType()) |ct| {
break :blk try Mime.parse(ct);
}
break :blk .unknown;
};
try jws.objectField("mimeType");
try jws.write(mime.contentTypeString());
try jws.objectField("charset");
try jws.write(mime.charsetString());
}
{ {
// chromedp doesn't like having duplicate header names. It's pretty // chromedp doesn't like having duplicate header names. It's pretty
// common to get these from a server (e.g. for Cache-Control), but // common to get these from a server (e.g. for Cache-Control), but

View File

@@ -20,7 +20,6 @@ const std = @import("std");
const Page = @import("../../browser/page.zig").Page; const Page = @import("../../browser/page.zig").Page;
const timestampF = @import("../../datetime.zig").timestamp; const timestampF = @import("../../datetime.zig").timestamp;
const Notification = @import("../../notification.zig").Notification; const Notification = @import("../../notification.zig").Notification;
const log = @import("../../log.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -33,7 +32,6 @@ pub fn processMessage(cmd: anytype) !void {
createIsolatedWorld, createIsolatedWorld,
navigate, navigate,
stopLoading, stopLoading,
close,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
@@ -44,7 +42,6 @@ pub fn processMessage(cmd: anytype) !void {
.createIsolatedWorld => return createIsolatedWorld(cmd), .createIsolatedWorld => return createIsolatedWorld(cmd),
.navigate => return navigate(cmd), .navigate => return navigate(cmd),
.stopLoading => return cmd.sendResult(null, .{}), .stopLoading => return cmd.sendResult(null, .{}),
.close => return close(cmd),
} }
} }
@@ -132,51 +129,14 @@ fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
}, .{}); }, .{});
} }
fn close(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const target_id = bc.target_id orelse return error.TargetNotLoaded;
// can't be null if we have a target_id
std.debug.assert(bc.session.page != null);
try cmd.sendResult(.{}, .{});
// Following code is similar to target.closeTarget
//
// could be null, created but never attached
if (bc.session_id) |session_id| {
// Inspector.detached event
try cmd.sendEvent("Inspector.detached", .{
.reason = "Render process gone.",
}, .{ .session_id = session_id });
// detachedFromTarget event
try cmd.sendEvent("Target.detachedFromTarget", .{
.targetId = target_id,
.sessionId = session_id,
.reason = "Render process gone.",
}, .{});
bc.session_id = null;
}
bc.session.removePage();
for (bc.isolated_worlds.items) |*world| {
world.deinit();
}
bc.isolated_worlds.clearRetainingCapacity();
bc.target_id = null;
}
fn createIsolatedWorld(cmd: anytype) !void { fn createIsolatedWorld(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
frameId: []const u8, frameId: []const u8,
worldName: []const u8, worldName: []const u8,
grantUniveralAccess: bool = false, grantUniveralAccess: bool,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
if (!params.grantUniveralAccess) { if (!params.grantUniveralAccess) {
log.warn(.cdp, "not implemented", .{ .feature = "grantUniveralAccess == false is not yet implemented" }); std.debug.print("grantUniveralAccess == false is not yet implemented", .{});
// When grantUniveralAccess == false and the client attempts to resolve // When grantUniveralAccess == false and the client attempts to resolve
// or otherwise access a DOM or other JS Object from another context that should fail. // or otherwise access a DOM or other JS Object from another context that should fail.
} }
@@ -215,6 +175,7 @@ fn navigate(cmd: anytype) !void {
} }
var page = bc.session.currentPage() orelse return error.PageNotLoaded; var page = bc.session.currentPage() orelse return error.PageNotLoaded;
bc.loader_id = bc.cdp.loader_id_gen.next();
try page.navigate(params.url, .{ try page.navigate(params.url, .{
.reason = .address_bar, .reason = .address_bar,
@@ -227,7 +188,8 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
// things, but no session. // things, but no session.
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{event.req_id}); bc.loader_id = bc.cdp.loader_id_gen.next();
const loader_id = bc.loader_id;
const target_id = bc.target_id orelse unreachable; const target_id = bc.target_id orelse unreachable;
bc.reset(); bc.reset();
@@ -271,30 +233,6 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
try cdp.sendEvent("Page.frameStartedLoading", .{ try cdp.sendEvent("Page.frameStartedLoading", .{
.frameId = target_id, .frameId = target_id,
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
}
pub fn pageRemove(bc: anytype) !void {
// The main page is going to be removed, we need to remove contexts from other worlds first.
for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.removeContext();
}
}
pub fn pageCreated(bc: anytype, page: *Page) !void {
for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.createContextAndLoadPolyfills(bc.arena, page);
}
}
pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{event.req_id});
const target_id = bc.target_id orelse unreachable;
const timestamp = event.timestamp;
var cdp = bc.cdp;
// Drivers are sensitive to the order of events. Some more than others. // Drivers are sensitive to the order of events. Some more than others.
// The result for the Page.navigate seems like it _must_ come after // The result for the Page.navigate seems like it _must_ come after
@@ -321,17 +259,6 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
const reason_: ?[]const u8 = switch (event.opts.reason) {
.anchor => "anchorClick",
.script, .history, .navigation => "scriptInitiated",
.form => switch (event.opts.method) {
.GET => "formSubmissionGet",
.POST => "formSubmissionPost",
else => unreachable,
},
.address_bar => null,
};
if (reason_ != null) { if (reason_ != null) {
try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{ try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{
.frameId = target_id, .frameId = target_id,
@@ -365,14 +292,37 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
false, false,
); );
} }
}
pub fn pageRemove(bc: anytype) !void {
// The main page is going to be removed, we need to remove contexts from other worlds first.
for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.removeContext();
}
}
pub fn pageCreated(bc: anytype, page: *Page) !void {
for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.createContextAndLoadPolyfills(bc.arena, page);
}
}
pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
const loader_id = bc.loader_id;
const target_id = bc.target_id orelse unreachable;
const timestamp = event.timestamp;
var cdp = bc.cdp;
// frameNavigated event // frameNavigated event
try cdp.sendEvent("Page.frameNavigated", .{ try cdp.sendEvent("Page.frameNavigated", .{
.type = "Navigation", .type = "Navigation",
.frame = Frame{ .frame = Frame{
.id = target_id, .id = target_id,
.url = event.url, .url = event.url,
.loaderId = loader_id, .loaderId = bc.loader_id,
.securityOrigin = bc.security_origin, .securityOrigin = bc.security_origin,
.secureContextType = bc.secure_context_type, .secureContextType = bc.secure_context_type,
}, },

View File

@@ -24,7 +24,6 @@ const LOADER_ID = "LOADERID42AA389647D702B4D805F49A";
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
getTargets,
attachToTarget, attachToTarget,
closeTarget, closeTarget,
createBrowserContext, createBrowserContext,
@@ -39,7 +38,6 @@ pub fn processMessage(cmd: anytype) !void {
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
.getTargets => return getTargets(cmd),
.attachToTarget => return attachToTarget(cmd), .attachToTarget => return attachToTarget(cmd),
.closeTarget => return closeTarget(cmd), .closeTarget => return closeTarget(cmd),
.createBrowserContext => return createBrowserContext(cmd), .createBrowserContext => return createBrowserContext(cmd),
@@ -54,31 +52,6 @@ pub fn processMessage(cmd: anytype) !void {
} }
} }
fn getTargets(cmd: anytype) !void {
// Some clients like Stagehand expects to have an existing context.
const bc = cmd.browser_context orelse cmd.createBrowserContext() catch |err| switch (err) {
error.AlreadyExists => unreachable,
else => return err,
};
const target_id = bc.target_id orelse {
return cmd.sendResult(.{
.targetInfos = [_]TargetInfo{},
}, .{ .include_session_id = false });
};
return cmd.sendResult(.{
.targetInfos = [_]TargetInfo{.{
.targetId = target_id,
.type = "page",
.title = bc.getTitle() orelse "about:blank",
.url = bc.getURL() orelse "about:blank",
.attached = true,
.canAccessOpener = false,
}},
}, .{ .include_session_id = false });
}
fn getBrowserContexts(cmd: anytype) !void { fn getBrowserContexts(cmd: anytype) !void {
var browser_context_ids: []const []const u8 = undefined; var browser_context_ids: []const []const u8 = undefined;
if (cmd.browser_context) |bc| { if (cmd.browser_context) |bc| {
@@ -194,7 +167,7 @@ fn createTarget(cmd: anytype) !void {
.targetInfo = TargetInfo{ .targetInfo = TargetInfo{
.attached = false, .attached = false,
.targetId = target_id, .targetId = target_id,
.title = "about:blank", .title = params.url,
.browserContextId = bc.id, .browserContextId = bc.id,
.url = "about:blank", .url = "about:blank",
}, },
@@ -205,11 +178,9 @@ fn createTarget(cmd: anytype) !void {
try doAttachtoTarget(cmd, target_id); try doAttachtoTarget(cmd, target_id);
} }
if (!std.mem.eql(u8, "about:blank", params.url)) { try page.navigate(params.url, .{
try page.navigate(params.url, .{ .reason = .address_bar,
.reason = .address_bar, });
});
}
try cmd.sendResult(.{ try cmd.sendResult(.{
.targetId = target_id, .targetId = target_id,
@@ -228,10 +199,12 @@ fn attachToTarget(cmd: anytype) !void {
return error.UnknownTargetId; return error.UnknownTargetId;
} }
if (bc.session_id == null) { if (bc.session_id != null) {
try doAttachtoTarget(cmd, target_id); return error.SessionAlreadyLoaded;
} }
try doAttachtoTarget(cmd, target_id);
return cmd.sendResult( return cmd.sendResult(
.{ .sessionId = bc.session_id }, .{ .sessionId = bc.session_id },
.{ .include_session_id = false }, .{ .include_session_id = false },
@@ -296,8 +269,8 @@ fn getTargetInfo(cmd: anytype) !void {
.targetInfo = TargetInfo{ .targetInfo = TargetInfo{
.targetId = target_id, .targetId = target_id,
.type = "page", .type = "page",
.title = bc.getTitle() orelse "about:blank", .title = "",
.url = bc.getURL() orelse "about:blank", .url = "",
.attached = true, .attached = true,
.canAccessOpener = false, .canAccessOpener = false,
}, },
@@ -308,8 +281,8 @@ fn getTargetInfo(cmd: anytype) !void {
.targetInfo = TargetInfo{ .targetInfo = TargetInfo{
.targetId = "TID-STARTUP-B", .targetId = "TID-STARTUP-B",
.type = "browser", .type = "browser",
.title = "about:blank", .title = "",
.url = "about:blank", .url = "",
.attached = true, .attached = true,
.canAccessOpener = false, .canAccessOpener = false,
}, },
@@ -655,8 +628,8 @@ test "cdp.target: getTargetInfo" {
try ctx.expectSentResult(.{ try ctx.expectSentResult(.{
.targetInfo = .{ .targetInfo = .{
.type = "browser", .type = "browser",
.title = "about:blank", .title = "",
.url = "about:blank", .url = "",
.attached = true, .attached = true,
.canAccessOpener = false, .canAccessOpener = false,
}, },
@@ -689,7 +662,7 @@ test "cdp.target: getTargetInfo" {
.targetId = "TID-A", .targetId = "TID-A",
.type = "page", .type = "page",
.title = "", .title = "",
.url = "about:blank", .url = "",
.attached = true, .attached = true,
.canAccessOpener = false, .canAccessOpener = false,
}, },

View File

@@ -261,16 +261,6 @@ pub fn fulfillTransfer(self: *Client, transfer: *Transfer, status: u16, headers:
return transfer.fulfill(status, headers, body); return transfer.fulfill(status, headers, body);
} }
pub fn nextReqId(self: *Client) usize {
return self.next_request_id + 1;
}
pub fn incrReqId(self: *Client) usize {
const id = self.next_request_id + 1;
self.next_request_id = id;
return id;
}
fn makeTransfer(self: *Client, req: Request) !*Transfer { fn makeTransfer(self: *Client, req: Request) !*Transfer {
errdefer req.headers.deinit(); errdefer req.headers.deinit();
@@ -283,7 +273,8 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
const transfer = try self.transfer_pool.create(); const transfer = try self.transfer_pool.create();
errdefer self.transfer_pool.destroy(transfer); errdefer self.transfer_pool.destroy(transfer);
const id = self.incrReqId(); const id = self.next_request_id + 1;
self.next_request_id = id;
transfer.* = .{ transfer.* = .{
.arena = ArenaAllocator.init(self.allocator), .arena = ArenaAllocator.init(self.allocator),
.id = id, .id = id,
@@ -688,19 +679,6 @@ pub const Request = struct {
xhr, xhr,
script, script,
fetch, fetch,
// Allowed Values: Document, Stylesheet, Image, Media, Font, Script,
// TextTrack, XHR, Fetch, Prefetch, EventSource, WebSocket, Manifest,
// SignedExchange, Ping, CSPViolationReport, Preflight, FedCM, Other
// https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ResourceType
pub fn string(self: ResourceType) []const u8 {
return switch (self) {
.document => "Document",
.xhr => "XHR",
.script => "Script",
.fetch => "Fetch",
};
}
}; };
}; };

View File

@@ -40,7 +40,6 @@ pub const Scope = enum {
fetch, fetch,
polyfill, polyfill,
interceptor, interceptor,
input,
}; };
const Opts = struct { const Opts = struct {

View File

@@ -23,42 +23,67 @@ const Allocator = std.mem.Allocator;
const log = @import("log.zig"); const log = @import("log.zig");
const App = @import("app.zig").App; const App = @import("app.zig").App;
const Server = @import("server.zig").Server; const Server = @import("server.zig").Server;
const SigHandler = @import("sighandler.zig").SigHandler;
const Browser = @import("browser/browser.zig").Browser; const Browser = @import("browser/browser.zig").Browser;
const DumpStripMode = @import("browser/dump.zig").Opts.StripMode; const DumpStripMode = @import("browser/dump.zig").Opts.StripMode;
const build_config = @import("build_config"); const build_config = @import("build_config");
var _app: ?*App = null;
var _server: ?Server = null;
pub fn main() !void { pub fn main() !void {
// allocator // allocator
// - in Debug mode we use the General Purpose Allocator to detect memory leaks // - in Debug mode we use the General Purpose Allocator to detect memory leaks
// - in Release mode we use the c allocator // - in Release mode we use the c allocator
var gpa_instance: std.heap.DebugAllocator(.{}) = .init; var gpa: std.heap.DebugAllocator(.{}) = .init;
const gpa = if (builtin.mode == .Debug) gpa_instance.allocator() else std.heap.c_allocator; const alloc = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator;
defer if (builtin.mode == .Debug) { defer if (builtin.mode == .Debug) {
if (gpa_instance.detectLeaks()) std.posix.exit(1); if (gpa.detectLeaks()) std.posix.exit(1);
}; };
var arena_instance = std.heap.ArenaAllocator.init(gpa); run(alloc) catch |err| {
const arena = arena_instance.allocator();
defer arena_instance.deinit();
var sighandler = SigHandler{ .arena = arena };
try sighandler.install();
run(gpa, arena, &sighandler) catch |err| {
// If explicit filters were set, they won't be valid anymore because // If explicit filters were set, they won't be valid anymore because
// the arena is gone. We need to set it to something that's not // the args_arena is gone. We need to set it to something that's not
// invalid. (We should just move the arena up to main) // invalid. (We should just move the args_arena up to main)
log.opts.filter_scopes = &.{}; log.opts.filter_scopes = &.{};
log.fatal(.app, "exit", .{ .err = err }); log.fatal(.app, "exit", .{ .err = err });
std.posix.exit(1); std.posix.exit(1);
}; };
} }
fn run(gpa: Allocator, arena: Allocator, sighandler: *SigHandler) !void { // Handle app shutdown gracefuly on signals.
const args = try parseArgs(arena); 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());
switch (args.mode) { switch (args.mode) {
.help => { .help => {
@@ -85,13 +110,13 @@ fn run(gpa: Allocator, arena: Allocator, sighandler: *SigHandler) !void {
const user_agent = blk: { const user_agent = blk: {
const USER_AGENT = "User-Agent: Lightpanda/1.0"; const USER_AGENT = "User-Agent: Lightpanda/1.0";
if (args.userAgentSuffix()) |suffix| { if (args.userAgentSuffix()) |suffix| {
break :blk try std.fmt.allocPrintSentinel(arena, "{s} {s}", .{ USER_AGENT, suffix }, 0); break :blk try std.fmt.allocPrintSentinel(args_arena.allocator(), "{s} {s}", .{ USER_AGENT, suffix }, 0);
} }
break :blk USER_AGENT; break :blk USER_AGENT;
}; };
// _app is global to handle graceful shutdown. // _app is global to handle graceful shutdown.
var app = try App.init(gpa, .{ _app = try App.init(alloc, .{
.run_mode = args.mode, .run_mode = args.mode,
.http_proxy = args.httpProxy(), .http_proxy = args.httpProxy(),
.proxy_bearer_token = args.proxyBearerToken(), .proxy_bearer_token = args.proxyBearerToken(),
@@ -102,23 +127,24 @@ fn run(gpa: Allocator, arena: Allocator, sighandler: *SigHandler) !void {
.http_max_concurrent = args.httpMaxConcurrent(), .http_max_concurrent = args.httpMaxConcurrent(),
.user_agent = user_agent, .user_agent = user_agent,
}); });
const app = _app.?;
defer app.deinit(); defer app.deinit();
app.telemetry.record(.{ .run = {} }); app.telemetry.record(.{ .run = {} });
switch (args.mode) { switch (args.mode) {
.serve => |opts| { .serve => |opts| {
log.debug(.app, "startup", .{ .mode = "serve" }); log.debug(.app, "startup", .{ .mode = "serve" });
const address = std.net.Address.parseIp(opts.host, opts.port) catch |err| { const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| {
log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port }); log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port });
return args.printUsageAndExit(false); return args.printUsageAndExit(false);
}; };
// _server is global to handle graceful shutdown. // _server is global to handle graceful shutdown.
var server = try Server.init(app, address); _server = try Server.init(app, address);
const server = &_server.?;
defer server.deinit(); defer server.deinit();
try sighandler.on(Server.stop, .{&server});
// max timeout of 1 week. // max timeout of 1 week.
const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(i32, opts.timeout) * 1000; const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(i32, opts.timeout) * 1000;
server.run(address, timeout) catch |err| { server.run(address, timeout) catch |err| {
@@ -862,7 +888,7 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
} }
if (std.mem.eql(u8, path, "/xhr/json")) { if (std.mem.eql(u8, path, "/xhr/json")) {
return req.respond("{\"over\":\"9000!!!\",\"updated_at\":1765867200000}", .{ return req.respond("{\"over\":\"9000!!!\"}", .{
.extra_headers = &.{ .extra_headers = &.{
.{ .name = "Content-Type", .value = "application/json" }, .{ .name = "Content-Type", .value = "application/json" },
}, },

View File

@@ -90,17 +90,14 @@ pub const Notification = struct {
pub const PageRemove = struct {}; pub const PageRemove = struct {};
pub const PageNavigate = struct { pub const PageNavigate = struct {
req_id: usize,
timestamp: u32, timestamp: u32,
url: []const u8, url: []const u8,
opts: page.NavigateOpts, opts: page.NavigateOpts,
}; };
pub const PageNavigated = struct { pub const PageNavigated = struct {
req_id: usize,
timestamp: u32, timestamp: u32,
url: []const u8, url: []const u8,
opts: page.NavigatedOpts,
}; };
pub const PageNetworkIdle = struct { pub const PageNetworkIdle = struct {
@@ -299,7 +296,6 @@ test "Notification" {
// noop // noop
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 4, .timestamp = 4,
.url = undefined, .url = undefined,
.opts = .{}, .opts = .{},
@@ -309,7 +305,6 @@ test "Notification" {
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 4, .timestamp = 4,
.url = undefined, .url = undefined,
.opts = .{}, .opts = .{},
@@ -318,7 +313,6 @@ test "Notification" {
notifier.unregisterAll(&tc); notifier.unregisterAll(&tc);
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 10, .timestamp = 10,
.url = undefined, .url = undefined,
.opts = .{}, .opts = .{},
@@ -328,23 +322,21 @@ test "Notification" {
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated); try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 10, .timestamp = 10,
.url = undefined, .url = undefined,
.opts = .{}, .opts = .{},
}); });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigated, &.{ .timestamp = 6, .url = undefined });
try testing.expectEqual(14, tc.page_navigate); try testing.expectEqual(14, tc.page_navigate);
try testing.expectEqual(6, tc.page_navigated); try testing.expectEqual(6, tc.page_navigated);
notifier.unregisterAll(&tc); notifier.unregisterAll(&tc);
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 100, .timestamp = 100,
.url = undefined, .url = undefined,
.opts = .{}, .opts = .{},
}); });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigated, &.{ .timestamp = 100, .url = undefined });
try testing.expectEqual(14, tc.page_navigate); try testing.expectEqual(14, tc.page_navigate);
try testing.expectEqual(6, tc.page_navigated); try testing.expectEqual(6, tc.page_navigated);
@@ -352,27 +344,27 @@ test "Notification" {
// unregister // unregister
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated); try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(1006, tc.page_navigated); try testing.expectEqual(1006, tc.page_navigated);
notifier.unregister(.page_navigate, &tc); notifier.unregister(.page_navigate, &tc);
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated); try testing.expectEqual(2006, tc.page_navigated);
notifier.unregister(.page_navigated, &tc); notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated); try testing.expectEqual(2006, tc.page_navigated);
// already unregistered, try anyways // already unregistered, try anyways
notifier.unregister(.page_navigated, &tc); notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated); try testing.expectEqual(2006, tc.page_navigated);
} }

View File

@@ -38,7 +38,7 @@ const MAX_MESSAGE_SIZE = 512 * 1024 + 14 + 140;
pub const Server = struct { pub const Server = struct {
app: *App, app: *App,
shutdown: bool = false, shutdown: bool,
allocator: Allocator, allocator: Allocator,
client: ?posix.socket_t, client: ?posix.socket_t,
listener: ?posix.socket_t, listener: ?posix.socket_t,
@@ -53,36 +53,16 @@ pub const Server = struct {
.app = app, .app = app,
.client = null, .client = null,
.listener = null, .listener = null,
.shutdown = false,
.allocator = allocator, .allocator = allocator,
.json_version_response = json_version_response, .json_version_response = json_version_response,
}; };
} }
/// Interrupts the server so that main can complete normally and call all defer handlers.
pub fn stop(self: *Server) void {
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
return;
}
// Linux and BSD/macOS handle canceling a socket blocked on accept differently.
// For Linux, we use std.shutdown, which will cause accept to return error.SocketNotListening (EINVAL).
// For BSD, shutdown will return an error. Instead we call posix.close, which will result with error.ConnectionAborted (BADF).
if (self.listener) |listener| switch (builtin.target.os.tag) {
.linux => posix.shutdown(listener, .recv) catch |err| {
log.warn(.app, "listener shutdown", .{ .err = err });
},
.macos, .freebsd, .netbsd, .openbsd => {
self.listener = null;
posix.close(listener);
},
else => unreachable,
};
}
pub fn deinit(self: *Server) void { pub fn deinit(self: *Server) void {
self.shutdown = true;
if (self.listener) |listener| { if (self.listener) |listener| {
posix.close(listener); posix.close(listener);
self.listener = null;
} }
// *if* server.run is running, we should really wait for it to return // *if* server.run is running, we should really wait for it to return
// before existing from here. // before existing from here.
@@ -103,19 +83,14 @@ pub const Server = struct {
try posix.listen(listener, 1); try posix.listen(listener, 1);
log.info(.app, "server running", .{ .address = address }); log.info(.app, "server running", .{ .address = address });
while (!@atomicLoad(bool, &self.shutdown, .monotonic)) { while (true) {
const socket = posix.accept(listener, null, null, posix.SOCK.NONBLOCK) catch |err| { const socket = posix.accept(listener, null, null, posix.SOCK.NONBLOCK) catch |err| {
switch (err) { if (self.shutdown) {
error.SocketNotListening, error.ConnectionAborted => { return;
log.info(.app, "server stopped", .{});
break;
},
else => {
log.err(.app, "CDP accept", .{ .err = err });
std.Thread.sleep(std.time.ns_per_s);
continue;
},
} }
log.err(.app, "CDP accept", .{ .err = err });
std.Thread.sleep(std.time.ns_per_s);
continue;
}; };
self.client = socket; self.client = socket;

View File

@@ -1,88 +0,0 @@
//! 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,
}
}
}
};

View File

@@ -1,25 +0,0 @@
<!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>

View File

@@ -113,13 +113,4 @@
// doesn't crash on null receiver // doesn't crash on null receiver
content.addEventListener('he2', null); content.addEventListener('he2', null);
content.dispatchEvent(new Event('he2')); content.dispatchEvent(new Event('he2'));
// Test that EventTarget constructor properly initializes vtable
const et = new EventTarget();
testing.expectEqual('[object EventTarget]', et.toString());
let constructorTestCalled = false;
et.addEventListener('test', () => { constructorTestCalled = true; });
et.dispatchEvent(new Event('test'));
testing.expectEqual(true, constructorTestCalled);
</script> </script>

View File

@@ -12,8 +12,7 @@
}); });
testing.async(promise1, (json) => { testing.async(promise1, (json) => {
testing.expectEqual("number", typeof json.updated_at); testing.expectEqual({over: '9000!!!'}, json);
testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json);
}); });
</script> </script>
@@ -30,7 +29,6 @@
}); });
testing.async(promise1, (json) => { testing.async(promise1, (json) => {
testing.expectEqual("number", typeof json.updated_at); testing.expectEqual({over: '9000!!!'}, json);
testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json);
}); });
</script> </script>

View File

@@ -1,116 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=CanvasRenderingContext2D>
{
const element = document.createElement("canvas");
const ctx = element.getContext("2d");
testing.expectEqual(true, ctx instanceof CanvasRenderingContext2D);
// We can't really test this but let's try to call it at least.
ctx.fillRect(0, 0, 0, 0);
}
</script>
<script id=CanvasRenderingContext2D#fillStyle>
{
const element = document.createElement("canvas");
const ctx = element.getContext("2d");
// Black by default.
testing.expectEqual(ctx.fillStyle, "#000000");
ctx.fillStyle = "red";
testing.expectEqual(ctx.fillStyle, "#ff0000");
ctx.fillStyle = "rebeccapurple";
testing.expectEqual(ctx.fillStyle, "#663399");
// No changes made if color is invalid.
ctx.fillStyle = "invalid-color";
testing.expectEqual(ctx.fillStyle, "#663399");
ctx.fillStyle = "#fc0";
testing.expectEqual(ctx.fillStyle, "#ffcc00");
ctx.fillStyle = "#ff0000";
testing.expectEqual(ctx.fillStyle, "#ff0000");
ctx.fillStyle = "#fF00000F";
testing.expectEqual(ctx.fillStyle, "rgba(255, 0, 0, 0.06)");
}
</script>
<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>

View File

@@ -20,19 +20,3 @@
testing.expectEqual('P', t.content.childNodes[1].tagName); testing.expectEqual('P', t.content.childNodes[1].tagName);
testing.expectEqual('9000!', t.content.childNodes[1].innerHTML); testing.expectEqual('9000!', t.content.childNodes[1].innerHTML);
</script> </script>
<template id="hello"><p>hello, world</p></template>
<script id=template_parsing>
const tt = document.getElementById('hello');
testing.expectEqual('<p>hello, world</p>', tt.innerHTML);
// > The Node.childNodes property of the <template> element is always empty
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template#usage_notes
testing.expectEqual(0, tt.childNodes.length);
let out = document.createElement('div');
out.appendChild(tt.content.cloneNode(true));
testing.expectEqual('<p>hello, world</p>', out.innerHTML);
</script>

View File

@@ -65,8 +65,6 @@
testing.expectEqual(200, req3.status); testing.expectEqual(200, req3.status);
testing.expectEqual('OK', req3.statusText); testing.expectEqual('OK', req3.statusText);
testing.expectEqual('9000!!!', req3.response.over); testing.expectEqual('9000!!!', req3.response.over);
testing.expectEqual("number", typeof req3.response.updated_at);
testing.expectEqual(1765867200000, req3.response.updated_at);
}); });
</script> </script>

1
vendor/mbedtls vendored Submodule

Submodule vendor/mbedtls added at c765c831e5