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

View File

@@ -5,12 +5,8 @@ env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
on:
push:
tags:
- '*'
schedule:
- cron: "2 2 * * *"
@@ -30,9 +26,10 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
@@ -41,7 +38,7 @@ jobs:
arch: ${{env.ARCH}}
- 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
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -56,7 +53,7 @@ jobs:
with:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }}
tag: nightly
build-linux-aarch64:
env:
@@ -79,7 +76,7 @@ jobs:
arch: ${{env.ARCH}}
- 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
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -94,7 +91,7 @@ jobs:
with:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }}
tag: nightly
build-macos-aarch64:
env:
@@ -119,7 +116,7 @@ jobs:
arch: ${{env.ARCH}}
- 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
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -134,14 +131,19 @@ jobs:
with:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }}
tag: nightly
build-macos-x86_64:
env:
ARCH: x86_64
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
steps:
@@ -157,7 +159,7 @@ jobs:
arch: ${{env.ARCH}}
- 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
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -172,4 +174,4 @@ jobs:
with:
allowUpdates: true
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'
# branch should not be protected
branch: 'main'
allowlist: krichprollsch,francisbouvier,katie-lpd
allowlist: krichprollsch,francisbouvier,katie-lpd,sjorsdonkers,bornlex
remote-organization-name: lightpanda-io
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
steps:
- uses: actions/checkout@v6
- 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 }})
run: zig build -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: upload artifact
uses: actions/upload-artifact@v4
@@ -121,7 +122,7 @@ jobs:
needs: zig-build-release
env:
MAX_MEMORY: 28000
MAX_MEMORY: 27000
MAX_AVG_DURATION: 23
LIGHTPANDA_DISABLE_TELEMETRY: true

View File

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

View File

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

View File

@@ -47,15 +47,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- 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 debug
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a
run: zig build
- name: upload artifact
uses: actions/upload-artifact@v4
@@ -103,7 +104,7 @@ jobs:
- uses: ./.github/actions/install
- 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
run: |

1
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

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

View File

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

View File

@@ -34,7 +34,7 @@ endif
## Display this help screen
help:
@printf "\033[36m%-35s %s\033[0m\n" "Command" "Usage"
@printf "\e[36m%-35s %s\e[0m\n" "Command" "Usage"
@sed -n -e '/^## /{'\
-e 's/## //g;'\
-e 'h;'\
@@ -47,43 +47,54 @@ help:
# $(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:
@printf "\033[36mBuilding (release safe)...\033[0m\n"
$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
@printf "\033[33mBuild OK\033[0m\n"
@printf "\e[36mBuilding (release safe)...\e[0m\n"
$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mBuild OK\e[0m\n"
## Build in debug mode
build-dev:
@printf "\033[36mBuilding (debug)...\033[0m\n"
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
@printf "\033[33mBuild OK\033[0m\n"
@printf "\e[36mBuilding (debug)...\e[0m\n"
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mBuild OK\e[0m\n"
## Run the server in release mode
run: build
@printf "\033[36mRunning...\033[0m\n"
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
@printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
## Run the server in debug mode
run-debug: build-dev
@printf "\033[36mRunning...\033[0m\n"
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
@printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
## Run a JS shell in debug mode
shell:
@printf "\033[36mBuilding shell...\033[0m\n"
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
@printf "\e[36mBuilding shell...\e[0m\n"
@$(ZIG) build shell || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
## Run WPT tests
wpt:
@printf "\033[36mBuilding wpt...\033[0m\n"
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
@printf "\e[36mBuilding wpt...\e[0m\n"
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
wpt-summary:
@printf "\033[36mBuilding wpt...\033[0m\n"
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
@printf "\e[36mBuilding wpt...\e[0m\n"
@$(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
ifeq ($(OS), macos)
@@ -101,6 +112,19 @@ end2end:
@test -d ../demo
cd ../demo && go run runner/main.go
## v8
get-v8:
@printf "\e[36mGetting v8 source...\e[0m\n"
@$(ZIG) build get-v8
build-v8-dev:
@printf "\e[36mBuilding v8 (dev)...\e[0m\n"
@$(ZIG) build build-v8
build-v8:
@printf "\e[36mBuilding v8...\e[0m\n"
@$(ZIG) build -Doptimize=ReleaseSafe build-v8
# Install and build required dependencies commands
# ------------
.PHONY: install-submodule
@@ -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
# and stick to a specif version. Maybe build from source. Anyway not now.
_install-netsurf: clean-netsurf
@printf "\033[36mInstalling NetSurf...\033[0m\n" && \
ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\033[33mERROR: you need to execute 'make install-libiconv'\033[0m\n"; exit 1;) && \
@printf "\e[36mInstalling NetSurf...\e[0m\n" && \
ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\e[33mERROR: you need to execute 'make install-libiconv'\e[0m\n"; exit 1;) && \
mkdir -p $(BC_NS) && \
cp -R vendor/netsurf/share $(BC_NS) && \
export PREFIX=$(BC_NS) && \
export OPTLDFLAGS="-L$(ICONV)/lib" && \
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
printf "\033[33mInstalling libwapcaplet...\033[0m\n" && \
printf "\e[33mInstalling libwapcaplet...\e[0m\n" && \
cd vendor/netsurf/libwapcaplet && \
BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \
cd ../libparserutils && \
printf "\033[33mInstalling libparserutils...\033[0m\n" && \
printf "\e[33mInstalling libparserutils...\e[0m\n" && \
BUILDDIR=$(BC_NS)/build/libparserutils make install && \
cd ../libhubbub && \
printf "\033[33mInstalling libhubbub...\033[0m\n" && \
printf "\e[33mInstalling libhubbub...\e[0m\n" && \
BUILDDIR=$(BC_NS)/build/libhubbub make install && \
rm src/treebuilder/autogenerated-element-type.c && \
cd ../libdom && \
printf "\033[33mInstalling libdom...\033[0m\n" && \
printf "\e[33mInstalling libdom...\e[0m\n" && \
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 && \
$(ZIG) cc \
-I$(ICONV)/include \
@@ -164,14 +188,14 @@ _install-netsurf: clean-netsurf
$(ICONV)/lib/libiconv.a && \
./a.out > /dev/null && \
rm a.out && \
printf "\033[36mDone NetSurf $(OS)\033[0m\n"
printf "\e[36mDone NetSurf $(OS)\e[0m\n"
clean-netsurf:
@printf "\033[36mCleaning NetSurf build...\033[0m\n" && \
@printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
rm -Rf $(BC_NS)
test-netsurf:
@printf "\033[36mTesting NetSurf...\033[0m\n" && \
@printf "\e[36mTesting NetSurf...\e[0m\n" && \
export PREFIX=$(BC_NS) && \
export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \
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.
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
## Build from sources
### Prerequisites
@@ -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.
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/),
[Brotli](https://github.com/google/brotli),
[Netsurf libs](https://www.netsurf-browser.org/) and
[Mimalloc](https://microsoft.github.io/mimalloc).
To be able to build the v8 engine, 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:
@@ -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
[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html).
**v8**
First, get the tools necessary for building V8, as well as the V8 source code:
```
make get-v8
```
Next, build v8. This build task is very long and cpu consuming, as you will build v8 from sources.
```
make build-v8
```
For dev env, use `make build-v8-dev`.
## Test
### Unit Tests

140
build.zig
View File

@@ -21,21 +21,36 @@ const builtin = @import("builtin");
const Build = std.Build;
/// Do not rename this constant. It is scanned by some scripts to determine
/// which zig version to install.
const recommended_zig_version = "0.15.2";
pub fn build(b: *Build) !void {
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
.eq => {},
.lt => {
@compileError("The minimum version of Zig required to compile is '" ++ recommended_zig_version ++ "', found '" ++ builtin.zig_version_string ++ "'.");
},
.gt => {
std.debug.print(
"WARNING: Recommended Zig version '{s}', but found '{s}', build may fail...\n\n",
.{ recommended_zig_version, builtin.zig_version_string },
);
},
}
var opts = b.addOptions();
opts.addOption(
[]const u8,
"git_commit",
b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
);
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const manifest = Manifest.init(b);
const git_commit = b.option([]const u8, "git_commit", "Current git commit");
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
var opts = b.addOptions();
opts.addOption([]const u8, "version", manifest.version);
opts.addOption([]const u8, "git_commit", git_commit orelse "dev");
// We're still using llvm because the new x86 backend seems to crash with v8.
// This can be reproduced in zig-v8-fork.
// We're still using llvm because the new x86 backend seems to crash
// with v8. This can be reproduced in zig-v8-fork.
const lightpanda_module = b.addModule("lightpanda", .{
.root_source_file = b.path("src/main.zig"),
@@ -44,7 +59,7 @@ pub fn build(b: *Build) !void {
.link_libc = true,
.link_libcpp = true,
});
try addDependencies(b, lightpanda_module, opts, prebuilt_v8_path);
try addDependencies(b, lightpanda_module, opts);
{
// browser
@@ -98,7 +113,7 @@ pub fn build(b: *Build) !void {
.target = target,
.optimize = optimize,
});
try addDependencies(b, wpt_module, opts, prebuilt_v8_path);
try addDependencies(b, wpt_module, opts);
// compile and install
const wpt = b.addExecutable(.{
@@ -116,9 +131,27 @@ pub fn build(b: *Build) !void {
const wpt_step = b.step("wpt", "WPT tests");
wpt_step.dependOn(&wpt_cmd.step);
}
{
// get v8
// -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
const get_v8 = b.addRunArtifact(v8.artifact("get-v8"));
const get_step = b.step("get-v8", "Get v8");
get_step.dependOn(&get_v8.step);
}
{
// build v8
// -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
const build_v8 = b.addRunArtifact(v8.artifact("build-v8"));
const build_step = b.step("build-v8", "Build v8");
build_step.dependOn(&build_v8.step);
}
}
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, prebuilt_v8_path: ?[]const u8) !void {
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !void {
try moduleNetSurf(b, mod);
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 = .{
.target = target,
.optimize = mod.optimize.?,
.prebuilt_v8_path = prebuilt_v8_path,
.cache_root = b.pathFromRoot(".lp-cache"),
};
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");
v8_mod.addOptions("default_exports", v8_opts);
mod.addImport("v8", v8_mod);
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
const os = switch (target.result.os.tag) {
.linux => "linux",
.macos => "macos",
else => return error.UnsupportedPlatform,
};
var lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/out/{s}/{s}/obj/zig/libc_v8.a",
.{ os, release_dir },
);
std.fs.cwd().access(lib_path, .{}) catch {
// legacy path
lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/out/{s}/obj/zig/libc_v8.a",
.{release_dir},
);
};
mod.addObjectFile(mod.owner.path(lib_path));
switch (target.result.os.tag) {
.macos => {
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
mod.linkFramework("CoreFoundation", .{});
},
else => {},
}
}
{
@@ -313,27 +374,14 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options, pre
mod.addCMacro("STDC_HEADERS", "1");
mod.addCMacro("TIME_WITH_SYS_TIME", "1");
mod.addCMacro("USE_NGHTTP2", "1");
mod.addCMacro("USE_OPENSSL", "1");
mod.addCMacro("OPENSSL_IS_BORINGSSL", "1");
mod.addCMacro("USE_MBEDTLS", "1");
mod.addCMacro("USE_THREADS_POSIX", "1");
mod.addCMacro("USE_UNIX_SOCKETS", "1");
}
try buildZlib(b, mod);
try buildBrotli(b, mod);
const boringssl_dep = b.dependency("boringssl-zig", .{
.target = target,
.optimize = mod.optimize.?,
.force_pic = true,
});
const ssl = boringssl_dep.artifact("ssl");
ssl.bundle_ubsan_rt = false;
const crypto = boringssl_dep.artifact("crypto");
crypto.bundle_ubsan_rt = false;
mod.linkLibrary(ssl);
mod.linkLibrary(crypto);
try buildMbedtls(b, mod);
try buildNghttp2(b, mod);
try buildCurl(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/vauth.c",
root ++ "lib/vtls/cipher_suite.c",
root ++ "lib/vtls/openssl.c",
root ++ "lib/vtls/hostcheck.c",
root ++ "lib/vtls/keylog.c",
root ++ "lib/vtls/mbedtls.c",
root ++ "lib/vtls/mbedtls_threadlock.c",
root ++ "lib/vtls/vtls.c",
root ++ "lib/vtls/vtls_scache.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.
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,
.paths = .{""},
.version = "0.0.0",
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.fingerprint = 0xda130f3af836cea0,
.dependencies = .{
.v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/e047d2a4d5af5783763f0f6a652fab8982a08603.tar.gz",
.hash = "v8-0.0.0-xddH65gMBACRBQMM7EwmVgfi94FJyyX-0jpe5KhXYhfv",
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/84cdca7cd9065f67c7933388f2091810fc485bc6.tar.gz",
.hash = "v8-0.0.0-xddH67vcAwCuN2gBsAO8TBzEw523KMroIKGrdZwc-Q-y",
},
// .v8 = .{ .path = "../zig-v8-fork" },
//.v8 = .{ .path = "../zig-v8-fork" }
.@"ada-singleheader" = .{
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
},
.@"boringssl-zig" = .{
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
},
},
.paths = .{""},
}

View File

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

View File

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

View File

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

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);
}
pub fn isHexColor(value: []const u8) bool {
fn isHexColor(value: []const u8) bool {
if (value.len == 0) {
return false;
}
@@ -199,7 +199,7 @@ pub fn isHexColor(value: []const u8) bool {
}
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;
}
@@ -551,7 +551,6 @@ test "Browser: CSS.StyleDeclaration: isNumericWithUnit - edge cases and invalid
test "Browser: CSS.StyleDeclaration: isHexColor - valid hex colors" {
try testing.expect(isHexColor("#000"));
try testing.expect(isHexColor("#0000"));
try testing.expect(isHexColor("#fff"));
try testing.expect(isHexColor("#123456"));
try testing.expect(isHexColor("#abcdef"));
@@ -564,6 +563,7 @@ test "Browser: CSS.StyleDeclaration: isHexColor - invalid hex colors" {
try testing.expect(!isHexColor("#"));
try testing.expect(!isHexColor("000"));
try testing.expect(!isHexColor("#00"));
try testing.expect(!isHexColor("#0000"));
try testing.expect(!isHexColor("#00000"));
try testing.expect(!isHexColor("#0000000"));
try testing.expect(!isHexColor("#000000000"));

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

View File

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

View File

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

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 CompositionEvent = @import("composition_event.zig").CompositionEvent;
const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent;
const PageTransitionEvent = @import("../events/PageTransitionEvent.zig");
const NavigateEvent = @import("../navigation/root.zig").NavigateEvent;
// Event interfaces
pub const Interfaces = .{
@@ -54,7 +54,7 @@ pub const Interfaces = .{
PopStateEvent,
CompositionEvent,
NavigationCurrentEntryChangeEvent,
PageTransitionEvent,
NavigateEvent,
};
pub const Union = generate.Union(Interfaces);
@@ -87,7 +87,7 @@ pub const Event = struct {
.navigation_current_entry_change_event => .{
.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;
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" });
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);
}

View File

@@ -179,13 +179,17 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
if (self.body) |body| {
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" });
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);
}

View File

@@ -48,7 +48,7 @@ pub fn set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
}
pub fn get_state(_: *History, page: *Page) !?js.Value {
if (page.session.navigation.currentEntry().state.value) |state| {
if (page.session.navigation.currentEntry().state) |state| {
const value = try js.Value.fromJson(page.js, state);
return value;
} 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 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 {
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 json = try state.toJson(arena);
_ = try page.session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true);
entry.state = json;
entry.url = url;
}
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 (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 CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
const CanvasRenderingContext2D = @import("../canvas/CanvasRenderingContext2D.zig");
const WebGLRenderingContext = @import("../canvas/WebGLRenderingContext.zig");
const WalkerChildren = @import("../dom/walker.zig").WalkerChildren;
// HTMLElement interfaces
pub const Interfaces = .{
@@ -491,29 +487,6 @@ pub const HTMLCanvasElement = struct {
pub const Self = parser.Canvas;
pub const prototype = *HTMLElement;
pub const subtype = .node;
/// This should be a union once we support other context types.
const ContextAttributes = struct {
alpha: bool,
color_space: []const u8 = "srgb",
};
/// Returns a drawing context on the canvas, or null if the context identifier
/// is not supported, or the canvas has already been set to a different context mode.
pub fn _getContext(
ctx_type: []const u8,
_: ?ContextAttributes,
) ?union(enum) { @"2d": CanvasRenderingContext2D, webgl: WebGLRenderingContext } {
if (std.mem.eql(u8, ctx_type, "2d")) {
return .{ .@"2d" = .{} };
}
if (std.mem.eql(u8, ctx_type, "webgl") or std.mem.eql(u8, ctx_type, "experimental-webgl")) {
return .{ .webgl = .{} };
}
return null;
}
};
pub const HTMLDListElement = struct {
@@ -1227,22 +1200,11 @@ pub const HTMLTemplateElement = struct {
pub const subtype = .node;
pub fn get_content(self: *parser.Template, page: *Page) !*parser.DocumentFragment {
const n: *parser.Node = @ptrCast(@alignCast(self));
const state = try page.getOrCreateNodeState(n);
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
if (state.template_content) |tc| {
return tc;
}
const tc = try parser.documentCreateDocumentFragment(@ptrCast(page.window.document));
const ntc: *parser.Node = @ptrCast(@alignCast(tc));
// move existing template's childnodes to the fragment.
const walker = WalkerChildren{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(n, next) orelse break;
_ = try parser.nodeAppendChild(ntc, next.?);
}
state.template_content = tc;
return tc;
}
@@ -1394,7 +1356,3 @@ test "Browser: HTML.HtmlScriptElement" {
test "Browser: HTML.HtmlSlotElement" {
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});
};
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 {
@@ -96,7 +96,7 @@ pub const Location = struct {
}
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .replace = null });
return page.navigateFromWebAPI(url, .{ .reason = .script }, .replace);
}
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 NamedFunction = Caller.NamedFunction;
const PersistentObject = v8.Persistent(v8.Object);
const PersistentValue = v8.Persistent(v8.Value);
const PersistentModule = v8.Persistent(v8.Module);
const PersistentPromise = v8.Persistent(v8.Promise);
const PersistentFunction = v8.Persistent(v8.Function);
@@ -71,9 +70,6 @@ identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty,
// we now simply persist every time persist() is called.
js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty,
// js_value_list tracks persisted js values.
js_value_list: std.ArrayListUnmanaged(PersistentValue) = .empty,
// Various web APIs depend on having a persistent promise resolver. They
// require for this PromiseResolver to be valid for a lifetime longer than
// the function that resolves/rejects them.
@@ -153,10 +149,6 @@ pub fn deinit(self: *Context) void {
p.deinit();
}
for (self.js_value_list.items) |*p| {
p.deinit();
}
for (self.persisted_promise_resolvers.items) |*p| {
p.deinit();
}
@@ -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) {
const mod, const owned_url = blk: {
const arena = self.arena;
// gop will _always_ initiated if cacheable == true
var gop: std.StringHashMapUnmanaged(ModuleEntry).GetOrPutResult = undefined;
if (cacheable) {
gop = try self.module_cache.getOrPut(arena, url);
if (gop.found_existing) {
if (gop.value_ptr.module != null) {
return if (comptime want_result) gop.value_ptr.* else {};
if (self.module_cache.get(url)) |entry| {
// The dynamic import will create an entry without the
// module to prevent multiple calls from asynchronously
// loading the same module. If we're here, without the
// module, then it's time to load it.
if (entry.module != null) {
return if (comptime want_result) entry else {};
}
} else {
// first time seing this
gop.value_ptr.* = .{};
}
}
const m = try compileModule(self.isolate, src, url);
const owned_url = try arena.dupeZ(u8, url);
if (cacheable) {
// compileModule is synchronous - nothing can modify the cache during compilation
std.debug.assert(gop.value_ptr.module == null);
const arena = self.arena;
const owned_url = try arena.dupe(u8, url);
gop.value_ptr.module = PersistentModule.init(self.isolate, m);
if (!gop.found_existing) {
gop.key_ptr.* = owned_url;
}
}
break :blk .{ m, owned_url };
};
try self.postCompileModule(mod, owned_url);
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
errdefer _ = self.module_identifier.remove(m.getIdentityHash());
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;
}
const evaluated = mod.evaluate(v8_context) catch {
std.debug.assert(mod.getStatus() == .kErrored);
const evaluated = m.evaluate(v8_context) catch {
std.debug.assert(m.getStatus() == .kErrored);
// Some module-loading errors aren't handled by TryCatch. We need to
// get the error from the module itself.
log.warn(.js, "evaluate module", .{
.specifier = owned_url,
.message = self.valueToString(mod.getException(), .{}) catch "???",
.message = self.valueToString(m.getException(), .{}) catch "???",
});
return error.EvaluationError;
};
@@ -300,46 +301,28 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
// be cached
std.debug.assert(cacheable);
// entry has to have been created atop this function
const entry = self.module_cache.getPtr(owned_url).?;
const persisted_module = PersistentModule.init(self.isolate, m);
const persisted_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
// and the module must have been set after we compiled it
std.debug.assert(entry.module != null);
std.debug.assert(entry.module_promise == null);
var gop = try self.module_cache.getOrPut(arena, owned_url);
if (gop.found_existing) {
// If we're here, it's because we had a cache entry, but no
// module. This happens because both our synch and async
// module loaders create the entry to prevent concurrent
// loads of the same resource (like Go's Singleflight).
std.debug.assert(gop.value_ptr.module == null);
std.debug.assert(gop.value_ptr.module_promise == null);
entry.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
return if (comptime want_result) entry.* else {};
}
// After we compile a module, whether it's a top-level one, or a nested one,
// we always want to track its identity (so that, if this module imports other
// modules, we can resolve the full URL), and preload any dependent modules.
fn postCompileModule(self: *Context, mod: v8.Module, url: [:0]const u8) !void {
try self.module_identifier.putNoClobber(self.arena, mod.getIdentityHash(), url);
const v8_context = self.v8_context;
// Non-async modules are blocking. We can download them in parallel, but
// they need to be processed serially. So we want to get the list of
// dependent modules this module has and start downloading them asap.
const requests = mod.getModuleRequests();
const script_manager = self.script_manager.?;
for (0..requests.length()) |i| {
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
const specifier = try self.jsStringToZig(req.getSpecifier(), .{});
const normalized_specifier = try script_manager.resolveSpecifier(
self.call_arena,
specifier,
url,
);
const nested_gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
if (!nested_gop.found_existing) {
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
nested_gop.key_ptr.* = owned_specifier;
nested_gop.value_ptr.* = .{};
try script_manager.preloadImport(owned_specifier, url);
}
gop.value_ptr.module = persisted_module;
gop.value_ptr.module_promise = persisted_promise;
} else {
gop.value_ptr.* = ModuleEntry{
.module = persisted_module,
.module_promise = persisted_promise,
.resolver_promise = null,
};
}
return if (comptime want_result) gop.value_ptr.* else {};
}
// == Creators ==
@@ -417,8 +400,9 @@ pub fn zigValueToJs(self: *Context, value: anytype) !v8.Value {
},
.pointer => |ptr| switch (ptr.size) {
.one => {
if (types.has(ptr.child)) {
const template = self.templates[types.getId(ptr.child)];
const type_name = @typeName(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);
return js_obj.toValue();
}
@@ -452,8 +436,9 @@ pub fn zigValueToJs(self: *Context, value: anytype) !v8.Value {
else => {},
},
.@"struct" => |s| {
if (types.has(T)) {
const template = self.templates[types.getId(T)];
const type_name = @typeName(T);
if (@hasField(types.Lookup, type_name)) {
const template = self.templates[@field(types.LOOKUP, type_name)];
const js_obj = try self.mapZigInstanceToJs(template, value);
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.
// See the TaggedAnyOpaque struct for more details.
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.* = .{
.ptr = value,
@@ -669,7 +655,7 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
if (!js_value.isObject()) {
return error.InvalidArgument;
}
if (types.has(ptr.child)) {
if (@hasField(types.Lookup, @typeName(ptr.child))) {
const js_obj = js_value.castTo(v8.Object);
return self.typeTaggedAnyOpaque(named_function, *types.Receiver(ptr.child), js_obj);
}
@@ -785,39 +771,41 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
// Extracted so that it can be used in both jsValueToZig and in
// probeJsValueToZig. Avoids having to duplicate this logic when probing.
fn jsValueToStruct(self: *Context, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !?T {
return switch (T) {
js.Function => {
if (T == js.Function) {
if (!js_value.isFunction()) {
return null;
}
return try self.createFunction(js_value);
},
// zig fmt: off
js.TypedArray(u8), js.TypedArray(u16), js.TypedArray(u32), js.TypedArray(u64),
js.TypedArray(i8), js.TypedArray(i16), js.TypedArray(i32), js.TypedArray(i64),
js.TypedArray(f32), js.TypedArray(f64),
// zig fmt: on
=> {
const ValueType = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child;
const slice = (try self.jsValueToTypedArray(ValueType, js_value)) orelse return null;
return .{ .values = slice };
},
js.String => .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) },
// Caller wants an opaque js.Object. Probably a parameter
// that it needs to pass back into a callback.
js.Object => js.Object{
.js_obj = js_value.castTo(v8.Object),
.context = self,
},
else => {
}
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
const VT = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child;
const arr = (try self.jsValueToTypedArray(VT, js_value)) orelse return null;
return .{ .values = arr };
}
if (T == js.String) {
return .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) };
}
const js_obj = js_value.castTo(v8.Object);
if (comptime T == js.Object) {
// Caller wants an opaque js.Object. Probably a parameter
// that it needs to pass back into a callback
return js.Object{
.js_obj = js_obj,
.context = self,
};
}
if (!js_value.isObject()) {
return null;
}
const v8_context = self.v8_context;
const isolate = self.isolate;
var value: T = undefined;
inline for (@typeInfo(T).@"struct".fields) |field| {
const name = field.name;
@@ -832,8 +820,6 @@ fn jsValueToStruct(self: *Context, comptime named_function: NamedFunction, compt
}
}
return value;
},
};
}
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(
self.arena,
self.arena, // might need to survive until the module is loaded
specifier,
referrer_path,
);
const entry = self.module_cache.getPtr(normalized_specifier).?;
if (entry.module) |m| {
return m.castToModule().handle;
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
if (gop.found_existing) {
if (gop.value_ptr.module) |m| {
return m.handle;
}
// We don't have a module, but we do have a cache entry for it
// That means we're already trying to load it. We just have
// to wait for it to be done.
} else {
// I don't think it's possible for us to be here. This is
// only ever called by v8 when we evaluate a module. But
// before evaluating, we should have already started
// downloading all of the module's nested modules. So it
// should be impossible that this is the first time we've
// heard about this module.
// But, I'm not confident enough in that, and ther's little
// harm in handling this case.
@branchHint(.unlikely);
gop.value_ptr.* = .{};
try self.script_manager.?.preloadImport(normalized_specifier, referrer_path);
}
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);
defer try_catch.deinit();
const mod = try compileModule(self.isolate, source.src(), normalized_specifier);
try self.postCompileModule(mod, normalized_specifier);
entry.module = PersistentModule.init(self.isolate, mod);
return entry.module.?.castToModule().handle;
const entry = self.module(true, source.src(), normalized_specifier, true) catch |err| {
switch (err) {
error.EvaluationError => {
// This is a sentinel value telling us that the error was already
// logged. Some module-loading errors aren't captured by Try/Catch.
// We need to handle those errors differently, where the module
// exists.
},
else => log.warn(.js, "compile resolved module", .{
.specifier = normalized_specifier,
.stack = try_catch.stack(self.call_arena) catch null,
.src = try_catch.sourceLine(self.call_arena) catch "err",
.line = try_catch.sourceLineNumber() orelse 0,
.exception = (try_catch.exception(self.call_arena) catch @errorName(err)) orelse @errorName(err),
}),
}
return null;
};
// entry.module is always set when returning from self.module()
return entry.module.?.handle;
}
// 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
// since the module is alrady loaded,
std.debug.assert(gop.value_ptr.module != 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 });
}
}
std.debug.assert(gop.value_ptr.module_promise != null);
// like before, we want to set this up so that if anything else
// 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;
}
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));
}
const op = js_obj.getInternalField(0).castTo(v8.External).get();
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;
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);
}
const prototype_index = types.PrototypeTable[type_index];
const prototype_index = types.PROTOTYPE_TABLE[type_index];
if (prototype_index == expected_type_index) {
return @ptrFromInt(base_ptr + total_offset);
}
@@ -1587,7 +1582,7 @@ fn probeJsValueToZig(self: *Context, comptime named_function: NamedFunction, com
if (!js_value.isObject()) {
return .{ .invalid = {} };
}
if (types.has(ptr.child)) {
if (@hasField(types.Lookup, @typeName(ptr.child))) {
const js_obj = js_value.castTo(v8.Object);
// There's a bit of overhead in doing this, so instead
// of having a version of typeTaggedAnyOpaque which

View File

@@ -111,14 +111,16 @@ pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) {
const TI = @typeInfo(Struct.prototype);
const ProtoType = types.Receiver(TI.pointer.child);
if (!types.has(ProtoType)) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ @typeName(ProtoType), @typeName(Struct) }));
const proto_name = @typeName(types.Receiver(TI.pointer.child));
if (@hasField(types.Lookup, proto_name) == false) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ proto_name, @typeName(Struct) }));
}
// 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
// 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

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
// specific instance of the the Window.
if (@hasDecl(Global, "prototype")) {
const ProtoType = types.Receiver(@typeInfo(Global.prototype).pointer.child);
js_global.inherit(templates[types.getId(ProtoType)]);
const proto_type = types.Receiver(@typeInfo(Global.prototype).pointer.child);
const proto_name = @typeName(proto_type);
const proto_index = @field(types.LOOKUP, proto_name);
js_global.inherit(templates[proto_index]);
}
const 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().?;
if (@hasDecl(Struct, "prototype")) {
const ProtoType = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
if (!types.has(ProtoType)) {
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ @typeName(ProtoType));
const proto_type = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
const proto_name = @typeName(proto_type);
if (@hasField(types.Lookup, proto_name) == false) {
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name);
}
const 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();
_ = self_obj.setPrototype(v8_context, proto_obj);

View File

@@ -48,6 +48,8 @@ const NamedFunction = Context.NamedFunction;
// Env.JsObject. Want a TypedArray? Env.TypedArray.
pub fn TypedArray(comptime T: type) type {
return struct {
pub const _TYPED_ARRAY_ID_KLUDGE = true;
values: []const T,
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
@@ -148,8 +150,6 @@ pub const Exception = struct {
};
pub const Value = struct {
const PersistentValue = v8.Persistent(v8.Value);
value: v8.Value,
context: *const Context,
@@ -163,15 +163,6 @@ pub const Value = struct {
const value = try v8.Json.parse(ctx.v8_context, json_string);
return Value{ .context = ctx, .value = value };
}
pub fn persist(self: Value, context: *Context) !Value {
const js_value = self.value;
const persisted = PersistentValue.init(context.isolate, js_value);
try context.js_value_list.append(context.arena, persisted);
return Value{ .context = context, .value = persisted.toValue() };
}
};
pub const ValueIterator = struct {
@@ -336,8 +327,9 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
return v8.initNull(isolate).toValue();
},
.@"struct" => {
switch (@TypeOf(value)) {
ArrayBuffer => {
const T = @TypeOf(value);
if (T == ArrayBuffer) {
const values = value.values;
const len = values.len;
var array_buffer: v8.ArrayBuffer = undefined;
@@ -347,20 +339,16 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
return .{ .handle = array_buffer.handle };
},
// zig fmt: off
TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64),
TypedArray(i8), TypedArray(i16), TypedArray(i32), TypedArray(i64),
TypedArray(f32), TypedArray(f64),
// zig fmt: on
=> {
}
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 TypedArray type: " ++ @typeName(value_type)),
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
};
var array_buffer: v8.ArrayBuffer = undefined;
@@ -400,9 +388,7 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
}
// We normally don't fail in this function unless fail == true
// but this can never be valid.
@compileError("Invalid TypedArray type: " ++ @typeName(value_type));
},
else => {},
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
}
},
.@"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("../navigation/root.zig").Interfaces,
@import("../file/root.zig").Interfaces,
@import("../canvas/root.zig").Interfaces,
@import("../xhr/form_data.zig").Interfaces,
@import("../xmlserializer/xmlserializer.zig").Interfaces,
@import("../fetch/fetch.zig").Interfaces,
@@ -27,127 +26,104 @@ const Interfaces = generate.Tuple(.{
pub const Types = @typeInfo(Interfaces).@"struct".fields;
/// Integer type we use for `Index` enum. Can be u8 at min.
pub const BackingInt = std.math.IntFittingRange(0, @max(std.math.maxInt(u8), Types.len));
/// Imagine we have a type `Cat` which has a getter:
///
/// fn get_owner(self: *Cat) *Owner {
/// return self.owner;
/// }
///
/// When we execute `caller.getter`, we'll end up doing something like:
///
/// const res = @call(.auto, Cat.get_owner, .{cat_instance});
///
/// How do we turn `res`, which is an *Owner, into something we can return
/// to v8? We need the ObjectTemplate associated with Owner. How do we
/// get that? Well, we store all the ObjectTemplates in an array that's
/// tied to env. So we do something like:
///
/// env.templates[index_of_owner].initInstance(...);
///
/// But how do we get that `index_of_owner`? `Index` is an enum
/// that looks like:
///
/// pub const Index = enum(BackingInt) {
/// cat = 0,
/// owner = 1,
/// ...
/// }
///
/// (`BackingInt` is calculated at comptime regarding to interfaces we have)
/// So to get the template index of `owner`, simply do:
///
/// const index_id = types.getId(@TypeOf(res));
pub const Index = blk: {
var fields: [Types.len]std.builtin.Type.EnumField = undefined;
// Imagine we have a type Cat which has a getter:
//
// fn get_owner(self: *Cat) *Owner {
// return self.owner;
// }
//
// When we execute caller.getter, we'll end up doing something like:
// const res = @call(.auto, Cat.get_owner, .{cat_instance});
//
// How do we turn `res`, which is an *Owner, into something we can return
// to v8? We need the ObjectTemplate associated with Owner. How do we
// get that? Well, we store all the ObjectTemplates in an array that's
// tied to env. So we do something like:
//
// env.templates[index_of_owner].initInstance(...);
//
// But how do we get that `index_of_owner`? `Lookup` is a struct
// that looks like:
//
// const Lookup = struct {
// comptime cat: usize = 0,
// comptime owner: usize = 1,
// ...
// }
//
// So to get the template index of `owner`, we can do:
//
// const index_id = @field(type_lookup, @typeName(@TypeOf(res));
//
pub const Lookup = blk: {
var fields: [Types.len]std.builtin.Type.StructField = undefined;
for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
fields[i] = .{ .name = @typeName(Receiver(Struct)), .value = i };
}
break :blk @Type(.{
.@"enum" = .{
.fields = &fields,
.tag_type = BackingInt,
.is_exhaustive = true,
.decls = &.{},
},
});
};
/// 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),
})),
};
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype") and @typeInfo(Struct.prototype) != .pointer) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s} must be a pointer", .{ @typeName(Struct.prototype), @typeName(Struct) }));
}
break :proto_index i;
fields[i] = .{
.name = @typeName(Receiver(Struct)),
.type = usize,
.is_comptime = true,
.alignment = @alignOf(usize),
.default_value_ptr = &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;
};
/// This is essentially meta data for each type. Each is stored in `env.meta_lookup`.
/// The index for a type can be retrieved via:
/// const index = types.getIndex(Receiver(Struct));
/// const meta = env.meta_lookup[@intFromEnum(index)];
///
/// Or:
/// const id = types.getId(Receiver(Struct));
/// const meta = env.meta_lookup[id];
// This is essentially meta data for each type. Each is stored in env.meta_lookup
// The index for a type can be retrieved via:
// const index = @field(TYPE_LOOKUP, @typeName(Receiver(Struct)));
// const meta = env.meta_lookup[index];
pub const Meta = struct {
// Every type is given a unique index. That index is used to lookup various
// things, i.e. the prototype chain.
index: BackingInt,
index: u16,
// 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

View File

@@ -24,7 +24,6 @@ pub const Mime = struct {
// IANA defines max. charset value length as 40.
// We keep 41 for null-termination since HTML parser expects in this format.
charset: [41]u8 = default_charset,
charset_len: usize = 5,
/// String "UTF-8" continued by null characters.
pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
@@ -54,25 +53,9 @@ pub const Mime = struct {
other: struct { type: []const u8, sub_type: []const u8 },
};
pub fn contentTypeString(mime: *const Mime) []const u8 {
return switch (mime.content_type) {
.text_xml => "text/xml",
.text_html => "text/html",
.text_javascript => "application/javascript",
.text_plain => "text/plain",
.text_css => "text/css",
.application_json => "application/json",
else => "",
};
}
/// Returns the null-terminated charset value.
pub fn charsetStringZ(mime: *const Mime) [:0]const u8 {
return mime.charset[0..mime.charset_len :0];
}
pub fn charsetString(mime: *const Mime) []const u8 {
return mime.charset[0..mime.charset_len];
pub fn charsetString(mime: *const Mime) [:0]const u8 {
return @ptrCast(&mime.charset);
}
/// Removes quotes of value if quotes are given.
@@ -116,7 +99,6 @@ pub const Mime = struct {
const params = trimLeft(normalized[type_len..]);
var charset: [41]u8 = undefined;
var charset_len: usize = undefined;
var it = std.mem.splitScalar(u8, params, ';');
while (it.next()) |attr| {
@@ -142,7 +124,6 @@ pub const Mime = struct {
@memcpy(charset[0..attribute_value.len], attribute_value);
// Null-terminate right after attribute value.
charset[attribute_value.len] = 0;
charset_len = attribute_value.len;
},
}
}
@@ -150,7 +131,6 @@ pub const Mime = struct {
return .{
.params = params,
.charset = charset,
.charset_len = charset_len,
.content_type = content_type,
};
}
@@ -531,9 +511,9 @@ fn expect(expected: Expectation, input: []const u8) !void {
if (expected.charset) |ec| {
// We remove the null characters for testing purposes here.
try testing.expectEqual(ec, actual.charsetString());
try testing.expectEqual(ec, actual.charsetString()[0..ec.len]);
} else {
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 NavigationHistoryEntry = @import("root.zig").NavigationHistoryEntry;
const NavigationTransition = @import("root.zig").NavigationTransition;
const NavigationState = @import("root.zig").NavigationState;
const NavigationCurrentEntryChangeEvent = @import("root.zig").NavigationCurrentEntryChangeEvent;
const NavigationEventTarget = @import("NavigationEventTarget.zig");
@@ -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 {
switch (kind) {
.replace => {
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = null }, page, dispatch);
_ = try self.replaceEntry(url, null, page, dispatch);
},
.push => |state| {
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, dispatch);
_ = try self.pushEntry(url, state, page, dispatch);
},
.traverse => |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.
/// For that, use `navigate`.
pub fn pushEntry(
self: *Navigation,
_url: []const u8,
state: NavigationState,
page: *Page,
dispatch: bool,
) !*NavigationHistoryEntry {
pub fn pushEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry {
const arena = page.session.arena;
const url = try arena.dupe(u8, _url);
@@ -167,24 +160,18 @@ pub fn pushEntry(
// we don't always have a current entry...
const previous = if (self.entries.items.len > 0) self.currentEntry() else null;
try self.entries.append(arena, entry);
self.index = index;
if (previous) |prev| {
if (dispatch) {
NavigationCurrentEntryChangeEvent.dispatch(self, prev, .push);
}
}
self.index = index;
return entry;
}
pub fn replaceEntry(
self: *Navigation,
_url: []const u8,
state: NavigationState,
page: *Page,
dispatch: bool,
) !*NavigationHistoryEntry {
pub fn replaceEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry {
const arena = page.session.arena;
const url = try arena.dupe(u8, _url);
@@ -197,7 +184,7 @@ pub fn replaceEntry(
const entry = try arena.create(NavigationHistoryEntry);
entry.* = NavigationHistoryEntry{
.id = id_str,
.key = previous.key,
.key = id_str,
.url = url,
.state = state,
};
@@ -255,20 +242,7 @@ pub fn navigate(
// todo: Fire navigate event
try finished.resolve({});
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true);
} else {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
}
},
.replace => |state| {
if (is_same_document) {
page.url = new_url;
try committed.resolve({});
// todo: Fire navigate event
try finished.resolve({});
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true);
_ = try self.pushEntry(url, state, page, true);
} else {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
}
@@ -289,6 +263,7 @@ pub fn navigate(
.reload => {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
},
else => unreachable,
}
return .{
@@ -300,13 +275,7 @@ pub fn navigate(
pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn {
const opts = _opts orelse NavigateOptions{};
const json = if (opts.state) |state| state.toJson(page.session.arena) catch return error.DataClone else null;
const kind: NavigationKind = switch (opts.history) {
.replace => .{ .replace = json },
.push, .auto => .{ .push = json },
};
return try self.navigate(_url, kind, page);
return try self.navigate(_url, .{ .push = json }, page);
}
pub const ReloadOptions = struct {
@@ -321,7 +290,7 @@ pub fn _reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !Navigatio
const entry = self.currentEntry();
if (opts.state) |state| {
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);
}
@@ -354,6 +323,6 @@ pub fn _updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions
const arena = page.session.arena;
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);
}

View File

@@ -51,16 +51,11 @@ pub const NavigationType = enum {
pub const NavigationKind = union(NavigationType) {
push: ?[]const u8,
replace: ?[]const u8,
replace,
traverse: usize,
reload,
};
pub const NavigationState = struct {
source: enum { history, navigation },
value: ?[]const u8,
};
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry
pub const NavigationHistoryEntry = struct {
pub const prototype = *EventTarget;
@@ -69,7 +64,7 @@ pub const NavigationHistoryEntry = struct {
id: []const u8,
key: []const u8,
url: ?[]const u8,
state: NavigationState,
state: ?[]const u8,
pub fn get_id(self: *const NavigationHistoryEntry) []const u8 {
return self.id;
@@ -100,17 +95,13 @@ pub const NavigationHistoryEntry = struct {
return self.url;
}
pub const StateReturn = union(enum) { value: ?js.Value, undefined: void };
pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !StateReturn {
if (self.state.source == .navigation) {
if (self.state.value) |value| {
return .{ .value = try js.Value.fromJson(page.js, value) };
pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !?js.Value {
if (self.state) |state| {
return try js.Value.fromJson(page.js, state);
} else {
return null;
}
}
return .undefined;
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation
@@ -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");
test "Browser: Navigation" {
try testing.htmlRunner("html/navigation/navigation.html");

View File

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

View File

@@ -31,7 +31,6 @@ const Mime = @import("../mime.zig").Mime;
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const Http = @import("../../http/Http.zig");
const js = @import("../js/js.zig");
// XHR interfaces
// https://xhr.spec.whatwg.org/#interface-xmlhttprequest
@@ -129,19 +128,21 @@ pub const XMLHttpRequest = struct {
JSON,
};
const JSONValue = std.json.Value;
const Response = union(ResponseType) {
Empty: void,
Text: []const u8,
ArrayBuffer: void,
Blob: void,
Document: *parser.Document,
JSON: js.Value,
JSON: JSONValue,
};
const ResponseObj = union(enum) {
Document: *parser.Document,
Failure: void,
JSON: js.Value,
JSON: JSONValue,
fn deinit(self: ResponseObj) void {
switch (self) {
@@ -604,7 +605,7 @@ pub const XMLHttpRequest = struct {
}
// 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.state == .loading or self.state == .done) {
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
// on thiss received bytes. If that threw an exception, then return
// null.
self.setResponseObjJSON(page);
self.setResponseObjJSON();
}
if (self.response_obj) |obj| {
@@ -677,7 +678,7 @@ pub const XMLHttpRequest = struct {
}
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 = {} };
return;
};
@@ -690,24 +691,22 @@ pub const XMLHttpRequest = struct {
};
}
// setResponseObjJSON parses the received bytes as a js.Value.
fn setResponseObjJSON(self: *XMLHttpRequest, page: *Page) void {
const value = js.Value.fromJson(
page.js,
// setResponseObjJSON parses the received bytes as a std.json.Value.
fn setResponseObjJSON(self: *XMLHttpRequest) void {
// TODO should we use parseFromSliceLeaky if we expect the allocator is
// already an arena?
const p = std.json.parseFromSliceLeaky(
JSONValue,
self.arena,
self.response_bytes.items,
.{},
) catch |e| {
log.warn(.http, "invalid json", .{ .err = e, .url = self.url, .source = "xhr" });
self.response_obj = .{ .Failure = {} };
return;
};
const pvalue = value.persist(page.js) catch |e| {
log.warn(.http, "persist v8 json value", .{ .err = e, .url = self.url, .source = "xhr" });
self.response_obj = .{ .Failure = {} };
return;
};
self.response_obj = .{ .JSON = pvalue };
self.response_obj = .{ .JSON = p };
}
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),
else => {},
},
13 => switch (@as(u104, @bitCast(domain[0..13].*))) {
asUint(u104, "Accessibility") => return @import("domains/accessibility.zig").processMessage(command),
else => {},
},
else => {},
}
@@ -473,14 +468,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
return if (raw_url.len == 0) null else raw_url;
}
pub fn getTitle(self: *const Self) ?[]const u8 {
const page = self.session.currentPage() orelse return null;
return page.getTitle() catch |err| {
log.err(.cdp, "page title", .{ .err = err });
return null;
};
}
pub fn networkEnable(self: *Self) !void {
try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail);
try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart);
@@ -551,8 +538,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void {
const self: *Self = @ptrCast(@alignCast(ctx));
defer self.resetNotificationArena();
return @import("domains/page.zig").pageNavigated(self.notification_arena, self, msg);
return @import("domains/page.zig").pageNavigated(self, msg);
}
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,
getWindowForTarget,
setDownloadBehavior,
close,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
@@ -55,7 +54,6 @@ pub fn processMessage(cmd: anytype) !void {
.grantPermissions => return grantPermissions(cmd),
.getWindowForTarget => return getWindowForTarget(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 dom_node = @import("../../browser/dom/node.zig");
const Element = @import("../../browser/dom/element.zig").Element;
const dump = @import("../../browser/dump.zig");
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
@@ -42,8 +41,6 @@ pub fn processMessage(cmd: anytype) !void {
getBoxModel,
requestChildNodes,
getFrameOwner,
getOuterHTML,
requestNode,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
@@ -61,8 +58,6 @@ pub fn processMessage(cmd: anytype) !void {
.getBoxModel => return getBoxModel(cmd),
.requestChildNodes => return requestChildNodes(cmd),
.getFrameOwner => return getFrameOwner(cmd),
.getOuterHTML => return getOuterHTML(cmd),
.requestNode => return requestNode(cmd),
}
}
@@ -499,38 +494,6 @@ fn getFrameOwner(cmd: anytype) !void {
return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{});
}
fn getOuterHTML(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
objectId: ?[]const u8 = null,
includeShadowDOM: bool = false,
})) orelse return error.InvalidParams;
if (params.includeShadowDOM) {
log.warn(.cdp, "not implemented", .{ .feature = "DOM.getOuterHTML: Not implemented includeShadowDOM parameter" });
}
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
var aw = std.Io.Writer.Allocating.init(cmd.arena);
try dump.writeNode(node._node, .{}, &aw.writer);
return cmd.sendResult(.{ .outerHTML = aw.written() }, .{});
}
fn requestNode(cmd: anytype) !void {
const params = (try cmd.params(struct {
objectId: []const u8,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const node = try getNode(cmd.arena, bc, null, null, params.objectId);
return cmd.sendResult(.{ .nodeId = node.id }, .{});
}
const testing = @import("../testing.zig");
test "cdp.dom: getSearchResults unknown search id" {

View File

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

View File

@@ -20,7 +20,6 @@ const std = @import("std");
const Page = @import("../../browser/page.zig").Page;
const timestampF = @import("../../datetime.zig").timestamp;
const Notification = @import("../../notification.zig").Notification;
const log = @import("../../log.zig");
const Allocator = std.mem.Allocator;
@@ -33,7 +32,6 @@ pub fn processMessage(cmd: anytype) !void {
createIsolatedWorld,
navigate,
stopLoading,
close,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
@@ -44,7 +42,6 @@ pub fn processMessage(cmd: anytype) !void {
.createIsolatedWorld => return createIsolatedWorld(cmd),
.navigate => return navigate(cmd),
.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 {
const params = (try cmd.params(struct {
frameId: []const u8,
worldName: []const u8,
grantUniveralAccess: bool = false,
grantUniveralAccess: bool,
})) orelse return error.InvalidParams;
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
// 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;
bc.loader_id = bc.cdp.loader_id_gen.next();
try page.navigate(params.url, .{
.reason = .address_bar,
@@ -227,7 +188,8 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
// 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});
bc.loader_id = bc.cdp.loader_id_gen.next();
const loader_id = bc.loader_id;
const target_id = bc.target_id orelse unreachable;
bc.reset();
@@ -271,30 +233,6 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
try cdp.sendEvent("Page.frameStartedLoading", .{
.frameId = target_id,
}, .{ .session_id = session_id });
}
pub fn pageRemove(bc: anytype) !void {
// The main page is going to be removed, we need to remove contexts from other worlds first.
for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.removeContext();
}
}
pub fn pageCreated(bc: anytype, page: *Page) !void {
for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.createContextAndLoadPolyfills(bc.arena, page);
}
}
pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{event.req_id});
const target_id = bc.target_id orelse unreachable;
const timestamp = event.timestamp;
var cdp = bc.cdp;
// Drivers are sensitive to the order of events. Some more than others.
// The result for the Page.navigate seems like it _must_ come after
@@ -321,17 +259,6 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
}, .{ .session_id = session_id });
}
const reason_: ?[]const u8 = switch (event.opts.reason) {
.anchor => "anchorClick",
.script, .history, .navigation => "scriptInitiated",
.form => switch (event.opts.method) {
.GET => "formSubmissionGet",
.POST => "formSubmissionPost",
else => unreachable,
},
.address_bar => null,
};
if (reason_ != null) {
try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{
.frameId = target_id,
@@ -365,14 +292,37 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
false,
);
}
}
pub fn pageRemove(bc: anytype) !void {
// The main page is going to be removed, we need to remove contexts from other worlds first.
for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.removeContext();
}
}
pub fn pageCreated(bc: anytype, page: *Page) !void {
for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.createContextAndLoadPolyfills(bc.arena, page);
}
}
pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
const loader_id = bc.loader_id;
const target_id = bc.target_id orelse unreachable;
const timestamp = event.timestamp;
var cdp = bc.cdp;
// frameNavigated event
try cdp.sendEvent("Page.frameNavigated", .{
.type = "Navigation",
.frame = Frame{
.id = target_id,
.url = event.url,
.loaderId = loader_id,
.loaderId = bc.loader_id,
.securityOrigin = bc.security_origin,
.secureContextType = bc.secure_context_type,
},

View File

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

View File

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

View File

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

View File

@@ -23,42 +23,67 @@ const Allocator = std.mem.Allocator;
const log = @import("log.zig");
const App = @import("app.zig").App;
const Server = @import("server.zig").Server;
const SigHandler = @import("sighandler.zig").SigHandler;
const Browser = @import("browser/browser.zig").Browser;
const DumpStripMode = @import("browser/dump.zig").Opts.StripMode;
const build_config = @import("build_config");
var _app: ?*App = null;
var _server: ?Server = null;
pub fn main() !void {
// allocator
// - in Debug mode we use the General Purpose Allocator to detect memory leaks
// - in Release mode we use the c allocator
var gpa_instance: std.heap.DebugAllocator(.{}) = .init;
const gpa = if (builtin.mode == .Debug) gpa_instance.allocator() else std.heap.c_allocator;
var gpa: std.heap.DebugAllocator(.{}) = .init;
const alloc = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator;
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);
const arena = arena_instance.allocator();
defer arena_instance.deinit();
var sighandler = SigHandler{ .arena = arena };
try sighandler.install();
run(gpa, arena, &sighandler) catch |err| {
run(alloc) catch |err| {
// 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
// invalid. (We should just move the arena up to main)
// the args_arena is gone. We need to set it to something that's not
// invalid. (We should just move the args_arena up to main)
log.opts.filter_scopes = &.{};
log.fatal(.app, "exit", .{ .err = err });
std.posix.exit(1);
};
}
fn run(gpa: Allocator, arena: Allocator, sighandler: *SigHandler) !void {
const args = try parseArgs(arena);
// Handle app shutdown gracefuly on signals.
fn shutdown() void {
const sigaction: std.posix.Sigaction = .{
.handler = .{
.handler = struct {
pub fn handler(_: c_int) callconv(.c) void {
// Shutdown service gracefuly.
if (_server) |server| {
server.deinit();
}
if (_app) |app| {
app.deinit();
}
std.posix.exit(0);
}
}.handler,
},
.mask = std.posix.empty_sigset,
.flags = 0,
};
// Exit the program on SIGINT signal. When running the browser in a Docker
// container, sending a CTRL-C (SIGINT) signal is catched but doesn't exit
// the program. Here we force exiting on SIGINT.
std.posix.sigaction(std.posix.SIG.INT, &sigaction, null);
std.posix.sigaction(std.posix.SIG.TERM, &sigaction, null);
std.posix.sigaction(std.posix.SIG.QUIT, &sigaction, null);
}
fn run(alloc: Allocator) !void {
var args_arena = std.heap.ArenaAllocator.init(alloc);
defer args_arena.deinit();
const args = try parseArgs(args_arena.allocator());
switch (args.mode) {
.help => {
@@ -85,13 +110,13 @@ fn run(gpa: Allocator, arena: Allocator, sighandler: *SigHandler) !void {
const user_agent = blk: {
const USER_AGENT = "User-Agent: Lightpanda/1.0";
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;
};
// _app is global to handle graceful shutdown.
var app = try App.init(gpa, .{
_app = try App.init(alloc, .{
.run_mode = args.mode,
.http_proxy = args.httpProxy(),
.proxy_bearer_token = args.proxyBearerToken(),
@@ -102,23 +127,24 @@ fn run(gpa: Allocator, arena: Allocator, sighandler: *SigHandler) !void {
.http_max_concurrent = args.httpMaxConcurrent(),
.user_agent = user_agent,
});
const app = _app.?;
defer app.deinit();
app.telemetry.record(.{ .run = {} });
switch (args.mode) {
.serve => |opts| {
log.debug(.app, "startup", .{ .mode = "serve" });
const address = std.net.Address.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 });
return args.printUsageAndExit(false);
};
// _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();
try sighandler.on(Server.stop, .{&server});
// max timeout of 1 week.
const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(i32, opts.timeout) * 1000;
server.run(address, timeout) catch |err| {
@@ -862,7 +888,7 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
}
if (std.mem.eql(u8, path, "/xhr/json")) {
return req.respond("{\"over\":\"9000!!!\",\"updated_at\":1765867200000}", .{
return req.respond("{\"over\":\"9000!!!\"}", .{
.extra_headers = &.{
.{ .name = "Content-Type", .value = "application/json" },
},

View File

@@ -90,17 +90,14 @@ pub const Notification = struct {
pub const PageRemove = struct {};
pub const PageNavigate = struct {
req_id: usize,
timestamp: u32,
url: []const u8,
opts: page.NavigateOpts,
};
pub const PageNavigated = struct {
req_id: usize,
timestamp: u32,
url: []const u8,
opts: page.NavigatedOpts,
};
pub const PageNetworkIdle = struct {
@@ -299,7 +296,6 @@ test "Notification" {
// noop
notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 4,
.url = undefined,
.opts = .{},
@@ -309,7 +305,6 @@ test "Notification" {
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 4,
.url = undefined,
.opts = .{},
@@ -318,7 +313,6 @@ test "Notification" {
notifier.unregisterAll(&tc);
notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 10,
.url = undefined,
.opts = .{},
@@ -328,23 +322,21 @@ test "Notification" {
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 10,
.url = undefined,
.opts = .{},
});
notifier.dispatch(.page_navigated, &.{ .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(6, tc.page_navigated);
notifier.unregisterAll(&tc);
notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 100,
.url = undefined,
.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(6, tc.page_navigated);
@@ -352,27 +344,27 @@ test "Notification" {
// unregister
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(1006, tc.page_navigated);
notifier.unregister(.page_navigate, &tc);
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
// already unregistered, try anyways
notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate);
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 {
app: *App,
shutdown: bool = false,
shutdown: bool,
allocator: Allocator,
client: ?posix.socket_t,
listener: ?posix.socket_t,
@@ -53,36 +53,16 @@ pub const Server = struct {
.app = app,
.client = null,
.listener = null,
.shutdown = false,
.allocator = allocator,
.json_version_response = json_version_response,
};
}
/// Interrupts the server so that main can complete normally and call all defer handlers.
pub fn stop(self: *Server) void {
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
return;
}
// Linux and BSD/macOS handle canceling a socket blocked on accept differently.
// For Linux, we use std.shutdown, which will cause accept to return error.SocketNotListening (EINVAL).
// For BSD, shutdown will return an error. Instead we call posix.close, which will result with error.ConnectionAborted (BADF).
if (self.listener) |listener| switch (builtin.target.os.tag) {
.linux => posix.shutdown(listener, .recv) catch |err| {
log.warn(.app, "listener shutdown", .{ .err = err });
},
.macos, .freebsd, .netbsd, .openbsd => {
self.listener = null;
posix.close(listener);
},
else => unreachable,
};
}
pub fn deinit(self: *Server) void {
self.shutdown = true;
if (self.listener) |listener| {
posix.close(listener);
self.listener = null;
}
// *if* server.run is running, we should really wait for it to return
// before existing from here.
@@ -103,19 +83,14 @@ pub const Server = struct {
try posix.listen(listener, 1);
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| {
switch (err) {
error.SocketNotListening, error.ConnectionAborted => {
log.info(.app, "server stopped", .{});
break;
},
else => {
if (self.shutdown) {
return;
}
log.err(.app, "CDP accept", .{ .err = err });
std.Thread.sleep(std.time.ns_per_s);
continue;
},
}
};
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
content.addEventListener('he2', null);
content.dispatchEvent(new Event('he2'));
// Test that EventTarget constructor properly initializes vtable
const et = new EventTarget();
testing.expectEqual('[object EventTarget]', et.toString());
let constructorTestCalled = false;
et.addEventListener('test', () => { constructorTestCalled = true; });
et.dispatchEvent(new Event('test'));
testing.expectEqual(true, constructorTestCalled);
</script>

View File

@@ -12,8 +12,7 @@
});
testing.async(promise1, (json) => {
testing.expectEqual("number", typeof json.updated_at);
testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json);
testing.expectEqual({over: '9000!!!'}, json);
});
</script>
@@ -30,7 +29,6 @@
});
testing.async(promise1, (json) => {
testing.expectEqual("number", typeof json.updated_at);
testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json);
testing.expectEqual({over: '9000!!!'}, json);
});
</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('9000!', t.content.childNodes[1].innerHTML);
</script>
<template id="hello"><p>hello, world</p></template>
<script id=template_parsing>
const tt = document.getElementById('hello');
testing.expectEqual('<p>hello, world</p>', tt.innerHTML);
// > The Node.childNodes property of the <template> element is always empty
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template#usage_notes
testing.expectEqual(0, tt.childNodes.length);
let out = document.createElement('div');
out.appendChild(tt.content.cloneNode(true));
testing.expectEqual('<p>hello, world</p>', out.innerHTML);
</script>

View File

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

1
vendor/mbedtls vendored Submodule

Submodule vendor/mbedtls added at c765c831e5